@statewalker/cellb 0.3.0 → 0.4.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/dist/bucket/celld-cells.js +25 -3
- package/dist/bucket/celld-cells.js.map +1 -1
- package/dist/project/browser-shards.d.ts +8 -9
- package/dist/project/browser-shards.js +88 -52
- package/dist/project/browser-shards.js.map +1 -1
- package/dist/project/browser-tabs.d.ts +14 -4
- package/dist/project/browser-tabs.js +193 -208
- package/dist/project/browser-tabs.js.map +1 -1
- package/dist/transport/cells-http.d.ts +6 -0
- package/dist/transport/cells-http.js +22 -1
- package/dist/transport/cells-http.js.map +1 -1
- package/dist/transport/cells-link.d.ts +2 -0
- package/dist/transport/cells-link.js +5 -0
- package/dist/transport/cells-link.js.map +1 -1
- package/dist/transport/port.js +4 -0
- package/dist/transport/port.js.map +1 -1
- package/dist/ws/server.d.ts +17 -19
- package/dist/ws/server.js +53 -130
- package/dist/ws/server.js.map +1 -1
- package/package.json +4 -4
|
@@ -17,6 +17,9 @@ export function celldBucket(store, opts) {
|
|
|
17
17
|
let renew;
|
|
18
18
|
let deadline;
|
|
19
19
|
let leaseWritten;
|
|
20
|
+
/** The lease write on its way to the bucket, if any: `close()` waits for it before the delete. */
|
|
21
|
+
let writing = Promise.resolve();
|
|
22
|
+
let closed = false;
|
|
20
23
|
/** nodes/<node>.json, renewed every ttl/3; a failed renewal stops every cell (self-fencing). */
|
|
21
24
|
// celld decodes every node record (ownership_store.rs NodeLeaseWire / NodeLoadWire): a record
|
|
22
25
|
// missing a required load field breaks its fleet posture repair. This one is complete. It
|
|
@@ -47,7 +50,15 @@ export function celldBucket(store, opts) {
|
|
|
47
50
|
}
|
|
48
51
|
})();
|
|
49
52
|
let validUntil = 0;
|
|
50
|
-
const writeLease =
|
|
53
|
+
const writeLease = () => {
|
|
54
|
+
const w = putLease();
|
|
55
|
+
// Every write in flight, not only the latest: they may land in any order.
|
|
56
|
+
writing = Promise.all([writing, w.catch(() => { })]);
|
|
57
|
+
return w;
|
|
58
|
+
};
|
|
59
|
+
const putLease = async () => {
|
|
60
|
+
if (closed)
|
|
61
|
+
return;
|
|
51
62
|
const t = now();
|
|
52
63
|
const body = {
|
|
53
64
|
node: opts.node,
|
|
@@ -81,7 +92,8 @@ export function celldBucket(store, opts) {
|
|
|
81
92
|
// Our authority lasts until the record we just wrote expires — minus a margin, so a node
|
|
82
93
|
// that stalls (a paused process, a frozen tab) stops acknowledging before celld may take
|
|
83
94
|
// over: celld waits for expires_ms; we stop at expires_ms - margin.
|
|
84
|
-
|
|
95
|
+
if (!closed)
|
|
96
|
+
validUntil = t + ttl - Math.min(1_000, ttl / 4);
|
|
85
97
|
};
|
|
86
98
|
const owned = new Set();
|
|
87
99
|
/** Cells whose ownership record was found lost, with the time their refusal ends. */
|
|
@@ -89,6 +101,8 @@ export function celldBucket(store, opts) {
|
|
|
89
101
|
const ensureLease = () => {
|
|
90
102
|
leaseWritten ??= probeKeyReady.then(writeLease).then(() => {
|
|
91
103
|
const tick = () => {
|
|
104
|
+
if (closed)
|
|
105
|
+
return;
|
|
92
106
|
renew = setTimeout(() => {
|
|
93
107
|
writeLease().then(tick, (e) => lost.abort(e));
|
|
94
108
|
}, ttl / 3);
|
|
@@ -113,7 +127,7 @@ export function celldBucket(store, opts) {
|
|
|
113
127
|
};
|
|
114
128
|
// One LTX writer (ltx-log.ts) holds each cell's epoch, TXID and page mirror.
|
|
115
129
|
// A write is acknowledged only while our node lease is still ours (self-fencing).
|
|
116
|
-
const log = ltxLog(store, { now, authorized: () => now() < validUntil });
|
|
130
|
+
const log = ltxLog(store, { now, authorized: () => !closed && now() < validUntil });
|
|
117
131
|
const ownership = {
|
|
118
132
|
async tryAcquire(cellKey) {
|
|
119
133
|
await ensureLease();
|
|
@@ -260,10 +274,18 @@ export function celldBucket(store, opts) {
|
|
|
260
274
|
},
|
|
261
275
|
/** Clean shutdown: stop renewing, remove this node's lease record, close the store. */
|
|
262
276
|
async close() {
|
|
277
|
+
closed = true;
|
|
263
278
|
if (renew)
|
|
264
279
|
clearTimeout(renew);
|
|
265
280
|
if (deadline)
|
|
266
281
|
clearTimeout(deadline);
|
|
282
|
+
validUntil = 0; // no write is acknowledged once closing has begun
|
|
283
|
+
// A renewal already on its way would write the record back after the delete, and the
|
|
284
|
+
// node's cells would stay "owned by a live node" for a whole lease: let it land first —
|
|
285
|
+
// for at most a lease, since one that lands later expires within a lease anyway.
|
|
286
|
+
let waited;
|
|
287
|
+
await Promise.race([writing, new Promise((r) => (waited = setTimeout(r, ttl)))]);
|
|
288
|
+
clearTimeout(waited);
|
|
267
289
|
if (leaseWritten)
|
|
268
290
|
await store.delete(`nodes/${opts.node}.json`).catch(() => { });
|
|
269
291
|
store.close?.();
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"celld-cells.js","sourceRoot":"","sources":["../../src/bucket/celld-cells.ts"],"names":[],"mappings":"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","sourcesContent":["import { bytesToHex } from \"@noble/hashes/utils.js\";\nimport { scopeOf } from \"../core/celld-schema.js\";\nimport { decodeLtx, encodeLtx, ltxFileName } from \"../ltx/ltx.js\";\nimport type { BackupInfo, CellBackup, CellDurability } from \"../ports.js\";\nimport { equal, pad16, pageSizeOf, toRollback } from \"./celld-format.js\";\nimport { ltxLog } from \"./ltx-log.js\";\nimport { type ObjectStore, PreconditionFailedError } from \"./object-store.js\";\n\n/**\n * Cells stored the way celld stores them (docs/guarantees.md, and the keys celld dev writes\n * in `.celld/dev/objects.sqlite3`), on any ObjectStore:\n *\n * cells/<Class>:<idHex>/own.json {\"node\",\"epoch\"}: ownership record\n * cells/<Class>:<idHex>/ltx/e<epoch>/0000/<min>-<max>.ltx LTX v3, legacy LZ4 frames\n * nodes/<node>.json {\"node\",\"expires_ms\",\"addr\",…}: node lease\n *\n * Ownership: an activation creates the record (If-None-Match) or advances its epoch with a\n * compare-and-swap (If-Match), taking over only from a node whose lease expired. Durability:\n * each committed transaction becomes an LTX file in the cell's current epoch prefix — the\n * first one a snapshot (TXID 1), then the changed pages — and the output gate waits for the\n * PUT. Restore: the newest epoch that starts with a snapshot, its files in TXID order.\n */\nexport interface CelldBucketOptions {\n /** This node's name (celld: `dev-…` / the fleet node name). */\n node: string;\n addr?: string;\n leaseTtlMs?: number;\n now?: () => number;\n /** The waker-role lease (celld: 3 × its due-scan tick); match 3 × the runtime's wakePollMs. */\n wakerTtlMs?: number;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\n/** celld 0.6.3: how long a node refuses a cell whose ownership record it found lost. */\nexport const LOST_RECORD_RECHECK_MS = 10_000;\nconst lostRecordError = (scope: string) =>\n Object.assign(\n new Error(\n `ownership record lost: cells/${scope}/own.json is missing, but the bucket holds LTX under ` +\n `cells/${scope}/ltx/; refusing to claim ${scope} as a new cell (celld docs/guarantees.md: ` +\n `write a released record {\"node\": \"\", \"epoch\": <newest epoch>} to restore it)`,\n ),\n { name: \"OwnershipRecordLostError\" },\n );\n\n/** The bucket's `close()` also closes `store` (when it has a `close`): pass a store of its own. */\nexport function celldBucket(store: ObjectStore & { close?(): void }, opts: CelldBucketOptions) {\n const now = opts.now ?? (() => Date.now());\n const ttl = opts.leaseTtlMs ?? 10_000;\n const lost = new AbortController();\n let renew: ReturnType<typeof setTimeout> | undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let leaseWritten: Promise<void> | undefined;\n\n /** nodes/<node>.json, renewed every ttl/3; a failed renewal stops every cell (self-fencing). */\n // celld decodes every node record (ownership_store.rs NodeLeaseWire / NodeLoadWire): a record\n // missing a required load field breaks its fleet posture repair. This one is complete. It\n // reports pressure and zero placement weight, so celld never offers this node a cell — it\n // takes cells only when a request arrives.\n //\n // The address must not be empty: celld treats a live owner WITHOUT an address as dead and\n // takes its cells over (logic/lib.rs apply_node_lease_result) — a split brain. With an\n // address celld routes a request for our cell to it; a node that does not accept celld's\n // peer protocol (a browser) gets an unroutable placeholder, so the request fails instead.\n let addr = opts.addr || `${opts.node}.cellb.invalid:9`;\n // celld's per-process generation is its signed-probe public key (Ed25519, peer_probe.rs):\n // `probe_public_key` = `ownership_index_generation`. A real key lets `celld diagnose --peer`\n // verify that a probe reached this node.\n let generation = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));\n let probeKey: CryptoKey | undefined;\n const probeKeyReady = (async () => {\n try {\n const pair = (await crypto.subtle.generateKey({ name: \"Ed25519\" }, true, [\n \"sign\",\n \"verify\",\n ])) as CryptoKeyPair;\n probeKey = pair.privateKey;\n generation = bytesToHex(new Uint8Array(await crypto.subtle.exportKey(\"raw\", pair.publicKey)));\n } catch {\n // no Ed25519 in this WebCrypto: the record keeps a random generation (probes then fail)\n }\n })();\n let validUntil = 0;\n const writeLease = async () => {\n const t = now();\n const body = {\n node: opts.node,\n expires_ms: t + ttl,\n addr,\n probe_public_key: generation,\n peer_protocol: 5,\n paced_handoff: false,\n ownership_index_generation: generation,\n load: {\n sampled_ms: t,\n owned_cells: owned.size,\n placement_weight: 0,\n bucket_format: 2,\n resident_cells: owned.size,\n host_websockets: 0,\n rss_bytes: 0,\n in_use_bytes: 0,\n cpu_percent_x100: 0,\n open_fds: 0,\n fd_limit: 0,\n pressured: true,\n memory_headroom: false,\n shed_cells: 0,\n restoring: 0,\n rebalance_paused: false,\n draining: false,\n },\n };\n await store.put(`nodes/${opts.node}.json`, enc.encode(JSON.stringify(body)));\n // Our authority lasts until the record we just wrote expires — minus a margin, so a node\n // that stalls (a paused process, a frozen tab) stops acknowledging before celld may take\n // over: celld waits for expires_ms; we stop at expires_ms - margin.\n validUntil = t + ttl - Math.min(1_000, ttl / 4);\n };\n const owned = new Set<string>();\n /** Cells whose ownership record was found lost, with the time their refusal ends. */\n const lostRecords = new Map<string, number>();\n const ensureLease = () => {\n leaseWritten ??= probeKeyReady.then(writeLease).then(() => {\n const tick = () => {\n renew = setTimeout(() => {\n writeLease().then(tick, (e) => lost.abort(e));\n }, ttl / 3);\n // If renewals stop arriving (a stalled process, an unreachable bucket), every cell\n // stops at the local deadline — before celld may take it over.\n if (deadline) clearTimeout(deadline);\n deadline = setTimeout(\n () => {\n if (now() >= validUntil)\n lost.abort(new Error(`node ${opts.node}: lease deadline passed`));\n },\n Math.max(0, validUntil - now()),\n );\n (deadline as { unref?: () => void }).unref?.();\n (renew as { unref?: () => void }).unref?.();\n };\n tick();\n });\n return leaseWritten;\n };\n\n const readJson = async <T>(key: string) => {\n const o = await store.get(key);\n return o ? { value: JSON.parse(dec.decode(o.body)) as T, etag: o.etag } : undefined;\n };\n\n // One LTX writer (ltx-log.ts) holds each cell's epoch, TXID and page mirror.\n // A write is acknowledged only while our node lease is still ours (self-fencing).\n const log = ltxLog(store, { now, authorized: () => now() < validUntil });\n\n const ownership = {\n async tryAcquire(cellKey: string) {\n await ensureLease();\n const scope = scopeOf(cellKey);\n const key = `cells/${scope}/own.json`;\n const t0 = now();\n if ((lostRecords.get(scope) ?? 0) > t0) throw lostRecordError(scope); // still refused\n let cur = await readJson<{ node: string; epoch: number }>(key);\n // celld 0.6.3 (actor.rs read_owner_for_claim): a missing record next to LTX means the\n // record was LOST (e.g. a lifecycle rule) — claiming epoch 1 would start the cell empty.\n // Read once more, then refuse the cell for 10 s without asking the bucket again.\n if (!cur && (await store.list(`cells/${scope}/ltx`)).length > 0) {\n cur = await readJson<{ node: string; epoch: number }>(key);\n if (!cur) {\n lostRecords.set(scope, t0 + LOST_RECORD_RECHECK_MS);\n throw lostRecordError(scope);\n }\n }\n let epoch: number;\n try {\n if (!cur) {\n epoch = 1;\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifNoneMatch: \"*\",\n });\n } else {\n const holder = cur.value.node;\n if (holder && holder !== opts.node) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${holder}.json`);\n if (lease && lease.value.expires_ms > now()) return undefined; // a live owner\n }\n epoch = cur.value.epoch + 1; // every activation advances the epoch\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifMatch: cur.etag,\n });\n }\n } catch (e) {\n if (e instanceof PreconditionFailedError) return undefined; // another node won the race\n throw e;\n }\n log.begin(cellKey, epoch); // a new owner epoch starts a new generation\n owned.add(cellKey);\n return {\n name: cellKey,\n epoch,\n lost: lost.signal,\n release: async () => {\n const now2 = await readJson<{ node: string; epoch: number }>(key);\n if (now2?.value.node === opts.node && now2.value.epoch === epoch)\n await store\n .put(key, enc.encode(JSON.stringify({ node: \"\", epoch })), { ifMatch: now2.etag })\n .catch(() => {});\n log.forget(cellKey);\n owned.delete(cellKey);\n },\n };\n },\n };\n\n // celld's waker role (wake.rs try_hold_waker): one node per fleet wakes alarms of cells that\n // nobody owns or whose owner is dead — an advisory CAS lease in wake/waker.json; every node\n // wakes its own cells; a live owner's cells are never hinted.\n const holdWaker = async (ttlMs: number) => {\n const key = \"wake/waker.json\";\n const cur = await readJson<{ node: string; expires_ms: number }>(key);\n const t = now();\n if (cur && cur.value.node !== opts.node && cur.value.expires_ms > t) return false;\n try {\n await store.put(\n key,\n enc.encode(JSON.stringify({ node: opts.node, expires_ms: t + ttlMs })),\n cur ? { ifMatch: cur.etag } : { ifNoneMatch: \"*\" },\n );\n return true;\n } catch (e) {\n if (!(e instanceof PreconditionFailedError)) return false;\n const again = await readJson<{ node: string; expires_ms: number }>(key);\n return again?.value.node === opts.node && again.value.expires_ms > t;\n }\n };\n const allWakes = log.wakes!.bind(log);\n const durability = Object.assign(log, {\n async wakes() {\n const due = await allWakes();\n let elected: boolean | undefined;\n const out: typeof due = [];\n for (const w of due) {\n const own = await readJson<{ node: string }>(`cells/${scopeOf(w.cellKey)}/own.json`);\n const owner = own?.value.node;\n if (owner === opts.node) {\n out.push(w); // our own cell: always\n continue;\n }\n let ownerExpires = 0;\n if (owner) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${owner}.json`);\n ownerExpires = lease?.value.expires_ms ?? 0;\n }\n elected ??= await holdWaker(opts.wakerTtlMs ?? 90_000);\n if (!elected) continue; // unowned or dead owners are the elected waker's job\n // A live owner fires its own alarms; the waker looks again when that owner's lease\n // would have run out (a stopped owner's cells are then due here).\n out.push(ownerExpires > now() ? { ...w, at: Math.max(w.at, ownerExpires + 1) } : w);\n }\n return out;\n },\n });\n\n return {\n ownership,\n durability,\n backup: durability,\n /** The fleet bucket itself (R2 objects live directly in it, under `r2/<bucket>/`). */\n objects: store,\n /**\n * The address celld nodes reach this node at (its celld peer server): published in the\n * node record now, so celld routes requests for our cells here instead of refusing them.\n */\n /**\n * Answers celld's signed peer probe (peer_probe.rs): an Ed25519 signature over\n * \"cells-peer-probe-v1\\n<node>\\n<advertised addr>\\n<challenge>\" by this node's probe key.\n */\n async probe(challenge: string) {\n await probeKeyReady;\n if (!probeKey || !/^[0-9a-f]{64}$/.test(challenge))\n throw new Error(\"cannot answer the probe\");\n const payload = `cells-peer-probe-v1\\n${opts.node}\\n${addr}\\n${challenge}`;\n const signature = new Uint8Array(\n await crypto.subtle.sign(\"Ed25519\", probeKey, new TextEncoder().encode(payload)),\n );\n return {\n version: 1,\n node: opts.node,\n advertise: addr,\n challenge,\n signature: bytesToHex(signature),\n };\n },\n async advertise(address: string) {\n addr = address;\n if (leaseWritten) await writeLease();\n },\n /** Clean shutdown: stop renewing, remove this node's lease record, close the store. */\n async close() {\n if (renew) clearTimeout(renew);\n if (deadline) clearTimeout(deadline);\n if (leaseWritten) await store.delete(`nodes/${opts.node}.json`).catch(() => {});\n store.close?.();\n },\n };\n}\n"]}
|
|
1
|
+
{"version":3,"file":"celld-cells.js","sourceRoot":"","sources":["../../src/bucket/celld-cells.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,UAAU,EAAE,MAAM,wBAAwB,CAAC;AACpD,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAIlD,OAAO,EAAE,MAAM,EAAE,MAAM,cAAc,CAAC;AACtC,OAAO,EAAoB,uBAAuB,EAAE,MAAM,mBAAmB,CAAC;AA0B9E,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;AAC9B,MAAM,GAAG,GAAG,IAAI,WAAW,EAAE,CAAC;AAE9B,wFAAwF;AACxF,MAAM,CAAC,MAAM,sBAAsB,GAAG,MAAM,CAAC;AAC7C,MAAM,eAAe,GAAG,CAAC,KAAa,EAAE,EAAE,CACxC,MAAM,CAAC,MAAM,CACX,IAAI,KAAK,CACP,gCAAgC,KAAK,uDAAuD;IAC1F,SAAS,KAAK,4BAA4B,KAAK,4CAA4C;IAC3F,8EAA8E,CACjF,EACD,EAAE,IAAI,EAAE,0BAA0B,EAAE,CACrC,CAAC;AAEJ,mGAAmG;AACnG,MAAM,UAAU,WAAW,CAAC,KAAuC,EAAE,IAAwB;IAC3F,MAAM,GAAG,GAAG,IAAI,CAAC,GAAG,IAAI,CAAC,GAAG,EAAE,CAAC,IAAI,CAAC,GAAG,EAAE,CAAC,CAAC;IAC3C,MAAM,GAAG,GAAG,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC;IACtC,MAAM,IAAI,GAAG,IAAI,eAAe,EAAE,CAAC;IACnC,IAAI,KAAgD,CAAC;IACrD,IAAI,QAAmD,CAAC;IACxD,IAAI,YAAuC,CAAC;IAC5C,kGAAkG;IAClG,IAAI,OAAO,GAAqB,OAAO,CAAC,OAAO,EAAE,CAAC;IAClD,IAAI,MAAM,GAAG,KAAK,CAAC;IAEnB,gGAAgG;IAChG,8FAA8F;IAC9F,0FAA0F;IAC1F,0FAA0F;IAC1F,2CAA2C;IAC3C,EAAE;IACF,0FAA0F;IAC1F,uFAAuF;IACvF,yFAAyF;IACzF,0FAA0F;IAC1F,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,IAAI,GAAG,IAAI,CAAC,IAAI,kBAAkB,CAAC;IACvD,0FAA0F;IAC1F,6FAA6F;IAC7F,yCAAyC;IACzC,IAAI,UAAU,GAAG,UAAU,CAAC,MAAM,CAAC,eAAe,CAAC,IAAI,UAAU,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;IACxE,IAAI,QAA+B,CAAC;IACpC,MAAM,aAAa,GAAG,CAAC,KAAK,IAAI,EAAE;QAChC,IAAI,CAAC;YACH,MAAM,IAAI,GAAG,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,WAAW,CAAC,EAAE,IAAI,EAAE,SAAS,EAAE,EAAE,IAAI,EAAE;gBACvE,MAAM;gBACN,QAAQ;aACT,CAAC,CAAkB,CAAC;YACrB,QAAQ,GAAG,IAAI,CAAC,UAAU,CAAC;YAC3B,UAAU,GAAG,UAAU,CAAC,IAAI,UAAU,CAAC,MAAM,MAAM,CAAC,MAAM,CAAC,SAAS,CAAC,KAAK,EAAE,IAAI,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC;QAChG,CAAC;QAAC,MAAM,CAAC;YACP,wFAAwF;QAC1F,CAAC;IACH,CAAC,CAAC,EAAE,CAAC;IACL,IAAI,UAAU,GAAG,CAAC,CAAC;IACnB,MAAM,UAAU,GAAG,GAAG,EAAE;QACtB,MAAM,CAAC,GAAG,QAAQ,EAAE,CAAC;QACrB,0EAA0E;QAC1E,OAAO,GAAG,OAAO,CAAC,GAAG,CAAC,CAAC,OAAO,EAAE,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACpD,OAAO,CAAC,CAAC;IACX,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,KAAK,IAAI,EAAE;QAC1B,IAAI,MAAM;YAAE,OAAO;QACnB,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG;YACX,IAAI,EAAE,IAAI,CAAC,IAAI;YACf,UAAU,EAAE,CAAC,GAAG,GAAG;YACnB,IAAI;YACJ,gBAAgB,EAAE,UAAU;YAC5B,aAAa,EAAE,CAAC;YAChB,aAAa,EAAE,KAAK;YACpB,0BAA0B,EAAE,UAAU;YACtC,IAAI,EAAE;gBACJ,UAAU,EAAE,CAAC;gBACb,WAAW,EAAE,KAAK,CAAC,IAAI;gBACvB,gBAAgB,EAAE,CAAC;gBACnB,aAAa,EAAE,CAAC;gBAChB,cAAc,EAAE,KAAK,CAAC,IAAI;gBAC1B,eAAe,EAAE,CAAC;gBAClB,SAAS,EAAE,CAAC;gBACZ,YAAY,EAAE,CAAC;gBACf,gBAAgB,EAAE,CAAC;gBACnB,QAAQ,EAAE,CAAC;gBACX,QAAQ,EAAE,CAAC;gBACX,SAAS,EAAE,IAAI;gBACf,eAAe,EAAE,KAAK;gBACtB,UAAU,EAAE,CAAC;gBACb,SAAS,EAAE,CAAC;gBACZ,gBAAgB,EAAE,KAAK;gBACvB,QAAQ,EAAE,KAAK;aAChB;SACF,CAAC;QACF,MAAM,KAAK,CAAC,GAAG,CAAC,SAAS,IAAI,CAAC,IAAI,OAAO,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC;QAC7E,yFAAyF;QACzF,yFAAyF;QACzF,oEAAoE;QACpE,IAAI,CAAC,MAAM;YAAE,UAAU,GAAG,CAAC,GAAG,GAAG,GAAG,IAAI,CAAC,GAAG,CAAC,KAAK,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;IAC/D,CAAC,CAAC;IACF,MAAM,KAAK,GAAG,IAAI,GAAG,EAAU,CAAC;IAChC,qFAAqF;IACrF,MAAM,WAAW,GAAG,IAAI,GAAG,EAAkB,CAAC;IAC9C,MAAM,WAAW,GAAG,GAAG,EAAE;QACvB,YAAY,KAAK,aAAa,CAAC,IAAI,CAAC,UAAU,CAAC,CAAC,IAAI,CAAC,GAAG,EAAE;YACxD,MAAM,IAAI,GAAG,GAAG,EAAE;gBAChB,IAAI,MAAM;oBAAE,OAAO;gBACnB,KAAK,GAAG,UAAU,CAAC,GAAG,EAAE;oBACtB,UAAU,EAAE,CAAC,IAAI,CAAC,IAAI,EAAE,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;gBAChD,CAAC,EAAE,GAAG,GAAG,CAAC,CAAC,CAAC;gBACZ,mFAAmF;gBACnF,+DAA+D;gBAC/D,IAAI,QAAQ;oBAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;gBACrC,QAAQ,GAAG,UAAU,CACnB,GAAG,EAAE;oBACH,IAAI,GAAG,EAAE,IAAI,UAAU;wBACrB,IAAI,CAAC,KAAK,CAAC,IAAI,KAAK,CAAC,QAAQ,IAAI,CAAC,IAAI,yBAAyB,CAAC,CAAC,CAAC;gBACtE,CAAC,EACD,IAAI,CAAC,GAAG,CAAC,CAAC,EAAE,UAAU,GAAG,GAAG,EAAE,CAAC,CAChC,CAAC;gBACD,QAAmC,CAAC,KAAK,EAAE,EAAE,CAAC;gBAC9C,KAAgC,CAAC,KAAK,EAAE,EAAE,CAAC;YAC9C,CAAC,CAAC;YACF,IAAI,EAAE,CAAC;QACT,CAAC,CAAC,CAAC;QACH,OAAO,YAAY,CAAC;IACtB,CAAC,CAAC;IAEF,MAAM,QAAQ,GAAG,KAAK,EAAK,GAAW,EAAE,EAAE;QACxC,MAAM,CAAC,GAAG,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QAC/B,OAAO,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,IAAI,CAAC,KAAK,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAM,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,SAAS,CAAC;IACtF,CAAC,CAAC;IAEF,6EAA6E;IAC7E,kFAAkF;IAClF,MAAM,GAAG,GAAG,MAAM,CAAC,KAAK,EAAE,EAAE,GAAG,EAAE,UAAU,EAAE,GAAG,EAAE,CAAC,CAAC,MAAM,IAAI,GAAG,EAAE,GAAG,UAAU,EAAE,CAAC,CAAC;IAEpF,MAAM,SAAS,GAAG;QAChB,KAAK,CAAC,UAAU,CAAC,OAAe;YAC9B,MAAM,WAAW,EAAE,CAAC;YACpB,MAAM,KAAK,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;YAC/B,MAAM,GAAG,GAAG,SAAS,KAAK,WAAW,CAAC;YACtC,MAAM,EAAE,GAAG,GAAG,EAAE,CAAC;YACjB,IAAI,CAAC,WAAW,CAAC,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,GAAG,EAAE;gBAAE,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC,CAAC,gBAAgB;YACtF,IAAI,GAAG,GAAG,MAAM,QAAQ,CAAkC,GAAG,CAAC,CAAC;YAC/D,sFAAsF;YACtF,yFAAyF;YACzF,iFAAiF;YACjF,IAAI,CAAC,GAAG,IAAI,CAAC,MAAM,KAAK,CAAC,IAAI,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,EAAE,CAAC;gBAChE,GAAG,GAAG,MAAM,QAAQ,CAAkC,GAAG,CAAC,CAAC;gBAC3D,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,WAAW,CAAC,GAAG,CAAC,KAAK,EAAE,EAAE,GAAG,sBAAsB,CAAC,CAAC;oBACpD,MAAM,eAAe,CAAC,KAAK,CAAC,CAAC;gBAC/B,CAAC;YACH,CAAC;YACD,IAAI,KAAa,CAAC;YAClB,IAAI,CAAC;gBACH,IAAI,CAAC,GAAG,EAAE,CAAC;oBACT,KAAK,GAAG,CAAC,CAAC;oBACV,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE;wBAC3E,WAAW,EAAE,GAAG;qBACjB,CAAC,CAAC;gBACL,CAAC;qBAAM,CAAC;oBACN,MAAM,MAAM,GAAG,GAAG,CAAC,KAAK,CAAC,IAAI,CAAC;oBAC9B,IAAI,MAAM,IAAI,MAAM,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;wBACnC,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAyB,SAAS,MAAM,OAAO,CAAC,CAAC;wBAC7E,IAAI,KAAK,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,GAAG,EAAE;4BAAE,OAAO,SAAS,CAAC,CAAC,eAAe;oBAChF,CAAC;oBACD,KAAK,GAAG,GAAG,CAAC,KAAK,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,sCAAsC;oBACnE,MAAM,KAAK,CAAC,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE;wBAC3E,OAAO,EAAE,GAAG,CAAC,IAAI;qBAClB,CAAC,CAAC;gBACL,CAAC;YACH,CAAC;YAAC,OAAO,CAAC,EAAE,CAAC;gBACX,IAAI,CAAC,YAAY,uBAAuB;oBAAE,OAAO,SAAS,CAAC,CAAC,4BAA4B;gBACxF,MAAM,CAAC,CAAC;YACV,CAAC;YACD,GAAG,CAAC,KAAK,CAAC,OAAO,EAAE,KAAK,CAAC,CAAC,CAAC,4CAA4C;YACvE,KAAK,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACnB,OAAO;gBACL,IAAI,EAAE,OAAO;gBACb,KAAK;gBACL,IAAI,EAAE,IAAI,CAAC,MAAM;gBACjB,OAAO,EAAE,KAAK,IAAI,EAAE;oBAClB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAkC,GAAG,CAAC,CAAC;oBAClE,IAAI,IAAI,EAAE,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,KAAK,CAAC,KAAK,KAAK,KAAK;wBAC9D,MAAM,KAAK;6BACR,GAAG,CAAC,GAAG,EAAE,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,EAAE,EAAE,KAAK,EAAE,CAAC,CAAC,EAAE,EAAE,OAAO,EAAE,IAAI,CAAC,IAAI,EAAE,CAAC;6BACjF,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBACrB,GAAG,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;oBACpB,KAAK,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;gBACxB,CAAC;aACF,CAAC;QACJ,CAAC;KACF,CAAC;IAEF,6FAA6F;IAC7F,4FAA4F;IAC5F,8DAA8D;IAC9D,MAAM,SAAS,GAAG,KAAK,EAAE,KAAa,EAAE,EAAE;QACxC,MAAM,GAAG,GAAG,iBAAiB,CAAC;QAC9B,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAuC,GAAG,CAAC,CAAC;QACtE,MAAM,CAAC,GAAG,GAAG,EAAE,CAAC;QAChB,IAAI,GAAG,IAAI,GAAG,CAAC,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,GAAG,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC;YAAE,OAAO,KAAK,CAAC;QAClF,IAAI,CAAC;YACH,MAAM,KAAK,CAAC,GAAG,CACb,GAAG,EACH,GAAG,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,IAAI,EAAE,IAAI,CAAC,IAAI,EAAE,UAAU,EAAE,CAAC,GAAG,KAAK,EAAE,CAAC,CAAC,EACtE,GAAG,CAAC,CAAC,CAAC,EAAE,OAAO,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC,CAAC,CAAC,EAAE,WAAW,EAAE,GAAG,EAAE,CACnD,CAAC;YACF,OAAO,IAAI,CAAC;QACd,CAAC;QAAC,OAAO,CAAC,EAAE,CAAC;YACX,IAAI,CAAC,CAAC,CAAC,YAAY,uBAAuB,CAAC;gBAAE,OAAO,KAAK,CAAC;YAC1D,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAuC,GAAG,CAAC,CAAC;YACxE,OAAO,KAAK,EAAE,KAAK,CAAC,IAAI,KAAK,IAAI,CAAC,IAAI,IAAI,KAAK,CAAC,KAAK,CAAC,UAAU,GAAG,CAAC,CAAC;QACvE,CAAC;IACH,CAAC,CAAC;IACF,MAAM,QAAQ,GAAG,GAAG,CAAC,KAAM,CAAC,IAAI,CAAC,GAAG,CAAC,CAAC;IACtC,MAAM,UAAU,GAAG,MAAM,CAAC,MAAM,CAAC,GAAG,EAAE;QACpC,KAAK,CAAC,KAAK;YACT,MAAM,GAAG,GAAG,MAAM,QAAQ,EAAE,CAAC;YAC7B,IAAI,OAA4B,CAAC;YACjC,MAAM,GAAG,GAAe,EAAE,CAAC;YAC3B,KAAK,MAAM,CAAC,IAAI,GAAG,EAAE,CAAC;gBACpB,MAAM,GAAG,GAAG,MAAM,QAAQ,CAAmB,SAAS,OAAO,CAAC,CAAC,CAAC,OAAO,CAAC,WAAW,CAAC,CAAC;gBACrF,MAAM,KAAK,GAAG,GAAG,EAAE,KAAK,CAAC,IAAI,CAAC;gBAC9B,IAAI,KAAK,KAAK,IAAI,CAAC,IAAI,EAAE,CAAC;oBACxB,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,CAAC,uBAAuB;oBACpC,SAAS;gBACX,CAAC;gBACD,IAAI,YAAY,GAAG,CAAC,CAAC;gBACrB,IAAI,KAAK,EAAE,CAAC;oBACV,MAAM,KAAK,GAAG,MAAM,QAAQ,CAAyB,SAAS,KAAK,OAAO,CAAC,CAAC;oBAC5E,YAAY,GAAG,KAAK,EAAE,KAAK,CAAC,UAAU,IAAI,CAAC,CAAC;gBAC9C,CAAC;gBACD,OAAO,KAAK,MAAM,SAAS,CAAC,IAAI,CAAC,UAAU,IAAI,MAAM,CAAC,CAAC;gBACvD,IAAI,CAAC,OAAO;oBAAE,SAAS,CAAC,qDAAqD;gBAC7E,mFAAmF;gBACnF,kEAAkE;gBAClE,GAAG,CAAC,IAAI,CAAC,YAAY,GAAG,GAAG,EAAE,CAAC,CAAC,CAAC,EAAE,GAAG,CAAC,EAAE,EAAE,EAAE,IAAI,CAAC,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,YAAY,GAAG,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACtF,CAAC;YACD,OAAO,GAAG,CAAC;QACb,CAAC;KACF,CAAC,CAAC;IAEH,OAAO;QACL,SAAS;QACT,UAAU;QACV,MAAM,EAAE,UAAU;QAClB,sFAAsF;QACtF,OAAO,EAAE,KAAK;QACd;;;WAGG;QACH;;;WAGG;QACH,KAAK,CAAC,KAAK,CAAC,SAAiB;YAC3B,MAAM,aAAa,CAAC;YACpB,IAAI,CAAC,QAAQ,IAAI,CAAC,gBAAgB,CAAC,IAAI,CAAC,SAAS,CAAC;gBAChD,MAAM,IAAI,KAAK,CAAC,yBAAyB,CAAC,CAAC;YAC7C,MAAM,OAAO,GAAG,wBAAwB,IAAI,CAAC,IAAI,KAAK,IAAI,KAAK,SAAS,EAAE,CAAC;YAC3E,MAAM,SAAS,GAAG,IAAI,UAAU,CAC9B,MAAM,MAAM,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,EAAE,QAAQ,EAAE,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC,CACjF,CAAC;YACF,OAAO;gBACL,OAAO,EAAE,CAAC;gBACV,IAAI,EAAE,IAAI,CAAC,IAAI;gBACf,SAAS,EAAE,IAAI;gBACf,SAAS;gBACT,SAAS,EAAE,UAAU,CAAC,SAAS,CAAC;aACjC,CAAC;QACJ,CAAC;QACD,KAAK,CAAC,SAAS,CAAC,OAAe;YAC7B,IAAI,GAAG,OAAO,CAAC;YACf,IAAI,YAAY;gBAAE,MAAM,UAAU,EAAE,CAAC;QACvC,CAAC;QACD,uFAAuF;QACvF,KAAK,CAAC,KAAK;YACT,MAAM,GAAG,IAAI,CAAC;YACd,IAAI,KAAK;gBAAE,YAAY,CAAC,KAAK,CAAC,CAAC;YAC/B,IAAI,QAAQ;gBAAE,YAAY,CAAC,QAAQ,CAAC,CAAC;YACrC,UAAU,GAAG,CAAC,CAAC,CAAC,kDAAkD;YAClE,qFAAqF;YACrF,wFAAwF;YACxF,iFAAiF;YACjF,IAAI,MAAiD,CAAC;YACtD,MAAM,OAAO,CAAC,IAAI,CAAC,CAAC,OAAO,EAAE,IAAI,OAAO,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,MAAM,GAAG,UAAU,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC;YACjF,YAAY,CAAC,MAAM,CAAC,CAAC;YACrB,IAAI,YAAY;gBAAE,MAAM,KAAK,CAAC,MAAM,CAAC,SAAS,IAAI,CAAC,IAAI,OAAO,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;YAChF,KAAK,CAAC,KAAK,EAAE,EAAE,CAAC;QAClB,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import { bytesToHex } from \"@noble/hashes/utils.js\";\nimport { scopeOf } from \"../core/celld-schema.js\";\nimport { decodeLtx, encodeLtx, ltxFileName } from \"../ltx/ltx.js\";\nimport type { BackupInfo, CellBackup, CellDurability } from \"../ports.js\";\nimport { equal, pad16, pageSizeOf, toRollback } from \"./celld-format.js\";\nimport { ltxLog } from \"./ltx-log.js\";\nimport { type ObjectStore, PreconditionFailedError } from \"./object-store.js\";\n\n/**\n * Cells stored the way celld stores them (docs/guarantees.md, and the keys celld dev writes\n * in `.celld/dev/objects.sqlite3`), on any ObjectStore:\n *\n * cells/<Class>:<idHex>/own.json {\"node\",\"epoch\"}: ownership record\n * cells/<Class>:<idHex>/ltx/e<epoch>/0000/<min>-<max>.ltx LTX v3, legacy LZ4 frames\n * nodes/<node>.json {\"node\",\"expires_ms\",\"addr\",…}: node lease\n *\n * Ownership: an activation creates the record (If-None-Match) or advances its epoch with a\n * compare-and-swap (If-Match), taking over only from a node whose lease expired. Durability:\n * each committed transaction becomes an LTX file in the cell's current epoch prefix — the\n * first one a snapshot (TXID 1), then the changed pages — and the output gate waits for the\n * PUT. Restore: the newest epoch that starts with a snapshot, its files in TXID order.\n */\nexport interface CelldBucketOptions {\n /** This node's name (celld: `dev-…` / the fleet node name). */\n node: string;\n addr?: string;\n leaseTtlMs?: number;\n now?: () => number;\n /** The waker-role lease (celld: 3 × its due-scan tick); match 3 × the runtime's wakePollMs. */\n wakerTtlMs?: number;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\n/** celld 0.6.3: how long a node refuses a cell whose ownership record it found lost. */\nexport const LOST_RECORD_RECHECK_MS = 10_000;\nconst lostRecordError = (scope: string) =>\n Object.assign(\n new Error(\n `ownership record lost: cells/${scope}/own.json is missing, but the bucket holds LTX under ` +\n `cells/${scope}/ltx/; refusing to claim ${scope} as a new cell (celld docs/guarantees.md: ` +\n `write a released record {\"node\": \"\", \"epoch\": <newest epoch>} to restore it)`,\n ),\n { name: \"OwnershipRecordLostError\" },\n );\n\n/** The bucket's `close()` also closes `store` (when it has a `close`): pass a store of its own. */\nexport function celldBucket(store: ObjectStore & { close?(): void }, opts: CelldBucketOptions) {\n const now = opts.now ?? (() => Date.now());\n const ttl = opts.leaseTtlMs ?? 10_000;\n const lost = new AbortController();\n let renew: ReturnType<typeof setTimeout> | undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let leaseWritten: Promise<void> | undefined;\n /** The lease write on its way to the bucket, if any: `close()` waits for it before the delete. */\n let writing: Promise<unknown> = Promise.resolve();\n let closed = false;\n\n /** nodes/<node>.json, renewed every ttl/3; a failed renewal stops every cell (self-fencing). */\n // celld decodes every node record (ownership_store.rs NodeLeaseWire / NodeLoadWire): a record\n // missing a required load field breaks its fleet posture repair. This one is complete. It\n // reports pressure and zero placement weight, so celld never offers this node a cell — it\n // takes cells only when a request arrives.\n //\n // The address must not be empty: celld treats a live owner WITHOUT an address as dead and\n // takes its cells over (logic/lib.rs apply_node_lease_result) — a split brain. With an\n // address celld routes a request for our cell to it; a node that does not accept celld's\n // peer protocol (a browser) gets an unroutable placeholder, so the request fails instead.\n let addr = opts.addr || `${opts.node}.cellb.invalid:9`;\n // celld's per-process generation is its signed-probe public key (Ed25519, peer_probe.rs):\n // `probe_public_key` = `ownership_index_generation`. A real key lets `celld diagnose --peer`\n // verify that a probe reached this node.\n let generation = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));\n let probeKey: CryptoKey | undefined;\n const probeKeyReady = (async () => {\n try {\n const pair = (await crypto.subtle.generateKey({ name: \"Ed25519\" }, true, [\n \"sign\",\n \"verify\",\n ])) as CryptoKeyPair;\n probeKey = pair.privateKey;\n generation = bytesToHex(new Uint8Array(await crypto.subtle.exportKey(\"raw\", pair.publicKey)));\n } catch {\n // no Ed25519 in this WebCrypto: the record keeps a random generation (probes then fail)\n }\n })();\n let validUntil = 0;\n const writeLease = () => {\n const w = putLease();\n // Every write in flight, not only the latest: they may land in any order.\n writing = Promise.all([writing, w.catch(() => {})]);\n return w;\n };\n const putLease = async () => {\n if (closed) return;\n const t = now();\n const body = {\n node: opts.node,\n expires_ms: t + ttl,\n addr,\n probe_public_key: generation,\n peer_protocol: 5,\n paced_handoff: false,\n ownership_index_generation: generation,\n load: {\n sampled_ms: t,\n owned_cells: owned.size,\n placement_weight: 0,\n bucket_format: 2,\n resident_cells: owned.size,\n host_websockets: 0,\n rss_bytes: 0,\n in_use_bytes: 0,\n cpu_percent_x100: 0,\n open_fds: 0,\n fd_limit: 0,\n pressured: true,\n memory_headroom: false,\n shed_cells: 0,\n restoring: 0,\n rebalance_paused: false,\n draining: false,\n },\n };\n await store.put(`nodes/${opts.node}.json`, enc.encode(JSON.stringify(body)));\n // Our authority lasts until the record we just wrote expires — minus a margin, so a node\n // that stalls (a paused process, a frozen tab) stops acknowledging before celld may take\n // over: celld waits for expires_ms; we stop at expires_ms - margin.\n if (!closed) validUntil = t + ttl - Math.min(1_000, ttl / 4);\n };\n const owned = new Set<string>();\n /** Cells whose ownership record was found lost, with the time their refusal ends. */\n const lostRecords = new Map<string, number>();\n const ensureLease = () => {\n leaseWritten ??= probeKeyReady.then(writeLease).then(() => {\n const tick = () => {\n if (closed) return;\n renew = setTimeout(() => {\n writeLease().then(tick, (e) => lost.abort(e));\n }, ttl / 3);\n // If renewals stop arriving (a stalled process, an unreachable bucket), every cell\n // stops at the local deadline — before celld may take it over.\n if (deadline) clearTimeout(deadline);\n deadline = setTimeout(\n () => {\n if (now() >= validUntil)\n lost.abort(new Error(`node ${opts.node}: lease deadline passed`));\n },\n Math.max(0, validUntil - now()),\n );\n (deadline as { unref?: () => void }).unref?.();\n (renew as { unref?: () => void }).unref?.();\n };\n tick();\n });\n return leaseWritten;\n };\n\n const readJson = async <T>(key: string) => {\n const o = await store.get(key);\n return o ? { value: JSON.parse(dec.decode(o.body)) as T, etag: o.etag } : undefined;\n };\n\n // One LTX writer (ltx-log.ts) holds each cell's epoch, TXID and page mirror.\n // A write is acknowledged only while our node lease is still ours (self-fencing).\n const log = ltxLog(store, { now, authorized: () => !closed && now() < validUntil });\n\n const ownership = {\n async tryAcquire(cellKey: string) {\n await ensureLease();\n const scope = scopeOf(cellKey);\n const key = `cells/${scope}/own.json`;\n const t0 = now();\n if ((lostRecords.get(scope) ?? 0) > t0) throw lostRecordError(scope); // still refused\n let cur = await readJson<{ node: string; epoch: number }>(key);\n // celld 0.6.3 (actor.rs read_owner_for_claim): a missing record next to LTX means the\n // record was LOST (e.g. a lifecycle rule) — claiming epoch 1 would start the cell empty.\n // Read once more, then refuse the cell for 10 s without asking the bucket again.\n if (!cur && (await store.list(`cells/${scope}/ltx`)).length > 0) {\n cur = await readJson<{ node: string; epoch: number }>(key);\n if (!cur) {\n lostRecords.set(scope, t0 + LOST_RECORD_RECHECK_MS);\n throw lostRecordError(scope);\n }\n }\n let epoch: number;\n try {\n if (!cur) {\n epoch = 1;\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifNoneMatch: \"*\",\n });\n } else {\n const holder = cur.value.node;\n if (holder && holder !== opts.node) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${holder}.json`);\n if (lease && lease.value.expires_ms > now()) return undefined; // a live owner\n }\n epoch = cur.value.epoch + 1; // every activation advances the epoch\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifMatch: cur.etag,\n });\n }\n } catch (e) {\n if (e instanceof PreconditionFailedError) return undefined; // another node won the race\n throw e;\n }\n log.begin(cellKey, epoch); // a new owner epoch starts a new generation\n owned.add(cellKey);\n return {\n name: cellKey,\n epoch,\n lost: lost.signal,\n release: async () => {\n const now2 = await readJson<{ node: string; epoch: number }>(key);\n if (now2?.value.node === opts.node && now2.value.epoch === epoch)\n await store\n .put(key, enc.encode(JSON.stringify({ node: \"\", epoch })), { ifMatch: now2.etag })\n .catch(() => {});\n log.forget(cellKey);\n owned.delete(cellKey);\n },\n };\n },\n };\n\n // celld's waker role (wake.rs try_hold_waker): one node per fleet wakes alarms of cells that\n // nobody owns or whose owner is dead — an advisory CAS lease in wake/waker.json; every node\n // wakes its own cells; a live owner's cells are never hinted.\n const holdWaker = async (ttlMs: number) => {\n const key = \"wake/waker.json\";\n const cur = await readJson<{ node: string; expires_ms: number }>(key);\n const t = now();\n if (cur && cur.value.node !== opts.node && cur.value.expires_ms > t) return false;\n try {\n await store.put(\n key,\n enc.encode(JSON.stringify({ node: opts.node, expires_ms: t + ttlMs })),\n cur ? { ifMatch: cur.etag } : { ifNoneMatch: \"*\" },\n );\n return true;\n } catch (e) {\n if (!(e instanceof PreconditionFailedError)) return false;\n const again = await readJson<{ node: string; expires_ms: number }>(key);\n return again?.value.node === opts.node && again.value.expires_ms > t;\n }\n };\n const allWakes = log.wakes!.bind(log);\n const durability = Object.assign(log, {\n async wakes() {\n const due = await allWakes();\n let elected: boolean | undefined;\n const out: typeof due = [];\n for (const w of due) {\n const own = await readJson<{ node: string }>(`cells/${scopeOf(w.cellKey)}/own.json`);\n const owner = own?.value.node;\n if (owner === opts.node) {\n out.push(w); // our own cell: always\n continue;\n }\n let ownerExpires = 0;\n if (owner) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${owner}.json`);\n ownerExpires = lease?.value.expires_ms ?? 0;\n }\n elected ??= await holdWaker(opts.wakerTtlMs ?? 90_000);\n if (!elected) continue; // unowned or dead owners are the elected waker's job\n // A live owner fires its own alarms; the waker looks again when that owner's lease\n // would have run out (a stopped owner's cells are then due here).\n out.push(ownerExpires > now() ? { ...w, at: Math.max(w.at, ownerExpires + 1) } : w);\n }\n return out;\n },\n });\n\n return {\n ownership,\n durability,\n backup: durability,\n /** The fleet bucket itself (R2 objects live directly in it, under `r2/<bucket>/`). */\n objects: store,\n /**\n * The address celld nodes reach this node at (its celld peer server): published in the\n * node record now, so celld routes requests for our cells here instead of refusing them.\n */\n /**\n * Answers celld's signed peer probe (peer_probe.rs): an Ed25519 signature over\n * \"cells-peer-probe-v1\\n<node>\\n<advertised addr>\\n<challenge>\" by this node's probe key.\n */\n async probe(challenge: string) {\n await probeKeyReady;\n if (!probeKey || !/^[0-9a-f]{64}$/.test(challenge))\n throw new Error(\"cannot answer the probe\");\n const payload = `cells-peer-probe-v1\\n${opts.node}\\n${addr}\\n${challenge}`;\n const signature = new Uint8Array(\n await crypto.subtle.sign(\"Ed25519\", probeKey, new TextEncoder().encode(payload)),\n );\n return {\n version: 1,\n node: opts.node,\n advertise: addr,\n challenge,\n signature: bytesToHex(signature),\n };\n },\n async advertise(address: string) {\n addr = address;\n if (leaseWritten) await writeLease();\n },\n /** Clean shutdown: stop renewing, remove this node's lease record, close the store. */\n async close() {\n closed = true;\n if (renew) clearTimeout(renew);\n if (deadline) clearTimeout(deadline);\n validUntil = 0; // no write is acknowledged once closing has begun\n // A renewal already on its way would write the record back after the delete, and the\n // node's cells would stay \"owned by a live node\" for a whole lease: let it land first —\n // for at most a lease, since one that lands later expires within a lease anyway.\n let waited: ReturnType<typeof setTimeout> | undefined;\n await Promise.race([writing, new Promise((r) => (waited = setTimeout(r, ttl)))]);\n clearTimeout(waited);\n if (leaseWritten) await store.delete(`nodes/${opts.node}.json`).catch(() => {});\n store.close?.();\n },\n };\n}\n"]}
|
|
@@ -1,14 +1,13 @@
|
|
|
1
|
-
import type
|
|
1
|
+
import { type RemoteRuntime } from "../transport/cells-http.js";
|
|
2
2
|
import { type BrowserBucket, type BrowserProjectOptions } from "./browser-host.js";
|
|
3
|
-
/**
|
|
3
|
+
/**
|
|
4
|
+
* A shard of a project shared by the tabs of an origin (`joinProjectShards`): it serves through
|
|
5
|
+
* the ServiceWorker's router (webrun-http-browser's `serveHttpInWorker`), which the tab attaches
|
|
6
|
+
* it to, and reaches the other shards through it.
|
|
7
|
+
*/
|
|
4
8
|
export declare const TAB_INIT = "cellb:tab-shard";
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
export declare const TAB_WATCH = "cellb:tab-shard-watch";
|
|
8
|
-
/** From a shard worker to its tab: a call waits for a router that is gone; wake the ServiceWorker. */
|
|
9
|
-
export declare const SHARD_WAKE = "cellb:tab-shard-wake";
|
|
10
|
-
/** From a shard worker to its tab: calls into other shards are in flight; keep the ServiceWorker up. */
|
|
11
|
-
export declare const SHARD_KEEPALIVE = "cellb:tab-shard-keepalive";
|
|
9
|
+
/** The host calls between shards go to through the router: `http://cellb.internal/<project>/`. */
|
|
10
|
+
export declare const INTERNAL_ORIGIN = "http://cellb.internal";
|
|
12
11
|
/** What a shard worker shared by tabs is told: no peers, it reaches them through the router. */
|
|
13
12
|
export interface TabShardInit {
|
|
14
13
|
type: typeof TAB_INIT;
|
|
@@ -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 {
|
|
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
|
-
/**
|
|
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
|
-
|
|
23
|
-
|
|
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
|
|
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
|
|
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
|
-
|
|
58
|
-
|
|
59
|
-
|
|
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
|
-
|
|
84
|
-
|
|
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:
|
|
77
|
+
route: throughRouter(init, serviceReady),
|
|
99
78
|
});
|
|
100
|
-
|
|
101
|
-
//
|
|
102
|
-
//
|
|
103
|
-
const
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
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":"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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 { 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":"AAAA;;;;;;;;;;;GAWG;AACH,OAAO,EACL,iBAAiB,EAEjB,wBAAwB,GACzB,MAAM,qCAAqC,CAAC;AAC7C,OAAO,EAAE,OAAO,EAAE,MAAM,yBAAyB,CAAC;AAElD,OAAO,EAAE,OAAO,EAAE,MAAM,kBAAkB,CAAC;AAC3C,OAAO,EAAE,gBAAgB,EAAsB,MAAM,4BAA4B,CAAC;AAClF,OAAO,EAAE,CAAC,EAAE,WAAW,EAAE,MAAM,4BAA4B,CAAC;AAC5D,OAAO,EAAE,cAAc,EAAE,YAAY,EAAE,YAAY,EAAE,MAAM,sBAAsB,CAAC;AAClF,OAAO,EAAE,aAAa,EAAE,WAAW,EAAE,cAAc,EAAE,MAAM,iBAAiB,CAAC;AAC7E,OAAO,EAGL,qBAAqB,GACtB,MAAM,mBAAmB,CAAC;AAE3B,MAAM,IAAI,GAAG,aAAa,CAAC;AAC3B,MAAM,KAAK,GAAG,mBAAmB,CAAC;AAClC;;;;GAIG;AACH,MAAM,CAAC,MAAM,QAAQ,GAAG,iBAAiB,CAAC;AAC1C,kGAAkG;AAClG,MAAM,CAAC,MAAM,eAAe,GAAG,uBAAuB,CAAC;AAmCvD;;;;GAIG;AACH,MAAM,UAAU,iBAAiB,CAAC,IAKjC;IACC,MAAM,KAAK,GAAG,UAGb,CAAC;IACF,IAAI,IAAmE,CAAC;IACxE,IAAI,OAAsC,CAAC;IAC3C,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,CAAC,CAAC,QAAQ,CAAC,IAAc,CAAC;YAAE,OAAO;QAC9D,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,oFAAoF;oBACpF,MAAM,OAAO,EAAE,KAAK,EAAE,CAAC,KAAK,CAAC,GAAG,EAAE,GAAE,CAAC,CAAC,CAAC;oBACvC,OAAO,GAAG,SAAS,CAAC;gBACtB,CAAC;gBACD,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,IAAI,KAAsC,CAAC;gBAC3C,MAAM,YAAY,GAAG,IAAI,OAAO,CAAoB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,CAAC,CAAC;gBACxE,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,aAAa,CAAC,IAAI,EAAE,YAAY,CAAC;iBACzC,CAAC,CAAC;gBACH,wFAAwF;gBACxF,wFAAwF;gBACxF,wCAAwC;gBACxC,MAAM,OAAO,GAAG,gBAAgB,CAAC,IAAI,CAAC,OAAO,EAAE,EAAE,YAAY,EAAE,IAAI,EAAE,CAAC,CAAC;gBACvE,OAAO,GAAG,iBAAiB,CAAC,KAAK,EAAE,GAAG,EAAE,EAAE,CAAC,eAAe,CAAC,MAAM,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC;gBAChF,KAAK,CAAC,OAAO,CAAC,CAAC;gBACf,OAAO,KAAK,CAAC,WAAW,CAAC,EAAE,EAAE,EAAE,IAAI,EAAE,WAAW,EAAE,IAAI,CAAC,WAAW,EAAE,CAAC,CAAC;YACxE,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;AAED;;;GAGG;AACH,SAAS,eAAe,CAAC,GAAa;IACpC,MAAM,EAAE,GAAI,GAAqD,CAAC,SAAS,CAAC;IAC5E,IAAI,GAAG,CAAC,MAAM,KAAK,GAAG,IAAI,CAAC,EAAE;QAAE,OAAO,GAAG,CAAC;IAC1C,OAAO,wBAAwB,CAC7B,cAAc,CAAC,EAAE,CAAC,EAClB,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAChD,CAAC;AACJ,CAAC;AAED;;;;GAIG;AACH,SAAS,aAAa,CACpB,IAAqD,EACrD,OAAmC;IAEnC,MAAM,MAAM,GAAG,GAAG,eAAe,IAAI,kBAAkB,CAAC,IAAI,CAAC,IAAI,CAAC,GAAG,CAAC;IACtE,OAAO,CAAC,IAAI,EAAE,EAAE,EAAE,EAAE;QAClB,MAAM,KAAK,GAAG,OAAO,CAAC,EAAE,CAAC,QAAQ,EAAE,EAAE,IAAI,CAAC,MAAM,CAAC,CAAC;QAClD,IAAI,KAAK,KAAK,IAAI,CAAC,KAAK;YAAE,OAAO,SAAS,CAAC;QAC3C,iFAAiF;QACjF,OAAO,KAAK,EAAE,GAAG,EAAE,EAAE;YACnB,MAAM,GAAG,GAAG,MAAM,OAAO,CAAC;YAC1B,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,SAAS,CAAC,EAAE,WAAW,EAAE,KAAK,WAAW,EAAE,CAAC;gBAC9D,MAAM,SAAS,GAAG,CAAC,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,wBAAwB,CAAC,IAAI,EAAE,CAAC;qBAChE,KAAK,CAAC,GAAG,CAAC;qBACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC;qBACpB,MAAM,CAAC,OAAO,CAAC,CAAC;gBACnB,MAAM,OAAO,GAAuB,EAAE,CAAC;gBACvC,KAAK,MAAM,CAAC,CAAC,EAAE,CAAC,CAAC,IAAI,GAAG,CAAC,OAAO;oBAAE,IAAI,CAAC,CAAC,UAAU,CAAC,SAAS,CAAC;wBAAE,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,CAAC;gBACpF,OAAO,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC;gBACzD,MAAM,EAAE,IAAI,EAAE,QAAQ,EAAE,GAAG,MAAM,GAAG,CAAC,gBAAgB,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,CAAC,CAAC;gBAClF,OAAO,IAAI,aAAa,CAAC,IAAI,EAAE;oBAC7B,MAAM,EAAE,GAAG;oBACX,SAAS,EAAE,IAAI,WAAW,CAAC,IAAI,CAAC;oBAChC,OAAO,EAAE,QAAQ,CAAC,CAAC,CAAC,EAAE,wBAAwB,EAAE,QAAQ,EAAE,CAAC,CAAC,CAAC,EAAE;iBAChE,CAAC,CAAC;YACL,CAAC;YACD,MAAM,KAAK,GAAG,MAAM,OAAO,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YACzC,MAAM,OAAO,GAAG,IAAI,OAAO,CAAC,GAAG,CAAC,OAAO,CAAC,CAAC;YACzC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,MAAM,CAAC,KAAK,CAAC,CAAC,CAAC;YACpC,OAAO,CAAC,GAAG,CAAC,CAAC,CAAC,GAAG,EAAE,GAAG,CAAC,GAAG,CAAC,CAAC;YAC5B,IAAI,CAAC;gBACH,OAAO,MAAM,GAAG,CAAC,KAAK,CAAC,WAAW,CAAC,GAAG,EAAE,IAAI,OAAO,CAAC,KAAK,EAAE,EAAE,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC;YAC5E,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACb,8EAA8E;gBAC9E,kFAAkF;gBAClF,sEAAsE;gBACtE,IACE,GAAG,YAAY,KAAK;oBACpB,GAAG,CAAC,IAAI,KAAK,eAAe;oBAC5B,GAAG,CAAC,OAAO,CAAC,UAAU,CAAC,0DAA0D,CAAC;oBAElF,MAAM,IAAI,KAAK,CACb,wFAAwF,CACzF,CAAC;gBACJ,MAAM,GAAG,CAAC;YACZ,CAAC;QACH,CAAC,CAAC;IACJ,CAAC,CAAC;AACJ,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 {\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 {
|
|
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
|
-
/**
|
|
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
|
-
/**
|
|
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
|
-
|
|
112
|
+
dispatcher: SwHttpDispatcher;
|
|
103
113
|
};
|