- From: Nikunj Mehta <nikunj@o-micron.com>
- Date: Tue, 6 Jul 2010 22:06:37 +0530
- To: public-webapps <public-webapps@w3.org>
Hi folks, There are several unimplemented proposals on strengthening and expanding IndexedDB. The reason I have not implemented them yet is because I am not convinced they are necessary in toto. Here's my attempt at explaining why. I apologize in advance for not responding to individual proposals due to personal time constraints. I will however respond in detail on individual bug reports, e.g., as I did with 9975. I used the current editor's draft asynchronous API to understand where some of the remaining programming difficulties remain. Based on this attempt, I find several areas to strengthen, the most prominent of which is how we use transactions. Another is to add the concept of a catalog as a special kind of object store. Here are the main areas I propose to address in the editor's spec: 1. It is time to separate the dynamic and static scope transaction creation so that they are asynchronous and synchronous respectively. 2. Provide a catalog object that can be used to atomically add/remove object stores and indexes as well as modify version. 3. Cursors may produce a null key or a null value. I don't see how this is valid signaling for non-preloaded cursors. I think we need to add a new flag on the cursor to find out if the cursor is exhausted. A couple of additional points: 1. I did not see any significant benefits of preloaded cursors in terms of programming ease. 2. *_NO_DUPLICATE simplifies programming as well as aids in good performance. I have shown one example that illustrates this. 3. Since it seems continue is acceptable to implementers, I am also suggesting we use delete instead of remove, for consistency sake. ------- IDL -------- [NoInterfaceObject] interface IDBDatabase { readonly attribute DOMString name; readonly attribute DOMString description; readonly attribute DOMStringList objectStores; /* Open an object store in the specified transaction. The transaction can be dynamic scoped, or the requested object store must be in the static scope. Returns IDBRequest whose success event of IDBTransactionEvent type contains a result with IDBObjectStore and transaction is an IDBTransactionRequest. */ IDBRequest openObjectStore(in DOMString name, in IDBTransaction txn, in optional unsigned short mode /* defaults to READ_WRITE */); /* Open the database catalog in the specified transaction for exclusive access. Returns IDBRequest whose success event of IDBTransactionEvent type contains a result with IDBCatalog and transaction is an IDBTransactionRequest. */ IDBRequest openCatalog(in IDBTransaction txn); /* Create a new static scoped transaction asynchronously. Returns IDBRequest whose success event of IDBSuccessEvent type contains a result with IDBTransactionRequest. */ IDBRequest openTransaction (in optional DOMStringList storeNames /* defaults to mean all object stores */, in optional unsigned short mode /* defaults to READ_WRITE */, in optional IDBTransaction parent /* defaults to null */, in optional unsigned long timeout /* defaults to no timeout*/); /* Create a new dynamic scoped transaction. This returns a transaction handle synchronously. */ IDBTransaction transaction (in optional IDBTransaction parent /* defaults to null */, in optional unsigned long timeout /* defaults to no timeout*/); }; [NoInterfaceObject] interface IDBCatalog { readonly attribute DOMString version; /* Change the version to the specified value as part of the transaction in which the catalog was opened. This version should not have been created prior to this call otherwise raises IDBDatabaseException with DATA_ERR. */ void setVersion ([TreatNullAs=EmptyString] in DOMString version) (raises IDBDatabaseException); /* Create an object store in the specified transaction, when the transaction is committed. Raises IDBDatabaseException with CONSTRAINT_ERR if an object store exists with the given name. */ void createObjectStore (in DOMString name, [TreatNullAs=EmptyString] in optional DOMString keyPath /* defaults to empty string */, in optional boolean autoIncrement /* defaults to true */) (raises IDBDatabaseException); /* Remove an object store in the specified transaction, when the transaction is committed. Raises IDBDatabaseException with NOT_FOUND_ERR if an object store does not exist with the given name. */ IDBRequest removeObjectStore (in DOMString storeName) (raises IDBDatabaseException); /* Create an index on the named object store in the specified transaction, when the transaction is committed. Raises IDBDatabaseException with NOT_FOUND_ERR if an object store does not exist with the given name and with CONSTRAINT_ERR if an index with the given name exists already. */ IDBRequest createIndex (in DOMString objectStore, in DOMString name, in DOMString keyPath, in optional boolean unique /* defaults to false */) (raises IDBDatabaseException); /* Remove an index on the named object store in the specified transaction, when the transaction is committed. Raises IDBDatabaseException with NOT_FOUND_ERR if the named object store or named index does not exist. */ IDBRequest removeIndex (in DOMString objectStore, in DOMString indexName) (raises IDBDatabaseException); }; [NoInterfaceObject] interface IDBObjectStore { const unsigned short READ_WRITE = 0; const unsigned short READ_ONLY = 1; const unsigned short SNAPSHOT_READ = 2; readonly attribute IDBTransaction txn; readonly attribute unsigned short mode; readonly attribute DOMString name; readonly attribute DOMString keyPath; readonly attribute DOMStringList indexNames; /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not opened in READ_WRITE mode. */ IDBRequest put (in any value, in optional any key) (raises IDBDatabaseException); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not opened in READ_WRITE mode. */ IDBRequest add (in any value, in optional any key) (raises IDBDatabaseException); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store was not opened in READ_WRITE mode. */ IDBRequest delete (in any key) (raises IDBDatabaseException); IDBRequest get (in any key); IDBRequest openCursor (in optional IDBKeyRange range /* defaults to all objects */, in optional unsigned short direction /* defaults to NEXT */, in optional boolean preload /* defaults to false */); IDBIndex index (in DOMString name); }; [NoInterfaceObject] interface IDBIndex { readonly attribute DOMString name; readonly attribute DOMString storeName; readonly attribute DOMString keyPath; readonly attribute boolean unique; readonly attribute IDBTransaction txn; IDBCursor openObjectCursor (in optional IDBKeyRange range /* defaults to all objects */, in optional unsigned short direction /* defaults to NEXT */, in optional boolean preload /* defaults to false */); IDBRequest openCursor (in optional IDBKeyRange range /* defaults to all objects */, in optional unsigned short direction /* defaults to NEXT */, in optional boolean preload /* defaults to false */); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this index was not opened in READ_WRITE mode. */ IDBRequest put (in any value, in optional any key) (raises IDBDatabaseException); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this index was not opened in READ_WRITE mode. */ IDBRequest add (in any value, in optional any key) (raises IDBDatabaseException); IDBRequest getObject (in any key); IDBRequest get (in any key); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the object store of this index was not opened in READ_WRITE mode. */ IDBRequest delete (in any key) (raises IDBDatabaseException); }; [NoInterfaceObject] interface IDBTransaction : IDBRequest { attribute boolean static; attribute IDBDatabase db; IDBObjectStore objectStore (in DOMString name, in optional unsigned short mode /* if static scope, defaults to the mode in which the transaction is created and READ_WRITE otherwise */) raises (IDBDatabaseException); void commit (); attribute Function onabort; attribute Function ontimeout; }; [NoInterfaceObject] interface IDBCursor { const unsigned short NEXT = 0; const unsigned short NEXT_NO_DUPLICATE = 1; const unsigned short PREV = 2; const unsigned short PREV_NO_DUPLICATE = 3; readonly attribute unsigned short direction; /* Could be null. */ readonly attribute any key; /* True when all the data in the cursor has been read and false otherwise */ readonly attribute boolean exhausted; /* Could be null only if cursor is on an object store with out-of-line keys and the key was using a null value. Can never be null in an index, unless there are no more values. */ readonly attribute any value; readonly attribute unsigned long long count; readonly attribute IDBTransaction txn; /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the backing object store was not opened in READ_WRITE mode. */ IDBRequest update (in any value); /* Returns false if this is a pre-loaded cursor and there are no more values in the cursor with the optional specified key. Otherwise returns true. */ boolean continue (in optional any key); /* Raises IDBDatabaseException with NOT_ALLOWED_ERR if the backing object store was not opened in READ_WRITE mode. */ IDBRequest delete () (raises IDBDatabaseException); }; [NoInterfaceObject] interface IDBPreloadedCursor: IDBCursor { readonly attribute unsigned long long count; }; ---------------ECMAScript--------- var parts = [{ number: 'p1', name: 'nut', color: 'red', weight: 12.0, city: 'London'}, { number: 'p2', name: 'bolt', color: 'green', weight: 17.0, city: 'Paris'}, { number: 'p3', name: 'screw', color: 'blue', weight: 17.0, city: 'Rome'}, { number: 'p4', name: 'screw', color: 'red', weight: 14.0, city: 'London'}, { number: 'p5', name: 'cam', color: 'blue', weight: 12.0, city: 'Paris'}, { number: 'p6', name: 'cog', color: 'red', weight: 19.0, city: 'London'}]; var suppliers = [{ number: 's1', name: 'Smith', status: '20', city: 'London'}, { number: 's2', name: 'Jones', status: '10', city: 'Paris'}, { number: 's3', name: 'Blake', status: '30', city: 'Paris'}, { number: 's4', name: 'Clark', status: '20', city: 'London'}, { number: 's5', name: 'Adams', status: '30', city: 'Athens'}]; var shipments = [{ part: 'p1', 'supplier: 's1', quantity: 300}, { part: 'p1', 'supplier: 's2', quantity: 300}, { part: 'p2', 'supplier: 's1', quantity: 200}, { part: 'p2', 'supplier: 's2', quantity: 400}, { part: 'p2', 'supplier: 's3', quantity: 200}, { part: 'p2', 'supplier: 's4', quantity: 200}, { part: 'p3', 'supplier: 's1', quantity: 400}, { part: 'p4', 'supplier: 's1', quantity: 200}, { part: 'p4', 'supplier: 's4', quantity: 300}, { part: 'p5', 'supplier: 's1', quantity: 100}, { part: 'p5', 'supplier: 's4', quantity: 400}, { part: 'p6', 'supplier: 's1', quantity: 100}]; var db; var dbRequest = indexedDB.open("parts", 'Part database'); dbRequest.onsuccess = function(event) { db = event.result; var txn = db.transaction(); // do not acquire any locks var catalogRequest = db.openCatalog(txn); // it is automatically READ_WRITE // this request is to obtain an exclusive lock on the catalog catalogRequest.ontimeout = function(event) { throw new Error("Could not get a transaction to set up database"); } catalogRequest.onsuccess = function(event) { var catalog = event.result; if (catalog.version == null) {// This means we have a brand new database catalog.setVersion("1"); catalog.createObjectStore("part", "number", false); catalog.createObjectStore("supplier", "number", false); catalog.createObjectStore("shipment", id); // there is no way to access the object store created here // until txn is committed catalog.createIndex("part", "partName", "name"); catalog.createIndex("part", "partColor", "color"); catalog.createIndex("part", "partCity", "city"); catalog.createIndex("supplier", "supplierStatus", "status"); catalog.createIndex("supplier", "supplierCity", "city"); catalog.createIndex("shipment", "partSupplier", "[part, supplier]"); // if there is some following code that throws, // then I would want to explicitly commit here txn.commit();// now no more data access can be performed using txn // if such access is attempted, exceptions will be thrown txn.onsuccess = function(event) { // by this time, any indices that were created in txn, may be eagerly // populated if their corresponding object stores had any data populate(); } txn.onerror = function(event) { throw new Error("Failed to set up database due to error: " + event.message); } } else { txn.abort(); // because we don't need to do any upgrades // and now the catalog object cannot be accessed either } } }; dbRequest.onerror = function(event) { if (event.code === IDBDatabaseException.UNKNOWN_ERR) throw new Error("Could not open the database"); } function populate() { var txnRequest = db.openTransaction(); //requesting to lock the entire database exclusively txnRequest.onsuccess = function(event) { var txn = txnRequest.transaction; var part = txn.objectStore("part", IDBObjectStore.READ_WRITE); var supplier = txn.objectStore("supplier", IDBObjectStore.READ_WRITE); var shipment = txn.objectStore("shipment", IDBObjectStore.READ_WRITE); var data = [{ store: part, values: parts}, { store: supplier, values: suppliers}, { store: shipment, values: shipments}]; for (var storeIndex = 0; storeIndex < data.length; ++storeIndex) { var task = data[storeIndex] for (var valueIndex = 0; valueIndex < task.values.length; ++valueIndex) { // since we are working in a transaction, this should be synchronous task.store.add(task.values[index]); // Ignoring the possibility that there may be errors while adding } } // this time I won't commit explicitly } } function findSuppliers(part, callback) { var txnRequest = db.openTransaction(["shipment"], IDBObjectStore.SNAPSHOT_READ); txnRequest.onsuccess = function(event) { var shipments = txn.objectStore("shipment"); // implicitly shipments.transaction should have been populated // TODO: How could we create a transaction without acquiring shared lock? var partSuppliers = shipments.index("partSuppliers"); var lookup = KeyRange.only([part]); var cursorRequest = partSuppliers.openObjectCursor(lookup, IDBCursor.NEXT); // we did not preload the results var suppliers = []; cursorRequest.onsuccess = function(event) { var cursor = event.result; //event.transaction === partSuppliers.transaction === shipments.transaction if (cursor.exhausted) { callback(suppliers); } else { suppliers.push(cursor.value.supplier); // when we continue, we will get the next part supplier // the order of this part's suppliers will be the same as used // to order the suppliers in the "supplier" object store cursor.continue(); } } cursorRequest.onerror = function(event) { // e.g., we couldn't find the part if (event.code === IDBDatabaseException.NOT_FOUND) { callback(suppliers); } } } } function allPartNames(callback) { var txnRequest = db.openTransaction(["part"], IDBObjectStore.SNAPSHOT_READ); txnRequest.onsuccess = function(event) { var parts = txn.objectStore("part"); // We don't acquire any locks yet because the individual data access // operations will trigger lock acquisition var partNames = shipments.index("partNames"); var lookup = KeyRange.only(partName); var cursorRequest = partNames.openCursor(lookup, IDBCursor.NEXT_NO_DUPLICATE); var parts = []; cursorRequest.onsuccess = function(event) { var cursor = event.result; //event.transaction === partNames.transaction === parts.transaction if (cursor.exhausted) { callback(parts); } else { parts.push(cursor.value); // when we continue, we will get the next unique part name cursor.continue(); } } } } function processShipment(shipment, callback) { // we need to validate the part exists in this city first and that the supplier is known var txn = db.transaction(); //synchronous because requesting locks as I go along var parts = txn.objectStore("part", IDBObjectStore.READ_ONLY); var partRequest = parts.get(shipment.part); partRequest.onerror = shipmentProcessingError; partRequest.onsuccess = function(event) { // the required part exists and we have now locked at least that key-value // so that it won't disappear when we add the shipment. var suppliers = txn.objectStore("supplier", IDBObjectStore.READ_ONLY); var supplierRequest = suppliers.get(shipment.supplier); supplierRequest.onerror = shipmentProcessingError; supplierRequest.onsuccess = function(event) { // the required supplier exists and we have now locked that key-value // so that it won't disappear when we add the shipment. var shipments = db.objectStore("shipment", IDBObjectStore.READ_WRITE); var shipmentRequest = shipments.add(shipment); supplierRequest.onerror = shipmentProcessingError; shipmentRequest.onsuccess = function(event) { var txnRequest = event.transaction.commit(); // before the callback, commit the stored record var key = event.result; txnRequest.oncommit = function() { callback(key); // which is the key generated during storage } txnRequest.onerror = shipmentProcessingError; } } } } function shipmentProcessingError(event) { if (event.code === IDBDatabaseException.TIMEOUT_ERROR) { throw new Error("Database is busy. Try later."); } else if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) { throw new Error("Invalid part or supplier specified"); } else if (event.code === IDBDatabaseException.SERIAL_ERROR) { throw new Error("Shipment could not be serialized"); } else if (event.code === IDBDatabaseException.DEADLOCK_ERROR) { throw new Error("Got mixed up and giving up. Retrying the transaction may work."); } else { throw new Error("Some error in processing shipment"); } } function partsInCities(start, end, callback) { var txnRequest = db.openTransaction(["part"], IDBObjectStore.SNAPSHOT_READ); txnRequest.onsuccess = function(event) { var parts = db.objectStore("part"); var cities = parts.index("partCity"); cityRange = KeyRange.bound(start, end, true, true); cursorRequest = cities.openCursor(cityRange, IDBCursor.NEXT); cursorRequest.onerror = function(event) { if (event.code === IDBDatabaseException.TIMEOUT_ERROR) { throw new Error("Database is busy. Try later."); } else if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) { throw new Error("No matching city found"); } else { throw new Error("Some error in finding parts"); } } var parts = []; cursorRequest.onsuccess = function(event) { var cursor = event.result; parts.push(cursor.value); if (cursor.exhausted) { callback(parts); } else { cursor.continue(); } } } } function upgradeDB() { // we will add a new index on supplier name var txn = db.transaction(); // this creates a transaction synchronously but does not acquire any locks var catalogRequest = db.openCatalog(txn, IDBObectStore.READ_WRITE); // this request is to obtain an exclusive lock on the catalog catalogRequest.ontimeout = function(event) { throw new Error("Could not get a transaction to upgrade database"); } catalogRequest.onsuccess = function(event) { var catalog = event.result; if (catalog.version === "1") { // This means we have suppliers object store defined catalog.setVersion("2"); catalog.createIndex("supplier", "supplierName", "name", true); // there is no way to access the object store created here // until txn is committed // if there is some following code that throws, // then I would want to explicitly commit here txn.commit();// now no more data access can be performed using txn // if such access is attempted, exceptions will be thrown txn.oncommit = function(event) { // should be able to use the new index now in a new transaction } txn.onabort = function(event) { // there may be a constraint violation in the creation of indices if (e.code === IDBDatabaseException.CONSTRAINT_ERR) { throw new Error("Database contains same name for multiple suppliers.") } else { throw new Error("Error when upgrading database.") } } } else { txn.abort(); // because we don't need to do any upgrades // and now the catalog object cannot be accessed either } } } } function changeCityName(oldName, newName, callback) { var txnRequest = db.transaction(["part", "supplier"]); // Not requesting other object stores txnRequest.ontimeout = function(event) { throw new Error("Could not get a transaction to change city name"); } txnRequest.onsuccess = function(event) { var txn = event.transaction; var parts = db.objectStore("part", IDBObjectStore.READ_WRITE); var suppliers = db.objectStore("supplier", IDBObjectStore.READ_WRITE); // We can run the two in parallel! var cityParts = parts.index("partCity"); var citySuppliers = suppliers.index("supplierCity"); var city = KeyRange.only(oldName); var storesUpdated = 0, recordsUpdated = 0, needingUpdate = 2; var c1 = cityParts.openObjectCursor(city, IDBCursor.NEXT); var c2 = citySuppliers.openObjectCursor(city, IDBCursor.NEXT); var updateCity = function updateCity(cursor) { cursor.value.city = newName; ++recordsUpdated; if (cursor.exhausted) { ++storesUpdated; if (storesUpdatd === needingUpdate) { txn.commit(); callback(recordsUpdated); } } else { cursor.continue(); } } c1.onsuccess = function(event) { updateCity(c1.result); } c2.onsuccess = function(event) { updateCity(c2.result); } c1.onerror = c2.onerror = function(event) { if (event.code === IDBDatabaseException.NOT_FOUND_ERROR) { --needingUpdate; } else if (event.code === IDBDatabaseException.TIMEOUT_ERROR) { throw new Error("Database is busy. Try later."); } else if (event.code === IDBDatabaseException.SERIAL_ERROR) { throw new Error("Record could not be serialized"); } else if (event.code === IDBDatabaseException.DEADLOCK_ERROR) { throw new Error("Got mixed up and giving up"); } else { throw new Error("Some error in processing city name update"); } } } } }
Received on Tuesday, 6 July 2010 16:37:11 UTC