@statewalker/cellb 0.3.0 → 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.
Files changed (57) hide show
  1. package/README.md +46 -3
  2. package/bin/cellb.mjs +20 -0
  3. package/dist/browser.d.ts +2 -0
  4. package/dist/browser.js +1 -0
  5. package/dist/browser.js.map +1 -1
  6. package/dist/bucket/celld-cells.js +25 -3
  7. package/dist/bucket/celld-cells.js.map +1 -1
  8. package/dist/cli.d.ts +3 -0
  9. package/dist/cli.js +62 -0
  10. package/dist/cli.js.map +1 -0
  11. package/dist/isolate/browser.d.ts +2 -1
  12. package/dist/isolate/browser.js +2 -1
  13. package/dist/isolate/browser.js.map +1 -1
  14. package/dist/isolate/isolate.js +5 -3
  15. package/dist/isolate/isolate.js.map +1 -1
  16. package/dist/isolate/protocol.d.ts +4 -2
  17. package/dist/isolate/protocol.js +4 -4
  18. package/dist/isolate/protocol.js.map +1 -1
  19. package/dist/node.d.ts +3 -0
  20. package/dist/node.js +2 -0
  21. package/dist/node.js.map +1 -1
  22. package/dist/project/browser-host.d.ts +6 -12
  23. package/dist/project/browser-host.js +2 -25
  24. package/dist/project/browser-host.js.map +1 -1
  25. package/dist/project/browser-load.d.ts +22 -0
  26. package/dist/project/browser-load.js +63 -0
  27. package/dist/project/browser-load.js.map +1 -0
  28. package/dist/project/browser-shards.d.ts +8 -9
  29. package/dist/project/browser-shards.js +88 -52
  30. package/dist/project/browser-shards.js.map +1 -1
  31. package/dist/project/browser-tabs.d.ts +14 -4
  32. package/dist/project/browser-tabs.js +193 -208
  33. package/dist/project/browser-tabs.js.map +1 -1
  34. package/dist/project/build.d.ts +14 -0
  35. package/dist/project/build.js +53 -0
  36. package/dist/project/build.js.map +1 -0
  37. package/dist/project/built-project.d.ts +88 -0
  38. package/dist/project/built-project.js +118 -0
  39. package/dist/project/built-project.js.map +1 -0
  40. package/dist/project/node-load.d.ts +14 -0
  41. package/dist/project/node-load.js +187 -0
  42. package/dist/project/node-load.js.map +1 -0
  43. package/dist/project/wrangler.d.ts +9 -0
  44. package/dist/project/wrangler.js +24 -0
  45. package/dist/project/wrangler.js.map +1 -1
  46. package/dist/transport/cells-http.d.ts +6 -0
  47. package/dist/transport/cells-http.js +22 -1
  48. package/dist/transport/cells-http.js.map +1 -1
  49. package/dist/transport/cells-link.d.ts +2 -0
  50. package/dist/transport/cells-link.js +5 -0
  51. package/dist/transport/cells-link.js.map +1 -1
  52. package/dist/transport/port.js +4 -0
  53. package/dist/transport/port.js.map +1 -1
  54. package/dist/ws/server.d.ts +39 -23
  55. package/dist/ws/server.js +99 -140
  56. package/dist/ws/server.js.map +1 -1
  57. package/package.json +9 -4
@@ -10,22 +10,24 @@
10
10
  * Placement is fixed by the cell id (no rebalancing), and a closed page takes its workers along:
11
11
  * for cells that must outlive a tab, use a fleet bucket (`bucket`), as for one worker.
12
12
  */
13
+ import { serveHttpInWorker, webSocketUpgradeResponse, } from "@statewalker/webrun-http-browser/sw";
14
+ import { withUrl } from "../core/request-body.js";
13
15
  import { shardOf } from "../core/shard.js";
14
- import { shardRouting } from "../router/shard-worker.js";
16
+ import { cellsHttpHandler } from "../transport/cells-http.js";
17
+ import { H, keepUpgrade } from "../transport/cells-link.js";
15
18
  import { connectRuntime, linkOverPort, serveRuntime } from "../transport/port.js";
16
- import { CfWebSocket } from "../ws/server.js";
19
+ import { CellsResponse, CfWebSocket, takeClientPort } from "../ws/server.js";
17
20
  import { browserProjectRuntime, } from "./browser-host.js";
18
21
  const INIT = "cellb:shard";
19
22
  const CLOSE = "cellb:shard-close";
20
- /** A shard of a project shared by the tabs of an origin (`joinProjectShards`): linked to a router. */
23
+ /**
24
+ * A shard of a project shared by the tabs of an origin (`joinProjectShards`): it serves through
25
+ * the ServiceWorker's router (webrun-http-browser's `serveHttpInWorker`), which the tab attaches
26
+ * it to, and reaches the other shards through it.
27
+ */
21
28
  export const TAB_INIT = "cellb:tab-shard";
22
- export const TAB_LINK = "cellb:tab-shard-link";
23
- /** The router the shard was just linked to: the Web Lock its ServiceWorker holds while alive. */
24
- export const TAB_WATCH = "cellb:tab-shard-watch";
25
- /** From a shard worker to its tab: a call waits for a router that is gone; wake the ServiceWorker. */
26
- export const SHARD_WAKE = "cellb:tab-shard-wake";
27
- /** From a shard worker to its tab: calls into other shards are in flight; keep the ServiceWorker up. */
28
- export const SHARD_KEEPALIVE = "cellb:tab-shard-keepalive";
29
+ /** The host calls between shards go to through the router: `http://cellb.internal/<project>/`. */
30
+ export const INTERNAL_ORIGIN = "http://cellb.internal";
29
31
  /**
30
32
  * The worker side: hosts one shard when a page says so — of `startProjectShards` (peers wired
31
33
  * directly) or of `joinProjectShards` (shared by the tabs, linked to the ServiceWorker's router).
@@ -34,15 +36,13 @@ export const SHARD_KEEPALIVE = "cellb:tab-shard-keepalive";
34
36
  export function serveProjectShard(opts) {
35
37
  const scope = globalThis;
36
38
  let host;
37
- let routed;
38
- let releaseOwner;
39
- let linkCount = 0; // TAB_LINKs so far: a router's watch drops only the links made for it
39
+ let service;
40
40
  const sqlite3 = () => typeof opts.sqlite3 === "function"
41
41
  ? opts.sqlite3()
42
42
  : Promise.resolve(opts.sqlite3);
43
43
  scope.addEventListener("message", async (e) => {
44
44
  const type = e.data?.type;
45
- if (![INIT, CLOSE, TAB_INIT, TAB_LINK, TAB_WATCH].includes(type))
45
+ if (![INIT, CLOSE, TAB_INIT].includes(type))
46
46
  return;
47
47
  const reply = e.ports[0];
48
48
  try {
@@ -54,37 +54,16 @@ export function serveProjectShard(opts) {
54
54
  }
55
55
  finally {
56
56
  host = undefined;
57
- routed = undefined;
58
- releaseOwner?.(); // the router sees this shard's owner gone, even if closing failed
59
- releaseOwner = undefined;
57
+ // The router sees this shard's owner gone (its owner lock), even if closing failed.
58
+ await service?.close().catch(() => { });
59
+ service = undefined;
60
60
  }
61
61
  return reply.postMessage({ ok: true });
62
62
  }
63
- if (type === TAB_LINK) {
64
- if (!routed)
65
- throw new Error("shard not started");
66
- linkCount++;
67
- await routed.link(e.ports[1], e.ports[2], e.ports[3]);
68
- return reply.postMessage({ ok: true });
69
- }
70
- if (type === TAB_WATCH) {
71
- // The lock is ours once the router's ServiceWorker is gone (the browser stops an idle one):
72
- // its ports are dead, though nothing says so. A call that waits wakes it through the tab.
73
- const linked = linkCount;
74
- const shard = routed;
75
- void navigator.locks.request(e.data.routerLock, () => {
76
- if (linkCount === linked && routed === shard)
77
- shard?.unlink();
78
- });
79
- return reply.postMessage({ ok: true });
80
- }
81
63
  if (type === TAB_INIT) {
82
64
  const init = e.data;
83
- const routing = shardRouting({
84
- ...init,
85
- onUnlinked: () => scope.postMessage({ type: SHARD_WAKE }),
86
- keepAlive: () => scope.postMessage({ type: SHARD_KEEPALIVE }),
87
- });
65
+ let ready;
66
+ const serviceReady = new Promise((r) => (ready = r));
88
67
  host = await browserProjectRuntime({
89
68
  config: init.config,
90
69
  code: init.code,
@@ -95,19 +74,15 @@ export function serveProjectShard(opts) {
95
74
  pool: init.pool,
96
75
  filesDir: init.filesDir,
97
76
  isolateScript: opts.isolateScript,
98
- route: routing.route,
77
+ route: throughRouter(init, serviceReady),
99
78
  });
100
- routed = routing.attach(host.runtime);
101
- // A lock only this worker holds: the browser releases it the moment the worker or its tab
102
- // dies, which tells the router its links are dead (MessagePorts report nothing).
103
- const ownerLock = `cellb:${init.name}:owner:${init.shard}:${crypto.randomUUID()}`;
104
- await new Promise((held) => {
105
- void navigator.locks.request(ownerLock, () => {
106
- held();
107
- return new Promise((release) => (releaseOwner = release));
108
- });
109
- });
110
- return reply.postMessage({ ok: true, ownerLock, unsupported: host.unsupported });
79
+ // Served through the ServiceWorker's router, which the tab attaches this worker to. The
80
+ // package keeps the link: an owner lock the router watches, re-linking after a restart,
81
+ // keep-alive while calls are in flight.
82
+ const handler = cellsHttpHandler(host.runtime, { passUpgrades: true });
83
+ service = serveHttpInWorker(async (req) => upgradeOverPort(await handler(req)));
84
+ ready(service);
85
+ return reply.postMessage({ ok: true, unsupported: host.unsupported });
111
86
  }
112
87
  const init = e.data;
113
88
  const links = new Map();
@@ -143,6 +118,67 @@ export function serveProjectShard(opts) {
143
118
  }
144
119
  });
145
120
  }
121
+ /**
122
+ * A 101 with a Workers WebSocket, as the ServiceWorker's router hands sockets over: the client
123
+ * end's port in webrun-http-browser's 101.
124
+ */
125
+ function upgradeOverPort(res) {
126
+ const ws = res.webSocket;
127
+ if (res.status !== 101 || !ws)
128
+ return res;
129
+ return webSocketUpgradeResponse(takeClientPort(ws), res.headers.get("Sec-WebSocket-Protocol") ?? "");
130
+ }
131
+ /**
132
+ * The cells of other shards, through the ServiceWorker's router: a call goes to
133
+ * `http://cellb.internal/<project>/` (the router's key for the project) naming its shard, with the
134
+ * url it was made with in `H.url`; a socket opens through the router the same way.
135
+ */
136
+ function throughRouter(init, service) {
137
+ const target = `${INTERNAL_ORIGIN}/${encodeURIComponent(init.name)}/`;
138
+ return (_key, id) => {
139
+ const owner = shardOf(id.toString(), init.shards);
140
+ if (owner === init.shard)
141
+ return undefined;
142
+ // `postVia` (the runtime's) adds the scheme header and rethrows error responses.
143
+ return async (req) => {
144
+ const svc = await service;
145
+ if (req.headers.get("upgrade")?.toLowerCase() === "websocket") {
146
+ const protocols = (req.headers.get("sec-websocket-protocol") ?? "")
147
+ .split(",")
148
+ .map((p) => p.trim())
149
+ .filter(Boolean);
150
+ const headers = [];
151
+ for (const [k, v] of req.headers)
152
+ if (k.startsWith("x-cellb"))
153
+ headers.push([k, v]);
154
+ headers.push([H.shard, String(owner)], [H.url, req.url]);
155
+ const { port, protocol } = await svc.connectWebSocket(target, protocols, headers);
156
+ return new CellsResponse(null, {
157
+ status: 101,
158
+ webSocket: new CfWebSocket(port),
159
+ headers: protocol ? { "Sec-WebSocket-Protocol": protocol } : {},
160
+ });
161
+ }
162
+ const moved = await withUrl(req, target);
163
+ const headers = new Headers(req.headers);
164
+ headers.set(H.shard, String(owner));
165
+ headers.set(H.url, req.url);
166
+ try {
167
+ return await svc.fetch(keepUpgrade(req, new Request(moved, { headers })));
168
+ }
169
+ catch (err) {
170
+ // The words Worker code has always seen for it (CellRouter's), for a call the
171
+ // ServiceWorker lost in flight only: a call that never left (no link, the handler
172
+ // closed) keeps the package's words, as it was certainly not applied.
173
+ if (err instanceof Error &&
174
+ err.name === "LinkLostError" &&
175
+ err.message.startsWith("The ServiceWorker was lost while this call was in flight"))
176
+ throw new Error("The router was lost while this call was in flight; it may or may not have been applied");
177
+ throw err;
178
+ }
179
+ };
180
+ };
181
+ }
146
182
  /** The page side: starts `shards` workers that each run `serveProjectShard`, wired together. */
147
183
  export async function startProjectShards(opts) {
148
184
  const workers = Array.from({ length: opts.shards }, (_, i) => opts.spawn(i));
@@ -1 +1 @@
1
- {"version":3,"file":"browser-shards.js","sourceRoot":"","sources":["../../src/project/browser-shards.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAoB,YAAY,EAAE,MAAM,2BAA2B,CAAC;AAE3E,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,WAAW,EAAE,MAAM,iBAAiB,CAAC;AAC9C,OAAO,EAGL,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,IAAI,GAAG,aAAa,CAAC;AAC3B,MAAM,KAAK,GAAG,mBAAmB,CAAC;AAClC,sGAAsG;AACtG,MAAM,CAAC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAC1C,MAAM,CAAC,MAAM,QAAQ,GAAG,sBAAsB,CAAC;AAC/C,iGAAiG;AACjG,MAAM,CAAC,MAAM,SAAS,GAAG,uBAAuB,CAAC;AACjD,sGAAsG;AACtG,MAAM,CAAC,MAAM,UAAU,GAAG,sBAAsB,CAAC;AACjD,wGAAwG;AACxG,MAAM,CAAC,MAAM,eAAe,GAAG,2BAA2B,CAAC;AAmC3D;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAKjC;IACC,MAAM,KAAK,GAAG,UAGb,CAAC;IACF,IAAI,IAAmE,CAAC;IACxE,IAAI,MAA+B,CAAC;IACpC,IAAI,YAAsC,CAAC;IAC3C,IAAI,SAAS,GAAG,CAAC,CAAC,CAAC,sEAAsE;IACzF,MAAM,OAAO,GAAG,GAAG,EAAE,CACnB,OAAO,IAAI,CAAC,OAAO,KAAK,UAAU;QAChC,CAAC,CAAE,IAAI,CAAC,OAAkC,EAAE;QAC5C,CAAC,CAAC,OAAO,CAAC,OAAO,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;IACpC,KAAK,CAAC,gBAAgB,CAAC,SAAS,EAAE,KAAK,EAAE,CAAe,EAAE,EAAE;QAC1D,MAAM,IAAI,GAAI,CAAC,CAAC,IAAsC,EAAE,IAAI,CAAC;QAC7D,IAAI,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,QAAQ,EAAE,QAAQ,EAAE,SAAS,CAAC,CAAC,QAAQ,CAAC,IAAc,CAAC;YAAE,OAAO;QACnF,MAAM,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,CAAgB,CAAC;QACxC,IAAI,CAAC;YACH,IAAI,IAAI,KAAK,KAAK,EAAE,CAAC;gBACnB,IAAI,CAAC;oBACH,wEAAwE;oBACxE,WAAW,CAAC,MAAM,EAAE,CAAC;oBACrB,MAAM,IAAI,EAAE,KAAK,EAAE,CAAC;gBACtB,CAAC;wBAAS,CAAC;oBACT,IAAI,GAAG,SAAS,CAAC;oBACjB,MAAM,GAAG,SAAS,CAAC;oBACnB,YAAY,EAAE,EAAE,CAAC,CAAC,kEAAkE;oBACpF,YAAY,GAAG,SAAS,CAAC;gBAC3B,CAAC;gBACD,OAAO,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,IAAI,CAAC,MAAM;oBAAE,MAAM,IAAI,KAAK,CAAC,mBAAmB,CAAC,CAAC;gBAClD,SAAS,EAAE,CAAC;gBACZ,MAAM,MAAM,CAAC,IAAI,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBACtD,OAAO,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,IAAI,KAAK,SAAS,EAAE,CAAC;gBACvB,4FAA4F;gBAC5F,0FAA0F;gBAC1F,MAAM,MAAM,GAAG,SAAS,CAAC;gBACzB,MAAM,KAAK,GAAG,MAAM,CAAC;gBACrB,KAAK,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,UAAU,EAAE,GAAG,EAAE;oBACnD,IAAI,SAAS,KAAK,MAAM,IAAI,MAAM,KAAK,KAAK;wBAAE,KAAK,EAAE,MAAM,EAAE,CAAC;gBAChE,CAAC,CAAC,CAAC;gBACH,OAAO,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,CAAC,CAAC;YACzC,CAAC;YACD,IAAI,IAAI,KAAK,QAAQ,EAAE,CAAC;gBACtB,MAAM,IAAI,GAAG,CAAC,CAAC,IAAoB,CAAC;gBACpC,MAAM,OAAO,GAAG,YAAY,CAAC;oBAC3B,GAAG,IAAI;oBACP,UAAU,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,UAAU,EAAE,CAAC;oBACzD,SAAS,EAAE,GAAG,EAAE,CAAC,KAAK,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,eAAe,EAAE,CAAC;iBAC9D,CAAC,CAAC;gBACH,IAAI,GAAG,MAAM,qBAAqB,CAAC;oBACjC,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,UAAU,EAAE,IAAI,CAAC,UAAU;oBAC3B,EAAE,EAAE,IAAI,CAAC,EAAE;oBACX,MAAM,EAAE,IAAI,CAAC,MAAM;oBACnB,OAAO,EAAE,MAAM,OAAO,EAAE;oBACxB,IAAI,EAAE,IAAI,CAAC,IAAI;oBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;oBACvB,aAAa,EAAE,IAAI,CAAC,aAAa;oBACjC,KAAK,EAAE,OAAO,CAAC,KAAK;iBACrB,CAAC,CAAC;gBACH,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC;gBACtC,0FAA0F;gBAC1F,iFAAiF;gBACjF,MAAM,SAAS,GAAG,SAAS,IAAI,CAAC,IAAI,UAAU,IAAI,CAAC,KAAK,IAAI,MAAM,CAAC,UAAU,EAAE,EAAE,CAAC;gBAClF,MAAM,IAAI,OAAO,CAAO,CAAC,IAAI,EAAE,EAAE;oBAC/B,KAAK,SAAS,CAAC,KAAK,CAAC,OAAO,CAAC,SAAS,EAAE,GAAG,EAAE;wBAC3C,IAAI,EAAE,CAAC;wBACP,OAAO,IAAI,OAAO,CAAO,CAAC,OAAO,EAAE,EAAE,CAAC,CAAC,YAAY,GAAG,OAAO,CAAC,CAAC,CAAC;oBAClE,CAAC,CAAC,CAAC;gBACL,CAAC,CAAC,CAAC;gBACH,OAAO,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,SAAS,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACnF,CAAC;YACD,MAAM,IAAI,GAAG,CAAC,CAAC,IAAiB,CAAC;YACjC,MAAM,KAAK,GAAG,IAAI,GAAG,EAA+C,CAAC;YACrE,MAAM,MAAM,GAAkB,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAgB,CAAC,CAAC,CAAC,wBAAwB;YACnF,KAAK,MAAM,CAAC,CAAC,EAAE,IAAI,CAAC,IAAI,IAAI,CAAC,KAAK,CAAC,OAAO,EAAE,EAAE,CAAC;gBAC7C,MAAM,IAAI,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC,GAAG,CAAC,CAAgB,CAAC;gBAC3C,IAAI,IAAI,CAAC,IAAI,KAAK,MAAM;oBAAE,KAAK,CAAC,GAAG,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,MAAM,YAAY,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;;oBAC5E,MAAM,CAAC,IAAI,CAAC,IAAI,CAAC,CAAC;YACzB,CAAC;YACD,IAAI,GAAG,MAAM,qBAAqB,CAAC;gBACjC,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,UAAU,EAAE,IAAI,CAAC,UAAU;gBAC3B,EAAE,EAAE,IAAI,CAAC,EAAE;gBACX,MAAM,EAAE,IAAI,CAAC,MAAM;gBACnB,OAAO,EAAE,MAAM,OAAO,EAAE;gBACxB,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,QAAQ,EAAE,IAAI,CAAC,QAAQ;gBACvB,aAAa,EAAE,IAAI,CAAC,aAAa;gBACjC,KAAK,EAAE,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;oBAClB,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;oBAClD,OAAO,KAAK,KAAK,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,SAAS,CAAC,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC;gBAC7D,CAAC;aACF,CAAC,CAAC;YACH,KAAK,MAAM,IAAI,IAAI,MAAM;gBAAE,MAAM,YAAY,CAAC,IAAI,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC;YAClE,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;QACjE,CAAC;QAAC,OAAO,GAAG,EAAE,CAAC;YACb,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,KAAK,EAAE,KAAK,EAAE,MAAM,CAAE,GAAa,EAAE,KAAK,IAAI,GAAG,CAAC,EAAE,CAAC,CAAC;QAChF,CAAC;IACH,CAAC,CAAC,CAAC;AACL,CAAC;AAaD,gGAAgG;AAChG,MAAM,CAAC,KAAK,UAAU,kBAAkB,CAAC,IAaxC;IACC,MAAM,OAAO,GAAG,KAAK,CAAC,IAAI,CAAC,EAAE,MAAM,EAAE,IAAI,CAAC,MAAM,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;IAC7E,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,IAAa,EAAE,KAAK,GAAkB,EAAE,EAAE,EAAE,CAClE,IAAI,OAAO,CAA0D,CAAC,OAAO,EAAE,EAAE;QAC/E,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,cAAc,EAAE,CAAC;QAC9C,KAAK,CAAC,SAAS,GAAG,CAAC,CAAC,EAAE,EAAE;YACtB,KAAK,CAAC,KAAK,EAAE,CAAC;YACd,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QAClB,CAAC,CAAC;QACF,CAAC,CAAC,WAAW,CAAC,IAAI,EAAE,CAAC,KAAK,EAAE,GAAG,KAAK,CAAC,CAAC,CAAC;IACzC,CAAC,CAAC,CAAC;IACL,qFAAqF;IACrF,MAAM,MAAM,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,CAAC;QAChC,KAAK,EAAE,EAAwB;QAC/B,KAAK,EAAE,EAAmB;KAC3B,CAAC,CAAC,CAAC;IACJ,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE;QAClC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,IAAI,CAAC,MAAM,EAAE,CAAC,EAAE,EAAE,CAAC;YACrC,IAAI,CAAC,KAAK,CAAC;gBAAE,SAAS;YACtB,MAAM,EAAE,KAAK,EAAE,KAAK,EAAE,GAAG,IAAI,cAAc,EAAE,CAAC;YAC9C,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,MAAM,EAAE,CAAC,CAAC;YACjD,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;YAC5B,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,EAAE,KAAK,EAAE,CAAC,EAAE,IAAI,EAAE,OAAO,EAAE,CAAC,CAAC;YAClD,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAC9B,CAAC;IACH,MAAM,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,CAAC,IAAI,cAAc,EAAE,CAAC,CAAC;IACxD,IAAI,CAAC;QACH,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAC/B,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CACnB,GAAG,CACD,CAAC,EACD;YACE,IAAI,EAAE,IAAI;YACV,KAAK,EAAE,CAAC;YACR,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,MAAM,EAAE,IAAI,CAAC,MAAM;YACnB,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,IAAI,CAAC,UAAU;YAC3B,EAAE,EAAE,IAAI,CAAC,EAAE;YACX,MAAM,EAAE,IAAI,CAAC,MAAM,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,IAAI,CAAC,MAAM,CAAC,IAAI,KAAK,CAAC,EAAE,EAAE;YAC5E,IAAI,EAAE,GAAG,IAAI,CAAC,IAAI,IAAI,CAAC,EAAE;YACzB,QAAQ,EAAE,GAAG,IAAI,CAAC,IAAI,UAAU,CAAC,EAAE;YACnC,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK;SACH,EACrB,CAAC,OAAO,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,GAAG,MAAM,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CACvC,CACF,CACF,CAAC;QACF,MAAM,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1C,IAAI,MAAM;YAAE,MAAM,IAAI,KAAK,CAAC,kCAAkC,MAAM,CAAC,KAAK,EAAE,CAAC,CAAC;QAC9E,MAAM,OAAO,GAAG,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,cAAc,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;QAC/E,IAAI,IAAI,GAAG,CAAC,CAAC;QACb,OAAO;YACL,KAAK,EAAE,CAAC,GAAG,EAAE,EAAE,CAAC,OAAO,CAAC,IAAI,EAAE,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC,KAAK,CAAC,GAAG,CAAC;YAC3D,MAAM,EAAE,OAAO;YACf,WAAW,EAAE,OAAO,CAAC,CAAC,CAAC,CAAC,WAAW,IAAI,EAAE;YACzC,KAAK,CAAC,KAAK;gBACT,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;gBACjD,MAAM,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,GAAG,CAAC,CAAC,EAAE,EAAE,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,CAAC,CAAC;gBAC/D,KAAK,MAAM,CAAC,IAAI,OAAO;oBAAE,CAAC,CAAC,SAAS,EAAE,CAAC;YACzC,CAAC;SACF,CAAC;IACJ,CAAC;IAAC,OAAO,GAAG,EAAE,CAAC;QACb,KAAK,MAAM,CAAC,IAAI,OAAO;YAAE,CAAC,CAAC,SAAS,EAAE,CAAC;QACvC,MAAM,GAAG,CAAC;IACZ,CAAC;AACH,CAAC","sourcesContent":["/**\n * One project in several browser workers, so that its cells run in parallel threads. Each worker\n * hosts the project with `browserProjectRuntime` and its own OPFS pool, owns the cells that\n * `shardOf(id, shards)` gives it, and reaches every other cell through the worker that owns it\n * over a MessagePort. Any worker takes ingress; the page spreads requests over them.\n *\n * page: startProjectShards({ shards, spawn, config, code, name }) → { fetch, close }\n * worker: serveProjectShard({ sqlite3 })\n *\n * Placement is fixed by the cell id (no rebalancing), and a closed page takes its workers along:\n * for cells that must outlive a tab, use a fleet bucket (`bucket`), as for one worker.\n */\nimport { shardOf } from \"../core/shard.js\";\nimport { type ShardWorker, shardRouting } from \"../router/shard-worker.js\";\nimport type { RemoteRuntime } from \"../transport/cells-http.js\";\nimport { connectRuntime, linkOverPort, serveRuntime } from \"../transport/port.js\";\nimport { CfWebSocket } from \"../ws/server.js\";\nimport {\n type BrowserBucket,\n type BrowserProjectOptions,\n browserProjectRuntime,\n} from \"./browser-host.js\";\n\nconst INIT = \"cellb:shard\";\nconst CLOSE = \"cellb:shard-close\";\n/** A shard of a project shared by the tabs of an origin (`joinProjectShards`): linked to a router. */\nexport const TAB_INIT = \"cellb:tab-shard\";\nexport const TAB_LINK = \"cellb:tab-shard-link\";\n/** The router the shard was just linked to: the Web Lock its ServiceWorker holds while alive. */\nexport const TAB_WATCH = \"cellb:tab-shard-watch\";\n/** From a shard worker to its tab: a call waits for a router that is gone; wake the ServiceWorker. */\nexport const SHARD_WAKE = \"cellb:tab-shard-wake\";\n/** From a shard worker to its tab: calls into other shards are in flight; keep the ServiceWorker up. */\nexport const SHARD_KEEPALIVE = \"cellb:tab-shard-keepalive\";\n\n/** What a shard worker shared by tabs is told: no peers, it reaches them through the router. */\nexport interface TabShardInit {\n type: typeof TAB_INIT;\n name: string;\n shard: number;\n shards: number;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n bucket?: BrowserBucket;\n pool: string;\n filesDir: string;\n}\n\n/** What a shard worker is told (the ports travel next to it). */\ninterface ShardInit {\n type: typeof INIT;\n shard: number;\n shards: number;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n bucket?: BrowserBucket;\n pool: string;\n filesDir: string;\n /** For ports[2..]: the shard each port links to (`link`) or is served to (`serve`). */\n peers: { shard: number; role: \"link\" | \"serve\" }[];\n}\n\n/**\n * The worker side: hosts one shard when a page says so — of `startProjectShards` (peers wired\n * directly) or of `joinProjectShards` (shared by the tabs, linked to the ServiceWorker's router).\n * Messages of other types are left to the worker's own listeners.\n */\nexport function serveProjectShard(opts: {\n /** An initialized SQLite WASM module, or a function that makes one. */\n sqlite3: unknown | (() => Promise<unknown>);\n /** `browserProjectRuntime`'s `isolateScript`. */\n isolateScript?: URL;\n}): void {\n const scope = globalThis as unknown as {\n addEventListener(type: \"message\", fn: (e: MessageEvent) => void): void;\n postMessage(message: unknown): void;\n };\n let host: Awaited<ReturnType<typeof browserProjectRuntime>> | undefined;\n let routed: ShardWorker | undefined;\n let releaseOwner: (() => void) | undefined;\n let linkCount = 0; // TAB_LINKs so far: a router's watch drops only the links made for it\n const sqlite3 = () =>\n typeof opts.sqlite3 === \"function\"\n ? (opts.sqlite3 as () => Promise<unknown>)()\n : Promise.resolve(opts.sqlite3);\n scope.addEventListener(\"message\", async (e: MessageEvent) => {\n const type = (e.data as { type?: string } | undefined)?.type;\n if (![INIT, CLOSE, TAB_INIT, TAB_LINK, TAB_WATCH].includes(type as string)) return;\n const reply = e.ports[0] as MessagePort;\n try {\n if (type === CLOSE) {\n try {\n // The cells move elsewhere: their sockets' clients reconnect there now.\n CfWebSocket.goAway();\n await host?.close();\n } finally {\n host = undefined;\n routed = undefined;\n releaseOwner?.(); // the router sees this shard's owner gone, even if closing failed\n releaseOwner = undefined;\n }\n return reply.postMessage({ ok: true });\n }\n if (type === TAB_LINK) {\n if (!routed) throw new Error(\"shard not started\");\n linkCount++;\n await routed.link(e.ports[1], e.ports[2], e.ports[3]);\n return reply.postMessage({ ok: true });\n }\n if (type === TAB_WATCH) {\n // The lock is ours once the router's ServiceWorker is gone (the browser stops an idle one):\n // its ports are dead, though nothing says so. A call that waits wakes it through the tab.\n const linked = linkCount;\n const shard = routed;\n void navigator.locks.request(e.data.routerLock, () => {\n if (linkCount === linked && routed === shard) shard?.unlink();\n });\n return reply.postMessage({ ok: true });\n }\n if (type === TAB_INIT) {\n const init = e.data as TabShardInit;\n const routing = shardRouting({\n ...init,\n onUnlinked: () => scope.postMessage({ type: SHARD_WAKE }),\n keepAlive: () => scope.postMessage({ type: SHARD_KEEPALIVE }),\n });\n host = await browserProjectRuntime({\n config: init.config,\n code: init.code,\n assetFiles: init.assetFiles,\n ai: init.ai,\n bucket: init.bucket,\n sqlite3: await sqlite3(),\n pool: init.pool,\n filesDir: init.filesDir,\n isolateScript: opts.isolateScript,\n route: routing.route,\n });\n routed = routing.attach(host.runtime);\n // A lock only this worker holds: the browser releases it the moment the worker or its tab\n // dies, which tells the router its links are dead (MessagePorts report nothing).\n const ownerLock = `cellb:${init.name}:owner:${init.shard}:${crypto.randomUUID()}`;\n await new Promise<void>((held) => {\n void navigator.locks.request(ownerLock, () => {\n held();\n return new Promise<void>((release) => (releaseOwner = release));\n });\n });\n return reply.postMessage({ ok: true, ownerLock, unsupported: host.unsupported });\n }\n const init = e.data as ShardInit;\n const links = new Map<number, (req: Request) => Promise<Response>>();\n const served: MessagePort[] = [e.ports[1] as MessagePort]; // ingress from the page\n for (const [i, peer] of init.peers.entries()) {\n const port = e.ports[i + 2] as MessagePort;\n if (peer.role === \"link\") links.set(peer.shard, (await linkOverPort(port)).send);\n else served.push(port);\n }\n host = await browserProjectRuntime({\n config: init.config,\n code: init.code,\n assetFiles: init.assetFiles,\n ai: init.ai,\n bucket: init.bucket,\n sqlite3: await sqlite3(),\n pool: init.pool,\n filesDir: init.filesDir,\n isolateScript: opts.isolateScript,\n route: (_key, id) => {\n const owner = shardOf(id.toString(), init.shards);\n return owner === init.shard ? undefined : links.get(owner);\n },\n });\n for (const port of served) await serveRuntime(port, host.runtime);\n reply.postMessage({ ok: true, unsupported: host.unsupported });\n } catch (err) {\n reply.postMessage({ ok: false, error: String((err as Error)?.stack ?? err) });\n }\n });\n}\n\nexport interface ProjectShards {\n /** Ingress: a request to the project's Worker, in the next shard (round robin). */\n fetch(req: Request): Promise<Response>;\n /** The shards' runtimes, by shard number. */\n readonly shards: RemoteRuntime[];\n /** Configuration entries the runtime does not support (as `browserProjectRuntime` reports). */\n readonly unsupported: string[];\n /** Closes every shard's runtime (releasing bucket leases), then its worker. */\n close(): Promise<void>;\n}\n\n/** The page side: starts `shards` workers that each run `serveProjectShard`, wired together. */\nexport async function startProjectShards(opts: {\n shards: number;\n /** Starts one worker whose script calls `serveProjectShard`. */\n spawn: (shard: number) => Worker;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n /** Each shard is its own fleet node, named `<node>-s<shard>`. */\n bucket?: BrowserBucket;\n /** Prefix of the shards' OPFS pools and files directories (`<name>-<shard>`). */\n name: string;\n}): Promise<ProjectShards> {\n const workers = Array.from({ length: opts.shards }, (_, i) => opts.spawn(i));\n const ask = (w: Worker, data: unknown, ports: MessagePort[] = []) =>\n new Promise<{ ok: boolean; error?: string; unsupported?: string[] }>((resolve) => {\n const { port1, port2 } = new MessageChannel();\n port1.onmessage = (e) => {\n port1.close();\n resolve(e.data);\n };\n w.postMessage(data, [port2, ...ports]);\n });\n // A channel between every two shards: one end links (sends to), the other is served.\n const wiring = workers.map(() => ({\n peers: [] as ShardInit[\"peers\"],\n ports: [] as MessagePort[],\n }));\n for (let i = 0; i < opts.shards; i++)\n for (let j = 0; j < opts.shards; j++) {\n if (i === j) continue;\n const { port1, port2 } = new MessageChannel();\n wiring[i].peers.push({ shard: j, role: \"link\" });\n wiring[i].ports.push(port1);\n wiring[j].peers.push({ shard: i, role: \"serve\" });\n wiring[j].ports.push(port2);\n }\n const ingress = workers.map(() => new MessageChannel());\n try {\n const started = await Promise.all(\n workers.map((w, i) =>\n ask(\n w,\n {\n type: INIT,\n shard: i,\n shards: opts.shards,\n config: opts.config,\n code: opts.code,\n assetFiles: opts.assetFiles,\n ai: opts.ai,\n bucket: opts.bucket && { ...opts.bucket, node: `${opts.bucket.node}-s${i}` },\n pool: `${opts.name}-${i}`,\n filesDir: `${opts.name}-files-${i}`,\n peers: wiring[i].peers,\n } satisfies ShardInit,\n [ingress[i].port2, ...wiring[i].ports],\n ),\n ),\n );\n const failed = started.find((r) => !r.ok);\n if (failed) throw new Error(`a project shard did not start: ${failed.error}`);\n const remotes = await Promise.all(ingress.map((c) => connectRuntime(c.port1)));\n let next = 0;\n return {\n fetch: (req) => remotes[next++ % remotes.length].fetch(req),\n shards: remotes,\n unsupported: started[0].unsupported ?? [],\n async close() {\n await Promise.all(remotes.map((r) => r.close()));\n await Promise.all(workers.map((w) => ask(w, { type: CLOSE })));\n for (const w of workers) w.terminate();\n },\n };\n } catch (err) {\n for (const w of workers) w.terminate();\n throw err;\n }\n}\n"]}
1
+ {"version":3,"file":"browser-shards.js","sourceRoot":"","sources":["../../src/project/browser-shards.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * One project in several browser workers, so that its cells run in parallel threads. Each worker\n * hosts the project with `browserProjectRuntime` and its own OPFS pool, owns the cells that\n * `shardOf(id, shards)` gives it, and reaches every other cell through the worker that owns it\n * over a MessagePort. Any worker takes ingress; the page spreads requests over them.\n *\n * page: startProjectShards({ shards, spawn, config, code, name }) → { fetch, close }\n * worker: serveProjectShard({ sqlite3 })\n *\n * Placement is fixed by the cell id (no rebalancing), and a closed page takes its workers along:\n * for cells that must outlive a tab, use a fleet bucket (`bucket`), as for one worker.\n */\nimport {\n serveHttpInWorker,\n type WorkerHttpService,\n webSocketUpgradeResponse,\n} from \"@statewalker/webrun-http-browser/sw\";\nimport { withUrl } from \"../core/request-body.js\";\nimport type { CellRuntimeOptions } from \"../core/runtime.js\";\nimport { shardOf } from \"../core/shard.js\";\nimport { cellsHttpHandler, type RemoteRuntime } from \"../transport/cells-http.js\";\nimport { H, keepUpgrade } from \"../transport/cells-link.js\";\nimport { connectRuntime, linkOverPort, serveRuntime } from \"../transport/port.js\";\nimport { CellsResponse, CfWebSocket, takeClientPort } from \"../ws/server.js\";\nimport {\n type BrowserBucket,\n type BrowserProjectOptions,\n browserProjectRuntime,\n} from \"./browser-host.js\";\n\nconst INIT = \"cellb:shard\";\nconst CLOSE = \"cellb:shard-close\";\n/**\n * A shard of a project shared by the tabs of an origin (`joinProjectShards`): it serves through\n * the ServiceWorker's router (webrun-http-browser's `serveHttpInWorker`), which the tab attaches\n * it to, and reaches the other shards through it.\n */\nexport const TAB_INIT = \"cellb:tab-shard\";\n/** The host calls between shards go to through the router: `http://cellb.internal/<project>/`. */\nexport const INTERNAL_ORIGIN = \"http://cellb.internal\";\n\n/** What a shard worker shared by tabs is told: no peers, it reaches them through the router. */\nexport interface TabShardInit {\n type: typeof TAB_INIT;\n name: string;\n shard: number;\n shards: number;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n bucket?: BrowserBucket;\n pool: string;\n filesDir: string;\n}\n\n/** What a shard worker is told (the ports travel next to it). */\ninterface ShardInit {\n type: typeof INIT;\n shard: number;\n shards: number;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n bucket?: BrowserBucket;\n pool: string;\n filesDir: string;\n /** For ports[2..]: the shard each port links to (`link`) or is served to (`serve`). */\n peers: { shard: number; role: \"link\" | \"serve\" }[];\n}\n\n/**\n * The worker side: hosts one shard when a page says so — of `startProjectShards` (peers wired\n * directly) or of `joinProjectShards` (shared by the tabs, linked to the ServiceWorker's router).\n * Messages of other types are left to the worker's own listeners.\n */\nexport function serveProjectShard(opts: {\n /** An initialized SQLite WASM module, or a function that makes one. */\n sqlite3: unknown | (() => Promise<unknown>);\n /** `browserProjectRuntime`'s `isolateScript`. */\n isolateScript?: URL;\n}): void {\n const scope = globalThis as unknown as {\n addEventListener(type: \"message\", fn: (e: MessageEvent) => void): void;\n postMessage(message: unknown): void;\n };\n let host: Awaited<ReturnType<typeof browserProjectRuntime>> | undefined;\n let service: WorkerHttpService | undefined;\n const sqlite3 = () =>\n typeof opts.sqlite3 === \"function\"\n ? (opts.sqlite3 as () => Promise<unknown>)()\n : Promise.resolve(opts.sqlite3);\n scope.addEventListener(\"message\", async (e: MessageEvent) => {\n const type = (e.data as { type?: string } | undefined)?.type;\n if (![INIT, CLOSE, TAB_INIT].includes(type as string)) return;\n const reply = e.ports[0] as MessagePort;\n try {\n if (type === CLOSE) {\n try {\n // The cells move elsewhere: their sockets' clients reconnect there now.\n CfWebSocket.goAway();\n await host?.close();\n } finally {\n host = undefined;\n // The router sees this shard's owner gone (its owner lock), even if closing failed.\n await service?.close().catch(() => {});\n service = undefined;\n }\n return reply.postMessage({ ok: true });\n }\n if (type === TAB_INIT) {\n const init = e.data as TabShardInit;\n let ready!: (s: WorkerHttpService) => void;\n const serviceReady = new Promise<WorkerHttpService>((r) => (ready = r));\n host = await browserProjectRuntime({\n config: init.config,\n code: init.code,\n assetFiles: init.assetFiles,\n ai: init.ai,\n bucket: init.bucket,\n sqlite3: await sqlite3(),\n pool: init.pool,\n filesDir: init.filesDir,\n isolateScript: opts.isolateScript,\n route: throughRouter(init, serviceReady),\n });\n // Served through the ServiceWorker's router, which the tab attaches this worker to. The\n // package keeps the link: an owner lock the router watches, re-linking after a restart,\n // keep-alive while calls are in flight.\n const handler = cellsHttpHandler(host.runtime, { passUpgrades: true });\n service = serveHttpInWorker(async (req) => upgradeOverPort(await handler(req)));\n ready(service);\n return reply.postMessage({ ok: true, unsupported: host.unsupported });\n }\n const init = e.data as ShardInit;\n const links = new Map<number, (req: Request) => Promise<Response>>();\n const served: MessagePort[] = [e.ports[1] as MessagePort]; // ingress from the page\n for (const [i, peer] of init.peers.entries()) {\n const port = e.ports[i + 2] as MessagePort;\n if (peer.role === \"link\") links.set(peer.shard, (await linkOverPort(port)).send);\n else served.push(port);\n }\n host = await browserProjectRuntime({\n config: init.config,\n code: init.code,\n assetFiles: init.assetFiles,\n ai: init.ai,\n bucket: init.bucket,\n sqlite3: await sqlite3(),\n pool: init.pool,\n filesDir: init.filesDir,\n isolateScript: opts.isolateScript,\n route: (_key, id) => {\n const owner = shardOf(id.toString(), init.shards);\n return owner === init.shard ? undefined : links.get(owner);\n },\n });\n for (const port of served) await serveRuntime(port, host.runtime);\n reply.postMessage({ ok: true, unsupported: host.unsupported });\n } catch (err) {\n reply.postMessage({ ok: false, error: String((err as Error)?.stack ?? err) });\n }\n });\n}\n\n/**\n * A 101 with a Workers WebSocket, as the ServiceWorker's router hands sockets over: the client\n * end's port in webrun-http-browser's 101.\n */\nfunction upgradeOverPort(res: Response): Response {\n const ws = (res as Response & { webSocket?: CfWebSocket | null }).webSocket;\n if (res.status !== 101 || !ws) return res;\n return webSocketUpgradeResponse(\n takeClientPort(ws),\n res.headers.get(\"Sec-WebSocket-Protocol\") ?? \"\",\n );\n}\n\n/**\n * The cells of other shards, through the ServiceWorker's router: a call goes to\n * `http://cellb.internal/<project>/` (the router's key for the project) naming its shard, with the\n * url it was made with in `H.url`; a socket opens through the router the same way.\n */\nfunction throughRouter(\n init: { name: string; shard: number; shards: number },\n service: Promise<WorkerHttpService>,\n): NonNullable<CellRuntimeOptions[\"route\"]> {\n const target = `${INTERNAL_ORIGIN}/${encodeURIComponent(init.name)}/`;\n return (_key, id) => {\n const owner = shardOf(id.toString(), init.shards);\n if (owner === init.shard) return undefined;\n // `postVia` (the runtime's) adds the scheme header and rethrows error responses.\n return async (req) => {\n const svc = await service;\n if (req.headers.get(\"upgrade\")?.toLowerCase() === \"websocket\") {\n const protocols = (req.headers.get(\"sec-websocket-protocol\") ?? \"\")\n .split(\",\")\n .map((p) => p.trim())\n .filter(Boolean);\n const headers: [string, string][] = [];\n for (const [k, v] of req.headers) if (k.startsWith(\"x-cellb\")) headers.push([k, v]);\n headers.push([H.shard, String(owner)], [H.url, req.url]);\n const { port, protocol } = await svc.connectWebSocket(target, protocols, headers);\n return new CellsResponse(null, {\n status: 101,\n webSocket: new CfWebSocket(port),\n headers: protocol ? { \"Sec-WebSocket-Protocol\": protocol } : {},\n });\n }\n const moved = await withUrl(req, target);\n const headers = new Headers(req.headers);\n headers.set(H.shard, String(owner));\n headers.set(H.url, req.url);\n try {\n return await svc.fetch(keepUpgrade(req, new Request(moved, { headers })));\n } catch (err) {\n // The words Worker code has always seen for it (CellRouter's), for a call the\n // ServiceWorker lost in flight only: a call that never left (no link, the handler\n // closed) keeps the package's words, as it was certainly not applied.\n if (\n err instanceof Error &&\n err.name === \"LinkLostError\" &&\n err.message.startsWith(\"The ServiceWorker was lost while this call was in flight\")\n )\n throw new Error(\n \"The router was lost while this call was in flight; it may or may not have been applied\",\n );\n throw err;\n }\n };\n };\n}\n\nexport interface ProjectShards {\n /** Ingress: a request to the project's Worker, in the next shard (round robin). */\n fetch(req: Request): Promise<Response>;\n /** The shards' runtimes, by shard number. */\n readonly shards: RemoteRuntime[];\n /** Configuration entries the runtime does not support (as `browserProjectRuntime` reports). */\n readonly unsupported: string[];\n /** Closes every shard's runtime (releasing bucket leases), then its worker. */\n close(): Promise<void>;\n}\n\n/** The page side: starts `shards` workers that each run `serveProjectShard`, wired together. */\nexport async function startProjectShards(opts: {\n shards: number;\n /** Starts one worker whose script calls `serveProjectShard`. */\n spawn: (shard: number) => Worker;\n config: BrowserProjectOptions[\"config\"];\n code: BrowserProjectOptions[\"code\"];\n assetFiles?: BrowserProjectOptions[\"assetFiles\"];\n /** The OpenAI-compatible endpoint behind the project's `ai` binding. */\n ai?: BrowserProjectOptions[\"ai\"];\n /** Each shard is its own fleet node, named `<node>-s<shard>`. */\n bucket?: BrowserBucket;\n /** Prefix of the shards' OPFS pools and files directories (`<name>-<shard>`). */\n name: string;\n}): Promise<ProjectShards> {\n const workers = Array.from({ length: opts.shards }, (_, i) => opts.spawn(i));\n const ask = (w: Worker, data: unknown, ports: MessagePort[] = []) =>\n new Promise<{ ok: boolean; error?: string; unsupported?: string[] }>((resolve) => {\n const { port1, port2 } = new MessageChannel();\n port1.onmessage = (e) => {\n port1.close();\n resolve(e.data);\n };\n w.postMessage(data, [port2, ...ports]);\n });\n // A channel between every two shards: one end links (sends to), the other is served.\n const wiring = workers.map(() => ({\n peers: [] as ShardInit[\"peers\"],\n ports: [] as MessagePort[],\n }));\n for (let i = 0; i < opts.shards; i++)\n for (let j = 0; j < opts.shards; j++) {\n if (i === j) continue;\n const { port1, port2 } = new MessageChannel();\n wiring[i].peers.push({ shard: j, role: \"link\" });\n wiring[i].ports.push(port1);\n wiring[j].peers.push({ shard: i, role: \"serve\" });\n wiring[j].ports.push(port2);\n }\n const ingress = workers.map(() => new MessageChannel());\n try {\n const started = await Promise.all(\n workers.map((w, i) =>\n ask(\n w,\n {\n type: INIT,\n shard: i,\n shards: opts.shards,\n config: opts.config,\n code: opts.code,\n assetFiles: opts.assetFiles,\n ai: opts.ai,\n bucket: opts.bucket && { ...opts.bucket, node: `${opts.bucket.node}-s${i}` },\n pool: `${opts.name}-${i}`,\n filesDir: `${opts.name}-files-${i}`,\n peers: wiring[i].peers,\n } satisfies ShardInit,\n [ingress[i].port2, ...wiring[i].ports],\n ),\n ),\n );\n const failed = started.find((r) => !r.ok);\n if (failed) throw new Error(`a project shard did not start: ${failed.error}`);\n const remotes = await Promise.all(ingress.map((c) => connectRuntime(c.port1)));\n let next = 0;\n return {\n fetch: (req) => remotes[next++ % remotes.length].fetch(req),\n shards: remotes,\n unsupported: started[0].unsupported ?? [],\n async close() {\n await Promise.all(remotes.map((r) => r.close()));\n await Promise.all(workers.map((w) => ask(w, { type: CLOSE })));\n for (const w of workers) w.terminate();\n },\n };\n } catch (err) {\n for (const w of workers) w.terminate();\n throw err;\n }\n}\n"]}
@@ -16,7 +16,7 @@
16
16
  * finish, new ones wait for the next owner); a tab that closes runs no code, so the requests in
17
17
  * flight in its shards fail (ShardOwnerLostError) and the other tabs take the shards over.
18
18
  */
19
- import { CellRouter } from "../router/cell-router.js";
19
+ import { SwHttpDispatcher } from "@statewalker/webrun-http-browser/sw";
20
20
  import type { BrowserBucket, BrowserProjectOptions } from "./browser-host.js";
21
21
  /**
22
22
  * The tab that should run `shard`: the tabs sorted by id (ids start with the time the tab joined),
@@ -37,7 +37,12 @@ export interface ProjectTabsOptions {
37
37
  ai?: BrowserProjectOptions["ai"];
38
38
  /** Each shard is its own fleet node, `<node>-s<shard>`. */
39
39
  bucket?: BrowserBucket;
40
- /** The active ServiceWorker that runs `installProjectRouter`. */
40
+ /**
41
+ * The active ServiceWorker that runs `installProjectRouter`. This page must be inside its scope
42
+ * and controlled by it (webrun-http-browser's adapter asks the worker to claim the page if it
43
+ * is not, and rejects with a `ServiceWorkerControlError` after 30 s if that fails: after a hard
44
+ * reload, say).
45
+ */
41
46
  serviceWorker: () => Promise<ServiceWorker>;
42
47
  /** Called whenever the shards this tab runs change. */
43
48
  onChange?: (owned: number[]) => void;
@@ -70,7 +75,12 @@ export interface ServiceWorkerScope {
70
75
  readonly clients: unknown;
71
76
  addEventListener(type: string, listener: (event: never) => unknown): void;
72
77
  }
73
- /** The ServiceWorker side: one CellRouter per project, fed by the tabs' shard workers. */
78
+ /**
79
+ * The ServiceWorker side: webrun-http-browser's dispatcher, with the project as its key and the
80
+ * tabs' shard workers as its handlers. What cellb adds: its urls (`<basePath><name>/`, a root
81
+ * project, `http://cellb.internal/<name>/` for calls between shards), which shard a call goes to,
82
+ * the page's cookies, no control headers from pages, and the host page.
83
+ */
74
84
  export declare function installProjectRouter(scope: ServiceWorkerScope, opts: {
75
85
  /** Requests to `<basePath><name>/…` go to project `name`. */
76
86
  basePath: string;
@@ -99,5 +109,5 @@ export declare function installProjectRouter(scope: ServiceWorkerScope, opts: {
99
109
  hostWaitMs?: number;
100
110
  };
101
111
  }): {
102
- routers: Map<string, CellRouter>;
112
+ dispatcher: SwHttpDispatcher;
103
113
  };