diff --git a/__tests__/service-worker.test.js b/__tests__/service-worker.test.js index 5fddc49..b0b6fdb 100644 --- a/__tests__/service-worker.test.js +++ b/__tests__/service-worker.test.js @@ -386,7 +386,7 @@ describe("service-worker", () => { test("basic set-up: a stale cached valid config.json file gets used, no fetch happens, fresh config.json is retrieved using the configured plugins and cached", async () => { self.LibResilientConfig = null - var configData = {loggedComponents: ['service-worker', 'cache', 'resolve-config'], plugins: [{name: "cache"}, {name: "resolve-config"}], defaultPluginTimeout: 5000} + var configData = {loggedComponents: ['service-worker', 'cache', 'fetch'], plugins: [{name: "fetch"},{name: "cache"}], defaultPluginTimeout: 5000} var configUrl = '/config.json' var configResponse = new Response( new Blob( @@ -410,7 +410,7 @@ describe("service-worker", () => { }) - var newConfigData = {loggedComponents: ['service-worker', 'resolve-config'], plugins: [{name: "resolve-config"}], defaultPluginTimeout: 2000} + var newConfigData = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 2000} let resolveConfigFetch = jest.fn((request, init)=>{ return Promise.resolve( new Response( @@ -430,10 +430,10 @@ describe("service-worker", () => { }) ) }) - global.LibResilientPluginConstructors.set('resolve-config', ()=>{ + global.LibResilientPluginConstructors.set('fetch', ()=>{ return { - name: 'resolve-config', - description: 'Resolve with config data.', + name: 'fetch', + description: 'Resolve with config data (pretending to be fetch).', version: '0.0.1', fetch: resolveConfigFetch } @@ -451,8 +451,8 @@ describe("service-worker", () => { // verify current config (the one from the pre-cached stale `config.json`) expect(typeof self.LibResilientConfig).toEqual('object') expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000) - expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}, {name: "resolve-config"}]) - expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache', 'resolve-config']) + expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}]) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache', 'fetch']) expect(fetch).not.toHaveBeenCalled(); expect(resolveConfigFetch).toHaveBeenCalled(); @@ -555,31 +555,90 @@ describe("service-worker", () => { }) - test("fetching content should work", async () => { - self.LibResilientConfig = { - plugins: [{ - name: 'fetch' - }], - loggedComponents: [ - 'service-worker', 'fetch' - ] - } - require("../service-worker.js"); + test("basic set-up: a stale cached valid config.json file gets used, no fetch happens; valid config.json (configuring additional plugins) is retrieved using the configured plugins other than fetch, and is not cached", async () => { + self.LibResilientConfig = null + var configData = {loggedComponents: ['service-worker', 'resolve-config'], plugins: [{name: "resolve-config"}], defaultPluginTimeout: 5000} + var configUrl = '/config.json' + var configResponse = new Response( + new Blob( + [JSON.stringify(configData)], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader', + // very stale date + 'Date': new Date(0).toUTCString() + }, + url: configUrl + }) + await caches + .open('v1') + .then((cache)=>{ + return cache.put(configUrl, configResponse) + }) + + + var newConfigData = {loggedComponents: ['service-worker', 'resolve-config', 'cache'], plugins: [{name: "resolve-config"}, {name: "cache"}], defaultPluginTimeout: 2000} + let resolveConfigFetch = jest.fn((request, init)=>{ + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify(newConfigData)], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader', + // very current date + 'Date': new Date().toUTCString() + }, + url: configUrl + }) + ) + }) + global.LibResilientPluginConstructors.set('resolve-config', ()=>{ + return { + name: 'resolve-config', + description: 'Resolve with config data.', + version: '0.0.1', + fetch: resolveConfigFetch + } + }) + + try { + require("../service-worker.js"); + } catch(e) {} await self.trigger('install') // this is silly but works, and is necessary because // event.waitUntil() in the install event handler is not handled correctly in NodeJS await new Promise(resolve => setTimeout(resolve, 100)); await self.trigger('activate') - var response = await self.trigger('fetch', new Request('/test.json')) - expect(fetch).toHaveBeenCalled(); - expect(await response.json()).toEqual({ test: "success" }) - expect(response.headers.has('X-LibResilient-Method')).toEqual(true) - expect(response.headers.get('X-LibResilient-Method')).toEqual('fetch') - expect(response.headers.has('X-LibResilient-Etag')).toEqual(true) - expect(response.headers.get('X-LibResilient-ETag')).toEqual('TestingETagHeader') - }); + // verify current config (the one from the pre-cached stale `config.json`) + expect(typeof self.LibResilientConfig).toEqual('object') + expect(self.LibResilientConfig.defaultPluginTimeout).toBe(configData.defaultPluginTimeout) + expect(self.LibResilientConfig.plugins).toStrictEqual(configData.plugins) + expect(self.LibResilientConfig.loggedComponents).toStrictEqual(configData.loggedComponents) + expect(fetch).not.toHaveBeenCalled(); + expect(resolveConfigFetch).toHaveBeenCalled(); + + // verify that the *new* config got cached + cdata = await caches + .open('v1') + .then((cache)=>{ + return cache.match(configUrl) + }) + .then((cresponse)=>{ + return cresponse.json() + }) + expect(cdata).toStrictEqual(configData) + }) test("failed fetch by first configured plugin should not affect a successful fetch by a second one", async () => { self.LibResilientConfig = { diff --git a/service-worker.js b/service-worker.js index 9869ae4..08d2d45 100644 --- a/service-worker.js +++ b/service-worker.js @@ -340,7 +340,35 @@ let initServiceWorker = async () => { // did that work? if (cdata != false) { - self.log('service-worker', `config.json successfully fetched through plugins; caching.`) + + // 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 + 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) } @@ -351,6 +379,9 @@ let initServiceWorker = async () => { // 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; @@ -589,7 +620,7 @@ let initFromRequest = (req) => { let libresilientFetch = (plugin, url, init, reqInfo) => { // status of the plugin reqInfo.update({ - method: plugin.name, + method: (plugin && "name" in plugin) ? plugin.name : "unknown", state: "running" })