Documentation & FAQ of observers. See accompanying WebIDL file IDBObservers.webidl
Please file an issue if you have any feedback :)
Table of Contents
db
and not just transaction
in IDBObserver.observe
changes
records map?IndexedDB doesn’t have any observer support. This could normally be implemented by the needed website (or third party) as a wrapper around the database. However, IDB spans browsing contexts (tabs, workers, etc), and implementing a javascript wrapper that supports all of the needed features would be very difficult and performance optimization of the features would be impossible. This project aims to add IndexedDB observers as part of the specification.
Use cases for observers include:
See the EXAMPLES.md doc for more examples.
If we just want to know when an object store has changed. This isn’t the most efficient, but this might be the ‘starting block’ websites use to transition to observers, as at some point they would read the database using a transaction to update their UI.
// We just grab the transaction and give it to our UI refresh logic.
var refreshDataCallback = function(changes) {
refreshDataWithTransaction(changes.transaction);
}
// We disable records, so we just get the callback without any data.
// We ask for the transaction, which guarentees we're reading the current
// state of the database and we won't miss any changes.
var observer = new IDBObserver(refreshDataCallback);
observer.observe(
db, db.transact('users', 'readonly'),
{ noRecords: true, transaction: true, operations: ['add', 'put', 'delete', 'clear'] });
The interface IDBObserver
is added. This object owns the callback of the observer. We then use this object to observe databases (or targets), similar to IntersectionObserver and MutationObserver.
This creates the observer object with the callback. All observations initated with this object will use the given callback.
The function IDBObserver.observe(database, transaction, options)
is added.
This function starts observation on the target database connection using the given transaction. We start observing the object stores that the given transaction is operating on (the object stores returned by IDBTransaction.objectStoreNames
). Observation will start at the end of the given transaction, and the observer’s callback function will be called at the end of every transaction that operates on the chosen object stores until either the database connection is closed or IDBObserver.unobserve
is called with the target database.
The transaction CANNOT be an upgrade transaction.
See Exceptions.
The options
argument - with the operations
populated - is required
options
Argumentoperations
- required.
Lists the operations that the observer wants to see. This cannot be empty. Accepted values are put
, add
, delete
, and clear
.
includeTransaction
- optional
A transaction with a new mode - snapshot
- and a scope of the object stores being observered is always included in the changes argument. This transaction is read-only, and provides a snapshot of the post-commit state. This does not go through the normal transaction queue, but can delay subsequent transactions on the observer’s object stores. The transaction is active during the callback, and becomes inactive at the end of the callback task or microtask. Note: This transaction CANNOT be used for another observe call.
onlyExternal
- optional
Only changes from other database connections will be observed. This can be another connection on the same page, or a connection from a different browsing context (background worker, tab, etc).
includeValues
- optional
Values for all put
and add
will be included for the resptive object stores. However, these values can be large depending on your use of the IndexedDB, so use cautiously.
excludeRecords
- optional
Changes will never contain a records map. This is the most lightweight option having an observer.
ranges
map - optional
Specifies the exact IDBKeyRanges to observe, per object store. Changes outside of these ranges will not trigger an observe callback.
This stops observation of the given target database connection. This will stop all observe
registrations to the given database connection. An exception is thrown if we aren’t observing that connection (see Exceptions)
The observer callback function will be called whenever a transaction is successfully completed on the applicable object store/s. There is one observer callback per applicable transaction.
The observer functionality starts after the the transaction the observer was created in is completed. This allows the creator to read the ‘true’ state of the world before the observer starts. In other words, this allows the developer to control exactly when the observing begins.
The function will continue observing until either the database connection used to create the transaction is closed (and all pending transactions have completed), or stop()
is called on the observer.
changes
ArgumentThe changes
argument includes the following:
changes: {
db: <object>, // The database connection object. If null, then the change
// was from a different database connection.
transaction: <object>, // A 'snapshot' transaction over the object stores that
// this observer is listening to. This is populated when
// 'transaction' is set in the options.
records: record<DOMString, sequence<object>> // The changes per object store, outlined below.
}
(see IDBObservers.webidl)
records
The records value in the changes object is a javascript Map of object store name to the array of change records. This allows us to include changes from multiple object stores in our callback. (Ex: you are observing object stores ‘a’ and ‘b’, and a transaction modifies both of them)
The key
of the map is the object store name, and the value
element of the map is a JS array, with each value containing:
type
: add
, put
, delete
, or clear
key
: The key or IDBKeyRange for the operation (the clear
type does not populate a key)value
: The value inserted into the database by add
or put
. Included if the values
option is specified. Note: this is not included for delete
or clear
operations, because that would require reading from the database instead of just recording our change operations.Example records Map object:
{'objectStore1' => [{type: "add", key: IDBKeyRange.only(1), value: "val1"},
{type: "add", key: IDBKeyRange.only(2), value: "val2"},
{type: "put", key: IDBKeyRange.only(4), value: "val4"},
{type: "delete", key: IDBKeyRange.bound(5, 6, false, false)}],
'objectStore2' => [{type: "add", key: IDBKeyRange.only(1), value: "val1"},
{type: "add", key: IDBKeyRange.only(2), value: "val2"}]}
Note: putAll
and addAll
operations could be seperated into individual put and add changes.
The observer will hold a strong reference to the callback and database connections that the observer is observing hold a reference to the observer. The database releases it’s connections to it’s observers when either unobserve(db)
is called, or the database connection is closed.
In cases like corruption, the database connection is automatically closed, and that will then close all of the observers (see Issue #9).
To give the observer strong consistency of the world that it is observing, we need to allow it to
We accomplish #1 by incorporating a transaction into the creation of the observer. After this transaction completes (and has read anything that the observer needs to know), all subsequent changes to the observing object stores will be sent to the observer.
For #2, we optionally allow the observer to
We require that the transaction given to the observer callback is over ALL object stores observed, even if the change is to a subset. This means we can hit the following scenario:
The spec requires that the observer is called for both the B and C changes separately with the ability to have a readonly transaction for X and Y. When this happens can depend on the implementation.
See the html files for examples, hosted here: https://dmurph.github.io/indexed-db-observers/
Issues section here: https://github.com/WICG/indexed-db-observers/issues
For new features like coalescing, we don’t have a good way for feature detection. Hopefully heycam/webidl#107 will pan out - that looks like it would work for this as well.
See the SPEC_CHANGES.md document for informal notes about the spec changes for this feature.
The changes given to the observer could be coalesced. This eliminated changes that are overwritten in the same transaction or redundant. Here are some examples:
These changes can coalesced to simply be
In addition, deletes are combined when applicable:
This is coalesced to:
Note that these operations are still ordered. They are not a disjoint set.
This could be an boolean option in the IDBObserverDataStoreOptions object, coalescing
.
db
and not just transaction
in IDBObserver.observe
?This was done to maintain consistancy with other web platform observer features, like MutationObserver or IntersectionObserver. The idea is that the first argument to observe
will be tied to the lifetime of the observer object, where the target has a refptr to the observer and keeps it alive.
This spec does not offer a way to observe during onupgrade. Potential clients voiced they wouldn’t need this feature. This doesn’t seem like it’s needed either, as one can just read in any data they need with the transaction used to do the upgrading. Then the observer is guarenteed to begin at the end of that transaction (if one is added), and it wouldn’t miss any chanage.
IndexedDB was designed to allow range delete optimizations so that delete [0,10000]
doesn’t actually have to physically remove those items to return. Instead we can store range delete metadata to shortcut these operations when it makes sense. Since we have many assumptions for this baked our abstraction layer, getting an ‘original’ or ‘old’ value would be nontrivial and incur more overhead.
Following from the answer above, IndexedDB’s API is designed to allow mass deletion optimization, and in order to have the ‘deletes instead of clear’ functionality, this would involve expensive read operations within the database. If an observer needed to know exactly what was deleted, they can maintain their own state of the keys that they care about.
This is an important concept, and is the reason a lot of the API is the way it is.
To achieve an initial world state, one would use the transaction that is used to create the observer to read in all of the initial values that they care about. Then all changes after this transaction is committed are guarenteed to be reported to the observer without duplicates.
Another tool is the includeTransaction
option which can be used to read in an unchanging state of the world during the observer callback. This transaction will take place immediately after the transaction in which the given changes were performed is completed.
changes
records map?Object store objects are only valid when retrieved from transactions. The only relevant information of that object outside of the transaction is the name of the object store. Since the transaction is optional for the observation callback, we aren’t guaranteed to be able to create the IDBObjectStore object for the observer. However, it is easy for the observer to retrieve this object by
transaction
in the options mapThis is done to avoid data duplication of the object store name. This can be changed. Cons of the current approach:
See Issue #49.
The two main reasons are:
All changes (the keys and values) are structured cloneable, and are cloned from IDB. So they are not coming from a different realm.
This makes it so developers cannot reliable observe multiple object stores at the same time. Example:
Given
Order of operations
Even if o1 records the changes from T1 for o2, there is no guarantee that o2 it gets the changes from T1 before another transaction changes o1 again.
We also believe that keeping the ‘one observer call per transaction commit’ keeps observers easy to understand.