@braidlabs/sw 0.7.1 → 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.
@@ -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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"}
@@ -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;
@@ -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 cacheStorage.open(`${cachePrefix}:${fragmentId}`);
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
  }
@@ -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;AA4BxD,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,YAAY,CAAC,IAAI,CAAC,GAAG,WAAW,IAAI,UAAU,EAAE,CAAC,CAAC;YACtE,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"}
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"}
@@ -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 { braidNavigationHandler } from './compose.js';
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 ?? {});