Merge branch 'wip-json-config' into 'master'

use JSON config (instead of JavaScript)

Closes #31

See merge request rysiekpl/libresilient!11
merge-requests/12/merge
Michał "rysiek" Woźniak 2022-01-25 01:06:37 +00:00
commit 387009782e
12 zmienionych plików z 500 dodań i 237 usunięć

Wyświetl plik

@ -0,0 +1,13 @@
{
"plugins": [{
"name": "basic-integrity",
"requireIntegrity": false,
"integrity": {
"http://localhost:8000/__tests__/test.json": "sha256-FCNALvZ0mSxEs0+SjOgx/sDFFVuh0MwkhhYnI0UJWDg="
},
"uses": [{
"name": "fetch"
}]
}],
"loggedComponents": ["service-worker", "fetch", "cache", "basic-integrity"]
}

Wyświetl plik

@ -3,7 +3,11 @@ const makeServiceWorkerEnv = require('service-worker-mock');
global.fetch = require('node-fetch');
jest.mock('node-fetch')
global.fetch.mockImplementation((url, init) => {
describe("service-worker", () => {
beforeEach(() => {
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
@ -20,9 +24,7 @@ global.fetch.mockImplementation((url, init) => {
})
);
});
describe("service-worker", () => {
beforeEach(() => {
Object.assign(global, makeServiceWorkerEnv());
global.self = new ServiceWorkerGlobalScope()
jest.resetModules();
@ -81,15 +83,196 @@ describe("service-worker", () => {
}
})
test("basic set-up: LibResilientConfig", async () => {
test("basic set-up: use default LibResilientConfig values when config.json missing", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({ test: "fail" })],
{type: "application/json"}
),
{
status: 404,
statusText: "Not Found",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000)
expect(typeof self.LibResilientConfig.plugins).toEqual('object')
expect(self.LibResilientConfig.loggedComponents).toBeInstanceOf(Array)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache'])
})
test("basic set-up: use default LibResilientConfig values when config.json not valid JSON", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
["not a JSON"],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache'])
})
test("basic set-up: use default LibResilientConfig values when no valid 'plugins' field in config.json", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: 'not a valid array'})],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache'])
})
test("basic set-up: use default LibResilientConfig values when no valid 'loggedComponents' field in config.json", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({loggedComponents: 'not a valid array', plugins: [{name: "fetch"}]})],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache'])
})
test("basic set-up: use default LibResilientConfig values when 'defaultPluginTimeout' field in config.json contains an invalid value", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 'not an integer'})],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(10000)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "fetch"},{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'fetch', 'cache'])
})
test("basic set-up: use config values from a valid config.json file", async () => {
self.LibResilientConfig = null
global.fetch.mockImplementation((url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000})],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
},
url: url
})
);
});
try {
require("../service-worker.js");
} catch(e) {}
await self.trigger('install')
await self.trigger('activate')
expect(typeof self.LibResilientConfig).toEqual('object')
expect(self.LibResilientConfig.defaultPluginTimeout).toBe(5000)
expect(self.LibResilientConfig.plugins).toStrictEqual([{name: "cache"}])
expect(self.LibResilientConfig.loggedComponents).toStrictEqual(['service-worker', 'cache'])
})
test("fetching content should work", async () => {
@ -163,6 +346,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = await self.trigger('fetch', new Request('/test.json'))
expect(rejectingFetch).toHaveBeenCalled();
expect(resolvingFetch).toHaveBeenCalled();
@ -232,6 +418,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = await self.trigger('fetch', new Request('/test.json', initTest))
expect(rejectingFetch).toHaveBeenCalled();
expect(resolvingFetch).toHaveBeenCalled();
@ -267,6 +456,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = self.trigger('fetch', new Request('/test.json'))
jest.advanceTimersByTime(1000);
expect.assertions(2)
@ -443,6 +635,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = await self.trigger('fetch', new Request('/test.json'))
expect(resolvingFetch).toHaveBeenCalled();
expect(stashingStash).not.toHaveBeenCalled();
@ -530,6 +725,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = await self.trigger('fetch', {
request: new Request('/test.json'),
clientId: testClient.id
@ -592,6 +790,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var response = await self.trigger('fetch', new Request('/test.json'))
expect(resolvingFetch).toHaveBeenCalledTimes(2);
expect(await response.json()).toEqual({ test: "success" })
@ -726,6 +927,9 @@ describe("service-worker", () => {
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
var initTest = {
method: "GET",
// TODO: ref. https://gitlab.com/rysiekpl/libresilient/-/issues/23
@ -832,6 +1036,10 @@ describe("service-worker", () => {
}
})
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
await self.trigger(
'message',
{
@ -895,6 +1103,8 @@ describe("service-worker", () => {
}
})
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['dependent-test'])
expect(self.LibResilientPlugins[0].uses.map(p=>p.name)).toEqual(['dependency1-test', 'dependency2-test'])
})
@ -922,6 +1132,8 @@ describe("service-worker", () => {
}
})
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
expect(self.LibResilientPlugins.map(p=>p.name)).toEqual(['plugin-test', 'plugin-test', 'plugin-test'])
expect(self.LibResilientPlugins.map(p=>p.version)).toEqual(['0.0.1', '0.0.2', '0.0.3'])
})
@ -944,6 +1156,8 @@ describe("service-worker", () => {
}
})
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
expect.assertions(1)
try {
await self.trigger('fetch', new Request('/test.json', {method: "GET"}))
@ -1007,6 +1221,8 @@ describe("service-worker", () => {
}
})
require("../service-worker.js");
await self.trigger('install')
await self.trigger('activate')
await self.trigger('fetch', new Request('/test.json'))
})

Wyświetl plik

@ -0,0 +1,3 @@
{
"test": true
}

Wyświetl plik

@ -1,40 +0,0 @@
/*
* LibResilient config
*
* This is an example config for LibResilient. When deploying LibResilient on your website
* you will need to create your own config, using this one as a template.
*
*/
// plugins config
self.LibResilientConfig.plugins = [{
name: 'fetch'
},{
name: 'cache'
},{
name: 'any-of',
uses: [{
name: 'alt-fetch',
// configuring the alternate endpoints plugin to use IPNS gateways
//
// NOTICE: we cannot use CIDv0 with gateways that use hash directly in the (sub)domain:
// https://github.com/node-fetch/node-fetch/issues/260
// we *can* use CIDv1 with such gateways, and that's suggested:
// https://docs.ipfs.io/how-to/address-ipfs-on-web/#path-gateway
// https://cid.ipfs.io/
endpoints: [
'https://<CIDv1>.ipns.dweb.link/', // USA
'https://ipfs.kxv.io/ipns/<CIDv0-or-CIDv1>/', // Hong Kong
'https://jorropo.net/ipns/<CIDv0-or-CIDv1>/', // France
'https://gateway.pinata.cloud/ipns/<CIDv0-or-CIDv1>/', // Germany
'https://<CIDv1>.ipns.bluelight.link/' // Singapore
]
},{
name: 'gun-ipfs',
gunPubkey: '<your-gun-pubkey>'
}]
}]
// we need to explicitly list components we want to see debug messages from
self.LibResilientConfig.loggedComponents = ['service-worker', 'fetch', 'cache', 'any-of', 'alt-fetch', 'gun-ipfs']

Wyświetl plik

@ -0,0 +1,23 @@
{
"plugins": [{
"name": "fetch"
},{
"name": "cache"
},{
"name": "any-of",
"uses": [{
"name": "alt-fetch",
"endpoints": [
"https://<CIDv1>.ipns.dweb.link/",
"https://ipfs.kxv.io/ipns/<CIDv0-or-CIDv1>/",
"https://jorropo.net/ipns/<CIDv0-or-CIDv1>/",
"https://gateway.pinata.cloud/ipns/<CIDv0-or-CIDv1>/",
"https://<CIDv1>.ipns.bluelight.link/"
]
},{
"name": "gun-ipfs",
"gunPubkey": "<your-gun-pubkey>"
}]
}],
"loggedComponents": ["service-worker", "fetch", "cache", "any-of", "alt-fetch", "gun-ipfs"]
}

Wyświetl plik

@ -21,7 +21,7 @@ Methods these plugins implement:
- **Composing plugins**
Plugins that *compose* other plugins, for example by running them simultaneously to retrieve content from whichever succeeds first.
Methods these plugins implement depend on which plugins they compose. Additionally, plugins being composed the `uses` key, providing the configuration for them the same way configuration is provided for plugins in the `plugins` key of `LibResilientConfig`.
Methods these plugins implement depend on which plugins they compose. Additionally, plugins being composed the `uses` key, providing the configuration for them the same way configuration is provided for plugins in the `plugins` key of `LibResilientConfig` (which is configurable via `config.json`).
Every plugin needs to be implemented as a constructor function that is added to the `LibResilientPluginConstructors` [Map()](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Map) object for later instantiation.
@ -76,7 +76,7 @@ uses: config.uses
## Fetching a resource via LibResilient
Whenever a resource is being fetched on a LibResilient-enabled site, the `service-worker.js` script dispatches plugins in the set order. This order is configured via the `plugins` key of the `LibResilientConfig` variable, usually set in `config.js` config file.
Whenever a resource is being fetched on a LibResilient-enabled site, the `service-worker.js` script dispatches plugins in the set order. This order is configured via the `plugins` key of the `LibResilientConfig` variable, usually set via the `config.json` config file.
A minimal default configuration is hard-coded in case no site-specific configuration is provided. This default configuration runs these plugins:
@ -85,17 +85,19 @@ A minimal default configuration is hard-coded in case no site-specific configura
A more robust configuration could look like this:
```javascript
self.LibResilientConfig.plugins = [{
name: 'fetch'
},{
name: 'cache'
},{
name: 'alt-fetch',
endpoints: [
'https://fallback-endpoint.example.com'
]}
}]
```json
{
"plugins": [{
"name": "fetch"
},{
"name": "cache"
},{
"name": "alt-fetch",
"endpoints": [
"https://fallback-endpoint.example.com"
]}
}]
}
```
For each resource, such a config would:

Wyświetl plik

@ -42,30 +42,29 @@ If using `alt-fetch` as the transport pluging, we can rely on the Fetch API impl
The minimal config could in such a case look something like this:
```javascript
self.LibResilientConfig.plugins = [{
name: 'basic-integrity',
// integrity data for certain resources
integrity: {
'/some/image.png': 'sha256-<integrity-data>',
'/index.html': 'sha384-<integrity-data>',
'/css/style.css': 'sha512-<integrity-data>',
'/documents/example.pdf': 'sha384-<integrity-data> sha256-<integrity-data>'
},
// wrapped transport plugin, in this case alt-fetch
uses: [{
name: 'alt-fetch',
// configuring the alternate endpoints plugin to use IPNS gateways
endpoints: [
'https://<CIDv1>.ipns.dweb.link/', // USA
'https://ipfs.kxv.io/ipns/<CIDv0-or-CIDv1>/', // Hong Kong
'https://jorropo.net/ipns/<CIDv0-or-CIDv1>/', // France
'https://gateway.pinata.cloud/ipns/<CIDv0-or-CIDv1>/', // Germany
'https://<CIDv1>.ipns.bluelight.link/' // Singapore
```json
{
"plugins": [{
"name": "basic-integrity",
"integrity": {
"/some/image.png": "sha256-<integrity-data>",
"/index.html": "sha384-<integrity-data>",
"/css/style.css": "sha512-<integrity-data>",
"/documents/example.pdf": "sha384-<integrity-data> sha256-<integrity-data>"
},
"uses": [{
"name": "alt-fetch",
"endpoints": [
"https://<CIDv1>.ipns.dweb.link/",
"https://ipfs.kxv.io/ipns/<CIDv0-or-CIDv1>/",
"https://jorropo.net/ipns/<CIDv0-or-CIDv1>/",
"https://gateway.pinata.cloud/ipns/<CIDv0-or-CIDv1>/",
"https://<CIDv1>.ipns.bluelight.link/"
]
}]
}]
]
}]
}]
}
```
### Scenario 2. `non-fetch`, a hypothetical plugin not based on Fetch API
@ -74,27 +73,24 @@ When *not* using a Fetch API based plugin as the transport pluging, we must expl
Example minimal config:
```javascript
self.LibResilientConfig.plugins = [{
name: 'basic-integrity',
// integrity data for certain resources
integrity: {
'/some/image.png': 'sha256-<integrity-data>',
'/index.html': 'sha384-<integrity-data>',
'/css/style.css': 'sha512-<integrity-data>',
'/documents/example.pdf': 'sha384-<integrity-data> sha256-<integrity-data>'
},
// wrapped integrity-check plugin, ensuring integrity of content
// returned by the transport plugin will be verified
uses: [{
name: 'integrity-check',
uses: [{
// finally, the wrapped transport plugin, in this case not-fetch
name: 'not-fetch',
// any not-fetch related config here
}]
}]
}]
```json
{
"plugins": [{
"name": "basic-integrity",
"integrity": {
"/some/image.png": "sha256-<integrity-data>",
"/index.html": "sha384-<integrity-data>",
"/css/style.css": "sha512-<integrity-data>",
"/documents/example.pdf": "sha384-<integrity-data> sha256-<integrity-data>"
},
"uses": [{
"name": "integrity-check",
"uses": [{
"name": "not-fetch"
}]
}]
}]
}
```
## Integrity data for dynamic resources

Wyświetl plik

@ -499,7 +499,7 @@ if ('serviceWorker' in navigator) {
var serviceWorkerPath = scriptFolder + 'service-worker.js'
self.log('browser-side', 'Service Worker script at: ' + serviceWorkerPath)
// TODO: is there a way to provide config params for the Service Worker here?
// TODO: it would be good if the config.js script could reside outside of the libresilient directory
// TODO: it would be good if the config.json file could reside outside of the libresilient directory
// TODO: https://developer.mozilla.org/en-US/docs/Web/API/ServiceWorkerContainer/register
navigator.serviceWorker.register(serviceWorkerPath, {
// TODO: what is the scope relative to? is it the HTML file that included it, or this script?

Wyświetl plik

@ -5,7 +5,6 @@
/**
* this plugin does not implement any push method
*/
// no polluting of the global namespace please
(function(LRPC){
// this never changes
@ -45,6 +44,7 @@
* getting content using the wrapped plugin, but providing integrity data
*/
let fetchContent = (url, init={}) => {
LR.log(pluginName, `handling: ${url}`)
// integrity data
// a string, where we will combine integrity data from init

Wyświetl plik

@ -34,7 +34,7 @@ if (typeof window === 'undefined') {
let defaultConfig = {
// Gun nodes to use
gunNodes: ['https://gunjs.herokuapp.com/gun'],
// the pubkey of the preconfigured Gun user; always needs to be set in config.js
// the pubkey of the preconfigured Gun user; always needs to be set in config.json
gunPubkey: null,
// the IPFS gateway we're using for verification when publishing; default is usually ok
ipfsGateway: 'https://gateway.ipfs.io'

Wyświetl plik

@ -26,7 +26,7 @@
// sane defaults
let defaultConfig = {
// the pubkey of the preconfigured IPNS node; always needs to be set in config.js
// the pubkey of the preconfigured IPNS node; always needs to be set in config.json
ipnsPubkey: null,
// the IPFS gateway we're using for verification when publishing; default is usually ok
ipfsGateway: 'https://gateway.ipfs.io'

Wyświetl plik

@ -39,7 +39,7 @@ if (!Array.isArray(self.LibResilientPlugins)) {
// initialize the LibResilientConfig array
//
// this also sets some sane defaults,
// which then can be modified via config.js
// which then can be modified via config.json
if (typeof self.LibResilientConfig !== 'object' || self.LibResilientConfig === null) {
self.LibResilientConfig = {
// how long do we wait before we decide that a plugin is unresponsive,
@ -83,129 +83,181 @@ if (typeof self.LibResilientConfig !== 'object' || self.LibResilientConfig === n
* items - the rest of arguments will be passed to console.debug()
*/
self.log = function(component, ...items) {
if (self.LibResilientConfig.loggedComponents.indexOf(component) >= 0) {
console.debug(`LibResilient [COMMIT_UNKNOWN, ${component}] ::`, ...items)
if ( ('LibResilientConfig' in self) && ('loggedComponents' in self.LibResilientConfig) && (self.LibResilientConfig.loggedComponents != undefined)) {
if (self.LibResilientConfig.loggedComponents.indexOf(component) >= 0) {
console.debug(`LibResilient [COMMIT_UNKNOWN, ${component}] ::`, ...items)
}
}
}
/**
* verifying a config JSON
*
* cdata - config data to verify
*/
let verifyConfigData = (cdata) => {
// 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')
return false;
}
// basic check for the loggedComponents
if ( !("loggedComponents" in cdata) || !Array.isArray(cdata.loggedComponents) ) {
self.log('service-worker', 'fetched config does not contain a valid "loggedComponents" field')
return false;
}
// defaultPluginTimeout optional
if ("defaultPluginTimeout" in cdata) {
if (!Number.isInteger(cdata.defaultPluginTimeout)) {
self.log('service-worker', 'fetched config contains invalid "defaultPluginTimeout" data (integer expected)')
return false;
}
}
// we're good
return true;
}
// load the plugins
//
// everything in a try-catch block
// so that we get an informative message if there's an error
try {
// get the config
//
// self.registration.scope contains the scope this service worker is registered for
// so it makes sense to pull config from `config.js` file directly under that location
//
// TODO: providing config directly from browser-side control script via postMessage?
// TODO: `updateViaCache=imports` allows at least config.js to be updated using the cache plugin?
let initServiceWorker = async () => {
try {
self.importScripts(self.registration.scope + "config.js")
self.log('service-worker', 'config loaded.')
} catch (e) {
self.log('service-worker', 'config loading failed, using defaults')
}
// create the LibResilientPluginConstructors map
// the global... hack is here so that we can run tests; not the most elegant
// TODO: find a better way
var LibResilientPluginConstructors = self.LibResilientPluginConstructors || new Map()
// this is the stash for plugins that need dependencies instantiated first
var dependentPlugins = new Array()
// only now load the plugins (config.js could have changed the defaults)
while (self.LibResilientConfig.plugins.length > 0) {
// get the first plugin config from the array
let pluginConfig = self.LibResilientConfig.plugins.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}.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`)
// move the dependency plugin configs to LibResilientConfig to be worked on next
for (var 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
self.LibResilientConfig.plugins.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 {
// 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
// if pluginConfig is not false, rinse-repeat the plugin instantiation steps
// since we are dealing with the last element of dependentPlugins
} while (pluginConfig !== false)
}
// inform
self.log('service-worker', `DEBUG: Strategy in use: ${self.LibResilientPlugins.map(p=>p.name).join(', ')}`)
} catch(e) {
// we only get a cryptic "Error while registering a service worker"
// unless we explicitly print the errors out in the console
console.error(e)
throw e
// get the config
//
// self.registration.scope contains the scope this service worker is registered for
// so it makes sense to pull config from `config.json` file directly under that location
//
// TODO: providing config directly from browser-side control script via postMessage?
// TODO: `updateViaCache=imports` allows at least config.json to be updated using the cache plugin?
try {
//self.importScripts(self.registration.scope + "config.json")
var cdata = await fetch(self.registration.scope + "config.json")
if (cdata.status != 200) {
self.log('service-worker', `failed to fetch config (${cdata.status} ${cdata.statusText}).`)
} else {
cdata = await cdata.json()
if (verifyConfigData(cdata)) {
self.LibResilientConfig.plugins = cdata.plugins
self.LibResilientConfig.loggedComponents = cdata.loggedComponents
if ("defaultPluginTimeout" in cdata) {
self.LibResilientConfig.defaultPluginTimeout = cdata.defaultPluginTimeout
}
self.log('service-worker', 'config loaded.')
} else {
self.log('service-worker', 'ignoring invalid config, using defaults.')
}
}
} catch (e) {
self.log('service-worker', 'config loading failed, using defaults; error:', e)
}
// create the LibResilientPluginConstructors map
// the global... hack is here so that we can run tests; not the most elegant
// TODO: find a better way
self.LibResilientPluginConstructors = self.LibResilientPluginConstructors || new Map()
// copy of the plugins config
// we need to work on it so that self.LibResilientConfig.plugins remains unmodified
// in case we need it later (for example, when re-loading the config)
var pluginsConfig = [...self.LibResilientConfig.plugins]
// this is the stash for plugins that need dependencies instantiated first
var dependentPlugins = new Array()
// only now load the plugins (config.json could have changed the defaults)
while (pluginsConfig.length > 0) {
// 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}.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`)
// move the dependency plugin configs to LibResilientConfig to be worked on next
for (var 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 {
// 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
// if pluginConfig is not false, rinse-repeat the plugin instantiation steps
// since we are dealing with the last element of dependentPlugins
} while (pluginConfig !== false)
}
// inform
self.log('service-worker', `DEBUG: Strategy in use: ${self.LibResilientPlugins.map(p=>p.name).join(', ')}`)
} catch(e) {
// we only get a cryptic "Error while registering a service worker"
// unless we explicitly print the errors out in the console
console.error(e)
throw e
}
}
/**
@ -439,7 +491,6 @@ let initFromRequest = (req) => {
* reqInfo - instance of LibResilientResourceInfo
*/
let libresilientFetch = (plugin, url, init, reqInfo) => {
// status of the plugin
reqInfo.update({
method: plugin.name,
@ -487,19 +538,14 @@ let callOnLibResilientPlugin = (call, args) => {
* and returns a Promise resolving to a Response in case any of the plugins
* was able to get the resource
*
* request - string containing the URL we want to fetch
* url - the url we want to fetch
* init - the init data for responses
* clientId - string containing the clientId of the requesting client
* useStashed - use stashed resources; if false, only pull resources from live sources
* doStash - stash resources once fetched successfully; if false, do not stash pulled resources automagically
* stashedResponse - TBD
*/
let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStash=true, stashedResponse=null) => {
// clean the URL, removing any fragment identifier
var url = request.url.replace(/#.+$/, '');
// get the init object from Request
var init = initFromRequest(request)
let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doStash=true, stashedResponse=null) => {
// set-up reqInfo for the fetch event
var reqInfo = new LibResilientResourceInfo(url, clientId)
@ -569,8 +615,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)=>{
getResourceThroughLibResilient(url, init, clientId, false, true, response.clone()).catch((e)=>{
self.log('service-worker', 'background no-stashed fetch failed for:', url);
})
// return the response so that stuff can keep happening
@ -671,12 +716,11 @@ let getResourceThroughLibResilient = (request, clientId, useStashed=true, doStas
/* ========================================================================= *\
|* === Setting up the event handlers === *|
\* ========================================================================= */
self.addEventListener('install', event => {
self.addEventListener('install', async (event) => {
event.waitUntil(initServiceWorker())
// TODO: Might we want to have a local cache?
// "COMMIT_UNKNOWN" will be replaced with commit ID
self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
// TODO: should we do some plugin initialization here?
});
self.addEventListener('activate', event => {
@ -738,10 +782,16 @@ self.addEventListener('fetch', event => {
if (event.request.method !== 'GET') {
return void event.respondWith(fetch(event.request));
}
// clean the URL, removing any fragment identifier
var url = event.request.url.replace(/#.+$/, '');
// get the init object from Request
var init = initFromRequest(event.request)
// GET requests to our own domain that are *not* #libresilient-info requests
// get handled by plugins in case of an error
return void event.respondWith(getResourceThroughLibResilient(event.request, clientId))
return void event.respondWith(getResourceThroughLibResilient(url, init, clientId))
});