diff --git a/IndexedDbGetAllEntries/explainer.md b/IndexedDbGetAllEntries/explainer.md index f3583536..3368a087 100644 --- a/IndexedDbGetAllEntries/explainer.md +++ b/IndexedDbGetAllEntries/explainer.md @@ -1,185 +1,249 @@ -# IndexedDB: getAllEntries() +# IndexedDB: getAllRecords() ## Author: + - [Steve Becker](https://github.com/SteveBeckerMSFT) ## Participate + - https://github.com/w3c/IndexedDB/issues/206 ## Introduction -[`IndexedDB`](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) is a transactional database for client-side storage. Each record in the database contains a key-value pair. [`getAll()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll) enumerates database record values sorted by key in ascending order. [`getAllKeys()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys) enumerates database record primary keys sorted by key in ascending order. - -This explainer proposes a new operation, `getAllEntries()`, which combines [`getAllKeys()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys) with [`getAll()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll) to enumerate both primary keys and values at the same time. For an [`IDBIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex), `getAllEntries()` also provides the record's index key in addition to the primary key and value. Lastly, `getAllEntries()` offers a new option to enumerate records sorted by key in descending order. +[`IndexedDB`](https://developer.mozilla.org/en-US/docs/Web/API/IndexedDB_API) is a transactional database for client-side storage. Each record in the database contains a key-value pair. [`getAll()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll) enumerates database record values sorted by key in ascending order. [`getAllKeys()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys) enumerates database record primary keys sorted by key in ascending order. -## WebIDL - -```js -dictionary IDBGetAllEntriesOptions { - // A key or an `IDBKeyRange` identifying the records to retrieve. - any query = null; - - // The maximum number of results to retrieve. - [EnforceRange] unsigned long count; - - // Determines how to enumerate and sort results. - // Use 'prev' to enumerate and sort results by key in descending order. - IDBCursorDirection direction = 'next'; -}; - -[Exposed=(Window,Worker)] -partial interface IDBObjectStore { - // After the `getAllEntries()` request completes, the `IDBRequest::result` property - // contains an array of entries: - // `[[primaryKey1, value1], [primaryKey2, value2], ... ]` - [NewObject, RaisesException] - IDBRequest getAllEntries(optional IDBGetAllEntriesOptions options = {}); -} - -[Exposed=(Window,Worker)] -partial interface IDBIndex { - // Produces the same type of results as `IDBObjectStore::getAllEntries()` above, - // but each entry also includes the record's index key at array index 2: - // `[[primaryKey1, value1, indexKey1], [primaryKey2, value2, indexKey2], ... ]` - [NewObject, RaisesException] - IDBRequest getAllEntries(optional IDBGetAllEntriesOptions options = {}); -} -``` +This explainer proposes a new operation, `getAllRecords()`, which combines [`getAllKeys()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAllKeys) with [`getAll()`](https://developer.mozilla.org/en-US/docs/Web/API/IDBObjectStore/getAll) to enumerate both primary keys and values at the same time. For an [`IDBIndex`](https://developer.mozilla.org/en-US/docs/Web/API/IDBIndex), `getAllRecords()` also provides the record's index key in addition to the primary key and value. Lastly, `getAllRecords()` offers a new direction option to enumerate records sorted by key in descending order. ## Goals -Decrease the latency of database read operations. By retrieving the primary key, value and index key for database records through a single operation, `getAllEntries()` reduces the number of JavaScript events required to read records. Each JavaScript event runs as a task on the main JavaScript thread. These tasks can introduce overhead when reading records requires a sequence of tasks that go back and forth between the main JavaScript thread and the IndexedDB I/O thread. +Decrease the latency of database read operations. By retrieving the primary key, value and index key for database records through a single operation, `getAllRecords()` reduces the number of JavaScript events required to read records. Each JavaScript event runs as a task on the main JavaScript thread. These tasks can introduce overhead when reading records requires a sequence of tasks that go back and forth between the main JavaScript thread and the IndexedDB I/O thread. -For batched record iteration, for example, retrieving N records at a time, the primary and index keys provided by `getAllEntries()` can eliminate the need for an [`IDBCursor`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor), which further reduces the number of JavaScript events required. To read the next N records, instead of advancing a cursor to determine the range of the next batch, getAllEntries() can use the primary key or the index key retrieved by the results from the previous batch. +For batched record iteration, for example, retrieving *N* records at a time, the primary and index keys provided by `getAllRecords()` can eliminate the need for an [`IDBCursor`](https://developer.mozilla.org/en-US/docs/Web/API/IDBCursor), which further reduces the number of JavaScript events required. To read the next *N* records, instead of advancing a cursor to determine the range of the next batch, getAllRecords() can use the primary key or the index key retrieved by the results from the previous batch. -## Key scenarios +## `IDBObject::getAllRecords()` and `IDBIndex::getAllRecords()` -### Support paginated cursors using batched record iteration +This explainer proposes adding `getAllRecords()` to both [`IDBObjectStore`](https://www.w3.org/TR/IndexedDB/#idbobjectstore) and [`IDBIndex`](https://www.w3.org/TR/IndexedDB/#idbindex). `getAllRecords()` creates a new `IDBRequest` that queries its `IDBObjectStore` or `IDBIndex` owner. The `IDBRequest` completes with an array of `IDBRecord` results. Each `IDBRecord` contains the `key`, `primaryKey` and `value` attributes. For `IDBIndex`, `key` is the record's index key. For `IDBObjectStore`, both `key` and `primaryKey` return the same value. The pre-existing [`IDBCursorWithValue`](https://www.w3.org/TR/IndexedDB/#idbcursorwithvalue) interface contains the same attributes and values for both `IDBObjectStore` and `IDBIndex`. However, unlike `getAllRecords()`, a cursor may only read one record at a time. -Many scenarios read N database records at a time, waiting to read the next batch of records until needed. For example, a UI may display N records, starting with the last record in descending order. As the user scrolls, the UI will display new content by reading the next N records. +## Key scenarios -To support this access pattern, the UI calls `getAllEntries()` with the options `direction: 'prev'` and `count: N` to retrieve N records at a time in descending order. After the initial batch, the UI must specify the upper bound of the next batch using the primary key or index key from the `getAllEntries()` results of the previous batch. +### Read multiple database records through a single request ```js -// Define a helper that creates a basic read transaction using `getAllEntries()`. -// Wraps the transaction in a promise that resolves with the query results or +// Define a helper that creates a basic read transaction using `getAllRecords()`. +// Wraps the transaction in a promise that resolves with the query results or // rejects after an error. Queries `object_store_name` unless `optional_index_name` // is defined. -async function get_all_entries_with_promise( - database, object_store_name, query_options, optional_index_name) { +async function get_all_records_with_promise( + database, + object_store_name, + query_options, + optional_index_name +) { return await new Promise((fulfill, reject) => { // Create a read-only transaction. - const read_transaction = database.transaction(object_store_name, 'readonly'); + const read_transaction = database.transaction( + object_store_name, + "readonly" + ); + + // Get the object store or index to query. const object_store = read_transaction.objectStore(object_store_name); - let query_target = object_store; if (optional_index_name) { query_target = object_store.index(optional_index_name); } - // Start the `getAllEntries()` request. - const request = query_target.getAllEntries(query_options); + // Start the getAllRecords() request. + const request = query_target.getAllRecords(query_options); - // Resolve the promise with the array of entries after success. - request.onsuccess = event => { + // Resolve promise with results after success. + request.onsuccess = (event) => { fulfill(request.result); }; - // Reject promise with an error after failure. - request.onerror = () => { reject(request.error); }; - read_transaction.onerror = () => { reject(read_transaction.error); }; + // Reject promise with error after failure. + request.onerror = () => { + reject(request.error); + }; + read_transaction.onerror = () => { + reject(read_transaction.error); + }; }); } -// Create a simple reverse iterator where each call to `next()` retrieves -// `batch_size` database records in descending order from an `IDBIndex` with -// unique keys. -function reverse_idb_index_iterator( - database, object_store_name, index_name, batch_size) { - // Define iterator state. - let done = false; - - // Begin the iteration unbounded to retrieve the last records in the 'IDBIndex'. - let next_upper_bound = null; - - // Gets the next `batch_size` entries. - this.next = async function () { - if (done) { - return []; - } +// Read the first 5 records from an object store in the database. +const records = await get_all_records_with_promise( + database, + kObjectStoreName, + /*query_options=*/ { count: 5 } +); +console.log( + "The second record in the database contains: " + + `primaryKey: ${records[1].primaryKey}, key: ${records[1].key}, value: ${records[1].value}` +); +``` - let query; - if (next_upper_bound) { - query = IDBKeyRange.upperBound(next_upper_bound, /*is_exclusive=*/true); - } else { - // The very first query retrieves the last `batch_size` records. - } +### Read multiple database records into a Map + +Developers may use the results from `getAllRecords()` to construct a new [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) that contains a key-value pair for each database record returned by the query. + +```js +// This example uses the `get_all_records_with_promise()` helper defined above. +// +// Read the last 9 records from an index. +const records = await get_all_records_with_promise( + database, + kObjectStoreName, + /*query_options=*/ { count: 9, direction: 'prev' }, + kIndexName +); + +// Map the record's index key to the record's value +const map = new Map(records.map(({ key, value }) => [key, value])); + +// Returns the database record value for the index `key` when the record exists +// in `map`. +const value = map.get(key); + +// Use the following to create an iterator for each database record in `map`: +const index_key_iterator = map.keys(); +const value_iterator = map.values(); +const entry_iterator = map.entries(); // Enumerate both index keys and values. +``` + +### Support paginated cursors using batch record iteration - const entries = await get_all_entries_with_promise( - database, object_store_name, - /*options=*/{ query, count: batch_size, direction: 'prev' }, index_name); - - if (entries.length > 0) { - // Store the upper bound for the next iteration. - const last_entry = entries[entries.length-1]; - next_upper_bound = /*index_key=*/last_entry[2]; - } else { +Many scenarios read *N* database records at a time, waiting to read the next batch of records until needed. For example, a UI may display *N* records, starting with the last record in descending order. As the user scrolls, the UI will display new content by reading the next *N* records. + +To support this access pattern, the UI calls `getAllRecords()` with the options `direction: 'prev'` and `count: N` to retrieve *N* records at a time in descending order. After the initial batch, the UI must specify the upper bound of the next batch using the primary key or index key from the `getAllRecords()` results of the previous batch. + +```js +// This example uses the `get_all_records_with_promise()` helper defined above. +// +// Create a batch iterator where each call to `next()` retrieves `batch_size` database +// records in `direction` order from `object_store_name` or `optional_index_name`. +async function* idb_batch_record_iterator( + database, + object_store_name, + direction, + batch_size, + optional_index_name +) { + let is_done = false; + + // Begin the iteration unbounded to retrieve the first or last `batch_size` records. + let query; + + while (!is_done) { + const records = await get_all_records_with_promise( + database, + object_store_name, + /*query_options=*/ { query, count: batch_size, direction }, + optional_index_name + ); + + if (records.length < batch_size) { // We've iterated through all the database records! - done = true; + is_done = true; + return records; } - return entries; - }; -}; -// Get the last 5 records in the `IDBIndex` named `my_index`. -const reverse_iterator = new reverse_idb_index_iterator( - database, 'my_object_store', 'my_index', /*batch_size=*/5); + // Store the lower or upper bound for the next iteration. + const last_record = records[records.length - 1]; + if (direction === "next" || direction === "nextunique") { + query = IDBKeyRange.lowerBound(last_record.key, /*exclusive=*/ true); + } else { // direction === 'prev' || direction === 'prevunique' + query = IDBKeyRange.upperBound(last_record.key, /*exclusive=*/ true); + } + yield records; + } +} +// Create a reverse iterator that reads 5 records from an index at a time. +const reverse_iterator = idb_batch_record_iterator( + database, + "my_object_store", + /*direction=*/ "prev", + /*batch_size=*/ 5, + "my_index" +); + +// Get the last 5 records. let results = await reverse_iterator.next(); +let records = results.value; +console.log( + "The first record contains: " + + `primaryKey: ${records[0].primaryKey}, key: ${records[0].key}, value: ${records[0].value}` +); // Get the next batch of 5 records. -results = await reverse_iterator.next(); -``` +if (!results.done) { + results = await reverse_iterator.next(); +} +``` + +## Considered alternatives + +### `getAllEntries()` -### Read query results into a Map or Object +Similar to `getAllRecords()` but [provides results as an array of entries](https://github.com/w3c/IndexedDB/issues/206#issuecomment-566205600). Each entry is a two or three element array containing the record's key, value and optional index key. For example: -Developers may use the results from `getAllEntries()` to construct a new [`Map`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) or [`Object`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Object) that contains a key-value pair for each database record returned by the query. +`IDBObjectStore` entries provide array values with two elements: `[ [primaryKey1, value1], [primaryKey2, value2], ... ]` + +`IDBIndex` entries provide array values with three elements: `[ [primaryKey1, value1, indexKey1], [primaryKey2, value2, indexKey2], ... ]` + +Developers may directly use the entry results to construct a `Map` or `Object` since the entry results are inspired by ECMAScript's [Map.prototype.entries()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map/entries). However, `getAllEntries()` has unusual ergonomics, requiring indices like `0` and `1` to access the record properties like `key` and `value`. Also, IndexedDB database records do not map cleanly to ECMAScript entries. For `IDBIndex`, the results contain a third element for index key. For an alternate form, `[[ indexKey1, [ primaryKey1, value1]], [ indexKey2, [ primaryKey2, value2]], ... ]`, the index key cannot always serve as the entry's key since the index key may not be unique across all records. + +### Adding direction to `getAll()` and `getAllKeys()` + +This will be pursued separately. Join the discussion at https://github.com/w3c/IndexedDB/issues/130. Providing the direction option on `getAllKeys()` might be useful for reverse iteration scenarios that don't need to load every value enumerated. + +## WebIDL ```js -// These examples use the `get_all_entries_with_promise()` helper defined above. -// -// Example 1: Read the first 5 database records from the `IDBObjectStore` into a `Map`. -const result_map = new Map( - await get_all_entries_with_promise( - database, 'my_object_store', /*query_options=*/{ count: 5 })); - -// Returns the database record value for `key` when the record exists in `result_map`. -let value = result_map.get(key); - -// Use the following to create an iterator for each database record in `result_map`: -const primary_key_iterator = result_map.keys(); -const value_iterator = result_map.values(); -const entry_iterator = result_map.entries(); // Enumerate both primary keys and values. - -// Example 2: Read the database records from range `min_key` to `max_key` into an `Object`. -const result_object = Object.fromEntries( - await get_all_entries_with_promise( - database, 'my_object_store', /*query_options=*/{ query: IDBKeyRange.bound(min_key, max_key) })); - -// Returns the database record value for `key` when the record exists in `result_object`. -value = result_object[key]; - -// Use the following to create an array containing each database record in `result_object`: -const keys = Object.keys(result_object); -const values = Object.values(result_object); -const entries = Object.entries(result_object); // Produces the same array of key/value pairs - // as `IDBObjectStore::getAllEntries()`. +dictionary IDBGetAllRecordsOptions { + // A key or an `IDBKeyRange` identifying the records to retrieve. + any query = null; + + // The maximum number of results to retrieve. + [EnforceRange] unsigned long count; + + // Determines how to enumerate and sort results. + // Use 'prev' to enumerate and sort results by key in descending order. + IDBCursorDirection direction = 'next'; +}; + +interface IDBRecord { + // For `IDBIndex` records, `key` is the index key. For `IDBObjectStore` + // records, `key` is the same as `primaryKey`. + readonly attribute any key; + readonly attribute any primaryKey; + readonly attribute any value; +}; + +[Exposed=(Window,Worker)] +partial interface IDBObjectStore { + // After the `getAllRecords()` request completes, the `IDBRequest::result` property + // contains an array of records: + // `[[primaryKey1, value1], [primaryKey2, value2], ... ]` + [NewObject, RaisesException] + IDBRequest getAllRecords(optional IDBGetAllRecordsOptions options = {}); +} + +[Exposed=(Window,Worker)] +partial interface IDBIndex { + // Produces the same type of results as `IDBObjectStore::getAllRecords()` above, + // but each entry also includes the record's index key at array index 2: + // `[[primaryKey1, value1, indexKey1], [primaryKey2, value2, indexKey2], ... ]` + [NewObject, RaisesException] + IDBRequest getAllRecords(optional IDBGetAllRecordsOptions options = {}); +} ``` ## Stakeholder Feedback / Opposition - Web Developers: Positive - - Developers have reported the limitations addressed by `getAllEntries()`. A few examples: + - Developers have reported the limitations addressed by `getAllRecords()`. A few examples: - ["You cannot build a paginated cursor in descending order."](https://nolanlawson.com/2021/08/22/speeding-up-indexeddb-reads-and-writes/) - ["An example where getAll() could help but needs to retrieve the index key and primary key."](https://stackoverflow.com/questions/44349168/speeding-up-indexeddb-search-with-multiple-workers) - Chromium: Positive @@ -188,9 +252,9 @@ const entries = Object.entries(result_object); // Produces the same array of key ## References & acknowledgements -Special thanks to [Joshua Bell](https://github.com/inexorabletash) who proposed `getAllEntries()` in the [W3C IndexedDB issue](https://github.com/w3c/IndexedDB/issues/206). +Special thanks to [Joshua Bell](https://github.com/inexorabletash) who proposed `getAllRecords()` in the [W3C IndexedDB issue](https://github.com/w3c/IndexedDB/issues/206). Many thanks for valuable feedback and advice from: - [Rahul Singh](https://github.com/rahulsingh-msft) -- [Foromo Daniel Soromou](https://github.com/fosoromo_microsoft) \ No newline at end of file +- [Foromo Daniel Soromou](https://github.com/fosoromo_microsoft) diff --git a/README.md b/README.md index 4882e54c..3c090515 100644 --- a/README.md +++ b/README.md @@ -80,7 +80,7 @@ we move them into the [Alumni section](#alumni-) below. | [Set Default Audio Output Device](SetDefaultSinkId/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/SetDefaultSinkId?label=issues) | [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=kyerebo&labels=SetDefaultSinkId&template=setDefaultSinkId.md&title=%5BSetDefaultSinkId%5D+Issue) | WebRTC | | [Handwriting attribute](Handwriting/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/Handwriting?label=issues) | [New issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=adettenb&labels=Handwriting&template=Handwriting.md&title=%5BHandwriting%5D+Issue) | HTML | | [AudioContext Interrupted State](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/AudioContextInterruptedState/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/AudioContext%20Interrupted%20State?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=gabrielbrito&labels=AudioContext+Interrupted+State&title=%5BAudioContext+Interrupted+State%5D+%3CTITLE+HERE%3E) | WebAudio | -| [IndexedDB getAllEntries()](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/IndexedDbGetAllEntries/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/IndexedDB%20%20GetAllEntries?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=SteveBeckerMSFT&labels=IndexedDB%20%20GetAllEntries&title=%5BIndexedDB+getAllEntries()%5D+%3CTITLE+HERE%3E) | IndexedDB | +| [IndexedDB getAllRecords()](https://github.com/MicrosoftEdge/MSEdgeExplainers/blob/main/IndexedDbGetAllEntries/explainer.md) | ![GitHub issues by-label](https://img.shields.io/github/issues/MicrosoftEdge/MSEdgeExplainers/IndexedDB%20GetAllRecords?label=issues) | [New Issue...](https://github.com/MicrosoftEdge/MSEdgeExplainers/issues/new?assignees=SteveBeckerMSFT&labels=IndexedDB%20GetAllRecords&title=%5BIndexedDB+getAllRecords()%5D+%3CTITLE+HERE%3E) | IndexedDB | # Alumni 🎓