From b50b76d0d5d7421acdeeb37bf782c1600116de2a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20=27rysiek=27=20Wo=C5=BAniak?= Date: Sun, 11 Feb 2024 23:27:56 +0000 Subject: [PATCH] improved config.json error handling, including additional tests (ref. #48) --- .../service-worker/service-worker.test.js | 880 +++++++++++++----- service-worker.js | 255 ++--- 2 files changed, 769 insertions(+), 366 deletions(-) diff --git a/__tests__/service-worker/service-worker.test.js b/__tests__/service-worker/service-worker.test.js index 2829b68..7206a98 100644 --- a/__tests__/service-worker/service-worker.test.js +++ b/__tests__/service-worker/service-worker.test.js @@ -54,6 +54,7 @@ class FetchEvent extends Event { } } + beforeAll(async ()=>{ // default mocked response data @@ -66,6 +67,7 @@ beforeAll(async ()=>{ 'Last-Modified': 'TestingLastModifiedHeader', 'ETag': 'TestingETagHeader' } + } // get a Promise resolvint to a mocked Response object built based on supplied data window.getMockedResponse = (url, init, response_data={}) => { @@ -91,6 +93,7 @@ beforeAll(async ()=>{ ); return Promise.resolve(response); } + // get a mocked fetch()-like function that returns a Promise resolving to the above window.getMockedFetch = (response_data={}) => { return (url, init)=>{ @@ -98,6 +101,66 @@ beforeAll(async ()=>{ } } + // prepare a mocked transport plugin, returning a spied-on fetch()-like function + window.prepareMockedTransportConfigJSON = (plugin_name, config_data, stale=false) => { + // by default not stale + let date = new Date().toUTCString() + // very stale if need be + if (stale === true) { + date = new Date(0).toUTCString() + } + // prepare the function + let mockedFetch = spy(window.getMockedFetch({ + data: JSON.stringify(config_data), + headers: { + Date: date + } + })) + // create the plugin + window.LibResilientPluginConstructors.set(plugin_name, ()=>{ + return { + name: plugin_name, + description: 'Resolve with config data (pretending to be fetch).', + version: '0.0.1', + fetch: mockedFetch + } + }) + return mockedFetch + } + + /** + * caching a mocked config.json + */ + window.cacheMockedConfigJSON = async (cache_name, data, stale=false) => { + // prepare the data + let config_data = { + data: JSON.stringify(data) + } + // do we want a stale cache? + if (stale === true) { + config_data.headers = { + // very stale date + 'Date': new Date(0).toUTCString() + } + } else { + config_data.headers = { + // current date + 'Date': new Date().toUTCString() + } + } + // cache it in the relevant cache + return await caches + .open(cache_name) + .then(async (cache)=>{ + await cache + .put( + self.config_url, + await window + .getMockedResponse(config_url, {}, config_data) + ) + }) + } + /* * prototype of the plugin init object */ @@ -105,15 +168,6 @@ beforeAll(async ()=>{ name: 'cache' } - /* - * mocking our own ServiceWorker API, sigh! - * https://github.com/denoland/deno/issues/5957#issuecomment-985494969 - */ - window.registration = { - scope: "https://test.resilient.is/", - unregister: spy(()=>{}) - } - /* * mocking caches.match() * https://developer.mozilla.org/en-US/docs/Web/API/CacheStorage/match#browser_compatibility @@ -215,13 +269,14 @@ beforeAll(async ()=>{ // wait for caching of a URL, looped up to `tries` times window.waitForCacheAction = (url, cache_name, action="add", tries=100) => { - if (action != "add" && action != "remove") { - throw new Error('waitForCacheAction()\'s action parameter can only be "add" or "remove".') + if (action != "add" && action != "remove" && action != "update") { + throw new Error('waitForCacheAction()\'s action parameter can only be "add", "remove", or "update".') } console.log(`*** WAITING FOR CACHE (${cache_name}) ACTION:`, action, '\n - url:', url) return new Promise(async (resolve, reject)=>{ // get the cache object let cache = await caches.open(cache_name) + let last_result = false // try to match until we succeed, or run out of tries for (let i=0; i{ // > before the end of the test. return resolve(await cache_result.text()) } + + // waiting for cache content to be updated? + } else if (action === 'update') { + // we expect something to have already been in cache + if (cache_result === undefined) { + return reject('Nothing was in cache already, which is unexpected') + } + // save the "old" data + if (last_result === false) { + last_result = await cache_result.text() + // we are waiting for an update of the content + } else { + let cur = await cache_result.text() + if (last_result !== cur) { + return resolve(cur) + } + } + // waiting for content to be removed from cache? } else if (action === "remove") { if (cache_result === undefined) { @@ -282,7 +355,16 @@ beforeAll(async ()=>{ * and make sure window.init is clean and not modified by previous tests */ beforeEach(async ()=>{ + window.config_url = window.location.origin + 'config.json' window.fetch = spy(window.getMockedFetch()) + /* + * mocking our own ServiceWorker API, sigh! + * https://github.com/denoland/deno/issues/5957#issuecomment-985494969 + */ + window.registration = { + scope: "https://test.resilient.is/", + unregister: spy(()=>{}) + } window.init = { ...window.initPrototype } @@ -422,7 +504,24 @@ describe('service-worker', async () => { assertSpyCalls(self.fetch, 1) }) - it("should use default LibResilientConfig values when config.json not valid JSON", async () => { + it("should fail if default LibResilientConfig values are invalid and fetched config.json is not valid JSON", async () => { + + let mock_response_data = { + data: "Not JSON" + } + window.fetch = spy(window.getMockedFetch(mock_response_data)) + + self.LibResilientConfig = {} + + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + assertSpyCalls(self.fetch, 1) + assertSpyCalls(self.registration.unregister, 1) + }) + + it("should use default LibResilientConfig values when the fetched config.json is not valid JSON", async () => { let mock_response_data = { data: "Not JSON" @@ -560,15 +659,12 @@ describe('service-worker', async () => { assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, true) assertSpyCalls(self.fetch, 1) - // cacheConfigJSON() is called asynchronously in the Service Worker, - // if we don't make sure that the caching has completed, we will get an error. - // so we wait until config.json is cached, and use that to verify it is in fact cached assertEquals( - await window.waitForCacheAction(window.location.origin + 'config.json', 'v1'), + await window.waitForCacheAction(self.config_url, 'v1'), mock_response_data.data ); assertEquals( - await window.waitForCacheAction(window.location.origin + 'config.json', 'v1:verified'), + await window.waitForCacheAction(self.config_url, 'v1:verified'), mock_response_data.data ); }) @@ -799,286 +895,582 @@ describe('service-worker', async () => { assertEquals(self.LibResilientPlugins[1].uses[0].uses.length, 0) }) - it("should use a cached valid config.json file, with no fetch happening", async () => { + /* + * testing config.json fetching, verification, and caching + */ + + it("should use a valid config.json from v1 cache, with no fetch happening, caching it in v1:verified cache", async () => { + // v1 cache data + let v1_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} + // cache it in v1 cache, not stale + await self.cacheMockedConfigJSON("v1", v1_data, false) - // prepare the config request/response - let mock_response_data = { - data: JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000}) - } - var config_url = window.location.origin + 'config.json' - - // get the response containing config to cache - var config_response = await window.getMockedResponse(config_url, {}, mock_response_data) - - // cache it once you get it - await caches - .open('v1') - .then(async (cache)=>{ - await cache - .put( - config_url, - await window.getMockedResponse(config_url, {}, mock_response_data) - ) - }) - // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() + // no fetch assertSpyCalls(self.fetch, 0) + + // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) - // cacheConfigJSON() is called asynchronously in the Service Worker, - // if we don't make sure that the caching has completed, we will get an error. - // so we wait until config.json is cached, and use that to verify it is in fact cached + // in both caches we expect the contents of v1 cache assertEquals( - await window.waitForCacheAction(window.location.origin + 'config.json', 'v1:verified'), - mock_response_data.data + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1_data), + "v1:verified cache has incorrect config.json data" ); }) - it("should use a stale cached valid config.json file without a fetch, caching it in v1:verified cache, then retrieve and cache a fresh config.json using the configured plugins", async () => { - - // this does not change - var config_url = window.location.origin + 'config.json' - - // prepare the stale config request/response - let mock_response_data = { - data: JSON.stringify({loggedComponents: ['service-worker', 'cache', 'fetch'], plugins: [{name: "fetch"},{name: "cache"}], defaultPluginTimeout: 5000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // cache it once you get it - await caches - .open('v1') - .then(async (cache)=>{ - await cache - .put( - config_url, - await window.getMockedResponse(config_url, {}, mock_response_data) - ) - let resp = await cache - .match(config_url) - console.log(resp) - console.log(await resp.text()) - }) - - // prepare the fresh config request/response - let mock_response_data2 = { - data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 2000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // we need to be able to spy on the function that "fetches" the config - let resolveConfigFetch = spy(window.getMockedFetch(mock_response_data2)) - window.LibResilientPluginConstructors.set('fetch', ()=>{ - return { - name: 'fetch', - description: 'Resolve with config data (pretending to be fetch).', - version: '0.0.1', - fetch: resolveConfigFetch - } - }) + it("should use a valid config.json from v1:verified cache when not available in v1, with no fetch happening, caching it in v1 cache", async () => { + // v1:verified cache data + let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} + // cache it in v1 cache, not stale + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() - // verify current config (the one from the pre-cached stale `config.json`) - assertEquals(typeof self.LibResilientConfig, 'object') - assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) - assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}]) - assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache', 'fetch']) + // no fetch assertSpyCalls(self.fetch, 0) - assertSpyCalls(resolveConfigFetch, 1) - // cacheConfigJSON() is called asynchronously in the Service Worker, - // if we don't make sure that the caching has completed, we will get an error. - // so we wait until config.json is cached, and use that to verify it is in fact cached + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) + + // in both caches we expect the contents of v1 cache assertEquals( - await window.waitForCacheAction(window.location.origin + 'config.json', 'v1:verified'), - mock_response_data.data + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" ); - - // wait until verify the *new* config got cached - // running waitForCacheAdd only once might not be enough, as the cache - // already contained config.json! - // - // we have try to get it a few times, but limit how many times we try - // so as not to end up in a forever loop - for (let i=0; i<100; i++) { - // did we get the new config? - if (await window.waitForCacheAction(config_url, 'v1') === mock_response_data2.data) { - // we did! we're done - return true; - } - } - throw new Error('New config failed to cache, apparently!') }) - it("should use a stale cached valid config.json file without a fetch, caching it in v1:verified cache; invalid config.json retrieved using the configured plugins should not be cached", async () => { + it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (structurally), with no fetch happening, caching it in v1 cache", async () => { + // v1 cached data invalid (invalid structure) + let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, false) - // this does not change - var config_url = window.location.origin + 'config.json' - - // prepare the stale config request/response - let mock_response_data = { - data: JSON.stringify({loggedComponents: ['service-worker', 'cache', 'resolve-config'], plugins: [{name: "cache"}, {name: "resolve-config"}], defaultPluginTimeout: 5000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // cache it once you get it - await caches - .open('v1') - .then(async (cache)=>{ - await cache - .put( - config_url, - await window.getMockedResponse(config_url, {}, mock_response_data) - ) - let resp = await cache - .match(config_url) - console.log(resp) - console.log(await resp.text()) - }) - - // prepare the fresh invalid config request/response - let mock_response_data2 = { - data: JSON.stringify({loggedComponentsInvalid: ['service-worker', 'resolve-config'], pluginsInvalid: [{name: "resolve-config"}], defaultPluginTimeoutInvalid: 2000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // we need to be able to spy on the function that "fetches" the config - let resolveConfigFetch = spy(window.getMockedFetch(mock_response_data2)) - window.LibResilientPluginConstructors.set('resolve-config', ()=>{ - return { - name: 'resolve-config', - description: 'Resolve with config data.', - version: '0.0.1', - fetch: resolveConfigFetch - } - }) + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() - // verify current config (the one from the pre-cached stale `config.json`) - assertEquals(typeof self.LibResilientConfig, 'object') - assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) - assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}, {name: "resolve-config"}]) - assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache', 'resolve-config']) + // no fetch assertSpyCalls(self.fetch, 0) - assertSpyCalls(resolveConfigFetch, 1) - // cacheConfigJSON() is called asynchronously in the Service Worker, - // if we don't make sure that the caching has completed, we will get an error. - // so we wait until config.json is cached, and use that to verify it is in fact cached + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) + + // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated assertEquals( - await window.waitForCacheAction(window.location.origin + 'config.json', 'v1:verified'), - mock_response_data.data + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" ); - - // waiting for potential caching of the "new" config - for (let i=0; i<100; i++) { - // did we get the new config? - if (await window.waitForCacheAction(config_url, 'v1') === mock_response_data2.data) { - // we did! that's a paddling! - throw new Error('New config should not have been cached!') - } - } }) - it("should use a stale cached valid config.json file without a fetch; invalid config.json retrieved using the configured plugins other than fetch should not be cached", async () => { + it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (syntactically), with no fetch happening, caching it in v1 cache", async () => { + // v1 cached data invalid (not JSON) + let v1_data = "NOT JSON" + await self.cacheMockedConfigJSON("v1", v1_data, false) - // this does not change - var config_url = window.location.origin + 'config.json' - - // prepare the stale config request/response - let mock_response_data = { - data: JSON.stringify({loggedComponents: ['service-worker', 'resolve-config'], plugins: [{name: "resolve-config"}], defaultPluginTimeout: 5000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // cache it once you get it - await caches - .open('v1') - .then(async (cache)=>{ - await cache - .put( - config_url, - await window.getMockedResponse(config_url, {}, mock_response_data) - ) - let resp = await cache - .match(config_url) - console.log(resp) - console.log(await resp.text()) - }) - - // prepare the fresh valid config request/response - let mock_response_data2 = { - data: JSON.stringify({loggedComponents: ['service-worker', 'resolve-config', 'cache'], plugins: [{name: "resolve-config"}, {name: "cache"}], defaultPluginTimeout: 2000}), - headers: { - // very stale date - 'Date': new Date(0).toUTCString() - } - } - - // we need to be able to spy on the function that "fetches" the config - let resolveConfigFetch = spy(window.getMockedFetch(mock_response_data2)) - window.LibResilientPluginConstructors.set('resolve-config', ()=>{ - return { - name: 'resolve-config', - description: 'Resolve with config data.', - version: '0.0.1', - fetch: resolveConfigFetch - } - }) + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) // service worker is a go! await import("../../service-worker.js?" + window.test_id); await self.dispatchEvent(new Event('install')) await self.waitForSWInstall() - // verify current config (the one from the pre-cached stale `config.json`) + // no fetch + assertSpyCalls(self.fetch, 0) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) + + // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a valid config.json from v1:verified cache when the one in v1 is invalid (unavailable plugins), with no fetch happening, caching it in v1 cache", async () => { + // v1 cached data valid + let v1_data = {loggedComponents: ['service-worker', 'no-such-plugin'], plugins: [{name: "no-such-plugin"}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, false) + + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // no fetch + assertSpyCalls(self.fetch, 0) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) + + // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1 cache, with no fetch happening, caching it in v1:verified cache", async () => { + // v1 cached data valid + let v1_data = {loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // no fetch + assertSpyCalls(self.fetch, 0) + + // config applied assertEquals(typeof self.LibResilientConfig, 'object') assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) - assertEquals(self.LibResilientConfig.plugins, [{name: "resolve-config"}]) - assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'resolve-config']) - assertSpyCalls(self.fetch, 0) - assertSpyCalls(resolveConfigFetch, 1) + assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache']) - // waiting for potential caching of the "new" config - for (let i=0; i<100; i++) { - // did we get the new config? - if (await window.waitForCacheAction(config_url, 'v1') === mock_response_data2.data) { - // we did! that's a paddling - throw new Error('New config failed to cache, apparently!') - } - } + // in both caches we expect the contents of v1 cache; in v1 the contents need to be updated + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1_data), + "v1:verified cache has incorrect config.json data" + ); }) + it("should use a stale config.json from v1 cache, caching it in v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { + // v1 cached data valid + let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // no fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1', 'update'), + JSON.stringify(fetch_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1 cache, then try to fetch a fresh version, ignoring it if invalid (structurally) and caching the v1 version in v1:verified cache", async () => { + // v1 cached data valid + let v1_data = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // no fetch + assertSpyCalls(self.fetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) + assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch']) + + // v1 cache should *not* be updated + try { + await window.waitForCacheAction(self.config_url, 'v1', 'update') + } catch (e) { + assertEquals(e, 'Ran out of tries') + } + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1 cache, then try to fetch a fresh version, ignoring it if invalid (unavailable plugins) and caching the v1 version in v1:verified cache", async () => { + // v1 cached data valid + let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch'], plugins: [{name: "mocked-fetch"}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // no fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch']) + + // v1 cache should *not* be updated + try { + await window.waitForCacheAction(self.config_url, 'v1', 'update') + } catch (e) { + assertEquals(e, 'Ran out of tries') + } + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1', 'update'), + JSON.stringify(fetch_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1:verified cache, then try to fetch a fresh version, ignoring it if invalid (structurally) and caching the v1:verified version in v1 cache", async () => { + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(self.fetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should use a stale config.json from v1:verified cache, then try to fetch a fresh version, ignoring it if invalid (unavailable plugins) and caching the v1:verified version in v1 cache", async () => { + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch'], plugins: [{name: "mocked-fetch"}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // mocked fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should ignore invalid (structurally) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { + // v1 cached data invalid (invalid structure) + let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, false) + + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1', 'update'), + JSON.stringify(fetch_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should ignore invalid (syntactically) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { + // v1 cached data invalid (not JSON) + let v1_data = "NOT JSON" + await self.cacheMockedConfigJSON("v1", v1_data, false) + + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1', 'update'), + JSON.stringify(fetch_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should ignore invalid (unavailable plugins) config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, caching it if valid in v1 cache", async () => { + // v1 cached data invalid (unavailable plugin) + let v1_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'no-such-plugin'], plugins: [{name: "mocked-fetch"}, {name: 'no-such-plugin'}], defaultPluginTimeout: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, false) + + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "cache"}, {name: "mocked-fetch"}], defaultPluginTimeout: 7000} + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1', 'update'), + JSON.stringify(fetch_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + it("should ignore invalid config from v1 cache and use a stale config.json from v1:verified cache; then try to fetch a fresh version, ignoring it if invalid", async () => { + // v1 cached data invalid (invalid structure) + let v1_data = {loggedComponentsInvalid: ['service-worker', 'cache'], pluginsInvalid: [{name: "cache"}], defaultPluginTimeoutInvalid: 5000} + await self.cacheMockedConfigJSON("v1", v1_data, false) + + // v1:verified cached data valid + let v1verified_data = {loggedComponents: ['service-worker', 'mocked-fetch', 'cache'], plugins: [{name: "mocked-fetch"}, {name: 'cache'}], defaultPluginTimeout: 6000} + await self.cacheMockedConfigJSON("v1:verified", v1verified_data, true) // stale! + + let fetch_data = "NOT JSON" + let mockedFetch = self.prepareMockedTransportConfigJSON('mocked-fetch', fetch_data, false) + + // service worker is a go! + await import("../../service-worker.js?" + window.test_id); + await self.dispatchEvent(new Event('install')) + await self.waitForSWInstall() + + // fetch + assertSpyCalls(mockedFetch, 1) + + // config applied + assertEquals(typeof self.LibResilientConfig, 'object') + assertEquals(self.LibResilientConfig.defaultPluginTimeout, 6000) + assertEquals(self.LibResilientConfig.plugins, [{name: "mocked-fetch"}, {name: "cache"}]) + assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'mocked-fetch', 'cache']) + + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1'), + JSON.stringify(v1verified_data), + "v1 cache has incorrect config.json data" + ); + assertEquals( + await window.waitForCacheAction(self.config_url, 'v1:verified'), + JSON.stringify(v1verified_data), + "v1:verified cache has incorrect config.json data" + ); + }) + + /* + * end of testing config.json fetching, verification, and caching + */ + it("should ignore failed fetch by first configured plugin if followed by a successful fetch by a second one", async () => { window.LibResilientConfig = { plugins: [{ diff --git a/service-worker.js b/service-worker.js index e39353e..dc33d9d 100644 --- a/service-worker.js +++ b/service-worker.js @@ -195,6 +195,11 @@ self.guessMimeType = async function(ext, content) { * cdata - config data to verify */ let verifyConfigData = (cdata) => { + // cdata needs to be an object + if ( typeof cdata !== "object" || cdata === null ) { + self.log('service-worker', 'fetched config does not contain a valid JSON object') + return false; + } // 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') @@ -283,140 +288,126 @@ let executeConfig = (config) => { // working on a copy of the plugins config so that config.plugins remains unmodified // in case we need it later (for example, when re-loading the config) let pluginsConfig = [...config.plugins] + + // this is the stash for plugins that need dependencies instantiated first + let dependentPlugins = new Array() - // we want to catch any and all possible errors here - try { + // only now load the plugins (config.json could have changed the defaults) + while (pluginsConfig.length > 0) { - // this is the stash for plugins that need dependencies instantiated first - let dependentPlugins = new Array() + // get the first plugin config from the array + let pluginConfig = pluginsConfig.shift() + self.log('service-worker', `handling plugin type: ${pluginConfig.name}`) - // only now load the plugins (config.json could have changed the defaults) - while (pluginsConfig.length > 0) { + // 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`) - // 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`) + // move the dependency plugin configs to LibResilientConfig to be worked on next + for (let 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 } - // 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 (let 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) } - - // 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 + // instantiate the plugin + let plugin = LibResilientPluginConstructors.get(pluginConfig.name)(self, pluginConfig) + self.log('service-worker', `${pluginConfig.name}: instantiated`) - // 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) ) + // 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; + } - } - - // finally -- do we want to use MIME type guessing based on content? - // dealing with this at the very end so that we know we can safely set detectMimeFromBuffer - // and not need to re-set it back in case anything fails - if (config.useMimeSniffingLibrary === true) { - // we do not want to hit a NetworkError and end up using the default config - // much better to end up not using the fancy MIME type detection in such a case - try { - // we do! load the external lib - self.importScripts(`./lib/file-type.js`) - } catch (e) { - self.log('service-worker', `error when fetching external MIME sniffing library: ${e.message}`) + // 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 } - if (typeof fileType !== 'undefined' && "fileTypeFromBuffer" in fileType) { - detectMimeFromBuffer = fileType.fileTypeFromBuffer - self.log('service-worker', 'loaded external MIME sniffing library') - } else { - self.log('service-worker', 'failed to load external MIME sniffing library!') - } - } + + // 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 - // we're good! - return true; + // 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) ) - // exception? no bueno - } catch (e) { - // inform - self.log('service-worker', `error while executing config: ${e.message}`) - // cleanup after a failed config execution - self.LibResilientPluginConstructors = new Map(lrpcBackup) - self.LibResilientPlugins = new Array() - // we are not good - return false; } + // finally -- do we want to use MIME type guessing based on content? + // dealing with this at the very end so that we know we can safely set detectMimeFromBuffer + // and not need to re-set it back in case anything fails + if (config.useMimeSniffingLibrary === true) { + // we do not want to hit a NetworkError and end up using the default config + // much better to end up not using the fancy MIME type detection in such a case + try { + // we do! load the external lib + self.importScripts(`./lib/file-type.js`) + } catch (e) { + self.log('service-worker', `error when fetching external MIME sniffing library: ${e.message}`) + } + if (typeof fileType !== 'undefined' && "fileTypeFromBuffer" in fileType) { + detectMimeFromBuffer = fileType.fileTypeFromBuffer + self.log('service-worker', 'loaded external MIME sniffing library') + } else { + self.log('service-worker', 'failed to load external MIME sniffing library!') + } + } + + // we're good! + return true; + } @@ -530,7 +521,20 @@ let initServiceWorker = async () => { config = {...self.LibResilientConfig, ...cdata} // try executing the config - config_executed = executeConfig(config) + // we want to catch any and all possible errors here + try { + config_executed = executeConfig(config) + + // exception? no bueno + } catch (e) { + // inform + self.log('service-worker', `error while executing config: ${e.message}`) + // cleanup after a failed config execution + self.LibResilientPluginConstructors = new Map(lrpcBackup) + self.LibResilientPlugins = new Array() + // we are not good + config_executed = false; + } // if we're using the defaults, and yet loading of the config failed // something is massively wrong @@ -552,15 +556,22 @@ let initServiceWorker = async () => { // that is, if it comes from the "v1" cache... if (use_cache === "v1") { self.log('service-worker', `successfully loaded config.json; caching in cache: v1:verified`) - cacheConfigJSON(configURL, cresponse, 'v1:verified') - // or, was fetch()ed and valid (no caching if we're going with defaults, obviously) + await cacheConfigJSON(configURL, cresponse, 'v1:verified') + + // we used the v1:verified cache; we should cache config.json into the v1 cache + // as that will speed things up a bit next time we need to load the service worker + } else if (use_cache === "v1:verified") { + self.log('service-worker', `successfully loaded config.json; caching in cache: v1`) + await cacheConfigJSON(configURL, cresponse, 'v1') + + // or, was fetch()-ed and valid (no caching if we're going with defaults, obviously) } else if ( (use_cache === undefined) && (cresponse !== false) ) { self.log('service-worker', `successfully loaded config.json; caching in cache: v1, v1:verified`) // we want to cache to both, so that: // 1. we get the extra bit of performance from using the v1 cache that is checked first // 2. but we get the verified config already in the v1:verified cache for later - cacheConfigJSON(configURL, await cresponse.clone(), 'v1') - cacheConfigJSON(configURL, cresponse, 'v1:verified') + await cacheConfigJSON(configURL, await cresponse.clone(), 'v1') + await cacheConfigJSON(configURL, cresponse, 'v1:verified') } // inform