Improved user experience

merge-requests/23/merge
Michał "rysiek" Woźniak 2024-02-23 17:58:45 +00:00
rodzic dd1ef475e8
commit e85d77bfe5
10 zmienionych plików z 976 dodań i 112 usunięć

Wyświetl plik

@ -1,7 +1,17 @@
{
"defaultPluginTimeout": 4500,
"stillLoadingTimeout": 1,
"plugins": [{
"name": "test-plugin"
"name": "cache"
},{
"name": "delay",
"delay": [
["test.html$", 7000]
],
"defaultDelay": 5000,
"uses": [{
"name": "fetch"
}]
}],
"useMimeSniffingLibrary": true,
"loggedComponents": ["service-worker", "test-plugin"]
"loggedComponents": ["service-worker", "fetch", "delay"]
}

Wyświetl plik

@ -10,7 +10,8 @@ import {
assert,
assertThrows,
assertRejects,
assertEquals
assertEquals,
assertNotEquals
} from "https://deno.land/std@0.183.0/testing/asserts.ts";
import {
@ -32,10 +33,15 @@ class FetchEvent extends Event {
if (request.indexOf('http') != 0) {
request = window.location.origin + request
}
if (init == null) {
if (init === null) {
request = new Request(request)
} else {
request = new Request(request, init)
// for some reason setting the mode in the init object just doesn't work
// we need to go manual
if ('mode' in init) {
request.mode = init.mode
}
}
}
this.request = request
@ -69,7 +75,7 @@ beforeAll(async ()=>{
}
}
// get a Promise resolvint to a mocked Response object built based on supplied data
// get a Promise resolving to a mocked Response object built based on supplied data
window.getMockedResponse = (url, init, response_data={}) => {
let rdata = {
...responseMockedData,
@ -226,11 +232,18 @@ beforeAll(async ()=>{
postMessage: window.clients.prototypePostMessage
}
},
claim: async () => {
return undefined
},
// the actual spy function must be possible to reference
// but we want spy data per test, so we set it properly in beforeEach()
prototypePostMessage: null
}
window.skipWaiting = async () => {
return undefined;
}
// we need to be able to reliably wait for SW installation
// which is triggered by an "install" Event
window.sw_install_ran = false
@ -445,6 +458,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -478,6 +492,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -497,6 +512,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -517,6 +533,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -554,6 +571,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -574,6 +592,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -594,6 +613,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -614,6 +634,28 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
assertEquals(self.LibResilientConfig.useMimeSniffingLibrary, false)
assertSpyCalls(self.fetch, 1)
})
it("should use default LibResilientConfig values when 'stillLoadingTimeout' field in config.json contains an invalid value", async () => {
let mock_response_data = {
data: JSON.stringify({loggedComponents: ['service-worker', 'fetch'], plugins: [{name: "fetch"}], defaultPluginTimeout: 5000, stillLoadingTimeout: 'not an integer'})
}
window.fetch = spy(window.getMockedFetch(mock_response_data))
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -634,6 +676,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -654,6 +697,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 10000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 5000)
assertEquals(self.LibResilientConfig.plugins, [{name: "fetch"},{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'fetch', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, true)
@ -663,7 +707,7 @@ describe('service-worker', async () => {
it("should use config values from a valid fetched config.json file, caching it in both caches (v1, v1:verified)", async () => {
let mock_response_data = {
data: JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000, normalizeQueryParams: false, useMimeSniffingLibrary: true})
data: JSON.stringify({loggedComponents: ['service-worker', 'cache'], plugins: [{name: "cache"}], defaultPluginTimeout: 5000, stillLoadingTimeout: 1000, normalizeQueryParams: false, useMimeSniffingLibrary: true})
}
window.fetch = spy(window.getMockedFetch(mock_response_data))
@ -673,6 +717,7 @@ describe('service-worker', async () => {
assertEquals(typeof self.LibResilientConfig, "object")
assertEquals(self.LibResilientConfig.defaultPluginTimeout, 5000)
assertEquals(self.LibResilientConfig.stillLoadingTimeout, 1000)
assertEquals(self.LibResilientConfig.plugins, [{name: "cache"}])
assertEquals(self.LibResilientConfig.loggedComponents, ['service-worker', 'cache'])
assertEquals(self.LibResilientConfig.normalizeQueryParams, false)
@ -1906,7 +1951,7 @@ describe('service-worker', async () => {
assertSpyCalls(resolvingFetch2, 1)
assertSpyCall(
window.clients.prototypePostMessage,
6,
8,
{ args: [{
url: "https://test.resilient.is/test.json",
fetchedDiffers: true
@ -2614,4 +2659,227 @@ describe('service-worker', async () => {
assertEquals(await self.guessMimeType("no-such-extension", "test arg 2"), "")
assertSpyCalls(window.fileType.fileTypeFromBuffer, 1)
})
// ========================================================================
// ========================================================================
// ========================================================================
it("should use the still-loading screen when handling a navigation request with a stashing plugin configured and enabled and stillLoadingTimeout set to a positive integer", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'delayed-resolve'
},{
name: "mock-stash"
}],
defaultPluginTimeout: 100,
stillLoadingTimeout: 1,
loggedComponents: ['service-worker']
}
window.LibResilientPluginConstructors.set('delayed-resolve', ()=>{
return {
name: 'delayed-resolve',
description: 'Resolve all requests with a delay.',
version: '0.0.1',
fetch: async (url, init) => {
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
return fetch(url, init)
}
}
})
window.LibResilientPluginConstructors.set('mock-stash', ()=>{
return {
name: 'mock-stash',
description: 'No-op mock stashing plugin.',
version: '0.0.1',
fetch: a=>Promise.resolve(a),
stash: a=>Promise.resolve(a)
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json', {mode: "navigate"})
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
assertEquals((await response.text()).slice(0, 58), '<!DOCTYPE html><html><head><title>Still loading...</title>')
})
it("should not use the still-loading screen when handling a regular request, even if a stashing plugin is configured and enabled and stillLoadingTimeout set to a positive integer", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'delayed-resolve'
},{
name: "mock-stash"
}],
defaultPluginTimeout: 100,
stillLoadingTimeout: 1,
loggedComponents: ['service-worker']
}
window.LibResilientPluginConstructors.set('delayed-resolve', ()=>{
return {
name: 'delayed-resolve',
description: 'Resolve all requests with a delay.',
version: '0.0.1',
fetch: async (url, init) => {
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
return fetch(url, init)
}
}
})
window.LibResilientPluginConstructors.set('mock-stash', ()=>{
return {
name: 'mock-stash',
description: 'No-op mock stashing plugin.',
version: '0.0.1',
fetch: a=>Promise.resolve(a),
stash: a=>Promise.resolve(a)
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json')
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
assertEquals(await response.json(), { test: "success" })
})
it("should not use the still-loading screen when handling a navigation request, even if a stashing plugin is configured and enabled, but stillLoadingTimeout is not set to a positive integer", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'delayed-resolve'
},{
name: "mock-stash"
}],
defaultPluginTimeout: 100,
stillLoadingTimeout: 0,
loggedComponents: ['service-worker']
}
window.LibResilientPluginConstructors.set('delayed-resolve', ()=>{
return {
name: 'delayed-resolve',
description: 'Resolve all requests with a delay.',
version: '0.0.1',
fetch: async (url, init) => {
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
return fetch(url, init)
}
}
})
window.LibResilientPluginConstructors.set('mock-stash', ()=>{
return {
name: 'mock-stash',
description: 'No-op mock stashing plugin.',
version: '0.0.1',
fetch: a=>Promise.resolve(a),
stash: a=>Promise.resolve(a)
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json', {mode: "navigate"})
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
assertEquals(await response.json(), { test: "success" })
})
it("should not use the still-loading screen when handling a navigation request when a stashing plugin is not configured, even though stillLoadingTimeout is set to a positive integer", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'delayed-resolve'
}],
defaultPluginTimeout: 100,
stillLoadingTimeout: 1,
loggedComponents: ['service-worker']
}
window.LibResilientPluginConstructors.set('delayed-resolve', ()=>{
return {
name: 'delayed-resolve',
description: 'Resolve all requests with a delay.',
version: '0.0.1',
fetch: async (url, init) => {
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
return fetch(url, init)
}
}
})
window.LibResilientPluginConstructors.set('mock-stash', ()=>{
return {
name: 'mock-stash',
description: 'No-op mock stashing plugin.',
version: '0.0.1',
fetch: a=>Promise.resolve(a),
stash: a=>Promise.resolve(a)
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json', {mode: "navigate"})
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
assertEquals(await response.json(), { test: "success" })
})
it("should not use the still-loading screen when handling a navigation request when a stashing plugin is configured but not enabled, even though stillLoadingTimeout is set to a positive integer", async () => {
self.LibResilientConfig = {
plugins: [{
name: 'delayed-resolve'
},{
name: "mock-stash",
enabled: false
}],
defaultPluginTimeout: 100,
stillLoadingTimeout: 1,
loggedComponents: ['service-worker']
}
window.LibResilientPluginConstructors.set('delayed-resolve', ()=>{
return {
name: 'delayed-resolve',
description: 'Resolve all requests with a delay.',
version: '0.0.1',
fetch: async (url, init) => {
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
return fetch(url, init)
}
}
})
window.LibResilientPluginConstructors.set('mock-stash', ()=>{
return {
name: 'mock-stash',
description: 'No-op mock stashing plugin.',
version: '0.0.1',
fetch: a=>Promise.resolve(a),
stash: a=>Promise.resolve(a)
}
})
await import("../../service-worker.js?" + window.test_id);
await self.dispatchEvent(new Event('install'))
await self.waitForSWInstall()
let fetch_event = new FetchEvent(window.location.origin + 'test.json', {mode: "navigate"})
window.dispatchEvent(fetch_event)
let response = await fetch_event.waitForResponse()
await new Promise(resolve => setTimeout(resolve, (self.LibResilientConfig.stillLoadingTimeout + 50)))
assertEquals(await response.json(), { test: "success" })
})
})

Wyświetl plik

@ -155,10 +155,10 @@ LibResilient information is kept per-request in the Service Worker, meaning it i
The data provided (per each requested URL handled by the Service Worker) is:
- `clientId` &ndash; the [Client ID](https://developer.mozilla.org/en-US/docs/Web/API/FetchEvent/clientId) for the request (that is, the Client ID of this browser window)
- `url` &ndash; the URL of the request
- `Service Worker` &ndash; the commit SHA of the Service Worker that handled the request
- `fetchError` &ndash; `null` if the request completed successfully via regular HTTPS; otherwise the error message
- `method` &ndash; the method by which the request was completed: "`fetch`" is regular HTTPS `fetch()`, `gun-ipfs` means Gun and IPFS were used, etc.
- `state` &ndash; the state of the request (`running`, `error`, `success`)
- `serviceWorker` &ndash; the commit SHA of the Service Worker that handled the request
- `lastError` &ndash; the last error message emitted from any plugin
- `method` &ndash; the name of the plugin by which the request was completed
- `state` &ndash; the state of the request (`running`, `failed`, `success`)
The code in the browser window context is responsible for keeping a more permanent record of the URLs requested, the methods used, and the status of each, if needed.
@ -195,3 +195,27 @@ There are two levels of cache of the `config.json` file employed here: "*regular
Whenever a configuration file is successfully loaded and applied, it gets saved to the "*verified*" cache, so that it is available as a known-good fall-back in the future. After the `config.json` file is loaded and applied, if it was loaded from any of the caches it is checked for staleness. If it is stale (older than 24h), a an attempt is made to retrieve a newer version through the currently configured plugins. If it succeeds and the retrieved `config.json` passes verification, it is cached in the "*regular*" cache, to be used next time the service worker i initialized.
This verification involves checking the syntax of the file and if it contains all the necessary fields. If the file was retrieved using means other than regular `fetch()`, it is *also* checked in case it requires any plugins whose code has not been loaded in the currently deployed service worker. If it does, it is discarded — the Service Workers API specifies that code loaded by the service worker can *only* come from the original domain; if the config file was loaded using some other means, it might not be possible to load the necessary plugin code when initializing the service worker later.
## Still-loading screen
Depending on the plugin configuration, some requests can take a long time -- even tens of seconds. This is especially true for plugins using non-standard transports, like IPFS.
When requests that take that long, user experience suffers. Visitors have no clue if something went wrong and the request is just hanging there, or if they should just wait a bit longer. Browsers will at some point time out the request and just display a generic error screen.
To improve this user experience, the LibResilient service worker implements a "still-loading" screen for navigate requests. Navigate requests are requests that the browser understands as meaning to navigate between two different pages (in code this means that the [`Request` object has `mode` property set to `navigate`](https://developer.mozilla.org/en-US/docs/Web/API/Request/mode)).
Navigate requests are meant to return a resource that is directly displayed to the visitor. So, the still-loading screen will not be returned in case of `fetch()` requests for some HTML parts to inject in the page, or requests for style sheets, images, scripts, etc., that are to be used in an already displayed page. But it will be displayed when a visitor navigates to a resource to display it in their browser window and the request is taking too long -- even if that resource is a style sheet, script, or image.
If a navigate request takes too long (longer than `stillLoadingTimeout` configuration setting, to be precise, which by default is set to 5000ms), and there is a stashing plugin configured and enabled, the service worker will return a hard-coded "Still loading..." HTML page, with a simple throbber and attempts counter to indicate things are still happening in the background. It also contains a short explainer text and a link for the user to click if they think the request is taking too long.
That still-loading screen is listening to the messages from the service worker (sent using the [`Client.postMessage()`](https://developer.mozilla.org/en-US/docs/Web/API/Client/postMessage) API call). When the service worker indicates that content is ready, the page will automatically reload to display it. If instead the service worker indicates a final failure of the request, the text on the still-loading screen is modified to reflect that.
The still-loading screen is *only* displayed when *all* of these conditions are met:
1. the `stillLoadingTimeout` is set to number greater than zero;
1. there is at least one stashing plugin (normally, `cache`) configured and enabled;
1. the request in question is a navigation request;
1. the request is taking longer than `stillLoadingTimeout`.
The reason why a stashing plugin needs to be configured and enabled is to avoid loops. Consider a scenario, where a visitor is navigating to a page, and the request is taking very long. The still-loading screen is displayed (by way of the service worker returning the relevant HTML in response to the request). Eventually, the request completes in the background, but the response is discarded due to lack of a stashing plugin.
In such a case reloading the page will cause a repeat: request, still-loading screen, request completes in the background (and the result is discarded). The visitor would be stuck in a loop. If a stashing plugin (like `cache`) is enabled, this loop can be expected not to emerge, since the second request would quickly return the cached response.

Wyświetl plik

@ -0,0 +1,12 @@
# Plugin: `delay`
- **status**: beta
- **type**: wrapping
This plugin wraps a plugin, and delays returning the response from it by configurable amount.
## Configuration:
TBD

Wyświetl plik

@ -0,0 +1,126 @@
import {
describe,
it,
afterEach,
beforeEach
} from "https://deno.land/std@0.183.0/testing/bdd.ts";
import {
assert,
assertRejects,
assertEquals
} from "https://deno.land/std@0.183.0/testing/asserts.ts";
import {
assertSpyCall,
assertSpyCalls,
spy,
} from "https://deno.land/std@0.183.0/testing/mock.ts";
beforeEach(()=>{
window.fetch = spy(window.resolvingFetch)
})
afterEach(()=>{
window.fetch = null
})
describe('browser: fetch plugin', async () => {
window.LibResilientPluginConstructors = new Map()
window.LR = {
log: (component, ...items)=>{
console.debug(component + ' :: ', ...items)
}
}
window.resolvingFetch = (url, init) => {
return Promise.resolve(
new Response(
new Blob(
[JSON.stringify({ test: "success" })],
{type: "application/json"}
),
{
status: 200,
statusText: "OK",
headers: {
'ETag': 'TestingETagHeader'
}
}
)
)
}
window.fetch = null
await import("../../../plugins/fetch/index.js");
it("should register in LibResilientPluginConstructors", () => {
assertEquals(LibResilientPluginConstructors.get('fetch')(LR).name, 'fetch');
});
it("should return data from fetch()", async () => {
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
assertSpyCalls(fetch, 1);
assertEquals(await response.json(), {test: "success"})
});
it("should pass the Request() init data to fetch()", async () => {
var initTest = {
method: "GET",
headers: new Headers({"x-stub": "STUB"}),
mode: "mode-stub",
credentials: "credentials-stub",
cache: "cache-stub",
referrer: "referrer-stub",
redirect: "redirect-stub",
integrity: "integrity-stub"
}
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json', initTest);
assertSpyCall(
fetch,
0,
{
args: [
'https://resilient.is/test.json',
initTest // TODO: does the initTest actually properly work here?
]
})
assertEquals(await response.json(), {test: "success"})
});
it("should set the LibResilient headers", async () => {
const response = await LibResilientPluginConstructors.get('fetch')(LR).fetch('https://resilient.is/test.json');
assertSpyCalls(fetch, 1);
assertEquals(await response.json(), {test: "success"})
assertEquals(response.headers.has('X-LibResilient-Method'), true)
assertEquals(response.headers.get('X-LibResilient-Method'), 'fetch')
assertEquals(response.headers.has('X-LibResilient-Etag'), true)
assertEquals(response.headers.get('X-LibResilient-ETag'), 'TestingETagHeader')
});
it("should throw an error when HTTP status is >= 400", async () => {
window.fetch = (url, init) => {
const response = new Response(
new Blob(
["Not Found"],
{type: "text/plain"}
),
{
status: 404,
statusText: "Not Found",
url: url
});
return Promise.resolve(response);
}
assertRejects(
async () => {
return await LibResilientPluginConstructors
.get('fetch')(LR)
.fetch('https://resilient.is/test.json') },
Error,
'HTTP Error: 404 Not Found'
)
});
})

Wyświetl plik

@ -0,0 +1,81 @@
/* ========================================================================= *\
|* === Delay plugin === *|
\* ========================================================================= */
/**
* this plugin does not implement any push method
*/
// no polluting of the global namespace please
(function(LRPC){
// this never changes
const pluginName = "delay"
LRPC.set(pluginName, (LR, init={})=>{
/*
* plugin config settings
*/
// sane defaults
let defaultConfig = {
// array of two-element arrays
// ["/regex_match/", <slowdown in ms>]
// first match wins
delay: [],
// default delay, in ms
defaultDelay: 1000,
// plugin to wrap, regular fetch by default
uses: [{
name: "fetch"
}]
}
// merge the defaults with settings from init
let config = {...defaultConfig, ...init}
/**
* getting content using regular HTTP(S) fetch()
*/
let fetchContent = (url, init={}) => {
LR.log(pluginName, `delayed retrieval: ${url}`)
// we really want to make fetch happen, Regina!
// TODO: this change should *probably* be handled on the Service Worker level
init.cache = 'reload'
// establish the default delay
let impose_delay = config.defaultDelay
LR.log(pluginName, `default delay: ${impose_delay}`)
// see if we have any specific delay rule that matches
// first match wins
for (sp of config.delay) {
LR.log(pluginName, `checking delay rule: ${sp}`)
let re = new RegExp(sp[0])
if (url.search(re) > -1) {
LR.log(pluginName, `delay rule matched: ${sp[0]}, delay set to: ${sp[1]}`)
impose_delay = sp[1]
break;
}
}
// wait a bit and run the first wrapped plugin's fetch()
return promiseTimeout(impose_delay, true)[0]
.then(() => {
return config.uses[0].fetch(url, init)
})
}
// return the plugin
return {
name: pluginName,
description: 'Configurable delay!',
version: 'COMMIT_UNKNOWN',
fetch: fetchContent,
uses: config.uses
}
})
// done with not polluting the global namespace
})(LibResilientPluginConstructors)

Wyświetl plik

@ -0,0 +1,4 @@
# Plugin: `error`
- **status**: alpha
- **type**: debugging plugin, used during development and testing

Wyświetl plik

@ -0,0 +1,18 @@
<!DOCTYPE html>
<html>
<head>
<script>
self.LibResilientPluginConstructors = self.LibResilientPluginConstructors || new Map()
self.log = console.log
</script>
<script src="./index.js"></script>
<script>
var theplugin = LibResilientPluginConstructors.get('fetch')(self)
</script>
</head>
<body>
<h1><code>error</code></h1>
<p>This is a simple debugging harness for the <code>error</code> plugin of LibResilient.</p>
<p>The plugin should now have been initialized in the <code>theplugin</code> global variable. Open your browser's JavaScript console to start playing with it.</p>
</body>
</html>

Wyświetl plik

@ -0,0 +1,76 @@
/* ========================================================================= *\
|* === Error plugin === *|
\* ========================================================================= */
/**
* this plugin is just a debugging plugin to be used in testing configurations
* when an erroring-out plugin is needed
*/
// no polluting of the global namespace please
(function(LRPC){
// this never changes
const pluginName = "error"
LRPC.set(pluginName, (LR, init={})=>{
/*
* plugin config settings
*/
// sane defaults
let defaultConfig = {
// type can be "exception" or "http"
type: "http",
// only valid if type: http
code: 500,
// valid either way
message: "Internal server error"
}
// merge the defaults with settings from init
let config = {...defaultConfig, ...init}
/**
* getting content using regular HTTP(S) fetch()
*/
let errorOut = (url, init={}) => {
// exception?
if (config.type !== "http") {
LR.log(pluginName, `erroring out for: ${url} — exception`)
throw new Error(config.message)
}
LR.log(pluginName, `erroring out for: ${url} — HTTP error`)
// I guess we want a HTTP error then
var responseInit = {
status: config.code,
statusText: config.message,
headers: {},
url: url
};
responseInit.headers['Content-Type'] = "text/plain"
let blob = new Blob(
[config.message],
{type: "text/plain"}
)
// shouldn't this be a Promise though?
return Promise.resolve(new Response(
blob,
responseInit
))
}
// return the plugin
return {
name: pluginName,
description: 'Errors, errors everywhere',
version: 'COMMIT_UNKNOWN',
fetch: errorOut
}
})
// done with not polluting the global namespace
})(LibResilientPluginConstructors)

Wyświetl plik

@ -12,6 +12,14 @@ if (typeof self.LibResilientConfig !== 'object' || self.LibResilientConfig === n
// how long do we wait before we decide that a plugin is unresponsive,
// and move on?
defaultPluginTimeout: 10000,
// how long should LibResilient wait before displaying the "still loading" screen
// to the user if the request mode is "navigate"?
//
// NOTICE: the still-loading screen is only used if this setting is > 0
// NOTICE: *and* there is a stashing plugin (normally, `cache`) configured and enabled in the config
// NOTICE: this is done to avoid loops -- otherwise, user would find themselves in a (manual, but still) loop
stillLoadingTimeout: 5000,
// plugins settings namespace
//
@ -217,6 +225,13 @@ let verifyConfigData = (cdata) => {
return false;
}
}
// stillLoadingTimeout is optional
if ("stillLoadingTimeout" in cdata) {
if (!Number.isInteger(cdata.stillLoadingTimeout)) {
self.log('service-worker', 'fetched config contains invalid "stillLoadingTimeout" data (integer expected)')
return false;
}
}
// normalizeQueryParams is optional
if ("normalizeQueryParams" in cdata) {
if (cdata.normalizeQueryParams !== true && cdata.normalizeQueryParams !== false) {
@ -292,6 +307,10 @@ let executeConfig = (config) => {
// this is the stash for plugins that need dependencies instantiated first
let dependentPlugins = new Array()
// do we have any stashing plugins enabled?
// this is important for the still-loading screen
let stashingEnabled = false
// only now load the plugins (config.json could have changed the defaults)
while (pluginsConfig.length > 0) {
@ -346,12 +365,16 @@ let executeConfig = (config) => {
let plugin = LibResilientPluginConstructors.get(pluginConfig.name)(self, pluginConfig)
self.log('service-worker', `${pluginConfig.name}: instantiated`)
// is this a stashing plugin?
// we need at least one stashing plugin to be able to use the still-loading screen
stashingEnabled = stashingEnabled || ( ( "stash" in plugin ) && ( typeof plugin.stash === "function" ) )
// 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`)
self.log('service-worker', `${pluginConfig.name}: no dependent plugins left, pushing directly to LibResilientPlugins`)
break;
}
@ -385,7 +408,7 @@ let executeConfig = (config) => {
}
// finally -- do we want to use MIME type guessing based on content?
// do we want to use MIME type guessing based on content?
// dealing with this at the very end so that we know we can safely set detectMimeFromBuffer
// and not need to re-set it back in case anything fails
if (config.useMimeSniffingLibrary === true) {
@ -405,6 +428,13 @@ let executeConfig = (config) => {
}
}
// finally -- if we do not have *any* stashing plugins enabled,
// we need to disable the still-loading screen
if ( ! stashingEnabled ) {
config.stillLoadingTimeout = 0
self.log('service-worker', 'still-loading screen disabled, as there are no stashing plugins enabled')
}
// we're good!
return true;
@ -693,19 +723,10 @@ let decrementActiveFetches = (clientId) => {
// client has to be smart enough to know if that is just temporary
// (and new fetches will fire in a moment, because a CSS file just
// got fetched) or not
self.clients.get(clientId).then((client)=>{
if (client !== null) {
try {
client.postMessage({
allFetched: true
})
} catch(err) {
self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`)
}
}
})
.then(()=>{
self.log('service-worker', 'all-fetched message sent.')
postMessage(clientId, {
allFetched: true
}).then(()=>{
self.log('service-worker', 'all-fetched message queued.')
})
}
}
@ -744,7 +765,97 @@ let promiseTimeout = (time, timeout_resolves=false, error_message=false) => {
/* ========================================================================= *\
|* === LibResilientResourceInfo === *|
|* === LibResilientClient === *|
\* ========================================================================= */
/**
* Libresilient client class
*
* handles communication with a client
*
* TODO: track active fetches as part of this class?
* TODO: https://gitlab.com/rysiekpl/libresilient/-/issues/83
*/
let LibResilientClient = class {
async postMessage(message) {
// log
self.log('service-worker', 'postMessage():', JSON.stringify(message))
// add our message to the message queue
this.messageQueue.push(message)
// try to get the client from Client API based on clientId
if (! this.client) {
this.client = await self
.clients
.get(this.clientId)
}
// now, we might still not have a valid client here
if (! this.client) {
// store it for later, when we do get a valid client
self.log('service-worker', `postMessage(): no valid client for id: ${this.clientId}, added message to the queue`)
// we have a valid client, it seems!
} else {
// we want all messages to be delivered, and in order they were added
// our message is at the end and will get handled in due course
let msg = false
while (msg = this.messageQueue.shift()) {
try {
this.client.postMessage(msg);
} catch (err) {
// if we fail for whatever reason, bail from the loop
self.log('service-worker', `postMessage(): client seems valid, but postMessage failed; message left in the queue\n- Error message: ${err}`)
this.messageQueue.unshift(msg)
break
}
}
}
}
constructor(clientId) {
// we often get the clientId long before
// we are able to get a valid client out of it
//
// so we need to keep both
this.clientId = clientId
this.client = null;
// queued messages for when we have a client available
this.messageQueue = []
}
}
// map of all known clients
let LibResilientClients = new Map()
/**
* getting a client based on clientId and sending a message
* (or queueing it for later if we cannot get a valid client)
*/
let postMessage = async (clientId, message) => {
// do we already have a LibResilientClient instance for that client id?
let client = LibResilientClients.get(clientId)
// if not, create it
if (client === undefined) {
client = new LibResilientClient(clientId)
LibResilientClients.set(clientId, client)
}
// send (or queue) the message
await client.postMessage(message)
}
/* ========================================================================= *\
|* === LibResilientResourceInfo === *|
\* ========================================================================= */
@ -768,34 +879,28 @@ let LibResilientResourceInfo = class {
// actual values of the fields
// only used internally, and stored into the Indexed DB
this.values = {
url: '', // read only after initialization
clientId: null,
fetchError: null,
method: null,
state: null, // can be "error", "success", "running"
url: '', // read only after initialization
clientId: null, // the client on whose behalf that request is being processed
lastError: null, // error from the previous plugin (for state:running) or the last emitted error (for state:failed or state:success)
method: null, // name of the current plugin (in case of state:running) or last plugin (for state:failed or state:success)
state: null, // can be "failed", "success", "running"
serviceWorker: 'COMMIT_UNKNOWN' // this will be replaced by commit sha in CI/CD; read-only
}
this.client = null;
// queued messages for when we have a client available
this.messageQueue = []
// set it
this.values.url = url
this.values.clientId = clientId
// we might not have a non-empty clientId if it's a cross-origin fetch
if (clientId) {
// get the client from Client API based on clientId
self.clients.get(clientId).then((client)=>{
// set the client
this.client = client
// Send a message to the client
if (this.client !== null) {
try {
this.client.postMessage(this.values);
} catch(err) {
self.log("service-worker", `postMessage failed for client: ${this.client}\n- Error message: ${err}`)
}
}
})
postMessage(
clientId,
{...this.values}
)
}
}
@ -813,7 +918,7 @@ let LibResilientResourceInfo = class {
Object
.keys(data)
.filter((k)=>{
return ['fetchError', 'method', 'state'].includes(k)
return ['lastError', 'method', 'state'].includes(k)
})
.forEach((k)=>{
msg += '\n+-- ' + k + ': ' + data[k]
@ -825,20 +930,19 @@ let LibResilientResourceInfo = class {
})
self.log('service-worker', msg)
// send the message to the client
if (this.client && changed && (this.client !== null)) {
try {
this.client.postMessage(this.values);
} catch(err) {
self.log("service-worker", `postMessage failed for client: ${this.client}\n- Error message: ${err}`)
}
if (changed) {
postMessage(
this.values.clientId,
{...this.values}
);
}
}
/**
* fetchError property
* lastError property
*/
get fetchError() {
return this.values.fetchError
get lastError() {
return this.values.lastError
}
/**
@ -923,6 +1027,11 @@ let libresilientFetch = (plugin, url, init, reqInfo) => {
'\n+-- using method(s):', plugin.name
)
// starting the fetch...
// if it errors out immediately, at least we don't have to deal
// with a dangling promise timeout, set up below
let fetch_promise = plugin.fetch(url, init)
let timeout_promise, timeout_id
[timeout_promise, timeout_id] = promiseTimeout(
self.LibResilientConfig.defaultPluginTimeout,
@ -930,27 +1039,24 @@ let libresilientFetch = (plugin, url, init, reqInfo) => {
`LibResilient request using ${plugin.name} timed out after ${self.LibResilientConfig.defaultPluginTimeout}ms.`
)
// race the plugin(s) vs. a timeout
let race_promise = Promise
.race([
plugin.fetch(url, init),
timeout_promise
])
// making sure there are no dangling promises etc
//
// this should happen asynchronously
race_promise
// this has to happen asynchronously
fetch_promise
// make sure the timeout is cancelled as soon as the promise resolves
// we do not want any dangling promises/timeouts after all!
.then(()=>{
.finally(()=>{
clearTimeout(timeout_id)
})
// no-op to make sure we don't end up with dangling rejected premises
.catch((e)=>{})
// return the racing promise
return race_promise;
// race the plugin(s) vs. the timeout
return Promise
.race([
fetch_promise,
timeout_promise
]);
}
@ -1022,8 +1128,7 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
'\n+-- error : ' + error.toString())
// save info in reqInfo -- status of the previous method
reqInfo.update({
state: "error",
fetchError: error.toString()
lastError: error.toString()
})
return libresilientFetch(currentPlugin, url, init, reqInfo)
})
@ -1039,7 +1144,10 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
decrementActiveFetches(clientId)
// record the success
reqInfo.update({state:"success"})
reqInfo.update({
lastError: null,
state:"success"
})
// get the plugin that was used to fetch content
let plugin = self.LibResilientPlugins.find(p=>p.name===reqInfo.method)
@ -1054,9 +1162,14 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
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
getResourceThroughLibResilient(url, init, clientId, false, true, response.clone()).catch((e)=>{
self.log('service-worker', 'background no-stashed fetch failed for:', url);
})
try {
getResourceThroughLibResilient(url, init, clientId, false, true, response.clone())
.catch((e)=>{
self.log('service-worker', 'background no-stashed fetch failed for:', url);
})
} catch(e) {
self.log('service-worker', 'background no-stashed fetch failed for:', url, `\n+-- error: ${e}`);
}
// return the response so that stuff can keep happening
return response
@ -1080,17 +1193,9 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
|| ( stashedResponse.headers.get('X-LibResilient-ETag') !== response.headers.get('X-LibResilient-ETag') ) ) {
// inform!
self.log('service-worker', 'fetched version method or ETag differs from stashed for:', url)
self.clients.get(reqInfo.clientId).then((client)=>{
if (client !== null) {
try {
client.postMessage({
url: url,
fetchedDiffers: true
})
} catch(err) {
self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`)
}
}
postMessage(reqInfo.clientId, {
url: url,
fetchedDiffers: true
})
// TODO: this should probably modify doStash?
}
@ -1134,18 +1239,14 @@ let getResourceThroughLibResilient = (url, init, clientId, useStashed=true, doSt
})
// a final catch... in case all plugins fail
.catch((err)=>{
self.log('service-worker', "LibResilient also failed completely: ", err,
self.log('service-worker', "LibResilient failed completely: ", err,
'\n+-- URL : ' + url)
// cleanup
reqInfo.update({
state: "error",
fetchError: err.toString()
state: "failed",
lastError: err.toString()
})
// this is very naïve and should in fact be handled
// inside the relevant plugin, probably
// TODO: is this even needed?
reqInfo.update({method: null})
decrementActiveFetches(clientId)
// rethrow
throw err
@ -1159,14 +1260,15 @@ self.addEventListener('install', async (event) => {
let init_promise = initServiceWorker()
await event.waitUntil(init_promise)
if (await init_promise === true) {
self.skipWaiting()
// "COMMIT_UNKNOWN" will be replaced with commit ID
self.log('service-worker', "0. Installed LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
self.log('service-worker', "installed LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
}
});
self.addEventListener('activate', async event => {
self.log('service-worker', "1. Activated LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
// TODO: should we do some plugin initialization here?
await event.waitUntil(self.clients.claim())
self.log('service-worker', "activated LibResilient Service Worker (commit: COMMIT_UNKNOWN).");
});
self.addEventListener('fetch', async event => {
@ -1193,21 +1295,11 @@ self.addEventListener('fetch', async event => {
// so let's also send the plugin list, why not
//
// *sigh* JS is great *sigh*
self.clients
.get(clientId)
.then((client)=>{
if (client !== null) {
try {
client.postMessage({
clientId: clientId,
plugins: self.LibResilientPlugins.map((p)=>{return p.name}),
serviceWorker: 'COMMIT_UNKNOWN'
})
} catch(err) {
self.log("service-worker", `postMessage failed for client: ${client}\n- Error message: ${err}`)
}
}
})
postMessage(clientId, {
clientId: clientId,
plugins: self.LibResilientPlugins.map((p)=>{return p.name}),
serviceWorker: 'COMMIT_UNKNOWN'
})
}
// counter!
@ -1218,6 +1310,7 @@ self.addEventListener('fetch', async event => {
// info
self.log('service-worker', "Fetching!",
"\n+-- url :", event.request.url,
"\n+-- mode :", event.request.mode,
"\n+-- clientId :", event.clientId,
"\n+-- resultingClientId:", event.resultingClientId,
"\n +-- activeFetches[" + clientId + "]:", self.activeFetches.get(clientId)
@ -1253,7 +1346,159 @@ self.addEventListener('fetch', async event => {
// GET requests to our own domain that are *not* #libresilient-info requests
// get handled by plugins in case of an error
return getResourceThroughLibResilient(url, init, clientId)
let lrPromise = getResourceThroughLibResilient(url, init, clientId)
// is the stillLoadingScreen enabled, and are we navigating, or just fetching some resource?
if ( ( self.LibResilientConfig.stillLoadingTimeout > 0 ) && ( event.request.mode === 'navigate' ) ) {
self.log('service-worker', `handling a navigate request; still-loading timeout: ${self.LibResilientConfig.stillLoadingTimeout}.`)
let slPromise, slTimeoutId
[slPromise, slTimeoutId] = promiseTimeout(self.LibResilientConfig.stillLoadingTimeout, true)
// make sure to clear the timeout related to slPromise
// in case we manage to get the content through the plugins
lrPromise
.then(()=>{
self.log('service-worker', `content retrieved; still-loading timeout cleared.`)
clearTimeout(slTimeoutId)
})
// return a Promise that races the "still loading" screen promise against the LibResilient plugins
return Promise.race([
// regular fetch-through-plugins
lrPromise,
// the "still loading screen"
//
// this will delay a specified time, and ten return a Response
// with very basic HTML informing the user that the page is still loading,
// a Refresh header set, and a link for the user to reload the screen manually
slPromise
.then(()=>{
// inform
self.log('service-worker', 'handling a navigate request is taking too long, showing the still-loading screen')
// we need to create a new Response object
// with all the headers added explicitly,
// since response.headers is immutable
var responseInit = {
status: 202,
statusText: "Accepted",
headers: {},
url: url
};
responseInit.headers['Content-Type'] = "text/html"
// refresh: we want a minimum of 1s; stillLoadingTimeout is in ms!
//responseInit.headers['Refresh'] = Math.ceil( self.LibResilientConfig.stillLoadingTimeout / 1000 )
//responseInit.headers['ETag'] = ???
//responseInit.headers['X-LibResilient-ETag'] = ???
responseInit.headers['X-LibResilient-Method'] = "still-loading"
// TODO: make this configurable via config.json
// TODO: https://gitlab.com/rysiekpl/libresilient/-/issues/82
let stillLoadingHTML = `<!DOCTYPE html><html><head><title>Still loading...</title></head><body>
<style>
body {
margin: auto;
width: 30em;
margin: 2em auto;
background: #dddddd;
color: black;
font-family: sans-serif;
}
h1 {
height: 1em;
margin-bottom:
0.2em
}
h1 > span {
animation: throbber 2s infinite 0s linear;
font-size: 70%;
position: relative;
top: 0.2em;
}
#throbber1 {
animation-delay: 0s;
}
#throbber2 {
animation-delay: 0.5s;
}
#throbber3 {
animation-delay: 1s;
}
@keyframes throbber {
0% {opacity: 1.0;}
50% {opacity: 0.1;}
100% {opacity: 1.0;}
}
#working {
color: gray;
font-family: monospace;
font-weight: bold;
display: flex;
flex-wrap: nowrap;
justify-content: left;
margin-top: 0em;
}
a {
color: #2d5589
}
</style>
<h1 id="header">Still loading<span id="throbber1">&#x2022;</span><span id="throbber2">&#x2022;</span><span id="throbber3">&#x2022;</span></h1>
<p id="working">attempts:&nbsp;<span id="status">1</span></p>
<p id="text">The content is still being loaded, thank you for your patience.<br/><br/>This page will auto-reload in a few seconds. If it does not, please <a href="./">click here</a>.</p>
<script>
let attempts = 0;
let header = document.getElementById('header')
let text = document.getElementById('text')
let status = document.getElementById('status')
navigator.serviceWorker.addEventListener('message', event => {
if ( event.data.url === window.location.href ) {
if ( event.data.state === 'success' ) {
header.innerHTML = "Loaded, redirecting!"
text.innerHTML = "The content has loaded, you are being redirected."
window.location.reload()
} else if ( event.data.state === 'failed' ) {
header.innerHTML = "Loading failed."
text.innerHTML = "We're sorry, we were unable to load this page."
}
if ( ( 'lastError' in event.data ) && ( typeof event.data.lastError === 'string' ) ) {
attempts += 1;
status.innerHTML = attempts;
}
}
})
</script></body></html>`
let blob = new Blob(
[stillLoadingHTML],
{type: "text/html"}
)
return new Response(
blob,
responseInit
)
})
])
// nope, just fetching a resource
} else {
if ( event.request.mode === 'navigate' ) {
self.log('service-worker', `handling a navigate request, but still-loading screen is disabled.`)
} else {
self.log('service-worker', 'handling a regular request; still-loading screen will not be used.')
}
// no need for the whole "still loading screen" flow
return lrPromise;
}
}())
});