inexorabletash / indexeddb-promises

Proposal for incremental support for Promises in the Indexed DB API
48 stars 2 forks source link

Cancel errors? #6

Open inexorabletash opened 9 years ago

inexorabletash commented 9 years ago

With the event-based API, an error on a request that is not prevented causes the transaction to abort:

var tx = db.transaction('team', 'readwrite');
var store = tx.objectStore('team');
var r1 = store.add({name: 'alice'}, 1);
var r2 = store.add({name: 'bob'}, 1);
r2.onerror = function(e) {
  assert(r2.error.name === 'ConstraintError'); // due duplicate IDs
  e.preventDefault(); // otherwise, transaction will abort
};

Is there a Promise-friendly way to express this?

inexorabletash commented 9 years ago

We need to precisely specify promise resolution & microtask execution vs. event dispatch. Are events dispatched before the request's promise is fulfilled or after?

If after, it's too late by the time a rejection runs, since the transaction is aborted once control returns from the event dispatch if the event's default is not prevented. Options are (1) require use of event handlers here or (2) introduce some property of the request e.g. .doNotAbortOnError (but with a better name). Note that that's still awkward since you need to capture the request:

let r2 = store.add({name: 'bob'}, 1);
r2.ready.catch((reason) => {r2.doNotAbortOnError = true;});
await r2.ready;

If before... same problem as (2) above. What do you operate on to prevent the default?

Horrible idea: introduce a DOMException subclass with a .preventDefault() method, and use this for ConstraintError and other cases here. Then rewire things so the promise rejection handlers get to run before the transaction gets aborted. Problem - the rejection handlers may get propagated through various async hops, and it wouldn't be clear which would get a crack at preventing the default.

inexorabletash commented 8 years ago

Hallway conversations with @littledan - no solutions, still gathering thoughts.

To clarify the problem a bit more, if transactions were not automatically aborted here's how you could write it:

const tx = db.transaction('team', 'readwrite');
const store = tx.objectStore('team');
tx.waitUntil((async () => {
  try {
    await Promise.all([
      store.add({name:'alice'}, 1),
      store.add({name: 'bob'}, 1)
    ]);
  } catch (ex) {
    assert(ex.name ===  'ConstraintError');
  }
})());

It's even worse if you want to do it just on the second request. Regardless, that won't work as-is since the failure of the second request already aborts the transaction, regardless of how it's consumed.

Note that you can catch exceptions on the transaction, and you will normally bind a variable to a transaction even in async code, so this is actually valid/correct:

const tx = db.transaction('team', 'readwrite');
const store = tx.objectStore('team');
tx.onerror = function(ev) {
  assert(tx.error.name === 'ConstraintError'); // due duplicate IDs
  ev.preventDefault(); // otherwise, transaction will abort
};
store.add({name:'alice'}, 1);
store.add({name: 'bob'}, 1);
await tx.complete;

Another idea: (3) add options to every request type that can fail with preventable errors (just put() and add() ???) allowing you to ignore errors. You wouldn't observe the errors, though - is that a blocker?

littledan commented 8 years ago

Another thought to gather of a related construct, but it feels to me like a dead end:

There's a related concept in Promises, the "unhanded Promise rejection callback". Each Promise has a [[PromiseIsHandled]] internal slot, which says whether it's unhandled or not. If only you could abort the transaction on unhandled Promises, it could be resolved. However, then you'd also want to make sure that the resulting Promise chain, if the rejection isn't handled, will abort the transaction. I don't see any great way to do this tracking--it seems really hard to tell whether a subsequent Promise rejection with no handler is related to a particular earlier Promise.

dfahlander commented 8 years ago

Hi, as I've implemented a promise based indexedDB wrapper, Dexie.js, I just want to share that using Promise.catch() for preventing transactions from aborting has worked out very well, in compliance with @littledan 's suggestion.

I believe it would be the most logical way to do it.

In Dexie, any error (thrown or idb error event) will abort transaction unless you catch it. This means that you can catch idb error events in order to prohibit transaction from aborting.

If user for some reason wants to catch an operation for logging/debug purpose, he/she must rethrow the error if still wanting transaction to abort.

[https://github.com/dfahlander/Dexie.js/wiki/Best Practices#6-rethrow-errors-if-transaction-should-be-aborted](https://github.com/dfahlander/Dexie.js/wiki/Best Practices#6-rethrow-errors-if-transaction-should-be-aborted)

Think about how the code will function in async await flow: if you surround the operations with a try/catch, you tell your caller that this was handled. See the idb transaction as the caller.