kopia lustrzana https://gitlab.com/rysiekpl/libresilient
Merge branch 'wip-json-config' into 'master'
use JSON config (instead of JavaScript) Closes #31 See merge request rysiekpl/libresilient!11merge-requests/12/merge
commit
387009782e
|
@ -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"]
|
||||
}
|
|
@ -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'))
|
||||
})
|
||||
|
||||
|
|
|
@ -0,0 +1,3 @@
|
|||
{
|
||||
"test": true
|
||||
}
|
|
@ -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']
|
|
@ -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"]
|
||||
}
|
|
@ -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:
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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?
|
||||
|
|
|
@ -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
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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'
|
||||
|
|
|
@ -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))
|
||||
});
|
||||
|
||||
|
||||
|
|
Ładowanie…
Reference in New Issue