Re: [slightlyoff/ServiceWorker] Provide cache.putAll() method (#867)

Transaction-wise, it seems like most of the use cases can be characterized as wanting to synchronize an entire bundle of versioned resources in an all-or-nothing fashion.  Partial progress isn't a problem other than if it's accidentally perceived as completed.  In fact, it seems beneficial for partial progress to be stored so that forward progress is always made, especially if partial progress keeps happening for reasons related to resource exhaustion.  caches.move (or I've seen rename used by Jake in other issues) seems like it establishes a perfect idiom for this without requiring transactions and the potentially user-hostile issues that could crop up.

Imagine a hypothetical game "foogame" where levels are characterized by a versioned JSON manifest consisting of paths, file lengths, and hashes of the files.  The SW receives a request for "level2", and the pseudo-code goes like this:

1. The front-end asks for level2/manifest.json first since manifest.json is the magic filename we use to do the following magic things[1].
1. We invoke caches.open("level2-pending") and caches.open("level2").  There is no "level2-pending"; if there were and we didn't have some weird "level2-in-use" mutex somehow[1 again], we would have done caches.move("level2-pending", "level2") and then used what was level2-pending.  But since it didn't exist, we just use level2 since it's coherently there.
1. A version-check process is spun off (using waitUntil) which pulls the manifest.json out of the cache while also fetching the current level2/manifest.json from the network.
1. caches.open("level2-next") is opened, it exists, so we know we started to sync a newer version of the level in some previous turn of the event loop.
1. We pull the manifest.json out of "level2-next" stashed in it as the first step of the sync process last time.  (Actually, what we did was new a Cache(), diffed the old manifest.json and the new manifest.json, finding any files that existed in both with the same hash, then putAll()[2] the new manifest and all of those already existing files from level2.  This allows us to assume that if there's anything in level2-next that we've already stolen what we can from level2.)
1. We find that the manifest.json in level2-next is the same as the one we just pulled down from the network, so we know that any progress in level2-next is reusable.
1. We run keys() against the contents of level2-next and remove anything already in there from our to-do list.
1. We issue separate add() calls for everything still on the to-do list, accumulating them into a list of Promises that we Promise.all().  This avoids all-or-nothing throwing away of progress.
1. If that promise resolves, we're done, we can move(level2-next, level2-pending) or to level2 if there's no weird mutex thing.  Maybe if we're thorough we first verify that all the entries in the cache have the proper lengths because of Jake's scary research about truncation.

1: re: magical mutex thing or coordination amongst multiple separate fetch events.  I'm still coming up to speed about Service Workers and idioms, so I'm not clear if there's a solution in place already for this case already.  That is, I'm aware that this could theoretically be handled by rev'ing the service-worker and using the installing/installed/activating/etc. state machine, but that assumes everything is sliced up into nice bite-sized pieces.  I'm doubting most developers would be on board with this.

The thing about the proposed transaction model where transactions can stay alive arbitrarily is that it seems like a backdoor mechanism to introduce mutexes/blackboards for this coordination process at the expense of massive complexity for the Cache API.  It seems far better to be able to create an explicit API to support this instead.  For example, a particularly excessive one with weak-ish references to clients so that entries can automatically disappear would be: 
```js
clientKeyedMaps['levels'].set(event.clientId, 2);
const levelUsed = (level) => clientKeyedMaps['levels'].values().some(x => x === level);
```

2: Transaction-wise, I do think it makes sense to enable more control of creating a single list of CacheBatchOperation dictionaries for a single invocation of Batch Cache Operations.  Exposing a wrapper so that multiple deletes and puts could be placed in there doesn't increase complexity.  This avoids developers needing to reason about transactions stacking up and/or the nightmarish potential interactions of fetching/incumbent records.

---
You are receiving this because you are subscribed to this thread.
Reply to this email directly or view it on GitHub:
https://github.com/slightlyoff/ServiceWorker/issues/867#issuecomment-206502562

Received on Wednesday, 6 April 2016 18:23:56 UTC