/* * LibResilient Service Worker. */ /* * we need a Promise.any() polyfill * so here it is * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise/any * * TODO: remove once Promise.any() is implemented broadly */ if (typeof Promise.any === 'undefined') { Promise.any = async (promises) => { // Promise.all() is the polar opposite of Promise.any() // in that it returns as soon as there is a first rejection // but without it, it returns an array of resolved results return Promise.all( promises.map(p => { return new Promise((resolve, reject) => // swap reject and resolve, so that we can use Promise.all() // and get the result we need Promise.resolve(p).then(reject, resolve) ); }) // now, swap errors and values back ).then( err => Promise.reject(err), val => Promise.resolve(val) ); }; } // initialize the LibResilientPlugins array if (!Array.isArray(self.LibResilientPlugins)) { self.LibResilientPlugins = new Array() } // initialize the LibResilientConfig array // // this also sets some sane defaults, // which then can be modified via config.json if (typeof self.LibResilientConfig !== 'object' || self.LibResilientConfig === null) { self.LibResilientConfig = { // how long do we wait before we decide that a plugin is unresponsive, // and move on? defaultPluginTimeout: 10000, // plugins settings namespace // // this defines which plugins get loaded, // and the order in which they are deployed to try to retrieve content // assumption: plugin path = ./plugins/.js // // this relies on JavaScript preserving the insertion order for properties // https://stackoverflow.com/a/5525820 plugins: [{ name: 'fetch' },{ name: 'cache' } ], // which components should be logged? // this is an array of strings, components not listed here // will have their debug output disabled // // by default, the service worker and all enabled plugins // (so, all components that are used) loggedComponents: [ 'service-worker', 'fetch', 'cache' ] } } /** * internal logging facility * * component - name of the component being logged about * if the component is not in the LibResilientConfig.loggedComponents array, * message will not be displayed * items - the rest of arguments will be passed to console.debug() */ self.log = function(component, ...items) { if ( ('LibResilientConfig' in self) && ('loggedComponents' in self.LibResilientConfig) && (self.LibResilientConfig.loggedComponents != undefined)) { if (self.LibResilientConfig.loggedComponents.indexOf(component) >= 0) { console.debug(`LibResilient [COMMIT_UNKNOWN, ${component}] ::`, ...items) } } } /** * verifying a config data object * * we are *NOT* checking for fields that are unknown/unexpected * as resilience is more important than conrrectness here: * we do want the config to load if at all it can be loaded, * an extra field or two is not a problem here * * cdata - config data to verify */ let verifyConfigData = (cdata) => { // basic check for the plugins field if ( !("plugins" in cdata) || ! Array.isArray(cdata.plugins) ) { self.log('service-worker', 'fetched config does not contain a valid "plugins" field') return false; } // basic check for the loggedComponents if ( !("loggedComponents" in cdata) || !Array.isArray(cdata.loggedComponents) ) { self.log('service-worker', 'fetched config does not contain a valid "loggedComponents" field') return false; } // defaultPluginTimeout is optional if ("defaultPluginTimeout" in cdata) { if (!Number.isInteger(cdata.defaultPluginTimeout)) { self.log('service-worker', 'fetched config contains invalid "defaultPluginTimeout" data (integer expected)') return false; } } // we're good return true; } /** * cache the `config.json` response, wherever from we got it * * configURL - url of the config file * cresponse - response we're caching */ let cacheConfigJSON = async (configURL, cresponse) => { try { var cache = await caches.open('v1') await cache.put(configURL, cresponse) self.log('service-worker', 'config cached.') } catch(e) { self.log('service-worker', `failed to cache config: ${e}`) } } /** * get config JSON and verify it's valid * * cresponse - the Response object to work with */ let getConfigJSON = async (cresponse) => { if (cresponse == undefined) { self.log('service-worker', 'config.json response is undefined') return false; } if (cresponse.status != 200) { self.log('service-worker', `config.json response status is not 200: ${cdata.status} ${cdata.statusText})`) return false; } // cloning the response before applying json() // so that we can cache the response later var cdata = await cresponse.clone().json() if (verifyConfigData(cdata)) { return cdata; } return false; } // flag signifying the SW has been initialized already var initDone = false // load the plugins let initServiceWorker = async () => { // if init has already been done, skip! if (initDone) { self.log('service-worker', 'skipping service-worker init, already done') return false; } // everything in a try-catch block // so that we get an informative message if there's an error try { // we'll need this later var cresponse = null // get the config // // self.registration.scope contains the scope this service worker is registered for // so it makes sense to pull config from `config.json` file directly under that location try { // config.json URL var configURL = self.registration.scope + "config.json" // get the config data from cache cresponse = await caches.match(configURL) var cdata = await getConfigJSON(cresponse) // did it work? if (cdata != false) { // we need to know if the config was already cached self.log('service-worker', `valid config file retrieved from cache.`) // cache failed to deliver } else { // try fetching directly from the main domain self.log('service-worker', `config file not found in cache, fetching.`) var cresponse = await fetch(configURL) cdata = await getConfigJSON(cresponse) // did that work? if (cdata != false) { // cache it, asynchronously cacheConfigJSON(configURL, cresponse) } else { // we ain't got nothing useful -- justs set cdata to an empty object cdata = {} self.log('service-worker', 'ignoring invalid config, using defaults.') } } // merge configs self.LibResilientConfig = {...self.LibResilientConfig, ...cdata} self.log('service-worker', 'config loaded.') } catch (e) { self.log('service-worker', 'config loading failed, using defaults; error:', e) } // create the LibResilientPluginConstructors map // the global... hack is here so that we can run tests; not the most elegant // TODO: find a better way self.LibResilientPluginConstructors = self.LibResilientPluginConstructors || new Map() // copy of the plugins config // we need to work on it so that self.LibResilientConfig.plugins remains unmodified // in case we need it later (for example, when re-loading the config) var pluginsConfig = [...self.LibResilientConfig.plugins] // this is the stash for plugins that need dependencies instantiated first var dependentPlugins = new Array() // only now load the plugins (config.json could have changed the defaults) while (pluginsConfig.length > 0) { // get the first plugin config from the array let pluginConfig = pluginsConfig.shift() self.log('service-worker', `handling plugin type: ${pluginConfig.name}`) // load the relevant plugin script (if not yet loaded) if (!LibResilientPluginConstructors.has(pluginConfig.name)) { self.log('service-worker', `${pluginConfig.name}: loading plugin's source`) self.importScripts(`./plugins/${pluginConfig.name}/index.js`) } // do we have any dependencies we should handle first? if (typeof pluginConfig.uses !== "undefined") { self.log('service-worker', `${pluginConfig.name}: ${pluginConfig.uses.length} dependencies found`) // move the dependency plugin configs to LibResilientConfig to be worked on next for (var i=(pluginConfig.uses.length); i--; i>=0) { self.log('service-worker', `${pluginConfig.name}: dependency found: ${pluginConfig.uses[i].name}`) // put the plugin config in front of the plugin configs array pluginsConfig.unshift(pluginConfig.uses[i]) // set each dependency plugin config to false so that we can keep track // as we fill those gaps later with instantiated dependency plugins pluginConfig.uses[i] = false } // stash the plugin config until we have all the dependencies handled self.log('service-worker', `${pluginConfig.name}: not instantiating until dependencies are ready`) dependentPlugins.push(pluginConfig) // move on to the next plugin config, which at this point will be // the first of dependencies for the plugin whose config got stashed continue; } do { // if the plugin is not enabled, no instantiation for it nor for its dependencies // if the pluginConfig does not have an "enabled" field, it should be assumed to be "true" if ( ( "enabled" in pluginConfig ) && ( pluginConfig.enabled != true ) ) { self.log('service-worker', `skipping ${pluginConfig.name} instantiation: plugin not enabled (dependencies will also not be instantiated)`) pluginConfig = dependentPlugins.pop() if (pluginConfig !== undefined) { let didx = pluginConfig.uses.indexOf(false) pluginConfig.uses.splice(didx, 1) } continue; } // instantiate the plugin let plugin = LibResilientPluginConstructors.get(pluginConfig.name)(self, pluginConfig) self.log('service-worker', `${pluginConfig.name}: instantiated`) // do we have a stashed plugin that requires dependencies? if (dependentPlugins.length === 0) { // no we don't; so, this plugin goes directly to the plugin list self.LibResilientPlugins.push(plugin) // we're done here self.log('service-worker', `${pluginConfig.name}: no dependent plugins, pushing directly to LibResilientPlugins`) break; } // at this point clearly there is at least one element in dependentPlugins // so we can safely assume that the freshly instantiated plugin is a dependency // // in that case let's find the first empty spot for a dependency let didx = dependentPlugins[dependentPlugins.length - 1].uses.indexOf(false) // assign the freshly instantiated plugin as that dependency dependentPlugins[dependentPlugins.length - 1].uses[didx] = plugin self.log('service-worker', `${pluginConfig.name}: assigning as dependency (#${didx}) to ${dependentPlugins[dependentPlugins.length - 1].name}`) // was this the last one? if (didx >= dependentPlugins[dependentPlugins.length - 1].uses.length - 1) { // yup, last one! self.log('service-worker', `${pluginConfig.name}: this was the last dependency of ${dependentPlugins[dependentPlugins.length - 1].name}`) // we can now proceed to instantiate the last element of dependentPlugins pluginConfig = dependentPlugins.pop() continue } // it is not the last one, so there should be more dependency plugins to instantiate first // before we can instantiate the last of element of dependentPlugins // but that requires the full treatment, including checing the `uses` field for their configs self.log('service-worker', `${pluginConfig.name}: not yet the last dependency of ${dependentPlugins[dependentPlugins.length - 1].name}`) pluginConfig = false // if pluginConfig is not false, rinse-repeat the plugin instantiation steps // since we are dealing with the last element of dependentPlugins } while ( (pluginConfig !== false) && (pluginConfig !== undefined) ) } // inform self.log('service-worker', `service worker initialized.\nstrategy in use: ${self.LibResilientPlugins.map(p=>p.name).join(', ')}`) initDone = true; // regardless how we got the config file, if it's older than 24h... if ( (new Date()) - Date.parse(cresponse.headers.get('date')) > 86400000) { // try to get it asynchronously through the plugins, and cache it self.log('service-worker', `config.json stale, fetching through plugins`) getResourceThroughLibResilient(configURL, {}, 'libresilient-internal', false, false) .then(async cresponse=>{ // extract JSON and verify it var cdata = await getConfigJSON(cresponse) // did that work? if (cdata != false) { // if we got the new config.json via a method *other* than plain old fetch(), // we will not be able to use importScripts() to load any pugins that have not been loaded already if (cresponse.headers.get('x-libresilient-method') != 'fetch') { // go through the plugins in the new config and check if we already have their constructors // i.e. if the plugin scripts have already been loaded // FIXME: this does not currently dive into dependencies! // FIXME: https://gitlab.com/rysiekpl/libresilient/-/issues/48 for (p in cdata.plugins) { var pname = cdata.plugins[p].name // plugin constructor not available, meaning: we'd have to importScripts() it // but we can't since this was not retrieved via fetch(), so we cannot assume // that the main domain of the website is up and available // // if we cache this newly retrieved config.json, next time the service worker gets restarted // we will end up with an error while trying to run importScripts() for this plugin // which in turn would lead to the service worker being unregistered // // if the main domain is not available, this would mean the website stops working // even though we *were* able to retrieve the new config.json via plugins! // so, ignoring this new config.json. if (!LibResilientPluginConstructors.has(pname)) { self.log('service-worker', `config.json was retrieved through plugins other than fetch, but specifies additional plugins (${pname}); ignoring.`) return false; } } } self.log('service-worker', `config.json successfully retrieved through plugins; caching.`) // cache it, asynchronously cacheConfigJSON(configURL, cresponse) } }) } } catch(e) { // we only get a cryptic "Error while registering a service worker" // unless we explicitly print the errors out in the console console.error(e) // we got an error while initializing the plugins // better play it safe! self.registration.unregister() throw e } return true; } /** * fetch counter per clientId * * we need to keep track of active fetches per clientId * so that we can inform a given clientId when we're completely done */ self.activeFetches = new Map(); /** * decrement fetches counter * and inform the correct clientId if all is finished done */ let decrementActiveFetches = (clientId) => { // decrement the fetch counter for the client self.activeFetches.set(clientId, self.activeFetches.get(clientId)-1) self.log('service-worker', '+-- activeFetches[' + clientId + ']:', self.activeFetches.get(clientId)) if (self.activeFetches.get(clientId) === 0) { self.log('service-worker', 'All fetches done!') // inform the client // client has to be smart enough to know if that is just temporary // (and new fetches will fire in a moment, because a CSS file just // got fetched) or not self.clients.get(clientId).then((client)=>{ if (client !== null) { try { client.postMessage({ allFetched: true }) } catch(err) { self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`) } } }) .then(()=>{ self.log('service-worker', 'all-fetched message sent.') }) } } /* * returns a Promise that either resolves or rejects after a set timeout * optionally with a specific error message * * time - the timeout (in ms) * timeout_resolves - whether the Promise should resolve() or reject() when hitting the timeout (default: false (reject)) * error_message - optional error message to use when rejecting (default: false (no error message)) */ let promiseTimeout = (time, timeout_resolves=false, error_message=false) => { return new Promise((resolve, reject)=>{ setTimeout(()=>{ if (timeout_resolves) { resolve(time); } else { if (error_message) { reject(new Error(error_message)) } else { reject(time) } } },time); }); }; /* ========================================================================= *\ |* === LibResilientResourceInfo === *| \* ========================================================================= */ /** * LibResilient resource info class * * keeps the values as long as the service worker is running, * and communicates all changes to relevant clients * * clients are responsible for saving and keeping the values across * service worker restarts, if that's required */ let LibResilientResourceInfo = class { /** * constructor * needed to set the URL and clientId */ constructor(url, clientId) { // actual values of the fields // only used internally, and stored into the Indexed DB this.values = { url: '', // read only after initialization clientId: null, fetchError: null, method: null, state: null, // can be "error", "success", "running" serviceWorker: 'COMMIT_UNKNOWN' // this will be replaced by commit sha in CI/CD; read-only } this.client = null; // set it this.values.url = url this.values.clientId = clientId // we might not have a non-empty clientId if it's a cross-origin fetch if (clientId) { // get the client from Client API based on clientId self.clients.get(clientId).then((client)=>{ // set the client this.client = client // Send a message to the client if (this.client !== null) { try { this.client.postMessage(this.values); } catch(err) { self.log("service-worker", `postMessage failed for client: ${this.client}\n- Error message: ${err}`) } } }) } } /** * update this.values and immediately postMessage() to the relevant client * * data - an object with items to set in this.values */ update(data) { // debug var msg = 'Updated LibResilientResourceInfo for: ' + this.values.url // was there a change? if not, no need to postMessage var changed = false // update the properties that are read-write Object .keys(data) .filter((k)=>{ return ['fetchError', 'method', 'state'].includes(k) }) .forEach((k)=>{ msg += '\n+-- ' + k + ': ' + data[k] if (this.values[k] !== data[k]) { msg += ' (changed!)' changed = true } this.values[k] = data[k] }) self.log('service-worker', msg) // send the message to the client if (this.client && changed && (this.client !== null)) { try { this.client.postMessage(this.values); } catch(err) { self.log("service-worker", `postMessage failed for client: ${this.client}\n- Error message: ${err}`) } } } /** * fetchError property */ get fetchError() { return this.values.fetchError } /** * method property */ get method() { return this.values.method } /** * state property */ get state() { return this.values.state } /** * serviceWorker property (read-only) */ get serviceWorker() { return this.values.serviceWorker } /** * url property (read-only) */ get url() { return this.values.url } /** * clientId property (read-only) */ get clientId() { return this.values.clientId } } /* ========================================================================= *\ |* === Main Brain of LibResilient === *| \* ========================================================================= */ /** * generate Request()-compatible init object from an existing Request * * req - the request to work off of */ let initFromRequest = (req) => { return { method: req.method, // TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23 //headers: req.headers, TODO: commented out: https://stackoverflow.com/questions/32500073/request-header-field-access-control-allow-headers-is-not-allowed-by-itself-in-pr //mode: req.mode, TODO: commented out because mode: navigate is haram in service worker, it seems //credentials: req.credentials, TODO: commented out because credentials: "include" is haram if the Access-Control-Allow-Origin header is '*' cache: req.cache, redirect: req.redirect, referrer: req.referrer, integrity: req.integrity } } /** * run a plugin's fetch() method * while handling all the auxiliary stuff like saving info in reqInfo * * plugin - the plugin to use * url - string containing the URL to fetch * init - Request() initialization parameters * reqInfo - instance of LibResilientResourceInfo */ let libresilientFetch = (plugin, url, init, reqInfo) => { // status of the plugin reqInfo.update({ method: (plugin && "name" in plugin) ? plugin.name : "unknown", state: "running" }) // log stuff self.log('service-worker', "LibResilient Service Worker handling URL:", url, '\n+-- init:', Object.getOwnPropertyNames(init).map(p=>`\n - ${p}: ${init[p]}`).join(''), '\n+-- using method(s):', plugin.name ) // race the plugin(s) vs. a timeout return Promise.race([ plugin.fetch(url, init), promiseTimeout( self.LibResilientConfig.defaultPluginTimeout, false, `LibResilient request using ${plugin.name} timed out after ${self.LibResilientConfig.defaultPluginTimeout}ms.` ) ]) } /** * calling a libresilient plugin function on the first plugin that implements it * * call - method name to call * args - arguments that will be passed to it */ let callOnLibResilientPlugin = (call, args) => { // find the first plugin implementing the method for (i=0; i { // set-up reqInfo for the fetch event var reqInfo = new LibResilientResourceInfo(url, clientId) // fetch counter self.activeFetches.set(clientId, self.activeFetches.get(clientId)+1) // filter out stash plugins if need be var LibResilientPluginsRun = self.LibResilientPlugins.filter((plugin)=>{ return ( useStashed || typeof plugin.stash !== 'function') }) /** * this uses Array.reduce() to chain the LibResilientPlugins[]-generated Promises * using the Promise the first registered plugin as the default value * * see: https://css-tricks.com/why-using-reduce-to-sequentially-resolve-promises-works/ * * this also means that LibResilientPlugins[0].fetch() below will run first * (counter-intutively!) * * we are slice()-ing it so that the first plugin is only run once; it is * run in the initialValue parameter below already * * ref: * https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Array/Reduce */ return LibResilientPluginsRun .slice(1) .reduce( (prevPromise, currentPlugin)=>{ return prevPromise.catch((error)=>{ self.log('service-worker', "LibResilient plugin error for:", url, '\n+-- method : ' + reqInfo.method, '\n+-- error : ' + error.toString()) // save info in reqInfo -- status of the previous method reqInfo.update({ state: "error", fetchError: error.toString() }) return libresilientFetch(currentPlugin, url, init, reqInfo) }) }, // this libresilientFetch() will run first // all other promises generated by LibResilientPlugins[] will be chained on it // using the catch() in reduce() above // skipping this very first plugin by way of slice(1) libresilientFetch(LibResilientPluginsRun[0], url, init, reqInfo) ) .then((response)=>{ // we got a successful response decrementActiveFetches(clientId) // record the success reqInfo.update({state:"success"}) // get the plugin that was used to fetch content plugin = self.LibResilientPlugins.find(p=>p.name===reqInfo.method) // if it's a stashing plugin... if (typeof plugin.stash === 'function') { // we obviously do not want to stash self.log('service-worker', 'Not stashing, since resource is already retrieved by a stashing plugin:', url); // since we got the data from a stashing plugin, // let's run the rest of plugins in the background to check if we can get a fresher resource // and stash it in cache for later use self.log('service-worker', 'starting background no-stashed fetch for:', url); // event.waitUntil? // https://stackoverflow.com/questions/37902441/what-does-event-waituntil-do-in-service-worker-and-why-is-it-needed/37906330#37906330 getResourceThroughLibResilient(url, init, clientId, false, true, response.clone()).catch((e)=>{ self.log('service-worker', 'background no-stashed fetch failed for:', url); }) // return the response so that stuff can keep happening return response // otherwise, let's see if we want to stash // and if we already had a stashed version that differs } else { // do we have a stashed version that differs? if (stashedResponse && stashedResponse.headers) { // this is where we check if the response from whatever plugin we got it from // is newer than what we've stashed self.log('service-worker', 'checking freshness of stashed version of:', url, '\n+-- stashed from :', stashedResponse.headers.get('X-LibResilient-Method'), '\n+-- fetched using :', response.headers.get('X-LibResilient-Method'), '\n+-- stashed X-LibResilient-ETag :', stashedResponse.headers.get('X-LibResilient-ETag'), '\n+-- fetched X-LibResilient-ETag :', response.headers.get('X-LibResilient-ETag')) // if the method does not match, or if it matches but the ETag doesn't // we have a different response // which means *probably* fresher content if ( ( stashedResponse.headers.get('X-LibResilient-Method') !== response.headers.get('X-LibResilient-Method') ) || ( stashedResponse.headers.get('X-LibResilient-ETag') !== response.headers.get('X-LibResilient-ETag') ) ) { // inform! self.log('service-worker', 'fetched version method or ETag differs from stashed for:', url) self.clients.get(reqInfo.clientId).then((client)=>{ if (client !== null) { try { client.postMessage({ url: url, fetchedDiffers: true }) } catch(err) { self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`) } } }) // TODO: this should probably modify doStash? } } // do we want to stash? if (doStash) { // find the first stashing plugin for (i=0; i{ hdrs += `\n +-- ${k} : ${v}` }) self.log( 'service-worker', `stashing a successful fetch of: ${url}`, `\n+-- fetched using : ${response.headers.get('X-LibResilient-Method')}`, `\n+-- stashing using : ${self.LibResilientPlugins[i].name}`, hdrs ) // working on clone()'ed response so that the original one is not touched // TODO: should a failed stashing break the flow here? probably not! return self.LibResilientPlugins[i].stash(response.clone(), url) .then((res)=>{ // original response will be needed further down return response }) } } } } // if we're here it means we went through the whole list of plugins // and found not a single stashing plugin // or we don't want to stash the resources in the first place // that's fine, but let's make sure the response goes forth return response }) // a final catch... in case all plugins fail .catch((err)=>{ self.log('service-worker', "LibResilient also failed completely: ", err, '\n+-- URL : ' + url) // cleanup reqInfo.update({ state: "error", fetchError: err.toString() }) // this is very naïve and should in fact be handled // inside the relevant plugin, probably // TODO: is this even needed? reqInfo.update({method: null}) decrementActiveFetches(clientId) // rethrow throw err }) } /* ========================================================================= *\ |* === Setting up the event handlers === *| \* ========================================================================= */ self.addEventListener('install', async (event) => { event.waitUntil(initServiceWorker()) // "COMMIT_UNKNOWN" will be replaced with commit ID self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN)."); }); self.addEventListener('activate', async event => { self.log('service-worker', "1. Activated LibResilient Service Worker (commit: COMMIT_UNKNOWN)."); // TODO: should we do some plugin initialization here? }); self.addEventListener('fetch', async event => { return void event.respondWith(async function () { // initialize the SW; this is necessary as SW can be stopped at any time // and restarted when an event gets triggered -- `fetch` is just such an event. // // `install` and `activate` events only handle the initial installation of the SW // this means that we might end up here without initServiceWorker() ever being run // and so in a situation where plugins have never been actually set-up! // // the good news is that the config.json should have been cached already await initServiceWorker() // if event.resultingClientId is available, we need to use this // otherwise event.clientId is what we want // ref. https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/resultingClientId var clientId = (event.clientId !== null) ? event.clientId : 'unknown-client' if (event.resultingClientId) { clientId = event.resultingClientId // yeah, we seem to have to send the client their clientId // because there is no way to get that client-side // and we need that for sane messaging later // // so let's also send the plugin list, why not // // *sigh* JS is great *sigh* self.clients .get(clientId) .then((client)=>{ if (client !== null) { try { client.postMessage({ clientId: clientId, plugins: self.LibResilientPlugins.map((p)=>{return p.name}), serviceWorker: 'COMMIT_UNKNOWN' }) } catch(err) { self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`) } } }) } // counter! if (typeof self.activeFetches.get(clientId) !== "number") { self.activeFetches.set(clientId, 0) } // info self.log('service-worker', "Fetching!", "\n+-- url :", event.request.url, "\n+-- clientId :", event.clientId, "\n+-- resultingClientId:", event.resultingClientId, "\n +-- activeFetches[" + clientId + "]:", self.activeFetches.get(clientId) ) // External requests go through a regular fetch() if (!event.request.url.startsWith(self.location.origin)) { self.log('service-worker', 'External request; current origin: ' + self.location.origin) return fetch(event.request); } // Non-GET requests go through a regular fetch() if (event.request.method !== 'GET') { return fetch(event.request); } // clean the URL, removing any fragment identifier var url = event.request.url.replace(/#.+$/, ''); // get the init object from Request var init = initFromRequest(event.request) // GET requests to our own domain that are *not* #libresilient-info requests // get handled by plugins in case of an error return getResourceThroughLibResilient(url, init, clientId) }()) }); /** * assumptions to be considered: * every message contains clientId (so that we know where to respond if/when we need to) */ self.addEventListener('message', async (event) => { // initialize the SW; this is necessary as SW can be stopped at any time // and restarted when an event gets triggered -- `message` is just such an event. // // `install` and `activate` events only handle the initial installation of the SW // this means that we might end up here without initServiceWorker() ever being run // and so in a situation where plugins have never been actually set-up! // // the good news is that the config.json should have been cached already await initServiceWorker() // inform var msg = 'Message received!' Object.keys(event.data).forEach((k)=>{ msg += '\n+-- key: ' + k + " :: val: " + event.data[k] }) self.log('service-worker', msg); /* * supporting stash(), unstash(), and publish() only */ if (event.data.stash || event.data.unstash || event.data.publish) { if (event.data.stash) { callOnLibResilientPlugin('stash', event.data.stash) } if (event.data.unstash) { callOnLibResilientPlugin('unstash', event.data.unstash) } if (event.data.publish) { callOnLibResilientPlugin('publish', event.data.publish) } } });