diff --git a/__tests__/plugins/alt-fetch.test.js b/__tests__/plugins/alt-fetch.test.js index 603854c..047fae7 100644 --- a/__tests__/plugins/alt-fetch.test.js +++ b/__tests__/plugins/alt-fetch.test.js @@ -134,6 +134,62 @@ describe("plugin: alt-fetch", () => { expect(response.url).toEqual('https://resilient.is/test.json') }) + test("it should pass the Request() init data to fetch() for all used endpoints", async () => { + + 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"); + + global.fetch.mockImplementation((url, init) => { + const response = new Response( + new Blob( + [JSON.stringify({ test: "success" })], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }); + return Promise.resolve(response); + }); + + var initTest = { + method: "GET", + headers: new Headers({"x-stub": "STUB"}), + mode: "mode-stub", + credentials: "credentials-stub", + cache: "cache-stub", + referrer: "referrer-stub", + // these are not implemented by service-worker-mock + // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 + redirect: undefined, + integrity: undefined, + cache: undefined + } + + const response = await LibResilientPluginConstructors.get('alt-fetch')(LR, init).fetch('https://resilient.is/test.json', initTest); + + expect(fetch).toHaveBeenCalledTimes(3); + expect(fetch).toHaveBeenNthCalledWith(1, expect.stringContaining('/test.json'), initTest); + expect(fetch).toHaveBeenNthCalledWith(2, expect.stringContaining('/test.json'), initTest); + expect(fetch).toHaveBeenNthCalledWith(3, expect.stringContaining('/test.json'), initTest); + expect(await response.json()).toEqual({test: "success"}) + expect(response.url).toEqual('https://resilient.is/test.json') + }) + test("it should set the LibResilient headers", async () => { require("../../plugins/alt-fetch.js"); diff --git a/__tests__/plugins/any-of.test.js b/__tests__/plugins/any-of.test.js index 7b87ae9..1ecf29b 100644 --- a/__tests__/plugins/any-of.test.js +++ b/__tests__/plugins/any-of.test.js @@ -93,6 +93,31 @@ describe("plugin: any-of", () => { expect(response.url).toEqual('https://resilient.is/test.json') }); + test("it should return data from fetch()", async () => { + require("../../plugins/any-of.js"); + + var initTest = { + method: "GET", + headers: new Headers({"x-stub": "STUB"}), + mode: "mode-stub", + credentials: "credentials-stub", + cache: "cache-stub", + referrer: "referrer-stub", + // these are not implemented by service-worker-mock + // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 + redirect: undefined, + integrity: undefined, + cache: undefined + } + + const response = await LibResilientPluginConstructors.get('any-of')(LR, init).fetch('https://resilient.is/test.json', initTest); + + expect(fetch).toHaveBeenCalled(); + expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest); + expect(await response.json()).toEqual({test: "success"}) + expect(response.url).toEqual('https://resilient.is/test.json') + }); + test("it should throw an error when HTTP status is >= 400", async () => { global.fetch.mockImplementation((url, init) => { diff --git a/__tests__/plugins/fetch.test.js b/__tests__/plugins/fetch.test.js index 68d899c..82273c6 100644 --- a/__tests__/plugins/fetch.test.js +++ b/__tests__/plugins/fetch.test.js @@ -3,25 +3,25 @@ const makeServiceWorkerEnv = require('service-worker-mock'); global.fetch = require('node-fetch'); jest.mock('node-fetch') -global.fetch.mockImplementation((url, init) => { - const response = new Response( - new Blob( - [JSON.stringify({ test: "success" })], - {type: "application/json"} - ), - { - status: 200, - statusText: "OK", - headers: { - 'ETag': 'TestingETagHeader' - }, - url: url - }); - return Promise.resolve(response); - }); - describe("plugin: fetch", () => { beforeEach(() => { + global.fetch.mockImplementation((url, init) => { + const response = new Response( + new Blob( + [JSON.stringify({ test: "success" })], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: url + }); + return Promise.resolve(response); + }); + Object.assign(global, makeServiceWorkerEnv()); jest.resetModules(); global.LibResilientPluginConstructors = new Map() @@ -31,6 +31,7 @@ describe("plugin: fetch", () => { } } }) + test("it should register in LibResilientPluginConstructors", () => { require("../../plugins/fetch.js"); expect(LibResilientPluginConstructors.get('fetch')().name).toEqual('fetch'); @@ -46,6 +47,30 @@ describe("plugin: fetch", () => { expect(response.url).toEqual('https://resilient.is/test.json') }); + test("it should pass the Request() init data to fetch()", async () => { + require("../../plugins/fetch.js"); + + var initTest = { + method: "GET", + headers: new Headers({"x-stub": "STUB"}), + mode: "mode-stub", + credentials: "credentials-stub", + cache: "cache-stub", + referrer: "referrer-stub", + // these are not implemented by service-worker-mock + // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 + redirect: undefined, + integrity: undefined, + cache: undefined + } + + const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json', initTest); + + expect(fetch).toHaveBeenCalledWith('https://resilient.is/test.json', initTest); + expect(await response.json()).toEqual({test: "success"}) + expect(response.url).toEqual('https://resilient.is/test.json') + }); + test("it should set the LibResilient headers", async () => { require("../../plugins/fetch.js"); diff --git a/__tests__/service-worker.test.js b/__tests__/service-worker.test.js index 9868161..472bd7b 100644 --- a/__tests__/service-worker.test.js +++ b/__tests__/service-worker.test.js @@ -169,6 +169,76 @@ describe("service-worker", () => { expect(await response.json()).toEqual({ test: "success" }) }); + test("plugins should receive the Request() init data", async () => { + self.LibResilientConfig = { + plugins: [{ + name: 'reject-all' + },{ + name: 'resolve-all' + }], + loggedComponents: [ + 'service-worker' + ] + } + let rejectingFetch = jest.fn((request, init)=>{ return Promise.reject(request); }) + let resolvingFetch = jest.fn((request, init)=>{ + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({ test: "success" })], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'TestingETagHeader' + }, + url: self.location.origin + '/test.json' + }) + ) + }) + global.LibResilientPluginConstructors.set('reject-all', ()=>{ + return { + name: 'reject-all', + description: 'Reject all requests.', + version: '0.0.1', + fetch: rejectingFetch + } + }) + global.LibResilientPluginConstructors.set('resolve-all', ()=>{ + return { + name: 'resolve-all', + description: 'Resolve all requests.', + version: '0.0.1', + fetch: resolvingFetch + } + }) + + var initTest = { + method: "GET", + headers: new Headers({"x-stub": "STUB"}), + mode: "mode-stub", + credentials: "credentials-stub", + cache: "cache-stub", + referrer: "referrer-stub", + // these are not implemented by service-worker-mock + // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 + redirect: undefined, + integrity: undefined, + cache: undefined + } + + require("../service-worker.js"); + + var response = await self.trigger('fetch', new Request('/test.json', initTest)) + expect(rejectingFetch).toHaveBeenCalled(); + expect(resolvingFetch).toHaveBeenCalled(); + expect(await response.json()).toEqual({ test: "success" }) + expect(rejectingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest) + expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest) + }); + test("defaultPluginTimeout should be respected", async () => { jest.useFakeTimers() self.LibResilientConfig = { @@ -575,6 +645,110 @@ describe("service-worker", () => { })).toEqual({ test: "success" }) }); + test("after a retrieval from a stashing plugin, background plugin should receive the Request() init data", async () => { + self.LibResilientConfig = { + plugins: [{ + name: 'stashing-test' + },{ + name: 'resolve-all' + }], + loggedComponents: [ + 'service-worker' + ] + } + let resolvingFetch = jest.fn((request, init)=>{ + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({ test: "success" })], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'X-LibResilient-Method': 'resolve-all', + 'X-LibResilient-ETag': 'TestingETagHeader' + }, + url: self.location.origin + '/test.json' + }) + ) + }) + let resolvingFetch2 = jest.fn((request, init)=>{ + return Promise.resolve( + new Response( + new Blob( + [JSON.stringify({ test: "success2" })], + {type: "application/json"} + ), + { + status: 200, + statusText: "OK", + headers: { + 'ETag': 'NewTestingETagHeader' + }, + url: self.location.origin + '/test.json' + }) + ) + }) + let stashingStash = jest.fn(async (response, url)=>{ + expect(await response.json()).toEqual({ test: "success2" }) + expect(response.headers.get('ETag')).toEqual('NewTestingETagHeader') + }) + + global.LibResilientPluginConstructors.set('stashing-test', ()=>{ + return { + name: 'stashing-test', + description: 'Mock stashing plugin.', + version: '0.0.1', + fetch: resolvingFetch, + stash: stashingStash + } + }) + global.LibResilientPluginConstructors.set('resolve-all', ()=>{ + return { + name: 'resolve-all', + description: 'Resolve all requests.', + version: '0.0.1', + fetch: resolvingFetch2 + } + }) + + var testClient = new Client() + self.clients.clients.push(testClient) + var fetchedDiffersFound = false + testClient.addEventListener('message', event => { + if (event.data.fetchedDiffers) { + fetchedDiffersFound = true + } + }) + + require("../service-worker.js"); + + var initTest = { + method: "GET", + headers: new Headers({"x-stub": "STUB"}), + mode: "mode-stub", + credentials: "credentials-stub", + cache: "cache-stub", + referrer: "referrer-stub", + // these are not implemented by service-worker-mock + // https://github.com/zackargyle/service-workers/blob/master/packages/service-worker-mock/models/Request.js#L20 + redirect: undefined, + integrity: undefined, + cache: undefined + } + + var response = await self.trigger('fetch', { + request: new Request('/test.json', initTest), + clientId: testClient.id + }) + expect(resolvingFetch).toHaveBeenCalled(); + expect(await response.json()).toEqual({ test: "success" }) + expect(resolvingFetch).toHaveBeenCalledWith('https://www.test.com/test.json', initTest); + expect(resolvingFetch2).toHaveBeenCalledWith('https://www.test.com/test.json', initTest); + }); + test("unstashing content explicitly should work", async () => { self.LibResilientConfig = { plugins: [{ diff --git a/plugins/alt-fetch.js b/plugins/alt-fetch.js index 6cd984c..05f2881 100644 --- a/plugins/alt-fetch.js +++ b/plugins/alt-fetch.js @@ -62,12 +62,16 @@ /** * getting content using regular HTTP(S) fetch() */ - let fetchContentFromAlternativeEndpoints = (url) => { + let fetchContentFromAlternativeEndpoints = (url, init={}) => { // we're going to try a random endpoint and building an URL of the form: // https://// var path = url.replace(/https?:\/\/[^/]+\//, '') + // we really want to make fetch happen, Regina! + // TODO: this change should *probably* be handled on the Service Worker level + init.cache = 'reload' + // we don't want to modify the original endpoints array var sourceEndpoints = [...config.endpoints] @@ -91,7 +95,7 @@ return Promise.any( useEndpoints.map( - u=>fetch(u, {cache: "reload"}) + u=>fetch(u, init) )) .then((response) => { // 4xx? 5xx? that's a paddlin' diff --git a/plugins/any-of.js b/plugins/any-of.js index c1e5b99..a58c29d 100644 --- a/plugins/any-of.js +++ b/plugins/any-of.js @@ -32,10 +32,10 @@ /** * getting content using regular HTTP(S) fetch() */ - let fetchContent = (url) => { + let fetchContent = (url, init={}) => { LR.log(pluginName, `using: [${config.uses.map(p=>p.name).join(', ')}]!`) return Promise.any( - config.uses.map(p=>p.fetch(url)) + config.uses.map(p=>p.fetch(url, init)) ) } diff --git a/plugins/cache.js b/plugins/cache.js index 1372942..80b1154 100644 --- a/plugins/cache.js +++ b/plugins/cache.js @@ -21,7 +21,7 @@ /** * getting content from cache */ - let getContentFromCache = (url) => { + let getContentFromCache = (url, init={}) => { LR.log(pluginName, `getting from cache: ${url}`) return caches.open('v1') .then((cache) => { diff --git a/plugins/fetch.js b/plugins/fetch.js index 660fcca..d7fe153 100644 --- a/plugins/fetch.js +++ b/plugins/fetch.js @@ -24,9 +24,13 @@ /** * getting content using regular HTTP(S) fetch() */ - let fetchContent = (url) => { + let fetchContent = (url, init={}) => { LR.log(pluginName, `regular fetch: ${url}`) - return fetch(url, {cache: "reload"}) + // we really want to make fetch happen, Regina! + // TODO: this change should *probably* be handled on the Service Worker level + init.cache = 'reload' + // run built-in regular fetch() + return fetch(url, init) .then((response) => { // 4xx? 5xx? that's a paddlin' if (response.status >= 400) { diff --git a/plugins/gun-ipfs.js b/plugins/gun-ipfs.js index 8c80b0c..9e570fd 100644 --- a/plugins/gun-ipfs.js +++ b/plugins/gun-ipfs.js @@ -165,7 +165,7 @@ if (typeof window === 'undefined') { /** * the workhorse of this plugin */ - async function getContentFromGunAndIPFS(url) { + async function getContentFromGunAndIPFS(url, init={}) { await setup_gun_ipfs(); diff --git a/plugins/ipns-ipfs.js b/plugins/ipns-ipfs.js index c7bc44a..8454013 100644 --- a/plugins/ipns-ipfs.js +++ b/plugins/ipns-ipfs.js @@ -96,7 +96,7 @@ /** * the workhorse of this plugin */ - async function getContentFromIPNSAndIPFS(url) { + async function getContentFromIPNSAndIPFS(url, init={}) { return new Error("Not implemented yet.") var urlArray = url.replace(/https?:\/\//, '').split('/') diff --git a/service-worker.js b/service-worker.js index 103f7f2..36f1716 100644 --- a/service-worker.js +++ b/service-worker.js @@ -398,15 +398,35 @@ let LibResilientResourceInfo = class { |* === Main Brain of LibResilient === *| \* ========================================================================= */ +/** + * generate Request()-compatible init object from an existing Request + * + * req - the request to work off of + */ +let initFromRequest = (req) => { + return { + method: req.method, + headers: req.headers, + mode: req.mode, + credentials: req.credentials, + cache: req.cache, + redirect: req.redirect, + referrer: req.referrer, + integrity: req.integrity + } +} + + /** * run a plugin's fetch() method * while handling all the auxiliary stuff like saving info in reqInfo * * plugin - the plugin to use * url - string containing the URL to fetch + * init - Request() initialization parameters * reqInfo - instance of LibResilientResourceInfo */ -let libresilientFetch = (plugin, url, reqInfo) => { +let libresilientFetch = (plugin, url, init, reqInfo) => { // status of the plugin reqInfo.update({ @@ -416,11 +436,13 @@ let libresilientFetch = (plugin, url, reqInfo) => { // log stuff self.log('service-worker', "LibResilient Service Worker handling URL:", url, - '\n+-- using method(s):', plugin.name) + '\n+-- init:', Object.getOwnPropertyNames(init).map(p=>`\n - ${p}: ${init[p]}`).join(''), + '\n+-- using method(s):', plugin.name + ) // race the plugin(s) vs. a timeout return Promise.race([ - plugin.fetch(url), + plugin.fetch(url, init), promiseTimeout( self.LibResilientConfig.defaultPluginTimeout, false, @@ -464,6 +486,9 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas // clean the URL, removing any fragment identifier var url = request.url.replace(/#.+$/, ''); + // get the init object from Request + var init = initFromRequest(request) + // set-up reqInfo for the fetch event var reqInfo = new LibResilientResourceInfo(url, clientId) @@ -503,14 +528,14 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas state: "error", fetchError: error.toString() }) - return libresilientFetch(currentPlugin, url, reqInfo) + return libresilientFetch(currentPlugin, url, init, reqInfo) }) }, // this libresilientFetch() will run first // all other promises generated by LibResilientPlugins[] will be chained on it // using the catch() in reduce() above // skipping this very first plugin by way of slice(1) - libresilientFetch(LibResilientPluginsRun[0], url, reqInfo) + libresilientFetch(LibResilientPluginsRun[0], url, init, reqInfo) ) .then((response)=>{ // we got a successful response @@ -532,6 +557,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)=>{ self.log('service-worker', 'background no-stashed fetch failed for:', url); })