@nimbus-sh/worker 0.7.0 → 0.9.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 (102) hide show
  1. package/dist/facets/compose.d.ts +3 -0
  2. package/dist/facets/compose.d.ts.map +1 -1
  3. package/dist/facets/compose.js +1 -1
  4. package/dist/facets/manager.d.ts +26 -15
  5. package/dist/facets/manager.d.ts.map +1 -1
  6. package/dist/facets/manager.js +154 -112
  7. package/dist/facets/real-vite-hmr.d.ts +3 -1
  8. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  9. package/dist/facets/real-vite-hmr.js +18 -8
  10. package/dist/facets/ws-terminal.d.ts +9 -31
  11. package/dist/facets/ws-terminal.d.ts.map +1 -1
  12. package/dist/facets/ws-terminal.js +58 -51
  13. package/dist/hosted/commands.d.ts +12 -0
  14. package/dist/hosted/commands.d.ts.map +1 -0
  15. package/dist/hosted/commands.js +1647 -0
  16. package/dist/hosted/runtime.d.ts +135 -0
  17. package/dist/hosted/runtime.d.ts.map +1 -0
  18. package/dist/hosted/runtime.js +344 -0
  19. package/dist/hosted/services.d.ts +44 -0
  20. package/dist/hosted/services.d.ts.map +1 -0
  21. package/dist/hosted/services.js +522 -0
  22. package/dist/loaders/generated-workers.d.ts +1 -1
  23. package/dist/loaders/generated-workers.d.ts.map +1 -1
  24. package/dist/loaders/generated-workers.js +2 -2
  25. package/dist/node-shims-artifact.generated.js +3 -3
  26. package/dist/runtime/bash-repl.d.ts +6 -4
  27. package/dist/runtime/bash-repl.d.ts.map +1 -1
  28. package/dist/runtime/bash-repl.js +46 -7
  29. package/dist/runtime/bun-runner.js +1 -1
  30. package/dist/runtime/facet-loader-host.d.ts.map +1 -1
  31. package/dist/runtime/facet-loader-host.js +1 -3
  32. package/dist/runtime/node-runner.d.ts +1 -1
  33. package/dist/runtime/node-runner.d.ts.map +1 -1
  34. package/dist/runtime/node-runner.js +2 -2
  35. package/dist/runtime/node-shims.d.ts.map +1 -1
  36. package/dist/runtime/node-shims.js +14 -1
  37. package/dist/runtime/opencode-facet-runner.js +14 -14
  38. package/dist/runtime/package-manager.d.ts +35 -122
  39. package/dist/runtime/package-manager.d.ts.map +1 -1
  40. package/dist/runtime/package-manager.js +61 -596
  41. package/dist/runtime/process-logs-api.d.ts +1 -1
  42. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  43. package/dist/runtime/process-logs-api.js +1 -1
  44. package/dist/runtime/python-repl.d.ts +6 -4
  45. package/dist/runtime/python-repl.d.ts.map +1 -1
  46. package/dist/runtime/python-repl.js +92 -67
  47. package/dist/runtime/repl-session.d.ts +9 -47
  48. package/dist/runtime/repl-session.d.ts.map +1 -1
  49. package/dist/runtime/repl-session.js +105 -105
  50. package/dist/runtime/ruby-repl.d.ts +3 -2
  51. package/dist/runtime/ruby-repl.d.ts.map +1 -1
  52. package/dist/runtime/ruby-repl.js +7 -6
  53. package/dist/runtime/runtime-catalog.d.ts +13 -0
  54. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  55. package/dist/runtime/runtime-catalog.js +88 -0
  56. package/dist/runtime/wasi/preamble.d.ts.map +1 -1
  57. package/dist/runtime/wasi/preamble.js +74 -56
  58. package/dist/runtime-catalog.generated.js +1 -1
  59. package/dist/session/agent.d.ts +1 -21
  60. package/dist/session/agent.d.ts.map +1 -1
  61. package/dist/session/ai.d.ts +5 -1
  62. package/dist/session/ai.d.ts.map +1 -1
  63. package/dist/session/ai.js +7 -0
  64. package/dist/session/hibernation.d.ts +2 -1
  65. package/dist/session/hibernation.d.ts.map +1 -1
  66. package/dist/session/hibernation.js +7 -4
  67. package/dist/session/init.d.ts +1 -36
  68. package/dist/session/init.d.ts.map +1 -1
  69. package/dist/session/init.js +18 -1773
  70. package/dist/session/loopback.d.ts +2 -29
  71. package/dist/session/loopback.d.ts.map +1 -1
  72. package/dist/session/loopback.js +3 -27
  73. package/dist/session/nimbus-session.d.ts +28 -34
  74. package/dist/session/nimbus-session.d.ts.map +1 -1
  75. package/dist/session/nimbus-session.js +78 -518
  76. package/dist/session/npm-install-port.d.ts +3 -1
  77. package/dist/session/npm-install-port.d.ts.map +1 -1
  78. package/dist/session/port-capability.d.ts.map +1 -1
  79. package/dist/session/port-capability.js +11 -1
  80. package/dist/session/programmatic.d.ts +5 -38
  81. package/dist/session/programmatic.d.ts.map +1 -1
  82. package/dist/session/programmatic.js +17 -49
  83. package/dist/session/rpc.d.ts +3 -43
  84. package/dist/session/rpc.d.ts.map +1 -1
  85. package/dist/session/rpc.js +8 -67
  86. package/dist/session/supervisor-op.d.ts +10 -5
  87. package/dist/session/supervisor-op.d.ts.map +1 -1
  88. package/dist/session/supervisor-op.js +21 -48
  89. package/dist/session/supervisor-rpc.d.ts +33 -16
  90. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  91. package/dist/session/supervisor-rpc.js +49 -4
  92. package/dist/vfs/facet-resident-store.d.ts.map +1 -1
  93. package/dist/vfs/facet-resident-store.js +4 -2
  94. package/dist/workspace-host.d.ts +7 -0
  95. package/dist/workspace-host.d.ts.map +1 -0
  96. package/dist/workspace-host.js +5 -0
  97. package/dist/workspace.d.ts +2 -0
  98. package/dist/workspace.d.ts.map +1 -0
  99. package/dist/workspace.js +1 -0
  100. package/package.json +17 -12
  101. package/public/_assets/runtime/{node-shims-8f8c64edd2a1f0f5.js → node-shims-acaa037e6d26ec0d.js} +14 -1
  102. package/scripts/bundle-runtime.mjs +35 -8
@@ -0,0 +1,522 @@
1
+ import { staticStdinReader } from "@nimbus-sh/core/shell/stdin-adapter.js";
2
+ import { composeFacetManager } from "../facets/compose.js";
3
+ import { FacetProcessManager, textBytes } from "../facets/process.js";
4
+ import { ChildProcessSpawnPool } from "../loaders/child-process/spawn-pool.js";
5
+ import { CRED_KERNEL, CRED_SESSION_USER } from "@nimbus-sh/core/runtime/os-contracts.js";
6
+ import { SqliteFilesystemAuthority } from "@nimbus-sh/core/runtime/filesystem-authority.js";
7
+ import { EsbuildBundlePool } from "../facets/esbuild-bundle-pool.js";
8
+ import { EsbuildService } from "@nimbus-sh/core/runtime/esbuild-service.js";
9
+ import { CF_COMPAT_DATE } from "@nimbus-sh/core/constants.js";
10
+ import { notifyTerminalEvent } from "../runtime/process-logs-api.js";
11
+ // The supervisor terminates a facet's outbound sockets so inbound frames
12
+ // arrive as supervisor replies (VFS coherence witness 3).
13
+ import { WebSocketRelay } from "../session/ws-relay.js";
14
+ // ── Pure helpers extracted to ./nimbus-session-helpers.ts (S1) ────────
15
+ //
16
+ // renderNoDevServerHtml, BUNDLER_BIN_PREFIXES, NIMBUS_UNSUPPORTED_BINS,
17
+ // WRANGLER_IGNORED_FLAGS{,_WITH_VALUE}, WRANGLER_UNSUPPORTED_CONFIG_FIELDS,
18
+ // filterWranglerFlags, detectUnsupportedWranglerConfig, _CP_FACET_DIRECT,
19
+ // _CP_PURE_BUILTIN, _classifyCommand, detectBundlerBin, checkNodeModulesGuard
20
+ // all live in the helpers module now.
21
+ //
22
+ // They are imported here (so call sites in this file work unchanged) and
23
+ // re-exported (so external callers importing them from
24
+ // `./nimbus-session.js` keep working — back-compat).
25
+ //
26
+ // (esbuild wasm bytes are fetched from env.ASSETS by
27
+ // src/esbuild-wasm-bytes.ts at pool-construction time; A'.5 dropped
28
+ // the supervisor-resident cache + the SUPERVISOR.getEsbuildWasm RPC.)
29
+ // Helpers needed by this class file's own logic (not just re-export).
30
+ import { _classifyCommand } from "../session/helpers.js";
31
+ import { z } from "zod/v4";
32
+ const CpFacetDirectPayloadSchema = z.object({
33
+ command: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
34
+ args: z.array(z.unknown()).optional().transform((value) => (value || []).map((item) => String(item))),
35
+ env: z.record(z.string(), z.unknown()).optional().transform((value) => {
36
+ const out = {};
37
+ for (const [key, item] of Object.entries(value || {}))
38
+ out[key] = String(item);
39
+ return out;
40
+ }),
41
+ cwd: z.unknown().optional().transform((value) => value == null ? '/' : String(value)),
42
+ stdin: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
43
+ processPid: z.number().int().positive(),
44
+ }).passthrough();
45
+ function normalizeCpCommandName(name) {
46
+ const text = String(name || '').trim();
47
+ if (!text.startsWith('/'))
48
+ return text;
49
+ const slash = text.lastIndexOf('/');
50
+ const base = slash >= 0 ? text.slice(slash + 1) : text;
51
+ const dir = text.slice(0, Math.max(0, text.length - base.length));
52
+ if (dir === '/bin/' || dir === '/usr/bin/' || dir === '/usr/local/bin/') {
53
+ return base;
54
+ }
55
+ return text;
56
+ }
57
+ export function ensureBundlePool(self, runtimeContext) {
58
+ if (!self.bundlePool)
59
+ self.bundlePool = new EsbuildBundlePool(runtimeContext.env, runtimeContext.ctx);
60
+ return self.bundlePool;
61
+ }
62
+ export function ensureFacetManager(self, runtimeContext) {
63
+ if (!self.facetManagerComposed) {
64
+ // The manager is composed over the filesystem, so the filesystem comes
65
+ // first. Cheap and idempotent; every caller already stood it up or is
66
+ // about to.
67
+ self.ensureSqliteFs();
68
+ const filesystem = runtimeContext.filesystem();
69
+ // The manager reaches the disk behind the authority (boot images, the
70
+ // launch journal), so a host that credentials something other than this
71
+ // session's SQLite filesystem cannot compose one.
72
+ if (!(filesystem instanceof SqliteFilesystemAuthority)) {
73
+ throw new Error('Nimbus: the facet manager needs the session SqliteFilesystemAuthority, not a foreign filesystem authority');
74
+ }
75
+ self.facetManagerComposed = composeFacetManager({
76
+ ctx: runtimeContext.ctx,
77
+ env: runtimeContext.env,
78
+ processes: self.processes,
79
+ portRegistry: self.portRegistry,
80
+ vfs: filesystem.vfs,
81
+ filesystem,
82
+ ...(self.esbuildService ? { esbuild: self.esbuildService } : {}),
83
+ hooks: {
84
+ onExternalExit: (pid, code, reason) => self._reportExternalExit(pid, code, reason),
85
+ requestLaunchTurn: (notBefore) => runtimeContext.requestLaunchTurn(notBefore),
86
+ resolveWorkerLaunch: runtimeContext.resolveWorkerLaunch,
87
+ notify: (line) => runtimeContext.notify(line),
88
+ onSpawn: (pid, command, longRunning) => {
89
+ const attachedTty = self.processes.get(pid)?.attachedTty === true;
90
+ if (longRunning) {
91
+ try {
92
+ self.processes.openInput(pid);
93
+ }
94
+ catch { }
95
+ }
96
+ // Only surface long-running / user-visible spawns to keep
97
+ // the terminal uncluttered. Short `node <file>` evals also
98
+ // get a line because users want the pid for `logs`/`kill`.
99
+ if (!self.terminal)
100
+ return;
101
+ const label = longRunning ? 'started (long-running)' : 'started';
102
+ self.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
103
+ // Structured event so the tabs UI can auto-open a log tab
104
+ // for long-running processes (vite, wrangler dev, etc.).
105
+ notifyTerminalEvent(self.terminal, {
106
+ type: 'spawn', pid, command, longRunning, attachedTty,
107
+ });
108
+ },
109
+ },
110
+ });
111
+ }
112
+ const composed = self.facetManagerComposed;
113
+ // W3.5 Fix B: share the session's lazy esbuildService with the
114
+ // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
115
+ // wasm-init twice. The session may construct it after the manager
116
+ // exists, so the share is re-offered on every call; FacetManager
117
+ // otherwise lazy-creates its own on first exec — same wasm bytes,
118
+ // same ~10ms init cost, just paid once per surface.
119
+ if (self.esbuildService) {
120
+ composed.manager.setEsbuildService(self.esbuildService);
121
+ }
122
+ return composed;
123
+ }
124
+ export function _ensureWebSocketRelay(self, runtimeContext) {
125
+ if (!self.webSocketRelay)
126
+ self.webSocketRelay = new WebSocketRelay();
127
+ return self.webSocketRelay;
128
+ }
129
+ export function _ensureFacetProcessManager(self, runtimeContext) {
130
+ if (self.facetProcessManager)
131
+ return self.facetProcessManager;
132
+ self.ensureSqliteFs();
133
+ self.ensureFacetManager();
134
+ // FacetProcessManager is statically imported at top-of-file (W8).
135
+ // No lazy-import: workerd doesn't ship CJS require, and the dynamic
136
+ // import would be async — making _ensureFacetProcessManager async
137
+ // would force every cp* RPC entry point to also be async on the
138
+ // promise-resolution path, which is fine but uglier. Compile-time
139
+ // tree-shaking handles unused-when-no-cp-RPC paths.
140
+ // Adapter for FacetManagerLike — wraps the existing FacetManager.exec
141
+ // with a streaming surface. Phase 1 simplification: facet-direct
142
+ // commands are dispatched through the shell registry the same way
143
+ // shell.execute does, but with the per-PID hooks routed.
144
+ const facetMgrAdapter = {
145
+ execStream: async (codeJson, opts, hooks) => {
146
+ // codeJson is a payload from FacetProcessManager._dispatch facet-direct
147
+ // path: {command, args, env, cwd, stdin}. We dispatch through the
148
+ // existing shell registry by resolving the command and invoking
149
+ // it with synthesized output streams that route to hooks.
150
+ let payload;
151
+ try {
152
+ const parsed = CpFacetDirectPayloadSchema.safeParse(JSON.parse(codeJson));
153
+ if (!parsed.success) {
154
+ hooks.onStderr(textBytes('child_process: facet dispatch requires a broker-assigned process pid\n'));
155
+ return 1;
156
+ }
157
+ payload = parsed.data;
158
+ }
159
+ catch {
160
+ hooks.onStderr(textBytes('child_process: invalid facet dispatch payload\n'));
161
+ return 1;
162
+ }
163
+ const registry = self._cpRegistry;
164
+ if (!registry) {
165
+ hooks.onStderr(textBytes('child_process: command registry unavailable\n'));
166
+ return 127;
167
+ }
168
+ const commandName = normalizeCpCommandName(payload.command);
169
+ const cmd = await registry.resolve(commandName);
170
+ if (!cmd) {
171
+ hooks.onStderr(textBytes(`${payload.command}: command not found\n`));
172
+ return 127;
173
+ }
174
+ // Synthesize a CommandContext for the internal shell substrate.
175
+ const stdoutStream = { write: (d) => hooks.onStdout(textBytes(String(d))) };
176
+ const stderrStream = { write: (d) => hooks.onStderr(textBytes(String(d))) };
177
+ const ac = new AbortController();
178
+ const cred = self.processes.cred(payload.processPid);
179
+ const ctx = {
180
+ pid: payload.processPid,
181
+ cred,
182
+ args: payload.args || [],
183
+ env: payload.env || {},
184
+ cwd: payload.cwd || '/home/user',
185
+ vfs: self.sqliteFs.as(cred),
186
+ stdout: stdoutStream,
187
+ stderr: stderrStream,
188
+ signal: ac.signal,
189
+ // For commands that need stdin we pass a tiny adapter.
190
+ stdin: staticStdinReader(payload.stdin || ''),
191
+ setUmask: (mask) => { self.processes.setUmask(payload.processPid, mask); },
192
+ runAs: async (targetCred, argv) => {
193
+ if (argv.length === 0)
194
+ return 0;
195
+ const child = self.processes.spawn(argv.join(' '), argv, payload.cwd, { parentPid: payload.processPid, cred: targetCred });
196
+ let exitCode = 1;
197
+ try {
198
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), payload.env, payload.cwd, payload.stdin, hooks);
199
+ return exitCode;
200
+ }
201
+ finally {
202
+ self.processes.exit(child.pid, exitCode);
203
+ }
204
+ },
205
+ __nimbusCaptureOutput: true,
206
+ };
207
+ try {
208
+ const code = await cmd(ctx);
209
+ return typeof code === 'number' ? code : 0;
210
+ }
211
+ catch (e) {
212
+ hooks.onStderr(textBytes(`${payload.command}: ${e?.message || String(e)}\n`));
213
+ return 1;
214
+ }
215
+ },
216
+ abort: (facetName) => {
217
+ // Best-effort: relay to ctx.facets.abort, mirroring FacetManager.kill.
218
+ try {
219
+ runtimeContext.ctx.facets?.abort?.(facetName, new Error('SIGKILL'));
220
+ }
221
+ catch { }
222
+ return true;
223
+ },
224
+ };
225
+ // Adapter for CommandRegistryLike. The shared shell registry is
226
+ // attached to `this._cpRegistry` by the shell-init path (see
227
+ // construction near line 2058 — registry passed as ctor arg there).
228
+ const cmdRegistryAdapter = {
229
+ // Consult the live shell registry FIRST so dynamically-registered
230
+ // commands (registerUnixCommands / git / npm / wrangler etc.) are seen
231
+ // even if they're not in the static _CP_PURE_BUILTIN allow-list. Falls
232
+ // back to the static facet-direct table for known facet-only commands.
233
+ // Returns null
234
+ // (→ exit 127) for everything unknown.
235
+ resolve: (name) => {
236
+ const commandName = normalizeCpCommandName(name);
237
+ const registry = self._cpRegistry;
238
+ if (registry && typeof registry.has === 'function' && registry.has(commandName)) {
239
+ // Registered — classify by name. Reuse the static table so
240
+ // facet-direct commands (node/npm/git/...) keep their kind
241
+ // even when they ALSO happen to be registry entries.
242
+ return _classifyCommand(commandName) || { kind: 'pure-builtin' };
243
+ }
244
+ return _classifyCommand(commandName);
245
+ },
246
+ runPureBuiltin: async (pid, name, args, env, cwd, stdin, hooks) => {
247
+ const registry = self._cpRegistry;
248
+ if (!registry) {
249
+ hooks.onStderr(textBytes('cp: registry unavailable\n'));
250
+ return 127;
251
+ }
252
+ const commandName = normalizeCpCommandName(name);
253
+ const cmd = await registry.resolve(commandName);
254
+ if (!cmd) {
255
+ hooks.onStderr(textBytes(`${name}: command not found\n`));
256
+ return 127;
257
+ }
258
+ const cred = self.processes.cred(pid);
259
+ const ac = new AbortController();
260
+ const ctx = {
261
+ pid,
262
+ cred,
263
+ args, env, cwd,
264
+ vfs: self.sqliteFs.as(cred),
265
+ stdout: { write: (d) => hooks.onStdout(textBytes(String(d))) },
266
+ stderr: { write: (d) => hooks.onStderr(textBytes(String(d))) },
267
+ signal: ac.signal,
268
+ stdin: staticStdinReader(stdin),
269
+ setUmask: (mask) => { self.processes.setUmask(pid, mask); },
270
+ runAs: async (targetCred, argv) => {
271
+ if (argv.length === 0)
272
+ return 0;
273
+ const child = self.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
274
+ let exitCode = 1;
275
+ try {
276
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
277
+ return exitCode;
278
+ }
279
+ finally {
280
+ self.processes.exit(child.pid, exitCode);
281
+ }
282
+ },
283
+ };
284
+ try {
285
+ const code = await cmd(ctx);
286
+ return typeof code === 'number' ? code : 0;
287
+ }
288
+ catch (e) {
289
+ hooks.onStderr(textBytes(`${name}: ${e?.message || String(e)}\n`));
290
+ return 1;
291
+ }
292
+ },
293
+ };
294
+ // Construct the child-process Loader pool when the binding is available.
295
+ // Unit-test hosts without LOADER continue through direct dispatch.
296
+ let spawnPool;
297
+ try {
298
+ const envAny = runtimeContext.env;
299
+ if (envAny?.LOADER && typeof envAny.LOADER.get === 'function') {
300
+ spawnPool = new ChildProcessSpawnPool(runtimeContext.env, runtimeContext.ctx);
301
+ }
302
+ }
303
+ catch {
304
+ spawnPool = undefined;
305
+ }
306
+ self.facetProcessManager = new FacetProcessManager({
307
+ facetMgr: facetMgrAdapter,
308
+ processes: self.processes,
309
+ vfsForProcess: (pid) => self.sqliteFs.as(self.processes.cred(pid)),
310
+ commandRegistry: cmdRegistryAdapter,
311
+ shellExecutor: {
312
+ execute: async (pid, commandLine, env, cwd, stdin, hooks) => {
313
+ if (!self.shell) {
314
+ hooks.onStderr(textBytes('sh: shell unavailable\n'));
315
+ return 127;
316
+ }
317
+ const cred = self.processes.cred(pid);
318
+ const setUmask = (mask) => { self.processes.setUmask(pid, mask); };
319
+ const runAs = async (_parent, targetCred, argv) => {
320
+ if (argv.length === 0)
321
+ return 0;
322
+ const child = self.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
323
+ let exitCode = 1;
324
+ try {
325
+ exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
326
+ return exitCode;
327
+ }
328
+ finally {
329
+ self.processes.exit(child.pid, exitCode);
330
+ }
331
+ };
332
+ const result = await self.shell.execute(String(commandLine), {
333
+ cwd: cwd || '/home/user',
334
+ env: { ...self.shell.env, ...(env || {}) },
335
+ onStdout: hooks.onStdout,
336
+ onStderr: hooks.onStderr,
337
+ stdin,
338
+ isolateShellState: true,
339
+ commandContext: { pid, cred, setUmask },
340
+ runAs,
341
+ });
342
+ return typeof result?.exitCode === 'number' ? result.exitCode : 0;
343
+ },
344
+ },
345
+ ctx: runtimeContext.ctx,
346
+ spawnPool,
347
+ });
348
+ return self.facetProcessManager;
349
+ }
350
+ export function ensureFetchProxy(self, runtimeContext, log) {
351
+ if (self.fetchProxyEntrypoint)
352
+ return self.fetchProxyEntrypoint;
353
+ try {
354
+ const env = runtimeContext.env;
355
+ if (!env?.LOADER?.load) {
356
+ log?.('LOADER.load not available — using global fetch');
357
+ return null;
358
+ }
359
+ // Buffered proxy: reads the entire response body into an ArrayBuffer
360
+ // and returns it in ONE message instead of forwarding a ReadableStream.
361
+ // In workerd local dev, streaming responses across a service-binding
362
+ // RPC fabric opens a separate loopback socket PER chunk (~16KB), which
363
+ // exhausts ephemeral ports for larger installs (npm registry packuments
364
+ // are 500KB-3MB, tarballs up to 5MB). Buffering to arrayBuffer means
365
+ // 1 stub call = 1 loopback connection, not N connections.
366
+ //
367
+ // 32MB cap prevents a malformed giant response from OOMing the proxy
368
+ // isolate. Packages with tarballs larger than 32MB will fail to install
369
+ // cleanly (returned as 413 → caller treats as failed fetch).
370
+ const proxyCode = [
371
+ 'const MAX_BYTES = 32 * 1024 * 1024;',
372
+ 'export default {',
373
+ ' async fetch(request, workerEnv) {',
374
+ ' try {',
375
+ ' const body = await request.json();',
376
+ ' const resp = await fetch(body.url, {',
377
+ ' method: body.method || "GET",',
378
+ ' headers: body.headers || {},',
379
+ ' });',
380
+ ' // Check advertised Content-Length before buffering',
381
+ ' const clStr = resp.headers.get("content-length");',
382
+ ' if (clStr) {',
383
+ ' const cl = parseInt(clStr, 10);',
384
+ ' if (cl > MAX_BYTES) {',
385
+ ' return new Response(',
386
+ ' JSON.stringify({ error: "response too large: " + cl + " bytes (cap " + MAX_BYTES + ")" }),',
387
+ ' { status: 413, headers: { "Content-Type": "application/json" } }',
388
+ ' );',
389
+ ' }',
390
+ ' }',
391
+ ' // Buffer entire body — ONE message, not streamed chunks',
392
+ ' const buf = await resp.arrayBuffer();',
393
+ ' if (buf.byteLength > MAX_BYTES) {',
394
+ ' return new Response(',
395
+ ' JSON.stringify({ error: "response exceeded cap: " + buf.byteLength + " bytes" }),',
396
+ ' { status: 413, headers: { "Content-Type": "application/json" } }',
397
+ ' );',
398
+ ' }',
399
+ ' return new Response(buf, {',
400
+ ' status: resp.status,',
401
+ ' statusText: resp.statusText,',
402
+ ' headers: Object.fromEntries(resp.headers.entries()),',
403
+ ' });',
404
+ ' } catch (e) {',
405
+ ' return new Response(JSON.stringify({ error: e.message }), {',
406
+ ' status: 502,',
407
+ ' headers: { "Content-Type": "application/json" },',
408
+ ' });',
409
+ ' }',
410
+ ' }',
411
+ '};',
412
+ ].join('\n');
413
+ const worker = env.LOADER.load({
414
+ compatibilityDate: CF_COMPAT_DATE,
415
+ compatibilityFlags: ['nodejs_compat'],
416
+ mainModule: 'fetch-proxy.js',
417
+ modules: { 'fetch-proxy.js': proxyCode },
418
+ });
419
+ self.fetchProxyEntrypoint = worker.getEntrypoint();
420
+ log?.('Fetch proxy worker created (singleton)');
421
+ return self.fetchProxyEntrypoint;
422
+ }
423
+ catch (e) {
424
+ log?.(`Fetch proxy creation failed: ${e?.message}`);
425
+ return null;
426
+ }
427
+ }
428
+ export function buildFetchFn(self, runtimeContext, log) {
429
+ const entrypoint = self.ensureFetchProxy(log);
430
+ if (!entrypoint)
431
+ return undefined;
432
+ return async (url, init) => {
433
+ const headers = {};
434
+ if (init?.headers) {
435
+ if (init.headers instanceof Headers) {
436
+ init.headers.forEach((v, k) => { headers[k] = v; });
437
+ }
438
+ else if (typeof init.headers === 'object') {
439
+ Object.assign(headers, init.headers);
440
+ }
441
+ }
442
+ return entrypoint.fetch(new Request('http://fetch-proxy/do-fetch', {
443
+ method: 'POST',
444
+ body: JSON.stringify({ url, method: init?.method || 'GET', headers }),
445
+ }));
446
+ };
447
+ }
448
+ export async function ensureNpmInstaller(self, runtimeContext, onProgress) {
449
+ self.ensureSqliteFs();
450
+ if (!self.esbuildService) {
451
+ if (!self.sqliteFs)
452
+ throw new Error('Session VFS is not initialized');
453
+ self.esbuildService = new EsbuildService(self.sqliteFs.as(CRED_KERNEL));
454
+ }
455
+ // Lazy-load the installer (+ its ~216 KB resolver/facet/loader-pool
456
+ // subgraph) on first npm use so it stays out of the cold script-eval
457
+ // graph. The install command paths that call this are already async.
458
+ const { NpmInstaller } = await import('../npm/installer.js');
459
+ // ── Lazy fetch-proxy ────────────────────────────────────────────
460
+ // The fetch-proxy is a singleton dynamic worker (LOADER.load) that
461
+ // buffers registry responses to dodge wrangler-local-dev port
462
+ // exhaustion. It is only needed for the in-supervisor npm paths.
463
+ // When the resolver and install paths run in facets (default-on),
464
+ // they use bare globalThis.fetch and need no proxy.
465
+ //
466
+ // workerd has a per-DO cap on concurrent dynamic workers (~5-6
467
+ // empirically). A permanent live proxy worker eats one of those
468
+ // slots for the entire DO lifetime, so the proxy is built only when
469
+ // any facet path is disabled via its env flag.
470
+ const useFacetResolver = self._envFlagDefaultOn('NIMBUS_FACET_RESOLVER');
471
+ const useFacetInstall = self._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL');
472
+ const useBatchFacet = self._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL_BATCH');
473
+ const needProxy = !(useFacetResolver && useFacetInstall && useBatchFacet);
474
+ const fetchFn = needProxy ? self.buildFetchFn(onProgress) : undefined;
475
+ if (!needProxy) {
476
+ onProgress?.(`[npm] Lazy fetch-proxy: skipped (all facet paths default-on)`);
477
+ }
478
+ self.npmInstaller = new NpmInstaller(self.sqliteFs, runtimeContext.ctx.storage.sql, {
479
+ esbuild: self.esbuildService,
480
+ bundlePool: self.ensureBundlePool(),
481
+ ctx: runtimeContext.ctx,
482
+ env: runtimeContext.env,
483
+ onProgress,
484
+ fetchFn,
485
+ });
486
+ return self.npmInstaller;
487
+ }
488
+ export function _envFlagDefaultOn(self, runtimeContext, name) {
489
+ const raw = runtimeContext.env?.[name];
490
+ if (raw === undefined || raw === null)
491
+ return true;
492
+ const s = String(raw).toLowerCase();
493
+ if (s === '0' || s === '' || s === 'false' || s === 'off' || s === 'no')
494
+ return false;
495
+ return true;
496
+ }
497
+ export function ensureGlobalPrefixDirs(self, runtimeContext, prefix) {
498
+ const fs = self.sqliteFs.as(CRED_SESSION_USER);
499
+ const dirs = [
500
+ prefix,
501
+ `${prefix}/lib`,
502
+ `${prefix}/lib/node_modules`,
503
+ `${prefix}/bin`,
504
+ ];
505
+ for (const dir of dirs) {
506
+ if (!fs.exists(dir))
507
+ fs.mkdir(dir, { recursive: true });
508
+ }
509
+ }
510
+ export function bindRuntimeServices(host, context) {
511
+ return {
512
+ ensureBundlePool: ensureBundlePool.bind(null, host, context),
513
+ ensureFacetManager: ensureFacetManager.bind(null, host, context),
514
+ _ensureWebSocketRelay: _ensureWebSocketRelay.bind(null, host, context),
515
+ _ensureFacetProcessManager: _ensureFacetProcessManager.bind(null, host, context),
516
+ ensureFetchProxy: ensureFetchProxy.bind(null, host, context),
517
+ buildFetchFn: buildFetchFn.bind(null, host, context),
518
+ ensureNpmInstaller: ensureNpmInstaller.bind(null, host, context),
519
+ _envFlagDefaultOn: _envFlagDefaultOn.bind(null, host, context),
520
+ ensureGlobalPrefixDirs: ensureGlobalPrefixDirs.bind(null, host, context),
521
+ };
522
+ }
@@ -16,7 +16,7 @@
16
16
  * W7_MAGIC, W7_MAX_RECORD_BYTES.
17
17
  *
18
18
  * Tar size: 4.62 KiB
19
- * W7 size: 28.97 KiB
19
+ * W7 size: 29.55 KiB
20
20
  */
21
21
  export declare const TAR_STREAM_PREAMBLE: string;
22
22
  export declare const W7_FRAME_PREAMBLE: string;
@@ -1 +1 @@
1
- {"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAAk1J,CAAC;AAEr3J,eAAO,MAAM,iBAAiB,EAAE,MAA4/7B,CAAC"}
1
+ {"version":3,"file":"generated-workers.d.ts","sourceRoot":"","sources":["../../src/loaders/generated-workers.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;GAmBG;AAEH,eAAO,MAAM,mBAAmB,EAAE,MAAk1J,CAAC;AAEr3J,eAAO,MAAM,iBAAiB,EAAE,MAAwm9B,CAAC"}