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