Last active
February 15, 2020 19:43
-
-
Save nyroDev/d13f1f4891161a5493b9d80d78041c35 to your computer and use it in GitHub Desktop.
Service Worker with centralized listeners
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import SWHandler from './sw/SWHandler.mjs'; | |
| import AuthHandler from './sw/Handler/AuthHandler.mjs'; | |
| import DemoHandler from './sw/Handler/DemoHandler.mjs'; | |
| import CacheHandler from './sw/Handler/CacheHandler.mjs'; | |
| const swHandler = new SWHandler(self); | |
| swHandler.addHandler(new AuthHandler(), 'auth'); | |
| swHandler.addHandler(new DemoHandler()); | |
| // CacheHandler should always be at the end | |
| swHandler.addHandler(new CacheHandler()); | |
| swHandler.init(); |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| /* | |
| * Abstract Handler that should be extended to create a new SW handler. | |
| * | |
| * Possibles function to implement are listed below. | |
| * | |
| * init() | |
| * Called in the addHandler process, just after setting the swHandler | |
| * | |
| * install(event) | |
| * @param event The install event | |
| * @return Promise|void | |
| * Called during a install event, promise will be used in the waitUntil in a Promise.all wrapper | |
| * | |
| * activate(event) | |
| * @param event The activate event | |
| * @return Promise|void | |
| * Called during a activate event, promise will be used in the waitUntil in a Promise.all wrapper | |
| * | |
| * fetchRegular(payloadRequest) | |
| * @param payloadRequest PayloadRequest object (see below) | |
| * @return true if a regular fetch should be used | |
| * Called at the beginning of the fetch event, to completly ignore SW fetch handling, | |
| * if at least 1 handler return true from this function. | |
| * | |
| * fetchRequest(payloadRequest) | |
| * @param payloadRequest PayloadRequest object (see below) | |
| * @return Promise that should resolve with the payloadRequest or the payloadRequest | |
| * Called at the beginning of the fetch event, one by one, | |
| * in order to alter the payloadRequest used in the following fetchRequest and/or in the fetch call | |
| * | |
| * fetch(payloadRequest) | |
| * @param payloadRequest object (see below) | |
| * @return Promise|void that will be used in the event.waitUntil call | |
| * Should handle the real fetch, if it's approprioate. | |
| * If it's not, it should return false to be ignored, and the next fetch function will be used. | |
| * The return is a Promise that will be used in the event.waitUntil function | |
| * NB : Only the first function that return a promise will be used | |
| * | |
| * message(data) | |
| * @param data Event data received from the message | |
| * Called on every message received in the message listener | |
| * | |
| * sync(event) | |
| * @param event The sync event | |
| * @return Promise|event that should resolve to the with the sync event or the sync event | |
| * Called on a sync event. | |
| * Each function is called one by one, and the whole Promise chain will be used | |
| * in the event.waitUntil call | |
| * | |
| * clean() | |
| * use to trigger cleaning of indexDb data the handler is storing, (and eventually associated cache files...) | |
| * | |
| * pushNotif(notifData) | |
| * @param notifData Object to use to build options for nativefunction .showNotification(title, options) | |
| * @return Promise|void that will be used in the event.waitUntil call | |
| * NB : Only the first function that return a promise will be used | |
| * | |
| * payloadRequest: | |
| * This object have the following properties: | |
| * - event: The original fetch event object | |
| * - request: The request to use in a final fetch | |
| * - requestUrl: URL object of the request to use in a final fetch | |
| * - isSameOrigin: boolean indicates if the original request is on the same origin than the SW | |
| */ | |
| export default class AbstractHandler { | |
| set swHandler(swHandler) { | |
| this._swHandler = swHandler; | |
| } | |
| get swHandler() { | |
| return this._swHandler; | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AbstractHandler from './AbstractHandler.mjs'; | |
| import StorableRequest from './../utils/StorableRequest.mjs'; // Same as found in workbox https://github.com/GoogleChrome/workbox/blob/master/packages/workbox-background-sync/models/StorableRequest.mjs | |
| const loginCheckUrl = '/login_check'; | |
| const logoutUrl = '/logout'; | |
| export default class AuthHandler extends AbstractHandler { | |
| getAuthHeaders() { | |
| return Promise.resolve({ | |
| 'X-AUTH-USER': '@todo', | |
| 'X-AUTH-TOKEN': 'todo' | |
| }); | |
| } | |
| _authRequest(request) { | |
| return this.getAuthHeaders() | |
| .then((headers) => { | |
| const headersKeys = Object.keys(headers); | |
| if (headersKeys.length === 0) { | |
| return request; | |
| } | |
| return StorableRequest.fromRequest(request.clone()) | |
| .then(storableRequest => { | |
| headersKeys.forEach((k) => { | |
| storableRequest.requestInit.headers[k] = headers[k]; | |
| }); | |
| return storableRequest.toRequest(); | |
| }); | |
| }); | |
| } | |
| fetchRequest(payload) { | |
| if ( | |
| payload.isSameOrigin && ['OPTIONS', 'POST', 'PUT', 'PATCH', 'DELETE'].indexOf(payload.request.method) !== -1 && | |
| payload.requestUrl.pathname !== loginCheckUrl | |
| ) { | |
| // This is an action/logged request, handle and authenticate it | |
| return this._authRequest(payload.request) | |
| .then((request) => { | |
| payload.request = request; | |
| return payload; | |
| }); | |
| } | |
| return Promise.payload; | |
| } | |
| fetch(payload) { | |
| if (!payload.isSameOrigin) { | |
| return; | |
| } | |
| if (payload.requestUrl.pathname === loginCheckUrl) { | |
| // Pass request to server and save infos if success | |
| return this.swHandler.fetch(payload.request.clone()) | |
| .then((response) => { | |
| // add login info into idb | |
| response.clone().json() | |
| .then((loginInfos) => { | |
| //@todo save it in indexedDb | |
| }); | |
| return response; | |
| }); | |
| } else if (payload.requestUrl.pathname === logoutUrl) { | |
| return this._authRequest(payload.request) | |
| .then((request) => { | |
| return this.swHandler.fetch(request) | |
| .then((response) => { | |
| //@todo clean login info into idb | |
| return response; | |
| }) | |
| }); | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AbstractHandler from './AbstractHandler.mjs'; | |
| const cachePrefix = 'cache_'; | |
| const cacheName = 'cache_1234'; // @todo should change on every build | |
| const otherCacheToKeep = 'cache_name_to_keep'; | |
| const assets = [ | |
| '/path/to/cache.css', | |
| '/path/to/cache.js' | |
| ]; | |
| export default class CacheHandler extends AbstractHandler { | |
| install() { | |
| let assetsToCache = assets.map(path => { | |
| return new URL(path, location).toString(); | |
| }); | |
| return caches | |
| .open(cacheName) | |
| .then(cache => { | |
| return cache.addAll(assetsToCache); | |
| }) | |
| .catch(error => { | |
| console.error(error); | |
| throw error; | |
| }); | |
| } | |
| activate() { | |
| const allCaches = [ | |
| cacheName, | |
| otherCacheToKeep | |
| ]; | |
| return caches.keys().then(cacheNames => { | |
| return Promise.all( | |
| cacheNames.filter(cn => { | |
| return cn.startsWith(cachePrefix) && !allCaches.includes(cn); | |
| }).map(function(cn) { | |
| return caches.delete(cn); | |
| }) | |
| ); | |
| }); | |
| } | |
| fetch(payload) { | |
| // Ignore non GET request. | |
| if (payload.request.method !== 'GET') { | |
| return; | |
| } | |
| return caches.match(payload.request).then(response => { | |
| if (response) { | |
| if (payload.request.headers.get('range')) { | |
| return workbox.rangeRequests.createPartialResponse(payload.request, response); | |
| } | |
| return response; | |
| } | |
| return response; | |
| }); | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| import AbstractHandler from './AbstractHandler.mjs'; | |
| const demoUrl = '/demo'; | |
| export default class DemoHandler extends AbstractHandler { | |
| fetch(payload) { | |
| if (!payload.isSameOrigin) { | |
| return; | |
| } | |
| if (payload.requestUrl.pathname === demoUrl) { | |
| // Request sync when a request fail or something... | |
| this.swHandler.requestSync('syncEventDemo'); | |
| return this.swHandler.jsonResponse({ | |
| demo: 'This a demo response from DemoHandler' | |
| }); | |
| } | |
| } | |
| message(data) { | |
| if (data.action === 'messageFromClient') { | |
| //@todo do something | |
| } | |
| } | |
| sync(e) { | |
| if (e.tag === 'syncEventDemo') { | |
| // @todo do something | |
| } | |
| } | |
| } |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| let BreakException = {}; | |
| export default class SWHandler { | |
| constructor() { | |
| this._handlers = []; | |
| this._nameHandlers = {}; | |
| } | |
| init() { | |
| self.addEventListener('install', event => { | |
| this._handleInstall(event); | |
| }); | |
| self.addEventListener('activate', event => { | |
| this._handleActivate(event); | |
| }); | |
| self.addEventListener('fetch', event => { | |
| this._handleFetch(event); | |
| }); | |
| self.addEventListener('message', event => { | |
| this._handleMessage(event); | |
| }); | |
| self.addEventListener('sync', event => { | |
| this._handleSync(event); | |
| }); | |
| self.addEventListener('push', event => { | |
| this._handlePush(event); | |
| }); | |
| self.addEventListener('notificationclick', event => { | |
| this._handleNotificationclick(event); | |
| }); | |
| // Trigger clean | |
| this._clean(); | |
| } | |
| _handleInstall(event) { | |
| let promises = []; | |
| this._handlers.forEach((handler) => { | |
| const handlerPromise = handler.install ? handler.install(event) : false; | |
| if (handlerPromise) { | |
| promises.push(handlerPromise); | |
| } | |
| }); | |
| if (promises.length) { | |
| event.waitUntil(Promise.all(promises)); | |
| } | |
| } | |
| _handleActivate(event) { | |
| let promises = []; | |
| this._handlers.forEach((handler) => { | |
| const handlerPromise = handler.activate ? handler.activate(event) : false; | |
| if (handlerPromise) { | |
| promises.push(handlerPromise); | |
| } | |
| }); | |
| if (promises.length) { | |
| event.waitUntil(Promise.all(promises)); | |
| } | |
| this._clean(); | |
| } | |
| _handleFetch(event) { | |
| const requestUrl = new URL(event.request.url); | |
| let payload = { | |
| event, | |
| request: event.request, | |
| requestUrl: requestUrl, | |
| isSameOrigin: requestUrl.origin === location.origin | |
| }; | |
| // First call fetchRequest callback to enable handler to update request if needed (like auth) | |
| let payloadPromise = Promise.resolve(payload); | |
| let regularFetch = false; | |
| this._handlers.forEach((handler) => { | |
| if (!regularFetch && handler.fetchRegular) { | |
| regularFetch = handler.fetchRegular(payload); | |
| } | |
| if (!regularFetch && handler.fetchRequest) { | |
| payloadPromise = payloadPromise.then(handler.fetchRequest.bind(handler)); | |
| } | |
| }); | |
| if (regularFetch) { | |
| // A handler indicated that we should use regular fetch, return here and do nothing with the event | |
| return; | |
| } | |
| event.respondWith( | |
| payloadPromise.then((payload) => { | |
| // Then try each fetch request, and stop to the first one that returns something | |
| let promise; | |
| try { | |
| this._handlers.forEach((handler) => { | |
| if (handler.fetch) { | |
| promise = handler.fetch(payload); | |
| if (promise) { | |
| throw BreakException; | |
| } | |
| } | |
| }); | |
| } catch (e) { | |
| if (e !== BreakException) { | |
| throw e; | |
| } | |
| } | |
| if (!promise) { | |
| promise = fetch(payload.request.clone()); | |
| } else { | |
| promise = promise.then(response => { | |
| if (response) { | |
| return response; | |
| } | |
| return fetch(payload.request.clone()); | |
| }); | |
| } | |
| return promise; | |
| }) | |
| ); | |
| } | |
| triggerCustomMessage(data) { | |
| return this._handleMessage({ | |
| data: data | |
| }); | |
| } | |
| _handleMessage(event) { | |
| this._handlers.forEach((handler) => { | |
| if (handler.message) { | |
| handler.message(event.data); | |
| } | |
| }); | |
| } | |
| _handleSync(event) { | |
| let promises = []; | |
| this._handlers.forEach((handler) => { | |
| const handlerPromise = handler.sync ? handler.sync(event) : false; | |
| if (handlerPromise) { | |
| promises.push(handlerPromise); | |
| } | |
| }); | |
| if (promises.length) { | |
| event.waitUntil(Promise.all(promises)); | |
| } | |
| } | |
| _handlePush(event) { | |
| if (Notification.permission == 'granted') { | |
| var notif = event.data.json(); | |
| let promise, | |
| handled = false; | |
| this._handlers.forEach((handler) => { | |
| if (handler.pushNotif && !handled) { | |
| promise = handler.pushNotif(notif); | |
| if (promise) { | |
| event.waitUntil(promise); | |
| handled = true; | |
| } | |
| } | |
| }); | |
| } | |
| } | |
| _handleNotificationclick(event) { | |
| } | |
| _clean() { | |
| // call each handler having clean method. | |
| this._handlers.forEach((handler) => { | |
| if (handler.clean) { | |
| if (!this._options.isProd) { | |
| console.log(handler.constructor.name + '->clean()'); | |
| } | |
| handler.clean(); | |
| } | |
| }); | |
| } | |
| addHandler(handler, name) { | |
| handler.swHandler = this; | |
| if (handler.init) { | |
| handler.init(); | |
| } | |
| this._handlers.push(handler); | |
| if (name) { | |
| this._nameHandlers[name] = this._handlers.length - 1; | |
| } | |
| } | |
| getHandler(name) { | |
| if (!(name in this._nameHandlers)) { | |
| throw 'Handler ' + name + ' not found'; | |
| } | |
| return this._handlers[this._nameHandlers[name]]; | |
| } | |
| jsonResponse(data, init) { | |
| if (!init) { | |
| init = {}; | |
| } | |
| init.headers = { | |
| 'Content-Type': 'application/json' | |
| }; | |
| return Promise.resolve(new Response(JSON.stringify(data), init)); | |
| } | |
| postMessage(msg) { | |
| return clients.matchAll() | |
| .then(function(clients) { | |
| clients.map(function(client) { | |
| client.postMessage(msg); | |
| }); | |
| }); | |
| } | |
| } |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment