@nimbus-sh/fabric 0.1.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 (75) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +487 -0
  3. package/dist/alarms.d.ts +134 -0
  4. package/dist/alarms.d.ts.map +1 -0
  5. package/dist/alarms.js +214 -0
  6. package/dist/bindings.d.ts +316 -0
  7. package/dist/bindings.d.ts.map +1 -0
  8. package/dist/bindings.js +678 -0
  9. package/dist/ctx-exports.d.ts +47 -0
  10. package/dist/ctx-exports.d.ts.map +1 -0
  11. package/dist/ctx-exports.js +54 -0
  12. package/dist/facet-image-store.d.ts +112 -0
  13. package/dist/facet-image-store.d.ts.map +1 -0
  14. package/dist/facet-image-store.js +181 -0
  15. package/dist/fanout-pool.d.ts +223 -0
  16. package/dist/fanout-pool.d.ts.map +1 -0
  17. package/dist/fanout-pool.js +368 -0
  18. package/dist/index.d.ts +26 -0
  19. package/dist/index.d.ts.map +1 -0
  20. package/dist/index.js +25 -0
  21. package/dist/inner-do-registry.d.ts +41 -0
  22. package/dist/inner-do-registry.d.ts.map +1 -0
  23. package/dist/inner-do-registry.js +51 -0
  24. package/dist/launch-journal.d.ts +170 -0
  25. package/dist/launch-journal.d.ts.map +1 -0
  26. package/dist/launch-journal.js +154 -0
  27. package/dist/launch-pacer.d.ts +173 -0
  28. package/dist/launch-pacer.d.ts.map +1 -0
  29. package/dist/launch-pacer.js +193 -0
  30. package/dist/loader-ledger.d.ts +57 -0
  31. package/dist/loader-ledger.d.ts.map +1 -0
  32. package/dist/loader-ledger.js +91 -0
  33. package/dist/loader-pool.d.ts +315 -0
  34. package/dist/loader-pool.d.ts.map +1 -0
  35. package/dist/loader-pool.js +666 -0
  36. package/dist/process-fabric.d.ts +524 -0
  37. package/dist/process-fabric.d.ts.map +1 -0
  38. package/dist/process-fabric.js +388 -0
  39. package/dist/process-host.d.ts +132 -0
  40. package/dist/process-host.d.ts.map +1 -0
  41. package/dist/process-host.js +444 -0
  42. package/dist/vendor/errors.d.ts +24 -0
  43. package/dist/vendor/errors.d.ts.map +1 -0
  44. package/dist/vendor/errors.js +46 -0
  45. package/dist/vendor/serialize.d.ts +3 -0
  46. package/dist/vendor/serialize.d.ts.map +1 -0
  47. package/dist/vendor/serialize.js +25 -0
  48. package/dist/vendor/types.d.ts +69 -0
  49. package/dist/vendor/types.d.ts.map +1 -0
  50. package/dist/vendor/types.js +4 -0
  51. package/dist/workerd-facet-host.d.ts +207 -0
  52. package/dist/workerd-facet-host.d.ts.map +1 -0
  53. package/dist/workerd-facet-host.js +508 -0
  54. package/dist/ws-hibernation-config.d.ts +73 -0
  55. package/dist/ws-hibernation-config.d.ts.map +1 -0
  56. package/dist/ws-hibernation-config.js +93 -0
  57. package/package.json +62 -0
  58. package/src/alarms.ts +275 -0
  59. package/src/bindings.ts +871 -0
  60. package/src/ctx-exports.ts +77 -0
  61. package/src/facet-image-store.ts +196 -0
  62. package/src/fanout-pool.ts +503 -0
  63. package/src/index.ts +26 -0
  64. package/src/inner-do-registry.ts +58 -0
  65. package/src/launch-journal.ts +229 -0
  66. package/src/launch-pacer.ts +231 -0
  67. package/src/loader-ledger.ts +112 -0
  68. package/src/loader-pool.ts +984 -0
  69. package/src/process-fabric.ts +729 -0
  70. package/src/process-host.ts +566 -0
  71. package/src/vendor/errors.ts +56 -0
  72. package/src/vendor/serialize.ts +37 -0
  73. package/src/vendor/types.ts +75 -0
  74. package/src/workerd-facet-host.ts +694 -0
  75. package/src/ws-hibernation-config.ts +123 -0
@@ -0,0 +1,444 @@
1
+ /**
2
+ * process-host.ts — the two substrates a resident process can run on, and the
3
+ * one value that picks between them.
4
+ *
5
+ * `process-fabric.ts` owns what a resident process IS. This module
6
+ * owns only where it lives, behind `ProcessHost`:
7
+ *
8
+ * facet — the process is a named child actor of the user's own session DO.
9
+ * peer — the process is a named child actor of a SIBLING session DO, and
10
+ * the coordinator reaches it over one held-open RPC.
11
+ *
12
+ * Both call the same `openResidentFacet`. The peer leg is not a second process
13
+ * implementation; it is the same call made on a different actor, which is why
14
+ * the runner, the boot spec, the class name, the writer handshake, the start
15
+ * contract and the lifecycle are shared code rather than parallel paths.
16
+ *
17
+ * Choosing between them
18
+ * ─────────────────────
19
+ * One value for the whole deployment, resolved by the embedder's config seam
20
+ * (Nimbus reads `NIMBUS_PROCESS_HOST` in the worker's own selector) and
21
+ * handed to `createProcessHost`. No spawn site chooses; no program name, mode
22
+ * or payload size reaches the choice. If a decision about a particular
23
+ * process ever appears here, the heavy/light classifier has grown back and
24
+ * should be deleted again.
25
+ *
26
+ * | | spawn | memory | CPU | SQLite |
27
+ * |----------|------------|-------------|------------|--------|
28
+ * | facet | 8-16 ms | independent | SHARED | own |
29
+ * | peer | 242-359 ms | independent | independent| own |
30
+ *
31
+ * Facet CPU is shared because facets are separate isolates inside ONE actor
32
+ * thread: awaiting I/O yields it completely (measured 0 ms of sibling impact),
33
+ * but a non-yielding loop stalls every sibling for its full duration
34
+ * (measured 6,852 ms). A peer pays ~20x the spawn cost to buy that back.
35
+ *
36
+ * What the peer leg has to do differently, and why none of it reaches the
37
+ * process
38
+ * ────────────────────────────────────────────────────────────────────────
39
+ * payloads — a whole structured-clone RPC value is capped at 32 MiB, and
40
+ * pi's node snapshot alone serializes to 44,252,709 bytes. Boot
41
+ * specs name their large members BY PATH, so what crosses is a
42
+ * path and the host reads the bytes off the coordinator's disk in
43
+ * 4 MiB ranges through the supervisor. Nothing large is ever an
44
+ * RPC argument, so nothing has to be streamed or replayed.
45
+ * requests — workerd refuses to transfer an object owned by a
46
+ * dynamically-loaded worker across a sibling-DO hop, so a request
47
+ * travels to the peer as PARTS and the response comes back as
48
+ * parts, both with plain `ReadableStream` bodies that RPC carries
49
+ * with flow control. A live SSE body still streams; nothing is
50
+ * buffered.
51
+ * liveness — the host leg is held open for the process's whole life, which
52
+ * is also what keeps the hosting DO resident. A peer therefore
53
+ * never outlives its coordinator: the coordinator dying cancels
54
+ * the inbound call and the facet dies with it. Nothing in this
55
+ * fabric arms an alarm, on either substrate.
56
+ *
57
+ * WebSockets are the one thing on that list the RPC path cannot carry at all.
58
+ * A 101 Response owns a live socket, and RPC's Request/Response transport
59
+ * reconstructs a value rather than handing the socket over, so an upgrade
60
+ * stays on FETCH semantics for every hop: a facet is fetched directly, and a
61
+ * peer is fetched as a service binding which then fetches its hosted facet.
62
+ * Two headers carry what the RPC arguments would have — see
63
+ * {@link HOSTED_WEBSOCKET_KEY_HEADER} — and a per-process capability makes
64
+ * that pair unforgeable by anything that did not open the process.
65
+ */
66
+ import { disposeRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
67
+ import { isTransientDoReset } from '@nimbus-sh/core/observability/oom-classify.js';
68
+ import { PEER_RETRY_BACKOFF_MS, PEER_TRANSIENT_RESET_RETRIES } from './fanout-pool.js';
69
+ import { DYNAMIC_WORKER_CODE_LIMIT_BYTES, openResidentFacet, residentFacetName, runOneShotWorker, } from './workerd-facet-host.js';
70
+ import { BindingError } from './vendor/errors.js';
71
+ /**
72
+ * The substrate for this deployment, resolved once. The mode arrives already
73
+ * decided — the embedder owns the config var that picks it, and refuses an
74
+ * unrecognized value there rather than defaulting, because a typo that
75
+ * silently kept the old substrate would make an operator's comparison a lie.
76
+ * `disk` is the coordinator's own filesystem reader; the peer host does not
77
+ * take it, because a peer reads the same disk through the supervisor instead.
78
+ */
79
+ export function createProcessHost(mode, ctx, env, disk) {
80
+ return mode === 'peer'
81
+ ? new PeerProcessHost(ctx, env)
82
+ : new FacetProcessHost(ctx, env, disk);
83
+ }
84
+ // ── facet: the process is a child of the user's own session DO ──────────────
85
+ class FacetProcessHost {
86
+ ctx;
87
+ disk;
88
+ /**
89
+ * The process shares its session's Durable Object, so the session's own
90
+ * store is reachable by copy-on-write — and its storage budget is the same
91
+ * budget. Both halves of that follow from the one fact, and neither is
92
+ * optional.
93
+ */
94
+ imageDelivery = {
95
+ reflink: 'same-object',
96
+ moduleCeilingBytes: DYNAMIC_WORKER_CODE_LIMIT_BYTES,
97
+ storageSharedWithSession: true,
98
+ };
99
+ env;
100
+ coordDoId;
101
+ constructor(ctx, env, disk) {
102
+ this.ctx = ctx;
103
+ this.disk = disk;
104
+ this.env = (env ?? {});
105
+ this.coordDoId = ctx.id.toString();
106
+ }
107
+ runOnce(params, consume) {
108
+ return runOneShotWorker(this.ctx, this.env, { doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
109
+ }
110
+ async open(params) {
111
+ const supervisor = {
112
+ doId: this.coordDoId,
113
+ pid: params.pid,
114
+ writerId: params.writerId,
115
+ };
116
+ const { slot, ...facet } = openResidentFacet(this.ctx, this.env, this.disk, supervisor, params);
117
+ return {
118
+ ...facet,
119
+ describe: () => `facet '${residentFacetName(slot)}' (pid ${params.pid})`
120
+ + ` of session ${this.coordDoId.slice(-12)}`
121
+ + `; ${describeImageDelivery(this.imageDelivery)}`,
122
+ };
123
+ }
124
+ }
125
+ /**
126
+ * The operator-facing half of the substrate difference, on the one line an
127
+ * operator actually reads. A comment in this file would not have told anyone
128
+ * who flipped the config that the image path changed under them.
129
+ */
130
+ function describeImageDelivery(delivery) {
131
+ return `fs image: reflink ${delivery.reflink}, module map ≤ ${delivery.moduleCeilingBytes}B, `
132
+ + `storage ${delivery.storageSharedWithSession ? 'shared with the session' : 'its own'}`;
133
+ }
134
+ // ── peer: the process is a child of a sibling session DO ────────────────────
135
+ /**
136
+ * How many sibling names to try before accepting one that co-located with a
137
+ * process already running. Measured: 1 shared pair in 24 fresh peers.
138
+ */
139
+ const PEER_PLACEMENT_MAX_ATTEMPTS = 4;
140
+ /**
141
+ * This workerd process's identity. Module scope, so two Durable Objects
142
+ * reporting the same token are in the same process — which is exactly the CPU
143
+ * sharing a peer exists to avoid, and the only way to detect it.
144
+ */
145
+ let _isolateToken = null;
146
+ export function isolateToken() {
147
+ if (!_isolateToken)
148
+ _isolateToken = crypto.randomUUID();
149
+ return _isolateToken;
150
+ }
151
+ /**
152
+ * Every method a hosting sibling must answer. Checked as a set at first
153
+ * contact: a stub that has the probe but not the router is a deployment skew,
154
+ * and finding that out mid-process is finding it out too late.
155
+ */
156
+ const PROCESS_PEER_METHODS = [
157
+ '_rpcProcessHostProbe',
158
+ '_rpcHostProcess',
159
+ '_rpcAwaitHostedOpen',
160
+ '_rpcAwaitHostedBoot',
161
+ '_rpcRouteHostedHttp',
162
+ '_rpcCancelHostProcess',
163
+ ];
164
+ /**
165
+ * Which hosted process a fetched upgrade is for. An upgrade cannot travel as
166
+ * RPC arguments, so the two values `_rpcRouteHostedHttp` would have taken ride
167
+ * as headers on the peer fetch instead.
168
+ *
169
+ * The key alone is guessable from a pid, so it is not enough on its own; the
170
+ * capability is minted per `open()` and known only to the coordinator that
171
+ * opened the process and the peer that hosts it. The receiving session strips
172
+ * both before the request reaches the process.
173
+ */
174
+ export const HOSTED_WEBSOCKET_KEY_HEADER = 'x-nimbus-hosted-websocket';
175
+ export const HOSTED_WEBSOCKET_CAPABILITY_HEADER = 'x-nimbus-hosted-websocket-capability';
176
+ function peerNamespace(env) {
177
+ const ns = (typeof env === 'object' || typeof env === 'function') && env !== null
178
+ ? Reflect.get(env, 'NIMBUS_SESSION')
179
+ : undefined;
180
+ if ((typeof ns !== 'object' && typeof ns !== 'function') || ns === null
181
+ || typeof Reflect.get(ns, 'idFromName') !== 'function'
182
+ || typeof Reflect.get(ns, 'get') !== 'function') {
183
+ throw new BindingError('ProcessFabric: env.NIMBUS_SESSION binding missing or invalid. '
184
+ + "NIMBUS_PROCESS_HOST='peer' hosts every resident process on a sibling "
185
+ + 'Durable Object; add the binding via durable_objects.bindings in wrangler.jsonc.');
186
+ }
187
+ return ns;
188
+ }
189
+ function processPeerStub(value, peerName) {
190
+ if ((typeof value !== 'object' && typeof value !== 'function') || value === null) {
191
+ throw new BindingError(`ProcessFabric: NIMBUS_SESSION.get() returned no stub for peer '${peerName}'.`);
192
+ }
193
+ for (const method of PROCESS_PEER_METHODS) {
194
+ if (typeof Reflect.get(value, method) !== 'function') {
195
+ throw new BindingError(`ProcessFabric: peer '${peerName}' does not expose ${method}(). `
196
+ + 'A sibling that answers some of the host-process surface and not the rest '
197
+ + 'would fail somewhere inside a running process instead of here.');
198
+ }
199
+ }
200
+ return value;
201
+ }
202
+ class PeerProcessHost {
203
+ ctx;
204
+ /**
205
+ * A peer buys independent CPU and its own storage budget, and pays for both
206
+ * with the image path: nothing crosses a Durable Object boundary by
207
+ * reference, so the whole session filesystem has to travel as bytes.
208
+ */
209
+ imageDelivery = {
210
+ reflink: 'impossible',
211
+ moduleCeilingBytes: DYNAMIC_WORKER_CODE_LIMIT_BYTES,
212
+ storageSharedWithSession: false,
213
+ };
214
+ ns;
215
+ env;
216
+ coordDoId;
217
+ /** pid → the isolate token of the peer currently hosting that process. */
218
+ tokensInUse = new Map();
219
+ constructor(ctx, env) {
220
+ this.ctx = ctx;
221
+ this.ns = peerNamespace(env);
222
+ this.env = (env ?? {});
223
+ this.coordDoId = ctx.id.toString();
224
+ }
225
+ /**
226
+ * Not placed on a sibling, and that is not a gap in this substrate.
227
+ * `peer` exists to buy a resident process independent CPU; a program that
228
+ * ends with the call it was started by has no residency to place, and its
229
+ * map is fully inline — so a hop would meet the RPC ceiling that by-path
230
+ * boot specs exist to avoid while buying nothing. It runs as a dynamic
231
+ * worker of the coordinator here exactly as it does on `facet`.
232
+ */
233
+ runOnce(params, consume) {
234
+ return runOneShotWorker(this.ctx, this.env, { doId: this.coordDoId, pid: params.pid, writerId: params.writerId }, params, consume);
235
+ }
236
+ async open(params) {
237
+ const placement = await this._place(params.pid);
238
+ this.tokensInUse.set(params.pid, placement.isolateToken);
239
+ // Minted per open, held only by this coordinator and the peer that hosts
240
+ // the process. The workerKey is derivable from a pid; this is not.
241
+ const webSocketCapability = crypto.randomUUID();
242
+ // Held open for the process's whole life: it is the lifecycle, and it is
243
+ // also what keeps the hosting DO resident. It settles when a `lifetime`
244
+ // runner exits or when a `boot` runner's host is cancelled, and rejects if
245
+ // the peer dies under either.
246
+ const hostLeg = placement.stub._rpcHostProcess(params.boot, {
247
+ coordinatorDoId: this.coordDoId,
248
+ pid: params.pid,
249
+ writerId: params.writerId,
250
+ workerKey: params.workerKey,
251
+ webSocketCapability,
252
+ startArgs: params.startArgs,
253
+ });
254
+ hostLeg.catch(() => { });
255
+ // The peer starts the runner as part of hosting it; this reads back that
256
+ // one boot payload without re-running anything, so `started` means exactly
257
+ // what it means on a facet. Racing the host leg is what turns a peer that
258
+ // died before the boot landed into a rejection rather than a hang — the
259
+ // peer that would have answered is the thing that is gone.
260
+ const booted = placement.stub._rpcAwaitHostedBoot(params.workerKey).then((r) => r.payload);
261
+ booted.catch(() => { });
262
+ const started = Promise.race([booted, hostLeg.then(() => booted)]);
263
+ started.catch(() => { });
264
+ // Do not return a handle for a process that was never opened. Opening a
265
+ // facet of one's own DO either throws or does not, before any handle
266
+ // exists; this is that same moment, one hop away.
267
+ try {
268
+ await Promise.race([
269
+ placement.stub._rpcAwaitHostedOpen(params.workerKey),
270
+ hostLeg.then(() => ({ ok: true })),
271
+ ]);
272
+ }
273
+ catch (error) {
274
+ this.tokensInUse.delete(params.pid);
275
+ // Awaited, not fired off: a spawn that rejects must mean nothing was
276
+ // left running, which is what a throw from `openResidentFacet` means on
277
+ // the other substrate.
278
+ try {
279
+ await this._cancel(params.workerKey, placement.peerName);
280
+ }
281
+ finally {
282
+ disposeRpcResource(placement.stub);
283
+ }
284
+ throw error;
285
+ }
286
+ let released = false;
287
+ return {
288
+ started,
289
+ // The held leg IS the process's residency here. It settles cleanly when
290
+ // the coordinator releases; anything else is the host going away under a
291
+ // process that was up, and the fabric ends the process on it.
292
+ lost: hostLeg.then(() => new Promise(() => { })),
293
+ handleHttpRequest: (request) => routeThroughPeer(placement.stub, params.workerKey, request),
294
+ handleWebSocketRequest: (request) => routeWebSocketThroughPeer(placement.stub, params.workerKey, webSocketCapability, request),
295
+ release: async () => {
296
+ if (released)
297
+ return;
298
+ released = true;
299
+ this.tokensInUse.delete(params.pid);
300
+ try {
301
+ await this._cancel(params.workerKey, placement.peerName);
302
+ }
303
+ finally {
304
+ disposeRpcResource(placement.stub);
305
+ }
306
+ },
307
+ describe: () => `peer '${placement.peerName}' (isolate ${placement.isolateToken.slice(0, 8)})`
308
+ + `; ${describeImageDelivery(this.imageDelivery)}`,
309
+ };
310
+ }
311
+ /**
312
+ * Probe successive sibling names until one reports an isolate token distinct
313
+ * from this coordinator's and from every peer already hosting a process. A
314
+ * peer that co-located bought nothing — it shares the CPU it was chosen to
315
+ * escape — so placement verifies rather than assumes.
316
+ */
317
+ async _place(pid) {
318
+ const denied = new Set([isolateToken(), ...this.tokensInUse.values()]);
319
+ for (let attempt = 0; attempt < PEER_PLACEMENT_MAX_ATTEMPTS - 1; attempt++) {
320
+ const candidate = await this._probePlacement(pid, attempt);
321
+ if (!denied.has(candidate.isolateToken))
322
+ return candidate;
323
+ disposeRpcResource(candidate.stub);
324
+ }
325
+ // Every attempt co-located, which happens in single-process topologies. Use
326
+ // the last one anyway: it runs the process correctly, it just shares the
327
+ // CPU it was chosen to escape, and the placement line names the isolate it
328
+ // landed in so that is visible rather than assumed.
329
+ return this._probePlacement(pid, PEER_PLACEMENT_MAX_ATTEMPTS - 1);
330
+ }
331
+ /** One sibling name, resolved and probed. Leaks nothing on failure. */
332
+ async _probePlacement(pid, attempt) {
333
+ const peerName = `${this.coordDoId}:proc:${pid}:${attempt}`;
334
+ let resource;
335
+ try {
336
+ resource = this.ns.get(this.ns.idFromName(peerName));
337
+ const stub = processPeerStub(resource, peerName);
338
+ const probe = await this._probe(stub, peerName);
339
+ return { stub, peerName, isolateToken: probe.isolateToken };
340
+ }
341
+ catch (error) {
342
+ disposeRpcResource(resource);
343
+ throw error;
344
+ }
345
+ }
346
+ /**
347
+ * First contact with a possibly-cold sibling DO: retry transient platform
348
+ * resets with the same bounded policy the fanout peers ship. Non-transient
349
+ * failures propagate on the first hit.
350
+ */
351
+ async _probe(stub, peerName) {
352
+ for (let attempt = 0;; attempt++) {
353
+ try {
354
+ const probe = await stub._rpcProcessHostProbe();
355
+ if (!probe || typeof probe.isolateToken !== 'string' || probe.isolateToken.length === 0) {
356
+ throw new Error(`ProcessFabric: peer '${peerName}' returned no isolate token`);
357
+ }
358
+ return probe;
359
+ }
360
+ catch (err) {
361
+ if (attempt < PEER_TRANSIENT_RESET_RETRIES && isTransientDoReset(err)) {
362
+ await new Promise((r) => setTimeout(r, PEER_RETRY_BACKOFF_MS[Math.min(attempt, PEER_RETRY_BACKOFF_MS.length - 1)]));
363
+ continue;
364
+ }
365
+ throw err;
366
+ }
367
+ }
368
+ }
369
+ /**
370
+ * A fresh stub to the hosting peer fires `_rpcCancelHostProcess`, which
371
+ * releases the peer's facet — the exact teardown a local facet gets — and
372
+ * only answers once it has. Awaiting it is what makes "released" mean the
373
+ * same thing on both substrates, so the writer identity is retired at the
374
+ * same point in the process's life either way. Also handed to `waitUntil`,
375
+ * so a kill nobody awaited still completes.
376
+ */
377
+ _cancel(workerKey, peerName) {
378
+ let stub;
379
+ try {
380
+ stub = processPeerStub(this.ns.get(this.ns.idFromName(peerName)), peerName);
381
+ }
382
+ catch {
383
+ return Promise.resolve();
384
+ }
385
+ const cancelled = Promise.resolve(stub._rpcCancelHostProcess(workerKey))
386
+ .then(() => undefined)
387
+ .catch(() => { })
388
+ .finally(() => disposeRpcResource(stub));
389
+ this.ctx.waitUntil(cancelled);
390
+ return cancelled;
391
+ }
392
+ }
393
+ async function routeThroughPeer(stub, workerKey, request) {
394
+ const result = await stub._rpcRouteHostedHttp(workerKey, {
395
+ method: request.method,
396
+ url: request.url,
397
+ headers: headerPairs(request.headers),
398
+ body: request.method === 'GET' || request.method === 'HEAD' ? null : request.body,
399
+ });
400
+ const responseHeaders = new Headers();
401
+ for (const [key, value] of result.headers)
402
+ responseHeaders.append(key, value);
403
+ return new Response(result.body, {
404
+ status: result.status,
405
+ statusText: result.statusText,
406
+ headers: responseHeaders,
407
+ });
408
+ }
409
+ /**
410
+ * The upgrade hop. `stub.fetch` is a service-binding fetch, so the peer's
411
+ * `Response` — socket and all — is handed back rather than reconstructed,
412
+ * which is the whole reason this is not an RPC.
413
+ */
414
+ function routeWebSocketThroughPeer(stub, workerKey, capability, request) {
415
+ const headers = new Headers(request.headers);
416
+ headers.set(HOSTED_WEBSOCKET_KEY_HEADER, workerKey);
417
+ headers.set(HOSTED_WEBSOCKET_CAPABILITY_HEADER, capability);
418
+ return stub.fetch(new Request(request.url, { method: request.method, headers }));
419
+ }
420
+ /**
421
+ * Headers as pairs, with every `Set-Cookie` kept separate.
422
+ *
423
+ * Iterating a `Headers` combines same-named fields into one comma-joined
424
+ * value, and for `Set-Cookie` that is not reversible — `append` cannot split
425
+ * `a=1; Path=/, b=2; Path=/` back into two cookies, and a browser reading the
426
+ * merged form sets one malformed cookie instead of two. Every other field
427
+ * combines by comma legally, so only this one needs the separate accessor.
428
+ * A user's server setting two cookies must not depend on which substrate its
429
+ * process happened to run on.
430
+ */
431
+ export function headerPairs(headers) {
432
+ const pairs = [];
433
+ headers.forEach((value, key) => {
434
+ if (key.toLowerCase() !== 'set-cookie')
435
+ pairs.push([key, value]);
436
+ });
437
+ const getSetCookie = headers.getSetCookie;
438
+ const cookies = typeof getSetCookie === 'function'
439
+ ? getSetCookie.call(headers)
440
+ : (headers.get('set-cookie') ? [headers.get('set-cookie')] : []);
441
+ for (const cookie of cookies)
442
+ pairs.push(['set-cookie', cookie]);
443
+ return pairs;
444
+ }
@@ -0,0 +1,24 @@
1
+ export declare class ParallelError extends Error {
2
+ constructor(message: string);
3
+ }
4
+ export declare class SerializationError extends ParallelError {
5
+ constructor(message: string);
6
+ }
7
+ export declare class ExecutionError extends ParallelError {
8
+ readonly remoteMessage: string;
9
+ readonly remoteStack?: string;
10
+ constructor(message: string, remoteStack?: string);
11
+ }
12
+ export declare class TimeoutError extends ParallelError {
13
+ readonly deadlineMs: number;
14
+ constructor(deadlineMs: number);
15
+ }
16
+ export declare class RetryExhaustedError extends ParallelError {
17
+ readonly lastError: Error;
18
+ readonly attempts: number;
19
+ constructor(attempts: number, lastError: Error);
20
+ }
21
+ export declare class BindingError extends ParallelError {
22
+ constructor(message: string);
23
+ }
24
+ //# sourceMappingURL=errors.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"errors.d.ts","sourceRoot":"","sources":["../../src/vendor/errors.ts"],"names":[],"mappings":"AAAA,qBAAa,aAAc,SAAQ,KAAK;gBAC1B,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,kBAAmB,SAAQ,aAAa;gBACvC,OAAO,EAAE,MAAM;CAI5B;AAED,qBAAa,cAAe,SAAQ,aAAa;IAC/C,QAAQ,CAAC,aAAa,EAAE,MAAM,CAAC;IAC/B,QAAQ,CAAC,WAAW,CAAC,EAAE,MAAM,CAAC;gBAElB,OAAO,EAAE,MAAM,EAAE,WAAW,CAAC,EAAE,MAAM;CAMlD;AAED,qBAAa,YAAa,SAAQ,aAAa;IAC7C,QAAQ,CAAC,UAAU,EAAE,MAAM,CAAC;gBAEhB,UAAU,EAAE,MAAM;CAK/B;AAED,qBAAa,mBAAoB,SAAQ,aAAa;IACpD,QAAQ,CAAC,SAAS,EAAE,KAAK,CAAC;IAC1B,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;gBAEd,QAAQ,EAAE,MAAM,EAAE,SAAS,EAAE,KAAK;CAQ/C;AAED,qBAAa,YAAa,SAAQ,aAAa;gBACjC,OAAO,EAAE,MAAM;CAI5B"}
@@ -0,0 +1,46 @@
1
+ export class ParallelError extends Error {
2
+ constructor(message) {
3
+ super(message);
4
+ this.name = 'ParallelError';
5
+ }
6
+ }
7
+ export class SerializationError extends ParallelError {
8
+ constructor(message) {
9
+ super(message);
10
+ this.name = 'SerializationError';
11
+ }
12
+ }
13
+ export class ExecutionError extends ParallelError {
14
+ remoteMessage;
15
+ remoteStack;
16
+ constructor(message, remoteStack) {
17
+ super(message);
18
+ this.name = 'ExecutionError';
19
+ this.remoteMessage = message;
20
+ this.remoteStack = remoteStack;
21
+ }
22
+ }
23
+ export class TimeoutError extends ParallelError {
24
+ deadlineMs;
25
+ constructor(deadlineMs) {
26
+ super(`Task exceeded ${deadlineMs}ms deadline`);
27
+ this.name = 'TimeoutError';
28
+ this.deadlineMs = deadlineMs;
29
+ }
30
+ }
31
+ export class RetryExhaustedError extends ParallelError {
32
+ lastError;
33
+ attempts;
34
+ constructor(attempts, lastError) {
35
+ super(`Task failed after ${attempts} attempt(s): ${lastError.message}`);
36
+ this.name = 'RetryExhaustedError';
37
+ this.attempts = attempts;
38
+ this.lastError = lastError;
39
+ }
40
+ }
41
+ export class BindingError extends ParallelError {
42
+ constructor(message) {
43
+ super(message);
44
+ this.name = 'BindingError';
45
+ }
46
+ }
@@ -0,0 +1,3 @@
1
+ export declare function serializeFunction(fn: Function): string;
2
+ export declare function hashSource(source: string): string;
3
+ //# sourceMappingURL=serialize.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"serialize.d.ts","sourceRoot":"","sources":["../../src/vendor/serialize.ts"],"names":[],"mappings":"AAEA,wBAAgB,iBAAiB,CAAC,EAAE,EAAE,QAAQ,GAAG,MAAM,CAyBtD;AAGD,wBAAgB,UAAU,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,CAMjD"}
@@ -0,0 +1,25 @@
1
+ import { SerializationError } from './errors.js';
2
+ export function serializeFunction(fn) {
3
+ if (typeof fn !== 'function') {
4
+ throw new SerializationError(`Expected a function, got ${typeof fn}`);
5
+ }
6
+ const source = fn.toString();
7
+ if (source.includes('[native code]')) {
8
+ throw new SerializationError(`Cannot serialize native function: ${fn.name || '(anonymous)'}. ` +
9
+ 'Only user-defined functions can be dispatched to remote isolates.');
10
+ }
11
+ // `this` has no receiver in the remote isolate — reject it early.
12
+ if (/\bthis\b/.test(source)) {
13
+ throw new SerializationError(`Function "${fn.name || '(anonymous)'}" references \`this\`, which is ` +
14
+ 'not available in a remote isolate. Pass values as explicit arguments instead.');
15
+ }
16
+ return source;
17
+ }
18
+ // djb2 hash — fast, deterministic, not cryptographic. Used only for loader cache key differentiation.
19
+ export function hashSource(source) {
20
+ let hash = 5381;
21
+ for (let i = 0; i < source.length; i++) {
22
+ hash = ((hash << 5) + hash + source.charCodeAt(i)) | 0;
23
+ }
24
+ return (hash >>> 0).toString(36);
25
+ }
@@ -0,0 +1,69 @@
1
+ export interface JsModule {
2
+ js: string;
3
+ }
4
+ export interface CjsModule {
5
+ cjs: string;
6
+ }
7
+ export interface PyModule {
8
+ py: string;
9
+ }
10
+ export interface TextModule {
11
+ text: string;
12
+ }
13
+ export interface DataModule {
14
+ data: ArrayBuffer;
15
+ }
16
+ export interface JsonModule {
17
+ json: unknown;
18
+ }
19
+ /**
20
+ * A compiled WebAssembly module, importable from a facet by module name.
21
+ * Nimbus ships these on every path — sql.js for node:sqlite, the interpreter
22
+ * images for python and ruby — so the omission here was the type lagging the
23
+ * API, not a kind the loader lacks.
24
+ */
25
+ export interface WasmModule {
26
+ wasm: ArrayBuffer;
27
+ }
28
+ /** Plain string = type inferred from file extension (.js or .py). */
29
+ export type ModuleContent = string | JsModule | CjsModule | PyModule | TextModule | DataModule | JsonModule | WasmModule;
30
+ export interface WorkerCode {
31
+ compatibilityDate: string;
32
+ compatibilityFlags?: string[];
33
+ allowExperimental?: boolean;
34
+ mainModule: string;
35
+ modules: Record<string, ModuleContent>;
36
+ env?: Record<string, unknown>;
37
+ /**
38
+ * Network access control:
39
+ * - `undefined`: inherit parent's network access.
40
+ * - `null`: block all outbound (fetch/connect throw).
41
+ * - A service stub: redirect outbound through that binding.
42
+ */
43
+ globalOutbound?: ServiceStub | null;
44
+ tails?: ServiceStub[];
45
+ }
46
+ export interface ServiceStub {
47
+ fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
48
+ }
49
+ export interface EntrypointOptions {
50
+ props?: Record<string, unknown>;
51
+ }
52
+ export interface WorkerStub {
53
+ getEntrypoint(): EntrypointStub;
54
+ getEntrypoint(name: string, opts?: EntrypointOptions): EntrypointStub;
55
+ }
56
+ export interface EntrypointStub {
57
+ fetch(input: RequestInfo, init?: RequestInit): Promise<Response>;
58
+ execute(...args: unknown[]): Promise<unknown>;
59
+ }
60
+ export type GetCodeCallback = () => Promise<WorkerCode>;
61
+ export interface WorkerLoader {
62
+ /**
63
+ * Load or reuse an isolate by ID. Same ID + same code may reuse an isolate;
64
+ * different IDs always get fresh isolates. `getCodeCallback` must return
65
+ * the same content for the same ID.
66
+ */
67
+ get(id: string, getCodeCallback: GetCodeCallback): WorkerStub;
68
+ }
69
+ //# sourceMappingURL=types.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"types.d.ts","sourceRoot":"","sources":["../../src/vendor/types.ts"],"names":[],"mappings":"AAIA,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,SAAS;IAAG,GAAG,EAAE,MAAM,CAAA;CAAE;AAC1C,MAAM,WAAW,QAAQ;IAAG,EAAE,EAAE,MAAM,CAAA;CAAE;AACxC,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,MAAM,CAAA;CAAE;AAC5C,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,CAAA;CAAE;AACjD,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,OAAO,CAAA;CAAE;AAC7C;;;;;GAKG;AACH,MAAM,WAAW,UAAU;IAAG,IAAI,EAAE,WAAW,CAAA;CAAE;AAEjD,qEAAqE;AACrE,MAAM,MAAM,aAAa,GACrB,MAAM,GACN,QAAQ,GACR,SAAS,GACT,QAAQ,GACR,UAAU,GACV,UAAU,GACV,UAAU,GACV,UAAU,CAAC;AAEf,MAAM,WAAW,UAAU;IACzB,iBAAiB,EAAE,MAAM,CAAC;IAC1B,kBAAkB,CAAC,EAAE,MAAM,EAAE,CAAC;IAC9B,iBAAiB,CAAC,EAAE,OAAO,CAAC;IAC5B,UAAU,EAAE,MAAM,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,aAAa,CAAC,CAAC;IACvC,GAAG,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;IAE9B;;;;;OAKG;IACH,cAAc,CAAC,EAAE,WAAW,GAAG,IAAI,CAAC;IACpC,KAAK,CAAC,EAAE,WAAW,EAAE,CAAC;CACvB;AAED,MAAM,WAAW,WAAW;IAC1B,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;CAClE;AAED,MAAM,WAAW,iBAAiB;IAChC,KAAK,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,CAAC;CACjC;AAED,MAAM,WAAW,UAAU;IACzB,aAAa,IAAI,cAAc,CAAC;IAChC,aAAa,CAAC,IAAI,EAAE,MAAM,EAAE,IAAI,CAAC,EAAE,iBAAiB,GAAG,cAAc,CAAC;CACvE;AAED,MAAM,WAAW,cAAc;IAC7B,KAAK,CAAC,KAAK,EAAE,WAAW,EAAE,IAAI,CAAC,EAAE,WAAW,GAAG,OAAO,CAAC,QAAQ,CAAC,CAAC;IACjE,OAAO,CAAC,GAAG,IAAI,EAAE,OAAO,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;CAC/C;AAED,MAAM,MAAM,eAAe,GAAG,MAAM,OAAO,CAAC,UAAU,CAAC,CAAC;AAExD,MAAM,WAAW,YAAY;IAC3B;;;;OAIG;IACH,GAAG,CAAC,EAAE,EAAE,MAAM,EAAE,eAAe,EAAE,eAAe,GAAG,UAAU,CAAC;CAC/D"}
@@ -0,0 +1,4 @@
1
+ // Types for the Cloudflare Worker Loader API (closed beta).
2
+ // Not yet in @cloudflare/workers-types. Remove this file when they ship.
3
+ // https://developers.cloudflare.com/workers/runtime-apis/bindings/worker-loader/
4
+ export {};