@braidlabs/sw 0.1.1
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 +160 -0
- package/package.json +40 -0
- package/src/index.d.ts +24 -0
- package/src/index.js +19 -0
- package/src/index.js.map +1 -0
- package/src/lib/asset-handler.d.ts +72 -0
- package/src/lib/asset-handler.js +92 -0
- package/src/lib/asset-handler.js.map +1 -0
- package/src/lib/compose.d.ts +50 -0
- package/src/lib/compose.js +153 -0
- package/src/lib/compose.js.map +1 -0
- package/src/lib/precache.d.ts +34 -0
- package/src/lib/precache.js +36 -0
- package/src/lib/precache.js.map +1 -0
- package/src/lib/reports.d.ts +54 -0
- package/src/lib/reports.js +59 -0
- package/src/lib/reports.js.map +1 -0
- package/src/lib/skew-report.d.ts +49 -0
- package/src/lib/skew-report.js +55 -0
- package/src/lib/skew-report.js.map +1 -0
- package/src/lib/worker.d.ts +61 -0
- package/src/lib/worker.js +96 -0
- package/src/lib/worker.js.map +1 -0
package/README.md
ADDED
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
# @braidlabs/sw
|
|
2
|
+
|
|
3
|
+
Skew-aware asset serving for composed pages.
|
|
4
|
+
|
|
5
|
+
The classic micro-frontend white screen: a user has the page open, a deploy lands, they click
|
|
6
|
+
something that lazy-loads `main.abc123.js` — and it is gone, because the new build wrote
|
|
7
|
+
`main.def456.js`. Nothing on the page can recover. The request comes from the module loader, the
|
|
8
|
+
response is a 404, and the route dies.
|
|
9
|
+
|
|
10
|
+
A service worker is the **only** layer that can answer that request from a copy it kept.
|
|
11
|
+
|
|
12
|
+
Braid makes this materially cleaner than it is for a monolith. Every fragment's assets live under
|
|
13
|
+
`/__braid/frag/:id/*`, so each fragment gets **its own cache partition keyed by its own build**.
|
|
14
|
+
Fragment A at build 5 and fragment B at build 12 coexist, with no shared cache generation for one
|
|
15
|
+
deploy to invalidate on the other's behalf. A monolith's worker has one bucket and one answer.
|
|
16
|
+
|
|
17
|
+
---
|
|
18
|
+
|
|
19
|
+
## Two shapes
|
|
20
|
+
|
|
21
|
+
Both ship, because "the app already has a service worker" is not a safe assumption — least of all
|
|
22
|
+
for internal enterprise apps.
|
|
23
|
+
|
|
24
|
+
**A handler, for a worker you already own.** Braid never takes your `fetch` event:
|
|
25
|
+
|
|
26
|
+
```js
|
|
27
|
+
// your sw.js
|
|
28
|
+
import { braidFetchHandler } from '@braidlabs/sw';
|
|
29
|
+
const braid = braidFetchHandler({ buildId: BUILD_ID });
|
|
30
|
+
|
|
31
|
+
self.addEventListener('fetch', (event) => {
|
|
32
|
+
const handled = braid(event.request); // null for anything not ours
|
|
33
|
+
if (handled) event.respondWith(handled);
|
|
34
|
+
});
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
**A complete worker, for shells with none.** Same handler, already wired:
|
|
38
|
+
|
|
39
|
+
```js
|
|
40
|
+
// sw.js
|
|
41
|
+
import { setupBraidWorker } from '@braidlabs/sw';
|
|
42
|
+
setupBraidWorker({ buildId: BUILD_ID, precache: ['billing', 'notifications'] });
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
## Let the gateway serve it
|
|
46
|
+
|
|
47
|
+
```ts
|
|
48
|
+
createGateway({ registry, serviceWorker: true }); // scope '/'
|
|
49
|
+
createGateway({ registry, serviceWorker: { scope: '/apps/' } }); // path-mounted deployments
|
|
50
|
+
```
|
|
51
|
+
|
|
52
|
+
```ts
|
|
53
|
+
import { registerBraidServiceWorker } from '@braidlabs/core';
|
|
54
|
+
await registerBraidServiceWorker({ buildId: BUILD_ID });
|
|
55
|
+
```
|
|
56
|
+
|
|
57
|
+
**A worker's scope is capped by the path it is served from.** A script at `/__braid/sw.js` defaults
|
|
58
|
+
to controlling `/__braid/` — enough for fragment assets, useless for the shell's own, which is the
|
|
59
|
+
chunk-failure case that matters most. Widening it needs `Service-Worker-Allowed` on the script
|
|
60
|
+
response, and the gateway is the one component already in front of the origin that can send it
|
|
61
|
+
without touching infrastructure config.
|
|
62
|
+
|
|
63
|
+
Claiming `/` sounds broad and costs nothing: scope precedence is longest-match, so a worker
|
|
64
|
+
registered at `/legacy/` still controls `/legacy/` whatever Braid claims. The root is a fallback,
|
|
65
|
+
not an exclusion — and the handler returns `null` outside the namespace anyway.
|
|
66
|
+
|
|
67
|
+
The generated script is **byte-stable across registry publishes**. Baking the snapshot id into it
|
|
68
|
+
would make every publish a worker update with its own waiting and activation lifecycle;
|
|
69
|
+
configuration churn must not become worker churn.
|
|
70
|
+
|
|
71
|
+
## What it will and will not do
|
|
72
|
+
|
|
73
|
+
| | |
|
|
74
|
+
| --- | --- |
|
|
75
|
+
| Serves a cached chunk when the origin 404s it | ✅ the deploy case |
|
|
76
|
+
| Serves from cache when the network is gone | ✅ offline |
|
|
77
|
+
| Answers a 500 from cache | ❌ that is an outage, and hiding it makes it someone else's mystery |
|
|
78
|
+
| Answers a POST | ❌ a cache cannot, and pretending turns a failed write into a stale success |
|
|
79
|
+
| Touches anything outside `/__braid/` | ❌ never |
|
|
80
|
+
| Caches fragment **documents** | ❌ composed markup's freshness is the gateway's business |
|
|
81
|
+
|
|
82
|
+
## Offline composition
|
|
83
|
+
|
|
84
|
+
Opt-in, and the piece with the most leverage: the worker runs **the gateway's own piercing**.
|
|
85
|
+
|
|
86
|
+
```js
|
|
87
|
+
setupBraidWorker({
|
|
88
|
+
buildId: BUILD_ID,
|
|
89
|
+
offline: { snapshotUrl: '/__braid/registry/pinned.json' },
|
|
90
|
+
});
|
|
91
|
+
```
|
|
92
|
+
|
|
93
|
+
The gateway core is runtime-neutral — no `node:` imports in the gateway, the registry, or the
|
|
94
|
+
rewriter — and a service worker is a web-standard runtime. So the worker holds the shell and the
|
|
95
|
+
fragment documents and interleaves them with the same `pierceShellHtml` the server runs. A second
|
|
96
|
+
implementation of piercing would be a second set of piercing bugs, found offline.
|
|
97
|
+
|
|
98
|
+
**It holds the parts, not the finished page.** Caching composed HTML would be simpler and much less
|
|
99
|
+
useful: the composed page is the cross-product of shell, fragments, and route. Holding the parts
|
|
100
|
+
means a route visited once composes offline afterwards, a fragment updates its own part, and a
|
|
101
|
+
fragment with nothing cached degrades to a placeholder instead of failing the page.
|
|
102
|
+
|
|
103
|
+
The shell is re-fetched with `sec-fetch-dest: empty`, which is how the gateway already distinguishes
|
|
104
|
+
a soft-navigation fetch from a document request — the same distinction every pierced response
|
|
105
|
+
declares in its `Vary` header, used from the other side.
|
|
106
|
+
|
|
107
|
+
Two things it will not do: compose while the network is reachable (the gateway is authoritative),
|
|
108
|
+
and cache a composed response (a page pierced against a registry that has since changed is exactly
|
|
109
|
+
the stale artifact this package exists to avoid). Composed pages carry
|
|
110
|
+
`x-braid-composed: offline` so a shell can say so rather than look merely slow.
|
|
111
|
+
|
|
112
|
+
## The worker is itself a skew vector
|
|
113
|
+
|
|
114
|
+
It is a long-lived deployment artifact that updates on its own schedule — a version boundary added
|
|
115
|
+
to a system whose purpose is managing version boundaries. So it gets the same discipline: it knows
|
|
116
|
+
its build and the snapshot it serves, and says so when they disagree with the page.
|
|
117
|
+
|
|
118
|
+
```ts
|
|
119
|
+
await registerBraidServiceWorker({
|
|
120
|
+
buildId: BUILD_ID,
|
|
121
|
+
onVersion: (report) => telemetry.record(report),
|
|
122
|
+
});
|
|
123
|
+
```
|
|
124
|
+
|
|
125
|
+
`claimClients` is off by default for the same reason. Claiming swaps the worker underneath a page
|
|
126
|
+
that is already running, so a page mid-session starts being served by a build it did not load
|
|
127
|
+
against. Waiting for the next navigation is the boring, correct behaviour.
|
|
128
|
+
|
|
129
|
+
## Reports that survive the tab closing
|
|
130
|
+
|
|
131
|
+
```js
|
|
132
|
+
setupBraidWorker({
|
|
133
|
+
buildId: BUILD_ID,
|
|
134
|
+
reports: { endpoint: '/__telemetry', driver: indexedDbRecordDriver({ database: 'braid-sw' }) },
|
|
135
|
+
});
|
|
136
|
+
```
|
|
137
|
+
|
|
138
|
+
The worker knows things nobody else can see — that a chunk was served from cache after the origin
|
|
139
|
+
404'd it, that its build disagrees with the page's. Handed to `onReport` those facts are
|
|
140
|
+
fire-and-forget, and the ones that go missing are disproportionately the interesting ones: a user who
|
|
141
|
+
hits a broken deploy is a user who closes the tab, and a `fetch` started during teardown loses that
|
|
142
|
+
race.
|
|
143
|
+
|
|
144
|
+
Queued reports are flushed on a Background Sync event — the browser wakes the worker when it judges
|
|
145
|
+
the network is back, with no page open and nothing racing teardown. Background Sync is Chromium-only,
|
|
146
|
+
so every activation also flushes opportunistically; a browser without it gets that path and nothing
|
|
147
|
+
else.
|
|
148
|
+
|
|
149
|
+
The queue is `@braidlabs/data`'s outbox rather than a second implementation: one record per entry, so
|
|
150
|
+
appending never reads the queue first and two contexts cannot lose each other's writes. A refused
|
|
151
|
+
batch stays queued in full — partial credit would need the endpoint to say which records it took,
|
|
152
|
+
and no telemetry endpoint does.
|
|
153
|
+
|
|
154
|
+
## Not a data cache
|
|
155
|
+
|
|
156
|
+
The Cache API stores `Request → Response`. The data layer needs per-entity keys, versioned
|
|
157
|
+
envelopes, and migrate-on-read; the Cache API offers none of the three, and storing enveloped
|
|
158
|
+
entities as opaque response bodies would defeat the projection the whole design rests on.
|
|
159
|
+
|
|
160
|
+
**Cache API for assets and stubs. IndexedDB (`@braidlabs/data`) for entities.**
|
package/package.json
ADDED
|
@@ -0,0 +1,40 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "@braidlabs/sw",
|
|
3
|
+
"version": "0.1.1",
|
|
4
|
+
"description": "Braid service worker — skew-aware asset serving for composed pages: each fragment gets its own build-keyed cache partition, so a page already open keeps getting the chunks it was built against after a deploy lands. Ships as a handler to compose into an existing worker, and as a complete worker.",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"micro-frontends",
|
|
7
|
+
"service-worker",
|
|
8
|
+
"version-skew",
|
|
9
|
+
"cache",
|
|
10
|
+
"offline"
|
|
11
|
+
],
|
|
12
|
+
"license": "MIT",
|
|
13
|
+
"engines": {
|
|
14
|
+
"node": ">=24"
|
|
15
|
+
},
|
|
16
|
+
"sideEffects": false,
|
|
17
|
+
"type": "module",
|
|
18
|
+
"main": "./src/index.js",
|
|
19
|
+
"types": "./src/index.d.ts",
|
|
20
|
+
"exports": {
|
|
21
|
+
".": {
|
|
22
|
+
"types": "./src/index.d.ts",
|
|
23
|
+
"default": "./src/index.js"
|
|
24
|
+
},
|
|
25
|
+
"./package.json": "./package.json"
|
|
26
|
+
},
|
|
27
|
+
"publishConfig": {
|
|
28
|
+
"access": "public"
|
|
29
|
+
},
|
|
30
|
+
"repository": {
|
|
31
|
+
"type": "git",
|
|
32
|
+
"url": "git+https://github.com/braidjs/braid.git",
|
|
33
|
+
"directory": "libs/braid-sw"
|
|
34
|
+
},
|
|
35
|
+
"homepage": "https://github.com/braidjs/braid#readme",
|
|
36
|
+
"bugs": {
|
|
37
|
+
"url": "https://github.com/braidjs/braid/issues"
|
|
38
|
+
},
|
|
39
|
+
"module": "./src/index.js"
|
|
40
|
+
}
|
package/src/index.d.ts
ADDED
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@braidlabs/sw` — skew-aware asset serving for composed pages.
|
|
3
|
+
*
|
|
4
|
+
* The classic micro-frontend white screen is a deploy landing under an open page: a lazy chunk is
|
|
5
|
+
* requested, the build that wrote it is gone, and nothing on the page can recover. A service worker
|
|
6
|
+
* is the only layer that can answer that request from a copy it kept — and because every fragment's
|
|
7
|
+
* assets live under `/__braid/frag/:id/*`, each fragment gets its own build-keyed cache partition
|
|
8
|
+
* rather than a monolith's single bucket with a single answer.
|
|
9
|
+
*
|
|
10
|
+
* Ships in both shapes on purpose: a handler to compose into a worker you already own, and a
|
|
11
|
+
* complete worker for the (common) case where you have none.
|
|
12
|
+
*/
|
|
13
|
+
export { braidFetchHandler, pruneFragmentCaches } from './lib/asset-handler.js';
|
|
14
|
+
export type { BraidFetchHandlerOptions, AssetOutcome, AssetReport } from './lib/asset-handler.js';
|
|
15
|
+
export { setupBraidWorker } from './lib/worker.js';
|
|
16
|
+
export type { BraidWorkerOptions } from './lib/worker.js';
|
|
17
|
+
export { braidNavigationHandler } from './lib/compose.js';
|
|
18
|
+
export type { OfflineCompositionOptions } from './lib/compose.js';
|
|
19
|
+
export { precacheRealmStubs } from './lib/precache.js';
|
|
20
|
+
export type { PrecacheOptions, PrecacheResult } from './lib/precache.js';
|
|
21
|
+
export { createReportQueue, BRAID_REPORT_SYNC_TAG } from './lib/reports.js';
|
|
22
|
+
export type { ReportQueue, ReportQueueOptions, BraidReport, FlushOutcome } from './lib/reports.js';
|
|
23
|
+
export { diagnoseSkew, BRAID_BUILD_HEADER, BRAID_SNAPSHOT_HEADER } from './lib/skew-report.js';
|
|
24
|
+
export type { SkewDiagnosis, SkewSeverity, WorkerVersion, PageVersion } from './lib/skew-report.js';
|
package/src/index.js
ADDED
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `@braidlabs/sw` — skew-aware asset serving for composed pages.
|
|
3
|
+
*
|
|
4
|
+
* The classic micro-frontend white screen is a deploy landing under an open page: a lazy chunk is
|
|
5
|
+
* requested, the build that wrote it is gone, and nothing on the page can recover. A service worker
|
|
6
|
+
* is the only layer that can answer that request from a copy it kept — and because every fragment's
|
|
7
|
+
* assets live under `/__braid/frag/:id/*`, each fragment gets its own build-keyed cache partition
|
|
8
|
+
* rather than a monolith's single bucket with a single answer.
|
|
9
|
+
*
|
|
10
|
+
* Ships in both shapes on purpose: a handler to compose into a worker you already own, and a
|
|
11
|
+
* complete worker for the (common) case where you have none.
|
|
12
|
+
*/
|
|
13
|
+
export { braidFetchHandler, pruneFragmentCaches } from './lib/asset-handler.js';
|
|
14
|
+
export { setupBraidWorker } from './lib/worker.js';
|
|
15
|
+
export { braidNavigationHandler } from './lib/compose.js';
|
|
16
|
+
export { precacheRealmStubs } from './lib/precache.js';
|
|
17
|
+
export { createReportQueue, BRAID_REPORT_SYNC_TAG } from './lib/reports.js';
|
|
18
|
+
export { diagnoseSkew, BRAID_BUILD_HEADER, BRAID_SNAPSHOT_HEADER } from './lib/skew-report.js';
|
|
19
|
+
//# sourceMappingURL=index.js.map
|
package/src/index.js.map
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.js","sourceRoot":"","sources":["../../../../libs/braid-sw/src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAEH,OAAO,EAAE,iBAAiB,EAAE,mBAAmB,EAAE,MAAM,wBAAwB,CAAC;AAGhF,OAAO,EAAE,gBAAgB,EAAE,MAAM,iBAAiB,CAAC;AAGnD,OAAO,EAAE,sBAAsB,EAAE,MAAM,kBAAkB,CAAC;AAG1D,OAAO,EAAE,kBAAkB,EAAE,MAAM,mBAAmB,CAAC;AAGvD,OAAO,EAAE,iBAAiB,EAAE,qBAAqB,EAAE,MAAM,kBAAkB,CAAC;AAG5E,OAAO,EAAE,YAAY,EAAE,kBAAkB,EAAE,qBAAqB,EAAE,MAAM,sBAAsB,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Skew-aware asset serving: the one layer that can keep an open page working after a deploy.
|
|
3
|
+
*
|
|
4
|
+
* The failure this exists for is the classic micro-frontend white screen. A user has the page open,
|
|
5
|
+
* a deploy lands, they click something that lazy-loads `main.abc123.js` — and it is gone from the
|
|
6
|
+
* origin, because the new build wrote `main.def456.js` instead. Nothing on the page can recover:
|
|
7
|
+
* the request is made by the module loader, the response is a 404, and the route dies.
|
|
8
|
+
*
|
|
9
|
+
* A service worker can, because it saw that chunk go past on the way in and kept a copy.
|
|
10
|
+
*
|
|
11
|
+
* **Braid makes this materially cleaner than it is for a monolith.** Every fragment's assets live
|
|
12
|
+
* under `/__braid/frag/:id/*`, so each fragment gets its own cache partition keyed by its own build.
|
|
13
|
+
* Fragment A at build 5 and fragment B at build 12 coexist with no shared cache generation to
|
|
14
|
+
* invalidate — a monolith's worker has one bucket and one answer for the whole page.
|
|
15
|
+
*
|
|
16
|
+
* Scope discipline: this handler answers for the Braid namespace and returns `null` for everything
|
|
17
|
+
* else. A shell composing it into its own worker keeps ownership of its own routes, and a shell
|
|
18
|
+
* using the prebuilt worker gets a worker that does nothing surprising to the rest of its origin.
|
|
19
|
+
*/
|
|
20
|
+
/** How a request was answered — the input to the diagnostics a silent worker would deny you. */
|
|
21
|
+
export type AssetOutcome = 'network' | 'cache'
|
|
22
|
+
/** Served from cache *after* the network said the asset is gone — the deploy-skew save. */
|
|
23
|
+
| 'cache-after-404' | 'offline';
|
|
24
|
+
export interface AssetReport {
|
|
25
|
+
fragmentId: string;
|
|
26
|
+
url: string;
|
|
27
|
+
outcome: AssetOutcome;
|
|
28
|
+
/** The cache partition that answered, or would have. */
|
|
29
|
+
partition: string;
|
|
30
|
+
}
|
|
31
|
+
export interface BraidFetchHandlerOptions {
|
|
32
|
+
/**
|
|
33
|
+
* The build this worker was shipped as.
|
|
34
|
+
*
|
|
35
|
+
* A worker is itself a long-lived deployment artifact that updates on its own schedule, so it is
|
|
36
|
+
* a skew vector in a system whose purpose is managing them. Naming its build is what lets a
|
|
37
|
+
* disagreement be reported instead of silently serving yesterday's assets to today's document.
|
|
38
|
+
*/
|
|
39
|
+
buildId?: string;
|
|
40
|
+
/** Cache name prefix. One cache per fragment, so eviction is per fragment too. */
|
|
41
|
+
cachePrefix?: string;
|
|
42
|
+
/** Called for every namespace request this handler answers. Wire it to your telemetry. */
|
|
43
|
+
onReport?: (report: AssetReport) => void;
|
|
44
|
+
/** Injectable for tests. Defaults to the worker's own `caches`. */
|
|
45
|
+
caches?: CacheStorage;
|
|
46
|
+
/** Injectable for tests. Defaults to the worker's own `fetch`. */
|
|
47
|
+
fetch?: typeof fetch;
|
|
48
|
+
}
|
|
49
|
+
/**
|
|
50
|
+
* Builds a fetch handler for the Braid namespace.
|
|
51
|
+
*
|
|
52
|
+
* ```js
|
|
53
|
+
* const braid = braidFetchHandler();
|
|
54
|
+
* self.addEventListener('fetch', (event) => {
|
|
55
|
+
* const handled = braid(event.request); // null for anything not ours
|
|
56
|
+
* if (handled) event.respondWith(handled);
|
|
57
|
+
* });
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export declare function braidFetchHandler(options?: BraidFetchHandlerOptions): (request: Request) => Promise<Response> | null;
|
|
61
|
+
/**
|
|
62
|
+
* Removes cache partitions for fragments that are no longer registered.
|
|
63
|
+
*
|
|
64
|
+
* Per fragment rather than per build: a fragment's own chunks are content-hashed, so its partition
|
|
65
|
+
* grows by a bounded amount per deploy and shrinking it on activate would throw away exactly the
|
|
66
|
+
* old chunks this handler exists to keep. A fragment that has left the registry, on the other hand,
|
|
67
|
+
* is never coming back for them.
|
|
68
|
+
*/
|
|
69
|
+
export declare function pruneFragmentCaches(keep: readonly string[], options?: {
|
|
70
|
+
cachePrefix?: string;
|
|
71
|
+
caches?: CacheStorage;
|
|
72
|
+
}): Promise<string[]>;
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
import { parseBraidPathname } from '@braidlabs/gateway';
|
|
2
|
+
const DEFAULT_CACHE_PREFIX = 'braid-frag';
|
|
3
|
+
/**
|
|
4
|
+
* Builds a fetch handler for the Braid namespace.
|
|
5
|
+
*
|
|
6
|
+
* ```js
|
|
7
|
+
* const braid = braidFetchHandler();
|
|
8
|
+
* self.addEventListener('fetch', (event) => {
|
|
9
|
+
* const handled = braid(event.request); // null for anything not ours
|
|
10
|
+
* if (handled) event.respondWith(handled);
|
|
11
|
+
* });
|
|
12
|
+
* ```
|
|
13
|
+
*/
|
|
14
|
+
export function braidFetchHandler(options = {}) {
|
|
15
|
+
const cachePrefix = options.cachePrefix ?? DEFAULT_CACHE_PREFIX;
|
|
16
|
+
const cacheStorage = options.caches ?? globalThis.caches;
|
|
17
|
+
const networkFetch = options.fetch ?? globalThis.fetch;
|
|
18
|
+
return (request) => {
|
|
19
|
+
// GET only. A cache cannot answer a POST, and pretending otherwise is how a worker turns a
|
|
20
|
+
// failed write into a stale success.
|
|
21
|
+
if (request.method !== 'GET')
|
|
22
|
+
return null;
|
|
23
|
+
const url = new URL(request.url);
|
|
24
|
+
const route = parseBraidPathname(url.pathname);
|
|
25
|
+
// Assets and realm stubs only. A fragment *document* is composed markup whose freshness is the
|
|
26
|
+
// gateway's business — caching one here would serve a page built against a registry that has
|
|
27
|
+
// since changed, which is the failure this package exists to prevent rather than cause.
|
|
28
|
+
if (!route || (route.kind !== 'fragment' && route.kind !== 'realm'))
|
|
29
|
+
return null;
|
|
30
|
+
return serve(request, url, route.fragmentId);
|
|
31
|
+
};
|
|
32
|
+
async function serve(request, url, fragmentId) {
|
|
33
|
+
const partition = `${cachePrefix}:${fragmentId}`;
|
|
34
|
+
const report = (outcome) => options.onReport?.({ fragmentId, url: url.href, outcome, partition });
|
|
35
|
+
const cache = await cacheStorage?.open(partition);
|
|
36
|
+
try {
|
|
37
|
+
const response = await networkFetch(request);
|
|
38
|
+
// 404 is the deploy case, and the only status worth reaching into the cache for. A 500 is the
|
|
39
|
+
// server having a bad moment about an asset that still exists; answering it from cache would
|
|
40
|
+
// paper over an outage and make it someone else's mystery.
|
|
41
|
+
if (response.status === 404) {
|
|
42
|
+
const cached = await cache?.match(request);
|
|
43
|
+
if (cached) {
|
|
44
|
+
report('cache-after-404');
|
|
45
|
+
return cached;
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
// Only successful, non-partial responses are worth keeping. A cached 206 answers a range
|
|
49
|
+
// request that was never asked for.
|
|
50
|
+
if (response.ok && response.status === 200 && cache) {
|
|
51
|
+
// Cached without awaiting: the page is waiting on this response, and the copy is an
|
|
52
|
+
// optimization for a future load rather than a precondition for this one.
|
|
53
|
+
void cache.put(request, response.clone()).catch(() => undefined);
|
|
54
|
+
}
|
|
55
|
+
report('network');
|
|
56
|
+
return response;
|
|
57
|
+
}
|
|
58
|
+
catch (error) {
|
|
59
|
+
// The network is gone, not merely unhappy. This is the offline path.
|
|
60
|
+
const cached = await cache?.match(request);
|
|
61
|
+
if (cached) {
|
|
62
|
+
report('offline');
|
|
63
|
+
return cached;
|
|
64
|
+
}
|
|
65
|
+
throw error;
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Removes cache partitions for fragments that are no longer registered.
|
|
71
|
+
*
|
|
72
|
+
* Per fragment rather than per build: a fragment's own chunks are content-hashed, so its partition
|
|
73
|
+
* grows by a bounded amount per deploy and shrinking it on activate would throw away exactly the
|
|
74
|
+
* old chunks this handler exists to keep. A fragment that has left the registry, on the other hand,
|
|
75
|
+
* is never coming back for them.
|
|
76
|
+
*/
|
|
77
|
+
export async function pruneFragmentCaches(keep, options = {}) {
|
|
78
|
+
const cachePrefix = options.cachePrefix ?? DEFAULT_CACHE_PREFIX;
|
|
79
|
+
const cacheStorage = options.caches ?? globalThis.caches;
|
|
80
|
+
if (!cacheStorage)
|
|
81
|
+
return [];
|
|
82
|
+
const kept = new Set(keep.map((id) => `${cachePrefix}:${id}`));
|
|
83
|
+
const removed = [];
|
|
84
|
+
for (const name of await cacheStorage.keys()) {
|
|
85
|
+
if (!name.startsWith(`${cachePrefix}:`) || kept.has(name))
|
|
86
|
+
continue;
|
|
87
|
+
await cacheStorage.delete(name);
|
|
88
|
+
removed.push(name);
|
|
89
|
+
}
|
|
90
|
+
return removed;
|
|
91
|
+
}
|
|
92
|
+
//# sourceMappingURL=asset-handler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"asset-handler.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/asset-handler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Offline composition: the gateway's own piercing, run inside the worker.
|
|
3
|
+
*
|
|
4
|
+
* This falls out of decisions already made rather than needing new machinery. The gateway core is
|
|
5
|
+
* runtime-neutral — no `node:` imports in the gateway, the registry, or the rewriter — and a service
|
|
6
|
+
* worker is a web-standard runtime. So the worker can hold the shell and the fragment documents and
|
|
7
|
+
* interleave them with the **same `pierceShellHtml` the server runs**, which matters more than the
|
|
8
|
+
* code reuse: a second implementation would be a second set of piercing bugs, discovered offline.
|
|
9
|
+
*
|
|
10
|
+
* **Why hold the pieces rather than the composed page.** Caching the finished HTML would be simpler
|
|
11
|
+
* and much less useful: the composed page is the cross-product of shell, fragments, and route, so a
|
|
12
|
+
* cache of finished pages only ever answers for routes the user already visited with the fragment
|
|
13
|
+
* versions they had. Holding the parts means a route visited once composes offline afterwards, and
|
|
14
|
+
* a fragment that updated on its own schedule updates its part alone.
|
|
15
|
+
*
|
|
16
|
+
* Two things this deliberately does not do. It never composes when the network is reachable — the
|
|
17
|
+
* gateway is authoritative and its answer is always preferred — and it never caches a *composed*
|
|
18
|
+
* response, because a page pierced against a registry that has since changed is precisely the stale
|
|
19
|
+
* artifact the rest of this package exists to avoid.
|
|
20
|
+
*/
|
|
21
|
+
export interface OfflineCompositionOptions {
|
|
22
|
+
/**
|
|
23
|
+
* Where the pinned registry snapshot is served from.
|
|
24
|
+
*
|
|
25
|
+
* Content-addressed and immutable, so it is cacheable under its id forever with no revalidation —
|
|
26
|
+
* which is what makes an offline compose possible at all: piercing needs to know which fragments
|
|
27
|
+
* claim this URL, and that answer must survive with no server to ask.
|
|
28
|
+
*/
|
|
29
|
+
snapshotUrl: string;
|
|
30
|
+
cachePrefix?: string;
|
|
31
|
+
caches?: CacheStorage;
|
|
32
|
+
fetch?: typeof fetch;
|
|
33
|
+
/** Called when a page is composed offline, so a shell can say so rather than look merely slow. */
|
|
34
|
+
onCompose?: (event: {
|
|
35
|
+
pathname: string;
|
|
36
|
+
fragmentIds: string[];
|
|
37
|
+
}) => void;
|
|
38
|
+
}
|
|
39
|
+
/**
|
|
40
|
+
* A navigation handler that composes from cache when the network is gone.
|
|
41
|
+
*
|
|
42
|
+
* ```js
|
|
43
|
+
* const compose = braidNavigationHandler({ snapshotUrl: '/__braid/registry/pinned.json' });
|
|
44
|
+
* self.addEventListener('fetch', (event) => {
|
|
45
|
+
* const handled = compose(event.request);
|
|
46
|
+
* if (handled) event.respondWith(handled);
|
|
47
|
+
* });
|
|
48
|
+
* ```
|
|
49
|
+
*/
|
|
50
|
+
export declare function braidNavigationHandler(options: OfflineCompositionOptions): (request: Request) => Promise<Response> | null;
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import { BRAID_DOCUMENT_PREFIX, Registry, pierceShellHtml, } from '@braidlabs/gateway';
|
|
2
|
+
const DEFAULT_CACHE_PREFIX = 'braid-frag';
|
|
3
|
+
const SHELL_CACHE = 'braid-shell';
|
|
4
|
+
const SNAPSHOT_CACHE = 'braid-snapshot';
|
|
5
|
+
/**
|
|
6
|
+
* A navigation handler that composes from cache when the network is gone.
|
|
7
|
+
*
|
|
8
|
+
* ```js
|
|
9
|
+
* const compose = braidNavigationHandler({ snapshotUrl: '/__braid/registry/pinned.json' });
|
|
10
|
+
* self.addEventListener('fetch', (event) => {
|
|
11
|
+
* const handled = compose(event.request);
|
|
12
|
+
* if (handled) event.respondWith(handled);
|
|
13
|
+
* });
|
|
14
|
+
* ```
|
|
15
|
+
*/
|
|
16
|
+
export function braidNavigationHandler(options) {
|
|
17
|
+
const cachePrefix = options.cachePrefix ?? DEFAULT_CACHE_PREFIX;
|
|
18
|
+
const cacheStorage = options.caches ?? globalThis.caches;
|
|
19
|
+
const networkFetch = options.fetch ?? globalThis.fetch;
|
|
20
|
+
return (request) => {
|
|
21
|
+
if (request.method !== 'GET' || !isNavigation(request))
|
|
22
|
+
return null;
|
|
23
|
+
return handle(request);
|
|
24
|
+
};
|
|
25
|
+
async function handle(request) {
|
|
26
|
+
try {
|
|
27
|
+
const response = await networkFetch(request);
|
|
28
|
+
// Kept for the next outage rather than awaited: the user is waiting on this navigation, and
|
|
29
|
+
// refreshing the parts is work for a future load.
|
|
30
|
+
void refresh(request).catch(() => undefined);
|
|
31
|
+
return response;
|
|
32
|
+
}
|
|
33
|
+
catch (error) {
|
|
34
|
+
const composed = await composeFromCache(request);
|
|
35
|
+
if (composed)
|
|
36
|
+
return composed;
|
|
37
|
+
throw error;
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
/**
|
|
41
|
+
* Re-fetches the parts a compose would need: the unpierced shell, and each matching fragment's
|
|
42
|
+
* prepared document.
|
|
43
|
+
*
|
|
44
|
+
* The shell is requested with `sec-fetch-dest: empty`, which is how the gateway distinguishes a
|
|
45
|
+
* soft-navigation fetch from a document request — so it answers with the shell alone. That is not
|
|
46
|
+
* a trick; it is the same distinction the `Vary` header on every pierced response already
|
|
47
|
+
* declares, used from the other side.
|
|
48
|
+
*/
|
|
49
|
+
async function refresh(request) {
|
|
50
|
+
if (!cacheStorage)
|
|
51
|
+
return;
|
|
52
|
+
const url = new URL(request.url);
|
|
53
|
+
const manifests = await loadSnapshot(url.origin);
|
|
54
|
+
if (!manifests)
|
|
55
|
+
return;
|
|
56
|
+
const registry = new Registry([...manifests]);
|
|
57
|
+
const matches = await registry.matchPierceRoutes(url.pathname);
|
|
58
|
+
const shell = await networkFetch(new Request(request.url, { headers: { 'sec-fetch-dest': 'empty' } }));
|
|
59
|
+
if (shell.ok) {
|
|
60
|
+
const shellCache = await cacheStorage.open(SHELL_CACHE);
|
|
61
|
+
await shellCache.put(shellKey(url), shell.clone());
|
|
62
|
+
}
|
|
63
|
+
for (const manifest of matches) {
|
|
64
|
+
const documentUrl = `${url.origin}${BRAID_DOCUMENT_PREFIX}${encodeURIComponent(manifest.id)}${documentPath(manifest, url)}`;
|
|
65
|
+
const response = await networkFetch(documentUrl);
|
|
66
|
+
if (!response.ok)
|
|
67
|
+
continue;
|
|
68
|
+
// In the fragment's own partition, so pruning a retired fragment takes its documents too.
|
|
69
|
+
const cache = await cacheStorage.open(`${cachePrefix}:${manifest.id}`);
|
|
70
|
+
await cache.put(documentUrl, response.clone());
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
async function composeFromCache(request) {
|
|
74
|
+
if (!cacheStorage)
|
|
75
|
+
return null;
|
|
76
|
+
const url = new URL(request.url);
|
|
77
|
+
const shellCache = await cacheStorage.open(SHELL_CACHE);
|
|
78
|
+
const shell = await shellCache.match(shellKey(url));
|
|
79
|
+
if (!shell?.body)
|
|
80
|
+
return null;
|
|
81
|
+
const manifests = await loadSnapshot(url.origin);
|
|
82
|
+
if (!manifests)
|
|
83
|
+
return null;
|
|
84
|
+
const registry = new Registry([...manifests]);
|
|
85
|
+
const matches = await registry.matchPierceRoutes(url.pathname);
|
|
86
|
+
const fragments = [];
|
|
87
|
+
for (const manifest of matches) {
|
|
88
|
+
const cache = await cacheStorage.open(`${cachePrefix}:${manifest.id}`);
|
|
89
|
+
const documentUrl = `${url.origin}${BRAID_DOCUMENT_PREFIX}${encodeURIComponent(manifest.id)}${documentPath(manifest, url)}`;
|
|
90
|
+
const cached = await cache.match(documentUrl);
|
|
91
|
+
// A fragment with nothing cached pierces nothing and is marked, exactly as a failed fetch is
|
|
92
|
+
// server-side: the slot degrades to a placeholder rather than the page failing.
|
|
93
|
+
fragments.push({
|
|
94
|
+
fragmentId: manifest.id,
|
|
95
|
+
content: cached?.body ?? null,
|
|
96
|
+
...(cached?.body ? {} : { fallbackReason: 'placeholder' }),
|
|
97
|
+
...(manifest.src === undefined ? {} : { src: manifest.src }),
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
options.onCompose?.({ pathname: url.pathname, fragmentIds: matches.map((manifest) => manifest.id) });
|
|
101
|
+
return new Response(pierceShellHtml({ shell: shell.body, fragments }), {
|
|
102
|
+
headers: {
|
|
103
|
+
'content-type': 'text/html; charset=utf-8',
|
|
104
|
+
// Says plainly where this page came from. A composed-offline page that looks identical to a
|
|
105
|
+
// served one is a page nobody can debug.
|
|
106
|
+
'x-braid-composed': 'offline',
|
|
107
|
+
},
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
/** The snapshot, from cache first — offline is the case that needs it. */
|
|
111
|
+
async function loadSnapshot(origin) {
|
|
112
|
+
if (!cacheStorage)
|
|
113
|
+
return null;
|
|
114
|
+
const url = new URL(options.snapshotUrl, origin).href;
|
|
115
|
+
const cache = await cacheStorage.open(SNAPSHOT_CACHE);
|
|
116
|
+
let response = await cache.match(url);
|
|
117
|
+
if (!response) {
|
|
118
|
+
try {
|
|
119
|
+
const fresh = await networkFetch(url);
|
|
120
|
+
if (!fresh.ok)
|
|
121
|
+
return null;
|
|
122
|
+
await cache.put(url, fresh.clone());
|
|
123
|
+
response = fresh;
|
|
124
|
+
}
|
|
125
|
+
catch {
|
|
126
|
+
return null;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
try {
|
|
130
|
+
const snapshot = (await response.json());
|
|
131
|
+
return Array.isArray(snapshot) ? snapshot : (snapshot.manifests ?? null);
|
|
132
|
+
}
|
|
133
|
+
catch {
|
|
134
|
+
return null;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
/** Unbound fragments are fetched at their own path, bound ones at the page's — as on the server. */
|
|
139
|
+
function documentPath(manifest, url) {
|
|
140
|
+
return manifest.bound === false && manifest.src ? manifest.src : `${url.pathname}${url.search}`;
|
|
141
|
+
}
|
|
142
|
+
/** Keyed by path alone: a shell does not vary by query, and keying on one would cache N copies. */
|
|
143
|
+
function shellKey(url) {
|
|
144
|
+
return `${url.origin}${url.pathname}`;
|
|
145
|
+
}
|
|
146
|
+
function isNavigation(request) {
|
|
147
|
+
if (request.mode === 'navigate')
|
|
148
|
+
return true;
|
|
149
|
+
// `mode` is not always populated outside a browser; the accept header is the same signal the
|
|
150
|
+
// gateway itself falls back to.
|
|
151
|
+
return request.headers.get('accept')?.includes('text/html') ?? false;
|
|
152
|
+
}
|
|
153
|
+
//# sourceMappingURL=compose.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"compose.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/compose.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Realm stub precaching: a fragment boot with no round trip in it.
|
|
3
|
+
*
|
|
4
|
+
* A realm stub is the tiny document a fragment's realm is created from. They are identical per URL
|
|
5
|
+
* and vary on nothing — which is what the protocol's path split bought — so they are the one thing
|
|
6
|
+
* in this system that can be fetched before anybody asks for it without guessing.
|
|
7
|
+
*
|
|
8
|
+
* Kept deliberately small in scope: stubs only. Precaching a fragment's *application* bundles would
|
|
9
|
+
* mean downloading every fragment on the origin whether or not the user ever visits a page that
|
|
10
|
+
* composes them, which trades a round trip the user might make for bandwidth they certainly spend.
|
|
11
|
+
*/
|
|
12
|
+
export interface PrecacheOptions {
|
|
13
|
+
cachePrefix?: string;
|
|
14
|
+
caches?: CacheStorage;
|
|
15
|
+
fetch?: typeof fetch;
|
|
16
|
+
/** Where the stubs live. Defaults to the current origin. */
|
|
17
|
+
origin?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface PrecacheResult {
|
|
20
|
+
cached: string[];
|
|
21
|
+
/** Stubs that could not be fetched, with why. A precache failure must not fail the install. */
|
|
22
|
+
failed: {
|
|
23
|
+
url: string;
|
|
24
|
+
reason: string;
|
|
25
|
+
}[];
|
|
26
|
+
}
|
|
27
|
+
/**
|
|
28
|
+
* Fetches and caches each fragment's realm stub.
|
|
29
|
+
*
|
|
30
|
+
* Call it from `install`, wrapped in `waitUntil`. Failures are collected rather than thrown: a
|
|
31
|
+
* worker that refuses to install because one fragment's origin was briefly unreachable is worse
|
|
32
|
+
* than a worker that installs and fetches that stub on demand like it used to.
|
|
33
|
+
*/
|
|
34
|
+
export declare function precacheRealmStubs(fragmentIds: readonly string[], options?: PrecacheOptions): Promise<PrecacheResult>;
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
import { BRAID_REALM_PREFIX } from '@braidlabs/gateway';
|
|
2
|
+
const DEFAULT_CACHE_PREFIX = 'braid-frag';
|
|
3
|
+
/**
|
|
4
|
+
* Fetches and caches each fragment's realm stub.
|
|
5
|
+
*
|
|
6
|
+
* Call it from `install`, wrapped in `waitUntil`. Failures are collected rather than thrown: a
|
|
7
|
+
* worker that refuses to install because one fragment's origin was briefly unreachable is worse
|
|
8
|
+
* than a worker that installs and fetches that stub on demand like it used to.
|
|
9
|
+
*/
|
|
10
|
+
export async function precacheRealmStubs(fragmentIds, options = {}) {
|
|
11
|
+
const cachePrefix = options.cachePrefix ?? DEFAULT_CACHE_PREFIX;
|
|
12
|
+
const cacheStorage = options.caches ?? globalThis.caches;
|
|
13
|
+
const networkFetch = options.fetch ?? globalThis.fetch;
|
|
14
|
+
const origin = options.origin ?? globalThis.location?.origin ?? '';
|
|
15
|
+
const result = { cached: [], failed: [] };
|
|
16
|
+
if (!cacheStorage)
|
|
17
|
+
return result;
|
|
18
|
+
for (const fragmentId of fragmentIds) {
|
|
19
|
+
const url = `${origin}${BRAID_REALM_PREFIX}${fragmentId}/`;
|
|
20
|
+
try {
|
|
21
|
+
const response = await networkFetch(url);
|
|
22
|
+
if (!response.ok) {
|
|
23
|
+
result.failed.push({ url, reason: `HTTP ${response.status}` });
|
|
24
|
+
continue;
|
|
25
|
+
}
|
|
26
|
+
const cache = await cacheStorage.open(`${cachePrefix}:${fragmentId}`);
|
|
27
|
+
await cache.put(url, response);
|
|
28
|
+
result.cached.push(url);
|
|
29
|
+
}
|
|
30
|
+
catch (error) {
|
|
31
|
+
result.failed.push({ url, reason: error instanceof Error ? error.message : String(error) });
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
return result;
|
|
35
|
+
}
|
|
36
|
+
//# sourceMappingURL=precache.js.map
|
|
@@ -0,0 +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"}
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
import { type RecordDriver } from '@braidlabs/data';
|
|
2
|
+
import type { AssetReport } from './asset-handler.js';
|
|
3
|
+
import type { SkewDiagnosis } from './skew-report.js';
|
|
4
|
+
/**
|
|
5
|
+
* Durable reports: telemetry that survives the tab being closed.
|
|
6
|
+
*
|
|
7
|
+
* The worker already knows things nobody else can see — that a chunk was served from cache after the
|
|
8
|
+
* origin 404'd it, that its build disagrees with the page's. Handed to an `onReport` callback those
|
|
9
|
+
* facts are fire-and-forget: a `fetch` started as the tab is closing races page teardown and usually
|
|
10
|
+
* loses, and the reports that go missing are disproportionately the interesting ones. A user hitting
|
|
11
|
+
* a broken deploy is a user who closes the tab.
|
|
12
|
+
*
|
|
13
|
+
* Background Sync fixes exactly that: records are queued durably and flushed when the browser next
|
|
14
|
+
* says it can, whether or not any page is open. It is **Chromium-only**, so this is an enhancement
|
|
15
|
+
* to sending reports rather than a replacement for it — the queue flushes opportunistically too, and
|
|
16
|
+
* a browser without `sync` simply gets the opportunistic path.
|
|
17
|
+
*
|
|
18
|
+
* The queue itself is `@braidlabs/data`'s outbox rather than a second implementation: one record per
|
|
19
|
+
* entry, so appending never reads the queue first and two contexts cannot lose each other's writes.
|
|
20
|
+
* That problem was solved once already, and solving it again here would mean debugging it again too.
|
|
21
|
+
*/
|
|
22
|
+
export type BraidReport = {
|
|
23
|
+
kind: 'asset';
|
|
24
|
+
at: string;
|
|
25
|
+
report: AssetReport;
|
|
26
|
+
} | {
|
|
27
|
+
kind: 'skew';
|
|
28
|
+
at: string;
|
|
29
|
+
diagnosis: SkewDiagnosis;
|
|
30
|
+
};
|
|
31
|
+
export interface ReportQueueOptions {
|
|
32
|
+
/** Where reports are POSTed, as JSON. */
|
|
33
|
+
endpoint: string;
|
|
34
|
+
/** Storage for the queue. In a worker, `indexedDbRecordDriver()`. */
|
|
35
|
+
driver: RecordDriver;
|
|
36
|
+
/** Distinguishes this worker's reports from anything else sharing the origin's storage. */
|
|
37
|
+
owner?: string;
|
|
38
|
+
/** Batch size per flush. Bounded so one flush cannot become an unbounded upload. */
|
|
39
|
+
batchSize?: number;
|
|
40
|
+
fetch?: typeof fetch;
|
|
41
|
+
}
|
|
42
|
+
export interface FlushOutcome {
|
|
43
|
+
sent: number;
|
|
44
|
+
/** Entries left queued — the endpoint refused them, and they are still owed. */
|
|
45
|
+
remaining: number;
|
|
46
|
+
}
|
|
47
|
+
export interface ReportQueue {
|
|
48
|
+
record(report: BraidReport): Promise<void>;
|
|
49
|
+
flush(): Promise<FlushOutcome>;
|
|
50
|
+
pending(): Promise<number>;
|
|
51
|
+
}
|
|
52
|
+
export declare function createReportQueue(options: ReportQueueOptions): ReportQueue;
|
|
53
|
+
/** The Background Sync tag this queue registers and answers to. */
|
|
54
|
+
export declare const BRAID_REPORT_SYNC_TAG = "braid-sw:reports";
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import { createOutbox } from '@braidlabs/data';
|
|
2
|
+
const REPORT_MUTATION = 'braid-sw.report';
|
|
3
|
+
export function createReportQueue(options) {
|
|
4
|
+
const networkFetch = options.fetch ?? globalThis.fetch;
|
|
5
|
+
const batchSize = options.batchSize ?? 50;
|
|
6
|
+
const outbox = createOutbox({
|
|
7
|
+
driver: options.driver,
|
|
8
|
+
owner: options.owner ?? 'braid-sw',
|
|
9
|
+
collection: 'braid-sw-reports',
|
|
10
|
+
});
|
|
11
|
+
return {
|
|
12
|
+
async record(report) {
|
|
13
|
+
await outbox.enqueue({ mutationId: REPORT_MUTATION, input: report });
|
|
14
|
+
},
|
|
15
|
+
async pending() {
|
|
16
|
+
return (await outbox.mine()).length;
|
|
17
|
+
},
|
|
18
|
+
/**
|
|
19
|
+
* Sends what is queued, oldest first, and removes only what the endpoint accepted.
|
|
20
|
+
*
|
|
21
|
+
* One request per batch rather than per record: a worker waking to flush forty reports should
|
|
22
|
+
* not make forty requests, and the endpoint learns more from a batch than from its parts. A
|
|
23
|
+
* refused batch stays queued in full — partial credit would need the endpoint to say which
|
|
24
|
+
* records it took, and no telemetry endpoint does.
|
|
25
|
+
*/
|
|
26
|
+
async flush() {
|
|
27
|
+
const entries = (await outbox.mine()).slice(0, batchSize);
|
|
28
|
+
if (entries.length === 0)
|
|
29
|
+
return { sent: 0, remaining: 0 };
|
|
30
|
+
try {
|
|
31
|
+
const response = await networkFetch(options.endpoint, {
|
|
32
|
+
method: 'POST',
|
|
33
|
+
headers: { 'content-type': 'application/json' },
|
|
34
|
+
body: JSON.stringify({ reports: entries.map((entry) => entry.input) }),
|
|
35
|
+
// Reports are diagnostics, not a session: sending credentials with them would attach
|
|
36
|
+
// identity to data nobody asked to have attached.
|
|
37
|
+
credentials: 'omit',
|
|
38
|
+
keepalive: true,
|
|
39
|
+
});
|
|
40
|
+
if (!response.ok) {
|
|
41
|
+
await Promise.all(entries.map((entry) => outbox.recordFailure(entry.id, `HTTP ${response.status}`)));
|
|
42
|
+
return { sent: 0, remaining: (await outbox.mine()).length };
|
|
43
|
+
}
|
|
44
|
+
await Promise.all(entries.map((entry) => outbox.remove(entry.id)));
|
|
45
|
+
return { sent: entries.length, remaining: (await outbox.mine()).length };
|
|
46
|
+
}
|
|
47
|
+
catch (error) {
|
|
48
|
+
// Still offline. The whole point of the queue is that this is survivable, so the entries
|
|
49
|
+
// stay exactly where they are.
|
|
50
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
51
|
+
await Promise.all(entries.map((entry) => outbox.recordFailure(entry.id, message)));
|
|
52
|
+
return { sent: 0, remaining: (await outbox.mine()).length };
|
|
53
|
+
}
|
|
54
|
+
},
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
/** The Background Sync tag this queue registers and answers to. */
|
|
58
|
+
export const BRAID_REPORT_SYNC_TAG = 'braid-sw:reports';
|
|
59
|
+
//# sourceMappingURL=reports.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"reports.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/reports.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,49 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker's own version boundary.
|
|
3
|
+
*
|
|
4
|
+
* A service worker is a long-lived deployment artifact that updates on its own schedule, which
|
|
5
|
+
* makes it a skew vector inside a system whose entire purpose is managing skew. It gets the same
|
|
6
|
+
* discipline as everything else here: it knows its build and the registry snapshot it is serving,
|
|
7
|
+
* and when those disagree with the page it is answering for, it says so.
|
|
8
|
+
*
|
|
9
|
+
* The alternative is the failure worth designing against — a worker that quietly serves yesterday's
|
|
10
|
+
* assets to today's document. That does not look like a worker problem to anyone debugging it. It
|
|
11
|
+
* looks like an application bug, in an application that is fine.
|
|
12
|
+
*/
|
|
13
|
+
export type SkewSeverity = 'ok' | 'worker-behind' | 'worker-ahead' | 'snapshot-mismatch';
|
|
14
|
+
export interface WorkerVersion {
|
|
15
|
+
/** The build this worker script was shipped as. */
|
|
16
|
+
buildId?: string;
|
|
17
|
+
/** The registry snapshot id this worker is composing against. */
|
|
18
|
+
snapshotId?: string;
|
|
19
|
+
}
|
|
20
|
+
export interface PageVersion {
|
|
21
|
+
buildId?: string;
|
|
22
|
+
snapshotId?: string;
|
|
23
|
+
}
|
|
24
|
+
export interface SkewDiagnosis {
|
|
25
|
+
severity: SkewSeverity;
|
|
26
|
+
message: string;
|
|
27
|
+
worker: WorkerVersion;
|
|
28
|
+
page: PageVersion;
|
|
29
|
+
/** What to do about it, phrased as an instruction. */
|
|
30
|
+
fixHint?: string;
|
|
31
|
+
}
|
|
32
|
+
/**
|
|
33
|
+
* Compares the worker's version with the page's.
|
|
34
|
+
*
|
|
35
|
+
* Deliberately not an error: a mismatch is usually transient and self-healing — a deploy landed
|
|
36
|
+
* while a tab was open, and the worker updates on the next navigation. What it must never be is
|
|
37
|
+
* invisible, so this returns something a host can log, report, or show, and the prebuilt worker
|
|
38
|
+
* always logs it.
|
|
39
|
+
*/
|
|
40
|
+
export declare function diagnoseSkew(worker: WorkerVersion, page: PageVersion): SkewDiagnosis;
|
|
41
|
+
/**
|
|
42
|
+
* The header a gateway-served worker can read its page's build from.
|
|
43
|
+
*
|
|
44
|
+
* A header rather than a query parameter on the script URL, on purpose: baking the build into the
|
|
45
|
+
* worker's URL would change the URL on every deploy, and a changed worker URL is a new worker with
|
|
46
|
+
* its own install-and-wait lifecycle. Configuration churn should not become worker churn.
|
|
47
|
+
*/
|
|
48
|
+
export declare const BRAID_BUILD_HEADER = "x-braid-build";
|
|
49
|
+
export declare const BRAID_SNAPSHOT_HEADER = "x-braid-snapshot";
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The worker's own version boundary.
|
|
3
|
+
*
|
|
4
|
+
* A service worker is a long-lived deployment artifact that updates on its own schedule, which
|
|
5
|
+
* makes it a skew vector inside a system whose entire purpose is managing skew. It gets the same
|
|
6
|
+
* discipline as everything else here: it knows its build and the registry snapshot it is serving,
|
|
7
|
+
* and when those disagree with the page it is answering for, it says so.
|
|
8
|
+
*
|
|
9
|
+
* The alternative is the failure worth designing against — a worker that quietly serves yesterday's
|
|
10
|
+
* assets to today's document. That does not look like a worker problem to anyone debugging it. It
|
|
11
|
+
* looks like an application bug, in an application that is fine.
|
|
12
|
+
*/
|
|
13
|
+
/**
|
|
14
|
+
* Compares the worker's version with the page's.
|
|
15
|
+
*
|
|
16
|
+
* Deliberately not an error: a mismatch is usually transient and self-healing — a deploy landed
|
|
17
|
+
* while a tab was open, and the worker updates on the next navigation. What it must never be is
|
|
18
|
+
* invisible, so this returns something a host can log, report, or show, and the prebuilt worker
|
|
19
|
+
* always logs it.
|
|
20
|
+
*/
|
|
21
|
+
export function diagnoseSkew(worker, page) {
|
|
22
|
+
const base = { worker, page };
|
|
23
|
+
if (worker.snapshotId && page.snapshotId && worker.snapshotId !== page.snapshotId) {
|
|
24
|
+
return {
|
|
25
|
+
...base,
|
|
26
|
+
severity: 'snapshot-mismatch',
|
|
27
|
+
message: `braid-sw: this worker is composing against registry snapshot "${worker.snapshotId}" and the ` +
|
|
28
|
+
`page was rendered against "${page.snapshotId}" — the two disagree about which fragments exist`,
|
|
29
|
+
fixHint: 'the worker updates on the next navigation; if it persists, the gateway is serving a worker ' +
|
|
30
|
+
'pinned to a snapshot the shell no longer deploys',
|
|
31
|
+
};
|
|
32
|
+
}
|
|
33
|
+
if (worker.buildId && page.buildId && worker.buildId !== page.buildId) {
|
|
34
|
+
// Which side is stale is not decidable from two opaque ids, and guessing would produce a
|
|
35
|
+
// confident, wrong instruction. Both directions are named; the fix is the same either way.
|
|
36
|
+
return {
|
|
37
|
+
...base,
|
|
38
|
+
severity: 'worker-behind',
|
|
39
|
+
message: `braid-sw: worker build "${worker.buildId}" is serving a page built as "${page.buildId}" — ` +
|
|
40
|
+
'assets may be answered from the older build',
|
|
41
|
+
fixHint: 'reload once the worker has activated, or call skipWaiting in your own install handler',
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
return { ...base, severity: 'ok', message: 'braid-sw: worker and page agree' };
|
|
45
|
+
}
|
|
46
|
+
/**
|
|
47
|
+
* The header a gateway-served worker can read its page's build from.
|
|
48
|
+
*
|
|
49
|
+
* A header rather than a query parameter on the script URL, on purpose: baking the build into the
|
|
50
|
+
* worker's URL would change the URL on every deploy, and a changed worker URL is a new worker with
|
|
51
|
+
* its own install-and-wait lifecycle. Configuration churn should not become worker churn.
|
|
52
|
+
*/
|
|
53
|
+
export const BRAID_BUILD_HEADER = 'x-braid-build';
|
|
54
|
+
export const BRAID_SNAPSHOT_HEADER = 'x-braid-snapshot';
|
|
55
|
+
//# sourceMappingURL=skew-report.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"skew-report.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/skew-report.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AAyBH;;;;;;;GAOG;AACH,MAAM,UAAU,YAAY,CAAC,MAAqB,EAAE,IAAiB;IACnE,MAAM,IAAI,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;IAE9B,IAAI,MAAM,CAAC,UAAU,IAAI,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,UAAU,KAAK,IAAI,CAAC,UAAU,EAAE,CAAC;QAClF,OAAO;YACL,GAAG,IAAI;YACP,QAAQ,EAAE,mBAAmB;YAC7B,OAAO,EACL,iEAAiE,MAAM,CAAC,UAAU,YAAY;gBAC9F,8BAA8B,IAAI,CAAC,UAAU,kDAAkD;YACjG,OAAO,EACL,6FAA6F;gBAC7F,kDAAkD;SACrD,CAAC;IACJ,CAAC;IAED,IAAI,MAAM,CAAC,OAAO,IAAI,IAAI,CAAC,OAAO,IAAI,MAAM,CAAC,OAAO,KAAK,IAAI,CAAC,OAAO,EAAE,CAAC;QACtE,yFAAyF;QACzF,2FAA2F;QAC3F,OAAO;YACL,GAAG,IAAI;YACP,QAAQ,EAAE,eAAe;YACzB,OAAO,EACL,2BAA2B,MAAM,CAAC,OAAO,iCAAiC,IAAI,CAAC,OAAO,MAAM;gBAC5F,6CAA6C;YAC/C,OAAO,EAAE,uFAAuF;SACjG,CAAC;IACJ,CAAC;IAED,OAAO,EAAE,GAAG,IAAI,EAAE,QAAQ,EAAE,IAAI,EAAE,OAAO,EAAE,iCAAiC,EAAE,CAAC;AACjF,CAAC;AAED;;;;;;GAMG;AACH,MAAM,CAAC,MAAM,kBAAkB,GAAG,eAAe,CAAC;AAClD,MAAM,CAAC,MAAM,qBAAqB,GAAG,kBAAkB,CAAC"}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
import { type BraidFetchHandlerOptions } from './asset-handler.js';
|
|
2
|
+
import { type OfflineCompositionOptions } from './compose.js';
|
|
3
|
+
import { type ReportQueueOptions } from './reports.js';
|
|
4
|
+
/**
|
|
5
|
+
* The complete worker, for shells that do not already have one.
|
|
6
|
+
*
|
|
7
|
+
* Both shapes ship because the assumption that an application already has a service worker does not
|
|
8
|
+
* hold — least of all for internal enterprise apps, which is much of the audience. A shell that has
|
|
9
|
+
* a worker composes {@link braidFetchHandler} into it and keeps ownership of its own `fetch` event;
|
|
10
|
+
* a shell that has none calls this and authors nothing.
|
|
11
|
+
*/
|
|
12
|
+
export interface BraidWorkerOptions extends BraidFetchHandlerOptions {
|
|
13
|
+
/** Fragment ids whose realm stubs are precached at install. */
|
|
14
|
+
precache?: readonly string[];
|
|
15
|
+
/** The registry snapshot this worker composes against, for the skew diagnostic. */
|
|
16
|
+
snapshotId?: string;
|
|
17
|
+
/**
|
|
18
|
+
* Take over open pages as soon as this worker activates.
|
|
19
|
+
*
|
|
20
|
+
* Off by default, and that default is the conservative one for a reason: claiming clients swaps
|
|
21
|
+
* the worker underneath a page that is already running, so a page mid-session starts being served
|
|
22
|
+
* by a build it did not load against. Waiting for the next navigation is the boring, correct
|
|
23
|
+
* behaviour; turn this on when you would rather have the update than the continuity.
|
|
24
|
+
*/
|
|
25
|
+
claimClients?: boolean;
|
|
26
|
+
/**
|
|
27
|
+
* Compose pages from cache when the network is gone.
|
|
28
|
+
*
|
|
29
|
+
* Opt-in, because it changes what a failed navigation *is*: without it an offline navigation is
|
|
30
|
+
* the browser's offline page, which is at least unambiguous. With it, a route the user has
|
|
31
|
+
* visited before comes back composed — so a shell turning this on should say so on the page, and
|
|
32
|
+
* the `x-braid-composed: offline` header is there to make that possible.
|
|
33
|
+
*/
|
|
34
|
+
offline?: OfflineCompositionOptions;
|
|
35
|
+
/**
|
|
36
|
+
* Send the worker's own reports durably, surviving a closed tab.
|
|
37
|
+
*
|
|
38
|
+
* Without this, `onReport` is fire-and-forget and the reports most likely to be lost are the ones
|
|
39
|
+
* worth having: a user who hits a broken deploy is a user who closes the tab.
|
|
40
|
+
*/
|
|
41
|
+
reports?: Omit<ReportQueueOptions, 'fetch'>;
|
|
42
|
+
}
|
|
43
|
+
/** The subset of the worker global this uses, so a test can supply one without a worker. */
|
|
44
|
+
export interface WorkerScopeLike {
|
|
45
|
+
addEventListener(type: string, listener: (event: never) => void): void;
|
|
46
|
+
skipWaiting?(): Promise<void>;
|
|
47
|
+
clients?: {
|
|
48
|
+
claim(): Promise<void>;
|
|
49
|
+
};
|
|
50
|
+
registration?: unknown;
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Installs Braid's handlers on a worker scope.
|
|
54
|
+
*
|
|
55
|
+
* ```js
|
|
56
|
+
* // sw.js
|
|
57
|
+
* import { setupBraidWorker } from '@braidlabs/sw';
|
|
58
|
+
* setupBraidWorker({ buildId: BUILD_ID, precache: ['billing', 'notifications'] });
|
|
59
|
+
* ```
|
|
60
|
+
*/
|
|
61
|
+
export declare function setupBraidWorker(options?: BraidWorkerOptions): void;
|
|
@@ -0,0 +1,96 @@
|
|
|
1
|
+
import { braidFetchHandler, pruneFragmentCaches } from './asset-handler.js';
|
|
2
|
+
import { braidNavigationHandler } from './compose.js';
|
|
3
|
+
import { precacheRealmStubs } from './precache.js';
|
|
4
|
+
import { diagnoseSkew } from './skew-report.js';
|
|
5
|
+
import { BRAID_REPORT_SYNC_TAG, createReportQueue } from './reports.js';
|
|
6
|
+
/**
|
|
7
|
+
* Installs Braid's handlers on a worker scope.
|
|
8
|
+
*
|
|
9
|
+
* ```js
|
|
10
|
+
* // sw.js
|
|
11
|
+
* import { setupBraidWorker } from '@braidlabs/sw';
|
|
12
|
+
* setupBraidWorker({ buildId: BUILD_ID, precache: ['billing', 'notifications'] });
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function setupBraidWorker(options = {}) {
|
|
16
|
+
const scope = globalThis;
|
|
17
|
+
const reports = options.reports
|
|
18
|
+
? createReportQueue({ ...options.reports, ...(options.fetch ? { fetch: options.fetch } : {}) })
|
|
19
|
+
: null;
|
|
20
|
+
const handler = braidFetchHandler({
|
|
21
|
+
...options,
|
|
22
|
+
...(reports
|
|
23
|
+
? {
|
|
24
|
+
onReport: (report) => {
|
|
25
|
+
options.onReport?.(report);
|
|
26
|
+
// Queued, not sent: the send is the part that loses races with page teardown.
|
|
27
|
+
void reports.record({ kind: 'asset', at: new Date().toISOString(), report });
|
|
28
|
+
},
|
|
29
|
+
}
|
|
30
|
+
: {}),
|
|
31
|
+
});
|
|
32
|
+
const version = {
|
|
33
|
+
...(options.buildId === undefined ? {} : { buildId: options.buildId }),
|
|
34
|
+
...(options.snapshotId === undefined ? {} : { snapshotId: options.snapshotId }),
|
|
35
|
+
};
|
|
36
|
+
scope.addEventListener('install', (event) => {
|
|
37
|
+
if (!options.precache?.length)
|
|
38
|
+
return;
|
|
39
|
+
// The precache is best-effort by construction: a worker that refuses to install because one
|
|
40
|
+
// fragment's origin blinked is worse than one that installs and fetches that stub on demand.
|
|
41
|
+
event.waitUntil?.(precacheRealmStubs(options.precache, options).then((result) => {
|
|
42
|
+
for (const failure of result.failed) {
|
|
43
|
+
console.warn(`braid-sw: could not precache ${failure.url} — ${failure.reason}`);
|
|
44
|
+
}
|
|
45
|
+
}));
|
|
46
|
+
});
|
|
47
|
+
scope.addEventListener('activate', (event) => {
|
|
48
|
+
const work = [];
|
|
49
|
+
if (options.precache?.length)
|
|
50
|
+
work.push(pruneFragmentCaches(options.precache, options));
|
|
51
|
+
if (options.claimClients && scope.clients)
|
|
52
|
+
work.push(scope.clients.claim());
|
|
53
|
+
// Background Sync is Chromium-only, so every activation also flushes opportunistically. A
|
|
54
|
+
// browser without `sync` gets this path and nothing else; one with it gets both.
|
|
55
|
+
if (reports)
|
|
56
|
+
work.push(reports.flush());
|
|
57
|
+
if (work.length)
|
|
58
|
+
event.waitUntil?.(Promise.all(work));
|
|
59
|
+
});
|
|
60
|
+
/**
|
|
61
|
+
* The durable path: the browser wakes the worker when it judges the network is back, with no page
|
|
62
|
+
* open and nothing racing teardown.
|
|
63
|
+
*/
|
|
64
|
+
if (reports) {
|
|
65
|
+
scope.addEventListener('sync', (event) => {
|
|
66
|
+
if (event.tag !== BRAID_REPORT_SYNC_TAG)
|
|
67
|
+
return;
|
|
68
|
+
event.waitUntil?.(reports.flush());
|
|
69
|
+
});
|
|
70
|
+
}
|
|
71
|
+
const navigate = options.offline ? braidNavigationHandler({ ...options, ...options.offline }) : null;
|
|
72
|
+
scope.addEventListener('fetch', (event) => {
|
|
73
|
+
// Assets first: they are the overwhelming majority, and a navigation is never one.
|
|
74
|
+
const handled = handler(event.request) ?? navigate?.(event.request);
|
|
75
|
+
if (handled)
|
|
76
|
+
event.respondWith?.(handled);
|
|
77
|
+
});
|
|
78
|
+
/**
|
|
79
|
+
* The page introduces itself, and the worker answers with what it is.
|
|
80
|
+
*
|
|
81
|
+
* A page cannot read a worker's build any other way, and the mismatch this surfaces is precisely
|
|
82
|
+
* the one that otherwise presents as an application bug in an application that is fine.
|
|
83
|
+
*/
|
|
84
|
+
scope.addEventListener('message', (event) => {
|
|
85
|
+
const data = event.data;
|
|
86
|
+
if (data?.type !== 'braid-sw:hello')
|
|
87
|
+
return;
|
|
88
|
+
const diagnosis = diagnoseSkew(version, data.page ?? {});
|
|
89
|
+
if (diagnosis.severity !== 'ok') {
|
|
90
|
+
console.warn(diagnosis.message);
|
|
91
|
+
void reports?.record({ kind: 'skew', at: new Date().toISOString(), diagnosis });
|
|
92
|
+
}
|
|
93
|
+
event.source?.postMessage?.({ type: 'braid-sw:version', diagnosis });
|
|
94
|
+
});
|
|
95
|
+
}
|
|
96
|
+
//# sourceMappingURL=worker.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"worker.js","sourceRoot":"","sources":["../../../../../libs/braid-sw/src/lib/worker.ts"],"names":[],"mappings":"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"}
|