Data operations are not rolled back in promisified IndexedDB transaction

Viewed 73

When I call function saveDamage() and an exception occurs in the Promise.all() at the end of the function. I expected all preceding operations be rolled back but they're not. What am I doing wrong please? Thank you!

saveDamage(damage) {
  return this.transaction('rw', [
    this.DAMAGE_STORE_NAME,
    this.DAMAGE_IMAGE_DATA_STORE_NAME,
    this.DAMAGE_IMAGE_DATA_TEMP_STORE_NAME
  ], (tx) => {
    let deletePromise = (damage.id)
        ? this._deleteDamage(tx, damage.id)
        : Promise.resolve();
    return deletePromise.then(() => {
      // Copy temporary image data of damage into permanent location and update their IDs
      let copyPromises = this._getDamageImageFields(damage).map(field => {
        return this._copyObject(tx, this.DAMAGE_IMAGE_DATA_TEMP_STORE_NAME, this.DAMAGE_IMAGE_DATA_STORE_NAME, field.value.imageDataId)
            .then(id => field.value.imageDataId = id)
      });
      return Promise.all(copyPromises)
          // Save damage
          /* The exception occurs inside _saveObject(). Previous DB operations are not rolled back. */
          .then(() => this._saveObject(tx, this.DAMAGE_STORE_NAME, damage, damage.id)) 
          .then(id => damage.id = id)
          // Delete temporary image data
          .then(() => this._clearStore(tx, this.DAMAGE_IMAGE_DATA_TEMP_STORE_NAME));
    });
  });
}

transaction(mode, storeNames, executorFnc) {
  let tx = this._db.transaction(this._transactionStoreNames(storeNames), this._transactionMode(mode));
  tx.onabort = (event) => reject("Transaction failed: " + event.target.errorCode);
  return executorFnc(tx);
}

_deleteDamage(tx, id) {
  // Fetch damage from DB
  return this._getObject(tx, this.DAMAGE_STORE_NAME, id).then(damage => {
    // Delete all big image data
    let promises = this._getIdsOfDamageImageData(damage)
        .map(imageDataId => this._deleteObject(tx, this.DAMAGE_IMAGE_DATA_STORE_NAME, imageDataId));
    // Delete damage itself
    promises.push(this._deleteObject(tx, this.DAMAGE_STORE_NAME, id));
    return Promise.all(promises);
  });
}

_deleteObject(tx, storeName, id) {
  return new Promise((resolve, reject) => {
    let result = tx.objectStore(storeName).delete(id);
    result.onsuccess = (event) => resolve();
    result.onerror = (event) => reject(event);
  });
}

_copyObject(tx, sourceStoreName, targetStoreName, id) {
  return this._getObject(tx, sourceStoreName, id)
      .then(obj => this._saveObject(tx, targetStoreName, obj));
}

_saveObject(tx, storeName, object, id) {
  return new Promise((resolve, reject) => {
    let store = tx.objectStore(storeName);
    let result = store.put(object, id);
    result.onsuccess = (event) => resolve(event.target.result /* object id */);
    result.onerror = (event) => reject(event);
  });
}

The exception is (I know how to fix it):

Uncaught (in promise) DOMException: Failed to execute 'put' on 'IDBObjectStore': The object store uses in-line keys and the key parameter was provided.

EDIT: After some experimenting I understood that as soon as I exit success handler and there are no more unfinished success handlers then the transaction commits. It looks like operations issued in 'then' function are executed in another transaction because 'then' is executed as a result of returning resolved promise when exitting a success handler. The only way I found to execute dependent operations in a single transaction is using plain success handlers. But using plain success handlers becomes difficult when next-step-operations depends on the result of the current operation. For example when I need to delete object 'A' stored in store 'a' and it's many sub-objects 'B' stored in store 'b'. In such case I need to fetch object 'A' first, then delete sub-objects 'B' one by one, which requires generating handling code dynamically, the delete 'A'. I can't believe it's so difficult to achieve such basic thing so I suspect I'm still missing something.

1 Answers
  1. The error means id is undefined, I think in _saveObject
  2. If your promises represent individual transactions, then all transactions in the Promise.all array that were committed prior to the error will have already committed regardless of the fact that the Promise.all promise rejected. Run all database operations a single transaction to achieve a complete roll back if any one operation fails.
Related