@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
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Which registered service owns a URL.
|
|
3
|
+
*
|
|
4
|
+
* A pure lookup: no ServiceWorker, no storage, no I/O, so the routing rules
|
|
5
|
+
* can be tested as arithmetic rather than through a browser.
|
|
6
|
+
*
|
|
7
|
+
* TWO KINDS OF MOUNT, AND WHY BOTH. A `path` is a prefix, which is all most
|
|
8
|
+
* hosts need and costs nothing to match. A `match` predicate is the escape
|
|
9
|
+
* hatch for anything richer -- a host that wants URLPattern brings it and pays
|
|
10
|
+
* for it; this file must stay dependency-free, because it runs in a
|
|
11
|
+
* ServiceWorker that has to start fast.
|
|
12
|
+
*
|
|
13
|
+
* SPECIFICITY, NOT REGISTRATION ORDER. Prefixes are tried longest-first, so a
|
|
14
|
+
* catch-all at "/" cannot swallow "/peers/" and a host need not register in a
|
|
15
|
+
* careful order. Predicates are opaque -- nothing can be said about how
|
|
16
|
+
* specific they are -- so they are tried after every prefix, in the order they
|
|
17
|
+
* were registered.
|
|
18
|
+
*/
|
|
19
|
+
export interface MountSpec {
|
|
20
|
+
/** A path prefix, e.g. `/peers/`. `/` is the whole origin. */
|
|
21
|
+
path?: string;
|
|
22
|
+
/** Anything richer. Consulted only when no prefix matches. */
|
|
23
|
+
match?: (url: URL) => boolean;
|
|
24
|
+
}
|
|
25
|
+
export interface MountTable {
|
|
26
|
+
/** Add or replace the mount for `key`. */
|
|
27
|
+
set(key: string, spec: MountSpec): void;
|
|
28
|
+
remove(key: string): void;
|
|
29
|
+
/** The key that owns `url`, or `undefined` — meaning "not the relay's". */
|
|
30
|
+
find(url: URL): string | undefined;
|
|
31
|
+
/**
|
|
32
|
+
* Is `url` reserved by `exclude`? `find` already applies it, but the relay
|
|
33
|
+
* has a SECOND route — the `/~<key>/` spelling, which does not go through
|
|
34
|
+
* the table at all — and "an excluded path is never claimed" has to hold
|
|
35
|
+
* for both. The predicate lives here so there is one copy of it.
|
|
36
|
+
*/
|
|
37
|
+
excludes(url: URL): boolean;
|
|
38
|
+
}
|
|
39
|
+
export interface MountTableOptions {
|
|
40
|
+
/**
|
|
41
|
+
* Paths the relay never claims, checked BEFORE the table. A root mount
|
|
42
|
+
* matches every path, so a host with files of its own (a relay page, a
|
|
43
|
+
* worker, hashed assets) is unusable without this.
|
|
44
|
+
*/
|
|
45
|
+
exclude?: (url: URL) => boolean;
|
|
46
|
+
}
|
|
47
|
+
export declare function newMountTable(options?: MountTableOptions): MountTable;
|
|
48
|
+
//# sourceMappingURL=mount-table.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"mount-table.d.ts","sourceRoot":"","sources":["../../src/relay/mount-table.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,MAAM,WAAW,SAAS;IACxB,8DAA8D;IAC9D,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,8DAA8D;IAC9D,KAAK,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CAC/B;AAED,MAAM,WAAW,UAAU;IACzB,0CAA0C;IAC1C,GAAG,CAAC,GAAG,EAAE,MAAM,EAAE,IAAI,EAAE,SAAS,GAAG,IAAI,CAAC;IACxC,MAAM,CAAC,GAAG,EAAE,MAAM,GAAG,IAAI,CAAC;IAC1B,2EAA2E;IAC3E,IAAI,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,GAAG,SAAS,CAAC;IACnC;;;;;OAKG;IACH,QAAQ,CAAC,GAAG,EAAE,GAAG,GAAG,OAAO,CAAC;CAC7B;AAED,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,CAAC,EAAE,CAAC,GAAG,EAAE,GAAG,KAAK,OAAO,CAAC;CACjC;AAyBD,wBAAgB,aAAa,CAAC,OAAO,GAAE,iBAAsB,GAAG,UAAU,CAyCzE"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"split-service-url.d.ts","sourceRoot":"","sources":["../../src/relay/split-service-url.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,SAAS,SAAM,GAAG,eAAe,
|
|
1
|
+
{"version":3,"file":"split-service-url.d.ts","sourceRoot":"","sources":["../../src/relay/split-service-url.ts"],"names":[],"mappings":"AAAA,MAAM,WAAW,eAAe;IAC9B,GAAG,EAAE,MAAM,CAAC;IACZ,GAAG,EAAE,MAAM,CAAC;IACZ,OAAO,EAAE,MAAM,CAAC;IAChB,IAAI,EAAE,MAAM,CAAC;IACb,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;CAC1B;AAED;;;GAGG;AACH,wBAAgB,eAAe,CAAC,GAAG,EAAE,GAAG,GAAG,MAAM,EAAE,SAAS,SAAM,GAAG,eAAe,CA6EnF"}
|
package/dist/relay-sw.js
CHANGED
|
@@ -1202,7 +1202,7 @@
|
|
|
1202
1202
|
}
|
|
1203
1203
|
};
|
|
1204
1204
|
//#endregion
|
|
1205
|
-
//#region
|
|
1205
|
+
//#region ../../node_modules/.pnpm/idb-keyval@6.3.0/node_modules/idb-keyval/dist/index.js
|
|
1206
1206
|
function promisifyRequest(request) {
|
|
1207
1207
|
return new Promise((resolve, reject) => {
|
|
1208
1208
|
request.oncomplete = request.onsuccess = () => resolve(request.result);
|
|
@@ -1319,6 +1319,22 @@
|
|
|
1319
1319
|
});
|
|
1320
1320
|
}
|
|
1321
1321
|
//#endregion
|
|
1322
|
+
//#region src/core/service-worker-control.ts
|
|
1323
|
+
/** Channel call a page sends to ask its ServiceWorker to `clients.claim()` it. */
|
|
1324
|
+
const CLAIM_CALL = "CLAIM";
|
|
1325
|
+
/**
|
|
1326
|
+
* ServiceWorker side: answers the page's `CLAIM` request with
|
|
1327
|
+
* `clients.claim()`, which takes over every uncontrolled client in scope.
|
|
1328
|
+
* Returns a function that stops answering.
|
|
1329
|
+
*/
|
|
1330
|
+
function handleClaimRequests(self) {
|
|
1331
|
+
return handleChannelCalls(self, CLAIM_CALL, (event) => {
|
|
1332
|
+
const claimed = self.clients.claim().then(() => true);
|
|
1333
|
+
event.waitUntil?.(claimed);
|
|
1334
|
+
return claimed;
|
|
1335
|
+
});
|
|
1336
|
+
}
|
|
1337
|
+
//#endregion
|
|
1322
1338
|
//#region src/core/data-channels.ts
|
|
1323
1339
|
const MESSAGE_TYPE_REQUEST = "REQUEST";
|
|
1324
1340
|
const MESSAGE_TYPE_RESPONSE = "RESPONSE";
|
|
@@ -1517,6 +1533,51 @@
|
|
|
1517
1533
|
})(request);
|
|
1518
1534
|
}
|
|
1519
1535
|
//#endregion
|
|
1536
|
+
//#region src/relay/mount-table.ts
|
|
1537
|
+
/** `/` stays `/`; `/peers` and `/peers/` both become `/peers/`. */
|
|
1538
|
+
function normalise(path) {
|
|
1539
|
+
if (path === "" || path === "/") return "/";
|
|
1540
|
+
const withSlash = path.startsWith("/") ? path : `/${path}`;
|
|
1541
|
+
return withSlash.endsWith("/") ? withSlash : `${withSlash}/`;
|
|
1542
|
+
}
|
|
1543
|
+
/** Does `prefix` own `pathname`? `/peers/` owns `/peers/`, `/peers` and `/peers/x`. */
|
|
1544
|
+
function owns(prefix, pathname) {
|
|
1545
|
+
if (prefix === "/") return true;
|
|
1546
|
+
if (pathname.startsWith(prefix)) return true;
|
|
1547
|
+
return `${pathname}/` === prefix;
|
|
1548
|
+
}
|
|
1549
|
+
function newMountTable(options = {}) {
|
|
1550
|
+
const entries = /* @__PURE__ */ new Map();
|
|
1551
|
+
let seq = 0;
|
|
1552
|
+
return {
|
|
1553
|
+
set(key, spec) {
|
|
1554
|
+
entries.set(key, {
|
|
1555
|
+
key,
|
|
1556
|
+
prefix: spec.path == null ? "" : normalise(spec.path),
|
|
1557
|
+
match: spec.match,
|
|
1558
|
+
seq: seq++
|
|
1559
|
+
});
|
|
1560
|
+
},
|
|
1561
|
+
remove(key) {
|
|
1562
|
+
entries.delete(key);
|
|
1563
|
+
},
|
|
1564
|
+
excludes(url) {
|
|
1565
|
+
return options.exclude?.(url) === true;
|
|
1566
|
+
},
|
|
1567
|
+
find(url) {
|
|
1568
|
+
if (options.exclude?.(url) === true) return void 0;
|
|
1569
|
+
let best;
|
|
1570
|
+
for (const entry of entries.values()) {
|
|
1571
|
+
if (entry.prefix === "" || !owns(entry.prefix, url.pathname)) continue;
|
|
1572
|
+
if (best == null || entry.prefix.length > best.prefix.length) best = entry;
|
|
1573
|
+
}
|
|
1574
|
+
if (best != null) return best.key;
|
|
1575
|
+
const predicates = [...entries.values()].filter((entry) => entry.match != null).sort((a, b) => a.seq - b.seq);
|
|
1576
|
+
for (const entry of predicates) if (entry.match?.(url) === true) return entry.key;
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
}
|
|
1580
|
+
//#endregion
|
|
1520
1581
|
//#region src/relay/split-service-url.ts
|
|
1521
1582
|
/**
|
|
1522
1583
|
* Splits a URL of the form `<base>/<separator><key>/<path>` into parts.
|
|
@@ -1524,36 +1585,165 @@
|
|
|
1524
1585
|
*/
|
|
1525
1586
|
function splitServiceUrl(url, separator = "~") {
|
|
1526
1587
|
const str = `${url}`;
|
|
1527
|
-
const
|
|
1528
|
-
|
|
1529
|
-
|
|
1530
|
-
|
|
1531
|
-
|
|
1532
|
-
|
|
1533
|
-
|
|
1534
|
-
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1588
|
+
const empty = {
|
|
1589
|
+
url: str,
|
|
1590
|
+
key: "",
|
|
1591
|
+
baseUrl: "",
|
|
1592
|
+
path: ""
|
|
1593
|
+
};
|
|
1594
|
+
const hashIdx = str.indexOf("#");
|
|
1595
|
+
const queryIdx = str.indexOf("?");
|
|
1596
|
+
let strippedEnd = str.length;
|
|
1597
|
+
if (hashIdx >= 0) strippedEnd = Math.min(strippedEnd, hashIdx);
|
|
1598
|
+
if (queryIdx >= 0) strippedEnd = Math.min(strippedEnd, queryIdx);
|
|
1599
|
+
const stripped = str.slice(0, strippedEnd);
|
|
1600
|
+
let prefixEnd = 0;
|
|
1601
|
+
const schemeMatch = stripped.match(/^[a-zA-Z][a-zA-Z0-9+\-.]*:\/\//);
|
|
1602
|
+
if (schemeMatch) {
|
|
1603
|
+
prefixEnd = schemeMatch[0].length;
|
|
1604
|
+
const slashIdx = stripped.indexOf("/", prefixEnd);
|
|
1605
|
+
if (slashIdx >= 0) prefixEnd = slashIdx;
|
|
1606
|
+
else return empty;
|
|
1607
|
+
} else if (stripped.startsWith("//")) {
|
|
1608
|
+
prefixEnd = 2;
|
|
1609
|
+
const slashIdx = stripped.indexOf("/", prefixEnd);
|
|
1610
|
+
if (slashIdx >= 0) prefixEnd = slashIdx;
|
|
1611
|
+
else return empty;
|
|
1540
1612
|
}
|
|
1613
|
+
const prefix = stripped.slice(0, prefixEnd);
|
|
1614
|
+
const pathPart = stripped.slice(prefixEnd);
|
|
1615
|
+
let keyStart;
|
|
1616
|
+
let rooted = false;
|
|
1617
|
+
if (prefix === "") {
|
|
1618
|
+
if (pathPart.startsWith(`/${separator}`)) {
|
|
1619
|
+
rooted = true;
|
|
1620
|
+
keyStart = separator.length + 1;
|
|
1621
|
+
} else if (pathPart.startsWith(separator)) keyStart = separator.length;
|
|
1622
|
+
else return empty;
|
|
1623
|
+
} else {
|
|
1624
|
+
if (!pathPart.startsWith(`/${separator}`)) return empty;
|
|
1625
|
+
keyStart = separator.length + 1;
|
|
1626
|
+
}
|
|
1627
|
+
const rest = pathPart.slice(keyStart);
|
|
1628
|
+
const slash = rest.indexOf("/");
|
|
1629
|
+
const key = slash < 0 ? rest : rest.slice(0, slash);
|
|
1630
|
+
if (key === "") return empty;
|
|
1541
1631
|
return {
|
|
1542
1632
|
url: str,
|
|
1543
1633
|
key,
|
|
1544
|
-
baseUrl
|
|
1545
|
-
path
|
|
1634
|
+
baseUrl: `${prefix === "" ? rooted ? "/" : "" : `${prefix}/`}${separator}${key}${slash < 0 ? "" : "/"}`,
|
|
1635
|
+
path: slash < 0 ? "" : rest.slice(slash + 1)
|
|
1546
1636
|
};
|
|
1547
1637
|
}
|
|
1548
1638
|
//#endregion
|
|
1549
1639
|
//#region src/relay/index-sw.ts
|
|
1550
1640
|
/**
|
|
1641
|
+
* One stored registry entry, whatever shape it is on disk.
|
|
1642
|
+
*
|
|
1643
|
+
* BEFORE MOUNTS THE VALUE WAS A BARE CLIENT ID. A browser that ran the earlier
|
|
1644
|
+
* worker still holds that shape, and reading it as an object would drop the id
|
|
1645
|
+
* and quietly unregister every service the visitor had.
|
|
1646
|
+
*/
|
|
1647
|
+
function readStoredEntry(value) {
|
|
1648
|
+
if (typeof value === "string") return { clientId: value };
|
|
1649
|
+
if (typeof value !== "object" || value === null) return void 0;
|
|
1650
|
+
const { clientId, path } = value;
|
|
1651
|
+
if (typeof clientId !== "string" || clientId === "") return void 0;
|
|
1652
|
+
return typeof path === "string" ? {
|
|
1653
|
+
clientId,
|
|
1654
|
+
path
|
|
1655
|
+
} : { clientId };
|
|
1656
|
+
}
|
|
1657
|
+
/**
|
|
1658
|
+
* Which service, if any, should answer `url`.
|
|
1659
|
+
*
|
|
1660
|
+
* `undefined` means NOT THE RELAY'S, and the caller must not call
|
|
1661
|
+
* `respondWith`: the request then goes to the network, which is how a host
|
|
1662
|
+
* keeps serving its own files from its own origin. Answering 404 here instead
|
|
1663
|
+
* would make a root mount fatal.
|
|
1664
|
+
*/
|
|
1665
|
+
function resolveServiceKey(url, table, selfOrigin) {
|
|
1666
|
+
if (url.origin !== selfOrigin) return void 0;
|
|
1667
|
+
if (table.excludes(url)) return void 0;
|
|
1668
|
+
const mounted = table.find(url);
|
|
1669
|
+
if (mounted != null) return mounted;
|
|
1670
|
+
const { key } = splitServiceUrl(url);
|
|
1671
|
+
return key === "" ? void 0 : key;
|
|
1672
|
+
}
|
|
1673
|
+
/**
|
|
1674
|
+
* Waits for the mount table to be restored from the registry before routing
|
|
1675
|
+
* `url` — but a restore failure must never wedge every fetch. `restored`
|
|
1676
|
+
* rejecting (blocked storage, quota, private-mode edge cases) would otherwise
|
|
1677
|
+
* propagate straight to `respondWith` on every request, including ones that
|
|
1678
|
+
* should reach the network, which breaks the one rule this file exists to
|
|
1679
|
+
* uphold. So: log the failure and route with whatever the in-memory table
|
|
1680
|
+
* already holds — possibly empty, never fatal.
|
|
1681
|
+
*/
|
|
1682
|
+
async function resolveAfterRestore(restored, url, table, selfOrigin) {
|
|
1683
|
+
try {
|
|
1684
|
+
await restored;
|
|
1685
|
+
} catch (error) {
|
|
1686
|
+
console.error("[relay] failed to restore mounts from the registry", error);
|
|
1687
|
+
}
|
|
1688
|
+
return resolveServiceKey(url, table, selfOrigin);
|
|
1689
|
+
}
|
|
1690
|
+
/**
|
|
1691
|
+
* What REGISTER does to the mount table: set it when `path` is given, or
|
|
1692
|
+
* remove any earlier mount when it is not. A path-less re-registration
|
|
1693
|
+
* reverts a service to `/~<key>/` addressing, and a stale prefix left behind
|
|
1694
|
+
* would keep routing requests to a mount that no longer exists.
|
|
1695
|
+
*
|
|
1696
|
+
* A REGISTRATION ONLY TOUCHES WHAT A REGISTRATION MADE. `hostKeys` names the
|
|
1697
|
+
* mounts the host declared itself, in `options.mounts`. Those are the host's
|
|
1698
|
+
* build-time decision and a page may not undo it: the documented static flow
|
|
1699
|
+
* -- host declares `{ key: "app", path: "/" }`, page calls `initHttpService(h,
|
|
1700
|
+
* { key: "app", port })` with no path -- would otherwise have its very first
|
|
1701
|
+
* registration delete the host's own mount and leave the origin unmounted. A
|
|
1702
|
+
* path-ful REGISTER naming a host key is ignored for the same reason (it would
|
|
1703
|
+
* replace a `match` predicate with a prefix of the page's choosing).
|
|
1704
|
+
*/
|
|
1705
|
+
function applyRegisteredMount(table, key, path, hostKeys = /* @__PURE__ */ new Set()) {
|
|
1706
|
+
if (hostKeys.has(key)) return;
|
|
1707
|
+
if (path != null) table.set(key, { path });
|
|
1708
|
+
else table.remove(key);
|
|
1709
|
+
}
|
|
1710
|
+
/**
|
|
1711
|
+
* What UNREGISTER does to the mount table: drop the mount a registration
|
|
1712
|
+
* made. A host-declared mount stays, for the reason `applyRegisteredMount`
|
|
1713
|
+
* gives -- a page tearing down its service must not take the host's table
|
|
1714
|
+
* with it.
|
|
1715
|
+
*/
|
|
1716
|
+
function removeRegisteredMount(table, key, hostKeys = /* @__PURE__ */ new Set()) {
|
|
1717
|
+
if (hostKeys.has(key)) return;
|
|
1718
|
+
table.remove(key);
|
|
1719
|
+
}
|
|
1720
|
+
/**
|
|
1721
|
+
* May `candidateId` take the key?
|
|
1722
|
+
*
|
|
1723
|
+
* `last-wins` is what the relay has always done and stays the default. With
|
|
1724
|
+
* `first-wins`, a LIVE holder keeps its key: on an origin whose name is
|
|
1725
|
+
* guessable, a second page proves nothing by existing. A holder that reloaded
|
|
1726
|
+
* is no longer live, so a host's own re-registration is never blocked.
|
|
1727
|
+
*/
|
|
1728
|
+
function mayRegister(args) {
|
|
1729
|
+
if (args.takeover === "last-wins") return true;
|
|
1730
|
+
if (args.current == null || !args.isCurrentLive) return true;
|
|
1731
|
+
return args.current.clientId === args.candidateId;
|
|
1732
|
+
}
|
|
1733
|
+
/**
|
|
1551
1734
|
* Boots the relay ServiceWorker: routes fetches shaped `<origin>/~<key>/…` to
|
|
1552
1735
|
* the client that registered `key`, and exposes REGISTER/UNREGISTER/CONNECT
|
|
1553
1736
|
* channel calls used by the page-side relay client.
|
|
1554
1737
|
*/
|
|
1555
|
-
function startRelayServiceWorker(self) {
|
|
1738
|
+
function startRelayServiceWorker(self, options = {}) {
|
|
1556
1739
|
const [register, clear] = newRegistry();
|
|
1740
|
+
const mounts = newMountTable({ exclude: options.exclude });
|
|
1741
|
+
const hostKeys = /* @__PURE__ */ new Set();
|
|
1742
|
+
for (const { key, ...spec } of options.mounts ?? []) {
|
|
1743
|
+
mounts.set(key, spec);
|
|
1744
|
+
hostKeys.add(key);
|
|
1745
|
+
}
|
|
1746
|
+
const takeover = options.takeover ?? "last-wins";
|
|
1557
1747
|
if (typeof self.skipWaiting === "function") self.addEventListener("install", (e) => {
|
|
1558
1748
|
e.waitUntil(self.skipWaiting());
|
|
1559
1749
|
});
|
|
@@ -1561,14 +1751,35 @@
|
|
|
1561
1751
|
e.waitUntil(self.clients.claim());
|
|
1562
1752
|
});
|
|
1563
1753
|
const clientsRegistry = newClientsRegistry({ self });
|
|
1754
|
+
const restored = clientsRegistry.restoreMounts(mounts, hostKeys);
|
|
1755
|
+
let restoreSettled = false;
|
|
1756
|
+
const latch = () => {
|
|
1757
|
+
restoreSettled = true;
|
|
1758
|
+
};
|
|
1759
|
+
restored.then(latch, latch);
|
|
1760
|
+
register(handleClaimRequests(self));
|
|
1564
1761
|
register(handleChannelCalls(self, "REGISTER", async (event, data) => {
|
|
1565
1762
|
const source = event.source;
|
|
1566
1763
|
if (!source) return false;
|
|
1567
|
-
const { key } = data;
|
|
1568
|
-
|
|
1764
|
+
const { key, path } = data;
|
|
1765
|
+
if (options.canRegister != null && !await options.canRegister(source, key)) throw new Error(`this client may not register "${key}"`);
|
|
1766
|
+
if (takeover === "first-wins") {
|
|
1767
|
+
const current = await clientsRegistry.getMount(key);
|
|
1768
|
+
const isCurrentLive = current != null && await clientsRegistry.getClient(key) != null;
|
|
1769
|
+
if (!mayRegister({
|
|
1770
|
+
current,
|
|
1771
|
+
candidateId: source.id,
|
|
1772
|
+
isCurrentLive,
|
|
1773
|
+
takeover
|
|
1774
|
+
})) throw new Error(`"${key}" is already served by another client`);
|
|
1775
|
+
}
|
|
1776
|
+
const added = await clientsRegistry.addClient(key, source, path);
|
|
1777
|
+
applyRegisteredMount(mounts, key, path, hostKeys);
|
|
1778
|
+
return added;
|
|
1569
1779
|
}));
|
|
1570
1780
|
register(handleChannelCalls(self, "UNREGISTER", async (_event, data) => {
|
|
1571
1781
|
const { key } = data;
|
|
1782
|
+
removeRegisteredMount(mounts, key, hostKeys);
|
|
1572
1783
|
return await clientsRegistry.removeClient(key);
|
|
1573
1784
|
}));
|
|
1574
1785
|
register(handleChannelCalls(self, "CONNECT", async (_event, data, port) => {
|
|
@@ -1577,30 +1788,43 @@
|
|
|
1577
1788
|
if (!client) throw new Error(`Target client was not found. Target key: "${key}".`);
|
|
1578
1789
|
return await callChannel(client, "CONNECT", data, port);
|
|
1579
1790
|
}));
|
|
1791
|
+
/** Answers `request` as the service registered under `key`. */
|
|
1792
|
+
async function serve(key, request, url) {
|
|
1793
|
+
const params = splitServiceUrl(url);
|
|
1794
|
+
try {
|
|
1795
|
+
const channel = new MessageChannel();
|
|
1796
|
+
const client = await clientsRegistry.getClient(key);
|
|
1797
|
+
if (!client) throw HttpError.errorResourceGone(params);
|
|
1798
|
+
if (!await callChannel(client, "CONNECT", {
|
|
1799
|
+
type: "http",
|
|
1800
|
+
key
|
|
1801
|
+
}, channel.port2)) throw HttpError.errorForbidden(params);
|
|
1802
|
+
const response = await sendHttpRequest(channel.port1, request);
|
|
1803
|
+
return options.decorateResponse?.(response, request) ?? response;
|
|
1804
|
+
} catch (error) {
|
|
1805
|
+
const httpError = HttpError.fromError(error);
|
|
1806
|
+
const errorOptions = httpError.getResponseOptions(params);
|
|
1807
|
+
const errorResponse = new Response(JSON.stringify(errorOptions), {
|
|
1808
|
+
status: httpError.status ?? 500,
|
|
1809
|
+
statusText: httpError.statusText ?? "Internal Error",
|
|
1810
|
+
headers: { "Content-Type": "application/json" }
|
|
1811
|
+
});
|
|
1812
|
+
return options.decorateResponse?.(errorResponse, request) ?? errorResponse;
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1580
1815
|
const fetchListener = (event) => {
|
|
1581
1816
|
const request = event.request;
|
|
1582
|
-
const
|
|
1583
|
-
|
|
1584
|
-
|
|
1817
|
+
const url = new URL(request.url);
|
|
1818
|
+
if (restoreSettled) {
|
|
1819
|
+
const key = resolveServiceKey(url, mounts, self.location.origin);
|
|
1820
|
+
if (key == null) return;
|
|
1821
|
+
event.respondWith(serve(key, request, url));
|
|
1822
|
+
return;
|
|
1823
|
+
}
|
|
1585
1824
|
event.respondWith((async () => {
|
|
1586
|
-
|
|
1587
|
-
|
|
1588
|
-
|
|
1589
|
-
if (!client) throw HttpError.errorResourceGone(params);
|
|
1590
|
-
if (!await callChannel(client, "CONNECT", {
|
|
1591
|
-
type: "http",
|
|
1592
|
-
key
|
|
1593
|
-
}, channel.port2)) throw HttpError.errorForbidden(params);
|
|
1594
|
-
return await sendHttpRequest(channel.port1, request);
|
|
1595
|
-
} catch (error) {
|
|
1596
|
-
const httpError = HttpError.fromError(error);
|
|
1597
|
-
const options = httpError.getResponseOptions(params);
|
|
1598
|
-
return new Response(JSON.stringify(options), {
|
|
1599
|
-
status: httpError.status ?? 500,
|
|
1600
|
-
statusText: httpError.statusText ?? "Internal Error",
|
|
1601
|
-
headers: { "Content-Type": "application/json" }
|
|
1602
|
-
});
|
|
1603
|
-
}
|
|
1825
|
+
const key = await resolveAfterRestore(restored, url, mounts, self.location.origin);
|
|
1826
|
+
if (key == null) return await fetch(request);
|
|
1827
|
+
return await serve(key, request, url);
|
|
1604
1828
|
})());
|
|
1605
1829
|
};
|
|
1606
1830
|
self.addEventListener("fetch", fetchListener);
|
|
@@ -1612,7 +1836,11 @@
|
|
|
1612
1836
|
async function loadClientsIndex() {
|
|
1613
1837
|
if (!_index) {
|
|
1614
1838
|
const entries = await get(key) ?? [];
|
|
1615
|
-
_index =
|
|
1839
|
+
_index = {};
|
|
1840
|
+
for (const [clientKey, value] of entries) {
|
|
1841
|
+
const entry = readStoredEntry(value);
|
|
1842
|
+
if (entry) _index[clientKey] = entry;
|
|
1843
|
+
}
|
|
1616
1844
|
}
|
|
1617
1845
|
return _index;
|
|
1618
1846
|
}
|
|
@@ -1621,11 +1849,14 @@
|
|
|
1621
1849
|
await set(key, Object.entries(index));
|
|
1622
1850
|
return index;
|
|
1623
1851
|
}
|
|
1624
|
-
async function addClient(clientKey, client) {
|
|
1852
|
+
async function addClient(clientKey, client, path) {
|
|
1625
1853
|
const index = await loadClientsIndex();
|
|
1626
|
-
const
|
|
1627
|
-
if (
|
|
1628
|
-
index[clientKey] = clientId
|
|
1854
|
+
const current = index[clientKey];
|
|
1855
|
+
if (current?.clientId === client.id && current.path === path) return false;
|
|
1856
|
+
index[clientKey] = path == null ? { clientId: client.id } : {
|
|
1857
|
+
clientId: client.id,
|
|
1858
|
+
path
|
|
1859
|
+
};
|
|
1629
1860
|
await storeClientsIndex();
|
|
1630
1861
|
return true;
|
|
1631
1862
|
}
|
|
@@ -1636,11 +1867,21 @@
|
|
|
1636
1867
|
await storeClientsIndex();
|
|
1637
1868
|
return true;
|
|
1638
1869
|
}
|
|
1870
|
+
async function getMount(clientKey) {
|
|
1871
|
+
return (await loadClientsIndex())[clientKey];
|
|
1872
|
+
}
|
|
1873
|
+
async function restoreMounts(table, hostKeys = /* @__PURE__ */ new Set()) {
|
|
1874
|
+
const index = await loadClientsIndex();
|
|
1875
|
+
for (const [clientKey, entry] of Object.entries(index)) {
|
|
1876
|
+
if (hostKeys.has(clientKey)) continue;
|
|
1877
|
+
if (entry.path != null) table.set(clientKey, { path: entry.path });
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1639
1880
|
async function getClient(clientKey) {
|
|
1640
1881
|
const index = await loadClientsIndex();
|
|
1641
|
-
const
|
|
1642
|
-
if (!
|
|
1643
|
-
const client = await self.clients.get(clientId);
|
|
1882
|
+
const entry = index[clientKey];
|
|
1883
|
+
if (!entry) return void 0;
|
|
1884
|
+
const client = await self.clients.get(entry.clientId);
|
|
1644
1885
|
if (!client) {
|
|
1645
1886
|
delete index[clientKey];
|
|
1646
1887
|
await storeClientsIndex();
|
|
@@ -1649,12 +1890,14 @@
|
|
|
1649
1890
|
}
|
|
1650
1891
|
return {
|
|
1651
1892
|
getClient,
|
|
1893
|
+
getMount,
|
|
1652
1894
|
addClient,
|
|
1653
|
-
removeClient
|
|
1895
|
+
removeClient,
|
|
1896
|
+
restoreMounts
|
|
1654
1897
|
};
|
|
1655
1898
|
}
|
|
1656
1899
|
//#endregion
|
|
1657
1900
|
//#region src/relay-sw.ts
|
|
1658
|
-
startRelayServiceWorker(self);
|
|
1901
|
+
startRelayServiceWorker(self, self.RELAY_OPTIONS ?? {});
|
|
1659
1902
|
//#endregion
|
|
1660
1903
|
})();
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* The relay ServiceWorker runtime, for a host that builds its own worker.
|
|
3
|
+
*
|
|
4
|
+
* TWO WAYS TO SHIP THE RELAY WORKER, and this is the typed one. A host that
|
|
5
|
+
* takes the prebuilt bundle loads `@statewalker/webrun-http-browser/relay-sw`
|
|
6
|
+
* through classic `importScripts` and passes its options in
|
|
7
|
+
* `self.RELAY_OPTIONS`, which cannot be typed from the outside because that
|
|
8
|
+
* bundle ships no declarations. A host that bundles its own worker imports
|
|
9
|
+
* this module instead and calls `startRelayServiceWorker(self, { … })`
|
|
10
|
+
* directly, with `RelayServiceWorkerOptions` to type the options — including
|
|
11
|
+
* `self.RELAY_OPTIONS`, for a host writing the tiny loader script by hand.
|
|
12
|
+
*
|
|
13
|
+
* Kept as its own entry rather than re-exported from the package root: this
|
|
14
|
+
* code only runs inside a ServiceWorker, and the root entry is what pages
|
|
15
|
+
* import.
|
|
16
|
+
*/
|
|
17
|
+
export type { RelayServiceWorkerOptions } from "./relay/index-sw.js";
|
|
18
|
+
export { startRelayServiceWorker } from "./relay/index-sw.js";
|
|
19
|
+
export type { MountSpec } from "./relay/mount-table.js";
|
|
20
|
+
//# sourceMappingURL=relay-worker.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"relay-worker.d.ts","sourceRoot":"","sources":["../src/relay-worker.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;GAeG;AAEH,YAAY,EAAE,yBAAyB,EAAE,MAAM,qBAAqB,CAAC;AACrE,OAAO,EAAE,uBAAuB,EAAE,MAAM,qBAAqB,CAAC;AAC9D,YAAY,EAAE,SAAS,EAAE,MAAM,wBAAwB,CAAC"}
|