diff --git a/__tests__/config.json b/__tests__/config.json new file mode 100644 index 0000000..639abb8 --- /dev/null +++ b/__tests__/config.json @@ -0,0 +1,13 @@ +{ + "plugins": [{ + "name": "basic-integrity", + "requireIntegrity": false, + "integrity": { + "http://localhost:8000/__tests__/test.json": "sha256-FCNALvZ0mSxEs0+SjOgx/sDFFVuh0MwkhhYnI0UJWDg=" + }, + "uses": [{ + "name": "fetch" + }] + }], + "loggedComponents": ["service-worker", "fetch", "cache", "basic-integrity"] +} diff --git a/__tests__/service-worker.test.js b/__tests__/service-worker.test.js index 9318b20..7dc4c78 100644 --- a/__tests__/service-worker.test.js +++ b/__tests__/service-worker.test.js @@ -3,7 +3,11 @@ const makeServiceWorkerEnv = require('service-worker-mock'); global.fetch = require('node-fetch'); jest.mock('node-fetch') -global.fetch.mockImplementation((url, init) => { + +describe("service-worker", () => { + beforeEach(() => { + + global.fetch.mockImplementation((url, init) => { return Promise.resolve( new Response( new Blob( @@ -20,9 +24,7 @@ global.fetch.mockImplementation((url, init) => { }) ); }); - -describe("service-worker", () => { - beforeEach(() => { + Object.assign(global, makeServiceWorkerEnv()); global.self = new ServiceWorkerGlobalScope() jest.resetModules(); @@ -81,15 +83,196 @@ describe("service-worker", () => { } }) - test("basic set-up: LibResilientConfig", async () => { + test("basic set-up: use default LibResilientConfig values when config.json missing", async () => { self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({ test: "fail" })], + {type: "application/json"} + ), + { + status: 404, + statusText: "Not Found", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + try { require("../service-worker.js"); } catch(e) {} + await self.trigger('install') + await self.trigger('activate') expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) - expect(typeof self.LibResilientConfig.plugins).toEqual('object') - expect(self.LibResilientConfig.loggedComponents).toBeInstanceOf(Array) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) + }) + + test("basic set-up: use default LibResilientConfig values when config.json not valid JSON", async () => { + self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + ["not a JSON"], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + + try { + require("../service-worker.js"); + } catch(e) {} + await self.trigger('install') + await self.trigger('activate') + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) + }) + + test("basic set-up: use default LibResilientConfig values when no valid 'plugins' field in config.json", async () => { + self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: 'not a valid array'})], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + + try { + require("../service-worker.js"); + } catch(e) {} + await self.trigger('install') + await self.trigger('activate') + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) + }) + + test("basic set-up: use default LibResilientConfig values when no valid 'loggedComponents' field in config.json", async () => { + self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({loggedComponents: 'not a valid array', plugins: [{name: "fetch"}]})], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + + try { + require("../service-worker.js"); + } catch(e) {} + await self.trigger('install') + await self.trigger('activate') + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) + }) + + test("basic set-up: use default LibResilientConfig values when 'defaultPluginTimeout' field in config.json contains an invalid value", async () => { + self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 'not an integer'})], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + + try { + require("../service-worker.js"); + } catch(e) {} + await self.trigger('install') + await self.trigger('activate') + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache']) + }) + + test("basic set-up: use config values from a valid config.json file", async () => { + self.LibResilientConfig = null + + global.fetch.mockImplementation((url, init) => { + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000})], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }) + ); + }); + + try { + require("../service-worker.js"); + } catch(e) {} + await self.trigger('install') + await self.trigger('activate') + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache']) }) test("fetching content should work", async () => { @@ -163,6 +346,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = await self.trigger('fetch', new Request('/test.json')) expect(rejectingFetch).toHaveBeenCalled(); expect(resolvingFetch).toHaveBeenCalled(); @@ -232,6 +418,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = await self.trigger('fetch', new Request('/test.json', initTest)) expect(rejectingFetch).toHaveBeenCalled(); expect(resolvingFetch).toHaveBeenCalled(); @@ -267,6 +456,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = self.trigger('fetch', new Request('/test.json')) jest.advanceTimersByTime(1000); expect.assertions(2) @@ -443,6 +635,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = await self.trigger('fetch', new Request('/test.json')) expect(resolvingFetch).toHaveBeenCalled(); expect(stashingStash).not.toHaveBeenCalled(); @@ -530,6 +725,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = await self.trigger('fetch', { request: new Request('/test.json'), clientId: testClient.id @@ -592,6 +790,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var response = await self.trigger('fetch', new Request('/test.json')) expect(resolvingFetch).toHaveBeenCalledTimes(2); expect(await response.json()).toEqual({ test: "success" }) @@ -726,6 +927,9 @@ describe("service-worker", () => { require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') + var initTest = { method: "GET", // TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23 @@ -832,6 +1036,10 @@ describe("service-worker", () => { } }) require("../service-worker.js"); + + await self.trigger('install') + await self.trigger('activate') + await self.trigger( 'message', { @@ -895,6 +1103,8 @@ describe("service-worker", () => { } }) require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['dependent-test']) expect(self.LibResilientPlugins[0].uses.map(p=>p.name)).toEqual(['dependency1-test', 'dependency2-test']) }) @@ -922,6 +1132,8 @@ describe("service-worker", () => { } }) require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['plugin-test', 'plugin-test', 'plugin-test']) expect(self.LibResilientPlugins.map(p=>p.version)).toEqual(['0.0.1', '0.0.2', '0.0.3']) }) @@ -944,6 +1156,8 @@ describe("service-worker", () => { } }) require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') expect.assertions(1) try { await self.trigger('fetch', new Request('/test.json', {method: "GET"})) @@ -1007,6 +1221,8 @@ describe("service-worker", () => { } }) require("../service-worker.js"); + await self.trigger('install') + await self.trigger('activate') await self.trigger('fetch', new Request('/test.json')) }) diff --git a/__tests__/test.json b/__tests__/test.json new file mode 100644 index 0000000..1d2b32c --- /dev/null +++ b/__tests__/test.json @@ -0,0 +1,3 @@ +{ + "test": true +} diff --git a/config.js.example b/config.js.example deleted file mode 100644 index f86daa6..0000000 --- a/config.js.example +++ /dev/null @@ -1,40 +0,0 @@ -/* - * LibResilient config - * - * This is an example config for LibResilient. When deploying LibResilient on your website - * you will need to create your own config, using this one as a template. - * - */ - -// plugins config -self.LibResilientConfig.plugins = [{ - name: 'fetch' - },{ - name: 'cache' - },{ - name: 'any-of', - uses: [{ - name: 'alt-fetch', - // configuring the alternate endpoints plugin to use IPNS gateways - // - // NOTICE: we cannot use CIDv0 with gateways that use hash directly in the (sub)domain: - // https://github.com/node-fetch/node-fetch/issues/260 - // we *can* use CIDv1 with such gateways, and that's suggested: - // https://docs.ipfs.io/how-to/address-ipfs-on-web/#path-gateway - // https://cid.ipfs.io/ - endpoints: [ - 'https://.ipns.dweb.link/', // USA - 'https://ipfs.kxv.io/ipns//', // Hong Kong - 'https://jorropo.net/ipns//', // France - 'https://gateway.pinata.cloud/ipns//', // Germany - 'https://.ipns.bluelight.link/' // Singapore - - ] - },{ - name: 'gun-ipfs', - gunPubkey: '' - }] - }] - -// we need to explicitly list components we want to see debug messages from -self.LibResilientConfig.loggedComponents = ['service-worker', 'fetch', 'cache', 'any-of', 'alt-fetch', 'gun-ipfs'] diff --git a/config.json.example b/config.json.example new file mode 100644 index 0000000..1899907 --- /dev/null +++ b/config.json.example @@ -0,0 +1,23 @@ +{ + "plugins": [{ + "name": "fetch" + },{ + "name": "cache" + },{ + "name": "any-of", + "uses": [{ + "name": "alt-fetch", + "endpoints": [ + "https://.ipns.dweb.link/", + "https://ipfs.kxv.io/ipns//", + "https://jorropo.net/ipns//", + "https://gateway.pinata.cloud/ipns//", + "https://.ipns.bluelight.link/" + ] + },{ + "name": "gun-ipfs", + "gunPubkey": "" + }] + }], + "loggedComponents": ["service-worker", "fetch", "cache", "any-of", "alt-fetch", "gun-ipfs"] +} diff --git a/docs/ARCHITECTURE.md b/docs/ARCHITECTURE.md index 57ff299..e6f500b 100644 --- a/docs/ARCHITECTURE.md +++ b/docs/ARCHITECTURE.md @@ -21,7 +21,7 @@ Methods these plugins implement: - **Composing plugins** Plugins that *compose* other plugins, for example by running them simultaneously to retrieve content from whichever succeeds first. -Methods these plugins implement depend on which plugins they compose. Additionally, plugins being composed the `uses` key, providing the configuration for them the same way configuration is provided for plugins in the `plugins` key of `LibResilientConfig`. +Methods these plugins implement depend on which plugins they compose. Additionally, plugins being composed the `uses` key, providing the configuration for them the same way configuration is provided for plugins in the `plugins` key of `LibResilientConfig` (which is configurable via `config.json`). Every plugin needs to be implemented as a constructor function that is added to the `LibResilientPluginConstructors` [Map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object for later instantiation. @@ -76,7 +76,7 @@ uses: config.uses ## Fetching a resource via LibResilient -Whenever a resource is being fetched on a LibResilient-enabled site, the `service-worker.js` script dispatches plugins in the set order. This order is configured via the `plugins` key of the `LibResilientConfig` variable, usually set in `config.js` config file. +Whenever a resource is being fetched on a LibResilient-enabled site, the `service-worker.js` script dispatches plugins in the set order. This order is configured via the `plugins` key of the `LibResilientConfig` variable, usually set via the `config.json` config file. A minimal default configuration is hard-coded in case no site-specific configuration is provided. This default configuration runs these plugins: @@ -85,17 +85,19 @@ A minimal default configuration is hard-coded in case no site-specific configura A more robust configuration could look like this: -```javascript -self.LibResilientConfig.plugins = [{ - name: 'fetch' - },{ - name: 'cache' - },{ - name: 'alt-fetch', - endpoints: [ - 'https://fallback-endpoint.example.com' - ]} - }] +```json +{ + "plugins": [{ + "name": "fetch" + },{ + "name": "cache" + },{ + "name": "alt-fetch", + "endpoints": [ + "https://fallback-endpoint.example.com" + ]} + }] +} ``` For each resource, such a config would: diff --git a/docs/CONTENT_INTEGRITY.md b/docs/CONTENT_INTEGRITY.md index 55e928b..b7fa64c 100644 --- a/docs/CONTENT_INTEGRITY.md +++ b/docs/CONTENT_INTEGRITY.md @@ -42,30 +42,29 @@ If using `alt-fetch` as the transport pluging, we can rely on the Fetch API impl The minimal config could in such a case look something like this: -```javascript -self.LibResilientConfig.plugins = [{ - name: 'basic-integrity', - // integrity data for certain resources - integrity: { - '/some/image.png': 'sha256-', - '/index.html': 'sha384-', - '/css/style.css': 'sha512-', - '/documents/example.pdf': 'sha384- sha256-' - }, - // wrapped transport plugin, in this case alt-fetch - uses: [{ - name: 'alt-fetch', - // configuring the alternate endpoints plugin to use IPNS gateways - endpoints: [ - 'https://.ipns.dweb.link/', // USA - 'https://ipfs.kxv.io/ipns//', // Hong Kong - 'https://jorropo.net/ipns//', // France - 'https://gateway.pinata.cloud/ipns//', // Germany - 'https://.ipns.bluelight.link/' // Singapore +```json +{ + "plugins": [{ + "name": "basic-integrity", + "integrity": { + "/some/image.png": "sha256-", + "/index.html": "sha384-", + "/css/style.css": "sha512-", + "/documents/example.pdf": "sha384- sha256-" + }, + "uses": [{ + "name": "alt-fetch", + "endpoints": [ + "https://.ipns.dweb.link/", + "https://ipfs.kxv.io/ipns//", + "https://jorropo.net/ipns//", + "https://gateway.pinata.cloud/ipns//", + "https://.ipns.bluelight.link/" - ] - }] - }] + ] + }] + }] +} ``` ### Scenario 2. `non-fetch`, a hypothetical plugin not based on Fetch API @@ -74,27 +73,24 @@ When *not* using a Fetch API based plugin as the transport pluging, we must expl Example minimal config: -```javascript -self.LibResilientConfig.plugins = [{ - name: 'basic-integrity', - // integrity data for certain resources - integrity: { - '/some/image.png': 'sha256-', - '/index.html': 'sha384-', - '/css/style.css': 'sha512-', - '/documents/example.pdf': 'sha384- sha256-' - }, - // wrapped integrity-check plugin, ensuring integrity of content - // returned by the transport plugin will be verified - uses: [{ - name: 'integrity-check', - uses: [{ - // finally, the wrapped transport plugin, in this case not-fetch - name: 'not-fetch', - // any not-fetch related config here - }] - }] - }] +```json +{ + "plugins": [{ + "name": "basic-integrity", + "integrity": { + "/some/image.png": "sha256-", + "/index.html": "sha384-", + "/css/style.css": "sha512-", + "/documents/example.pdf": "sha384- sha256-" + }, + "uses": [{ + "name": "integrity-check", + "uses": [{ + "name": "not-fetch" + }] + }] + }] +} ``` ## Integrity data for dynamic resources diff --git a/libresilient.js b/libresilient.js index 4784853..98e3a81 100644 --- a/libresilient.js +++ b/libresilient.js @@ -499,7 +499,7 @@ if ('serviceWorker' in navigator) { var serviceWorkerPath = scriptFolder + 'service-worker.js' self.log('browser-side', 'Service Worker script at: ' + serviceWorkerPath) // TODO: is there a way to provide config params for the Service Worker here? - // TODO: it would be good if the config.js script could reside outside of the libresilient directory + // TODO: it would be good if the config.json file could reside outside of the libresilient directory // TODO: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register navigator.serviceWorker.register(serviceWorkerPath, { // TODO: what is the scope relative to? is it the HTML file that included it, or this script? diff --git a/plugins/basic-integrity.js b/plugins/basic-integrity.js index b97ae25..b056f10 100644 --- a/plugins/basic-integrity.js +++ b/plugins/basic-integrity.js @@ -5,7 +5,6 @@ /** * this plugin does not implement any push method */ - // no polluting of the global namespace please (function(LRPC){ // this never changes @@ -45,6 +44,7 @@ * getting content using the wrapped plugin, but providing integrity data */ let fetchContent = (url, init={}) => { + LR.log(pluginName, `handling: ${url}`) // integrity data // a string, where we will combine integrity data from init diff --git a/plugins/gun-ipfs.js b/plugins/gun-ipfs.js index 57e2f75..5e75198 100644 --- a/plugins/gun-ipfs.js +++ b/plugins/gun-ipfs.js @@ -34,7 +34,7 @@ if (typeof window === 'undefined') { let defaultConfig = { // Gun nodes to use gunNodes: ['https://gunjs.herokuapp.com/gun'], - // the pubkey of the preconfigured Gun user; always needs to be set in config.js + // the pubkey of the preconfigured Gun user; always needs to be set in config.json gunPubkey: null, // the IPFS gateway we're using for verification when publishing; default is usually ok ipfsGateway: 'https://gateway.ipfs.io' diff --git a/plugins/ipns-ipfs.js b/plugins/ipns-ipfs.js index 8454013..c8ccb9b 100644 --- a/plugins/ipns-ipfs.js +++ b/plugins/ipns-ipfs.js @@ -26,7 +26,7 @@ // sane defaults let defaultConfig = { - // the pubkey of the preconfigured IPNS node; always needs to be set in config.js + // the pubkey of the preconfigured IPNS node; always needs to be set in config.json ipnsPubkey: null, // the IPFS gateway we're using for verification when publishing; default is usually ok ipfsGateway: 'https://gateway.ipfs.io' diff --git a/service-worker.js b/service-worker.js index d1a6251..2cd5c65 100644 --- a/service-worker.js +++ b/service-worker.js @@ -39,7 +39,7 @@ if (!Array.isArray(self.LibResilientPlugins)) { // initialize the LibResilientConfig array // // this also sets some sane defaults, -// which then can be modified via config.js +// 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, @@ -83,129 +83,181 @@ if (typeof self.LibResilientConfig !== 'object' || self.LibResilientConfig === n * items - the rest of arguments will be passed to console.debug() */ self.log = function(component, ...items) { - if (self.LibResilientConfig.loggedComponents.indexOf(component) >= 0) { - console.debug(`LibResilient [COMMIT_UNKNOWN, ${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 JSON + * + * 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 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; +} + + // load the plugins // // everything in a try-catch block // so that we get an informative message if there's an error -try { - - // get the config - // - // self.registration.scope contains the scope this service worker is registered for - // so it makes sense to pull config from `config.js` file directly under that location - // - // TODO: providing config directly from browser-side control script via postMessage? - // TODO: `updateViaCache=imports` allows at least config.js to be updated using the cache plugin? +let initServiceWorker = async () => { try { - self.importScripts(self.registration.scope + "config.js") - self.log('service-worker', 'config loaded.') - } catch (e) { - self.log('service-worker', 'config loading failed, using defaults') - } - - // create the LibResilientPluginConstructors map - // the global... hack is here so that we can run tests; not the most elegant - // TODO: find a better way - var LibResilientPluginConstructors = self.LibResilientPluginConstructors || new Map() - - // this is the stash for plugins that need dependencies instantiated first - var dependentPlugins = new Array() - - // only now load the plugins (config.js could have changed the defaults) - while (self.LibResilientConfig.plugins.length > 0) { - - // get the first plugin config from the array - let pluginConfig = self.LibResilientConfig.plugins.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}.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 - self.LibResilientConfig.plugins.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 { - - // 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) - - } - - // inform - self.log('service-worker', `DEBUG: Strategy in use: ${self.LibResilientPlugins.map(p=>p.name).join(', ')}`) -} 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) - throw e + // 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 + // + // TODO: providing config directly from browser-side control script via postMessage? + // TODO: `updateViaCache=imports` allows at least config.json to be updated using the cache plugin? + try { + //self.importScripts(self.registration.scope + "config.json") + var cdata = await fetch(self.registration.scope + "config.json") + if (cdata.status != 200) { + self.log('service-worker', `failed to fetch config (${cdata.status} ${cdata.statusText}).`) + } else { + cdata = await cdata.json() + if (verifyConfigData(cdata)) { + self.LibResilientConfig.plugins = cdata.plugins + self.LibResilientConfig.loggedComponents = cdata.loggedComponents + if ("defaultPluginTimeout" in cdata) { + self.LibResilientConfig.defaultPluginTimeout = cdata.defaultPluginTimeout + } + self.log('service-worker', 'config loaded.') + } else { + self.log('service-worker', 'ignoring invalid config, using defaults.') + } + } + } 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}.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 { + + // 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) + + } + + // inform + self.log('service-worker', `DEBUG: Strategy in use: ${self.LibResilientPlugins.map(p=>p.name).join(', ')}`) + + } 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) + throw e + } } /** @@ -439,7 +491,6 @@ let initFromRequest = (req) => { * reqInfo - instance of LibResilientResourceInfo */ let libresilientFetch = (plugin, url, init, reqInfo) => { - // status of the plugin reqInfo.update({ method: plugin.name, @@ -487,19 +538,14 @@ let callOnLibResilientPlugin = (call, args) => { * and returns a Promise resolving to a Response in case any of the plugins * was able to get the resource * - * request - string containing the URL we want to fetch + * url - the url we want to fetch + * init - the init data for responses * clientId - string containing the clientId of the requesting client * useStashed - use stashed resources; if false, only pull resources from live sources * doStash - stash resources once fetched successfully; if false, do not stash pulled resources automagically * stashedResponse - TBD */ -let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStash=true, stashedResponse=null) => { - - // clean the URL, removing any fragment identifier - var url = request.url.replace(/#.+$/, ''); - - // get the init object from Request - var init = initFromRequest(request) +let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doStash=true, stashedResponse=null) => { // set-up reqInfo for the fetch event var reqInfo = new LibResilientResourceInfo(url, clientId) @@ -569,8 +615,7 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas 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 - // TODO: perhaps don't use the `request` again? some wrapper? - getResourceThroughLibResilient(request, clientId, false, true, response.clone()).catch((e)=>{ + 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 @@ -671,12 +716,11 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas /* ========================================================================= *\ |* === Setting up the event handlers === *| \* ========================================================================= */ - -self.addEventListener('install', event => { +self.addEventListener('install', async (event) => { + event.waitUntil(initServiceWorker()) // TODO: Might we want to have a local cache? // "COMMIT_UNKNOWN" will be replaced with commit ID self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN)."); - // TODO: should we do some plugin initialization here? }); self.addEventListener('activate', event => { @@ -738,10 +782,16 @@ self.addEventListener('fetch', event => { if (event.request.method !== 'GET') { return void event.respondWith(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 void event.respondWith(getResourceThroughLibResilient(event.request, clientId)) + return void event.respondWith(getResourceThroughLibResilient(url, init, clientId)) });