Summary
Managing many Service Worker event handlers isolated from each other will require middleware to effectively handle that.
Design
A service worker plugin should at least have a index
module that exports a JavaScript object, that object should have event handlers as properties named after the event that it desires to handler (e.g. fetch
, install
).
Event handler functions should receive the event object as the first argument. Event handlers should return a promise or undefined
. Event handlers should not call waitUntil
, the middleware layer should be responsible for calling waitUntil
when a event handler returns a promise. This also goes for fetch
event handlers, they should not call respondWith
themselves, the middleware should call respondWith
when one of the fetch
event handlers' promise resolves to a response.
An example:
const CACHE_NAME = 'my-cache-v1';
const ASSETS = [
'index.html',
'/assets/app.js',
'/assets/vendor.js',
'/assets/app.css',
'/assets/vendor.css'
];
export default {
install() {
return caches
.open(CACHE_NAME)
.then((cache) => cache.addAll(ASSETS));
},
afterInstall() {
self.skipWaiting();
},
fetch(event) {
return caches.match(event.request, { cacheName: CACHE_NAME });
}
}
Middleware behaviour
The middleware layer should register one listener for each event using ServiceWorkerGlobalScope.addEventListener
, when one of those registered listeners is triggered, it should in turn call all other handlers from the modules from the service worker plugins. The behaviour specification per event type is specified below.
Activate, Install
The activate
and install
handler should execute all handlers at once and then wrap all returned promises by the handlers in a Promise.all
call and use that promise to call event.waitUntil
.
Service worker plugins should be able to register a afterActivate
, afterInstall
hook. These hooks should be executed after all the main handlers are resolved.
Example simplified implementation of the install
middleware handler:
self.addEventListener('install', (event) => {
let promises = serviceWorkerPlugins.map((plugin) => {
if (plugin.install) {
return plugin.install(event);
}
});
event.waitUntil(promises);
promises.then(() => {
serviceWorkerPlugins.map((plugin) => {
if (plugin.afterInstall) {
plugin.afterInstall();
}
});
});
});
Fetch
The fetch
handler should execute all handlers one by one. It should execute one handler, wait for the promise to resolve (if returned), then either call event.respondWith
if the handler resolves to a response or execute the next hander.
Example simplified implementation of the fetch
middleware handler:
self.addEventListener('fetch', (event) {
let fetchHandlers = serviceWorkerPlugins.reduce((handlers, plugin) => {
if (plugin.fetch) {
handlers.push(plugin.fetch);
}
return handlers;
}, []);
let resolveHandler = (resolve, reject, index) => {
if (!index) {
index = 0;
}
if (index >= fetchHandlers.length) {
return;
}
fetchHandlers[index](event)
.then((response) => {
if (response) {
return resolve(response);
} else {
return resolveHandler(resolve, reject, index + 1);
}
})
.catch(reject);
};
new Promise(resolveHandler)
.then((response) => {
if (response) {
event.respondWith(response);
}
});
});
All other events
As far as I know all other events are synchronous and thus the handlers can just be executed one by one, without any promise semantics.