@braidlabs/sw 0.7.0 → 0.8.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +149 -52
- package/package.json +7 -3
- package/src/gateway.d.ts +10 -0
- package/src/gateway.js +10 -0
- package/src/gateway.js.map +1 -0
- package/src/index.d.ts +1 -1
- package/src/index.js +1 -1
- package/src/index.js.map +1 -1
- package/src/lib/asset-handler.d.ts +3 -5
- package/src/lib/asset-handler.js +9 -7
- package/src/lib/asset-handler.js.map +1 -1
- package/src/lib/compose.d.ts +9 -0
- package/src/lib/compose.js +63 -8
- package/src/lib/compose.js.map +1 -1
- package/src/lib/fragment-caches.d.ts +55 -0
- package/src/lib/fragment-caches.js +100 -0
- package/src/lib/fragment-caches.js.map +1 -0
- package/src/lib/gateway.d.ts +92 -0
- package/src/lib/gateway.js +164 -0
- package/src/lib/gateway.js.map +1 -0
- package/src/lib/precache.d.ts +2 -3
- package/src/lib/precache.js +4 -1
- package/src/lib/precache.js.map +1 -1
- package/src/lib/static-routes.d.ts +56 -0
- package/src/lib/static-routes.js +76 -0
- package/src/lib/static-routes.js.map +1 -0
- package/src/lib/worker.d.ts +10 -0
- package/src/lib/worker.js +35 -2
- package/src/lib/worker.js.map +1 -1
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Where a fragment's cached files live: one Cache Storage cache per fragment (`braid-frag:<id>`),
|
|
3
|
+
* or, with `buckets: true` where the browser has Storage Buckets (Chromium), that same cache inside
|
|
4
|
+
* a bucket of the fragment's own.
|
|
5
|
+
*
|
|
6
|
+
* A bucket is the browser's unit of eviction, quota and deletion. In one shared Cache Storage, a
|
|
7
|
+
* device short on space can evict every fragment's files at once, and the deploy-skew save — the
|
|
8
|
+
* chunk an old page still asks for after a deploy — goes with them. Per-fragment buckets are evicted
|
|
9
|
+
* one fragment at a time, and pruning a fragment that left the registry deletes its bucket outright.
|
|
10
|
+
*
|
|
11
|
+
* Opt-in: turning it on moves where files are kept, so a worker that switches starts with empty
|
|
12
|
+
* fragment caches once (pruning removes the old ones).
|
|
13
|
+
*/
|
|
14
|
+
/** The per-fragment cache prefix, unless a worker names its own. */
|
|
15
|
+
export const DEFAULT_CACHE_PREFIX = 'braid-frag';
|
|
16
|
+
/** The cache a fragment's files are kept in, or undefined where there is no cache storage. */
|
|
17
|
+
export async function openFragmentCache(fragmentId, options = {}) {
|
|
18
|
+
const name = `${options.cachePrefix ?? DEFAULT_CACHE_PREFIX}:${fragmentId}`;
|
|
19
|
+
const buckets = bucketsOf(options);
|
|
20
|
+
if (buckets) {
|
|
21
|
+
try {
|
|
22
|
+
return await (await buckets.open(bucketFor(fragmentId, options))).caches.open(name);
|
|
23
|
+
}
|
|
24
|
+
catch {
|
|
25
|
+
// refused (quota policy, an unsupported context): the shared cache storage still works
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
return (options.caches ?? globalThis.caches)?.open(name);
|
|
29
|
+
}
|
|
30
|
+
/**
|
|
31
|
+
* Deletes the fragment buckets this worker's prefix made, except those for `keep` (all of them when
|
|
32
|
+
* null). Only names in this worker's exact format: buckets are the whole origin's, and a looser match
|
|
33
|
+
* would take the realm's database buckets (`braid-db-…`), another worker's, or the app's own.
|
|
34
|
+
*
|
|
35
|
+
* `always` deletes even when `buckets` is off, where the API exists: sign-out must not leave a
|
|
36
|
+
* previous configuration's buckets, which may hold a previous user's offline documents.
|
|
37
|
+
*/
|
|
38
|
+
export async function deleteFragmentBuckets(keep, options = {}, { always = false } = {}) {
|
|
39
|
+
const buckets = always ? bucketManager(options) : bucketsOf(options);
|
|
40
|
+
if (!buckets)
|
|
41
|
+
return [];
|
|
42
|
+
const format = new RegExp(`^${escapeRegExp(bucketPrefix(options))}_[a-z0-9-]{1,25}_[0-9a-f]{8}$`);
|
|
43
|
+
const kept = new Set((keep ?? []).map((id) => bucketFor(id, options)));
|
|
44
|
+
const deleted = [];
|
|
45
|
+
for (const name of await buckets.keys()) {
|
|
46
|
+
if (!format.test(name) || kept.has(name))
|
|
47
|
+
continue;
|
|
48
|
+
await buckets.delete(name);
|
|
49
|
+
deleted.push(name);
|
|
50
|
+
}
|
|
51
|
+
return deleted;
|
|
52
|
+
}
|
|
53
|
+
/** Whether this worker keeps fragment caches in buckets: asked for, and available. */
|
|
54
|
+
export function usesBuckets(options) {
|
|
55
|
+
return bucketsOf(options) !== undefined;
|
|
56
|
+
}
|
|
57
|
+
/** The fragments among `keep` whose bucket exists, so their shared caches are superseded. */
|
|
58
|
+
export async function keptWithBuckets(keep, options) {
|
|
59
|
+
const buckets = bucketsOf(options);
|
|
60
|
+
if (!buckets)
|
|
61
|
+
return [];
|
|
62
|
+
const existing = new Set(await buckets.keys());
|
|
63
|
+
return keep.filter((id) => existing.has(bucketFor(id, options)));
|
|
64
|
+
}
|
|
65
|
+
function bucketsOf(options) {
|
|
66
|
+
return options.buckets ? bucketManager(options) : undefined;
|
|
67
|
+
}
|
|
68
|
+
function bucketManager(options) {
|
|
69
|
+
return options.storageBuckets ?? globalThis.navigator?.storageBuckets;
|
|
70
|
+
}
|
|
71
|
+
function escapeRegExp(text) {
|
|
72
|
+
return text.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
|
|
73
|
+
}
|
|
74
|
+
/**
|
|
75
|
+
* A fragment's bucket name: lowercase letters, digits, `-` and `_`, at most 64 characters, as the
|
|
76
|
+
* API requires. The readable part may collide once folded; the hash of the exact id keeps two
|
|
77
|
+
* fragments apart.
|
|
78
|
+
*/
|
|
79
|
+
export function bucketFor(fragmentId, options = {}) {
|
|
80
|
+
return `${bucketPrefix(options)}_${slug(fragmentId, 25)}_${fnv1a(fragmentId)}`;
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* `braid-sw_` and the cache prefix: a namespace the realm's database buckets (`braid-db_`) never use.
|
|
84
|
+
* Underscores delimit and slugs never contain one, so one prefix's names cannot be read as another's.
|
|
85
|
+
*/
|
|
86
|
+
function bucketPrefix(options) {
|
|
87
|
+
return `braid-sw_${slug(options.cachePrefix ?? DEFAULT_CACHE_PREFIX, 20)}`;
|
|
88
|
+
}
|
|
89
|
+
function slug(text, length) {
|
|
90
|
+
return text.toLowerCase().replace(/[^a-z0-9-]+/g, '-').replace(/^-+|-+$/g, '').slice(0, length) || 'x';
|
|
91
|
+
}
|
|
92
|
+
function fnv1a(text) {
|
|
93
|
+
let hash = 0x811c9dc5;
|
|
94
|
+
for (let i = 0; i < text.length; i++) {
|
|
95
|
+
hash ^= text.charCodeAt(i);
|
|
96
|
+
hash = Math.imul(hash, 0x01000193) >>> 0;
|
|
97
|
+
}
|
|
98
|
+
return hash.toString(16).padStart(8, '0');
|
|
99
|
+
}
|
|
100
|
+
//# sourceMappingURL=fragment-caches.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"fragment-caches.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/fragment-caches.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { type BraidGateway, type GatewayOptions } from '@braidlabs/gateway';
|
|
2
|
+
import { type BraidFetchHandlerOptions } from './asset-handler.js';
|
|
3
|
+
/**
|
|
4
|
+
* The gateway itself, run inside the worker: composition for an origin that has no server to put
|
|
5
|
+
* a gateway on — static hosting, a CDN bucket, a shell someone else deploys.
|
|
6
|
+
*
|
|
7
|
+
* Not a second gateway. The gateway core is runtime-neutral, so this is the same `createGateway`
|
|
8
|
+
* the server runs, given the network as its shell. What this module adds is what a worker needs
|
|
9
|
+
* and a server does not: a decision, made synchronously, about which requests to answer — a
|
|
10
|
+
* `fetch` listener must call `respondWith` before it returns, long before the gateway knows
|
|
11
|
+
* whether a page will pierce.
|
|
12
|
+
*
|
|
13
|
+
* Two things carry over differently from a server, and both are the browser's rules rather than
|
|
14
|
+
* this module's. The worker's `fetch` cannot set forbidden headers, so `Cookie` is not forwarded
|
|
15
|
+
* by name — a same-origin endpoint gets the browser's own cookies, a cross-origin one none, as
|
|
16
|
+
* for the page's own requests. And a cross-origin endpoint must answer CORS at all, because the
|
|
17
|
+
* worker reads the response it pierces.
|
|
18
|
+
*/
|
|
19
|
+
/** The gateway's options, less the one that would have the worker serve a worker. */
|
|
20
|
+
export type BraidGatewayHandlerOptions = Omit<GatewayOptions, 'serviceWorker'>;
|
|
21
|
+
export interface BraidGatewayHandler {
|
|
22
|
+
/**
|
|
23
|
+
* Answers a request the gateway owns, or returns `null` — synchronously — for one it leaves to
|
|
24
|
+
* the browser. Pass the event's `preloadResponse` and a navigation's shell is the preloaded one;
|
|
25
|
+
* a worker that turns preload on should know the browser preloads realm frames too, which this
|
|
26
|
+
* answers itself, so each also reaches the server.
|
|
27
|
+
*/
|
|
28
|
+
(request: Request, preloadResponse?: Promise<Response | undefined>): Promise<Response> | null;
|
|
29
|
+
/**
|
|
30
|
+
* Any request through the gateway, the network behind it. Hand this to {@link braidFetchHandler}
|
|
31
|
+
* as its `fetch` and the skew cache sits in front of the in-worker gateway, as it would sit in
|
|
32
|
+
* front of a server one. Unlike the handler it claims nothing: whatever it is given, it routes.
|
|
33
|
+
*/
|
|
34
|
+
readonly fetch: typeof fetch;
|
|
35
|
+
readonly gateway: BraidGateway;
|
|
36
|
+
/** Whether the handler would claim this request, decided as the handler decides it. */
|
|
37
|
+
claims(request: Request): boolean;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* Builds the in-worker gateway, to compose into a worker you already own.
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* import { braidGatewayHandler } from '@braidlabs/sw/gateway';
|
|
44
|
+
*
|
|
45
|
+
* const braid = braidGatewayHandler({ registry: [{ id: 'billing', endpoint: '/billing-app/', pierce: ['/billing/*'] }] });
|
|
46
|
+
* self.addEventListener('fetch', (event) => {
|
|
47
|
+
* const handled = braid(event.request, event.preloadResponse);
|
|
48
|
+
* if (handled) event.respondWith(handled);
|
|
49
|
+
* });
|
|
50
|
+
* ```
|
|
51
|
+
*
|
|
52
|
+
* Claims a same-origin request in the Braid namespace (under `basePath`, when set), and every GET
|
|
53
|
+
* navigation — whether a page pierces is the registry's answer, and it arrives too late to decide
|
|
54
|
+
* on. A navigation no fragment claims goes to the network unchanged, and so does a shell that
|
|
55
|
+
* answers with a redirect.
|
|
56
|
+
*/
|
|
57
|
+
export declare function braidGatewayHandler(options: BraidGatewayHandlerOptions): BraidGatewayHandler;
|
|
58
|
+
export interface BraidGatewayWorkerOptions extends BraidGatewayHandlerOptions {
|
|
59
|
+
/**
|
|
60
|
+
* Keep fragment assets in build-keyed partitions, as {@link braidFetchHandler} does, in front of
|
|
61
|
+
* the in-worker gateway. Off by default. Not yet supported together with `basePath`, where the
|
|
62
|
+
* asset handler would not recognise the namespace and cache nothing; combining them throws.
|
|
63
|
+
*/
|
|
64
|
+
assets?: Omit<BraidFetchHandlerOptions, 'fetch'>;
|
|
65
|
+
/**
|
|
66
|
+
* Take over open pages as soon as this worker activates, so the page that installed it composes
|
|
67
|
+
* without a reload — and answer a page that asks to be claimed, which is how a page loaded with
|
|
68
|
+
* the worker bypassed (a hard reload) gets it back. Off by default, for the reason it is off in
|
|
69
|
+
* `setupBraidWorker`: a page mid-session starts being served by a worker it did not load against.
|
|
70
|
+
*/
|
|
71
|
+
claimClients?: boolean;
|
|
72
|
+
/**
|
|
73
|
+
* Declare at install, where the browser supports it (Chromium's static routing), that requests
|
|
74
|
+
* this worker does not answer go straight to the network, so they never start it. On by default:
|
|
75
|
+
* this is the whole worker. Turn it off if the same worker file adds its own `fetch` listener,
|
|
76
|
+
* which the routes would bypass.
|
|
77
|
+
*/
|
|
78
|
+
staticRoutes?: boolean;
|
|
79
|
+
}
|
|
80
|
+
/** The message a page posts to an active worker that is not controlling it. */
|
|
81
|
+
export declare const BRAID_CLAIM_MESSAGE = "braid-sw:claim";
|
|
82
|
+
/**
|
|
83
|
+
* Installs the in-worker gateway as the whole worker, and returns the gateway it runs — the one
|
|
84
|
+
* thing a worker built this way might still want to reach, for `refreshRegistry()` say.
|
|
85
|
+
*
|
|
86
|
+
* ```js
|
|
87
|
+
* // sw.js
|
|
88
|
+
* import { setupBraidGateway } from '@braidlabs/sw/gateway';
|
|
89
|
+
* setupBraidGateway({ registry: [{ id: 'billing', endpoint: '/billing-app/', pierce: ['/billing/*'] }] });
|
|
90
|
+
* ```
|
|
91
|
+
*/
|
|
92
|
+
export declare function setupBraidGateway(options: BraidGatewayWorkerOptions): BraidGateway;
|
|
@@ -0,0 +1,164 @@
|
|
|
1
|
+
import { createGateway, toFetchHandler, toWebMiddleware, } from '@braidlabs/gateway';
|
|
2
|
+
import { braidFetchHandler, } from './asset-handler.js';
|
|
3
|
+
import { addStaticRoutes, patternLiteral, } from './static-routes.js';
|
|
4
|
+
/**
|
|
5
|
+
* Builds the in-worker gateway, to compose into a worker you already own.
|
|
6
|
+
*
|
|
7
|
+
* ```js
|
|
8
|
+
* import { braidGatewayHandler } from '@braidlabs/sw/gateway';
|
|
9
|
+
*
|
|
10
|
+
* const braid = braidGatewayHandler({ registry: [{ id: 'billing', endpoint: '/billing-app/', pierce: ['/billing/*'] }] });
|
|
11
|
+
* self.addEventListener('fetch', (event) => {
|
|
12
|
+
* const handled = braid(event.request, event.preloadResponse);
|
|
13
|
+
* if (handled) event.respondWith(handled);
|
|
14
|
+
* });
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* Claims a same-origin request in the Braid namespace (under `basePath`, when set), and every GET
|
|
18
|
+
* navigation — whether a page pierces is the registry's answer, and it arrives too late to decide
|
|
19
|
+
* on. A navigation no fragment claims goes to the network unchanged, and so does a shell that
|
|
20
|
+
* answers with a redirect.
|
|
21
|
+
*/
|
|
22
|
+
export function braidGatewayHandler(options) {
|
|
23
|
+
const gateway = createGateway(options);
|
|
24
|
+
const namespace = namespaceOf(options.basePath);
|
|
25
|
+
const network = (request) => globalThis.fetch(request);
|
|
26
|
+
const viaGateway = toFetchHandler(gateway, network);
|
|
27
|
+
const compose = toWebMiddleware(gateway);
|
|
28
|
+
const claims = (request) => {
|
|
29
|
+
const url = new URL(request.url);
|
|
30
|
+
// `location` is the worker's own; without one (a test, a non-worker runtime) there is no
|
|
31
|
+
// origin to compare against, and nothing to leave to a browser either
|
|
32
|
+
const origin = globalThis.location
|
|
33
|
+
?.origin;
|
|
34
|
+
if (origin !== undefined && url.origin !== origin)
|
|
35
|
+
return false;
|
|
36
|
+
return (url.pathname.startsWith(namespace) ||
|
|
37
|
+
(request.method === 'GET' && isNavigation(request)));
|
|
38
|
+
};
|
|
39
|
+
const handler = (request, preloadResponse) => {
|
|
40
|
+
if (!claims(request))
|
|
41
|
+
return null;
|
|
42
|
+
if (new URL(request.url).pathname.startsWith(namespace))
|
|
43
|
+
return asGatewayRequest(request).then(viaGateway);
|
|
44
|
+
// The shell is the browser's own navigation — preloaded while the worker booted, when it was,
|
|
45
|
+
// and asked for again if the preload failed — and the gateway gets a copy it can derive
|
|
46
|
+
// fragment requests from.
|
|
47
|
+
const shell = () => Promise.resolve(preloadResponse).then((preloaded) => preloaded ?? network(request), () => network(request));
|
|
48
|
+
return asGatewayRequest(request).then((copy) => compose(copy, shell));
|
|
49
|
+
};
|
|
50
|
+
const fetchViaGateway = (input, init) => asGatewayRequest(input instanceof Request && init === undefined
|
|
51
|
+
? input
|
|
52
|
+
: new Request(input, init)).then(viaGateway);
|
|
53
|
+
return Object.assign(handler, {
|
|
54
|
+
fetch: fetchViaGateway,
|
|
55
|
+
gateway,
|
|
56
|
+
claims,
|
|
57
|
+
});
|
|
58
|
+
}
|
|
59
|
+
/** The message a page posts to an active worker that is not controlling it. */
|
|
60
|
+
export const BRAID_CLAIM_MESSAGE = 'braid-sw:claim';
|
|
61
|
+
/**
|
|
62
|
+
* Installs the in-worker gateway as the whole worker, and returns the gateway it runs — the one
|
|
63
|
+
* thing a worker built this way might still want to reach, for `refreshRegistry()` say.
|
|
64
|
+
*
|
|
65
|
+
* ```js
|
|
66
|
+
* // sw.js
|
|
67
|
+
* import { setupBraidGateway } from '@braidlabs/sw/gateway';
|
|
68
|
+
* setupBraidGateway({ registry: [{ id: 'billing', endpoint: '/billing-app/', pierce: ['/billing/*'] }] });
|
|
69
|
+
* ```
|
|
70
|
+
*/
|
|
71
|
+
export function setupBraidGateway(options) {
|
|
72
|
+
const scope = globalThis;
|
|
73
|
+
const { assets, claimClients, staticRoutes = true, ...gatewayOptions } = options;
|
|
74
|
+
if (assets && gatewayOptions.basePath) {
|
|
75
|
+
throw new Error('braid-sw: `assets` is not yet supported together with `basePath`');
|
|
76
|
+
}
|
|
77
|
+
const braid = braidGatewayHandler(gatewayOptions);
|
|
78
|
+
const cached = assets
|
|
79
|
+
? braidFetchHandler({ ...assets, fetch: braid.fetch })
|
|
80
|
+
: null;
|
|
81
|
+
if (staticRoutes) {
|
|
82
|
+
scope.addEventListener('install', (event) => {
|
|
83
|
+
// exactly what `braid.claims` answers: this origin's namespace, and every GET navigation
|
|
84
|
+
const routed = addStaticRoutes(event, {
|
|
85
|
+
pathnames: [`${patternLiteral(namespaceOf(gatewayOptions.basePath))}*`],
|
|
86
|
+
origin: globalThis.location?.origin,
|
|
87
|
+
navigations: true,
|
|
88
|
+
});
|
|
89
|
+
if (routed)
|
|
90
|
+
event.waitUntil?.(routed);
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
scope.addEventListener('activate', (event) => {
|
|
94
|
+
// Off, though every navigation passes through: the browser preloads *every* navigation in
|
|
95
|
+
// scope, the realm frames this worker answers itself among them, so each would also reach a
|
|
96
|
+
// server that has no answer for it. And the setting outlives the worker that set it.
|
|
97
|
+
const work = [
|
|
98
|
+
scope.registration?.navigationPreload?.disable().catch(() => undefined) ??
|
|
99
|
+
Promise.resolve(),
|
|
100
|
+
];
|
|
101
|
+
if (claimClients && scope.clients)
|
|
102
|
+
work.push(scope.clients.claim());
|
|
103
|
+
event.waitUntil?.(Promise.all(work));
|
|
104
|
+
});
|
|
105
|
+
scope.addEventListener('message', (event) => {
|
|
106
|
+
if (!claimClients ||
|
|
107
|
+
event.data?.type !==
|
|
108
|
+
BRAID_CLAIM_MESSAGE)
|
|
109
|
+
return;
|
|
110
|
+
if (scope.clients)
|
|
111
|
+
event.waitUntil?.(scope.clients.claim());
|
|
112
|
+
});
|
|
113
|
+
scope.addEventListener('fetch', (event) => {
|
|
114
|
+
// the claim decision first: the asset cache matches by path alone, on any origin
|
|
115
|
+
if (!braid.claims(event.request))
|
|
116
|
+
return;
|
|
117
|
+
const handled = cached?.(event.request) ?? braid(event.request, event.preloadResponse);
|
|
118
|
+
if (handled)
|
|
119
|
+
event.respondWith?.(handled);
|
|
120
|
+
});
|
|
121
|
+
return braid.gateway;
|
|
122
|
+
}
|
|
123
|
+
/**
|
|
124
|
+
* A navigation, as a request the gateway can build its own from.
|
|
125
|
+
*
|
|
126
|
+
* The gateway derives each fragment request with `new Request(url, request)`, which a browser
|
|
127
|
+
* refuses for a `navigate`-mode request — and the gateway, catching that, would serve every
|
|
128
|
+
* pierced slot as a placeholder. A server never sees that mode, so only a worker has to do this.
|
|
129
|
+
*
|
|
130
|
+
* Credentials go as a page's own `fetch` sends them, same-origin only, rather than as the
|
|
131
|
+
* navigation's `include`: a pierce must not be a credentialed CORS request where the page's own
|
|
132
|
+
* requests for the same fragment are not. A body is read up front, since a browser streams a
|
|
133
|
+
* request body only over HTTP/2.
|
|
134
|
+
*/
|
|
135
|
+
async function asGatewayRequest(request) {
|
|
136
|
+
if (request.mode !== 'navigate')
|
|
137
|
+
return request;
|
|
138
|
+
const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
|
|
139
|
+
return new Request(request.url, {
|
|
140
|
+
method: request.method,
|
|
141
|
+
headers: request.headers,
|
|
142
|
+
credentials: 'same-origin',
|
|
143
|
+
// `only-if-cached` is refused outside same-origin mode, which a copy cannot have
|
|
144
|
+
cache: request.cache === 'only-if-cached' ? 'default' : request.cache,
|
|
145
|
+
redirect: request.redirect,
|
|
146
|
+
referrer: request.referrer,
|
|
147
|
+
referrerPolicy: request.referrerPolicy,
|
|
148
|
+
signal: request.signal,
|
|
149
|
+
...(hasBody ? { body: await request.arrayBuffer() } : {}),
|
|
150
|
+
});
|
|
151
|
+
}
|
|
152
|
+
/** The Braid namespace under a base path, as the gateway trims it (it has already refused a malformed one). */
|
|
153
|
+
function namespaceOf(basePath) {
|
|
154
|
+
return `${(basePath ?? '').replace(/\/+$/, '')}/__braid/`;
|
|
155
|
+
}
|
|
156
|
+
/**
|
|
157
|
+
* Only the browser's own navigations. A page's `fetch` with an HTML `Accept` (htmx, Turbo, a
|
|
158
|
+
* prefetch) is a fetch, and the gateway — which in a worker cannot see `Sec-Fetch-Dest` — would
|
|
159
|
+
* otherwise pierce it as a document.
|
|
160
|
+
*/
|
|
161
|
+
function isNavigation(request) {
|
|
162
|
+
return request.mode === 'navigate';
|
|
163
|
+
}
|
|
164
|
+
//# sourceMappingURL=gateway.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"gateway.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/gateway.ts"],"names":[],"mappings":"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"}
|
package/src/lib/precache.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { type FragmentCacheOptions } from './fragment-caches.js';
|
|
1
2
|
/**
|
|
2
3
|
* Realm stub precaching: a fragment boot with no round trip in it.
|
|
3
4
|
*
|
|
@@ -9,9 +10,7 @@
|
|
|
9
10
|
* mean downloading every fragment on the origin whether or not the user ever visits a page that
|
|
10
11
|
* composes them, which trades a round trip the user might make for bandwidth they certainly spend.
|
|
11
12
|
*/
|
|
12
|
-
export interface PrecacheOptions {
|
|
13
|
-
cachePrefix?: string;
|
|
14
|
-
caches?: CacheStorage;
|
|
13
|
+
export interface PrecacheOptions extends FragmentCacheOptions {
|
|
15
14
|
fetch?: typeof fetch;
|
|
16
15
|
/** Where the stubs live. Defaults to the current origin. */
|
|
17
16
|
origin?: string;
|
package/src/lib/precache.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { BRAID_REALM_PREFIX } from '@braidlabs/gateway';
|
|
2
|
+
import { openFragmentCache } from './fragment-caches.js';
|
|
2
3
|
const DEFAULT_CACHE_PREFIX = 'braid-frag';
|
|
3
4
|
/**
|
|
4
5
|
* Fetches and caches each fragment's realm stub.
|
|
@@ -23,7 +24,9 @@ export async function precacheRealmStubs(fragmentIds, options = {}) {
|
|
|
23
24
|
result.failed.push({ url, reason: `HTTP ${response.status}` });
|
|
24
25
|
continue;
|
|
25
26
|
}
|
|
26
|
-
const cache = await
|
|
27
|
+
const cache = await openFragmentCache(fragmentId, options);
|
|
28
|
+
if (!cache)
|
|
29
|
+
continue;
|
|
27
30
|
await cache.put(url, response);
|
|
28
31
|
result.cached.push(url);
|
|
29
32
|
}
|
package/src/lib/precache.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"precache.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/precache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;
|
|
1
|
+
{"version":3,"file":"precache.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/precache.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,kBAAkB,EAAE,MAAM,oBAAoB,CAAC;AACxD,OAAO,EAAE,iBAAiB,EAA6B,MAAM,sBAAsB,CAAC;AA0BpF,MAAM,oBAAoB,GAAG,YAAY,CAAC;AAE1C;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,kBAAkB,CACtC,WAA8B,EAC9B,UAA2B,EAAE;IAE7B,MAAM,WAAW,GAAG,OAAO,CAAC,WAAW,IAAI,oBAAoB,CAAC;IAChE,MAAM,YAAY,GAAG,OAAO,CAAC,MAAM,IAAK,UAAwC,CAAC,MAAM,CAAC;IACxF,MAAM,YAAY,GAAG,OAAO,CAAC,KAAK,IAAI,UAAU,CAAC,KAAK,CAAC;IACvD,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,IAAK,UAAgD,CAAC,QAAQ,EAAE,MAAM,IAAI,EAAE,CAAC;IAE1G,MAAM,MAAM,GAAmB,EAAE,MAAM,EAAE,EAAE,EAAE,MAAM,EAAE,EAAE,EAAE,CAAC;IAC1D,IAAI,CAAC,YAAY;QAAE,OAAO,MAAM,CAAC;IAEjC,KAAK,MAAM,UAAU,IAAI,WAAW,EAAE,CAAC;QACrC,MAAM,GAAG,GAAG,GAAG,MAAM,GAAG,kBAAkB,GAAG,UAAU,GAAG,CAAC;QAC3D,IAAI,CAAC;YACH,MAAM,QAAQ,GAAG,MAAM,YAAY,CAAC,GAAG,CAAC,CAAC;YACzC,IAAI,CAAC,QAAQ,CAAC,EAAE,EAAE,CAAC;gBACjB,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,QAAQ,QAAQ,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC;gBAC/D,SAAS;YACX,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,iBAAiB,CAAC,UAAU,EAAE,OAAO,CAAC,CAAC;YAC3D,IAAI,CAAC,KAAK;gBAAE,SAAS;YACrB,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;YAC/B,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;QAAC,OAAO,KAAK,EAAE,CAAC;YACf,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,EAAE,GAAG,EAAE,MAAM,EAAE,KAAK,YAAY,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,MAAM,CAAC,KAAK,CAAC,EAAE,CAAC,CAAC;QAC9F,CAAC;IACH,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-time routes: the Service Worker Static Routing API (`InstallEvent.addRoutes`, Chromium).
|
|
3
|
+
*
|
|
4
|
+
* A worker with a `fetch` listener is started for every request in its scope, including the ones
|
|
5
|
+
* its listener then lets pass: the shell's own scripts, images and API calls each wait on a worker
|
|
6
|
+
* that has nothing to say about them. Routes declared at install let the browser decide without
|
|
7
|
+
* the worker, so only what the worker actually answers starts it.
|
|
8
|
+
*
|
|
9
|
+
* The routes have to say exactly what the worker's listener claims, no more and no less. A route
|
|
10
|
+
* that sends a claimed request to the network bypasses the worker; one that sends an unclaimed
|
|
11
|
+
* request to the listener only costs what it costs today. So everything a worker claims is listed
|
|
12
|
+
* as `fetch-event`, and the network takes the rest.
|
|
13
|
+
*
|
|
14
|
+
* Every other `fetch` listener in the same worker is bypassed too, which is why a worker that may
|
|
15
|
+
* share its file with an application's own listener only does this when asked.
|
|
16
|
+
*/
|
|
17
|
+
/** What a worker answers, as its `fetch` listener decides it. */
|
|
18
|
+
export interface ClaimedRequests {
|
|
19
|
+
/** Pathname patterns (URLPattern syntax) the worker answers, e.g. `/__braid/*`. */
|
|
20
|
+
pathnames: readonly string[];
|
|
21
|
+
/** The origin those pathnames are answered on; any origin when absent. */
|
|
22
|
+
origin?: string;
|
|
23
|
+
/** Answer only GET requests under those pathnames. */
|
|
24
|
+
getOnly?: boolean;
|
|
25
|
+
/** Whether the worker answers GET navigations, wherever they go. */
|
|
26
|
+
navigations: boolean;
|
|
27
|
+
}
|
|
28
|
+
/** A condition and source as `addRoutes` takes them. */
|
|
29
|
+
export interface StaticRoute {
|
|
30
|
+
condition: {
|
|
31
|
+
urlPattern?: unknown;
|
|
32
|
+
requestMethod?: string;
|
|
33
|
+
requestMode?: string;
|
|
34
|
+
};
|
|
35
|
+
source: 'network' | 'fetch-event';
|
|
36
|
+
}
|
|
37
|
+
/** The slice of `InstallEvent` used here. */
|
|
38
|
+
export interface RoutableInstallEvent {
|
|
39
|
+
addRoutes?(rules: StaticRoute | readonly StaticRoute[]): Promise<void>;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* The rules for a worker that answers `claimed`, in the order the browser evaluates them: the
|
|
43
|
+
* first match wins, so the worker's own requests come first and the catch-all last. Null where
|
|
44
|
+
* `URLPattern` is missing, since every rule needs one.
|
|
45
|
+
*/
|
|
46
|
+
export declare function staticRoutesFor(claimed: ClaimedRequests): StaticRoute[] | null;
|
|
47
|
+
/**
|
|
48
|
+
* Registers the routes for `claimed` on an install event, where the browser supports them.
|
|
49
|
+
*
|
|
50
|
+
* Returns the registration to wait on, or undefined where there is nothing to do. A browser that
|
|
51
|
+
* refuses the rules leaves the worker exactly as it was without them — started for everything,
|
|
52
|
+
* deciding in its listener — so a refusal is a warning, never a failed install.
|
|
53
|
+
*/
|
|
54
|
+
export declare function addStaticRoutes(event: RoutableInstallEvent, claimed: ClaimedRequests): Promise<void> | undefined;
|
|
55
|
+
/** A path prefix as a literal in URLPattern syntax, so a base path is matched as written. */
|
|
56
|
+
export declare function patternLiteral(path: string): string;
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Install-time routes: the Service Worker Static Routing API (`InstallEvent.addRoutes`, Chromium).
|
|
3
|
+
*
|
|
4
|
+
* A worker with a `fetch` listener is started for every request in its scope, including the ones
|
|
5
|
+
* its listener then lets pass: the shell's own scripts, images and API calls each wait on a worker
|
|
6
|
+
* that has nothing to say about them. Routes declared at install let the browser decide without
|
|
7
|
+
* the worker, so only what the worker actually answers starts it.
|
|
8
|
+
*
|
|
9
|
+
* The routes have to say exactly what the worker's listener claims, no more and no less. A route
|
|
10
|
+
* that sends a claimed request to the network bypasses the worker; one that sends an unclaimed
|
|
11
|
+
* request to the listener only costs what it costs today. So everything a worker claims is listed
|
|
12
|
+
* as `fetch-event`, and the network takes the rest.
|
|
13
|
+
*
|
|
14
|
+
* Every other `fetch` listener in the same worker is bypassed too, which is why a worker that may
|
|
15
|
+
* share its file with an application's own listener only does this when asked.
|
|
16
|
+
*/
|
|
17
|
+
/**
|
|
18
|
+
* The rules for a worker that answers `claimed`, in the order the browser evaluates them: the
|
|
19
|
+
* first match wins, so the worker's own requests come first and the catch-all last. Null where
|
|
20
|
+
* `URLPattern` is missing, since every rule needs one.
|
|
21
|
+
*/
|
|
22
|
+
export function staticRoutesFor(claimed) {
|
|
23
|
+
const Pattern = globalThis.URLPattern;
|
|
24
|
+
if (!Pattern)
|
|
25
|
+
return null;
|
|
26
|
+
const rules = claimed.pathnames.map((pathname) => ({
|
|
27
|
+
condition: {
|
|
28
|
+
urlPattern: new Pattern({ pathname, ...(claimed.origin === undefined ? {} : { baseURL: claimed.origin }) }),
|
|
29
|
+
...(claimed.getOnly ? { requestMethod: 'GET' } : {}),
|
|
30
|
+
},
|
|
31
|
+
source: 'fetch-event',
|
|
32
|
+
}));
|
|
33
|
+
if (claimed.navigations) {
|
|
34
|
+
// A worker only ever sees navigations into its own scope, so the origin is belt and braces:
|
|
35
|
+
// it keeps the rule saying exactly what the listener does. Through `baseURL`, which escapes
|
|
36
|
+
// what it takes, so an IPv6 host (`[::1]`) is not read as pattern syntax.
|
|
37
|
+
rules.push({
|
|
38
|
+
condition: {
|
|
39
|
+
...(claimed.origin === undefined ? {} : { urlPattern: new Pattern({ pathname: '*', baseURL: claimed.origin }) }),
|
|
40
|
+
requestMode: 'navigate',
|
|
41
|
+
requestMethod: 'GET',
|
|
42
|
+
},
|
|
43
|
+
source: 'fetch-event',
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
rules.push({ condition: { urlPattern: new Pattern({}) }, source: 'network' });
|
|
47
|
+
return rules;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Registers the routes for `claimed` on an install event, where the browser supports them.
|
|
51
|
+
*
|
|
52
|
+
* Returns the registration to wait on, or undefined where there is nothing to do. A browser that
|
|
53
|
+
* refuses the rules leaves the worker exactly as it was without them — started for everything,
|
|
54
|
+
* deciding in its listener — so a refusal is a warning, never a failed install.
|
|
55
|
+
*/
|
|
56
|
+
export function addStaticRoutes(event, claimed) {
|
|
57
|
+
if (typeof event.addRoutes !== 'function')
|
|
58
|
+
return undefined;
|
|
59
|
+
const warn = (error) => console.warn('braid-sw: static routes were not registered; every request will start the worker', error);
|
|
60
|
+
let rules;
|
|
61
|
+
try {
|
|
62
|
+
rules = staticRoutesFor(claimed);
|
|
63
|
+
}
|
|
64
|
+
catch (error) {
|
|
65
|
+
warn(error);
|
|
66
|
+
return undefined;
|
|
67
|
+
}
|
|
68
|
+
if (!rules)
|
|
69
|
+
return undefined;
|
|
70
|
+
return event.addRoutes(rules).catch(warn);
|
|
71
|
+
}
|
|
72
|
+
/** A path prefix as a literal in URLPattern syntax, so a base path is matched as written. */
|
|
73
|
+
export function patternLiteral(path) {
|
|
74
|
+
return path.replace(/[\\:*?+(){}]/g, '\\$&');
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=static-routes.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"static-routes.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/static-routes.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AA2BH;;;;GAIG;AACH,MAAM,UAAU,eAAe,CAAC,OAAwB;IACtD,MAAM,OAAO,GAAI,UAAqD,CAAC,UAAU,CAAC;IAClF,IAAI,CAAC,OAAO;QAAE,OAAO,IAAI,CAAC;IAE1B,MAAM,KAAK,GAAkB,OAAO,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,QAAQ,EAAE,EAAE,CAAC,CAAC;QAChE,SAAS,EAAE;YACT,UAAU,EAAE,IAAI,OAAO,CAAC,EAAE,QAAQ,EAAE,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;YAC3G,GAAG,CAAC,OAAO,CAAC,OAAO,CAAC,CAAC,CAAC,EAAE,aAAa,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,EAAE,CAAC;SACrD;QACD,MAAM,EAAE,aAAa;KACtB,CAAC,CAAC,CAAC;IACJ,IAAI,OAAO,CAAC,WAAW,EAAE,CAAC;QACxB,4FAA4F;QAC5F,4FAA4F;QAC5F,0EAA0E;QAC1E,KAAK,CAAC,IAAI,CAAC;YACT,SAAS,EAAE;gBACT,GAAG,CAAC,OAAO,CAAC,MAAM,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,UAAU,EAAE,IAAI,OAAO,CAAC,EAAE,QAAQ,EAAE,GAAG,EAAE,OAAO,EAAE,OAAO,CAAC,MAAM,EAAE,CAAC,EAAE,CAAC;gBAChH,WAAW,EAAE,UAAU;gBACvB,aAAa,EAAE,KAAK;aACrB;YACD,MAAM,EAAE,aAAa;SACtB,CAAC,CAAC;IACL,CAAC;IACD,KAAK,CAAC,IAAI,CAAC,EAAE,SAAS,EAAE,EAAE,UAAU,EAAE,IAAI,OAAO,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,EAAE,SAAS,EAAE,CAAC,CAAC;IAC9E,OAAO,KAAK,CAAC;AACf,CAAC;AAED;;;;;;GAMG;AACH,MAAM,UAAU,eAAe,CAAC,KAA2B,EAAE,OAAwB;IACnF,IAAI,OAAO,KAAK,CAAC,SAAS,KAAK,UAAU;QAAE,OAAO,SAAS,CAAC;IAC5D,MAAM,IAAI,GAAG,CAAC,KAAc,EAAE,EAAE,CAC9B,OAAO,CAAC,IAAI,CAAC,kFAAkF,EAAE,KAAK,CAAC,CAAC;IAC1G,IAAI,KAA2B,CAAC;IAChC,IAAI,CAAC;QACH,KAAK,GAAG,eAAe,CAAC,OAAO,CAAC,CAAC;IACnC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,IAAI,CAAC,KAAK,CAAC,CAAC;QACZ,OAAO,SAAS,CAAC;IACnB,CAAC;IACD,IAAI,CAAC,KAAK;QAAE,OAAO,SAAS,CAAC;IAC7B,OAAO,KAAK,CAAC,SAAS,CAAC,KAAK,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC;AAC5C,CAAC;AAED,6FAA6F;AAC7F,MAAM,UAAU,cAAc,CAAC,IAAY;IACzC,OAAO,IAAI,CAAC,OAAO,CAAC,eAAe,EAAE,MAAM,CAAC,CAAC;AAC/C,CAAC"}
|
package/src/lib/worker.d.ts
CHANGED
|
@@ -39,6 +39,16 @@ export interface BraidWorkerOptions extends BraidFetchHandlerOptions {
|
|
|
39
39
|
* worth having: a user who hits a broken deploy is a user who closes the tab.
|
|
40
40
|
*/
|
|
41
41
|
reports?: Omit<ReportQueueOptions, 'fetch'>;
|
|
42
|
+
/**
|
|
43
|
+
* Declare at install, where the browser supports it (Chromium's static routing), that requests
|
|
44
|
+
* this worker does not answer go straight to the network, so they never start it.
|
|
45
|
+
*
|
|
46
|
+
* Off by default: the routes bypass every `fetch` listener in the worker, including any the same
|
|
47
|
+
* file adds of its own. Turn it on when `setupBraidWorker` is the only thing listening. Ignored
|
|
48
|
+
* with `offline`, whose navigation handler also answers fetches for HTML, which no route can
|
|
49
|
+
* single out.
|
|
50
|
+
*/
|
|
51
|
+
staticRoutes?: boolean;
|
|
42
52
|
}
|
|
43
53
|
/** The subset of the worker global this uses, so a test can supply one without a worker. */
|
|
44
54
|
export interface WorkerScopeLike {
|
package/src/lib/worker.js
CHANGED
|
@@ -1,5 +1,7 @@
|
|
|
1
|
+
import { BRAID_FRAGMENT_PREFIX, BRAID_REALM_PREFIX } from '@braidlabs/gateway';
|
|
1
2
|
import { braidFetchHandler, pruneFragmentCaches } from './asset-handler.js';
|
|
2
|
-
import {
|
|
3
|
+
import { addStaticRoutes } from './static-routes.js';
|
|
4
|
+
import { braidNavigationHandler, clearBraidCaches } from './compose.js';
|
|
3
5
|
import { precacheRealmStubs } from './precache.js';
|
|
4
6
|
import { diagnoseSkew } from './skew-report.js';
|
|
5
7
|
import { BRAID_REPORT_SYNC_TAG, createReportQueue } from './reports.js';
|
|
@@ -33,7 +35,25 @@ export function setupBraidWorker(options = {}) {
|
|
|
33
35
|
...(options.buildId === undefined ? {} : { buildId: options.buildId }),
|
|
34
36
|
...(options.snapshotId === undefined ? {} : { snapshotId: options.snapshotId }),
|
|
35
37
|
};
|
|
38
|
+
const navigate = options.offline ? braidNavigationHandler({ ...options, ...options.offline }) : null;
|
|
36
39
|
scope.addEventListener('install', (event) => {
|
|
40
|
+
if (options.staticRoutes && navigate) {
|
|
41
|
+
// Offline composition also answers a page's own fetch for HTML (htmx, Turbo), which a route
|
|
42
|
+
// cannot single out: it can match a request's mode, but not its Accept header. Routing the
|
|
43
|
+
// rest to the network would bypass those, so a worker that composes offline keeps no routes.
|
|
44
|
+
console.warn('braid-sw: staticRoutes is ignored with offline composition, which also answers fetches for HTML');
|
|
45
|
+
}
|
|
46
|
+
else if (options.staticRoutes) {
|
|
47
|
+
// exactly what the fetch listener answers: fragment assets and realm stubs on any origin, as
|
|
48
|
+
// `braidFetchHandler` matches them
|
|
49
|
+
const routed = addStaticRoutes(event, {
|
|
50
|
+
pathnames: [`${BRAID_FRAGMENT_PREFIX}*`, `${BRAID_REALM_PREFIX}*`],
|
|
51
|
+
getOnly: true,
|
|
52
|
+
navigations: false,
|
|
53
|
+
});
|
|
54
|
+
if (routed)
|
|
55
|
+
event.waitUntil?.(routed);
|
|
56
|
+
}
|
|
37
57
|
if (!options.precache?.length)
|
|
38
58
|
return;
|
|
39
59
|
// The precache is best-effort by construction: a worker that refuses to install because one
|
|
@@ -44,7 +64,6 @@ export function setupBraidWorker(options = {}) {
|
|
|
44
64
|
}
|
|
45
65
|
}));
|
|
46
66
|
});
|
|
47
|
-
const navigate = options.offline ? braidNavigationHandler({ ...options, ...options.offline }) : null;
|
|
48
67
|
scope.addEventListener('activate', (event) => {
|
|
49
68
|
const work = [];
|
|
50
69
|
// A worker that handles navigations sits in front of every one of them, so without this each
|
|
@@ -91,6 +110,20 @@ export function setupBraidWorker(options = {}) {
|
|
|
91
110
|
*/
|
|
92
111
|
scope.addEventListener('message', (event) => {
|
|
93
112
|
const data = event.data;
|
|
113
|
+
// Sign-out: drop what offline composition kept, then say so, so the page can wait for it.
|
|
114
|
+
if (data?.type === 'braid-sw:clear') {
|
|
115
|
+
// The options composition itself writes with: `offline` overrides the top-level ones.
|
|
116
|
+
const composing = { ...options, ...options.offline };
|
|
117
|
+
const cleared = clearBraidCaches({
|
|
118
|
+
...(composing.caches === undefined ? {} : { caches: composing.caches }),
|
|
119
|
+
...(composing.cachePrefix === undefined ? {} : { cachePrefix: composing.cachePrefix }),
|
|
120
|
+
...(composing.buckets === undefined ? {} : { buckets: composing.buckets }),
|
|
121
|
+
}).then(() => event.source?.postMessage?.({ type: 'braid-sw:cleared', ok: true }),
|
|
122
|
+
// Answered either way, so a page waiting on the reply at sign-out is never left hanging.
|
|
123
|
+
() => event.source?.postMessage?.({ type: 'braid-sw:cleared', ok: false }));
|
|
124
|
+
event.waitUntil?.(cleared);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
94
127
|
if (data?.type !== 'braid-sw:hello')
|
|
95
128
|
return;
|
|
96
129
|
const diagnosis = diagnoseSkew(version, data.page ?? {});
|