@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,678 @@
1
+ /**
2
+ * session/bindings.ts — Inner-Worker + assets binding shims (W10).
3
+ *
4
+ * `nimbus-wrangler dev` runs a USER worker as a child process. That
5
+ * child needs working `env` bindings (env.ASSETS, env.LOADER, env.MY_DO,
6
+ * etc.) but the DO's `env` belongs to the supervisor's contract — we
7
+ * can't pass it through directly. Workerd's enable_ctx_exports
8
+ * (compat date 2026-04-01+) auto-populates Service Bindings from
9
+ * top-level WorkerEntrypoint classes; these classes ARE those entry
10
+ * points. They forward each binding kind back to the supervisor DO via
11
+ * RPC stub (`env.NIMBUS_SESSION.idFromString(doId).get()...`).
12
+ *
13
+ * The shims have NO interaction with NimbusSession internals except
14
+ * through that RPC stub. Co-located here for grep-ability.
15
+ *
16
+ * NimbusAssetsRPC, NimbusLoaderRPC, NimbusLoadedWorker,
17
+ * NimbusLoadedEntrypoint, NimbusDurableObjectNamespace and NimbusDOStub are
18
+ * public API of the embedder's Worker: wrangler resolves them by class name
19
+ * and ctx.exports auto-populates them by export name, so the embedder's entry
20
+ * module re-exports them under exactly these names.
21
+ *
22
+ * Bundle-graph note: these classes must remain reachable from the embedder's
23
+ * entry module for Wrangler to bundle the WorkerEntrypoint exports.
24
+ */
25
+ import { WorkerEntrypoint } from 'cloudflare:workers';
26
+ import { z } from 'zod/v4';
27
+ import { disposeRpcResource, useRpcResource } from '@nimbus-sh/core/_shared/rpc-dispose.js';
28
+ import { supervisorEntrypoint, supervisorEntrypointName } from './ctx-exports.js';
29
+ import { requireStagedBootAssembler } from './process-fabric.js';
30
+ import { assertModuleMapWithinCodeLimit } from './workerd-facet-host.js';
31
+ /**
32
+ * `ctx.exports` — workerd's loopback bag, which the installed
33
+ * @cloudflare/workers-types does not put on `ExecutionContext`. Probed rather
34
+ * than declared, so a runtime that predates it reads as absent, which is also
35
+ * what the module-level holder's fallback keys off.
36
+ */
37
+ function ctxExportsOf(ctx) {
38
+ if (!ctx || typeof ctx !== 'object' || !('exports' in ctx))
39
+ return undefined;
40
+ return ctx.exports;
41
+ }
42
+ /**
43
+ * The loopback factories a hop may need. An absent bag reads as an empty set,
44
+ * which each hop reports as the specific class it could not find.
45
+ */
46
+ function shimCtxExports(ctx) {
47
+ const exports = ctxExportsOf(ctx);
48
+ if (!exports || typeof exports !== 'object')
49
+ return {};
50
+ // The bag workerd populated. Which names are in it is the hop's question,
51
+ // and each hop answers it with its own error.
52
+ return exports;
53
+ }
54
+ /**
55
+ * Assets binding shim. The inner Worker calls `env.ASSETS.fetch(request)`
56
+ * and we serve the file from VFS under `<vfsRoot>/<assetsDir>/<pathname>`.
57
+ *
58
+ * Props (passed via ctx.props when this binding is constructed):
59
+ * vfsRoot — project root in VFS (e.g. "home/user/myapp")
60
+ * assetsDir — directory declared in wrangler.jsonc.assets.directory
61
+ * (e.g. "./public" → we trim the leading ./)
62
+ *
63
+ * The hostname on the incoming Request is irrelevant (Workers Assets
64
+ * convention); only pathname matters. Path traversal (`..`) is clamped.
65
+ * Directories resolve to their `index.html` child; missing files fall
66
+ * back to the assetsDir root `index.html` (SPA convention), then 404.
67
+ *
68
+ * The VFS is read from the supervisor DO via the class property
69
+ * `_nimbusVfsResolver` set by NimbusSession at construction. WorkerEntrypoint
70
+ * instances don't have direct access to the supervisor's SqliteVFS, so we
71
+ * reach it through the supervisor stub (env.NIMBUS_SESSION.idFromString).
72
+ * For Phase 1, we use a simpler approach: the props carry a supervisor
73
+ * DO id so we can round-trip through an RPC method that reads the file.
74
+ */
75
+ export class NimbusAssetsRPC extends WorkerEntrypoint {
76
+ /**
77
+ * Fetch a static asset. Called by the inner Worker as
78
+ * `env.ASSETS.fetch(request)`. The request URL's pathname is used to
79
+ * resolve a file under the configured assets directory.
80
+ */
81
+ async fetch(request) {
82
+ const url = new URL(request.url);
83
+ const props = this.ctx.props || {};
84
+ const vfsRoot = String(props.vfsRoot || '');
85
+ const assetsDir = String(props.assetsDir || '').replace(/^\.\//, '').replace(/^\/+/, '').replace(/\/+$/, '');
86
+ const doId = String(props.doId || '');
87
+ // Normalize pathname: no leading /, drop .. segments entirely.
88
+ let clean = url.pathname.replace(/^\/+/, '');
89
+ const parts = clean.split('/').filter((p) => p && p !== '..' && p !== '.');
90
+ clean = parts.join('/');
91
+ // Resolve the supervisor DO stub so we can call its VFS read RPC.
92
+ const ns = this.env.NIMBUS_SESSION;
93
+ if (!ns || !doId) {
94
+ return new Response('ASSETS binding not wired: missing NIMBUS_SESSION or doId', { status: 500 });
95
+ }
96
+ const stub = ns.get(ns.idFromString(doId));
97
+ // Candidate VFS paths, tried in order. The assetsDir is relative to
98
+ // the project root in VFS. Trailing-slash and bare dir → index.html.
99
+ const base = (vfsRoot ? vfsRoot + '/' : '') + (assetsDir ? assetsDir + '/' : '');
100
+ const candidates = [];
101
+ if (clean) {
102
+ candidates.push(base + clean);
103
+ if (!clean.endsWith('.html') && !clean.includes('.')) {
104
+ candidates.push(base + clean.replace(/\/+$/, '') + '/index.html');
105
+ }
106
+ }
107
+ else {
108
+ candidates.push(base + 'index.html');
109
+ }
110
+ // SPA fallback: any unmatched path serves the top-level index.html.
111
+ candidates.push(base + 'index.html');
112
+ try {
113
+ for (const candidate of candidates) {
114
+ try {
115
+ const response = await useRpcResource(stub._rpcReadFileBytes(candidate), (bytes) => {
116
+ if (!bytes || bytes.byteLength === undefined)
117
+ return null;
118
+ return new Response(bytes, {
119
+ status: 200,
120
+ headers: {
121
+ 'Content-Type': mimeTypeForPath(candidate),
122
+ 'Cache-Control': 'no-store',
123
+ },
124
+ });
125
+ });
126
+ if (response)
127
+ return response;
128
+ }
129
+ catch { /* try next */ }
130
+ }
131
+ }
132
+ finally {
133
+ disposeRpcResource(stub);
134
+ }
135
+ return new Response('Not found', { status: 404 });
136
+ }
137
+ }
138
+ /**
139
+ * Pick a sensible content-type from a filename. Conservative list; the
140
+ * inner Worker can always override via the response it constructs
141
+ * (which Workers Assets won't touch for env.ASSETS.fetch results).
142
+ */
143
+ function mimeTypeForPath(path) {
144
+ const i = path.lastIndexOf('.');
145
+ if (i < 0)
146
+ return 'application/octet-stream';
147
+ const ext = path.slice(i + 1).toLowerCase();
148
+ switch (ext) {
149
+ case 'html':
150
+ case 'htm': return 'text/html; charset=utf-8';
151
+ case 'css': return 'text/css; charset=utf-8';
152
+ case 'js':
153
+ case 'mjs': return 'application/javascript; charset=utf-8';
154
+ case 'json': return 'application/json; charset=utf-8';
155
+ case 'svg': return 'image/svg+xml';
156
+ case 'png': return 'image/png';
157
+ case 'jpg':
158
+ case 'jpeg': return 'image/jpeg';
159
+ case 'webp': return 'image/webp';
160
+ case 'gif': return 'image/gif';
161
+ case 'ico': return 'image/x-icon';
162
+ case 'woff': return 'font/woff';
163
+ case 'woff2': return 'font/woff2';
164
+ case 'txt': return 'text/plain; charset=utf-8';
165
+ case 'xml': return 'application/xml; charset=utf-8';
166
+ case 'wasm': return 'application/wasm';
167
+ case 'map': return 'application/json; charset=utf-8';
168
+ default: return 'application/octet-stream';
169
+ }
170
+ }
171
+ /**
172
+ * Worker Loader binding shim.
173
+ *
174
+ * Option A — return the raw WorkerStub from RPC — was attempted first
175
+ * and failed at runtime with:
176
+ * "Could not serialize object of type \"WorkerStub\". This type does
177
+ * not support serialization."
178
+ *
179
+ * Option B — proxy the stub via chained WorkerEntrypoint classes — is
180
+ * implemented here. The three classes below mirror the three hops a
181
+ * caller makes:
182
+ *
183
+ * env.LOADER.load(code) → NimbusLoaderRPC.load (returns NimbusLoadedWorker)
184
+ * .getEntrypoint(name?) → NimbusLoadedWorker.getEntrypoint (returns NimbusLoadedEntrypoint)
185
+ * .fetch(request) → NimbusLoadedEntrypoint.fetch
186
+ *
187
+ * Each class is a WorkerEntrypoint, so Service Binding stubs for them
188
+ * pass across the isolate boundary cleanly. The outer WorkerStub lives
189
+ * at a module-level Map keyed by a random id that's carried in
190
+ * ctx.props so subsequent hops can look it up from the outer side.
191
+ *
192
+ * Depth cap (ctx.props.depth) prevents infinite nesting: Nimbus-in-
193
+ * Nimbus-in-Nimbus is fine; five levels deep is almost certainly a
194
+ * runaway and we throw a clear error. Default limit is 4; overridable
195
+ * via the NIMBUS_INNER_LOADER_DEPTH env var on the outermost session.
196
+ */
197
+ /**
198
+ * Module-level map of loaded worker CODE (not stubs), keyed by a random
199
+ * id. WorkerStubs are I/O objects tied to a request context, so they
200
+ * can't be stashed for later use ("Cannot perform I/O on behalf of a
201
+ * different request"). Storing the code instead lets each new outer
202
+ * request re-load the worker in its own context via env.LOADER.get(id)
203
+ * — workerd caches by id so repeated loads are essentially free.
204
+ *
205
+ * H7 (memory accounting cleanup). The pre-fix comment said "GC isn't
206
+ * needed" because "inner stubs that reference them die with the DO."
207
+ * That was true for STUBS but FALSE for these CODE entries: nothing
208
+ * deletes them. `wrangler dev`'s rebuild-on-save loop calls load()
209
+ * on every save, so the Map grows without bound until the supervisor
210
+ * isolate is evicted (or hits the 128 MiB hard cap and crashes).
211
+ *
212
+ * Fix: hard-cap LRU. The Map's iteration order is insertion order;
213
+ * we re-insert on every read AND eviction-on-overflow drops the
214
+ * oldest entry. _LOADED_CODES_MAX is a documented architectural cap
215
+ * (32 entries). Eviction count is observable via getLoadedCodesStats()
216
+ * which the diag endpoint surfaces.
217
+ *
218
+ * Why 32? wrangler dev's typical rebuild burst is < 5 entries before
219
+ * the user notices and stops typing. 32 covers a power user's
220
+ * iteration cycle and a reasonable amount of `LOADER.get(id, cb)`
221
+ * memoization without exposing more than a few MiB of code text in
222
+ * the worst case (typical user-worker bundle: 50-300 KiB; 32 × 300 KiB
223
+ * = ~10 MiB ceiling — well under the 64 MiB supervisor budget).
224
+ */
225
+ const _NIMBUS_LOADED_CODES = new Map();
226
+ const _LOADED_CODES_MAX = 32;
227
+ let _loadedCodesEvictions = 0;
228
+ const NimbusLoadedEntrypointPropsSchema = z.object({
229
+ key: z.string().min(1),
230
+ name: z.string().nullable().optional(),
231
+ depth: z.number().int().nonnegative().optional(),
232
+ supervisor: z.object({
233
+ doId: z.string().min(1),
234
+ pid: z.number().int().nonnegative(),
235
+ writerId: z.string().uuid(),
236
+ }).optional(),
237
+ /**
238
+ * Staged-artifact spec, for a ONE-SHOT run. The module map — ~23 MB for
239
+ * Nimbus's largest stage — is assembled HERE, in this stateless
240
+ * entrypoint's isolate, on the Worker-Loader cache-miss path, so a
241
+ * one-shot run never materializes the artifact sources anywhere else.
242
+ * Validated by the registered assembler.
243
+ */
244
+ stage: z.unknown().optional(),
245
+ }).passthrough();
246
+ async function materializeNestedRpcRequest(request) {
247
+ const hasBody = request.method !== 'GET' && request.method !== 'HEAD';
248
+ const init = {
249
+ method: request.method,
250
+ headers: new Headers(request.headers),
251
+ body: hasBody ? await request.arrayBuffer() : undefined,
252
+ };
253
+ if (hasBody)
254
+ init.duplex = 'half';
255
+ return new Request(request.url, init);
256
+ }
257
+ /**
258
+ * Insert OR refresh a key in the LRU. New keys may evict the oldest
259
+ * entry if at the cap; existing keys are re-inserted to update their
260
+ * recency.
261
+ */
262
+ function _loadedCodesPut(key, code) {
263
+ // If the key already exists, delete first so re-insertion lands at
264
+ // the MRU end of the iteration order (LRU-style refresh).
265
+ if (_NIMBUS_LOADED_CODES.has(key)) {
266
+ _NIMBUS_LOADED_CODES.delete(key);
267
+ }
268
+ else if (_NIMBUS_LOADED_CODES.size >= _LOADED_CODES_MAX) {
269
+ // Evict the LRU entry — the first key in insertion order.
270
+ const oldest = _NIMBUS_LOADED_CODES.keys().next();
271
+ if (!oldest.done) {
272
+ _NIMBUS_LOADED_CODES.delete(oldest.value);
273
+ _loadedCodesEvictions++;
274
+ }
275
+ }
276
+ _NIMBUS_LOADED_CODES.set(key, code);
277
+ }
278
+ function _loadedCodesGet(key) {
279
+ const v = _NIMBUS_LOADED_CODES.get(key);
280
+ if (v === undefined)
281
+ return undefined;
282
+ // LRU-refresh on read so memoization-style usage (LOADER.get(id, cb)
283
+ // re-hitting the same id repeatedly) keeps the entry warm.
284
+ _NIMBUS_LOADED_CODES.delete(key);
285
+ _NIMBUS_LOADED_CODES.set(key, v);
286
+ return v;
287
+ }
288
+ /**
289
+ * Diagnostic surface for /api/_diag/memory. Returns a snapshot of
290
+ * the Map state — entry count, configured cap, eviction counter
291
+ * since isolate boot. Pure read; no I/O.
292
+ */
293
+ export function getLoadedCodesStats() {
294
+ return {
295
+ entries: _NIMBUS_LOADED_CODES.size,
296
+ maxEntries: _LOADED_CODES_MAX,
297
+ evictions: _loadedCodesEvictions,
298
+ };
299
+ }
300
+ function _genStubId() {
301
+ return 'ldr-' + Math.random().toString(36).slice(2) + Date.now().toString(36);
302
+ }
303
+ /**
304
+ * Look up the stored code by key and create a fresh outer WorkerStub
305
+ * in the CURRENT request context. Uses LOADER.get(id, cb) so repeated
306
+ * calls reuse the same dynamic worker rather than spawning new ones.
307
+ */
308
+ function _resolveStubInCurrentContext(outerLoader, key) {
309
+ if (key === undefined)
310
+ return null;
311
+ const code = _loadedCodesGet(key);
312
+ if (!code)
313
+ return null;
314
+ return outerLoader.get(key, async () => code);
315
+ }
316
+ /** Hop 1: env.LOADER.{load,get} forwarded to the outer loader. */
317
+ export class NimbusLoaderRPC extends WorkerEntrypoint {
318
+ _currentDepth() {
319
+ const d = this.ctx.props?.depth;
320
+ return typeof d === 'number' && d >= 0 ? d : 0;
321
+ }
322
+ _maxDepth() {
323
+ const raw = this.env?.NIMBUS_INNER_LOADER_DEPTH;
324
+ const parsed = raw ? parseInt(String(raw), 10) : NaN;
325
+ return Number.isFinite(parsed) && parsed > 0 ? parsed : 4;
326
+ }
327
+ _assertDepthOk() {
328
+ const depth = this._currentDepth();
329
+ const max = this._maxDepth();
330
+ if (depth >= max) {
331
+ throw new Error(`Nimbus: refusing to spawn inner Worker Loader (depth=${depth + 1}, max=${max}). ` +
332
+ `Set NIMBUS_INNER_LOADER_DEPTH to raise the cap or break the recursion.`);
333
+ }
334
+ }
335
+ /**
336
+ * Inner: env.LOADER.load(code). Stashes the CODE (not a stub — stubs
337
+ * are I/O-bound to the calling request context) and returns a
338
+ * NimbusLoadedWorker RPC stub. Each downstream call re-loads the
339
+ * worker in its own request context via LOADER.get(key, cb).
340
+ */
341
+ load(code) {
342
+ this._assertDepthOk();
343
+ const outerLoader = this.env?.LOADER;
344
+ if (!outerLoader)
345
+ throw new Error('Nimbus: outer env.LOADER missing');
346
+ // Validate by loading once in THIS context (fails fast on bad code).
347
+ // The stub is discarded; downstream calls re-load fresh in their
348
+ // own context.
349
+ outerLoader.load(code);
350
+ const key = _genStubId();
351
+ _loadedCodesPut(key, code);
352
+ const ctxExports = shimCtxExports(this.ctx);
353
+ if (!ctxExports.NimbusLoadedWorker) {
354
+ throw new Error('Nimbus: ctx.exports.NimbusLoadedWorker unavailable');
355
+ }
356
+ return ctxExports.NimbusLoadedWorker({
357
+ props: { key, depth: this.ctx.props?.depth || 0 },
358
+ });
359
+ }
360
+ /**
361
+ * Inner: env.LOADER.get(id, callback). The inner's callback returns
362
+ * a code object; we treat `id` as the outer cache key (prefixed so
363
+ * it doesn't collide with load()-generated keys).
364
+ */
365
+ async get(id, callback) {
366
+ this._assertDepthOk();
367
+ const outerLoader = this.env?.LOADER;
368
+ if (!outerLoader)
369
+ throw new Error('Nimbus: outer env.LOADER missing');
370
+ const key = 'get:' + id;
371
+ if (_loadedCodesGet(key) === undefined) {
372
+ const code = await callback();
373
+ _loadedCodesPut(key, code);
374
+ }
375
+ const ctxExports = shimCtxExports(this.ctx);
376
+ if (!ctxExports.NimbusLoadedWorker) {
377
+ throw new Error('Nimbus: ctx.exports.NimbusLoadedWorker unavailable');
378
+ }
379
+ return ctxExports.NimbusLoadedWorker({
380
+ props: { key, depth: this.ctx.props?.depth || 0 },
381
+ });
382
+ }
383
+ }
384
+ /** Hop 2: the returned "worker" stub. Exposes .getEntrypoint(). */
385
+ export class NimbusLoadedWorker extends WorkerEntrypoint {
386
+ /**
387
+ * Returns a NimbusLoadedEntrypoint stub that carries the code key +
388
+ * entrypoint name forward. The actual outer-side load + fetch happens
389
+ * inside NimbusLoadedEntrypoint.fetch() so all outer hops run in a
390
+ * SINGLE outer request context (the cross-request-I/O limitation is
391
+ * real — stubs created in one outer request can't be used by another).
392
+ */
393
+ getEntrypoint(name) {
394
+ const props = this.ctx.props || {};
395
+ const ctxExports = shimCtxExports(this.ctx);
396
+ if (!ctxExports.NimbusLoadedEntrypoint) {
397
+ throw new Error('Nimbus: ctx.exports.NimbusLoadedEntrypoint unavailable');
398
+ }
399
+ return ctxExports.NimbusLoadedEntrypoint({
400
+ props: { key: props.key, name: name || null, depth: props.depth },
401
+ });
402
+ }
403
+ /**
404
+ * Pass-through to outer worker.getDurableObjectClass(name). The
405
+ * returned stub is tied to THIS method's outer request context; if
406
+ * the caller (the inner worker) uses the class in a later request
407
+ * it will fail the cross-request-I/O check. For Phase 3 DO binding
408
+ * synthesis we resolve classes directly from nimbus-wrangler's own
409
+ * request context (which is the build-time context), not through
410
+ * this method.
411
+ */
412
+ getDurableObjectClass(name) {
413
+ const props = this.ctx.props || {};
414
+ const outerLoader = this.env?.LOADER;
415
+ if (!outerLoader)
416
+ throw new Error('Nimbus: outer env.LOADER missing');
417
+ const outer = _resolveStubInCurrentContext(outerLoader, props.key);
418
+ if (!outer)
419
+ throw new Error('Nimbus: loaded worker code missing (key=' + props.key + ')');
420
+ return outer.getDurableObjectClass(name);
421
+ }
422
+ }
423
+ /** Hop 3: a named-or-default entrypoint. Exposes .fetch(). */
424
+ export class NimbusLoadedEntrypoint extends WorkerEntrypoint {
425
+ _props() {
426
+ return NimbusLoadedEntrypointPropsSchema.parse(this.ctx.props || {});
427
+ }
428
+ async _supervisorBinding(props) {
429
+ if (!props.supervisor)
430
+ return undefined;
431
+ const factory = supervisorEntrypoint(ctxExportsOf(this.ctx));
432
+ if (!factory) {
433
+ throw new Error(`Nimbus: ctx.exports.${supervisorEntrypointName() ?? '<supervisor entrypoint>'} unavailable`);
434
+ }
435
+ return await factory({ props: props.supervisor });
436
+ }
437
+ async _resolveEntrypoint() {
438
+ const props = this._props();
439
+ const outerLoader = this.env?.LOADER;
440
+ if (!outerLoader)
441
+ throw new Error('Nimbus: outer env.LOADER missing');
442
+ let outerStub;
443
+ if (props.stage !== undefined) {
444
+ // Staged artifact: assemble the full module map lazily, ONLY on a
445
+ // loader miss, in THIS stateless isolate. The facet's SUPERVISOR
446
+ // binding is created in this request context — the caller holds the
447
+ // one-shot fetch open for the whole run, which keeps that context
448
+ // alive.
449
+ const stage = props.stage;
450
+ outerStub = outerLoader.get(props.key, async () => {
451
+ const assembled = await requireStagedBootAssembler()(this.env, stage);
452
+ assertModuleMapWithinCodeLimit(assembled.modules ?? {});
453
+ const supervisorBinding = await this._supervisorBinding(props);
454
+ if (!supervisorBinding)
455
+ return assembled;
456
+ return { ...assembled, env: { SUPERVISOR: supervisorBinding } };
457
+ });
458
+ }
459
+ else {
460
+ // No spec in props: resolve the ALREADY-LOADED worker. First the inner
461
+ // Worker Loader shim's code map (nimbus-in-nimbus), else the outer
462
+ // loader's own cache. The cache-miss callback fails loud: a spec-free
463
+ // stub is a handle on a worker someone else loaded — re-loading it from
464
+ // code would boot an empty isolate, a silent wrong answer.
465
+ outerStub = _resolveStubInCurrentContext(outerLoader, props.key)
466
+ ?? outerLoader.get(props.key, async () => {
467
+ throw new Error(`Nimbus: dynamic worker '${props.key}' is no longer loaded (evicted?)`);
468
+ });
469
+ }
470
+ const outer = await outerStub;
471
+ if (!outer)
472
+ throw new Error('Nimbus: loaded worker code missing');
473
+ return await (props.name ? outer.getEntrypoint(props.name) : outer.getEntrypoint());
474
+ }
475
+ /**
476
+ * Relay the inner entrypoint's Response to the caller with a LIVE body.
477
+ * The body streams through an identity pipe and the entrypoint stub is
478
+ * disposed only once the body finishes — materializing (arrayBuffer) here
479
+ * buffered every routed response to stream-end, which froze SSE/chunked
480
+ * bodies (an agent server's /event live-sync, `curl -N` loopback, external
481
+ * preview) until the facet closed the stream.
482
+ */
483
+ _relayNestedRpcResponse(ep, response) {
484
+ if (!(response instanceof Response)) {
485
+ disposeRpcResource(response);
486
+ disposeRpcResource(ep);
487
+ return new Response('Nimbus: loaded worker entrypoint returned a non-Response value', { status: 502 });
488
+ }
489
+ const init = {
490
+ status: response.status,
491
+ statusText: response.statusText,
492
+ headers: new Headers(response.headers),
493
+ };
494
+ if (!response.body) {
495
+ disposeRpcResource(ep);
496
+ return new Response(null, init);
497
+ }
498
+ const { readable, writable } = new IdentityTransformStream();
499
+ this.ctx.waitUntil(response.body
500
+ .pipeTo(writable)
501
+ .catch(() => { })
502
+ .finally(() => disposeRpcResource(ep)));
503
+ return new Response(readable, init);
504
+ }
505
+ /**
506
+ * Invoke the facet's HTTP handler.
507
+ *
508
+ * The call must be written as `ep.method(request)`. An RPC stub's method is a
509
+ * JsRpcProperty, whose every property access is a WILDCARD that extends a
510
+ * pipelined path (`JSG_WILDCARD_PROPERTY`, workerd api/worker-rpc.h) — so
511
+ * `method.call(ep, request)` does NOT reach Function.prototype.call. It builds
512
+ * the path `handleHttpRequest.call` and invokes it remotely with `ep` as its
513
+ * first ARGUMENT. Serializing `ep` — an entrypoint to a dynamically-loaded
514
+ * worker — is what workerd refuses:
515
+ *
516
+ * DataCloneError: Entrypoints to dynamically-loaded workers cannot be
517
+ * transferred to other Workers
518
+ *
519
+ * (server.c++ `requireAllowsTransfer` → `throwDynamicEntrypointTransferError`).
520
+ * The facet is never entered, because the failure is in serializing the
521
+ * arguments, before the call is delivered.
522
+ */
523
+ _callHttpHandler(ep, request) {
524
+ return typeof ep.handleHttpRequest === 'function'
525
+ ? ep.handleHttpRequest(request)
526
+ : ep.fetch(request);
527
+ }
528
+ async handleHttpRequest(request) {
529
+ const ep = await this._resolveEntrypoint();
530
+ try {
531
+ if (typeof ep.handleHttpRequest !== 'function' && typeof ep.fetch !== 'function') {
532
+ disposeRpcResource(ep);
533
+ return new Response('Nimbus: loaded worker entrypoint has no HTTP request handler', { status: 502 });
534
+ }
535
+ const response = await this._callHttpHandler(ep, await materializeNestedRpcRequest(request));
536
+ return this._relayNestedRpcResponse(ep, response);
537
+ }
538
+ catch (e) {
539
+ disposeRpcResource(ep);
540
+ throw e;
541
+ }
542
+ }
543
+ /**
544
+ * Forward fetch() to the outer worker's entrypoint. All three outer
545
+ * hops (load → getEntrypoint → fetch) run in the same outer request
546
+ * context (this method's invocation), which sidesteps the
547
+ * cross-request-I/O limitation.
548
+ */
549
+ async fetch(request) {
550
+ const ep = await this._resolveEntrypoint();
551
+ try {
552
+ const response = await ep.fetch(await materializeNestedRpcRequest(request));
553
+ return this._relayNestedRpcResponse(ep, response);
554
+ }
555
+ catch (e) {
556
+ disposeRpcResource(ep);
557
+ throw e;
558
+ }
559
+ }
560
+ }
561
+ /**
562
+ * `env.MY_DO` shim — a DurableObjectNamespace-like WorkerEntrypoint.
563
+ *
564
+ * Usage from inner Worker:
565
+ * const id = await env.MY_DO.idFromName('x'); // AWAIT required
566
+ * const stub = env.MY_DO.get(id);
567
+ * await stub.fetch(request);
568
+ *
569
+ * IMPORTANT: unlike the real DurableObjectNamespace, idFromName /
570
+ * newUniqueId / idFromString here return **Promises**, because they're
571
+ * RPC-backed WorkerEntrypoint methods. The inner caller MUST `await`
572
+ * them before passing the result to `.get()`. Workers RPC pipelining
573
+ * does not currently allow passing an RpcPromise as a method argument
574
+ * — the no-await form fails with:
575
+ * "Could not serialize object of type \"RpcPromise\"."
576
+ *
577
+ * Typical real-Worker code written for Cloudflare's synchronous
578
+ * DurableObjectNamespace needs a one-word change (add `await`).
579
+ *
580
+ * idFromName produces prefix `name:` (deterministic FNV-style hash);
581
+ * newUniqueId uses `uniq:` (random). The prefixes keep the two id
582
+ * spaces distinct so a name-derived id can't collide with a random
583
+ * one.
584
+ */
585
+ export class NimbusDurableObjectNamespace extends WorkerEntrypoint {
586
+ /** Stable string id derived from a name. Hash is deterministic. */
587
+ idFromName(name) {
588
+ // Simple 64-bit-ish FNV-style hash → hex. Stable across runs;
589
+ // distinct names → distinct strings; same name → same string.
590
+ let h1 = 0xdeadbeef ^ name.length;
591
+ let h2 = 0x41c6ce57 ^ name.length;
592
+ for (let i = 0; i < name.length; i++) {
593
+ const ch = name.charCodeAt(i);
594
+ h1 = Math.imul(h1 ^ ch, 2654435761);
595
+ h2 = Math.imul(h2 ^ ch, 1597334677);
596
+ }
597
+ h1 = Math.imul(h1 ^ (h1 >>> 16), 2246822507) ^ Math.imul(h2 ^ (h2 >>> 13), 3266489909);
598
+ h2 = Math.imul(h2 ^ (h2 >>> 16), 2246822507) ^ Math.imul(h1 ^ (h1 >>> 13), 3266489909);
599
+ const high = (h1 >>> 0).toString(16).padStart(8, '0');
600
+ const low = (h2 >>> 0).toString(16).padStart(8, '0');
601
+ return 'name:' + high + low;
602
+ }
603
+ /** Fresh random id (matches DurableObjectNamespace.newUniqueId()). */
604
+ newUniqueId() {
605
+ return 'uniq:' + Math.random().toString(36).slice(2) + Date.now().toString(36);
606
+ }
607
+ /** Accept-through for an already-formatted id. */
608
+ idFromString(s) {
609
+ return s;
610
+ }
611
+ /** Return a stub bound to the given id. */
612
+ get(id) {
613
+ const ctxExports = shimCtxExports(this.ctx);
614
+ if (!ctxExports.NimbusDOStub)
615
+ throw new Error('Nimbus: ctx.exports.NimbusDOStub unavailable');
616
+ const props = this.ctx.props || {};
617
+ return ctxExports.NimbusDOStub({
618
+ props: {
619
+ bindingName: props.bindingName,
620
+ supervisorDoId: props.supervisorDoId,
621
+ id: String(id),
622
+ },
623
+ });
624
+ }
625
+ }
626
+ /**
627
+ * A Durable-Object-namespace-stub for a specific id. Exposes fetch()
628
+ * and will, if we later need it, forward RPC method calls through a
629
+ * dispatch helper. The important invariant: EVERY call resolves the
630
+ * inner DO class via getInnerDoClass() (./inner-do-registry.js) and
631
+ * spins up / attaches to a facet via the supervisor's ctx.facets in
632
+ * the SAME outer request context — never reusing stubs across requests.
633
+ */
634
+ export class NimbusDOStub extends WorkerEntrypoint {
635
+ /**
636
+ * Resolve the supervisor DO from env.NIMBUS_SESSION and route through
637
+ * its _rpcInnerDoFetch RPC method, which runs ctx.facets.get(...) in
638
+ * its own context and forwards the request.
639
+ */
640
+ async fetch(request) {
641
+ const props = this.ctx.props || {};
642
+ const ns = this.env?.NIMBUS_SESSION;
643
+ if (!ns)
644
+ return new Response('Nimbus: env.NIMBUS_SESSION unavailable', { status: 500 });
645
+ const supervisorDoId = String(props.supervisorDoId || '');
646
+ if (!supervisorDoId)
647
+ return new Response('Nimbus: supervisorDoId missing', { status: 500 });
648
+ const bindingName = String(props.bindingName || '');
649
+ const id = String(props.id || '');
650
+ const stub = ns.get(ns.idFromString(supervisorDoId));
651
+ // Forward the full request (method, body, headers preserved) by
652
+ // serializing what's needed and reconstructing on the other side.
653
+ // The supervisor reconstitutes the Request from these fields and
654
+ // invokes the facet.
655
+ const body = request.method !== 'GET' && request.method !== 'HEAD'
656
+ ? await request.arrayBuffer()
657
+ : null;
658
+ const headerList = [];
659
+ request.headers.forEach((v, k) => { headerList.push([k, v]); });
660
+ try {
661
+ return await useRpcResource(stub._rpcInnerDoFetch({
662
+ bindingName,
663
+ id,
664
+ method: request.method,
665
+ url: request.url,
666
+ headers: headerList,
667
+ body,
668
+ }), (res) => new Response(res.body, {
669
+ status: res.status,
670
+ statusText: res.statusText,
671
+ headers: res.headers,
672
+ }));
673
+ }
674
+ finally {
675
+ disposeRpcResource(stub);
676
+ }
677
+ }
678
+ }