@statewalker/webrun-http-browser 0.4.2 → 0.6.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 +207 -12
- 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 +328 -67
- package/dist/relay/index-sw.d.ts +86 -1
- package/dist/relay/index-sw.d.ts.map +1 -1
- package/dist/relay/index.d.ts +32 -8
- package/dist/relay/index.d.ts.map +1 -1
- package/dist/relay/mount-table.d.ts +48 -0
- package/dist/relay/mount-table.d.ts.map +1 -0
- package/dist/relay/split-service-url.d.ts.map +1 -1
- package/dist/relay-sw.js +293 -50
- package/dist/relay-worker.d.ts +20 -0
- package/dist/relay-worker.d.ts.map +1 -0
- package/dist/relay-worker.js +1899 -0
- 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 +9 -1
- 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 +293 -37
- package/src/relay/index.ts +125 -51
- package/src/relay/mount-table.ts +115 -0
- package/src/relay/split-service-url.ts +73 -13
- package/src/relay-sw.ts +11 -3
- package/src/relay-worker.ts +20 -0
- package/src/sw/sw-dispatcher.ts +72 -45
- package/public-relay/heartbeat.js +0 -31
|
@@ -3,6 +3,19 @@ export interface SwPortHandlerOptions {
|
|
|
3
3
|
scope?: string;
|
|
4
4
|
serviceWorkerUrl?: string;
|
|
5
5
|
bindPort: (port: MessagePort) => void | Promise<void>;
|
|
6
|
+
/**
|
|
7
|
+
* Upper bound, in ms, for `start()`'s wait for the worker to activate, take
|
|
8
|
+
* control of the page and answer the handshake. `start()` rejects with a
|
|
9
|
+
* `ServiceWorkerControlError` past it instead of waiting forever.
|
|
10
|
+
* Default `DEFAULT_SERVICE_WORKER_TIMEOUT` (30 s).
|
|
11
|
+
*/
|
|
12
|
+
timeout?: number;
|
|
13
|
+
/**
|
|
14
|
+
* If the page is still uncontrolled once the worker is active — after a
|
|
15
|
+
* hard reload, say — and the worker does not take it over when asked,
|
|
16
|
+
* reload the page once instead of rejecting. Default `false`.
|
|
17
|
+
*/
|
|
18
|
+
reloadIfUncontrolled?: boolean;
|
|
6
19
|
}
|
|
7
20
|
interface ChannelInfo {
|
|
8
21
|
key: string;
|
|
@@ -28,6 +41,15 @@ export declare class SwPortHandler {
|
|
|
28
41
|
protected _setCommunicationPort(port: MessagePort): Promise<void>;
|
|
29
42
|
protected _updateCommunicationChannel(): Promise<void>;
|
|
30
43
|
protected _getRegistrationInfo(): ChannelInfo;
|
|
44
|
+
/**
|
|
45
|
+
* Registers the worker and waits until it is activated and controls this
|
|
46
|
+
* page, then opens the port to it. The page must be controlled, because
|
|
47
|
+
* only a controlled page's `fetch()` reaches the worker. When the page
|
|
48
|
+
* loaded uncontrolled (a hard reload does that), the worker is asked to
|
|
49
|
+
* claim it. Rejects with a `ServiceWorkerControlError` when that fails or
|
|
50
|
+
* `timeout` passes (`reason` says which step); a later `start()` tries
|
|
51
|
+
* again.
|
|
52
|
+
*/
|
|
31
53
|
start(): Promise<void>;
|
|
32
54
|
stop(): Promise<void>;
|
|
33
55
|
}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"sw-dispatcher.d.ts","sourceRoot":"","sources":["../../src/sw/sw-dispatcher.ts"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"sw-dispatcher.d.ts","sourceRoot":"","sources":["../../src/sw/sw-dispatcher.ts"],"names":[],"mappings":"AAWA,MAAM,WAAW,oBAAoB;IACnC,GAAG,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,gBAAgB,CAAC,EAAE,MAAM,CAAC;IAC1B,QAAQ,EAAE,CAAC,IAAI,EAAE,WAAW,KAAK,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IACtD;;;;;OAKG;IACH,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB;;;;OAIG;IACH,oBAAoB,CAAC,EAAE,OAAO,CAAC;CAChC;AAED,UAAU,WAAW;IACnB,GAAG,EAAE,MAAM,CAAC;IACZ,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,WAAW,CAAC;CACpB;AAED;;;GAGG;AACH,qBAAa,aAAa;IACxB,QAAQ,CAAC,OAAO,EAAE,oBAAoB,CAAC;IACvC,OAAO,CAAC,iBAAiB,CAAC,CAAS;IACnC,OAAO,CAAC,cAAc,CAAC,CAAgB;IACvC,OAAO,CAAC,oBAAoB,CAAC,CAAgB;IAC7C,OAAO,CAAC,qBAAqB,CAAC,CAAa;IAE3C,YAAY,OAAO,EAAE,oBAAoB,EAIxC;IAED,IAAI,GAAG,IAAI,MAAM,CAEhB;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED,IAAI,OAAO,IAAI,GAAG,CAEjB;IAED,IAAI,gBAAgB,IAAI,MAAM,CAa7B;IAED,UAAgB,wBAAwB,IAAI,OAAO,CAAC,CAAC,WAAW,EAAE,WAAW,CAAC,CAAC,CAG9E;IAED,UAAgB,qBAAqB,CAAC,IAAI,EAAE,WAAW,GAAG,OAAO,CAAC,IAAI,CAAC,CAEtE;IAED,UAAgB,2BAA2B,IAAI,OAAO,CAAC,IAAI,CAAC,CAU3D;IAED,SAAS,CAAC,oBAAoB,IAAI,WAAW,CAE5C;IAED;;;;;;;;OAQG;IACG,KAAK,IAAI,OAAO,CAAC,IAAI,CAAC,CAsD3B;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAU1B;CACF;AAED,MAAM,WAAW,uBAAuB;IACtC,IAAI,EAAE,wBAAwB,CAAC;IAC/B,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;CACpC;AAED;;;GAGG;AACH,qBAAa,gBAAgB;IAC3B,QAAQ,CAAC,IAAI,EAAE,wBAAwB,CAAC;IACxC,QAAQ,CAAC,GAAG,EAAE,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,KAAK,IAAI,CAAC;IAC3C,QAAQ,CAAC,aAAa,2BAAkC;IACxD;;;;;OAKG;IACH,QAAQ,CAAC,WAAW,cAAqB;IACzC,OAAO,CAAC,QAAQ,CAAC,CAAa;IAC9B,OAAO,CAAC,kBAAkB,CAAC,CAAgB;IAE3C,YAAY,EAAE,IAAI,EAAE,GAAc,EAAE,EAAE,uBAAuB,EAG5D;IAED,IAAI,KAAK,IAAI,MAAM,CAElB;IAED;;;;OAIG;IACG,eAAe,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAAC,WAAW,GAAG,SAAS,CAAC,CAgBnE;IAED,yDAAyD;IACzD,YAAY,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,CAEjC;IAED,KAAK,IAAI,IAAI,CAgCZ;IAEK,IAAI,IAAI,OAAO,CAAC,IAAI,CAAC,CAK1B;IAED,OAAO,CAAC,gBAAgB;YAOV,SAAS;YAgBT,kBAAkB;YAWlB,cAAc;YAOd,gBAAgB;CAY/B"}
|
package/dist/sw-worker.js
CHANGED
|
@@ -1341,7 +1341,7 @@
|
|
|
1341
1341
|
})(request);
|
|
1342
1342
|
}
|
|
1343
1343
|
//#endregion
|
|
1344
|
-
//#region
|
|
1344
|
+
//#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
|
|
1345
1345
|
function promisifyRequest(request) {
|
|
1346
1346
|
return new Promise((resolve, reject) => {
|
|
1347
1347
|
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
@@ -1424,6 +1424,22 @@
|
|
|
1424
1424
|
return () => target.removeEventListener("message", listener);
|
|
1425
1425
|
}
|
|
1426
1426
|
//#endregion
|
|
1427
|
+
//#region src/core/service-worker-control.ts
|
|
1428
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
1429
|
+
const CLAIM_CALL = "CLAIM";
|
|
1430
|
+
/**
|
|
1431
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
1432
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
1433
|
+
* Returns a function that stops answering.
|
|
1434
|
+
*/
|
|
1435
|
+
function handleClaimRequests(self) {
|
|
1436
|
+
return handleChannelCalls(self, CLAIM_CALL, (event) => {
|
|
1437
|
+
const claimed = self.clients.claim().then(() => true);
|
|
1438
|
+
event.waitUntil?.(claimed);
|
|
1439
|
+
return claimed;
|
|
1440
|
+
});
|
|
1441
|
+
}
|
|
1442
|
+
//#endregion
|
|
1427
1443
|
//#region src/sw/sw-dispatcher.ts
|
|
1428
1444
|
/**
|
|
1429
1445
|
* ServiceWorker-side counterpart: maintains an index of connected clients keyed by
|
|
@@ -1472,7 +1488,8 @@
|
|
|
1472
1488
|
return this.claimedKeys.has(key);
|
|
1473
1489
|
}
|
|
1474
1490
|
start() {
|
|
1475
|
-
|
|
1491
|
+
const stopClaims = handleClaimRequests(this.self);
|
|
1492
|
+
const stopPortUpdates = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
1476
1493
|
const clientId = event.source?.id;
|
|
1477
1494
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
1478
1495
|
await this._updateChannelInfo({
|
|
@@ -1482,6 +1499,10 @@
|
|
|
1482
1499
|
});
|
|
1483
1500
|
return { ...channelInfo };
|
|
1484
1501
|
});
|
|
1502
|
+
this._cleanup = () => {
|
|
1503
|
+
stopClaims();
|
|
1504
|
+
stopPortUpdates();
|
|
1505
|
+
};
|
|
1485
1506
|
this.self.addEventListener("install", (event) => {
|
|
1486
1507
|
this.log("Skip waiting on install.", event);
|
|
1487
1508
|
this.self.skipWaiting();
|
package/dist/sw.js
CHANGED
|
@@ -1440,7 +1440,7 @@ async function sendHttpRequest(communicationPort, request) {
|
|
|
1440
1440
|
})(request);
|
|
1441
1441
|
}
|
|
1442
1442
|
//#endregion
|
|
1443
|
-
//#region
|
|
1443
|
+
//#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
|
|
1444
1444
|
function promisifyRequest(request) {
|
|
1445
1445
|
return new Promise((resolve, reject) => {
|
|
1446
1446
|
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
@@ -1523,6 +1523,29 @@ function handleChannelCalls(target, callType, handler) {
|
|
|
1523
1523
|
return () => target.removeEventListener("message", listener);
|
|
1524
1524
|
}
|
|
1525
1525
|
//#endregion
|
|
1526
|
+
//#region src/core/deadline.ts
|
|
1527
|
+
/**
|
|
1528
|
+
* Settles like `promise` if it settles before `deadline` (a `Date.now()`
|
|
1529
|
+
* value). Otherwise calls `onTimeout`: an `Error` it returns rejects, any
|
|
1530
|
+
* other value resolves. Internal; not re-exported.
|
|
1531
|
+
*/
|
|
1532
|
+
function withDeadline(deadline, promise, onTimeout) {
|
|
1533
|
+
return new Promise((resolve, reject) => {
|
|
1534
|
+
const timer = setTimeout(() => {
|
|
1535
|
+
const outcome = onTimeout();
|
|
1536
|
+
if (outcome instanceof Error) reject(outcome);
|
|
1537
|
+
else resolve(outcome);
|
|
1538
|
+
}, Math.max(0, deadline - Date.now()));
|
|
1539
|
+
promise.then((value) => {
|
|
1540
|
+
clearTimeout(timer);
|
|
1541
|
+
resolve(value);
|
|
1542
|
+
}, (error) => {
|
|
1543
|
+
clearTimeout(timer);
|
|
1544
|
+
reject(error);
|
|
1545
|
+
});
|
|
1546
|
+
});
|
|
1547
|
+
}
|
|
1548
|
+
//#endregion
|
|
1526
1549
|
//#region src/core/registry.ts
|
|
1527
1550
|
function newRegistry(onError = console.error) {
|
|
1528
1551
|
let counter = 0;
|
|
@@ -1557,6 +1580,169 @@ function newRegistry(onError = console.error) {
|
|
|
1557
1580
|
});
|
|
1558
1581
|
}
|
|
1559
1582
|
//#endregion
|
|
1583
|
+
//#region src/core/service-worker-control.ts
|
|
1584
|
+
/**
|
|
1585
|
+
* How long, by default, the page waits for its ServiceWorker to activate and
|
|
1586
|
+
* to take control before giving up. Generous: a first install downloads and
|
|
1587
|
+
* evaluates the worker script, which on a slow link takes seconds.
|
|
1588
|
+
*/
|
|
1589
|
+
const DEFAULT_SERVICE_WORKER_TIMEOUT = 3e4;
|
|
1590
|
+
/**
|
|
1591
|
+
* How long the page waits for `controllerchange` once the worker has
|
|
1592
|
+
* answered the `CLAIM` request. `clients.claim()` resolves only after the
|
|
1593
|
+
* browser has queued that event, so this is a grace period for delivery, not
|
|
1594
|
+
* a second budget.
|
|
1595
|
+
*/
|
|
1596
|
+
const CLAIM_GRACE_MS = 1e3;
|
|
1597
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
1598
|
+
const CLAIM_CALL = "CLAIM";
|
|
1599
|
+
/**
|
|
1600
|
+
* Why a page could not get a working ServiceWorker. `reason` says which wait
|
|
1601
|
+
* failed; check it (or `name`) rather than `instanceof`, because each of this
|
|
1602
|
+
* package's bundles carries its own copy of this class.
|
|
1603
|
+
*/
|
|
1604
|
+
var ServiceWorkerControlError = class extends Error {
|
|
1605
|
+
reason;
|
|
1606
|
+
constructor(reason, message) {
|
|
1607
|
+
super(message);
|
|
1608
|
+
this.name = "ServiceWorkerControlError";
|
|
1609
|
+
this.reason = reason;
|
|
1610
|
+
}
|
|
1611
|
+
};
|
|
1612
|
+
/**
|
|
1613
|
+
* Resolves with the registration's worker once it is `activated`. Rejects
|
|
1614
|
+
* with a `ServiceWorkerControlError` (`reason: "activation-timeout"`) if that
|
|
1615
|
+
* takes longer than `timeout` — an install that throws or never finishes
|
|
1616
|
+
* would otherwise leave the caller waiting forever.
|
|
1617
|
+
*/
|
|
1618
|
+
async function awaitActiveServiceWorker(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT } = {}) {
|
|
1619
|
+
return await withDeadline(Date.now() + timeout, waitForActivated(registration), () => {
|
|
1620
|
+
const worker = registration.installing ?? registration.waiting ?? registration.active;
|
|
1621
|
+
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.");
|
|
1622
|
+
});
|
|
1623
|
+
}
|
|
1624
|
+
/**
|
|
1625
|
+
* Resolves with the ServiceWorker that controls this page, once `registration`
|
|
1626
|
+
* has an activated worker and the page is controlled by it.
|
|
1627
|
+
*
|
|
1628
|
+
* A page can stay uncontrolled while its worker is active, and then no
|
|
1629
|
+
* `controllerchange` ever fires on its own:
|
|
1630
|
+
* - a hard reload (Ctrl+Shift+R) bypasses the worker for that load, and the
|
|
1631
|
+
* worker's `clients.claim()` already ran when it activated;
|
|
1632
|
+
* - Firefox can leave a page loaded while the worker is running uncontrolled.
|
|
1633
|
+
*
|
|
1634
|
+
* So when the page is uncontrolled, this asks the active worker to claim it
|
|
1635
|
+
* again (a `CLAIM` channel call — this package's workers answer it) and waits
|
|
1636
|
+
* for `controllerchange`. Every wait is bounded by `timeout`. If control never
|
|
1637
|
+
* comes it reloads once (`reloadIfUncontrolled`) or rejects with a
|
|
1638
|
+
* `ServiceWorkerControlError` (`reason: "uncontrolled"`) that says what
|
|
1639
|
+
* happened and what to do.
|
|
1640
|
+
*/
|
|
1641
|
+
async function awaitServiceWorkerControl(registration, { timeout = DEFAULT_SERVICE_WORKER_TIMEOUT, reloadIfUncontrolled = false, container = navigator.serviceWorker } = {}) {
|
|
1642
|
+
const deadline = Date.now() + timeout;
|
|
1643
|
+
const controlled = waitForController(container);
|
|
1644
|
+
try {
|
|
1645
|
+
const active = await awaitActiveServiceWorker(registration, { timeout });
|
|
1646
|
+
let controller = container.controller;
|
|
1647
|
+
if (!controller) controller = await withDeadline(deadline, (async () => {
|
|
1648
|
+
await Promise.race([callChannel(active, CLAIM_CALL, {}), controlled.promise]);
|
|
1649
|
+
return await Promise.race([controlled.promise, delay(CLAIM_GRACE_MS, null)]);
|
|
1650
|
+
})(), () => null);
|
|
1651
|
+
if (controller) {
|
|
1652
|
+
forgetReload(registration);
|
|
1653
|
+
return controller;
|
|
1654
|
+
}
|
|
1655
|
+
if (reloadIfUncontrolled && markReload(registration)) {
|
|
1656
|
+
location.reload();
|
|
1657
|
+
return await new Promise(() => {});
|
|
1658
|
+
}
|
|
1659
|
+
forgetReload(registration);
|
|
1660
|
+
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).`);
|
|
1661
|
+
} finally {
|
|
1662
|
+
controlled.cancel();
|
|
1663
|
+
}
|
|
1664
|
+
}
|
|
1665
|
+
/**
|
|
1666
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
1667
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
1668
|
+
* Returns a function that stops answering.
|
|
1669
|
+
*/
|
|
1670
|
+
function handleClaimRequests(self) {
|
|
1671
|
+
return handleChannelCalls(self, CLAIM_CALL, (event) => {
|
|
1672
|
+
const claimed = self.clients.claim().then(() => true);
|
|
1673
|
+
event.waitUntil?.(claimed);
|
|
1674
|
+
return claimed;
|
|
1675
|
+
});
|
|
1676
|
+
}
|
|
1677
|
+
function waitForActivated(registration) {
|
|
1678
|
+
return new Promise((resolve) => {
|
|
1679
|
+
const watched = /* @__PURE__ */ new Set();
|
|
1680
|
+
const check = () => {
|
|
1681
|
+
const active = registration.active;
|
|
1682
|
+
if (active?.state === "activated") {
|
|
1683
|
+
registration.removeEventListener("updatefound", watch);
|
|
1684
|
+
for (const worker of watched) worker.removeEventListener("statechange", check);
|
|
1685
|
+
resolve(active);
|
|
1686
|
+
return;
|
|
1687
|
+
}
|
|
1688
|
+
watch();
|
|
1689
|
+
};
|
|
1690
|
+
function watch() {
|
|
1691
|
+
for (const worker of [
|
|
1692
|
+
registration.installing,
|
|
1693
|
+
registration.waiting,
|
|
1694
|
+
registration.active
|
|
1695
|
+
]) {
|
|
1696
|
+
if (!worker || watched.has(worker)) continue;
|
|
1697
|
+
watched.add(worker);
|
|
1698
|
+
worker.addEventListener("statechange", check);
|
|
1699
|
+
}
|
|
1700
|
+
}
|
|
1701
|
+
registration.addEventListener("updatefound", watch);
|
|
1702
|
+
check();
|
|
1703
|
+
});
|
|
1704
|
+
}
|
|
1705
|
+
function waitForController(container) {
|
|
1706
|
+
let cancel = () => {};
|
|
1707
|
+
return {
|
|
1708
|
+
promise: new Promise((resolve) => {
|
|
1709
|
+
const onChange = () => {
|
|
1710
|
+
if (!container.controller) return;
|
|
1711
|
+
cancel();
|
|
1712
|
+
resolve(container.controller);
|
|
1713
|
+
};
|
|
1714
|
+
cancel = () => container.removeEventListener("controllerchange", onChange);
|
|
1715
|
+
container.addEventListener("controllerchange", onChange);
|
|
1716
|
+
}),
|
|
1717
|
+
cancel
|
|
1718
|
+
};
|
|
1719
|
+
}
|
|
1720
|
+
function delay(ms, value) {
|
|
1721
|
+
return new Promise((resolve) => setTimeout(() => resolve(value), ms));
|
|
1722
|
+
}
|
|
1723
|
+
function scriptUrl(worker) {
|
|
1724
|
+
return worker?.scriptURL ? `"${worker.scriptURL}"` : "(no worker)";
|
|
1725
|
+
}
|
|
1726
|
+
function reloadKey(registration) {
|
|
1727
|
+
return `webrun-http-browser:reloaded-uncontrolled:${registration.scope}`;
|
|
1728
|
+
}
|
|
1729
|
+
/** Records the reload about to happen. `false` if one already happened, or if it cannot be recorded. */
|
|
1730
|
+
function markReload(registration) {
|
|
1731
|
+
try {
|
|
1732
|
+
const key = reloadKey(registration);
|
|
1733
|
+
if (sessionStorage.getItem(key)) return false;
|
|
1734
|
+
sessionStorage.setItem(key, "1");
|
|
1735
|
+
return true;
|
|
1736
|
+
} catch {
|
|
1737
|
+
return false;
|
|
1738
|
+
}
|
|
1739
|
+
}
|
|
1740
|
+
function forgetReload(registration) {
|
|
1741
|
+
try {
|
|
1742
|
+
sessionStorage.removeItem(reloadKey(registration));
|
|
1743
|
+
} catch {}
|
|
1744
|
+
}
|
|
1745
|
+
//#endregion
|
|
1560
1746
|
//#region src/sw/sw-dispatcher.ts
|
|
1561
1747
|
/**
|
|
1562
1748
|
* Page-side counterpart: registers a ServiceWorker and sets up a
|
|
@@ -1605,6 +1791,15 @@ var SwPortHandler = class {
|
|
|
1605
1791
|
_getRegistrationInfo() {
|
|
1606
1792
|
return { key: this.key };
|
|
1607
1793
|
}
|
|
1794
|
+
/**
|
|
1795
|
+
* Registers the worker and waits until it is activated and controls this
|
|
1796
|
+
* page, then opens the port to it. The page must be controlled, because
|
|
1797
|
+
* only a controlled page's `fetch()` reaches the worker. When the page
|
|
1798
|
+
* loaded uncontrolled (a hard reload does that), the worker is asked to
|
|
1799
|
+
* claim it. Rejects with a `ServiceWorkerControlError` when that fails or
|
|
1800
|
+
* `timeout` passes (`reason` says which step); a later `start()` tries
|
|
1801
|
+
* again.
|
|
1802
|
+
*/
|
|
1608
1803
|
async start() {
|
|
1609
1804
|
if (!this._registrationPromise) this._registrationPromise = (async () => {
|
|
1610
1805
|
const [register, cleanup] = newRegistry();
|
|
@@ -1614,13 +1809,25 @@ var SwPortHandler = class {
|
|
|
1614
1809
|
});
|
|
1615
1810
|
const registration = await navigator.serviceWorker.register(this.serviceWorkerUrl, { scope: this.scope });
|
|
1616
1811
|
register(() => registration.unregister());
|
|
1617
|
-
|
|
1812
|
+
const stopListening = handleChannelCalls(navigator.serviceWorker, "UPDATE_COMMUNICATION_PORT", async (_event, _params, port) => {
|
|
1618
1813
|
await this._setCommunicationPort(port);
|
|
1619
1814
|
return this._getRegistrationInfo();
|
|
1620
|
-
})
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1815
|
+
});
|
|
1816
|
+
register(stopListening);
|
|
1817
|
+
const timeout = this.options.timeout ?? 3e4;
|
|
1818
|
+
const deadline = Date.now() + timeout;
|
|
1819
|
+
try {
|
|
1820
|
+
this._serviceWorker = await awaitServiceWorkerControl(registration, {
|
|
1821
|
+
timeout,
|
|
1822
|
+
reloadIfUncontrolled: this.options.reloadIfUncontrolled
|
|
1823
|
+
});
|
|
1824
|
+
await withDeadline(deadline, this._updateCommunicationChannel(), () => new ServiceWorkerControlError("unresponsive", `ServiceWorker "${this._serviceWorker?.scriptURL}" controls this page but did not answer the UPDATE_COMMUNICATION_PORT handshake within ${timeout} ms. Check that the worker script runs this package's same-origin dispatcher (it importScripts \`sw-worker.js\`, or calls \`startHttpDispatcher\`).`));
|
|
1825
|
+
} catch (error) {
|
|
1826
|
+
stopListening();
|
|
1827
|
+
this._cleanupRegistrations = void 0;
|
|
1828
|
+
this._registrationPromise = void 0;
|
|
1829
|
+
throw error;
|
|
1830
|
+
}
|
|
1624
1831
|
})();
|
|
1625
1832
|
return this._registrationPromise;
|
|
1626
1833
|
}
|
|
@@ -1634,35 +1841,6 @@ var SwPortHandler = class {
|
|
|
1634
1841
|
}
|
|
1635
1842
|
}
|
|
1636
1843
|
};
|
|
1637
|
-
function getServiceWorkerController() {
|
|
1638
|
-
return new Promise((resolve) => {
|
|
1639
|
-
const container = navigator.serviceWorker;
|
|
1640
|
-
if (container.controller) {
|
|
1641
|
-
resolve(container.controller);
|
|
1642
|
-
return;
|
|
1643
|
-
}
|
|
1644
|
-
const onChange = () => {
|
|
1645
|
-
if (!container.controller) return;
|
|
1646
|
-
resolve(container.controller);
|
|
1647
|
-
container.removeEventListener("controllerchange", onChange);
|
|
1648
|
-
};
|
|
1649
|
-
container.addEventListener("controllerchange", onChange);
|
|
1650
|
-
});
|
|
1651
|
-
}
|
|
1652
|
-
function awaitServiceWorkerActivation(worker) {
|
|
1653
|
-
return new Promise((resolve) => {
|
|
1654
|
-
if (worker.state === "activated") {
|
|
1655
|
-
resolve();
|
|
1656
|
-
return;
|
|
1657
|
-
}
|
|
1658
|
-
const onStateChange = () => {
|
|
1659
|
-
if (worker.state !== "activated") return;
|
|
1660
|
-
worker.removeEventListener("statechange", onStateChange);
|
|
1661
|
-
resolve();
|
|
1662
|
-
};
|
|
1663
|
-
worker.addEventListener("statechange", onStateChange);
|
|
1664
|
-
});
|
|
1665
|
-
}
|
|
1666
1844
|
/**
|
|
1667
1845
|
* ServiceWorker-side counterpart: maintains an index of connected clients keyed by
|
|
1668
1846
|
* clientId, persisted in IndexedDB so it can reclaim ports across SW restarts.
|
|
@@ -1710,7 +1888,8 @@ var SwPortDispatcher = class {
|
|
|
1710
1888
|
return this.claimedKeys.has(key);
|
|
1711
1889
|
}
|
|
1712
1890
|
start() {
|
|
1713
|
-
|
|
1891
|
+
const stopClaims = handleClaimRequests(this.self);
|
|
1892
|
+
const stopPortUpdates = handleChannelCalls(this.self, "UPDATE_COMMUNICATION_PORT", async (event, channelInfo, port) => {
|
|
1714
1893
|
const clientId = event.source?.id;
|
|
1715
1894
|
this.log("[UPDATE_COMMUNICATION_PORT]", clientId, channelInfo);
|
|
1716
1895
|
await this._updateChannelInfo({
|
|
@@ -1720,6 +1899,10 @@ var SwPortDispatcher = class {
|
|
|
1720
1899
|
});
|
|
1721
1900
|
return { ...channelInfo };
|
|
1722
1901
|
});
|
|
1902
|
+
this._cleanup = () => {
|
|
1903
|
+
stopClaims();
|
|
1904
|
+
stopPortUpdates();
|
|
1905
|
+
};
|
|
1723
1906
|
this.self.addEventListener("install", (event) => {
|
|
1724
1907
|
this.log("Skip waiting on install.", event);
|
|
1725
1908
|
this.self.skipWaiting();
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@statewalker/webrun-http-browser",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.6.0",
|
|
4
4
|
"private": false,
|
|
5
5
|
"type": "module",
|
|
6
6
|
"description": "ServiceWorker-based HTTP server for browsers, with relay and same-origin dispatch modes",
|
|
@@ -29,6 +29,10 @@
|
|
|
29
29
|
"./relay-sw": {
|
|
30
30
|
"default": "./dist/relay-sw.js"
|
|
31
31
|
},
|
|
32
|
+
"./relay-worker": {
|
|
33
|
+
"types": "./dist/relay-worker.d.ts",
|
|
34
|
+
"import": "./dist/relay-worker.js"
|
|
35
|
+
},
|
|
32
36
|
"./sw-worker": {
|
|
33
37
|
"default": "./dist/sw-worker.js"
|
|
34
38
|
}
|
|
@@ -47,11 +51,14 @@
|
|
|
47
51
|
"@statewalker/webrun-streams": "0.2.0"
|
|
48
52
|
},
|
|
49
53
|
"devDependencies": {
|
|
54
|
+
"@biomejs/biome": "^2.5.8",
|
|
50
55
|
"@types/node": "^26.2.0",
|
|
51
56
|
"http-server": "^14.1.1",
|
|
57
|
+
"playwright": "^1.62.1",
|
|
52
58
|
"rimraf": "^6.1.3",
|
|
53
59
|
"rolldown": "^1.2.4",
|
|
54
60
|
"typescript": "^7.0.2",
|
|
61
|
+
"vite": "^8.2.1",
|
|
55
62
|
"vitest": "^4.1.10"
|
|
56
63
|
},
|
|
57
64
|
"sideEffects": false,
|
|
@@ -61,6 +68,7 @@
|
|
|
61
68
|
"scripts": {
|
|
62
69
|
"build": "rimraf dist && rolldown -c && tsc --emitDeclarationOnly --declaration",
|
|
63
70
|
"test": "vitest run",
|
|
71
|
+
"test:browser": "pnpm run build && vitest run --config vitest.browser.config.ts",
|
|
64
72
|
"lint": "biome check src tests",
|
|
65
73
|
"serve": "http-server -p 5173 -c-1 -s .",
|
|
66
74
|
"example:same-origin": "pnpm run build && http-server -p 5173 -c-1 -o /public/index.html .",
|
|
@@ -0,0 +1,31 @@
|
|
|
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 function withDeadline<T, F>(
|
|
7
|
+
deadline: number,
|
|
8
|
+
promise: Promise<T>,
|
|
9
|
+
onTimeout: () => F,
|
|
10
|
+
): Promise<T | (F extends Error ? never : F)> {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const timer = setTimeout(
|
|
13
|
+
() => {
|
|
14
|
+
const outcome = onTimeout();
|
|
15
|
+
if (outcome instanceof Error) reject(outcome);
|
|
16
|
+
else resolve(outcome as F extends Error ? never : F);
|
|
17
|
+
},
|
|
18
|
+
Math.max(0, deadline - Date.now()),
|
|
19
|
+
);
|
|
20
|
+
promise.then(
|
|
21
|
+
(value) => {
|
|
22
|
+
clearTimeout(timer);
|
|
23
|
+
resolve(value);
|
|
24
|
+
},
|
|
25
|
+
(error) => {
|
|
26
|
+
clearTimeout(timer);
|
|
27
|
+
reject(error);
|
|
28
|
+
},
|
|
29
|
+
);
|
|
30
|
+
});
|
|
31
|
+
}
|
package/src/core/index.ts
CHANGED