indexeddb

How should an app react when indexedDB is blocked


I was told in another question about detecting block and unblock events that "A blocked open (or delete) is not canceled, just... blocked. Once unblocked the open (or delete) will proceed."

I was wondering how an app should respond to a blocked event then, if it is possible that the path following a successful event will still occur, eventually.

If I want my app to respond quickly, and encounter a blocked event, should I cancel the success path? By path, I am referring to the series of statements and function calls and continuations that are performed in the event of a successful opening of the database.

Previously I assumed that the blocked event prevented the success path from continuing. I have written my app to treat a blocked event as analogous to an error, meaning the operation cannot continue, and should report back with an error, and go do something else or return to an idle state.

The problem for me is that if the success event can eventually continue, then this means I am forking, and both the error path and the success path will evaluate, and will probably result in some unwanted behavior.

Or is my initial understanding correct, and I do not need to worry about canceling the things that happen onsuccess, because if onblocked fires then I can safely infer that onsuccess will not.

It feels really ugly to do something like the following, but this is the only immediate thing that comes to mind as a way to avoid my problem.

var r = indexedDB.open(...);
var wasPreviouslyBlocked = false;
r.onsuccess = function() {
  // Cancel the success if previously blocked
  if(wasPreviouslyBlocked) {
    return;
  }

  // Proceed as normal
  doNextThing();
};

r.onblocked = function() {
  wasPreviouslyBlocked = true;
};

Is there a better way to react to this scenario?


Solution

  • The problem for me is that if the success event can eventually continue, then this means I am forking, and both the error path and the success path will evaluate, and will probably result in some unwanted behavior.

    That is correct.

    Or is my initial understanding correct, and I do not need to worry about canceling the things that happen onsuccess, because if onblocked fires then I can safely infer that onsuccess will not.

    When in doubt, actually try it! All you need are a few tabs and a local server. Add logging to the blocked, success, and upgradeneeded handlers for the request and the versionchange handler for the connection.

    As background, imagine one tab opens v1 of the database:

    var r = indexedDB.open('db', 1);
    r.onupgradeneeded = function(e) {
    var db = r.result;
      // schema v1: has store s1
      db.createObjectStore('s1');
    };
    r.onsuccess = function(e) {
      window.db = r.result;
    };
    

    Now a second tab is opened and pulls down newer code that wants to do an upgrade:

    var r = indexedDB.open('db', 2);
    r.onupgradeneeded = function(e) {
      // schema v1: has store s1
      // schema v2: adds store s1
      var db = r.result;
      if (e.oldVersion < 1) {
        db.createObjectStore('s1');
      }
      db.createObjectStore('s2');
    };
    r.onblocked = function(e) {
      console.log('uh oh...');
    };
    

    There are at least three general approaches you can take in response to a blocked upgrade.

    1. Have "old" connections watch for versionchange events and close promptly to unblock the upgrade.
    2. Have the "new" connection watch for blocked events and notify the user to close the other tabs
    3. Have the "new" connection watch for blocked events and ignore the upgrade.

    Since you're interested in #3, here's how you'd pull it off:

    var r = indexedDB.open('db', 2);
    r.onupgradeneeded = function(e) {
    
      // If we ever saw a blocked event, abort this upgrade.
      if (r.was_blocked) {
        r.transaction.abort();
        return;
      }
    
      var db = r.result;
      if (e.oldVersion < 1) {
        db.createObjectStore('s1');
      }
      db.createObjectStore('s2');
    };
    
    r.onblocked = function(e) {
      // Record that we saw a blocked event so this upgrade
      // can be ignored.
      r.was_blocked = true;
    };
    

    This is pretty close to what you ended up with in your wasPreviouslyBlocked attempt, but there's a critical bug in your code: you don't abort the upgrade, you just don't actually modify the schema. So you'll end up with a database with schema version 2 but without any of the changes from v2. If the database opens again it will already be at v2 so the upgrade won't fire and you'll be missing the schema changes you expected.