service-worker: code for handling edge-cases with new config.json configuring not-yet-loaded plugins (ref. #30)

merge-requests/12/merge
Michał 'rysiek' Woźniak 2022-02-08 02:44:01 +00:00
rodzic 1fb9ab7e05
commit 50fad1daa0
2 zmienionych plików z 117 dodań i 27 usunięć

Wyświetl plik

@ -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 = {

Wyświetl plik

@ -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"
})