@nimbus-sh/worker 0.6.0 → 0.8.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 (184) hide show
  1. package/README.md +2 -2
  2. package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
  3. package/dist/cirrus-npm-cjs.generated.js +1 -1
  4. package/dist/facets/cirrus-real.d.ts.map +1 -1
  5. package/dist/facets/cirrus-real.js +6 -5
  6. package/dist/facets/compose.d.ts +148 -0
  7. package/dist/facets/compose.d.ts.map +1 -0
  8. package/dist/facets/compose.js +146 -0
  9. package/dist/facets/durable-images.d.ts +9 -5
  10. package/dist/facets/durable-images.d.ts.map +1 -1
  11. package/dist/facets/durable-images.js +17 -7
  12. package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
  13. package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
  14. package/dist/facets/esbuild-bundle-pool.js +122 -0
  15. package/dist/facets/exec-telemetry.d.ts +8 -0
  16. package/dist/facets/exec-telemetry.d.ts.map +1 -1
  17. package/dist/facets/manager.d.ts +240 -109
  18. package/dist/facets/manager.d.ts.map +1 -1
  19. package/dist/facets/manager.js +677 -342
  20. package/dist/facets/process.d.ts +20 -16
  21. package/dist/facets/process.d.ts.map +1 -1
  22. package/dist/facets/process.js +59 -28
  23. package/dist/facets/real-vite-fs-shim.js +2 -2
  24. package/dist/facets/real-vite-hmr.d.ts +3 -1
  25. package/dist/facets/real-vite-hmr.d.ts.map +1 -1
  26. package/dist/facets/real-vite-hmr.js +18 -8
  27. package/dist/facets/vite-dev-server.d.ts +25 -47
  28. package/dist/facets/vite-dev-server.d.ts.map +1 -1
  29. package/dist/facets/vite-dev-server.js +185 -219
  30. package/dist/facets/wasm-image-digest.d.ts +25 -0
  31. package/dist/facets/wasm-image-digest.d.ts.map +1 -0
  32. package/dist/facets/wasm-image-digest.js +25 -0
  33. package/dist/facets/wasm-swap-registry.d.ts +51 -92
  34. package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
  35. package/dist/facets/wasm-swap-registry.js +68 -227
  36. package/dist/facets/ws-terminal.d.ts +9 -31
  37. package/dist/facets/ws-terminal.d.ts.map +1 -1
  38. package/dist/facets/ws-terminal.js +58 -51
  39. package/dist/git/network-facet.d.ts.map +1 -1
  40. package/dist/git/network-facet.js +6 -5
  41. package/dist/hosted/commands.d.ts +12 -0
  42. package/dist/hosted/commands.d.ts.map +1 -0
  43. package/dist/hosted/commands.js +1658 -0
  44. package/dist/hosted/runtime.d.ts +114 -0
  45. package/dist/hosted/runtime.d.ts.map +1 -0
  46. package/dist/hosted/runtime.js +288 -0
  47. package/dist/hosted/services.d.ts +40 -0
  48. package/dist/hosted/services.d.ts.map +1 -0
  49. package/dist/hosted/services.js +512 -0
  50. package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
  51. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
  52. package/dist/loaders/child-process/spawn-pool.js +3 -2
  53. package/dist/loaders/generated-workers.d.ts +2 -2
  54. package/dist/loaders/generated-workers.d.ts.map +1 -1
  55. package/dist/loaders/generated-workers.js +3 -3
  56. package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
  57. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
  58. package/dist/loaders/npm-resolve-preamble.js +34 -108
  59. package/dist/node-shims-artifact.generated.js +3 -3
  60. package/dist/npm/install-batch-facet.d.ts +3 -2
  61. package/dist/npm/install-batch-facet.d.ts.map +1 -1
  62. package/dist/npm/install-batch-facet.js +45 -15
  63. package/dist/npm/installer.d.ts +22 -17
  64. package/dist/npm/installer.d.ts.map +1 -1
  65. package/dist/npm/installer.js +209 -193
  66. package/dist/npm/pre-bundle-facet.d.ts +1 -1
  67. package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
  68. package/dist/npm/pre-bundle-facet.js +1 -1
  69. package/dist/npm/resolve-facet.d.ts +6 -0
  70. package/dist/npm/resolve-facet.d.ts.map +1 -1
  71. package/dist/npm/resolve-one-facet.d.ts +21 -20
  72. package/dist/npm/resolve-one-facet.d.ts.map +1 -1
  73. package/dist/npm/resolve-one-facet.js +85 -84
  74. package/dist/npm/semver.d.ts +59 -0
  75. package/dist/npm/semver.d.ts.map +1 -0
  76. package/dist/npm/semver.js +226 -0
  77. package/dist/router/remote-api.js +31 -1
  78. package/dist/runtime/bash-repl.d.ts +1 -1
  79. package/dist/runtime/bash-repl.d.ts.map +1 -1
  80. package/dist/runtime/bash-repl.js +38 -4
  81. package/dist/runtime/bun-runner.js +1 -1
  82. package/dist/runtime/html-entrypoint.d.ts +7 -0
  83. package/dist/runtime/html-entrypoint.d.ts.map +1 -1
  84. package/dist/runtime/html-entrypoint.js +18 -2
  85. package/dist/runtime/node-runner.d.ts +4 -1
  86. package/dist/runtime/node-runner.d.ts.map +1 -1
  87. package/dist/runtime/node-runner.js +9 -2
  88. package/dist/runtime/node-shims.d.ts.map +1 -1
  89. package/dist/runtime/node-shims.js +305 -29
  90. package/dist/runtime/opencode-facet-runner.js +16 -16
  91. package/dist/runtime/package-manager.d.ts +35 -122
  92. package/dist/runtime/package-manager.d.ts.map +1 -1
  93. package/dist/runtime/package-manager.js +61 -596
  94. package/dist/runtime/process-logs-api.d.ts +1 -1
  95. package/dist/runtime/process-logs-api.d.ts.map +1 -1
  96. package/dist/runtime/process-logs-api.js +1 -1
  97. package/dist/runtime/python-repl.d.ts +2 -1
  98. package/dist/runtime/python-repl.d.ts.map +1 -1
  99. package/dist/runtime/python-repl.js +82 -59
  100. package/dist/runtime/repl-session.d.ts +9 -47
  101. package/dist/runtime/repl-session.d.ts.map +1 -1
  102. package/dist/runtime/repl-session.js +105 -105
  103. package/dist/runtime/runtime-catalog.d.ts +13 -0
  104. package/dist/runtime/runtime-catalog.d.ts.map +1 -1
  105. package/dist/runtime/runtime-catalog.js +88 -0
  106. package/dist/session/agent.d.ts +1 -21
  107. package/dist/session/agent.d.ts.map +1 -1
  108. package/dist/session/ai.d.ts +5 -1
  109. package/dist/session/ai.d.ts.map +1 -1
  110. package/dist/session/ai.js +7 -0
  111. package/dist/session/helpers.d.ts +38 -0
  112. package/dist/session/helpers.d.ts.map +1 -1
  113. package/dist/session/helpers.js +76 -0
  114. package/dist/session/hibernation.d.ts +2 -1
  115. package/dist/session/hibernation.d.ts.map +1 -1
  116. package/dist/session/hibernation.js +7 -4
  117. package/dist/session/init-phases.d.ts +1 -1
  118. package/dist/session/init-phases.d.ts.map +1 -1
  119. package/dist/session/init-phases.js +2 -12
  120. package/dist/session/init.d.ts +15 -36
  121. package/dist/session/init.d.ts.map +1 -1
  122. package/dist/session/init.js +50 -2327
  123. package/dist/session/loopback.d.ts +2 -29
  124. package/dist/session/loopback.d.ts.map +1 -1
  125. package/dist/session/loopback.js +3 -27
  126. package/dist/session/nimbus-session.d.ts +77 -34
  127. package/dist/session/nimbus-session.d.ts.map +1 -1
  128. package/dist/session/nimbus-session.js +126 -514
  129. package/dist/session/npm-install-port.d.ts +18 -0
  130. package/dist/session/npm-install-port.d.ts.map +1 -0
  131. package/dist/session/npm-install-port.js +37 -0
  132. package/dist/session/port-capability.d.ts +44 -0
  133. package/dist/session/port-capability.d.ts.map +1 -1
  134. package/dist/session/port-capability.js +110 -0
  135. package/dist/session/programmatic.d.ts +36 -43
  136. package/dist/session/programmatic.d.ts.map +1 -1
  137. package/dist/session/programmatic.js +109 -115
  138. package/dist/session/routes.d.ts +30 -17
  139. package/dist/session/routes.d.ts.map +1 -1
  140. package/dist/session/routes.js +42 -131
  141. package/dist/session/rpc.d.ts +15 -15
  142. package/dist/session/rpc.d.ts.map +1 -1
  143. package/dist/session/rpc.js +98 -49
  144. package/dist/session/serving-port.d.ts +27 -0
  145. package/dist/session/serving-port.d.ts.map +1 -0
  146. package/dist/session/serving-port.js +27 -0
  147. package/dist/session/start-real-vite.d.ts +12 -0
  148. package/dist/session/start-real-vite.d.ts.map +1 -1
  149. package/dist/session/start-real-vite.js +4 -4
  150. package/dist/session/supervisor-op.d.ts +4 -4
  151. package/dist/session/supervisor-op.d.ts.map +1 -1
  152. package/dist/session/supervisor-op.js +24 -10
  153. package/dist/session/supervisor-rpc.d.ts +7 -7
  154. package/dist/session/supervisor-rpc.d.ts.map +1 -1
  155. package/dist/session/supervisor-rpc.js +4 -0
  156. package/dist/session/vite-command.d.ts +18 -0
  157. package/dist/session/vite-command.d.ts.map +1 -0
  158. package/dist/session/vite-command.js +609 -0
  159. package/dist/session/ws.d.ts +61 -0
  160. package/dist/session/ws.d.ts.map +1 -1
  161. package/dist/session/ws.js +122 -12
  162. package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
  163. package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
  164. package/dist/shell/npm-bin-entrypoints.js +12 -85
  165. package/dist/workspace-host.d.ts +6 -0
  166. package/dist/workspace-host.d.ts.map +1 -0
  167. package/dist/workspace-host.js +5 -0
  168. package/dist/workspace.d.ts +2 -0
  169. package/dist/workspace.d.ts.map +1 -0
  170. package/dist/workspace.js +1 -0
  171. package/package.json +34 -9
  172. package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
  173. package/public/index.html +1 -1
  174. package/scripts/bundle-facet-workers.mjs +1 -1
  175. package/scripts/bundle-npm-cjs.mjs +2 -2
  176. package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
  177. package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
  178. package/dist/facets/on-demand-bundle-gate.js +0 -125
  179. package/dist/npm/install-args.d.ts +0 -21
  180. package/dist/npm/install-args.d.ts.map +0 -1
  181. package/dist/npm/install-args.js +0 -70
  182. package/dist/npm/npm-log.d.ts +0 -31
  183. package/dist/npm/npm-log.d.ts.map +0 -1
  184. package/dist/npm/npm-log.js +0 -51
@@ -1,24 +1,10 @@
1
- /**
2
- * nimbus-session.ts — NimbusSession Durable Object (v2.0).
3
- *
4
- * The supervisor DO that owns the VFS, shell, and all commands.
5
- * `node` execution is delegated to dynamic workers via LOADER.load().
6
- * IPC between facets and the supervisor flows through SupervisorRPC.
7
- */
8
- import { staticStdinReader } from '@nimbus-sh/core/shell/stdin-adapter.js';
1
+ import * as runtimeServices from '../hosted/services.js';
9
2
  import { DurableObject as CloudflareDurableObject } from 'cloudflare:workers';
10
3
  import { SqliteVFS } from '@nimbus-sh/core/vfs/sqlite-vfs.js';
11
- import { FacetManager } from '../facets/manager.js';
12
- import { resolveDurableWorkerImage } from '../facets/durable-images.js';
13
- import { FacetProcessManager } from '../facets/process.js';
14
- import { ChildProcessSpawnPool } from '../loaders/child-process/spawn-pool.js';
15
4
  import { SessionProcessSupervisor } from '@nimbus-sh/core/runtime/session-process-supervisor.js';
16
5
  import { PID_GEN_STRIDE } from '@nimbus-sh/core/runtime/process-table.js';
17
- import { CRED_KERNEL, CRED_SESSION_USER, } from '@nimbus-sh/core/runtime/os-contracts.js';
6
+ import { CRED_KERNEL, CRED_SESSION_USER } from '@nimbus-sh/core/runtime/os-contracts.js';
18
7
  import { PortRegistry } from '@nimbus-sh/core/runtime/port-registry.js';
19
- import { EsbuildService } from '@nimbus-sh/core/runtime/esbuild-service.js';
20
- import { ESBUILD_TRANSFORM_WORKER_ID, esbuildTransformWorkerCode, } from '../facets/esbuild-transform.js';
21
- import { fetchEsbuildWasmBytes } from '../runtime/esbuild-wasm-bytes.js';
22
8
  import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js';
23
9
  // S10: oom-discriminator helpers (recordFailure, getFailures,
24
10
  // getLastRpcFrame, getLastFacetId, snapshotForStorage, rehydrateFromStorage)
@@ -32,58 +18,63 @@ import { registerAllocObserver } from '@nimbus-sh/platform/heavy-alloc-coord.js'
32
18
  // longer references them directly. Phase 2 A'.5 renamed the function
33
19
  // (was getEsbuildWasmBytes; cached) to fetchEsbuildWasmBytes (no
34
20
  // supervisor cache; goes through env.ASSETS on demand).
21
+ // S10: oom-discriminator helpers (recordFailure, getFailures,
22
+ // getLastRpcFrame, getLastFacetId, snapshotForStorage, rehydrateFromStorage)
23
+ // moved to sibling modules (-rpc uses recordFailure for _reportExternalExit;
24
+ // -ws uses recordFailure for webSocketError; -diag uses getFailures +
25
+ // snapshotForStorage + rehydrateFromStorage; -routes uses getFailures +
26
+ // getLastRpcFrame + getLastFacetId for /api/_diag/memory). Class file
27
+ // no longer references any of them directly.
28
+ // S10: classifyError, LRU_MAX_ENTRIES, fetchEsbuildWasmBytes moved to
29
+ // sibling modules (-rpc, -routes, esbuild-wasm-bytes); class file no
30
+ // longer references them directly. Phase 2 A'.5 renamed the function
31
+ // (was getEsbuildWasmBytes; cached) to fetchEsbuildWasmBytes (no
32
+ // supervisor cache; goes through env.ASSETS on demand).
35
33
  import { adoptCtxExports } from '@nimbus-sh/fabric/composition.js';
36
- import { NIMBUS_VERSION, DEFAULT_MOUNT_POINTS, CF_COMPAT_DATE } from '@nimbus-sh/core/constants.js';
34
+ import { NIMBUS_VERSION, DEFAULT_MOUNT_POINTS } from '@nimbus-sh/core/constants.js';
37
35
  import { seedBaseFilesystem } from '@nimbus-sh/core/workspace';
38
- import { seedProject } from '@nimbus-sh/core/vfs/seed-project.js';
36
+ import { routeSessionLoopback } from './ai.js';
37
+ import { seedProject, SEED_PROJECT_NAME } from '@nimbus-sh/core/vfs/seed-project.js';
39
38
  import { BASE_PATH_HEADER } from '../_shared/session-router.js';
40
39
  import { ATTACH_BOOTSTRAP_JTI_KEY_PREFIX, SESSION_DESTROYED_KEY } from './keys.js';
41
40
  import { appendScrollback } from './state-store.js';
42
- import { notifyTerminalEvent, wireProcessLogSocketBroadcast } from '../runtime/process-logs-api.js';
41
+ import { wireProcessLogSocketBroadcast } from '../runtime/process-logs-api.js';
42
+ // S3: tryEnableReplicas + getReplicaState extracted to ./nimbus-session-replica.ts.
43
43
  // S3: tryEnableReplicas + getReplicaState extracted to ./nimbus-session-replica.ts.
44
- import { wireReplicasOnConstruct as _w12WireReplicasOnConstruct, getReplicaState as _w12GetReplicaState, } from './replica-routes.js';
44
+ import { wireReplicasOnConstruct as _w12WireReplicasOnConstruct, getReplicaState as _w12GetReplicaState } from './replica-routes.js';
45
45
  // S5: storage-key constants moved to ./nimbus-session-keys.ts; consumed by
46
46
  // sibling modules (-hib, -diag, -ws). The class file itself no longer
47
47
  // references any storage key directly.
48
48
  // S4: W9 hibernation surface extracted.
49
- import { wireHibernationOnConstruct as _w9WireHibernationOnConstruct, wireProcessLogPersist as _w9DoWireProcessLogPersist, ensureHibSchema as _w9DoEnsureHibSchema, scheduleHibFlush as _w9DoScheduleHibFlush, clearDestroyedTombstone as _w1ClearDestroyedTombstone, dispatchAlarm as _w9DoDispatchAlarm, flushOnClose as _w9DoFlushOnClose, } from './hibernation.js';
49
+ // S5: storage-key constants moved to ./nimbus-session-keys.ts; consumed by
50
+ // sibling modules (-hib, -diag, -ws). The class file itself no longer
51
+ // references any storage key directly.
52
+ // S4: W9 hibernation surface extracted.
53
+ import { wireHibernationOnConstruct as _w9WireHibernationOnConstruct, wireProcessLogPersist as _w9DoWireProcessLogPersist, ensureHibSchema as _w9DoEnsureHibSchema, scheduleHibFlush as _w9DoScheduleHibFlush, clearDestroyedTombstone as _w1ClearDestroyedTombstone, dispatchAlarm as _w9DoDispatchAlarm, flushOnClose as _w9DoFlushOnClose } from './hibernation.js';
50
54
  import { timers } from '@nimbus-sh/fabric/timers.js';
51
55
  import { adoptGeneration, generation } from '@nimbus-sh/fabric/generation.js';
52
56
  // S6: initSession (1875 LOC of cmd registrations + boot wiring) extracted.
57
+ // S6: initSession (1875 LOC of cmd registrations + boot wiring) extracted.
53
58
  import { initSession as _w11InitSession } from './init.js';
54
59
  // S7: webSocket lifecycle (message, close, error, F1 discriminator,
55
60
  // _w5SafePersistRing) extracted.
56
- import { wsMessage as _wsDoMessage, wsClose as _wsDoClose, wsError as _wsDoError, safePersistRing as _wsDoSafePersistRing, } from './ws.js';
61
+ // S7: webSocket lifecycle (message, close, error, F1 discriminator,
62
+ // _w5SafePersistRing) extracted.
63
+ import { wsMessage as _wsDoMessage, wsClose as _wsDoClose, wsError as _wsDoError, safePersistRing as _wsDoSafePersistRing } from './ws.js';
64
+ // S8: Supervisor RPC + W8 cp* + legacy VFS impls extracted.
57
65
  // S8: Supervisor RPC + W8 cp* + legacy VFS impls extracted.
58
66
  import * as _rpc from './rpc.js';
59
67
  import { buildSessionSupervisorOps } from './supervisor-op.js';
60
- import { processHostFor } from '../loaders/process-host.js';
61
- // The supervisor terminates a facet's outbound sockets so inbound frames
62
- // arrive as supervisor replies (VFS coherence witness 3).
63
- import { WebSocketRelay } from './ws-relay.js';
68
+ // S9: HTTP fetch routing extracted (combined S9a + S9b).
64
69
  // S9: HTTP fetch routing extracted (combined S9a + S9b).
65
70
  import * as _routes from './routes.js';
71
+ import * as _portCapability from './port-capability.js';
72
+ // Programmatic SDK RPC surface.
66
73
  // Programmatic SDK RPC surface.
67
74
  import * as _programmatic from './programmatic.js';
68
75
  // S10: heap probe + W5 OOM-ring persistence extracted.
76
+ // S10: heap probe + W5 OOM-ring persistence extracted.
69
77
  import * as _diag from './diag.js';
70
- // ── Pure helpers extracted to ./nimbus-session-helpers.ts (S1) ────────
71
- //
72
- // renderNoDevServerHtml, BUNDLER_BIN_PREFIXES, NIMBUS_UNSUPPORTED_BINS,
73
- // WRANGLER_IGNORED_FLAGS{,_WITH_VALUE}, WRANGLER_UNSUPPORTED_CONFIG_FIELDS,
74
- // filterWranglerFlags, detectUnsupportedWranglerConfig, _CP_FACET_DIRECT,
75
- // _CP_PURE_BUILTIN, _classifyCommand, detectBundlerBin, checkNodeModulesGuard
76
- // all live in the helpers module now.
77
- //
78
- // They are imported here (so call sites in this file work unchanged) and
79
- // re-exported (so external callers importing them from
80
- // `./nimbus-session.js` keep working — back-compat).
81
- //
82
- // (esbuild wasm bytes are fetched from env.ASSETS by
83
- // src/esbuild-wasm-bytes.ts at pool-construction time; A'.5 dropped
84
- // the supervisor-resident cache + the SUPERVISOR.getEsbuildWasm RPC.)
85
- // Helpers needed by this class file's own logic (not just re-export).
86
- import { _classifyCommand, } from './helpers.js';
87
78
  import { z } from 'zod/v4';
88
79
  const CpFacetDirectPayloadSchema = z.object({
89
80
  command: z.unknown().optional().transform((value) => value == null ? '' : String(value)),
@@ -170,7 +161,7 @@ Cloud-native development environment on Cloudflare Workers.
170
161
  | --- | --- |
171
162
  | Run JavaScript | \`node hello.js\` |
172
163
  | Install npm packages | \`npm install <pkg>\` |
173
- | Start the starter app | \`cd app && npm install && npm run dev\` |
164
+ | Start the starter app | \`cd ${SEED_PROJECT_NAME} && npm install && npm run dev\` |
174
165
  | Run a Cloudflare Worker locally | \`nimbus-wrangler dev\` |
175
166
 
176
167
  ## AI Models
@@ -243,13 +234,36 @@ The editor opens this file in Markdown preview mode by default. Use
243
234
  `;
244
235
  }
245
236
  export class NimbusSession extends CloudflareDurableObject {
237
+ #runtimeServices;
246
238
  // this.ctx and this.env are provided by the DurableObject base class
239
+ runtimeWorkspace = null;
240
+ runtimeReady = null;
241
+ get runtimeManager() {
242
+ if (!this.runtimeWorkspace)
243
+ throw new Error('Nimbus runtime is not initialized');
244
+ return this.runtimeWorkspace.runtimes;
245
+ }
246
+ async routeLoopback(port, request) {
247
+ return routeSessionLoopback({ ctx: this.ctx, env: this.env, portRegistry: this.portRegistry }, port, request);
248
+ }
249
+ async ensureRuntimeReady() {
250
+ if (this.shell)
251
+ return;
252
+ this.runtimeReady ??= this.initSession(null).then(() => {
253
+ this._b4Phase = 'drained';
254
+ }).finally(() => { this.runtimeReady = null; });
255
+ await this.runtimeReady;
256
+ }
247
257
  sqliteFs = null;
248
258
  kernel = null;
249
259
  shell = null;
250
260
  shellProcessPid = null;
251
261
  terminal = null;
252
- facetManager = null;
262
+ /** The composed manager is the one field; `.manager` is derived. */
263
+ facetManagerComposed = null;
264
+ get facetManager() {
265
+ return this.facetManagerComposed?.manager ?? null;
266
+ }
253
267
  /** W8: child_process broker. Lazy — only constructed when first cp* RPC arrives. */
254
268
  facetProcessManager = null;
255
269
  /**
@@ -259,6 +273,12 @@ export class NimbusSession extends CloudflareDurableObject {
259
273
  */
260
274
  webSocketRelay = null;
261
275
  esbuildService = null;
276
+ /**
277
+ * The session's single esbuild facet pool, shared by the npm installer's
278
+ * pre-bundler and the dev server's on-demand /@modules/ path. Lazy; see
279
+ * ensureBundlePool. Disposed with the installer and dev server.
280
+ */
281
+ bundlePool = null;
262
282
  viteDevServer = null;
263
283
  /**
264
284
  * runtime primitive support (P5): PID + port the default-Cirrus vite shim is
@@ -406,7 +426,13 @@ export class NimbusSession extends CloudflareDurableObject {
406
426
  */
407
427
  wranglerAliasBannerShown = false;
408
428
  constructor(ctx, env) {
409
- super(ctx, env); // enables RPC on the DO
429
+ super(ctx, env);
430
+ this.#runtimeServices = runtimeServices.bindRuntimeServices(this, {
431
+ ctx,
432
+ env,
433
+ notify: (line) => this._notifySession(line),
434
+ requestLaunchTurn: (at) => this._scheduleLaunchTurn(at),
435
+ });
410
436
  // In `wrangler dev`, the outer Worker and this DO share a single
411
437
  // workerd process, so the `adoptCtxExports(ctx.exports)` call in the
412
438
  // outer fetch handler (src/index.ts) is visible here via the
@@ -656,23 +682,23 @@ export class NimbusSession extends CloudflareDurableObject {
656
682
  supervisorOp(envelope) {
657
683
  return this.supervisorOps().dispatch(envelope);
658
684
  }
659
- async _rpcReadFile(path, pid) { return _rpc._rpcReadFile(this, path, pid); }
660
- async _rpcReadFileBytes(path, pid) { return _rpc._rpcReadFileBytes(this, path, pid); }
685
+ async _rpcReadFile(path, pid, cred) { return _rpc._rpcReadFile(this, path, pid, cred); }
686
+ async _rpcReadFileBytes(path, pid, cred) { return _rpc._rpcReadFileBytes(this, path, pid, cred); }
661
687
  async _rpcInnerDoFetch(req) { return _rpc._rpcInnerDoFetch(this, req); }
662
- async _rpcWriteFile(path, content, pid) { return _rpc._rpcWriteFile(this, path, content, pid); }
688
+ async _rpcWriteFile(path, content, pid, cred) { return _rpc._rpcWriteFile(this, path, content, pid, cred); }
663
689
  async _rpcWriteProtectedRootFile(rootPath, path, content) {
664
690
  return _rpc._rpcWriteProtectedRootFile(this, rootPath, path, content);
665
691
  }
666
- async _rpcStat(path, pid) { return _rpc._rpcStat(this, path, pid); }
667
- async _rpcLstat(path, pid) { return _rpc._rpcLstat(this, path, pid); }
692
+ async _rpcStat(path, pid, cred) { return _rpc._rpcStat(this, path, pid, cred); }
693
+ async _rpcLstat(path, pid, cred) { return _rpc._rpcLstat(this, path, pid, cred); }
668
694
  async _rpcHasLegacySymlinkUnder(path, pid) {
669
695
  return _rpc._rpcHasLegacySymlinkUnder(this, path, pid);
670
696
  }
671
697
  async _rpcUtimes(path, atimeMs, mtimeMs, pid) {
672
698
  return _rpc._rpcUtimes(this, path, atimeMs, mtimeMs, pid);
673
699
  }
674
- async _rpcChmod(path, mode, pid) {
675
- return _rpc._rpcChmod(this, path, mode, pid);
700
+ async _rpcChmod(path, mode, pid, cred) {
701
+ return _rpc._rpcChmod(this, path, mode, pid, cred);
676
702
  }
677
703
  async _rpcAccess(path, mode, pid) {
678
704
  return _rpc._rpcAccess(this, path, mode, pid);
@@ -683,11 +709,11 @@ export class NimbusSession extends CloudflareDurableObject {
683
709
  async _rpcSetUmask(mask, pid) {
684
710
  return _rpc._rpcSetUmask(this, mask, pid);
685
711
  }
686
- async _rpcReaddir(path, pid) { return _rpc._rpcReaddir(this, path, pid); }
687
- async _rpcExists(path, pid) { return _rpc._rpcExists(this, path, pid); }
688
- async _rpcMkdir(path, pid) { return _rpc._rpcMkdir(this, path, pid); }
712
+ async _rpcReaddir(path, pid, cred) { return _rpc._rpcReaddir(this, path, pid, cred); }
713
+ async _rpcExists(path, pid, cred) { return _rpc._rpcExists(this, path, pid, cred); }
714
+ async _rpcMkdir(path, pid, cred) { return _rpc._rpcMkdir(this, path, pid, cred); }
689
715
  async _rpcRmdir(path, pid) { return _rpc._rpcRmdir(this, path, pid); }
690
- async _rpcRename(from, to, pid) { return _rpc._rpcRename(this, from, to, pid); }
716
+ async _rpcRename(from, to, pid, cred) { return _rpc._rpcRename(this, from, to, pid, cred); }
691
717
  async _rpcReadlink(path, pid) { return _rpc._rpcReadlink(this, path, pid); }
692
718
  async _rpcSymlink(target, path, pid) { return _rpc._rpcSymlink(this, target, path, pid); }
693
719
  async _rpcFsRevision(path, pid) { return _rpc._rpcFsRevision(this, path, pid); }
@@ -717,8 +743,8 @@ export class NimbusSession extends CloudflareDurableObject {
717
743
  return _rpc._rpcFsWrite(this, handleId, offset, bytes, pid);
718
744
  }
719
745
  async _rpcFsClose(handleId, pid) { return _rpc._rpcFsClose(this, handleId, pid); }
720
- async _rpcFsReadRange(path, offset, length, pid) {
721
- return _rpc._rpcFsReadRange(this, path, offset, length, pid);
746
+ async _rpcFsReadRange(path, offset, length, pid, cred) {
747
+ return _rpc._rpcFsReadRange(this, path, offset, length, pid, cred);
722
748
  }
723
749
  async _rpcFsReadRangeUncached(path, offset, length, pid) {
724
750
  return _rpc._rpcFsReadRangeUncached(this, path, offset, length, pid);
@@ -838,7 +864,15 @@ export class NimbusSession extends CloudflareDurableObject {
838
864
  async _rpcUnexposePort(port) { return _programmatic.rpcUnexposePort(this, port); }
839
865
  /** Capability-authenticated port route, for an embedder holding the token. */
840
866
  async _rpcRouteCapabilityPort(port, capability, request, innerPath) {
841
- return _routes.routeCapabilityPort(this, port, capability, request, innerPath);
867
+ return _portCapability.routeCapabilityPort(this, port, capability, request, innerPath);
868
+ }
869
+ /** The port route's dev-server restore, delegated — lives in routes.ts where the other session routes do. */
870
+ restorePersistedDevServer(onlyPort) {
871
+ return _routes.restorePersistedDevServer(this, onlyPort);
872
+ }
873
+ /** The port route's in-DO HMR accept, delegated — same reason. */
874
+ acceptCirrusHmrWs(request) {
875
+ return _routes.acceptCirrusHmrWs(this, request);
842
876
  }
843
877
  /**
844
878
  * The port route's recovery seam: a durable application journalled but
@@ -855,7 +889,11 @@ export class NimbusSession extends CloudflareDurableObject {
855
889
  this.ensureFacetManager();
856
890
  return this.facetManager.ensureDurableAppOnPort(port);
857
891
  }
858
- async _rpcDeleteFile(path, options) { return _programmatic.rpcDeleteFile(this, path, options); }
892
+ async _rpcDeleteFile(path, options, cred) { return _programmatic.rpcDeleteFile(this, path, options, cred); }
893
+ /** Colocated embedders only (DO stub); not on the remote dispatcher. See `rpcSpawnWorker`. */
894
+ async _rpcSpawnWorker(workerCode, command, cwd, opts) {
895
+ return _programmatic.rpcSpawnWorker(this, workerCode, command, cwd, opts);
896
+ }
859
897
  async _rpcDestroy(options) { return _programmatic.rpcDestroy(this, options); }
860
898
  // Legacy VFS (direct method calls)
861
899
  vfsReadFile(path) { return _rpc.vfsReadFile(this, path); }
@@ -944,6 +982,7 @@ export class NimbusSession extends CloudflareDurableObject {
944
982
  console.warn('[nimbus/W5] ring rehydrate failed:', e?.message);
945
983
  });
946
984
  }
985
+ return this.sqliteFs;
947
986
  }
948
987
  // ── W5 Lever 5: ring buffer persistence on DO storage ─────────────────
949
988
  // Storage key W5_RING_STORAGE_KEY lives in ./nimbus-session-keys.ts (S5).
@@ -967,309 +1006,29 @@ export class NimbusSession extends CloudflareDurableObject {
967
1006
  _w5PersistRing() {
968
1007
  return _diag.persistRing(this, this.ctx);
969
1008
  }
970
- ensureFacetManager() {
971
- if (!this.facetManager) {
972
- this.facetManager = new FacetManager(this.ctx, this.env, this.processes, this.portRegistry, processHostFor, {
973
- onExternalExit: (pid, code, reason) => this._reportExternalExit(pid, code, reason),
974
- requestLaunchTurn: (notBefore) => { void this._scheduleLaunchTurn(notBefore); },
975
- notify: (line) => this._notifySession(line),
976
- transformLargeEsm: async (code, options) => {
977
- const loader = Reflect.get(this.env, 'LOADER');
978
- if (!loader || typeof loader.get !== 'function') {
979
- throw new Error('Nimbus: env.LOADER unavailable for isolated esbuild transform');
980
- }
981
- const assets = Reflect.get(this.env, 'ASSETS');
982
- if (!assets || typeof assets.fetch !== 'function') {
983
- throw new Error('Nimbus: env.ASSETS unavailable for isolated esbuild transform');
984
- }
985
- const worker = await loader.get(ESBUILD_TRANSFORM_WORKER_ID, async () => esbuildTransformWorkerCode(await fetchEsbuildWasmBytes({ ASSETS: assets })));
986
- const transformClass = worker.getDurableObjectClass('EsbuildTransformFacet');
987
- const facet = this.ctx.facets.get(`esbuild-transform-${ESBUILD_TRANSFORM_WORKER_ID}`, async () => ({ class: transformClass }));
988
- return facet.transform(code, options);
989
- },
990
- onSpawn: (pid, command, longRunning) => {
991
- const attachedTty = this.processes.get(pid)?.attachedTty === true;
992
- if (longRunning) {
993
- try {
994
- this.processes.openInput(pid);
995
- }
996
- catch { }
997
- }
998
- // Only surface long-running / user-visible spawns to keep
999
- // the terminal uncluttered. Short `node <file>` evals also
1000
- // get a line because users want the pid for `logs`/`kill`.
1001
- if (!this.terminal)
1002
- return;
1003
- const label = longRunning ? 'started (long-running)' : 'started';
1004
- this.terminal.write(`\x1b[2m[facet ${label}: pid=${pid} cmd="${command}"]\x1b[0m\r\n`);
1005
- // Structured event so the tabs UI can auto-open a log tab
1006
- // for long-running processes (vite, wrangler dev, etc.).
1007
- notifyTerminalEvent(this.terminal, {
1008
- type: 'spawn', pid, command, longRunning, attachedTty,
1009
- });
1010
- },
1011
- // The fallback resolver a self-owned durable spawn re-drives
1012
- // through: read the image blobs the spawn persisted under
1013
- // `.nimbus/images/<sha256>` and restore the launch's env and
1014
- // modules from them. An embedder-owned launch answers its own
1015
- // bookkeeping through resolveWorkerLaunch instead — this hook is
1016
- // only the fallback for applications nobody else is keeping, and
1017
- // because it cannot re-mint a live globalOutbound binding the
1018
- // spawn path refuses such a launch under it.
1019
- resolveWorkerLaunchFallback: async (recipe) => {
1020
- this.ensureSqliteFs();
1021
- return resolveDurableWorkerImage(this.sqliteFs, recipe);
1022
- },
1023
- });
1024
- }
1025
- if (this.facetManager && this.sqliteFs) {
1026
- this.facetManager.setVfs(this.sqliteFs);
1027
- // W3.5 Fix B: share the session's lazy esbuildService with the
1028
- // FacetManager so the bundle's ESM→CJS pre-pass doesn't pay
1029
- // wasm-init twice. If the session hasn't constructed one yet,
1030
- // FacetManager will lazy-create its own on first exec — same
1031
- // wasm bytes, same ~10ms init cost, just paid once per surface.
1032
- if (this.esbuildService) {
1033
- this.facetManager.setEsbuildService(this.esbuildService);
1034
- }
1035
- }
1036
- }
1009
+ /** The session's esbuild facet pool provider; constructing it does no work. */
1010
+ ensureBundlePool() { return this.#runtimeServices.ensureBundlePool(); }
1011
+ /**
1012
+ * The session's FacetManager, composed through the one factory every host
1013
+ * uses (`facets/compose.ts`). What is wired here is only what is the
1014
+ * session's: the terminal a spawn is announced on, the scrollback a notice
1015
+ * survives in, the alarm that grants a launch its next turn, and the exit
1016
+ * report that keeps the process table honest. The isolated esbuild
1017
+ * transform and the durable image-store fallback are the factory's.
1018
+ */
1019
+ ensureFacetManager() { return this.#runtimeServices.ensureFacetManager(); }
1037
1020
  /**
1038
1021
  * The supervisor-owned WebSocket relay. Lazy, because most sessions never
1039
1022
  * open a socket and the sockets it holds are live objects that must not
1040
1023
  * outlive the session.
1041
1024
  */
1042
- _ensureWebSocketRelay() {
1043
- if (!this.webSocketRelay)
1044
- this.webSocketRelay = new WebSocketRelay();
1045
- return this.webSocketRelay;
1046
- }
1025
+ _ensureWebSocketRelay() { return this.#runtimeServices._ensureWebSocketRelay(); }
1047
1026
  /**
1048
1027
  * W8: lazily construct the FacetProcessManager when the first cp* RPC
1049
1028
  * arrives. Wired with adapters that bridge the Nimbus shell command
1050
1029
  * registry to the FacetProcessManager's CommandRegistryLike contract.
1051
1030
  */
1052
- _ensureFacetProcessManager() {
1053
- if (this.facetProcessManager)
1054
- return this.facetProcessManager;
1055
- this.ensureSqliteFs();
1056
- this.ensureFacetManager();
1057
- // FacetProcessManager is statically imported at top-of-file (W8).
1058
- // No lazy-import: workerd doesn't ship CJS require, and the dynamic
1059
- // import would be async — making _ensureFacetProcessManager async
1060
- // would force every cp* RPC entry point to also be async on the
1061
- // promise-resolution path, which is fine but uglier. Compile-time
1062
- // tree-shaking handles unused-when-no-cp-RPC paths.
1063
- // Adapter for FacetManagerLike — wraps the existing FacetManager.exec
1064
- // with a streaming surface. Phase 1 simplification: facet-direct
1065
- // commands are dispatched through the shell registry the same way
1066
- // shell.execute does, but with the per-PID hooks routed.
1067
- const facetMgrAdapter = {
1068
- execStream: async (codeJson, opts, hooks) => {
1069
- // codeJson is a payload from FacetProcessManager._dispatch facet-direct
1070
- // path: {command, args, env, cwd, stdin}. We dispatch through the
1071
- // existing shell registry by resolving the command and invoking
1072
- // it with synthesized output streams that route to hooks.
1073
- let payload;
1074
- try {
1075
- const parsed = CpFacetDirectPayloadSchema.safeParse(JSON.parse(codeJson));
1076
- if (!parsed.success) {
1077
- hooks.onStderr('child_process: facet dispatch requires a broker-assigned process pid\n');
1078
- return 1;
1079
- }
1080
- payload = parsed.data;
1081
- }
1082
- catch {
1083
- hooks.onStderr('child_process: invalid facet dispatch payload\n');
1084
- return 1;
1085
- }
1086
- const registry = this._cpRegistry;
1087
- if (!registry) {
1088
- hooks.onStderr('child_process: command registry unavailable\n');
1089
- return 127;
1090
- }
1091
- const commandName = normalizeCpCommandName(payload.command);
1092
- const cmd = await registry.resolve(commandName);
1093
- if (!cmd) {
1094
- hooks.onStderr(`${payload.command}: command not found\n`);
1095
- return 127;
1096
- }
1097
- // Synthesize a CommandContext for the internal shell substrate.
1098
- const stdoutStream = { write: (d) => hooks.onStdout(String(d)) };
1099
- const stderrStream = { write: (d) => hooks.onStderr(String(d)) };
1100
- const ac = new AbortController();
1101
- const cred = this.processes.cred(payload.processPid);
1102
- const ctx = {
1103
- pid: payload.processPid,
1104
- cred,
1105
- args: payload.args || [],
1106
- env: payload.env || {},
1107
- cwd: payload.cwd || '/home/user',
1108
- vfs: this.sqliteFs.as(cred),
1109
- stdout: stdoutStream,
1110
- stderr: stderrStream,
1111
- signal: ac.signal,
1112
- // For commands that need stdin we pass a tiny adapter.
1113
- stdin: staticStdinReader(payload.stdin || ''),
1114
- setUmask: (mask) => { this.processes.setUmask(payload.processPid, mask); },
1115
- runAs: async (targetCred, argv) => {
1116
- if (argv.length === 0)
1117
- return 0;
1118
- const child = this.processes.spawn(argv.join(' '), argv, payload.cwd, { parentPid: payload.processPid, cred: targetCred });
1119
- let exitCode = 1;
1120
- try {
1121
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), payload.env, payload.cwd, payload.stdin, hooks);
1122
- return exitCode;
1123
- }
1124
- finally {
1125
- this.processes.exit(child.pid, exitCode);
1126
- }
1127
- },
1128
- __nimbusCaptureOutput: true,
1129
- };
1130
- try {
1131
- const code = await cmd(ctx);
1132
- return typeof code === 'number' ? code : 0;
1133
- }
1134
- catch (e) {
1135
- hooks.onStderr(`${payload.command}: ${e?.message || String(e)}\n`);
1136
- return 1;
1137
- }
1138
- },
1139
- abort: (facetName) => {
1140
- // Best-effort: relay to ctx.facets.abort, mirroring FacetManager.kill.
1141
- try {
1142
- this.ctx.facets?.abort?.(facetName, new Error('SIGKILL'));
1143
- }
1144
- catch { }
1145
- return true;
1146
- },
1147
- };
1148
- // Adapter for CommandRegistryLike. The shared shell registry is
1149
- // attached to `this._cpRegistry` by the shell-init path (see
1150
- // construction near line 2058 — registry passed as ctor arg there).
1151
- const cmdRegistryAdapter = {
1152
- // Consult the live shell registry FIRST so dynamically-registered
1153
- // commands (registerUnixCommands / git / npm / wrangler etc.) are seen
1154
- // even if they're not in the static _CP_PURE_BUILTIN allow-list. Falls
1155
- // back to the static facet-direct table for known facet-only commands.
1156
- // Returns null
1157
- // (→ exit 127) for everything unknown.
1158
- resolve: (name) => {
1159
- const commandName = normalizeCpCommandName(name);
1160
- const registry = this._cpRegistry;
1161
- if (registry && typeof registry.has === 'function' && registry.has(commandName)) {
1162
- // Registered — classify by name. Reuse the static table so
1163
- // facet-direct commands (node/npm/git/...) keep their kind
1164
- // even when they ALSO happen to be registry entries.
1165
- return _classifyCommand(commandName) || { kind: 'pure-builtin' };
1166
- }
1167
- return _classifyCommand(commandName);
1168
- },
1169
- runPureBuiltin: async (pid, name, args, env, cwd, stdin, hooks) => {
1170
- const registry = this._cpRegistry;
1171
- if (!registry) {
1172
- hooks.onStderr('cp: registry unavailable\n');
1173
- return 127;
1174
- }
1175
- const commandName = normalizeCpCommandName(name);
1176
- const cmd = await registry.resolve(commandName);
1177
- if (!cmd) {
1178
- hooks.onStderr(`${name}: command not found\n`);
1179
- return 127;
1180
- }
1181
- const cred = this.processes.cred(pid);
1182
- const ac = new AbortController();
1183
- const ctx = {
1184
- pid,
1185
- cred,
1186
- args, env, cwd,
1187
- vfs: this.sqliteFs.as(cred),
1188
- stdout: { write: (d) => hooks.onStdout(String(d)) },
1189
- stderr: { write: (d) => hooks.onStderr(String(d)) },
1190
- signal: ac.signal,
1191
- stdin: staticStdinReader(stdin),
1192
- setUmask: (mask) => { this.processes.setUmask(pid, mask); },
1193
- runAs: async (targetCred, argv) => {
1194
- if (argv.length === 0)
1195
- return 0;
1196
- const child = this.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
1197
- let exitCode = 1;
1198
- try {
1199
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
1200
- return exitCode;
1201
- }
1202
- finally {
1203
- this.processes.exit(child.pid, exitCode);
1204
- }
1205
- },
1206
- };
1207
- try {
1208
- const code = await cmd(ctx);
1209
- return typeof code === 'number' ? code : 0;
1210
- }
1211
- catch (e) {
1212
- hooks.onStderr(`${name}: ${e?.message || String(e)}\n`);
1213
- return 1;
1214
- }
1215
- },
1216
- };
1217
- // Construct the child-process Loader pool when the binding is available.
1218
- // Unit-test hosts without LOADER continue through direct dispatch.
1219
- let spawnPool;
1220
- try {
1221
- const envAny = this.env;
1222
- if (envAny?.LOADER && typeof envAny.LOADER.get === 'function') {
1223
- spawnPool = new ChildProcessSpawnPool(this.env, this.ctx);
1224
- }
1225
- }
1226
- catch {
1227
- spawnPool = undefined;
1228
- }
1229
- this.facetProcessManager = new FacetProcessManager({
1230
- facetMgr: facetMgrAdapter,
1231
- processes: this.processes,
1232
- vfsForProcess: (pid) => this.sqliteFs.as(this.processes.cred(pid)),
1233
- commandRegistry: cmdRegistryAdapter,
1234
- shellExecutor: {
1235
- execute: async (pid, commandLine, env, cwd, stdin, hooks) => {
1236
- if (!this.shell) {
1237
- hooks.onStderr('sh: shell unavailable\n');
1238
- return 127;
1239
- }
1240
- const cred = this.processes.cred(pid);
1241
- const setUmask = (mask) => { this.processes.setUmask(pid, mask); };
1242
- const runAs = async (_parent, targetCred, argv) => {
1243
- if (argv.length === 0)
1244
- return 0;
1245
- const child = this.processes.spawn(argv.join(' '), argv, cwd, { parentPid: pid, cred: targetCred });
1246
- let exitCode = 1;
1247
- try {
1248
- exitCode = await cmdRegistryAdapter.runPureBuiltin(child.pid, argv[0], argv.slice(1), env, cwd, stdin, hooks);
1249
- return exitCode;
1250
- }
1251
- finally {
1252
- this.processes.exit(child.pid, exitCode);
1253
- }
1254
- };
1255
- const result = await this.shell.execute(String(commandLine), {
1256
- cwd: cwd || '/home/user',
1257
- env: { ...this.shell.env, ...(env || {}) },
1258
- onStdout: (d) => hooks.onStdout(String(d)),
1259
- onStderr: (d) => hooks.onStderr(String(d)),
1260
- stdin,
1261
- isolateShellState: true,
1262
- commandContext: { pid, cred, setUmask },
1263
- runAs,
1264
- });
1265
- return typeof result?.exitCode === 'number' ? result.exitCode : 0;
1266
- },
1267
- },
1268
- ctx: this.ctx,
1269
- spawnPool,
1270
- });
1271
- return this.facetProcessManager;
1272
- }
1031
+ _ensureFacetProcessManager() { return this.#runtimeServices._ensureFacetProcessManager(); }
1273
1032
  /**
1274
1033
  * Set the shell command registry for the W8 broker to dispatch
1275
1034
  * resolved commands. Called from the shell-init path right after
@@ -1283,146 +1042,13 @@ export class NimbusSession extends CloudflareDurableObject {
1283
1042
  * fetch calls across the lifetime of this DO instance. This prevents
1284
1043
  * ephemeral port exhaustion from creating a new worker per fetch.
1285
1044
  */
1286
- ensureFetchProxy(log) {
1287
- if (this.fetchProxyEntrypoint)
1288
- return this.fetchProxyEntrypoint;
1289
- try {
1290
- const env = this.env;
1291
- if (!env?.LOADER?.load) {
1292
- log?.('LOADER.load not available — using global fetch');
1293
- return null;
1294
- }
1295
- // Buffered proxy: reads the entire response body into an ArrayBuffer
1296
- // and returns it in ONE message instead of forwarding a ReadableStream.
1297
- // In workerd local dev, streaming responses across a service-binding
1298
- // RPC fabric opens a separate loopback socket PER chunk (~16KB), which
1299
- // exhausts ephemeral ports for larger installs (npm registry packuments
1300
- // are 500KB-3MB, tarballs up to 5MB). Buffering to arrayBuffer means
1301
- // 1 stub call = 1 loopback connection, not N connections.
1302
- //
1303
- // 32MB cap prevents a malformed giant response from OOMing the proxy
1304
- // isolate. Packages with tarballs larger than 32MB will fail to install
1305
- // cleanly (returned as 413 → caller treats as failed fetch).
1306
- const proxyCode = [
1307
- 'const MAX_BYTES = 32 * 1024 * 1024;',
1308
- 'export default {',
1309
- ' async fetch(request, workerEnv) {',
1310
- ' try {',
1311
- ' const body = await request.json();',
1312
- ' const resp = await fetch(body.url, {',
1313
- ' method: body.method || "GET",',
1314
- ' headers: body.headers || {},',
1315
- ' });',
1316
- ' // Check advertised Content-Length before buffering',
1317
- ' const clStr = resp.headers.get("content-length");',
1318
- ' if (clStr) {',
1319
- ' const cl = parseInt(clStr, 10);',
1320
- ' if (cl > MAX_BYTES) {',
1321
- ' return new Response(',
1322
- ' JSON.stringify({ error: "response too large: " + cl + " bytes (cap " + MAX_BYTES + ")" }),',
1323
- ' { status: 413, headers: { "Content-Type": "application/json" } }',
1324
- ' );',
1325
- ' }',
1326
- ' }',
1327
- ' // Buffer entire body — ONE message, not streamed chunks',
1328
- ' const buf = await resp.arrayBuffer();',
1329
- ' if (buf.byteLength > MAX_BYTES) {',
1330
- ' return new Response(',
1331
- ' JSON.stringify({ error: "response exceeded cap: " + buf.byteLength + " bytes" }),',
1332
- ' { status: 413, headers: { "Content-Type": "application/json" } }',
1333
- ' );',
1334
- ' }',
1335
- ' return new Response(buf, {',
1336
- ' status: resp.status,',
1337
- ' statusText: resp.statusText,',
1338
- ' headers: Object.fromEntries(resp.headers.entries()),',
1339
- ' });',
1340
- ' } catch (e) {',
1341
- ' return new Response(JSON.stringify({ error: e.message }), {',
1342
- ' status: 502,',
1343
- ' headers: { "Content-Type": "application/json" },',
1344
- ' });',
1345
- ' }',
1346
- ' }',
1347
- '};',
1348
- ].join('\n');
1349
- const worker = env.LOADER.load({
1350
- compatibilityDate: CF_COMPAT_DATE,
1351
- compatibilityFlags: ['nodejs_compat'],
1352
- mainModule: 'fetch-proxy.js',
1353
- modules: { 'fetch-proxy.js': proxyCode },
1354
- });
1355
- this.fetchProxyEntrypoint = worker.getEntrypoint();
1356
- log?.('Fetch proxy worker created (singleton)');
1357
- return this.fetchProxyEntrypoint;
1358
- }
1359
- catch (e) {
1360
- log?.(`Fetch proxy creation failed: ${e?.message}`);
1361
- return null;
1362
- }
1363
- }
1045
+ ensureFetchProxy(log) { return this.#runtimeServices.ensureFetchProxy(log); }
1364
1046
  /**
1365
1047
  * Build a FetchFn that routes through the singleton proxy entrypoint.
1366
1048
  * All concurrent fetches share ONE worker — no port exhaustion.
1367
1049
  */
1368
- buildFetchFn(log) {
1369
- const entrypoint = this.ensureFetchProxy(log);
1370
- if (!entrypoint)
1371
- return undefined;
1372
- return async (url, init) => {
1373
- const headers = {};
1374
- if (init?.headers) {
1375
- if (init.headers instanceof Headers) {
1376
- init.headers.forEach((v, k) => { headers[k] = v; });
1377
- }
1378
- else if (typeof init.headers === 'object') {
1379
- Object.assign(headers, init.headers);
1380
- }
1381
- }
1382
- return entrypoint.fetch(new Request('http://fetch-proxy/do-fetch', {
1383
- method: 'POST',
1384
- body: JSON.stringify({ url, method: init?.method || 'GET', headers }),
1385
- }));
1386
- };
1387
- }
1388
- async ensureNpmInstaller(onProgress) {
1389
- this.ensureSqliteFs();
1390
- if (!this.esbuildService) {
1391
- if (!this.sqliteFs)
1392
- throw new Error('Session VFS is not initialized');
1393
- this.esbuildService = new EsbuildService(this.sqliteFs.as(CRED_KERNEL));
1394
- }
1395
- // Lazy-load the installer (+ its ~216 KB resolver/facet/loader-pool
1396
- // subgraph) on first npm use so it stays out of the cold script-eval
1397
- // graph. The install command paths that call this are already async.
1398
- const { NpmInstaller } = await import('../npm/installer.js');
1399
- // ── Lazy fetch-proxy ────────────────────────────────────────────
1400
- // The fetch-proxy is a singleton dynamic worker (LOADER.load) that
1401
- // buffers registry responses to dodge wrangler-local-dev port
1402
- // exhaustion. It is only needed for the in-supervisor npm paths.
1403
- // When the resolver and install paths run in facets (default-on),
1404
- // they use bare globalThis.fetch and need no proxy.
1405
- //
1406
- // workerd has a per-DO cap on concurrent dynamic workers (~5-6
1407
- // empirically). A permanent live proxy worker eats one of those
1408
- // slots for the entire DO lifetime, so the proxy is built only when
1409
- // any facet path is disabled via its env flag.
1410
- const useFacetResolver = this._envFlagDefaultOn('NIMBUS_FACET_RESOLVER');
1411
- const useFacetInstall = this._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL');
1412
- const useBatchFacet = this._envFlagDefaultOn('NIMBUS_FACET_NPM_INSTALL_BATCH');
1413
- const needProxy = !(useFacetResolver && useFacetInstall && useBatchFacet);
1414
- const fetchFn = needProxy ? this.buildFetchFn(onProgress) : undefined;
1415
- if (!needProxy) {
1416
- onProgress?.(`[npm] Lazy fetch-proxy: skipped (all facet paths default-on)`);
1417
- }
1418
- this.npmInstaller = new NpmInstaller(this.sqliteFs, this.ctx.storage.sql, {
1419
- esbuild: this.esbuildService,
1420
- ctx: this.ctx,
1421
- env: this.env,
1422
- onProgress,
1423
- fetchFn,
1424
- });
1425
- }
1050
+ buildFetchFn(log) { return this.#runtimeServices.buildFetchFn(log); }
1051
+ async ensureNpmInstaller(onProgress) { return this.#runtimeServices.ensureNpmInstaller(onProgress); }
1426
1052
  /**
1427
1053
  * Read an environment flag with default-on semantics. Mirrors the
1428
1054
  * shouldUseFacetPool / shouldUseFacetResolver / shouldUseBatchFacet
@@ -1430,15 +1056,7 @@ export class NimbusSession extends CloudflareDurableObject {
1430
1056
  * lazy-proxy decision uses identical semantics without leaking that
1431
1057
  * private API across modules.
1432
1058
  */
1433
- _envFlagDefaultOn(name) {
1434
- const raw = this.env?.[name];
1435
- if (raw === undefined || raw === null)
1436
- return true;
1437
- const s = String(raw).toLowerCase();
1438
- if (s === '0' || s === '' || s === 'false' || s === 'off' || s === 'no')
1439
- return false;
1440
- return true;
1441
- }
1059
+ _envFlagDefaultOn(name) { return this.#runtimeServices._envFlagDefaultOn(name); }
1442
1060
  // ── Session initialization ────────────────────────────────────────────
1443
1061
  // ── Session initialization ────────────────────────────────────────────
1444
1062
  //
@@ -1446,7 +1064,7 @@ export class NimbusSession extends CloudflareDurableObject {
1446
1064
  // command registrations + boot wiring). The class retains `initSession`
1447
1065
  // as a delegator per plan §IX.4 R1. Visibility relaxed (was `private`)
1448
1066
  // so the SessionInternal interface declares it.
1449
- initSession(ws) {
1067
+ initSession(ws, options) {
1450
1068
  // A destroyed session id being legitimately re-initialized (shell WS
1451
1069
  // attach, or SDK ready via ensureProgrammaticReady which routes here)
1452
1070
  // lifts the tombstone so the recreated session's log-janitor can arm
@@ -1455,22 +1073,16 @@ export class NimbusSession extends CloudflareDurableObject {
1455
1073
  // Cast pattern (per plan §IX recommendation 1, used here only because
1456
1074
  // initSession reads this.ctx + this.env extensively; siblings that need
1457
1075
  // ctx/env take them as separate explicit args per DEFECT-D1).
1458
- return _w11InitSession(this, ws);
1076
+ return _w11InitSession(this, ws, options);
1459
1077
  }
1078
+ /**
1079
+ * The rebuild in flight for a shell socket that woke this instance from
1080
+ * hibernation, so every frame that arrives while it runs awaits the one
1081
+ * build instead of starting its own. See session/ws.ts bindShellSocket.
1082
+ */
1083
+ _wakeRebuild = null;
1460
1084
  // ── Filesystem seeding ────────────────────────────────────────────────
1461
- ensureGlobalPrefixDirs(prefix) {
1462
- const fs = this.sqliteFs.as(CRED_SESSION_USER);
1463
- const dirs = [
1464
- prefix,
1465
- `${prefix}/lib`,
1466
- `${prefix}/lib/node_modules`,
1467
- `${prefix}/bin`,
1468
- ];
1469
- for (const dir of dirs) {
1470
- if (!fs.exists(dir))
1471
- fs.mkdir(dir, { recursive: true });
1472
- }
1473
- }
1085
+ ensureGlobalPrefixDirs(prefix) { return this.#runtimeServices.ensureGlobalPrefixDirs(prefix); }
1474
1086
  /**
1475
1087
  * The starter content a fresh Nimbus session shows a user: the banner, the
1476
1088
  * sample files, and the Vite starter app.