idb icon indicating copy to clipboard operation
idb copied to clipboard

[Question] How to prevent errors from failing transaction

Open MikeDabrowski opened this issue 3 years ago • 7 comments

Hi, I want to only add items to the db and ignore those that are already present in db. ID is unique, I did

async storeUsers(users: StoredUserPreview[]) {
    const tx = await this.db.transaction('users', 'readwrite');
    return Promise.all(
      users.map((user) => tx.store.add(user)).concat(tx.done as any),
    );
  }

but when at least one key is already in the db whole transaction is rejected.
I found this issue that is build in native indexeddb and seems to be resolved. Is this possible to adapt?

I know that there is an option to first search if the key exists using count. What is the difference of those two methods (count vs catching error) performance-wise ?

MikeDabrowski avatar Feb 22 '22 21:02 MikeDabrowski

Here's how I'd do it with the current library:

function preventTransationCloseOnError(promise) {
  const request = unwrap(promise);
  request.addEventListener('error', (event) => {
    event.preventDefault();
    event.stopPropagation();
  });
  return promise;
}

const tx = db.transaction('users', 'readwrite');
return Promise.all(
  ...users.map((user) => preventTransationCloseOnError(tx.store.add(user))),
  tx.done,
);

However, this seems like a rough edge with the library, so I'll think about a better way to do this in a future version.

jakearchibald avatar Feb 23 '22 08:02 jakearchibald

unwrap is a function exported by the library.

jakearchibald avatar Feb 23 '22 08:02 jakearchibald

Thanks! I will test it later today. Is this approach even sensible? I feel like cheating a bit.

Yesterday I managed to use count to get to the same result. Maybe for reference if anyone else needs it:

async storeUsers(users: StoredUser[]) {
    const usersToAdd = []
    for (const user of users) {
      const isUserNotInDb = await this.db.countFromIndex('users', 'userId', user.id) === 0;
      if(isUserNotInDb) {
        usersToAdd.push(user);
      }
    }
    const tx = await this.db.transaction('users', 'readwrite');
    return Promise.all(
        usersToAdd.map(user => tx.store.add(user)).concat(tx.done as any)
    );
  }

MikeDabrowski avatar Feb 23 '22 08:02 MikeDabrowski

My gut feeling is that the preventTransationCloseOnError approach will be faster.

I think the simplest way to put this into the library would be a ignoreConstraints method, that takes a promise wrapping a Request, and:

  • Prevents defaults on requests where the error is a ConstraintError
  • Returns a promise similar to the input, but resolves with undefined if the request errors with a ConstraintError

In addition, I need to ensure I'm only reacting to events with a matching target.

jakearchibald avatar Feb 23 '22 09:02 jakearchibald

It would be great to have an option to have add not fail if the key already exists or perhaps to replace the data at the key.

RobbieTheWagner avatar Apr 03 '24 02:04 RobbieTheWagner

Isn't that exactly what set does?

jakearchibald avatar Apr 03 '24 05:04 jakearchibald

Isn't that exactly what set does?

I'm not sure what you mean. I used store.put for my use case because I want the values to update each time, but store.add errors if the key already exists. It might be nice to have an option for it to not error.

RobbieTheWagner avatar Apr 03 '24 14:04 RobbieTheWagner