@statewalker/webrun-http-browser 0.4.2 → 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +80 -7
- package/dist/core/deadline.d.ts +7 -0
- package/dist/core/deadline.d.ts.map +1 -0
- package/dist/core/index.d.ts +1 -0
- package/dist/core/index.d.ts.map +1 -1
- package/dist/core/service-worker-control.d.ts +72 -0
- package/dist/core/service-worker-control.d.ts.map +1 -0
- package/dist/index.js +236 -47
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +23 -6
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay-sw.js +18 -1
- package/dist/sw/sw-dispatcher.d.ts +22 -0
- package/dist/sw/sw-dispatcher.d.ts.map +1 -1
- package/dist/sw-worker.js +23 -2
- package/dist/sw.js +219 -36
- package/package.json +7 -3
- package/src/core/deadline.ts +31 -0
- package/src/core/index.ts +1 -0
- package/src/core/service-worker-control.ts +243 -0
- package/src/relay/index-sw.ts +5 -0
- package/src/relay/index.ts +65 -47
- package/src/sw/sw-dispatcher.ts +72 -45
package/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2022-2026 statewalker
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
package/README.md
CHANGED
|
@@ -48,7 +48,7 @@ npm install @statewalker/webrun-http-browser
|
|
|
48
48
|
| Subpath | Purpose |
|
|
49
49
|
| --- | --- |
|
|
50
50
|
| `@statewalker/webrun-http-browser` | Page-side relay API: `newRemoteRelayChannel`, `initHttpService`, `callHttpService`, `splitServiceUrl`, `initServiceWorker`, `newServiceWorkerPort`, `getRelayWindowMessageHandler`; the MessagePort call primitives (`callChannel`, `handleChannelCalls`, `newInvokationChannel`, `sendStream`, `handleStreams`, `newRegistry`); plus everything re-exported from `@statewalker/webrun-http-streams` (`HttpError`, the client/server stubs), `@statewalker/webrun-streams` (stream and error helpers) and the `MessageTarget` family from `@statewalker/webrun-rpc` |
|
|
51
|
-
| `@statewalker/webrun-http-browser/sw` | Same-origin adapter classes: `SwHttpAdapter` (page), `SwHttpDispatcher` (SW), `startHttpDispatcher` bootstrap |
|
|
51
|
+
| `@statewalker/webrun-http-browser/sw` | Same-origin adapter classes: `SwHttpAdapter` (page), `SwHttpDispatcher` (SW), `startHttpDispatcher` bootstrap; `start()` options `timeout` and `reloadIfUncontrolled` |
|
|
52
52
|
| `@statewalker/webrun-http-browser/relay-sw` | IIFE bundle of the relay SW runtime — load via `importScripts` from a loader script in your relay origin |
|
|
53
53
|
| `@statewalker/webrun-http-browser/sw-worker` | IIFE bundle of the same-origin SW runtime — ditto, for same-origin apps |
|
|
54
54
|
|
|
@@ -124,6 +124,45 @@ const { baseUrl } = await adapter.register(`${KEY}/api/`, async (request) => {
|
|
|
124
124
|
// fetch(`${baseUrl}anything`) is intercepted by the SW.
|
|
125
125
|
```
|
|
126
126
|
|
|
127
|
+
`start()` resolves once the worker is activated **and controls the page** —
|
|
128
|
+
only a controlled page's `fetch()` reaches the worker. Two options bound it:
|
|
129
|
+
|
|
130
|
+
| Option | Default | Meaning |
|
|
131
|
+
| --- | --- | --- |
|
|
132
|
+
| `timeout` | `30_000` | Upper bound, in ms, for the wait for the worker to activate, take control and answer the adapter's handshake. Past it `start()` rejects with a `ServiceWorkerControlError` (see `reason`) instead of waiting forever. |
|
|
133
|
+
| `reloadIfUncontrolled` | `false` | If the page is still uncontrolled once the worker is active, reload it once instead of rejecting (see below). |
|
|
134
|
+
|
|
135
|
+
#### When the page is not controlled
|
|
136
|
+
|
|
137
|
+
A page can load **uncontrolled although its worker is active**: a hard reload
|
|
138
|
+
(Ctrl+Shift+R / Cmd+Shift+R) bypasses ServiceWorkers for that load, and the
|
|
139
|
+
worker's `clients.claim()` already ran when it activated, so nothing ever
|
|
140
|
+
hands the page to it. (Firefox has also been seen leaving a second page of a
|
|
141
|
+
running worker uncontrolled.) `start()` then asks the worker to claim the page
|
|
142
|
+
again — a `CLAIM` call this package's workers answer with `clients.claim()` —
|
|
143
|
+
and waits for `controllerchange`. That takes the page over in Chromium and
|
|
144
|
+
Firefox, both verified by the browser tests.
|
|
145
|
+
|
|
146
|
+
If it still is not controlled (a worker that does not answer `CLAIM`, a page
|
|
147
|
+
outside the worker's scope), `start()` rejects with a
|
|
148
|
+
`ServiceWorkerControlError` whose `reason` is `"uncontrolled"` and whose message
|
|
149
|
+
says what happened and what to do. With `reloadIfUncontrolled: true` it reloads
|
|
150
|
+
the page instead — a normal reload is a controlled navigation — at most once:
|
|
151
|
+
a `sessionStorage` marker makes a second uncontrolled load reject rather than
|
|
152
|
+
loop. After a failure, calling `start()` again retries.
|
|
153
|
+
|
|
154
|
+
```ts
|
|
155
|
+
try {
|
|
156
|
+
await adapter.start();
|
|
157
|
+
} catch (error) {
|
|
158
|
+
if ((error as Error).name === "ServiceWorkerControlError") showReloadPrompt();
|
|
159
|
+
else throw error;
|
|
160
|
+
}
|
|
161
|
+
```
|
|
162
|
+
|
|
163
|
+
Check `name` or `reason`, not `instanceof`: each bundle of this package carries
|
|
164
|
+
its own copy of the class.
|
|
165
|
+
|
|
127
166
|
The SW script itself ships as a pre-built IIFE bundle. Put a tiny loader
|
|
128
167
|
next to your app pages so the SW's default scope covers them:
|
|
129
168
|
|
|
@@ -250,9 +289,14 @@ imports keep working after those extractions. Its own surface is below.
|
|
|
250
289
|
|
|
251
290
|
| Export | Kind | Purpose |
|
|
252
291
|
| --- | --- | --- |
|
|
253
|
-
| `initServiceWorker(opts)` | function | Registers a SW and resolves once it is activated
|
|
254
|
-
| `InitServiceWorkerOptions` | interface | `{ swUrl, scopeUrl?, type? }`. |
|
|
255
|
-
| `newServiceWorkerPort()` | function | A `MessagePort` that transparently bridges to the controlling SW. |
|
|
292
|
+
| `initServiceWorker(opts)` | function | Registers a SW and resolves with it once it is activated: the controller, or the registration's active worker when the page is not controlled (the relay only needs to message it). Rejects with a `ServiceWorkerControlError` past `timeout`. |
|
|
293
|
+
| `InitServiceWorkerOptions` | interface | `{ swUrl, scopeUrl?, type?, timeout? }`. |
|
|
294
|
+
| `newServiceWorkerPort(registration?)` | function | A `MessagePort` that transparently bridges to the controlling SW, or to `registration.active` while the page is not controlled. |
|
|
295
|
+
| `awaitActiveServiceWorker(registration, opts?)` | function | Resolves with the registration's worker once `activated`; rejects (`reason: "activation-timeout"`) past `timeout`. |
|
|
296
|
+
| `awaitServiceWorkerControl(registration, opts?)` | function | Resolves with the controller once the page is controlled, asking the worker to claim an uncontrolled page; bounded by `timeout`, optional `reloadIfUncontrolled`. What `SwHttpAdapter.start()` uses. |
|
|
297
|
+
| `handleClaimRequests(self)` | function | Worker side: answers the page's `CLAIM` call with `clients.claim()`. Both of this package's workers install it; use it in a worker of your own. |
|
|
298
|
+
| `ServiceWorkerControlError` | class | `name: "ServiceWorkerControlError"`, `reason`: `"activation-timeout"` (worker did not activate in time), `"uncontrolled"` (active, but the page is not controlled), `"unresponsive"` (controls the page, did not answer `SwHttpAdapter`'s handshake). |
|
|
299
|
+
| `DEFAULT_SERVICE_WORKER_TIMEOUT` / `CLAIM_CALL` | const | `30_000` ms / `"CLAIM"`. |
|
|
256
300
|
|
|
257
301
|
### Connection registry
|
|
258
302
|
|
|
@@ -310,10 +354,14 @@ src/
|
|
|
310
354
|
│ ├── data-calls.ts │ Transport primitives over a
|
|
311
355
|
│ ├── data-channels.ts │ `MessageTarget`: one-shot
|
|
312
356
|
│ ├── message-target.ts │ `callChannel` / `handleChannelCalls`,
|
|
313
|
-
│
|
|
357
|
+
│ ├── registry.ts │ the request/response
|
|
314
358
|
│ │ `newInvokationChannel`, streaming
|
|
315
|
-
│
|
|
359
|
+
│ └── service-worker-control.ts │ `sendStream` / `handleStreams` with
|
|
316
360
|
│ │ backpressure, and `newRegistry`.
|
|
361
|
+
│ │ Bounded waits for a worker to
|
|
362
|
+
│ │ activate / take control, and the
|
|
363
|
+
│ │ `CLAIM` request that takes over an
|
|
364
|
+
│ │ uncontrolled page.
|
|
317
365
|
│ │ Also re-exports
|
|
318
366
|
│ │ `@statewalker/webrun-streams`.
|
|
319
367
|
│ ┘
|
|
@@ -378,6 +426,17 @@ src/
|
|
|
378
426
|
backpressure — each `next(value)` returns a `Promise<boolean>` that resolves
|
|
379
427
|
once the consumer has dequeued — and drains in-flight producers on consumer
|
|
380
428
|
exit.
|
|
429
|
+
- **No literal `new URL("…", import.meta.url)` in page-side code**.
|
|
430
|
+
Bundlers (Vite among them) turn that pattern into an emitted asset at build
|
|
431
|
+
time, before tree-shaking; the relay defaults resolved `"../"` that way,
|
|
432
|
+
which is the package itself, so every Vite consumer shipped a dead copy of
|
|
433
|
+
`dist/index.js`. They resolve against a `moduleUrl` variable instead, and
|
|
434
|
+
`tests/dist/vite-consumer.dist.ts` builds a Vite app to prove nothing is
|
|
435
|
+
emitted.
|
|
436
|
+
- **Uncontrolled pages are handled per mode**. Same-origin mode needs
|
|
437
|
+
control — an uncontrolled page's `fetch()` never reaches the worker — so it
|
|
438
|
+
asks the worker to claim the page. Relay mode needs only a worker to message,
|
|
439
|
+
so an uncontrolled relay page bridges to `registration.active`.
|
|
381
440
|
- **SW client registry is IndexedDB-persisted**. Both `SwPortDispatcher`
|
|
382
441
|
(same-origin) and `relay/index-sw.ts` keep their client-lookup tables in
|
|
383
442
|
IndexedDB so a SW wake-up after idle doesn't lose its bindings.
|
|
@@ -398,6 +457,11 @@ src/
|
|
|
398
457
|
only controls pages and fetches under `/public/`. If you need a broader
|
|
399
458
|
scope, the SW script must be served with the
|
|
400
459
|
`Service-Worker-Allowed` HTTP header, *or* live higher in the origin.
|
|
460
|
+
- **A hard reload loads the page without its worker.** Handled — see
|
|
461
|
+
[When the page is not controlled](#when-the-page-is-not-controlled) — as long
|
|
462
|
+
as the worker answers `CLAIM`. A worker script of your own that
|
|
463
|
+
`importScripts` this package's `sw-worker.js` or `relay-sw.js` does; one
|
|
464
|
+
that does not should call `handleClaimRequests(self)`.
|
|
401
465
|
- **`http://localhost` or HTTPS only.** Browsers refuse to register SWs
|
|
402
466
|
on other `http://` origins.
|
|
403
467
|
- **Relay mode needs an iframe-capable sandbox.** Pages with strict CSP
|
|
@@ -421,7 +485,16 @@ Runtime:
|
|
|
421
485
|
|
|
422
486
|
Dev: TypeScript, vitest, rolldown, rimraf, `http-server` (for the
|
|
423
487
|
`example:*` scripts), `@types/node` (catalog versions from the monorepo
|
|
424
|
-
root).
|
|
488
|
+
root), `playwright` and `vite` (for `test:browser`).
|
|
489
|
+
|
|
490
|
+
## Tests
|
|
491
|
+
|
|
492
|
+
- `pnpm test` — unit tests under Node, against the source.
|
|
493
|
+
- `pnpm test:browser` — builds, then runs `tests/browser/` in real Chromium and
|
|
494
|
+
Firefox through Playwright against the built bundles (first visit, normal
|
|
495
|
+
reload, hard reload, second tab, the relay page, and the timeout and
|
|
496
|
+
uncontrolled errors), plus `tests/dist/`, which builds a Vite consumer of
|
|
497
|
+
the bundles. Needs the browsers: `npx playwright install chromium firefox`.
|
|
425
498
|
|
|
426
499
|
## License
|
|
427
500
|
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Settles like `promise` if it settles before `deadline` (a `Date.now()`
|
|
3
|
+
* value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
|
|
4
|
+
* other value resolves. Internal; not re-exported.
|
|
5
|
+
*/
|
|
6
|
+
export declare function withDeadline<T, F>(deadline: number, promise: Promise<T>, onTimeout: () => F): Promise<T | (F extends Error ? never : F)>;
|
|
7
|
+
//# sourceMappingURL=deadline.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"deadline.d.ts","sourceRoot":"","sources":["../../src/core/deadline.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AACH,wBAAgB,YAAY,CAAC,CAAC,EAAE,CAAC,EAC/B,QAAQ,EAAE,MAAM,EAChB,OAAO,EAAE,OAAO,CAAC,CAAC,CAAC,EACnB,SAAS,EAAE,MAAM,CAAC,GACjB,OAAO,CAAC,CAAC,GAAG,CAAC,CAAC,SAAS,KAAK,GAAG,KAAK,GAAG,CAAC,CAAC,CAAC,CAqB5C"}
|
package/dist/core/index.d.ts
CHANGED
package/dist/core/index.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAGA,cAAc,6BAA6B,CAAC;AAE5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC"}
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/core/index.ts"],"names":[],"mappings":"AAGA,cAAc,6BAA6B,CAAC;AAE5C,cAAc,iBAAiB,CAAC;AAChC,cAAc,oBAAoB,CAAC;AACnC,cAAc,qBAAqB,CAAC;AACpC,cAAc,eAAe,CAAC;AAC9B,cAAc,6BAA6B,CAAC"}
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* How long, by default, the page waits for its ServiceWorker to activate and
|
|
3
|
+
* to take control before giving up. Generous: a first install downloads and
|
|
4
|
+
* evaluates the worker script, which on a slow link takes seconds.
|
|
5
|
+
*/
|
|
6
|
+
export declare const DEFAULT_SERVICE_WORKER_TIMEOUT = 30000;
|
|
7
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
8
|
+
export declare const CLAIM_CALL = "CLAIM";
|
|
9
|
+
/**
|
|
10
|
+
* - `activation-timeout`: the worker did not activate in time.
|
|
11
|
+
* - `uncontrolled`: the worker is active but the page is not controlled by it.
|
|
12
|
+
* - `unresponsive`: the page is controlled, but the worker did not answer the
|
|
13
|
+
* adapter's handshake in time.
|
|
14
|
+
*/
|
|
15
|
+
export type ServiceWorkerControlFailure = "activation-timeout" | "uncontrolled" | "unresponsive";
|
|
16
|
+
/**
|
|
17
|
+
* Why a page could not get a working ServiceWorker. `reason` says which wait
|
|
18
|
+
* failed; check it (or `name`) rather than `instanceof`, because each of this
|
|
19
|
+
* package's bundles carries its own copy of this class.
|
|
20
|
+
*/
|
|
21
|
+
export declare class ServiceWorkerControlError extends Error {
|
|
22
|
+
readonly reason: ServiceWorkerControlFailure;
|
|
23
|
+
constructor(reason: ServiceWorkerControlFailure, message: string);
|
|
24
|
+
}
|
|
25
|
+
export interface AwaitServiceWorkerOptions {
|
|
26
|
+
/** Upper bound for the whole wait, in ms. Default `DEFAULT_SERVICE_WORKER_TIMEOUT`. */
|
|
27
|
+
timeout?: number;
|
|
28
|
+
}
|
|
29
|
+
export interface AwaitServiceWorkerControlOptions extends AwaitServiceWorkerOptions {
|
|
30
|
+
/**
|
|
31
|
+
* When the page is still uncontrolled after asking the worker to claim it,
|
|
32
|
+
* reload the page once instead of rejecting. A normal reload is a
|
|
33
|
+
* navigation, and navigations are controlled. Guarded by `sessionStorage`
|
|
34
|
+
* so it never loops: if the reloaded page is uncontrolled too, it rejects.
|
|
35
|
+
* Default `false`.
|
|
36
|
+
*/
|
|
37
|
+
reloadIfUncontrolled?: boolean;
|
|
38
|
+
/** For tests. Default `navigator.serviceWorker`. */
|
|
39
|
+
container?: ServiceWorkerContainer;
|
|
40
|
+
}
|
|
41
|
+
/**
|
|
42
|
+
* Resolves with the registration's worker once it is `activated`. Rejects
|
|
43
|
+
* with a `ServiceWorkerControlError` (`reason: "activation-timeout"`) if that
|
|
44
|
+
* takes longer than `timeout` — an install that throws or never finishes
|
|
45
|
+
* would otherwise leave the caller waiting forever.
|
|
46
|
+
*/
|
|
47
|
+
export declare function awaitActiveServiceWorker(registration: ServiceWorkerRegistration, { timeout }?: AwaitServiceWorkerOptions): Promise<ServiceWorker>;
|
|
48
|
+
/**
|
|
49
|
+
* Resolves with the ServiceWorker that controls this page, once `registration`
|
|
50
|
+
* has an activated worker and the page is controlled by it.
|
|
51
|
+
*
|
|
52
|
+
* A page can stay uncontrolled while its worker is active, and then no
|
|
53
|
+
* `controllerchange` ever fires on its own:
|
|
54
|
+
* - a hard reload (Ctrl+Shift+R) bypasses the worker for that load, and the
|
|
55
|
+
* worker's `clients.claim()` already ran when it activated;
|
|
56
|
+
* - Firefox can leave a page loaded while the worker is running uncontrolled.
|
|
57
|
+
*
|
|
58
|
+
* So when the page is uncontrolled, this asks the active worker to claim it
|
|
59
|
+
* again (a `CLAIM` channel call — this package's workers answer it) and waits
|
|
60
|
+
* for `controllerchange`. Every wait is bounded by `timeout`. If control never
|
|
61
|
+
* comes it reloads once (`reloadIfUncontrolled`) or rejects with a
|
|
62
|
+
* `ServiceWorkerControlError` (`reason: "uncontrolled"`) that says what
|
|
63
|
+
* happened and what to do.
|
|
64
|
+
*/
|
|
65
|
+
export declare function awaitServiceWorkerControl(registration: ServiceWorkerRegistration, { timeout, reloadIfUncontrolled, container, }?: AwaitServiceWorkerControlOptions): Promise<ServiceWorker>;
|
|
66
|
+
/**
|
|
67
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
68
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
69
|
+
* Returns a function that stops answering.
|
|
70
|
+
*/
|
|
71
|
+
export declare function handleClaimRequests(self: ServiceWorkerGlobalScope): () => void;
|
|
72
|
+
//# sourceMappingURL=service-worker-control.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"service-worker-control.d.ts","sourceRoot":"","sources":["../../src/core/service-worker-control.ts"],"names":[],"mappings":"AAKA;;;;GAIG;AACH,eAAO,MAAM,8BAA8B,QAAS,CAAC;AAUrD,kFAAkF;AAClF,eAAO,MAAM,UAAU,UAAU,CAAC;AAElC;;;;;GAKG;AACH,MAAM,MAAM,2BAA2B,GAAG,oBAAoB,GAAG,cAAc,GAAG,cAAc,CAAC;AAEjG;;;;GAIG;AACH,qBAAa,yBAA0B,SAAQ,KAAK;IAClD,QAAQ,CAAC,MAAM,EAAE,2BAA2B,CAAC;IAC7C,YAAY,MAAM,EAAE,2BAA2B,EAAE,OAAO,EAAE,MAAM,EAI/D;CACF;AAED,MAAM,WAAW,yBAAyB;IACxC,uFAAuF;IACvF,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,gCAAiC,SAAQ,yBAAyB;IACjF;;;;;;OAMG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;IAC/B,oDAAoD;IACpD,SAAS,CAAC,EAAE,sBAAsB,CAAC;CACpC;AAED;;;;;GAKG;AACH,wBAAsB,wBAAwB,CAC5C,YAAY,EAAE,yBAAyB,EACvC,EAAE,OAAwC,EAAE,GAAE,yBAA8B,GAC3E,OAAO,CAAC,aAAa,CAAC,CAaxB;AAED;;;;;;;;;;;;;;;;GAgBG;AACH,wBAAsB,yBAAyB,CAC7C,YAAY,EAAE,yBAAyB,EACvC,EACE,OAAwC,EACxC,oBAA4B,EAC5B,SAAmC,GACpC,GAAE,gCAAqC,GACvC,OAAO,CAAC,aAAa,CAAC,CA6CxB;AAED;;;;GAIG;AACH,wBAAgB,mBAAmB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,IAAI,CAM9E"}
|
package/dist/index.js
CHANGED
|
@@ -1125,6 +1125,192 @@ function newRegistry(onError = console.error) {
|
|
|
1125
1125
|
});
|
|
1126
1126
|
}
|
|
1127
1127
|
//#endregion
|
|
1128
|
+
//#region src/core/deadline.ts
|
|
1129
|
+
/**
|
|
1130
|
+
* Settles like `promise` if it settles before `deadline` (a `Date.now()`
|
|
1131
|
+
* value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
|
|
1132
|
+
* other value resolves. Internal; not re-exported.
|
|
1133
|
+
*/
|
|
1134
|
+
function withDeadline(deadline, promise, onTimeout) {
|
|
1135
|
+
return new Promise((resolve, reject) => {
|
|
1136
|
+
const timer = setTimeout(() => {
|
|
1137
|
+
const outcome = onTimeout();
|
|
1138
|
+
if (outcome instanceof Error) reject(outcome);
|
|
1139
|
+
else resolve(outcome);
|
|
1140
|
+
}, Math.max(0, deadline - Date.now()));
|
|
1141
|
+
promise.then((value) => {
|
|
1142
|
+
clearTimeout(timer);
|
|
1143
|
+
resolve(value);
|
|
1144
|
+
}, (error) => {
|
|
1145
|
+
clearTimeout(timer);
|
|
1146
|
+
reject(error);
|
|
1147
|
+
});
|
|
1148
|
+
});
|
|
1149
|
+
}
|
|
1150
|
+
//#endregion
|
|
1151
|
+
//#region src/core/service-worker-control.ts
|
|
1152
|
+
/**
|
|
1153
|
+
* How long, by default, the page waits for its ServiceWorker to activate and
|
|
1154
|
+
* to take control before giving up. Generous: a first install downloads and
|
|
1155
|
+
* evaluates the worker script, which on a slow link takes seconds.
|
|
1156
|
+
*/
|
|
1157
|
+
const DEFAULT_SERVICE_WORKER_TIMEOUT = 3e4;
|
|
1158
|
+
/**
|
|
1159
|
+
* How long the page waits for `controllerchange` once the worker has
|
|
1160
|
+
* answered the `CLAIM` request. `clients.claim()` resolves only after the
|
|
1161
|
+
* browser has queued that event, so this is a grace period for delivery, not
|
|
1162
|
+
* a second budget.
|
|
1163
|
+
*/
|
|
1164
|
+
const CLAIM_GRACE_MS = 1e3;
|
|
1165
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
1166
|
+
const CLAIM_CALL = "CLAIM";
|
|
1167
|
+
/**
|
|
1168
|
+
* Why a page could not get a working ServiceWorker. `reason` says which wait
|
|
1169
|
+
* failed; check it (or `name`) rather than `instanceof`, because each of this
|
|
1170
|
+
* package's bundles carries its own copy of this class.
|
|
1171
|
+
*/
|
|
1172
|
+
var ServiceWorkerControlError = class extends Error {
|
|
1173
|
+
reason;
|
|
1174
|
+
constructor(reason, message) {
|
|
1175
|
+
super(message);
|
|
1176
|
+
this.name = "ServiceWorkerControlError";
|
|
1177
|
+
this.reason = reason;
|
|
1178
|
+
}
|
|
1179
|
+
};
|
|
1180
|
+
/**
|
|
1181
|
+
* Resolves with the registration's worker once it is `activated`. Rejects
|
|
1182
|
+
* with a `ServiceWorkerControlError` (`reason: "activation-timeout"`) if that
|
|
1183
|
+
* takes longer than `timeout` — an install that throws or never finishes
|
|
1184
|
+
* would otherwise leave the caller waiting forever.
|
|
1185
|
+
*/
|
|
1186
|
+
async function awaitActiveServiceWorker(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT } = {}) {
|
|
1187
|
+
return await withDeadline(Date.now() + timeout, waitForActivated(registration), () => {
|
|
1188
|
+
const worker = registration.installing ?? registration.waiting ?? registration.active;
|
|
1189
|
+
return new ServiceWorkerControlError("activation-timeout", `ServiceWorker ${scriptUrl(worker)} (scope ${registration.scope}) did not activate within ${timeout} ms` + (worker ? `; it is "${worker.state}"` : "") + ". Check the worker script for errors during install (DevTools → Application → Service Workers), or raise the `timeout` option.");
|
|
1190
|
+
});
|
|
1191
|
+
}
|
|
1192
|
+
/**
|
|
1193
|
+
* Resolves with the ServiceWorker that controls this page, once `registration`
|
|
1194
|
+
* has an activated worker and the page is controlled by it.
|
|
1195
|
+
*
|
|
1196
|
+
* A page can stay uncontrolled while its worker is active, and then no
|
|
1197
|
+
* `controllerchange` ever fires on its own:
|
|
1198
|
+
* - a hard reload (Ctrl+Shift+R) bypasses the worker for that load, and the
|
|
1199
|
+
* worker's `clients.claim()` already ran when it activated;
|
|
1200
|
+
* - Firefox can leave a page loaded while the worker is running uncontrolled.
|
|
1201
|
+
*
|
|
1202
|
+
* So when the page is uncontrolled, this asks the active worker to claim it
|
|
1203
|
+
* again (a `CLAIM` channel call — this package's workers answer it) and waits
|
|
1204
|
+
* for `controllerchange`. Every wait is bounded by `timeout`. If control never
|
|
1205
|
+
* comes it reloads once (`reloadIfUncontrolled`) or rejects with a
|
|
1206
|
+
* `ServiceWorkerControlError` (`reason: "uncontrolled"`) that says what
|
|
1207
|
+
* happened and what to do.
|
|
1208
|
+
*/
|
|
1209
|
+
async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT, reloadIfUncontrolled = false, container = navigator.serviceWorker } = {}) {
|
|
1210
|
+
const deadline = Date.now() + timeout;
|
|
1211
|
+
const controlled = waitForController(container);
|
|
1212
|
+
try {
|
|
1213
|
+
const active = await awaitActiveServiceWorker(registration, { timeout });
|
|
1214
|
+
let controller = container.controller;
|
|
1215
|
+
if (!controller) controller = await withDeadline(deadline, (async () => {
|
|
1216
|
+
await Promise.race([callChannel(active, CLAIM_CALL, {}), controlled.promise]);
|
|
1217
|
+
return await Promise.race([controlled.promise, delay(CLAIM_GRACE_MS, null)]);
|
|
1218
|
+
})(), () => null);
|
|
1219
|
+
if (controller) {
|
|
1220
|
+
forgetReload(registration);
|
|
1221
|
+
return controller;
|
|
1222
|
+
}
|
|
1223
|
+
if (reloadIfUncontrolled && markReload(registration)) {
|
|
1224
|
+
location.reload();
|
|
1225
|
+
return await new Promise(() => {});
|
|
1226
|
+
}
|
|
1227
|
+
forgetReload(registration);
|
|
1228
|
+
throw new ServiceWorkerControlError("uncontrolled", `This page is not controlled by its ServiceWorker ${scriptUrl(active)} (scope ${registration.scope}), although the worker is active, and the worker did not take control when asked (clients.claim()) within ${timeout} ms. This happens after a hard reload (Ctrl+Shift+R / Cmd+Shift+R), which bypasses ServiceWorkers for that load, and in Firefox for some pages opened while the worker was already running. Requests from this page would not reach the worker. Reload the page normally, pass \`reloadIfUncontrolled: true\` to do that automatically, check that the page is inside the worker's scope, and that the worker answers the "${CLAIM_CALL}" request (this package's workers do).`);
|
|
1229
|
+
} finally {
|
|
1230
|
+
controlled.cancel();
|
|
1231
|
+
}
|
|
1232
|
+
}
|
|
1233
|
+
/**
|
|
1234
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
1235
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
1236
|
+
* Returns a function that stops answering.
|
|
1237
|
+
*/
|
|
1238
|
+
function handleClaimRequests(self) {
|
|
1239
|
+
return handleChannelCalls(self, CLAIM_CALL, (event) => {
|
|
1240
|
+
const claimed = self.clients.claim().then(() => true);
|
|
1241
|
+
event.waitUntil?.(claimed);
|
|
1242
|
+
return claimed;
|
|
1243
|
+
});
|
|
1244
|
+
}
|
|
1245
|
+
function waitForActivated(registration) {
|
|
1246
|
+
return new Promise((resolve) => {
|
|
1247
|
+
const watched = /* @__PURE__ */ new Set();
|
|
1248
|
+
const check = () => {
|
|
1249
|
+
const active = registration.active;
|
|
1250
|
+
if (active?.state === "activated") {
|
|
1251
|
+
registration.removeEventListener("updatefound", watch);
|
|
1252
|
+
for (const worker of watched) worker.removeEventListener("statechange", check);
|
|
1253
|
+
resolve(active);
|
|
1254
|
+
return;
|
|
1255
|
+
}
|
|
1256
|
+
watch();
|
|
1257
|
+
};
|
|
1258
|
+
function watch() {
|
|
1259
|
+
for (const worker of [
|
|
1260
|
+
registration.installing,
|
|
1261
|
+
registration.waiting,
|
|
1262
|
+
registration.active
|
|
1263
|
+
]) {
|
|
1264
|
+
if (!worker || watched.has(worker)) continue;
|
|
1265
|
+
watched.add(worker);
|
|
1266
|
+
worker.addEventListener("statechange", check);
|
|
1267
|
+
}
|
|
1268
|
+
}
|
|
1269
|
+
registration.addEventListener("updatefound", watch);
|
|
1270
|
+
check();
|
|
1271
|
+
});
|
|
1272
|
+
}
|
|
1273
|
+
function waitForController(container) {
|
|
1274
|
+
let cancel = () => {};
|
|
1275
|
+
return {
|
|
1276
|
+
promise: new Promise((resolve) => {
|
|
1277
|
+
const onChange = () => {
|
|
1278
|
+
if (!container.controller) return;
|
|
1279
|
+
cancel();
|
|
1280
|
+
resolve(container.controller);
|
|
1281
|
+
};
|
|
1282
|
+
cancel = () => container.removeEventListener("controllerchange", onChange);
|
|
1283
|
+
container.addEventListener("controllerchange", onChange);
|
|
1284
|
+
}),
|
|
1285
|
+
cancel
|
|
1286
|
+
};
|
|
1287
|
+
}
|
|
1288
|
+
function delay(ms, value) {
|
|
1289
|
+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
1290
|
+
}
|
|
1291
|
+
function scriptUrl(worker) {
|
|
1292
|
+
return worker?.scriptURL ? `"${worker.scriptURL}"` : "(no worker)";
|
|
1293
|
+
}
|
|
1294
|
+
function reloadKey(registration) {
|
|
1295
|
+
return `webrun-http-browser:reloaded-uncontrolled:${registration.scope}`;
|
|
1296
|
+
}
|
|
1297
|
+
/** Records the reload about to happen. `false` if one already happened, or if it cannot be recorded. */
|
|
1298
|
+
function markReload(registration) {
|
|
1299
|
+
try {
|
|
1300
|
+
const key = reloadKey(registration);
|
|
1301
|
+
if (sessionStorage.getItem(key)) return false;
|
|
1302
|
+
sessionStorage.setItem(key, "1");
|
|
1303
|
+
return true;
|
|
1304
|
+
} catch {
|
|
1305
|
+
return false;
|
|
1306
|
+
}
|
|
1307
|
+
}
|
|
1308
|
+
function forgetReload(registration) {
|
|
1309
|
+
try {
|
|
1310
|
+
sessionStorage.removeItem(reloadKey(registration));
|
|
1311
|
+
} catch {}
|
|
1312
|
+
}
|
|
1313
|
+
//#endregion
|
|
1128
1314
|
//#region ../webrun-http-streams/dist/index.js
|
|
1129
1315
|
const CR = 13;
|
|
1130
1316
|
const LF = 10;
|
|
@@ -2498,13 +2684,25 @@ function splitServiceUrl(url, separator = "~") {
|
|
|
2498
2684
|
//#endregion
|
|
2499
2685
|
//#region src/relay/index.ts
|
|
2500
2686
|
/**
|
|
2687
|
+
* The URL of this module, kept in a variable on purpose. Bundlers (Vite
|
|
2688
|
+
* among them) rewrite every literal `new URL("<path>", import.meta.url)` into
|
|
2689
|
+
* an emitted asset at build time, before tree-shaking — so the defaults below
|
|
2690
|
+
* made every Vite consumer of this entry emit a dead copy of the package's
|
|
2691
|
+
* own `dist/index.js` (`"../"` resolves to the package, hence to its `main`).
|
|
2692
|
+
* Resolving against a variable is the same URL at run time and invisible to
|
|
2693
|
+
* that transform.
|
|
2694
|
+
*/
|
|
2695
|
+
const moduleUrl = import.meta.url;
|
|
2696
|
+
/**
|
|
2501
2697
|
* Returns a MessagePort that transparently bridges messages to/from the
|
|
2502
|
-
* ServiceWorker controlling
|
|
2698
|
+
* page's ServiceWorker: the one controlling the page, or — when the page is
|
|
2699
|
+
* not controlled (a hard reload, or a page Firefox left uncontrolled) — the
|
|
2700
|
+
* active worker of `registration`, which answers messages all the same.
|
|
2503
2701
|
*/
|
|
2504
|
-
function newServiceWorkerPort() {
|
|
2702
|
+
function newServiceWorkerPort(registration) {
|
|
2505
2703
|
const channel = new MessageChannel();
|
|
2506
2704
|
channel.port1.onmessage = (event) => {
|
|
2507
|
-
navigator.serviceWorker.controller?.postMessage(event.data, [...event.ports]);
|
|
2705
|
+
(navigator.serviceWorker.controller ?? registration?.active)?.postMessage(event.data, [...event.ports]);
|
|
2508
2706
|
};
|
|
2509
2707
|
navigator.serviceWorker.addEventListener("message", (event) => {
|
|
2510
2708
|
channel.port1.postMessage(event.data, [...event.ports]);
|
|
@@ -2512,45 +2710,26 @@ function newServiceWorkerPort() {
|
|
|
2512
2710
|
return channel.port2;
|
|
2513
2711
|
}
|
|
2514
2712
|
/**
|
|
2515
|
-
* Registers a ServiceWorker and resolves once it
|
|
2713
|
+
* Registers a ServiceWorker and resolves with it once it is activated: the
|
|
2714
|
+
* worker controlling the page, or the registration's active worker when the
|
|
2715
|
+
* page is not controlled. Messaging works either way, which is all the relay
|
|
2716
|
+
* needs; nothing here waits for control, because an uncontrolled page (hard
|
|
2717
|
+
* reload; Firefox) may never get it. Rejects with a
|
|
2718
|
+
* `ServiceWorkerControlError` if activation takes longer than `timeout`.
|
|
2516
2719
|
*/
|
|
2517
|
-
async function initServiceWorker(
|
|
2518
|
-
await
|
|
2720
|
+
async function initServiceWorker(options) {
|
|
2721
|
+
return (await registerServiceWorker(options)).worker;
|
|
2722
|
+
}
|
|
2723
|
+
async function registerServiceWorker({ swUrl, scopeUrl, type, timeout = DEFAULT_SERVICE_WORKER_TIMEOUT }) {
|
|
2724
|
+
const registration = await navigator.serviceWorker.register(swUrl, {
|
|
2519
2725
|
type,
|
|
2520
2726
|
scope: scopeUrl
|
|
2521
2727
|
});
|
|
2522
|
-
const
|
|
2523
|
-
|
|
2524
|
-
|
|
2525
|
-
|
|
2526
|
-
|
|
2527
|
-
return new Promise((resolve) => {
|
|
2528
|
-
const container = navigator.serviceWorker;
|
|
2529
|
-
if (container.controller) {
|
|
2530
|
-
resolve(container.controller);
|
|
2531
|
-
return;
|
|
2532
|
-
}
|
|
2533
|
-
const onChange = () => {
|
|
2534
|
-
if (!container.controller) return;
|
|
2535
|
-
resolve(container.controller);
|
|
2536
|
-
container.removeEventListener("controllerchange", onChange);
|
|
2537
|
-
};
|
|
2538
|
-
container.addEventListener("controllerchange", onChange);
|
|
2539
|
-
});
|
|
2540
|
-
}
|
|
2541
|
-
function awaitServiceWorkerActivation(worker) {
|
|
2542
|
-
return new Promise((resolve) => {
|
|
2543
|
-
if (worker.state === "activated") {
|
|
2544
|
-
resolve();
|
|
2545
|
-
return;
|
|
2546
|
-
}
|
|
2547
|
-
const onStateChange = () => {
|
|
2548
|
-
if (worker.state !== "activated") return;
|
|
2549
|
-
worker.removeEventListener("statechange", onStateChange);
|
|
2550
|
-
resolve();
|
|
2551
|
-
};
|
|
2552
|
-
worker.addEventListener("statechange", onStateChange);
|
|
2553
|
-
});
|
|
2728
|
+
const active = await awaitActiveServiceWorker(registration, { timeout });
|
|
2729
|
+
return {
|
|
2730
|
+
registration,
|
|
2731
|
+
worker: navigator.serviceWorker.controller ?? active
|
|
2732
|
+
};
|
|
2554
2733
|
}
|
|
2555
2734
|
/**
|
|
2556
2735
|
* Registers `handler` as the server for the given service `key` on the relay.
|
|
@@ -2581,9 +2760,11 @@ async function callHttpService(request, { key, port }) {
|
|
|
2581
2760
|
/**
|
|
2582
2761
|
* Returns a `window.onmessage` handler for use inside the relay iframe:
|
|
2583
2762
|
* it accepts a CONNECT message, starts the relay ServiceWorker, and bridges
|
|
2584
|
-
* the parent's MessagePort with the SW.
|
|
2763
|
+
* the parent's MessagePort with the SW. If the worker cannot be started, every
|
|
2764
|
+
* call the parent makes on that port is answered with the error, so the
|
|
2765
|
+
* parent's `initHttpService` / `callHttpService` reject instead of waiting.
|
|
2585
2766
|
*/
|
|
2586
|
-
function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js",
|
|
2767
|
+
function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", moduleUrl)}`, scopeUrl = `${new URL("../", moduleUrl)}`, timeout } = {}) {
|
|
2587
2768
|
let externalPort;
|
|
2588
2769
|
return async (ev) => {
|
|
2589
2770
|
if (ev.data?.type !== "CONNECT") return;
|
|
@@ -2594,11 +2775,19 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
|
|
|
2594
2775
|
return;
|
|
2595
2776
|
}
|
|
2596
2777
|
externalPort = newExternalPort;
|
|
2597
|
-
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2778
|
+
let registration;
|
|
2779
|
+
try {
|
|
2780
|
+
({registration} = await registerServiceWorker({
|
|
2781
|
+
swUrl,
|
|
2782
|
+
scopeUrl,
|
|
2783
|
+
timeout
|
|
2784
|
+
}));
|
|
2785
|
+
} catch (error) {
|
|
2786
|
+
const serialized = serializeError(error);
|
|
2787
|
+
externalPort.onmessage = (event) => event.ports[0]?.postMessage({ error: serialized });
|
|
2788
|
+
throw error;
|
|
2789
|
+
}
|
|
2790
|
+
const serviceWorkerPort = newServiceWorkerPort(registration);
|
|
2602
2791
|
serviceWorkerPort.onmessage = (event) => {
|
|
2603
2792
|
externalPort?.postMessage(event.data, [...event.ports]);
|
|
2604
2793
|
};
|
|
@@ -2611,7 +2800,7 @@ function getRelayWindowMessageHandler({ swUrl = `${new URL("./index-sw.js", impo
|
|
|
2611
2800
|
* Embeds a hidden relay iframe, establishes a MessageChannel with it, and
|
|
2612
2801
|
* returns the port to be used with `initHttpService` / `callHttpService`.
|
|
2613
2802
|
*/
|
|
2614
|
-
async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/",
|
|
2803
|
+
async function newRemoteRelayChannel({ baseUrl = new URL("../public-relay/", moduleUrl), url = new URL("relay.html", baseUrl), container = document.body } = {}) {
|
|
2615
2804
|
const messageChannel = new MessageChannel();
|
|
2616
2805
|
const { iframe, promise } = newIFrame(url);
|
|
2617
2806
|
Object.assign(iframe.style, {
|
|
@@ -2680,4 +2869,4 @@ async function registerConnectionsHandler({ key, handler, communicationPort }) {
|
|
|
2680
2869
|
return cleanup;
|
|
2681
2870
|
}
|
|
2682
2871
|
//#endregion
|
|
2683
|
-
export { DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, TransportClosedError, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };
|
|
2872
|
+
export { CLAIM_CALL, DEFAULT_SERVICE_WORKER_TIMEOUT, DuplexSiteBuilder, HttpError, HttpParseError, PEER_ERROR_HEADER, ServiceWorkerControlError, TransportClosedError, awaitActiveServiceWorker, awaitServiceWorkerControl, callChannel, callHttpService, collect, collectBytes, collectString, decodeJsonl, decodeMessage, decodeText, defaultCodec, deserializeError, emulateMux, encodeJsonl, encodeMessage, encodeText, fetchOverDuplex, fromReadableStream, getRelayWindowMessageHandler, handleChannelCalls, handleClaimRequests, handleHttpRequests, handleStreams, httpCodec, httpFetch, httpServe, initHttpService, initServiceWorker, initializeConnection, joinLines, jsonEnvelopeCodec, map, newAsyncGenerator, newCreditGrantor, newCreditLedger, newHttpClientStub, newHttpCodec, newHttpServerStub, newInvokationChannel, newRegistry, newRemoteRelayChannel, newServiceWorkerPort, newSniffingCodec, normalizeToUint8Array, recieveIterator, registerConnectionsHandler, sendHttpRequest, sendIterator, sendStream, serializeError, serveFetchOverDuplex, splitLines, splitServiceUrl, toChunks, toReadableStream };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"index-sw.d.ts","sourceRoot":"","sources":["../../src/relay/index-sw.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"index-sw.d.ts","sourceRoot":"","sources":["../../src/relay/index-sw.ts"],"names":[],"mappings":"AAQA;;;;GAIG;AACH,wBAAgB,uBAAuB,CAAC,IAAI,EAAE,wBAAwB,GAAG,MAAM,IAAI,CA4ElF"}
|