Search code examples
dexie

Async changes in Dexie updating hook


I am trying to figure out the best approach for using hooks to add some fields to an object when it gets stored or changed.

The basic idea is there are entry objects which must contain a bunch of properties that are based on some complex queries and calculations of other entries. These calculated properties are all stored under a property called derived. It would far prohibitively expensive to calculate entry.derived every time it is needed, or when it is read from the DB. Instead I opted to populate the derived property ahead of time, and hooks seem to be the best place to do this.

This seems to be no problem for the creating hook. However, I also need to re-generate derived if any other property in entry is changed. The updating hook requires I submit additional changes by returning them, which is problematic because the only way I can generate the change is via an async call.

Below is some minimal code that tries to demonstrate the problem. I haven't tried option B yet, but I suspect it won't work either.

const entryDerivedData = function(entry) {
    // query a bunch of data from entries table then do some calculations
    return db.entries.where('exerciseID').equals(entry.exerciseID).toArray()
        .then(e => {
            // do some calculation and return
            return calculateDerivedData(e);
        });
};

// A: Can't do this because `hook` isn't expecting a generator func
db.entries.hook('updating', function*(mods, primKey, entry, transaction) {
    const derived = yield entryDerivedData(entry);
    return derived;
});

// B: Another possibility, but probably won't work either
db.entries.hook('updating', function(mods, primKey, entry, transaction) {
    transaction.scopeFunc = function() {
        return entryDerivedData(entry)
            .then(derived => {
                // Won't this result in another invocation of the updating hook?
                return db.entries.update(entry.id, {derived});
            });
    };
});

Solution

  • Unfortunately, hooks are synchronous and there's no way to do async calls within them. This is something that is going to change but I cannot promise when. Hoping to rewrite the hook framework within the next 6 months or so and allow bulk hooks (more performant) that can be async (keeping backward compatibility for existing hooks).

    Until then, you could utilize the fact that the hook is always called within a transaction (no matter whether the user does an explicit transaction or not), and you can do additional operations onto the current transaction. Just need to make sure you don't end up in an infinite loop as your additional changes may trigger your hook again.

    An example would be this:

    db.entries.hook('creating', (primKey, entry, trans) => {
      entryDerivedData(entry).then(derived => {
            db.entries.update(primKey, { derived }).catch (e => {
               // Failed to update. Abort transaction by rethrow error:
               throw new Error ("Could not make sure derived property was set properly");
            });
        });
    });
    
    db.entries.hook('updating', (mods, primKey, entry, trans) => {
        if ('derived' in mods) return; // We're the one triggering this change. Ignore.
        // First, apply the mods onto entry:
        var entryClone = Dexie.deepClone(entry);
        Object.keys(mods).forEach(keyPath => {
            if (mods[keyPath] === undefined)
                Dexie.delByKeyPath(entryClone, keyPath);
            else
                Dexie.setByKeyPath(entryClone, keyPath, mods[keyPath]);
        });
    
        entryDerivedData(entryClone).then(derived => {
            db.entries.update(primKey, { derived }).catch (e => {
               // Failed to update. Abort transaction by rethrow error:
               throw new Error ("Could not make sure derived property was set properly");
            });
        });
    });