diff --git a/__tests__/plugins/alt-fetch.test.js b/__tests__/plugins/alt-fetch.test.js index c4e5c58..1f9b84d 100644 --- a/__tests__/plugins/alt-fetch.test.js +++ b/__tests__/plugins/alt-fetch.test.js @@ -36,38 +36,36 @@ describe("plugin: alt-fetch", () => { beforeEach(() => { Object.assign(global, makeServiceWorkerEnv()); jest.resetModules(); - self.LibResilientPlugins = new Array() - self.LibResilientConfig = { - plugins: { - 'alt-fetch':{ - endpoints: [ - 'https://alt.resilient.is/test.json', - 'https://error.resilientis/test.json', - 'https://timeout.resilientis/test.json' - ] - } + global.LibResilientPluginConstructors = new Map() + init = { + name: 'alt-fetch', + endpoints: [ + 'https://alt.resilient.is/test.json', + 'https://error.resilientis/test.json', + 'https://timeout.resilientis/test.json' + ]} + LR = { + log: (component, ...items)=>{ + console.debug(component + ' :: ', ...items) } } - self.log = function(component, ...items) { - console.debug(component + ' :: ', ...items) - } }) test("it should register in LibResilientPlugins", () => { require("../../plugins/alt-fetch.js"); - expect(self.LibResilientPlugins[0].name).toEqual('alt-fetch'); + expect(LibResilientPluginConstructors.get('alt-fetch')().name).toEqual('alt-fetch'); }); test("it should fail with bad config", () => { - self.LibResilientConfig = { - plugins: { - 'alt-fetch':{ - endpoints: "this is incorrect" - } - } + init = { + name: 'alt-fetch', + endpoints: "this is incorrect" } + require("../../plugins/alt-fetch.js") expect.assertions(1) - expect(()=>{require("../../plugins/alt-fetch.js")}).toThrow(Error); + expect(()=>{ + LibResilientPluginConstructors.get('alt-fetch')(LR, init) + }).toThrow(Error); }); test("it should fetch the content, trying all configured endpoints (if fewer or equal to concurrency setting)", async () => { @@ -90,7 +88,7 @@ describe("plugin: alt-fetch", () => { return Promise.resolve(response); }); - const response = await self.LibResilientPlugins[0].fetch('https://resilient.is/test.json'); + const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json'); expect(fetch).toHaveBeenCalledTimes(3); expect(await response.json()).toEqual({test: "success"}) @@ -99,20 +97,16 @@ describe("plugin: alt-fetch", () => { test("it should fetch the content using, trying random endpoints out of all configured (if more than concurrency setting)", async () => { - self.LibResilientConfig = { - plugins: { - 'alt-fetch':{ - endpoints: [ - 'https://alt.resilient.is/test.json', - 'https://error.resilientis/test.json', - 'https://timeout.resilientis/test.json', - 'https://alt2.resilient.is/test.json', - 'https://alt3.resilient.is/test.json', - 'https://alt4.resilient.is/test.json' - ] - } - } - } + init = { + name: 'alt-fetch', + endpoints: [ + 'https://alt.resilient.is/test.json', + 'https://error.resilientis/test.json', + 'https://timeout.resilientis/test.json', + 'https://alt2.resilient.is/test.json', + 'https://alt3.resilient.is/test.json', + 'https://alt4.resilient.is/test.json' + ]} require("../../plugins/alt-fetch.js"); @@ -133,7 +127,7 @@ describe("plugin: alt-fetch", () => { return Promise.resolve(response); }); - const response = await self.LibResilientPlugins[0].fetch('https://resilient.is/test.json'); + const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json'); expect(fetch).toHaveBeenCalledTimes(3); expect(await response.json()).toEqual({test: "success"}) @@ -143,7 +137,7 @@ describe("plugin: alt-fetch", () => { test("it should set the LibResilient headers", async () => { require("../../plugins/alt-fetch.js"); - const response = await self.LibResilientPlugins[0].fetch('https://resilient.is/test.json'); + const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json'); expect(fetch).toHaveBeenCalledTimes(3); expect(await response.json()).toEqual({test: "success"}) @@ -154,7 +148,7 @@ describe("plugin: alt-fetch", () => { expect(response.headers.get('X-LibResilient-ETag')).toEqual('TestingETagHeader') }); - test("it should set the LibResilient ETag basd on Kast-Modified header (if ETag is not available in the original response)", async () => { + test("it should set the LibResilient ETag basd on Last-Modified header (if ETag is not available in the original response)", async () => { require("../../plugins/alt-fetch.js"); global.fetch.mockImplementation((url, init) => { @@ -174,7 +168,7 @@ describe("plugin: alt-fetch", () => { return Promise.resolve(response); }); - const response = await self.LibResilientPlugins[0].fetch('https://resilient.is/test.json'); + const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json'); expect(fetch).toHaveBeenCalledTimes(3); expect(await response.json()).toEqual({test: "success"}) @@ -204,7 +198,7 @@ describe("plugin: alt-fetch", () => { require("../../plugins/alt-fetch.js"); expect.assertions(1) - expect(self.LibResilientPlugins[0].fetch('https://resilient.is/test.json')).rejects.toThrow(Error) + expect(LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json')).rejects.toThrow(Error) }); }); diff --git a/plugins/alt-fetch.js b/plugins/alt-fetch.js index a432d2a..78f2f60 100644 --- a/plugins/alt-fetch.js +++ b/plugins/alt-fetch.js @@ -11,141 +11,141 @@ */ // no polluting of the global namespace please -(function () { +(function(LRPC){ + // this never changes + const pluginName = "alt-fetch" + LRPC.set(pluginName, (LR, init={})=>{ - /* - * plugin config settings - */ + /* + * plugin config settings + */ + + // sane defaults + let defaultConfig = { + // endpoints to use + // + // they have to respond to requests formatted like: + // / + // + // let's say the endpoint is: + // https://example.com/api/endpoint/ + // ...and that we are trying to get: + // /some/path/img.png + // + // the endpoint is supposed to return the expected image + // when this URL is requested: + // https://example.com/api/endpoint/some/path/img.png + // + // this has to be explicitly configured by the website admin + endpoints: [], - // sane defaults - let defaultConfig = { - // name of this plugin - // should not be changed - name: "alt-fetch", - // endpoints to use - // - // they have to respond to requests formatted like: - // / - // - // let's say the endpoint is: - // https://example.com/api/endpoint/ - // ...and that we are trying to get: - // /some/path/img.png - // - // the endpoint is supposed to return the expected image - // when this URL is requested: - // https://example.com/api/endpoint/some/path/img.png - // - // this has to be explicitly configured by the website admin - endpoints: [], - - // how many simultaneous connections to different endpoints do we want - // - // more concurrency means higher chance of a request succeeding - // but uses more bandwidth and other resources; - // - // 3 seems to be a reasonable default - concurrency: 3 - } + // how many simultaneous connections to different endpoints do we want + // + // more concurrency means higher chance of a request succeeding + // but uses more bandwidth and other resources; + // + // 3 seems to be a reasonable default + concurrency: 3 + } - // merge the defaults with settings from LibResilientConfig - let config = {...defaultConfig, ...self.LibResilientConfig.plugins[defaultConfig.name]} + // merge the defaults with settings from the init var + let config = {...defaultConfig, ...init} - // reality check: endpoints need to be set to an array of non-empty strings - if (typeof(config.endpoints) !== "object" || !Array.isArray(config.endpoints)) { - let err = new Error("endpoints not confgured") - console.error(err) - throw err - } - - - /** - * getting content using regular HTTP(S) fetch() - */ - let fetchContentFromAlternativeEndpoints = (url) => { - - // we're going to try a random endpoint and building an URL of the form: - // https://// - var path = url.replace(/https?:\/\/[^/]+\//, '') - - // we don't want to modify the original endpoints array - var sourceEndpoints = [...config.endpoints] - - // if we have fewer than the configured concurrency or just as many, use all of them - if (sourceEndpoints.length <= config.concurrency) { - var useEndpoints = sourceEndpoints - // otherwise get `config.concurrency` endpoints at random - } else { - var useEndpoints = new Array() - while (useEndpoints.length < config.concurrency) { - // put in the address while we're at it - useEndpoints.push( - sourceEndpoints - .splice(Math.floor(Math.random() * sourceEndpoints.length), 1)[0] + path - ) - } + // reality check: endpoints need to be set to an array of non-empty strings + if (typeof(config.endpoints) !== "object" || !Array.isArray(config.endpoints)) { + let err = new Error("endpoints not confgured") + console.error(err) + throw err } - // debug log - self.log(config.name, `fetching from alternative endpoints:\n ${useEndpoints.join('\n ')}`) - - return Promise.any( - useEndpoints.map( - u=>fetch(u, {cache: "reload"}) - )) - .then((response) => { - // 4xx? 5xx? that's a paddlin' - if (response.status >= 400) { - // throw an Error to fall back to other plugins: - throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); - } - // all good, it seems - self.log(config.name, "fetched:", response.url); - - // we need to create a new Response object - // with all the headers added explicitly, - // since response.headers is immutable - var init = { - status: response.status, - statusText: response.statusText, - headers: {}, - url: url - }; - response.headers.forEach(function(val, header){ - init.headers[header] = val; - }); - - // add the X-LibResilient-* headers to the mix - init.headers['X-LibResilient-Method'] = config.name - - // we will not have it most of the time, due to CORS rules: - // https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header - init.headers['X-LibResilient-ETag'] = response.headers.get('ETag') - if (init.headers['X-LibResilient-ETag'] === null) { - // far from perfect, but what are we going to do, eh? - init.headers['X-LibResilient-ETag'] = response.headers.get('last-modified') - } - - // return the new response, using the Blob from the original one - return response - .blob() - .then((blob) => { - return new Response( - blob, - init - ) - }) - }) - } - // and add ourselves to it - // with some additional metadata - self.LibResilientPlugins.push({ - name: config.name, - description: 'HTTP(S) fetch() using alternative endpoints', - version: 'COMMIT_UNKNOWN', - fetch: fetchContentFromAlternativeEndpoints - }) + /** + * getting content using regular HTTP(S) fetch() + */ + let fetchContentFromAlternativeEndpoints = (url) => { + + // we're going to try a random endpoint and building an URL of the form: + // https://// + var path = url.replace(/https?:\/\/[^/]+\//, '') + + // we don't want to modify the original endpoints array + var sourceEndpoints = [...config.endpoints] + + // if we have fewer than the configured concurrency or just as many, use all of them + if (sourceEndpoints.length <= config.concurrency) { + var useEndpoints = sourceEndpoints + // otherwise get `config.concurrency` endpoints at random + } else { + var useEndpoints = new Array() + while (useEndpoints.length < config.concurrency) { + // put in the address while we're at it + useEndpoints.push( + sourceEndpoints + .splice(Math.floor(Math.random() * sourceEndpoints.length), 1)[0] + path + ) + } + } + + // debug log + LR.log(pluginName, `fetching from alternative endpoints:\n ${useEndpoints.join('\n ')}`) + + return Promise.any( + useEndpoints.map( + u=>fetch(u, {cache: "reload"}) + )) + .then((response) => { + // 4xx? 5xx? that's a paddlin' + if (response.status >= 400) { + // throw an Error to fall back to other plugins: + throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); + } + // all good, it seems + LR.log(pluginName, "fetched:", response.url); + + // we need to create a new Response object + // with all the headers added explicitly, + // since response.headers is immutable + var responseInit = { + status: response.status, + statusText: response.statusText, + headers: {}, + url: url + }; + response.headers.forEach(function(val, header){ + responseInit.headers[header] = val; + }); + + // add the X-LibResilient-* headers to the mix + responseInit.headers['X-LibResilient-Method'] = pluginName + + // we will not have it most of the time, due to CORS rules: + // https://developer.mozilla.org/en-US/docs/Glossary/CORS-safelisted_response_header + responseInit.headers['X-LibResilient-ETag'] = response.headers.get('ETag') + if (responseInit.headers['X-LibResilient-ETag'] === null) { + // far from perfect, but what are we going to do, eh? + responseInit.headers['X-LibResilient-ETag'] = response.headers.get('last-modified') + } + + // return the new response, using the Blob from the original one + return response + .blob() + .then((blob) => { + return new Response( + blob, + responseInit + ) + }) + }) + } + + // return the plugin data structure + return { + name: pluginName, + description: 'HTTP(S) fetch() using alternative endpoints', + version: 'COMMIT_UNKNOWN', + fetch: fetchContentFromAlternativeEndpoints + } + }) // done with not polluting the global namespace -})() +})(LibResilientPluginConstructors) diff --git a/plugins/fetch.js b/plugins/fetch.js index 0bb62c7..de6a0b5 100644 --- a/plugins/fetch.js +++ b/plugins/fetch.js @@ -15,67 +15,67 @@ const pluginName = "fetch" LRPC.set(pluginName, (LR, init={})=>{ - /* - * plugin config settings - */ - - // sane defaults - let defaultConfig = {} - - // merge the defaults with settings from init - let config = {...defaultConfig, ...init} - /** - * getting content using regular HTTP(S) fetch() - */ - let fetchContent = (url) => { - LR.log(pluginName, `regular fetch: ${url}`) - return fetch(url, {cache: "reload"}) - .then((response) => { - // 4xx? 5xx? that's a paddlin' - if (response.status >= 400) { - // throw an Error to fall back to LibResilient: - throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); - } - // all good, it seems - LR.log(pluginName, `fetched successfully: ${response.url}`); - - // we need to create a new Response object - // with all the headers added explicitly, - // since response.headers is immutable - var responseInit = { - status: response.status, - statusText: response.statusText, - headers: {}, - url: response.url - }; - response.headers.forEach(function(val, header){ - responseInit.headers[header] = val; - }); - - // add the X-LibResilient-* headers to the mix - responseInit.headers['X-LibResilient-Method'] = pluginName - responseInit.headers['X-LibResilient-ETag'] = response.headers.get('ETag') - - // return the new response, using the Blob from the original one - return response - .blob() - .then((blob) => { - return new Response( - blob, - responseInit - ) - }) - }) - } + /* + * plugin config settings + */ + + // sane defaults + let defaultConfig = {} + + // merge the defaults with settings from init + let config = {...defaultConfig, ...init} + /** + * getting content using regular HTTP(S) fetch() + */ + let fetchContent = (url) => { + LR.log(pluginName, `regular fetch: ${url}`) + return fetch(url, {cache: "reload"}) + .then((response) => { + // 4xx? 5xx? that's a paddlin' + if (response.status >= 400) { + // throw an Error to fall back to LibResilient: + throw new Error('HTTP Error: ' + response.status + ' ' + response.statusText); + } + // all good, it seems + LR.log(pluginName, `fetched successfully: ${response.url}`); + + // we need to create a new Response object + // with all the headers added explicitly, + // since response.headers is immutable + var responseInit = { + status: response.status, + statusText: response.statusText, + headers: {}, + url: response.url + }; + response.headers.forEach(function(val, header){ + responseInit.headers[header] = val; + }); + + // add the X-LibResilient-* headers to the mix + responseInit.headers['X-LibResilient-Method'] = pluginName + responseInit.headers['X-LibResilient-ETag'] = response.headers.get('ETag') + + // return the new response, using the Blob from the original one + return response + .blob() + .then((blob) => { + return new Response( + blob, + responseInit + ) + }) + }) + } - // return the plugin - return { - name: pluginName, - description: 'Just a regular HTTP(S) fetch()', - version: 'COMMIT_UNKNOWN', - fetch: fetchContent - } + // return the plugin + return { + name: pluginName, + description: 'Just a regular HTTP(S) fetch()', + version: 'COMMIT_UNKNOWN', + fetch: fetchContent + } - }) + }) // done with not polluting the global namespace })(LibResilientPluginConstructors)