@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.
- package/README.md +2 -2
- package/dist/cirrus-npm-cjs.generated.d.ts +1 -1
- package/dist/cirrus-npm-cjs.generated.js +1 -1
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +6 -5
- package/dist/facets/compose.d.ts +148 -0
- package/dist/facets/compose.d.ts.map +1 -0
- package/dist/facets/compose.js +146 -0
- package/dist/facets/durable-images.d.ts +9 -5
- package/dist/facets/durable-images.d.ts.map +1 -1
- package/dist/facets/durable-images.js +17 -7
- package/dist/facets/esbuild-bundle-pool.d.ts +49 -0
- package/dist/facets/esbuild-bundle-pool.d.ts.map +1 -0
- package/dist/facets/esbuild-bundle-pool.js +122 -0
- package/dist/facets/exec-telemetry.d.ts +8 -0
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +240 -109
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +677 -342
- package/dist/facets/process.d.ts +20 -16
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +59 -28
- package/dist/facets/real-vite-fs-shim.js +2 -2
- package/dist/facets/real-vite-hmr.d.ts +3 -1
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +18 -8
- package/dist/facets/vite-dev-server.d.ts +25 -47
- package/dist/facets/vite-dev-server.d.ts.map +1 -1
- package/dist/facets/vite-dev-server.js +185 -219
- package/dist/facets/wasm-image-digest.d.ts +25 -0
- package/dist/facets/wasm-image-digest.d.ts.map +1 -0
- package/dist/facets/wasm-image-digest.js +25 -0
- package/dist/facets/wasm-swap-registry.d.ts +51 -92
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +68 -227
- package/dist/facets/ws-terminal.d.ts +9 -31
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +58 -51
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +6 -5
- package/dist/hosted/commands.d.ts +12 -0
- package/dist/hosted/commands.d.ts.map +1 -0
- package/dist/hosted/commands.js +1658 -0
- package/dist/hosted/runtime.d.ts +114 -0
- package/dist/hosted/runtime.d.ts.map +1 -0
- package/dist/hosted/runtime.js +288 -0
- package/dist/hosted/services.d.ts +40 -0
- package/dist/hosted/services.d.ts.map +1 -0
- package/dist/hosted/services.js +512 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +3 -2
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +3 -2
- package/dist/loaders/generated-workers.d.ts +2 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +3 -3
- package/dist/loaders/npm-resolve-preamble.d.ts +3 -6
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +34 -108
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/npm/install-batch-facet.d.ts +3 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +45 -15
- package/dist/npm/installer.d.ts +22 -17
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +209 -193
- package/dist/npm/pre-bundle-facet.d.ts +1 -1
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +1 -1
- package/dist/npm/resolve-facet.d.ts +6 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.d.ts +21 -20
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +85 -84
- package/dist/npm/semver.d.ts +59 -0
- package/dist/npm/semver.d.ts.map +1 -0
- package/dist/npm/semver.js +226 -0
- package/dist/router/remote-api.js +31 -1
- package/dist/runtime/bash-repl.d.ts +1 -1
- package/dist/runtime/bash-repl.d.ts.map +1 -1
- package/dist/runtime/bash-repl.js +38 -4
- package/dist/runtime/bun-runner.js +1 -1
- package/dist/runtime/html-entrypoint.d.ts +7 -0
- package/dist/runtime/html-entrypoint.d.ts.map +1 -1
- package/dist/runtime/html-entrypoint.js +18 -2
- package/dist/runtime/node-runner.d.ts +4 -1
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-runner.js +9 -2
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +305 -29
- package/dist/runtime/opencode-facet-runner.js +16 -16
- package/dist/runtime/package-manager.d.ts +35 -122
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +61 -596
- package/dist/runtime/process-logs-api.d.ts +1 -1
- package/dist/runtime/process-logs-api.d.ts.map +1 -1
- package/dist/runtime/process-logs-api.js +1 -1
- package/dist/runtime/python-repl.d.ts +2 -1
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +82 -59
- package/dist/runtime/repl-session.d.ts +9 -47
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +105 -105
- package/dist/runtime/runtime-catalog.d.ts +13 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +88 -0
- package/dist/session/agent.d.ts +1 -21
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/ai.d.ts +5 -1
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +7 -0
- package/dist/session/helpers.d.ts +38 -0
- package/dist/session/helpers.d.ts.map +1 -1
- package/dist/session/helpers.js +76 -0
- package/dist/session/hibernation.d.ts +2 -1
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +7 -4
- package/dist/session/init-phases.d.ts +1 -1
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +2 -12
- package/dist/session/init.d.ts +15 -36
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +50 -2327
- package/dist/session/loopback.d.ts +2 -29
- package/dist/session/loopback.d.ts.map +1 -1
- package/dist/session/loopback.js +3 -27
- package/dist/session/nimbus-session.d.ts +77 -34
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +126 -514
- package/dist/session/npm-install-port.d.ts +18 -0
- package/dist/session/npm-install-port.d.ts.map +1 -0
- package/dist/session/npm-install-port.js +37 -0
- package/dist/session/port-capability.d.ts +44 -0
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +110 -0
- package/dist/session/programmatic.d.ts +36 -43
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +109 -115
- package/dist/session/routes.d.ts +30 -17
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +42 -131
- package/dist/session/rpc.d.ts +15 -15
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +98 -49
- package/dist/session/serving-port.d.ts +27 -0
- package/dist/session/serving-port.d.ts.map +1 -0
- package/dist/session/serving-port.js +27 -0
- package/dist/session/start-real-vite.d.ts +12 -0
- package/dist/session/start-real-vite.d.ts.map +1 -1
- package/dist/session/start-real-vite.js +4 -4
- package/dist/session/supervisor-op.d.ts +4 -4
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +24 -10
- package/dist/session/supervisor-rpc.d.ts +7 -7
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +4 -0
- package/dist/session/vite-command.d.ts +18 -0
- package/dist/session/vite-command.d.ts.map +1 -0
- package/dist/session/vite-command.js +609 -0
- package/dist/session/ws.d.ts +61 -0
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +122 -12
- package/dist/shell/npm-bin-entrypoints.d.ts +0 -35
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +12 -85
- package/dist/workspace-host.d.ts +6 -0
- package/dist/workspace-host.d.ts.map +1 -0
- package/dist/workspace-host.js +5 -0
- package/dist/workspace.d.ts +2 -0
- package/dist/workspace.d.ts.map +1 -0
- package/dist/workspace.js +1 -0
- package/package.json +34 -9
- package/public/_assets/runtime/{node-shims-9e1380d9ee430fcc.js → node-shims-8f8c64edd2a1f0f5.js} +305 -29
- package/public/index.html +1 -1
- package/scripts/bundle-facet-workers.mjs +1 -1
- package/scripts/bundle-npm-cjs.mjs +2 -2
- package/dist/facets/on-demand-bundle-gate.d.ts +0 -65
- package/dist/facets/on-demand-bundle-gate.d.ts.map +0 -1
- package/dist/facets/on-demand-bundle-gate.js +0 -125
- package/dist/npm/install-args.d.ts +0 -21
- package/dist/npm/install-args.d.ts.map +0 -1
- package/dist/npm/install-args.js +0 -70
- package/dist/npm/npm-log.d.ts +0 -31
- package/dist/npm/npm-log.d.ts.map +0 -1
- package/dist/npm/npm-log.js +0 -51
package/dist/facets/manager.js
CHANGED
|
@@ -21,14 +21,14 @@ import { getRealNodeImportsCode } from '@nimbus-sh/core/_shared/real-node-import
|
|
|
21
21
|
import { FACET_RESIDENT_STORE_SOURCE } from '../vfs/facet-resident-store.js';
|
|
22
22
|
import { VFS_CURSOR_SEED_SOURCE, serializeFacetVfsCursor, } from '@nimbus-sh/core/_shared/facet-vfs-cursor.js';
|
|
23
23
|
import { VFS_WRITE_LEDGER_SOURCE } from '@nimbus-sh/core/_shared/vfs-write-ledger.js';
|
|
24
|
-
import { vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
|
|
24
|
+
import { stripLeadingSlashes, vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
|
|
25
25
|
import { clearPortCapability, listPortReservations, readPortReservation, readPortReservationByOwner, releasePortReservation, restoreReservedPortCapability, } from '../session/port-capability.js';
|
|
26
26
|
import { deriveResidentOwner } from './resident-identity.js';
|
|
27
27
|
import { z } from 'zod/v4';
|
|
28
28
|
import { RESIDENT_OWNER_KEY_PREFIX, DURABLE_IMAGES_KEY_PREFIX } from '../session/keys.js';
|
|
29
29
|
import { PORT_CAPABILITY_KEY_PREFIX } from '../session/keys.js';
|
|
30
30
|
import { unbindPublicPortCapability } from '../router/public-directory.js';
|
|
31
|
-
import { prefetchForRequire } from '@nimbus-sh/core/runtime/require-resolver.js';
|
|
31
|
+
import { prefetchForRequire, ClosureBoundExceededError } from '@nimbus-sh/core/runtime/require-resolver.js';
|
|
32
32
|
import { hasTopLevelModuleSyntax } from '@nimbus-sh/core/runtime/javascript-ast.js';
|
|
33
33
|
import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
|
|
34
34
|
import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
@@ -36,7 +36,7 @@ import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
|
36
36
|
import { TurnBudget, PacedWork, turnChunkMaxBytes, withResolvers } from '@nimbus-sh/fabric/turn-budget.js';
|
|
37
37
|
import { onColdStart } from '@nimbus-sh/fabric/generation.js';
|
|
38
38
|
import { FencedWork, FENCED_WORK_KEY_PREFIX, } from '@nimbus-sh/fabric/fenced-work.js';
|
|
39
|
-
import { EsbuildService, rewriteBundledEsmToCjs, } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
39
|
+
import { EsbuildService, rewriteBundledEsmToCjs, rewriteProvidedCommonJsModules, } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
40
40
|
import { isExecDiagEnabled, recordExecTelemetry } from './exec-telemetry.js';
|
|
41
41
|
import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
42
42
|
import { sqliteWasmModuleEntry } from './opencode-staging.js';
|
|
@@ -48,10 +48,11 @@ import { persistDurableWorkerImage, purgeDurableWorkerImages, } from './durable-
|
|
|
48
48
|
import { SQLITE_WASM_MODULE_NAME, } from '../runtime/opencode-facet-runner.js';
|
|
49
49
|
import { parsePortFromArgv, resolveLongRunningPort } from '@nimbus-sh/core/runtime/long-running-handle.js';
|
|
50
50
|
import { DEFAULT_FACET_BUNDLE_PROFILE, } from '@nimbus-sh/core/runtime/bundle-profile.js';
|
|
51
|
-
import {
|
|
51
|
+
import { CF_COMPAT_DATE, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES, PREFETCH_CACHE_MAX_BYTES, } from '@nimbus-sh/core/constants.js';
|
|
52
52
|
import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
|
|
53
53
|
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
54
54
|
import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
|
|
55
|
+
import { wasmImageDigest } from './wasm-image-digest.js';
|
|
55
56
|
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
|
|
56
57
|
const ISOLATED_ESM_TRANSFORM_MIN_BYTES = 512 * 1024;
|
|
57
58
|
/**
|
|
@@ -131,34 +132,6 @@ function parseFacetManagerEnv(env) {
|
|
|
131
132
|
rawEnv: env,
|
|
132
133
|
};
|
|
133
134
|
}
|
|
134
|
-
/**
|
|
135
|
-
* Reserve held back from a one-shot facet's lifetime so a program that runs
|
|
136
|
-
* out of time is still alive to say so.
|
|
137
|
-
*
|
|
138
|
-
* A one-shot exec is ALREADY bounded: `_execWithTimeout` kills it at
|
|
139
|
-
* FACET_TIMEOUT_MS with exit 124 and "[process killed: timeout after 30s]".
|
|
140
|
-
* The entry drain must therefore not be a second, tighter, independent
|
|
141
|
-
* timeout. Measured against a deployed Worker, floating async work of 5s /
|
|
142
|
-
* 15s / 25s completes and 40s is killed by that outer bound at exactly 30s —
|
|
143
|
-
* so the fixed 8s budget this used to carry was abandoning programs 22
|
|
144
|
-
* seconds before anything actually required it.
|
|
145
|
-
*
|
|
146
|
-
* The drain therefore runs to the outer bound MINUS this reserve, which is
|
|
147
|
-
* what buys the facet time to flush and report the honest "still in flight"
|
|
148
|
-
* reason instead of the supervisor's generic kill. The reserve has to cover
|
|
149
|
-
* the longest tail a facet can have after the drain: settling pending RPC,
|
|
150
|
-
* writing back __vfsWrites (bounded by MAX_RPC_SAFE_PAYLOAD_BYTES; a 20 MiB
|
|
151
|
-
* write-back measures ~1.5s), draining children, then reportExit.
|
|
152
|
-
*/
|
|
153
|
-
export const ONE_SHOT_EXIT_RESERVE_MS = 3_000;
|
|
154
|
-
/**
|
|
155
|
-
* Budget used when the supervisor did not stamp an absolute deadline on the
|
|
156
|
-
* payload. The deadline is the real bound — see `entryDeadlineAt` — because
|
|
157
|
-
* it is measured from the supervisor's own timer rather than restarted when
|
|
158
|
-
* the drain begins, so a slow module init cannot push the drain past the kill
|
|
159
|
-
* and lose the honest message.
|
|
160
|
-
*/
|
|
161
|
-
export const ONE_SHOT_ENTRY_DEADLINE_MS = FACET_TIMEOUT_MS - ONE_SHOT_EXIT_RESERVE_MS;
|
|
162
135
|
/**
|
|
163
136
|
* How long a RESIDENT facet settles its startup before answering its boot
|
|
164
137
|
* call. It keeps running afterwards, so this is not a lifetime decision: the
|
|
@@ -174,8 +147,11 @@ export const RESIDENT_BOOT_SETTLE_MS = 1000;
|
|
|
174
147
|
* servers, requests in flight. A promise is not a handle: a program whose
|
|
175
148
|
* last act leaves `new Promise(() => {})` unsettled prints its output and
|
|
176
149
|
* exits 0. Counting unsettled promises as work was a real divergence from
|
|
177
|
-
* that — such a program
|
|
178
|
-
* reported as having not finished.
|
|
150
|
+
* that — such a program's drain used to end early and it was
|
|
151
|
+
* reported as having not finished. A user-invoked program runs this loop
|
|
152
|
+
* with NO deadline — Node itself has no wall-clock kill; the only endings
|
|
153
|
+
* are the program's exit and a signal. Callers that still pass a finite
|
|
154
|
+
* deadline (the resident boot settle) arm the expiry timer.
|
|
179
155
|
*
|
|
180
156
|
* Three kinds of handle, each owned by the shim that creates them:
|
|
181
157
|
*
|
|
@@ -188,13 +164,6 @@ export const RESIDENT_BOOT_SETTLE_MS = 1000;
|
|
|
188
164
|
* - listening SERVERS (`__portRegistry`), open until the program closes
|
|
189
165
|
* them.
|
|
190
166
|
*
|
|
191
|
-
* The bound is a REAL wall-clock deadline, armed as a timer rather than
|
|
192
|
-
* compared against `Date.now()`: a `setTimeout(0)` turn in workerd costs
|
|
193
|
-
* ~5µs, so the pass budget this loop used to carry (50k) expired after
|
|
194
|
-
* ~150ms and silently overrode every longer deadline the callers declared —
|
|
195
|
-
* anything slower than that, including an ordinary network fetch, was
|
|
196
|
-
* abandoned mid-flight and reported as a clean exit.
|
|
197
|
-
*
|
|
198
167
|
* The loop subscribes to the exit promise ONCE — a per-pass
|
|
199
168
|
* `exitPromise.then()` allocates a promise every iteration — and yields
|
|
200
169
|
* through the raw setTimeout so its own ticks don't inflate the timer count
|
|
@@ -231,16 +200,21 @@ async function __nimbusRunEventLoop(__countHandles, __exitPromise, __deadlineMs,
|
|
|
231
200
|
? globalThis.__nimbusRawClearTimeout
|
|
232
201
|
: globalThis.clearTimeout;
|
|
233
202
|
let __expired = false;
|
|
234
|
-
const __deadline = __rawSetTimeout(() => { __expired = true; }, __deadlineMs);
|
|
235
203
|
let __pass = 0;
|
|
204
|
+
// A user-invoked program runs until it exits or is killed — there is no
|
|
205
|
+
// wall-clock deadline, so no expiry timer is armed at all. (Callers that
|
|
206
|
+
// still pass a finite deadline get the timer for compatibility.)
|
|
207
|
+
const __deadline = Number.isFinite(__deadlineMs)
|
|
208
|
+
? __rawSetTimeout(() => { __expired = true; }, __deadlineMs)
|
|
209
|
+
: null;
|
|
236
210
|
while (!__exited && !__expired && (__pass < __minPasses || __countHandles() > 0)) {
|
|
237
211
|
// The warm-up passes give a settling microtask chain its turns and cost
|
|
238
212
|
// ~5µs each; past them the loop is waiting on wall-clock work, where
|
|
239
|
-
// spinning at 0ms would burn the isolate's CPU
|
|
213
|
+
// spinning at 0ms would burn the isolate's CPU indefinitely.
|
|
240
214
|
await new Promise((resolve) => __rawSetTimeout(resolve, __pass < __minPasses ? 0 : 1));
|
|
241
215
|
__pass++;
|
|
242
216
|
}
|
|
243
|
-
try { __rawClearTimeout(__deadline); } catch {}
|
|
217
|
+
if (__deadline !== null) { try { __rawClearTimeout(__deadline); } catch {} }
|
|
244
218
|
// \`pending\` is what the caller reports when it gives up: a one-shot program
|
|
245
219
|
// still holding a handle did NOT finish, and exiting 0 would claim it did.
|
|
246
220
|
return { passes: __pass, pending: __exited ? 0 : __countHandles() };
|
|
@@ -395,8 +369,8 @@ const SQLITE_FACET_IMPORT = `import __nimbusSqliteWasmModule from "${SQLITE_WASM
|
|
|
395
369
|
/**
|
|
396
370
|
* Generate one-shot runtime code with a plain fetch handler.
|
|
397
371
|
*/
|
|
398
|
-
export async function generateEntrypointCode(userCode, vfsState, usesSqlite, shims) {
|
|
399
|
-
const safeCode = JSON.stringify(userCode);
|
|
372
|
+
export async function generateEntrypointCode(userCode, vfsState, usesSqlite, shims, wasmImports = []) {
|
|
373
|
+
const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
|
|
400
374
|
const bundleSource = await facetVfsBundleSourceFor(vfsState);
|
|
401
375
|
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
402
376
|
const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
|
|
@@ -405,6 +379,7 @@ export async function generateEntrypointCode(userCode, vfsState, usesSqlite, shi
|
|
|
405
379
|
${bundleSource.imports}
|
|
406
380
|
${REAL_NODE_IMPORTS}
|
|
407
381
|
${usesSqlite ? SQLITE_FACET_IMPORT : ''}
|
|
382
|
+
${facetWasmImportsSource(wasmImports)}
|
|
408
383
|
const USER_CODE = ${safeCode};
|
|
409
384
|
const __NimbusHostResponse = globalThis.Response;
|
|
410
385
|
|
|
@@ -454,20 +429,16 @@ class __ProcessExit extends Error {
|
|
|
454
429
|
export default {
|
|
455
430
|
async fetch(request, workerEnv) {
|
|
456
431
|
const args = await request.json();
|
|
457
|
-
const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag,
|
|
432
|
+
const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag, vfsCursor } = args;
|
|
458
433
|
// Per invocation, not per module: this body is cached on
|
|
459
434
|
// hash(code + bundle + manifest) and reused by any session whose snapshot
|
|
460
435
|
// hashes the same, and epochs are per supervisor incarnation.
|
|
461
436
|
const __MODULE_VFS_CURSOR = vfsCursor || null;
|
|
462
437
|
${VFS_CURSOR_SEED_SOURCE}
|
|
463
|
-
//
|
|
464
|
-
//
|
|
465
|
-
//
|
|
466
|
-
|
|
467
|
-
// phaseDeadline.
|
|
468
|
-
const __entryBudgetMs = Number.isFinite(entryDeadlineAt)
|
|
469
|
-
? Math.max(0, Number(entryDeadlineAt) - Date.now())
|
|
470
|
-
: ${ONE_SHOT_ENTRY_DEADLINE_MS};
|
|
438
|
+
// A user-invoked program has no wall-clock lifetime: the drain runs the
|
|
439
|
+
// entrypoint's event loop until Node would exit, and only a kill (Ctrl-C)
|
|
440
|
+
// ends it early.
|
|
441
|
+
const __entryBudgetMs = Infinity;
|
|
471
442
|
let __drainPasses = 0;
|
|
472
443
|
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
473
444
|
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
@@ -486,11 +457,12 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
486
457
|
};
|
|
487
458
|
let __rpcWriteChain = Promise.resolve();
|
|
488
459
|
let __rpcWriteCount = 0;
|
|
489
|
-
|
|
460
|
+
// The relay carries bytes (see "Process output is bytes" in the shims).
|
|
461
|
+
const __queueRpcWrite = (method, bytes) => {
|
|
490
462
|
__rpcWriteCount++;
|
|
491
463
|
const __task = __rpcWriteChain
|
|
492
|
-
.then(() => __supervisor[method](
|
|
493
|
-
.catch((e) => __onRpcDrop(
|
|
464
|
+
.then(() => __supervisor[method](bytes))
|
|
465
|
+
.catch((e) => __onRpcDrop(bytes.byteLength, e));
|
|
494
466
|
__rpcWriteChain = __task.then(() => {}, () => {});
|
|
495
467
|
__pendingIO.push(__task);
|
|
496
468
|
};
|
|
@@ -509,13 +481,13 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
509
481
|
|
|
510
482
|
// Override console AND process.stdout/stderr for live SUPERVISOR streaming
|
|
511
483
|
if (__supervisor && !captureOutput) {
|
|
512
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", s); };
|
|
513
|
-
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", s); };
|
|
484
|
+
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
485
|
+
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
514
486
|
__consoleMod.warn = __consoleMod.error;
|
|
515
487
|
__consoleMod.info = __consoleMod.log;
|
|
516
488
|
__consoleMod.debug = __consoleMod.log;
|
|
517
|
-
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const
|
|
518
|
-
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const
|
|
489
|
+
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
490
|
+
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
519
491
|
}
|
|
520
492
|
|
|
521
493
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -546,18 +518,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
546
518
|
const __drain = await __nimbusRunEntrypointToExit(__entryResult, __entryBudgetMs);
|
|
547
519
|
__drainPasses = __drain.passes;
|
|
548
520
|
if (__nimbusProcessExitCode !== null) exitCode = __nimbusProcessExitCode;
|
|
549
|
-
|
|
550
|
-
// the deadline passes is work that will never run. Reporting exit 0
|
|
551
|
-
// there is the silent-truncation failure: name the limit and fail.
|
|
552
|
-
else if (__drain.pending > 0) {
|
|
553
|
-
const __why = "node: reached the ${FACET_TIMEOUT_MS / 1000}s facet lifetime limit with " +
|
|
554
|
-
__drain.pending + " operation(s) still in flight; the rest of the program did not run. " +
|
|
555
|
-
"A program that needs longer than ${FACET_TIMEOUT_MS / 1000}s has to run as a " +
|
|
556
|
-
"long-running process (node --watch, or a server), which is not bound by it.\\n";
|
|
557
|
-
stderr += __why;
|
|
558
|
-
exitCode = 1;
|
|
559
|
-
if (__supervisor && !captureOutput) __queueRpcWrite("stderr", __why);
|
|
560
|
-
}
|
|
521
|
+
|
|
561
522
|
if (__nimbusLiveStdinPump && !__nimbusAttachedTty) await __nimbusLiveStdinPump;
|
|
562
523
|
} catch (e) {
|
|
563
524
|
if (e instanceof __ProcessExit) { exitCode = e.code; }
|
|
@@ -566,7 +527,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
566
527
|
stderr += trace + "\\n";
|
|
567
528
|
exitCode = 1;
|
|
568
529
|
if (__supervisor && !captureOutput) {
|
|
569
|
-
try {
|
|
530
|
+
try { const __traceBytes = __nimbusOutEnc.encode(trace + "\\n"); __pendingIO.push(__supervisor.stderr(__traceBytes).catch((e2) => __onRpcDrop(__traceBytes.byteLength, e2))); } catch {}
|
|
570
531
|
}
|
|
571
532
|
}
|
|
572
533
|
}
|
|
@@ -592,7 +553,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
592
553
|
if (__residencyReport) {
|
|
593
554
|
stderr += __residencyReport;
|
|
594
555
|
if (exitCode === 0) exitCode = 1;
|
|
595
|
-
if (__supervisor && !captureOutput) __queueRpcWrite("stderr", __residencyReport);
|
|
556
|
+
if (__supervisor && !captureOutput) __queueRpcWrite("stderr", __nimbusOutEnc.encode(__residencyReport));
|
|
596
557
|
}
|
|
597
558
|
|
|
598
559
|
await __drainPendingIO();
|
|
@@ -606,7 +567,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
606
567
|
exitCode = 1;
|
|
607
568
|
if (!captureOutput) {
|
|
608
569
|
try {
|
|
609
|
-
await __supervisor.stderr(trace + "\\n");
|
|
570
|
+
await __supervisor.stderr(__nimbusOutEnc.encode(trace + "\\n"));
|
|
610
571
|
} catch {}
|
|
611
572
|
}
|
|
612
573
|
}
|
|
@@ -656,8 +617,63 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
656
617
|
* compiled user entry is booted once and the exported entrypoint keeps
|
|
657
618
|
* serving HTTP requests from the shimmed http.Server registry.
|
|
658
619
|
*/
|
|
620
|
+
/**
|
|
621
|
+
* Hand a record's entries to a consumer one at a time, releasing each as it
|
|
622
|
+
* goes, so the record stops being a second holder of the whole set.
|
|
623
|
+
*
|
|
624
|
+
* The image store takes its images as a sequence for exactly this: a launch's
|
|
625
|
+
* module text is the largest thing the coordinator assembles, and holding it
|
|
626
|
+
* in two places at once is what the 128 MiB isolate could not survive.
|
|
627
|
+
*/
|
|
628
|
+
function* drainSources(sources) {
|
|
629
|
+
for (const name of Object.keys(sources)) {
|
|
630
|
+
const source = sources[name];
|
|
631
|
+
delete sources[name];
|
|
632
|
+
yield [name, source];
|
|
633
|
+
}
|
|
634
|
+
}
|
|
635
|
+
/** The module-map name a precompiled wasm image travels under. */
|
|
636
|
+
export function facetWasmModuleName(index) {
|
|
637
|
+
return `__nimbus_wasm_${index}.wasm`;
|
|
638
|
+
}
|
|
639
|
+
/**
|
|
640
|
+
* The wasm imports one launch stages: the images its options name, then
|
|
641
|
+
* every image the closure walk recorded (FacetVfsState.wasmImages) that the
|
|
642
|
+
* options did not already name by path. One member per path; the closure's
|
|
643
|
+
* record supplies the digest an option without one lacks.
|
|
644
|
+
*/
|
|
645
|
+
export function facetWasmImports(named, closure) {
|
|
646
|
+
const byPath = new Map();
|
|
647
|
+
for (const image of named)
|
|
648
|
+
byPath.set(image.vfsPath, image);
|
|
649
|
+
for (const image of closure) {
|
|
650
|
+
const seen = byPath.get(image.vfsPath);
|
|
651
|
+
if (seen === undefined)
|
|
652
|
+
byPath.set(image.vfsPath, image);
|
|
653
|
+
else if (seen.digest === undefined)
|
|
654
|
+
byPath.set(image.vfsPath, { ...seen, digest: image.digest });
|
|
655
|
+
}
|
|
656
|
+
return [...byPath.values()].map((image, index) => ({ ...image, moduleName: facetWasmModuleName(index) }));
|
|
657
|
+
}
|
|
658
|
+
/**
|
|
659
|
+
* The static imports that compile a launch's wasm images at module eval and
|
|
660
|
+
* park them by VFS path for the node-shims WebAssembly seam.
|
|
661
|
+
*/
|
|
662
|
+
function facetWasmImportsSource(wasmImports) {
|
|
663
|
+
if (wasmImports.length === 0)
|
|
664
|
+
return '';
|
|
665
|
+
const lines = wasmImports.map((entry, index) => `import __nimbusWasm${index} from ${JSON.stringify(entry.moduleName)};`);
|
|
666
|
+
const entries = wasmImports.map((entry, index) => `[${JSON.stringify(stripLeadingSlashes(entry.vfsPath))}, __nimbusWasm${index}]`);
|
|
667
|
+
const byDigest = wasmImports
|
|
668
|
+
.map((entry, index) => (entry.digest === undefined
|
|
669
|
+
? null
|
|
670
|
+
: `[${JSON.stringify(entry.digest)}, __nimbusWasm${index}]`))
|
|
671
|
+
.filter((line) => line !== null);
|
|
672
|
+
return `${lines.join('\n')}\nglobalThis.__nimbusPrecompiledWasm = new Map([${entries.join(', ')}]);`
|
|
673
|
+
+ `\nglobalThis.__nimbusPrecompiledWasmByDigest = new Map([${byDigest.join(', ')}]);`;
|
|
674
|
+
}
|
|
659
675
|
export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite, shims, pacer) {
|
|
660
|
-
const safeCode = JSON.stringify(userCode);
|
|
676
|
+
const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
|
|
661
677
|
const safeArgs = JSON.stringify({
|
|
662
678
|
argv: opts.argv || [],
|
|
663
679
|
env: opts.env || {},
|
|
@@ -669,14 +685,15 @@ export async function generateLongRunningNodeCode(userCode, vfsState, opts, uses
|
|
|
669
685
|
cred: opts.cred,
|
|
670
686
|
});
|
|
671
687
|
const bundleSource = await facetVfsBundleSourceFor(vfsState, pacer);
|
|
672
|
-
const safeManifest = JSON.stringify(vfsState.manifest);
|
|
673
|
-
const safeMetadata = JSON.stringify(vfsState.metadata);
|
|
688
|
+
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
689
|
+
const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
|
|
674
690
|
return {
|
|
675
691
|
code: `
|
|
676
692
|
${bundleSource.imports}
|
|
677
693
|
import { DurableObject } from "cloudflare:workers";
|
|
678
694
|
${REAL_NODE_IMPORTS}
|
|
679
695
|
${usesSqlite ? SQLITE_FACET_IMPORT : ''}
|
|
696
|
+
${facetWasmImportsSource(opts.wasmImports ?? [])}
|
|
680
697
|
const USER_CODE = ${safeCode};
|
|
681
698
|
const __NIMBUS_ARGS = ${safeArgs};
|
|
682
699
|
const __NimbusHostResponse = globalThis.Response;
|
|
@@ -832,11 +849,12 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
832
849
|
};
|
|
833
850
|
let __rpcWriteChain = Promise.resolve();
|
|
834
851
|
let __rpcWriteCount = 0;
|
|
835
|
-
|
|
852
|
+
// The relay carries bytes (see "Process output is bytes" in the shims).
|
|
853
|
+
const __queueRpcWrite = (method, bytes) => {
|
|
836
854
|
__rpcWriteCount++;
|
|
837
855
|
const __task = __rpcWriteChain
|
|
838
|
-
.then(() => __supervisor[method](
|
|
839
|
-
.catch((e) => __onRpcDrop(
|
|
856
|
+
.then(() => __supervisor[method](bytes))
|
|
857
|
+
.catch((e) => __onRpcDrop(bytes.byteLength, e));
|
|
840
858
|
__rpcWriteChain = __task.then(() => {}, () => {});
|
|
841
859
|
__pendingIO.push(__task);
|
|
842
860
|
};
|
|
@@ -854,13 +872,13 @@ ${ENTRYPOINT_EVENT_LOOP}
|
|
|
854
872
|
${RESIDENCY_MISS_REPORT}
|
|
855
873
|
|
|
856
874
|
if (__supervisor && !captureOutput) {
|
|
857
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", s); };
|
|
858
|
-
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", s); };
|
|
875
|
+
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
876
|
+
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
859
877
|
__consoleMod.warn = __consoleMod.error;
|
|
860
878
|
__consoleMod.info = __consoleMod.log;
|
|
861
879
|
__consoleMod.debug = __consoleMod.log;
|
|
862
|
-
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const
|
|
863
|
-
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const
|
|
880
|
+
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
881
|
+
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
864
882
|
}
|
|
865
883
|
|
|
866
884
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -914,7 +932,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
914
932
|
stderr += trace + "\\n";
|
|
915
933
|
exitCode = 1;
|
|
916
934
|
if (__supervisor && !captureOutput) {
|
|
917
|
-
try {
|
|
935
|
+
try { const __traceBytes = __nimbusOutEnc.encode(trace + "\\n"); __pendingIO.push(__supervisor.stderr(__traceBytes).catch((e2) => __onRpcDrop(__traceBytes.byteLength, e2))); } catch {}
|
|
918
936
|
}
|
|
919
937
|
}
|
|
920
938
|
}
|
|
@@ -943,7 +961,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
943
961
|
if (__residencyReport) {
|
|
944
962
|
stderr += __residencyReport;
|
|
945
963
|
if (Number(code ?? 0) === 0) code = 1;
|
|
946
|
-
try { await __supervisor.stderr(__residencyReport); } catch {}
|
|
964
|
+
try { await __supervisor.stderr(__nimbusOutEnc.encode(__residencyReport)); } catch {}
|
|
947
965
|
}
|
|
948
966
|
await __supervisor.reportExit(code, reason || "");
|
|
949
967
|
__nimbusProcessExitReported = true;
|
|
@@ -952,7 +970,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
952
970
|
const trace = (e && e.stack) || (e && e.message) || String(e);
|
|
953
971
|
stderr += trace + "\\n";
|
|
954
972
|
if (__supervisor) {
|
|
955
|
-
try { await __supervisor.stderr(trace + "\\n"); } catch {}
|
|
973
|
+
try { await __supervisor.stderr(__nimbusOutEnc.encode(trace + "\\n")); } catch {}
|
|
956
974
|
await __nimbusReportFinalExit(1, trace + "\\n");
|
|
957
975
|
}
|
|
958
976
|
};
|
|
@@ -1001,7 +1019,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
1001
1019
|
const tail = "[orphan output: " + __rpcDrops + " dropped RPC write(s), ~" +
|
|
1002
1020
|
__rpcDropBytes + " bytes lost" +
|
|
1003
1021
|
(__rpcLastError ? "; last error: " + __rpcLastError : "") + "]\\n";
|
|
1004
|
-
try { await __supervisor.stderr(tail); } catch {}
|
|
1022
|
+
try { await __supervisor.stderr(__nimbusOutEnc.encode(tail)); } catch {}
|
|
1005
1023
|
}
|
|
1006
1024
|
if (exitCode !== 0) {
|
|
1007
1025
|
if (__supervisor && !__nimbusProcessExitReported) {
|
|
@@ -1057,16 +1075,15 @@ export class NimbusProcess extends DurableObject {
|
|
|
1057
1075
|
* `bundleSource`, `serializedManifest` and `serializedMetadata` are total
|
|
1058
1076
|
* encodings of `bundle`, `manifest` and `metadata` — no caller can distinguish
|
|
1059
1077
|
* a state carrying both from one carrying only the serialized halves, because
|
|
1060
|
-
*
|
|
1078
|
+
* both facet generators read the serialized halves and nothing else does.
|
|
1061
1079
|
* Holding both doubles the cost of a cached entry for its whole lifetime, and
|
|
1062
1080
|
* that lifetime spans execs.
|
|
1063
1081
|
*
|
|
1064
|
-
*
|
|
1065
|
-
*
|
|
1066
|
-
*
|
|
1067
|
-
*
|
|
1068
|
-
* (`
|
|
1069
|
-
* goes through here.
|
|
1082
|
+
* Applied by `_buildProcessBundle` to every state it builds, whether or not
|
|
1083
|
+
* the entry turns out small enough to retain: the launch being served reads
|
|
1084
|
+
* the serialized forms too. `_stageOpencodeFacet` builds its own uncached
|
|
1085
|
+
* state and genuinely re-reads the raw cells
|
|
1086
|
+
* (`assertStagedBundleFitsRpcPayload`); it does not go through here.
|
|
1070
1087
|
*/
|
|
1071
1088
|
export function releaseSerializedSources(vfsState) {
|
|
1072
1089
|
if (vfsState.bundleSource)
|
|
@@ -1076,31 +1093,6 @@ export function releaseSerializedSources(vfsState) {
|
|
|
1076
1093
|
if (vfsState.serializedMetadata !== undefined)
|
|
1077
1094
|
vfsState.metadata = {};
|
|
1078
1095
|
}
|
|
1079
|
-
/**
|
|
1080
|
-
* Drop everything a resident launch has finished reading, in place.
|
|
1081
|
-
*
|
|
1082
|
-
* The one-shot path releases its map at LOADER.load; this is the same policy
|
|
1083
|
-
* for the path `spawnNode` takes, which is the path every attached-TTY npm bin
|
|
1084
|
-
* takes — how a real agentic CLI starts. The generated source is a total
|
|
1085
|
-
* encoding of the cells, the manifest and the metadata, and the only thing the
|
|
1086
|
-
* rest of a launch reads off the state is `cursor`. Everything else is a second
|
|
1087
|
-
* copy of the largest thing this DO builds — 22.9 MB for pi — held for exactly
|
|
1088
|
-
* as long as the facet takes to boot on it.
|
|
1089
|
-
*
|
|
1090
|
-
* Holding it reset the session isolate with exceededMemory, and an isolate
|
|
1091
|
-
* reset tears the terminal WebSocket down with no exit frame: the dead screen
|
|
1092
|
-
* reading "[process terminal closed]".
|
|
1093
|
-
*
|
|
1094
|
-
* An emptied state can still generate a map — it would just generate one with
|
|
1095
|
-
* no program in it — so this marks the state instead of trusting callers to
|
|
1096
|
-
* stop.
|
|
1097
|
-
*/
|
|
1098
|
-
export function releaseResidentLaunchSources(vfsState) {
|
|
1099
|
-
vfsState.bundle = {};
|
|
1100
|
-
vfsState.manifest = {};
|
|
1101
|
-
vfsState.metadata = {};
|
|
1102
|
-
vfsState.generatedSourcesReleased = true;
|
|
1103
|
-
}
|
|
1104
1096
|
/**
|
|
1105
1097
|
* Drop the serialized forms once a module map has been generated from them.
|
|
1106
1098
|
*
|
|
@@ -1108,12 +1100,19 @@ export function releaseResidentLaunchSources(vfsState) {
|
|
|
1108
1100
|
* bundle expression, the manifest and the metadata is inside it. Holding them
|
|
1109
1101
|
* afterwards keeps a second copy of the largest thing this DO builds alive for
|
|
1110
1102
|
* as long as the facet runs — for pi, 22.7 MB across the ~20 s window in which
|
|
1111
|
-
* the isolate was being reset.
|
|
1103
|
+
* the isolate was being reset. For the one-shot path that window is the run;
|
|
1104
|
+
* for a resident launch — the path every attached-TTY npm bin takes, how a
|
|
1105
|
+
* real agentic CLI starts — it is the boot, and holding the copy across it
|
|
1106
|
+
* reset the session isolate with exceededMemory, which tears the terminal
|
|
1107
|
+
* WebSocket down with no exit frame: the dead screen reading "[process
|
|
1108
|
+
* terminal closed]".
|
|
1112
1109
|
*
|
|
1113
1110
|
* Only for a state the prefetch cache refused. A retained entry's serialized
|
|
1114
|
-
* forms ARE the entry, and a later
|
|
1111
|
+
* forms ARE the entry, and a later launch is served from them. An emptied
|
|
1112
|
+
* state could still generate a map — one with no program in it — so this
|
|
1113
|
+
* marks the state instead of trusting callers to stop.
|
|
1115
1114
|
*/
|
|
1116
|
-
function releaseGeneratedSources(vfsState) {
|
|
1115
|
+
export function releaseGeneratedSources(vfsState) {
|
|
1117
1116
|
vfsState.bundleSource = undefined;
|
|
1118
1117
|
vfsState.serializedManifest = undefined;
|
|
1119
1118
|
vfsState.serializedMetadata = undefined;
|
|
@@ -1166,6 +1165,29 @@ function _readBundleCell(vfs, path) {
|
|
|
1166
1165
|
function _bundleCellLength(cell) {
|
|
1167
1166
|
return typeof cell === 'string' ? cell.length : cell.byteLength;
|
|
1168
1167
|
}
|
|
1168
|
+
/**
|
|
1169
|
+
* Pre-read admission check for a bundle candidate: refuse BEFORE reading
|
|
1170
|
+
* when the file's on-disk size already exceeds the remaining bundle budget
|
|
1171
|
+
* or the caller's per-file ceiling. Without this every helper read a file
|
|
1172
|
+
* in full, then discarded it when the post-read `cellLen` check tripped —
|
|
1173
|
+
* on trees with several 5–10 MiB artifacts that read alone was the memory
|
|
1174
|
+
* pressure that reset the isolate.
|
|
1175
|
+
*
|
|
1176
|
+
* `perFileCeiling` is the pass's own cap (e.g. CWD_SNAPSHOT_MAX_FILE_BYTES,
|
|
1177
|
+
* BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES); omit it for budget-only checks.
|
|
1178
|
+
*/
|
|
1179
|
+
function _bundleAdmits(vfs, path, budgetState, perFileCeiling) {
|
|
1180
|
+
let size;
|
|
1181
|
+
try {
|
|
1182
|
+
size = vfs.lstat(path).size;
|
|
1183
|
+
}
|
|
1184
|
+
catch {
|
|
1185
|
+
return false;
|
|
1186
|
+
}
|
|
1187
|
+
if (perFileCeiling !== undefined && size > perFileCeiling)
|
|
1188
|
+
return false;
|
|
1189
|
+
return budgetState.totalBytes + size <= VFS_BUNDLE_MAX_BYTES;
|
|
1190
|
+
}
|
|
1169
1191
|
/** What a bundle currently weighs, and so what one more pass over it costs. */
|
|
1170
1192
|
function _bundleWeight(bundle) {
|
|
1171
1193
|
let weight = 0;
|
|
@@ -1770,12 +1792,16 @@ function addUnreadableDenialCells(vfs, bundle, metadata) {
|
|
|
1770
1792
|
* computed-path requires). Bounded to package.json + 1 main-entry file
|
|
1771
1793
|
* per package — sub-agent §Q3 quantified the worst-case cumulative
|
|
1772
1794
|
* budget impact (~322 KiB for fastify, ~1.7 MiB for ts-jest).
|
|
1795
|
+
*
|
|
1796
|
+
* `requiredPaths` is the static require closure. A package the closure
|
|
1797
|
+
* already reached — but reached only through a SUBPATH — has its main entry
|
|
1798
|
+
* skipped; see `mainIsSpeculative`.
|
|
1773
1799
|
*/
|
|
1774
1800
|
// can verify the hash-chunk + shared/ oversample directly. Pre-X.5-C this
|
|
1775
1801
|
// was a file-local helper. Adding the named export is a pure surface
|
|
1776
1802
|
// addition — no callers other than buildPrefetchBundle (same file) and
|
|
1777
1803
|
// the new probe.
|
|
1778
|
-
export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
1804
|
+
export function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set()) {
|
|
1779
1805
|
let added = 0;
|
|
1780
1806
|
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1781
1807
|
const nmDir = cwdStripped + '/node_modules';
|
|
@@ -1801,7 +1827,16 @@ export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
|
1801
1827
|
// supervisor's headroom — on every invocation that never reads it.
|
|
1802
1828
|
// Anything the program really requires arrives through the closure,
|
|
1803
1829
|
// which is uncapped.
|
|
1804
|
-
if (vfs
|
|
1830
|
+
if (!_bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES))
|
|
1831
|
+
return false;
|
|
1832
|
+
// A guess must not be a native binary. Nothing in a Workers isolate can
|
|
1833
|
+
// load a `.node` addon or a `.exe` — the ABI policy classifies them
|
|
1834
|
+
// native-unsupported and the installer says so at install time — so
|
|
1835
|
+
// admitting one spends the budget on bytes no code path can reach.
|
|
1836
|
+
// Measured: freeing 1.3 MiB of rollup let `@napi-rs/lzma-linux-x64-gnu`'s
|
|
1837
|
+
// 1,445,448-byte `.node` in, which the size guard had been evicting.
|
|
1838
|
+
// Only the guess is filtered; a path the closure requires is untouched.
|
|
1839
|
+
if (stripped.endsWith('.node') || stripped.endsWith('.exe'))
|
|
1805
1840
|
return false;
|
|
1806
1841
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
1807
1842
|
const content = _readBundleCell(vfs, stripped);
|
|
@@ -1838,8 +1873,42 @@ export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
|
1838
1873
|
collectExportLeaves(node[k], out);
|
|
1839
1874
|
}
|
|
1840
1875
|
}
|
|
1876
|
+
/**
|
|
1877
|
+
* Whether guessing at `pkgDir`'s main entry is still a guess.
|
|
1878
|
+
*
|
|
1879
|
+
* It is, for a package nothing required: that is what this pass exists for,
|
|
1880
|
+
* and a dynamic `require(variable)` leaves no edge to follow. It is NOT for
|
|
1881
|
+
* a package the closure reached only through a subpath export — there the
|
|
1882
|
+
* program has told us exactly which corner of the package it uses, and the
|
|
1883
|
+
* main entry is a different graph that no require reaches. Adding it anyway
|
|
1884
|
+
* is how `rollup/parseAst` (7.9 KB of binding) dragged in rollup's whole
|
|
1885
|
+
* bundler: `dist/shared/rollup.js` at 941 KB and, through the `module`
|
|
1886
|
+
* candidate, `dist/es/shared/node-entry.js` at 951 KB — measured together
|
|
1887
|
+
* as 46.5% of a real-vite snapshot, reached by no require in it.
|
|
1888
|
+
*
|
|
1889
|
+
* A package that loads its own main from a subpath at runtime does so
|
|
1890
|
+
* through a static edge, so it is in the closure and unaffected.
|
|
1891
|
+
*/
|
|
1892
|
+
function mainIsSpeculative(pkgDir) {
|
|
1893
|
+
const prefix = pkgDir.replace(/^\/+/, '') + '/';
|
|
1894
|
+
let reached = false;
|
|
1895
|
+
for (const path of requiredPaths) {
|
|
1896
|
+
if (!path.startsWith(prefix))
|
|
1897
|
+
continue;
|
|
1898
|
+
// package.json alone is resolution metadata, not a use of the package.
|
|
1899
|
+
if (path === prefix + 'package.json')
|
|
1900
|
+
continue;
|
|
1901
|
+
reached = true;
|
|
1902
|
+
break;
|
|
1903
|
+
}
|
|
1904
|
+
return !reached;
|
|
1905
|
+
}
|
|
1841
1906
|
function addPkgEntry(pkgDir) {
|
|
1842
1907
|
addOne(pkgDir + '/package.json');
|
|
1908
|
+
// A package the closure reached keeps exactly what it reached; only an
|
|
1909
|
+
// unreached one gets the guess below.
|
|
1910
|
+
if (!mainIsSpeculative(pkgDir))
|
|
1911
|
+
return;
|
|
1843
1912
|
let meta;
|
|
1844
1913
|
try {
|
|
1845
1914
|
meta = JSON.parse(vfs.readFileString(pkgDir + '/package.json'));
|
|
@@ -1939,27 +2008,87 @@ export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
|
1939
2008
|
}
|
|
1940
2009
|
}
|
|
1941
2010
|
}
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1948
|
-
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1952
|
-
|
|
1953
|
-
|
|
1954
|
-
|
|
1955
|
-
|
|
1956
|
-
|
|
1957
|
-
|
|
2011
|
+
for (const pkgDir of speculativePackageDirs(vfs, cwdStripped, bundle))
|
|
2012
|
+
addPkgEntry(pkgDir);
|
|
2013
|
+
return { added };
|
|
2014
|
+
}
|
|
2015
|
+
/**
|
|
2016
|
+
* The packages a computed `require(name)` inside the program can plausibly
|
|
2017
|
+
* name: the project root's own runtime `dependencies`, plus every package
|
|
2018
|
+
* ONE `dependencies` hop from a package that already owns a file in the
|
|
2019
|
+
* static closure. Never devDependencies, never a second hop.
|
|
2020
|
+
*
|
|
2021
|
+
* Unbounded, the greedy oversample read every installed package's main:
|
|
2022
|
+
* for `node -e "import('got')"` in got's repo — a one-file static closure —
|
|
2023
|
+
* that was 1,526 files / 10.9 MB from 706 packages, which every later pass
|
|
2024
|
+
* re-scanned and esbuild-wasm transformed, and the exec path's 20 s bundle
|
|
2025
|
+
* deadline fired on a program that reads none of it. A bound that followed
|
|
2026
|
+
* dependency edges from the project's devDependencies reached all 772 of
|
|
2027
|
+
* them (measured), because a library repo's dev toolchain reaches the whole
|
|
2028
|
+
* tree. Computed requires almost always target a declared runtime
|
|
2029
|
+
* dependency of the package doing the requiring, so the bound is one hop
|
|
2030
|
+
* over `dependencies` only. Directories, sorted for a stable bundle.
|
|
2031
|
+
*/
|
|
2032
|
+
export function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
2033
|
+
const runtimeDeps = (pkgJsonPath) => {
|
|
2034
|
+
try {
|
|
2035
|
+
const meta = JSON.parse(vfs.readFileString(pkgJsonPath));
|
|
2036
|
+
const names = new Set();
|
|
2037
|
+
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
2038
|
+
const deps = meta?.[field];
|
|
2039
|
+
if (deps && typeof deps === 'object')
|
|
2040
|
+
for (const name of Object.keys(deps))
|
|
2041
|
+
names.add(name);
|
|
1958
2042
|
}
|
|
2043
|
+
return [...names];
|
|
2044
|
+
}
|
|
2045
|
+
catch {
|
|
2046
|
+
return [];
|
|
2047
|
+
}
|
|
2048
|
+
};
|
|
2049
|
+
// The package that owns a bundle file: the last node_modules segment.
|
|
2050
|
+
const ownerOf = (path) => {
|
|
2051
|
+
const idx = path.lastIndexOf('/node_modules/');
|
|
2052
|
+
if (idx === -1)
|
|
2053
|
+
return null;
|
|
2054
|
+
const segs = path.slice(idx + '/node_modules/'.length).split('/');
|
|
2055
|
+
const name = segs[0]?.startsWith('@') ? segs.slice(0, 2).join('/') : segs[0];
|
|
2056
|
+
return name ? path.slice(0, idx + '/node_modules/'.length) + name : null;
|
|
2057
|
+
};
|
|
2058
|
+
// Resolve a bare name the way require does from `fromDir`: the nearest
|
|
2059
|
+
// node_modules up the tree that has it.
|
|
2060
|
+
const resolveDir = (name, fromDir) => {
|
|
2061
|
+
let dir = fromDir;
|
|
2062
|
+
for (;;) {
|
|
2063
|
+
const candidate = dir + '/node_modules/' + name;
|
|
2064
|
+
if (vfs.exists(candidate + '/package.json'))
|
|
2065
|
+
return candidate;
|
|
2066
|
+
const idx = dir.lastIndexOf('/');
|
|
2067
|
+
if (idx <= 0)
|
|
2068
|
+
return null;
|
|
2069
|
+
dir = dir.slice(0, idx);
|
|
1959
2070
|
}
|
|
2071
|
+
};
|
|
2072
|
+
const reached = new Set();
|
|
2073
|
+
const hop = (fromDir) => {
|
|
2074
|
+
for (const name of runtimeDeps(fromDir + '/package.json')) {
|
|
2075
|
+
const dir = resolveDir(name, fromDir);
|
|
2076
|
+
if (dir !== null)
|
|
2077
|
+
reached.add(dir);
|
|
2078
|
+
}
|
|
2079
|
+
};
|
|
2080
|
+
hop(cwdStripped);
|
|
2081
|
+
const owners = new Set();
|
|
2082
|
+
for (const path of Object.keys(bundle)) {
|
|
2083
|
+
const owner = ownerOf(path);
|
|
2084
|
+
if (owner !== null)
|
|
2085
|
+
owners.add(owner);
|
|
1960
2086
|
}
|
|
1961
|
-
|
|
1962
|
-
|
|
2087
|
+
for (const owner of owners) {
|
|
2088
|
+
reached.add(owner);
|
|
2089
|
+
hop(owner);
|
|
2090
|
+
}
|
|
2091
|
+
return [...reached].sort();
|
|
1963
2092
|
}
|
|
1964
2093
|
/**
|
|
1965
2094
|
* X.5-Z3: scan every JS source already in `bundle` for static
|
|
@@ -2029,6 +2158,8 @@ export function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
|
2029
2158
|
try {
|
|
2030
2159
|
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
2031
2160
|
return false;
|
|
2161
|
+
if (!_bundleAdmits(vfs, stripped, budgetState))
|
|
2162
|
+
return false;
|
|
2032
2163
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
2033
2164
|
const content = _readBundleCell(vfs, stripped);
|
|
2034
2165
|
const cellLen = _bundleCellLength(content);
|
|
@@ -2202,6 +2333,8 @@ export function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, budgetSta
|
|
|
2202
2333
|
try {
|
|
2203
2334
|
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
2204
2335
|
return false;
|
|
2336
|
+
if (!_bundleAdmits(vfs, stripped, budgetState))
|
|
2337
|
+
return false;
|
|
2205
2338
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
2206
2339
|
const content = _readBundleCell(vfs, stripped);
|
|
2207
2340
|
const cellLen = _bundleCellLength(content);
|
|
@@ -2307,7 +2440,7 @@ export function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, budgetSta
|
|
|
2307
2440
|
*/
|
|
2308
2441
|
export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundleProfile) {
|
|
2309
2442
|
if (!scriptPath)
|
|
2310
|
-
return { added: 0 };
|
|
2443
|
+
return { added: 0, wasmPaths: [] };
|
|
2311
2444
|
const stripped = scriptPath.replace(/^\/+/, '');
|
|
2312
2445
|
// Find the *innermost* node_modules/<pkg> root. Handles scoped
|
|
2313
2446
|
// packages (`@org/name`) too.
|
|
@@ -2320,11 +2453,11 @@ export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundl
|
|
|
2320
2453
|
}
|
|
2321
2454
|
}
|
|
2322
2455
|
if (nmIdx < 0)
|
|
2323
|
-
return { added: 0 };
|
|
2456
|
+
return { added: 0, wasmPaths: [] };
|
|
2324
2457
|
const isScoped = segs[nmIdx + 1]?.startsWith('@');
|
|
2325
2458
|
const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
|
|
2326
2459
|
if (pkgEnd > segs.length)
|
|
2327
|
-
return { added: 0 };
|
|
2460
|
+
return { added: 0, wasmPaths: [] };
|
|
2328
2461
|
const pkgRoot = segs.slice(0, pkgEnd).join('/');
|
|
2329
2462
|
// npm-create-fix wave (2026-05-12): scaffold profile needs this cap
|
|
2330
2463
|
// high enough to cover
|
|
@@ -2350,6 +2483,10 @@ export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundl
|
|
|
2350
2483
|
// an unread multi-MiB cell costs a stat rather than a transfer.
|
|
2351
2484
|
let visited = 0;
|
|
2352
2485
|
const candidates = [];
|
|
2486
|
+
// Every wasm image under the package, whatever its size: an image rides in
|
|
2487
|
+
// the module map by path and is compiled by the loader, so the bundle's
|
|
2488
|
+
// per-file cap does not apply to it (see FacetVfsState.wasmImages).
|
|
2489
|
+
const wasmPaths = [];
|
|
2353
2490
|
const queue = [pkgRoot];
|
|
2354
2491
|
while (queue.length > 0 && visited < MAX_PKG_FILES) {
|
|
2355
2492
|
const dir = queue.shift();
|
|
@@ -2379,6 +2516,8 @@ export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundl
|
|
|
2379
2516
|
// to it) or outside this profile's package-data policy.
|
|
2380
2517
|
if (!shouldIncludeBinPackageFile(pkgRoot, child, bundleProfile))
|
|
2381
2518
|
continue;
|
|
2519
|
+
if (child.endsWith('.wasm'))
|
|
2520
|
+
wasmPaths.push(child);
|
|
2382
2521
|
if (bundle[child] !== undefined)
|
|
2383
2522
|
continue;
|
|
2384
2523
|
let size;
|
|
@@ -2409,6 +2548,8 @@ export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundl
|
|
|
2409
2548
|
break;
|
|
2410
2549
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2411
2550
|
break;
|
|
2551
|
+
if (candidate.size > VFS_BUNDLE_MAX_BYTES - budgetState.totalBytes)
|
|
2552
|
+
continue;
|
|
2412
2553
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
2413
2554
|
let content;
|
|
2414
2555
|
try {
|
|
@@ -2427,7 +2568,40 @@ export function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundl
|
|
|
2427
2568
|
budgetState.fileCount++;
|
|
2428
2569
|
added++;
|
|
2429
2570
|
}
|
|
2430
|
-
return { added };
|
|
2571
|
+
return { added, wasmPaths };
|
|
2572
|
+
}
|
|
2573
|
+
/**
|
|
2574
|
+
* The wasm images a program's closure holds, by path and content digest.
|
|
2575
|
+
*
|
|
2576
|
+
* Two sources, one record: a `.wasm` cell the walk already staged (digested
|
|
2577
|
+
* from the cell, no second read), and a `.wasm` file the bin-package pass
|
|
2578
|
+
* saw but did not stage because it is over the bundle's per-file cap —
|
|
2579
|
+
* esbuild-wasm's 11.9 MiB image is the motivating one. A launch stages each
|
|
2580
|
+
* as a module-map member the loader compiles, registered under both keys,
|
|
2581
|
+
* so the program's own `new WebAssembly.Module(bytes)` is answered from the
|
|
2582
|
+
* map whether it read the bytes by path or carried them inline. A file that
|
|
2583
|
+
* cannot be read is left out; the seam's refusal names the module later.
|
|
2584
|
+
*/
|
|
2585
|
+
export function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
|
|
2586
|
+
const byPath = new Map();
|
|
2587
|
+
for (const [path, cell] of Object.entries(bundle)) {
|
|
2588
|
+
if (!path.endsWith('.wasm') || !(cell instanceof Uint8Array))
|
|
2589
|
+
continue;
|
|
2590
|
+
byPath.set(path, { vfsPath: '/' + stripLeadingSlashes(path), digest: wasmImageDigest(cell) });
|
|
2591
|
+
}
|
|
2592
|
+
for (const path of unstagedPaths) {
|
|
2593
|
+
if (byPath.has(path))
|
|
2594
|
+
continue;
|
|
2595
|
+
let bytes;
|
|
2596
|
+
try {
|
|
2597
|
+
bytes = vfs.readFile(path);
|
|
2598
|
+
}
|
|
2599
|
+
catch {
|
|
2600
|
+
continue;
|
|
2601
|
+
}
|
|
2602
|
+
byPath.set(path, { vfsPath: '/' + stripLeadingSlashes(path), digest: wasmImageDigest(bytes) });
|
|
2603
|
+
}
|
|
2604
|
+
return [...byPath.values()];
|
|
2431
2605
|
}
|
|
2432
2606
|
/**
|
|
2433
2607
|
* Stage the paths an earlier run of the same entry read synchronously and did
|
|
@@ -2611,13 +2785,8 @@ function addCwdProjectFiles(vfs, cwd, bundle, budgetState) {
|
|
|
2611
2785
|
// Skip an oversized file before reading it: this walk guesses at what
|
|
2612
2786
|
// the program might read, and one guess must not spend the budget (or
|
|
2613
2787
|
// the supervisor's headroom) that every later invocation then carries.
|
|
2614
|
-
|
|
2615
|
-
if (vfs.lstat(child).size > CWD_SNAPSHOT_MAX_FILE_BYTES)
|
|
2616
|
-
continue;
|
|
2617
|
-
}
|
|
2618
|
-
catch {
|
|
2788
|
+
if (!_bundleAdmits(vfs, child, budgetState, CWD_SNAPSHOT_MAX_FILE_BYTES))
|
|
2619
2789
|
continue;
|
|
2620
|
-
}
|
|
2621
2790
|
let content;
|
|
2622
2791
|
try {
|
|
2623
2792
|
content = _readBundleCell(vfs, child);
|
|
@@ -2687,6 +2856,8 @@ function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
|
2687
2856
|
try {
|
|
2688
2857
|
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
2689
2858
|
return;
|
|
2859
|
+
if (!_bundleAdmits(vfs, stripped, budgetState))
|
|
2860
|
+
return;
|
|
2690
2861
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
2691
2862
|
const content = _readBundleCell(vfs, stripped);
|
|
2692
2863
|
const cellLen = _bundleCellLength(content);
|
|
@@ -2939,9 +3110,10 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
2939
3110
|
candidates.push(path);
|
|
2940
3111
|
}
|
|
2941
3112
|
for (const path of candidates) {
|
|
2942
|
-
const
|
|
2943
|
-
if (typeof
|
|
3113
|
+
const original = bundle[path];
|
|
3114
|
+
if (typeof original !== 'string')
|
|
2944
3115
|
continue;
|
|
3116
|
+
const src = bundleTypescriptLoader(path) === null ? rewriteProvidedCommonJsModules(original) : original;
|
|
2945
3117
|
// A TypeScript source keeps its bytes; its emit lands beside it.
|
|
2946
3118
|
const target = bundleTypescriptLoader(path) === null ? path : compiledCellKey(path);
|
|
2947
3119
|
// esbuild-wasm runs inside this isolate, so a transform is computation
|
|
@@ -3039,7 +3211,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3039
3211
|
* behaviour for code paths that don't have esbuild handy).
|
|
3040
3212
|
*
|
|
3041
3213
|
*/
|
|
3042
|
-
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
3214
|
+
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes) {
|
|
3043
3215
|
// This build accumulates raw VFS contents in the supervisor heap, and did it
|
|
3044
3216
|
// with nothing watching: the estimator read 9.4 MiB while these bytes were
|
|
3045
3217
|
// resetting the DO three times. Take the budget the enrichment passes are
|
|
@@ -3049,19 +3221,25 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
|
|
|
3049
3221
|
const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
|
|
3050
3222
|
prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
|
|
3051
3223
|
try {
|
|
3052
|
-
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform);
|
|
3224
|
+
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform, maxBundleBytes);
|
|
3053
3225
|
}
|
|
3054
3226
|
finally {
|
|
3055
3227
|
prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
|
|
3056
3228
|
lease.release();
|
|
3057
3229
|
}
|
|
3058
3230
|
}
|
|
3059
|
-
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
3231
|
+
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes = VFS_BUNDLE_MAX_BYTES) {
|
|
3060
3232
|
// Read the cursor BEFORE the walk: a mutation that lands while the bundle
|
|
3061
3233
|
// is being assembled must be reported as invalidated, not silently missed.
|
|
3062
3234
|
const cursor = { epoch: vfs.epoch, rev: vfs.revision() };
|
|
3063
3235
|
// 1. Static reachable-set walk from entry.
|
|
3064
|
-
const prefetch = prefetchForRequire(vfs, entryCode || '', cwd, scriptPath);
|
|
3236
|
+
const prefetch = prefetchForRequire(vfs, entryCode || '', cwd, scriptPath, maxBundleBytes);
|
|
3237
|
+
if ('kind' in prefetch) {
|
|
3238
|
+
// A required closure larger than the bound can never launch as a
|
|
3239
|
+
// snapshot. Surface it as the process's own failure rather than a
|
|
3240
|
+
// build error: the caller maps it to exit 1 + stderr.
|
|
3241
|
+
throw new ClosureBoundExceededError(prefetch);
|
|
3242
|
+
}
|
|
3065
3243
|
const bundle = { ...prefetch.bundle };
|
|
3066
3244
|
const requiredPaths = new Set(Object.keys(prefetch.bundle));
|
|
3067
3245
|
let truncated = false;
|
|
@@ -3092,7 +3270,7 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3092
3270
|
// Catches dynamic-require / `bindings()` / plugin-loader cases the
|
|
3093
3271
|
// regex prefetch misses. Its budget is independent from the complete
|
|
3094
3272
|
// static require closure, which is correctness-critical.
|
|
3095
|
-
const greedy = greedyAddMainEntries(vfs, cwd, bundle, budgetState);
|
|
3273
|
+
const greedy = greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths);
|
|
3096
3274
|
await paceAfterPass();
|
|
3097
3275
|
// 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
|
|
3098
3276
|
// bundle .js/.mjs/.cjs source for the canonical jsdom shape:
|
|
@@ -3125,7 +3303,6 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3125
3303
|
// as data, etc.) without needing a per-pkg whitelist.
|
|
3126
3304
|
// No-op when entry isn't inside node_modules.
|
|
3127
3305
|
const binSiblingAdd = addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundleProfile);
|
|
3128
|
-
void binSiblingAdd;
|
|
3129
3306
|
await paceAfterPass();
|
|
3130
3307
|
// 2.34 project-data snapshot: sync Node fs cannot await the
|
|
3131
3308
|
// supervisor. Include a bounded snapshot of the current working tree
|
|
@@ -3194,6 +3371,16 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3194
3371
|
// treatment so users see WHY the ESM file couldn't be transformed.
|
|
3195
3372
|
_markBundleEsmAsFailed(bundle, 'esbuild service not initialized (likely lazy-init failure)');
|
|
3196
3373
|
}
|
|
3374
|
+
for (const path of Object.keys(bundle)) {
|
|
3375
|
+
if (compiledCellPath(path) === null && (!isBundleModuleCandidate(path) || bundleTypescriptLoader(path) !== null))
|
|
3376
|
+
continue;
|
|
3377
|
+
const source = bundle[path];
|
|
3378
|
+
if (typeof source !== 'string')
|
|
3379
|
+
continue;
|
|
3380
|
+
await pacer?.spend(source.length);
|
|
3381
|
+
bundle[path] = rewriteProvidedCommonJsModules(source);
|
|
3382
|
+
}
|
|
3383
|
+
await paceAfterPass();
|
|
3197
3384
|
// 3. Manifest pass — UNCHANGED from W2.5b. Decouples directory shape
|
|
3198
3385
|
// from content cap so fs.readdirSync remains honest even if the
|
|
3199
3386
|
// content for a given file was capped out.
|
|
@@ -3241,6 +3428,7 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3241
3428
|
const bundleSideModulesRequired = size.bytes > BUNDLE_MAX_ENCODED_BYTES;
|
|
3242
3429
|
// Suppress lint: `greedy.added` is observed only via diagnostics.
|
|
3243
3430
|
void greedy;
|
|
3431
|
+
const wasmImages = collectClosureWasmImages(vfs, bundle, binSiblingAdd.wasmPaths);
|
|
3244
3432
|
return {
|
|
3245
3433
|
bundle,
|
|
3246
3434
|
manifest,
|
|
@@ -3249,8 +3437,11 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3249
3437
|
reachableCount: fileCount,
|
|
3250
3438
|
truncated,
|
|
3251
3439
|
bundleSideModulesRequired,
|
|
3440
|
+
...(wasmImages.length > 0 ? { wasmImages } : {}),
|
|
3252
3441
|
};
|
|
3253
3442
|
}
|
|
3443
|
+
/** The main module name a worker launch boots from unless told otherwise. */
|
|
3444
|
+
export const DEFAULT_WORKER_MAIN_MODULE = 'worker.js';
|
|
3254
3445
|
const ROUTEABLE_PORT_ATTACH_TIMEOUT_MS = 1_000;
|
|
3255
3446
|
/** A port request may wait this long for a durable app's re-drive to boot. */
|
|
3256
3447
|
const DURABLE_ENSURE_BOOT_BUDGET_MS = 12_000;
|
|
@@ -3316,7 +3507,6 @@ export class FacetManager {
|
|
|
3316
3507
|
* journal recovery rides the first pump.
|
|
3317
3508
|
*/
|
|
3318
3509
|
launchPump;
|
|
3319
|
-
timedOutProcessIds = new Set();
|
|
3320
3510
|
// attach-pid → serve-pid: the resident serve facet a bare-`opencode` dual
|
|
3321
3511
|
// spawn created as an OS-child of the attach TUI. When the attach process
|
|
3322
3512
|
// exits (reported / killed), its serve facet is torn down with it.
|
|
@@ -3564,61 +3754,92 @@ export class FacetManager {
|
|
|
3564
3754
|
*/
|
|
3565
3755
|
setEsbuildService(esbuild) { this.esbuild = esbuild; }
|
|
3566
3756
|
/**
|
|
3567
|
-
*
|
|
3568
|
-
*
|
|
3569
|
-
*
|
|
3570
|
-
* pass + source construction. See `prefetchBundleCache` for the
|
|
3571
|
-
* correctness argument behind the conservative global-revision watermark.
|
|
3572
|
-
*
|
|
3573
|
-
* The bundle source and manifest are computed once on the miss path and
|
|
3574
|
-
* stored so subsequent hits skip rebuilding them too.
|
|
3757
|
+
* The pacer every launch is built under: the session's alarm-driven turn
|
|
3758
|
+
* pump, the deployment's chunk bound, and the one check a suspended launch
|
|
3759
|
+
* makes when it resumes — that the process it is building for still exists.
|
|
3575
3760
|
*/
|
|
3761
|
+
_launchPacer(pid) {
|
|
3762
|
+
return new TurnBudget(this.launchPump, turnChunkMaxBytes(this.env), () => this._assertLaunchStillOwned(pid));
|
|
3763
|
+
}
|
|
3576
3764
|
/**
|
|
3577
|
-
*
|
|
3765
|
+
* Assemble the filesystem bundle a process boots on, across as many
|
|
3766
|
+
* Durable Object turns as it takes.
|
|
3767
|
+
*
|
|
3768
|
+
* The one builder for every Node process this manager starts. A one-shot
|
|
3769
|
+
* exec and a resident launch used to own two copies of this: exec's was
|
|
3770
|
+
* memoized behind the prefetch cache and raced a wall-clock deadline in a
|
|
3771
|
+
* single turn; the resident's was paged with a TurnBudget and never cached.
|
|
3772
|
+
* Two paths, one job — and a tree large enough to page on one path hit the
|
|
3773
|
+
* deadline on the other, failing every `node -e` in it with "assembling the
|
|
3774
|
+
* filesystem bundle … exceeded". What the two callers genuinely differ in is
|
|
3775
|
+
* the entry, the working directory and the process they build for; that is
|
|
3776
|
+
* all they supply. Everything else — the revision-keyed cache and its stale
|
|
3777
|
+
* eviction, the residency profile a previous miss learned, the reachable-set
|
|
3778
|
+
* walk and its enrichment passes, the ESM→CJS transform, the manifest and
|
|
3779
|
+
* metadata, the module-map serialization with its side-module split, the
|
|
3780
|
+
* `node:sqlite` answer, and the release of the raw cells once they are
|
|
3781
|
+
* serialized — happens here, once, and yields the turn whenever a chunk's
|
|
3782
|
+
* worth of it has been done.
|
|
3578
3783
|
*
|
|
3579
|
-
*
|
|
3580
|
-
*
|
|
3581
|
-
*
|
|
3582
|
-
*
|
|
3583
|
-
*
|
|
3584
|
-
* never returns, with no exit record for the process.
|
|
3784
|
+
* There is no deadline. A launch that spans turns costs turns, not a held
|
|
3785
|
+
* thread, so a large tree is not a defect to be reported at N seconds; the
|
|
3786
|
+
* pacer's `stillWanted` check is what ends a build nothing will use — a
|
|
3787
|
+
* process killed while its build was suspended throws from the next resume,
|
|
3788
|
+
* and the caller reports that as it reports any other launch failure.
|
|
3585
3789
|
*
|
|
3586
|
-
*
|
|
3587
|
-
*
|
|
3588
|
-
*
|
|
3589
|
-
*
|
|
3590
|
-
*
|
|
3591
|
-
*
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
* makes the remaining class the only one left to explain.
|
|
3790
|
+
* The returned state carries its serialized forms (`bundleSource`,
|
|
3791
|
+
* `serializedManifest`, `serializedMetadata`) and has already released the
|
|
3792
|
+
* raw ones: `generateEntrypointCode` and `generateLongRunningNodeCode` read
|
|
3793
|
+
* the serialized forms and nothing else. A state the cache retained belongs
|
|
3794
|
+
* to the cache — a caller must not release its serialized forms either
|
|
3795
|
+
* (`cacheRetained` says which); one the cache refused belongs to the caller
|
|
3796
|
+
* alone, and `releaseGeneratedSources` drops it once a map is generated.
|
|
3797
|
+
*
|
|
3798
|
+
* A session without a filesystem gets an empty state: there is nothing to
|
|
3799
|
+
* stage and nothing to yield for.
|
|
3597
3800
|
*/
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3801
|
+
/**
|
|
3802
|
+
* The closure's wasm images as module-map members for a one-shot facet,
|
|
3803
|
+
* read by path under the process's own credential. An image that cannot
|
|
3804
|
+
* be read is left out; the program's compile then meets the seam's own
|
|
3805
|
+
* refusal, which names the module.
|
|
3806
|
+
*/
|
|
3807
|
+
_wasmModulesByValue(entry, wasmImports) {
|
|
3808
|
+
const modules = {};
|
|
3809
|
+
if (!this.vfs || wasmImports.length === 0)
|
|
3810
|
+
return modules;
|
|
3811
|
+
const vfs = this.vfs.as(entry.cred);
|
|
3812
|
+
for (const image of wasmImports) {
|
|
3813
|
+
let bytes;
|
|
3814
|
+
try {
|
|
3815
|
+
bytes = vfs.readFile(stripLeadingSlashes(image.vfsPath));
|
|
3816
|
+
}
|
|
3817
|
+
catch {
|
|
3818
|
+
continue;
|
|
3819
|
+
}
|
|
3820
|
+
modules[image.moduleName] = {
|
|
3821
|
+
wasm: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
|
3822
|
+
};
|
|
3617
3823
|
}
|
|
3824
|
+
return modules;
|
|
3618
3825
|
}
|
|
3619
|
-
async
|
|
3620
|
-
|
|
3621
|
-
|
|
3826
|
+
async _buildProcessBundle(entry, spec, pacer) {
|
|
3827
|
+
if (!this.vfs) {
|
|
3828
|
+
return { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
|
|
3829
|
+
}
|
|
3830
|
+
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so ESM
|
|
3831
|
+
// source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
3832
|
+
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to CJS
|
|
3833
|
+
// before they hit the facet's `new Function` pre-compile loop. Lazy-create
|
|
3834
|
+
// one if NimbusSession didn't share its own.
|
|
3835
|
+
if (!this.esbuild)
|
|
3836
|
+
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
3837
|
+
this.imageStore.ensureDir();
|
|
3838
|
+
const vfs = this.vfs.as(entry.cred);
|
|
3839
|
+
const { cred } = entry;
|
|
3840
|
+
const profile = spec.bundleProfile ?? DEFAULT_FACET_BUNDLE_PROFILE;
|
|
3841
|
+
const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
|
|
3842
|
+
const key = `${profile}\x00${credKey}\x00${spec.cwd}\x00${spec.scriptPath ?? ''}\x00${_fnv1a(spec.entryCode)}`;
|
|
3622
3843
|
const revision = vfs.revision();
|
|
3623
3844
|
// An entry built at an older revision can never be SERVED again — the
|
|
3624
3845
|
// lookup below requires an exact match — so from the first write after it
|
|
@@ -3627,11 +3848,11 @@ export class FacetManager {
|
|
|
3627
3848
|
// replaces it allocates, so the stale filesystem graph and the new one
|
|
3628
3849
|
// never co-reside.
|
|
3629
3850
|
let evictedStale = false;
|
|
3630
|
-
for (const [staleKey,
|
|
3631
|
-
if (
|
|
3851
|
+
for (const [staleKey, stale] of this.prefetchBundleCache) {
|
|
3852
|
+
if (stale.revision === revision)
|
|
3632
3853
|
continue;
|
|
3633
3854
|
this.prefetchBundleCache.delete(staleKey);
|
|
3634
|
-
this.prefetchCacheBytes -=
|
|
3855
|
+
this.prefetchCacheBytes -= stale.bytes;
|
|
3635
3856
|
evictedStale = true;
|
|
3636
3857
|
}
|
|
3637
3858
|
if (evictedStale)
|
|
@@ -3643,24 +3864,26 @@ export class FacetManager {
|
|
|
3643
3864
|
this.prefetchBundleCache.set(key, cached);
|
|
3644
3865
|
return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
|
|
3645
3866
|
}
|
|
3646
|
-
const vfsState = await buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, this.esbuild
|
|
3867
|
+
const vfsState = await buildPrefetchBundle(vfs, spec.scriptPath, spec.cwd, spec.entryCode, this.esbuild, profile, this.residencyProfiles.get(key), pacer, this.hooks.transformLargeEsm);
|
|
3647
3868
|
vfsState.bundleKey = key;
|
|
3648
|
-
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired);
|
|
3869
|
+
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired, pacer);
|
|
3649
3870
|
vfsState.serializedManifest = JSON.stringify(vfsState.manifest);
|
|
3650
3871
|
vfsState.serializedMetadata = JSON.stringify(vfsState.metadata);
|
|
3872
|
+
// Two more serializations of the same weight as the passes before them.
|
|
3873
|
+
await pacer.spend(vfsState.serializedManifest.length + vfsState.serializedMetadata.length);
|
|
3651
3874
|
// The only consumer of the raw cells past this point is a single boolean,
|
|
3652
3875
|
// so answer it now rather than hold ~17 MB (pi) to answer it later.
|
|
3653
|
-
vfsState.usesNodeSqlite = bundleUsesNodeSqlite(entryCode, vfsState.bundle);
|
|
3876
|
+
vfsState.usesNodeSqlite = bundleUsesNodeSqlite(spec.entryCode, vfsState.bundle);
|
|
3654
3877
|
vfsState.cacheHit = false;
|
|
3655
3878
|
// Serialization is total: bundleSource/serializedManifest/serializedMetadata
|
|
3656
|
-
// carry every byte the raw cells and objects do, and
|
|
3657
|
-
//
|
|
3658
|
-
//
|
|
3659
|
-
//
|
|
3660
|
-
//
|
|
3661
|
-
//
|
|
3662
|
-
//
|
|
3663
|
-
//
|
|
3879
|
+
// carry every byte the raw cells and objects do, and both generators read
|
|
3880
|
+
// only the serialized forms. Retaining both doubled what an entry costs
|
|
3881
|
+
// for its whole lifetime — measured for pi at 502af77, per entry: raw
|
|
3882
|
+
// 17,253,610 + source 18,262,324 + manifest 600,060 + metadata 3,841,244
|
|
3883
|
+
// = 39,957,238 B, of which the raw halves are 21,694,914 B held to answer
|
|
3884
|
+
// `usesNodeSqlite`. Dropping them is a pure release: nothing downstream of
|
|
3885
|
+
// this method reads them, and a cache MISS rebuilds from the VFS rather
|
|
3886
|
+
// than from anything discarded here.
|
|
3664
3887
|
//
|
|
3665
3888
|
// Released BEFORE admission so the byte bound prices what the entry costs
|
|
3666
3889
|
// from here on, not the peak it passed through on the way in.
|
|
@@ -3823,7 +4046,7 @@ export class FacetManager {
|
|
|
3823
4046
|
if (opts.skipSpawn && opts.callerPid != null) {
|
|
3824
4047
|
// The caller already allocated the PID via the supervisor
|
|
3825
4048
|
// (with their own user-facing label). Look up the full entry
|
|
3826
|
-
// from the table —
|
|
4049
|
+
// from the table — the exec path needs the canonical
|
|
3827
4050
|
// ProcessEntry shape. Do NOT reap() either: reaping would
|
|
3828
4051
|
// clear the caller's just-spawned entry because its startTime
|
|
3829
4052
|
// is recent (< 60s) but reap() ALSO drops 'running' entries
|
|
@@ -3848,30 +4071,62 @@ export class FacetManager {
|
|
|
3848
4071
|
catch { }
|
|
3849
4072
|
}
|
|
3850
4073
|
}
|
|
3851
|
-
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so
|
|
3852
|
-
// ESM source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
3853
|
-
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to
|
|
3854
|
-
// CJS before they hit the facet's `new Function` pre-compile loop.
|
|
3855
|
-
// Lazy-create one if NimbusSession didn't share its own.
|
|
3856
|
-
if (this.vfs && !this.esbuild) {
|
|
3857
|
-
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
3858
|
-
}
|
|
3859
4074
|
const diagOn = isExecDiagEnabled();
|
|
3860
4075
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
4076
|
+
// Paced like a resident launch: a tree too large for one turn costs
|
|
4077
|
+
// turns, and the pacer's stillWanted check ends a build whose process was
|
|
4078
|
+
// killed while it was suspended. The pacer is settled in the finally
|
|
4079
|
+
// below and not before, because the invocation that granted the last
|
|
4080
|
+
// chunk awaits `chunkEnded` (PacedWork.pump) and has to stay the one
|
|
4081
|
+
// that owns the run: settling at the end of the build would release that
|
|
4082
|
+
// turn with the facet still to load and run on it.
|
|
4083
|
+
const pacer = this._launchPacer(entry.pid);
|
|
4084
|
+
let vfsState;
|
|
4085
|
+
try {
|
|
4086
|
+
vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd: opts.cwd || '/home/user', entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
|
|
4087
|
+
}
|
|
4088
|
+
catch (err) {
|
|
4089
|
+
// The require closure that cannot fit the snapshot bound is the
|
|
4090
|
+
// process's own answer, not a build failure: exit 1 with a stderr
|
|
4091
|
+
// that names the entry, the staged bytes at the stop, the bound,
|
|
4092
|
+
// and the remedy — the same shape the got/next guards print.
|
|
4093
|
+
pacer.settle();
|
|
4094
|
+
if (err instanceof ClosureBoundExceededError) {
|
|
4095
|
+
const o = err.outcome;
|
|
4096
|
+
const mib = (n) => `${(n / 1048576).toFixed(1)} MiB`;
|
|
4097
|
+
const stderr = `require closure for ${o.entry} exceeds the snapshot bound: ` +
|
|
4098
|
+
`${mib(o.bytesSeen)} staged when ${o.lastPath} crossed the ` +
|
|
4099
|
+
`${mib(o.bound)} bound; the process was not started.\n` +
|
|
4100
|
+
`Run it as a server or split the entry; there is no flag to bypass.`;
|
|
4101
|
+
if (this.processes.get(entry.pid)?.state === 'running') {
|
|
4102
|
+
this._failLaunch(entry.pid, stderr);
|
|
4103
|
+
}
|
|
4104
|
+
return { exitCode: 1, stdout: '', stderr };
|
|
4105
|
+
}
|
|
4106
|
+
// A failed build is thrown to the caller exactly as before. What must
|
|
4107
|
+
// not be left behind is the process entry: it was spawned above and
|
|
4108
|
+
// would otherwise sit 'running' forever for a process that never
|
|
4109
|
+
// started. A build ended by a kill finds its entry already exited and
|
|
4110
|
+
// reports nothing twice.
|
|
4111
|
+
if (this.processes.get(entry.pid)?.state === 'running') {
|
|
4112
|
+
this._failLaunch(entry.pid, `assembling the filesystem bundle for \`${command}\` failed: ${errorMessage(err)}`);
|
|
4113
|
+
}
|
|
4114
|
+
throw err;
|
|
4115
|
+
}
|
|
3868
4116
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
3869
4117
|
const diagSink = diagOn
|
|
3870
4118
|
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0, manifestBytes: 0, metadataBytes: 0 }
|
|
3871
4119
|
: undefined;
|
|
3872
4120
|
const abortController = new AbortController();
|
|
4121
|
+
// Ctrl-C / kill on a user program has to end the in-flight run, not just
|
|
4122
|
+
// mark the table row: the runOnce request carries this signal.
|
|
4123
|
+
this.processes.setTerminator(entry.pid, () => abortController.abort());
|
|
4124
|
+
// A shell Ctrl+C arrives as an abort on the command's signal — it is the
|
|
4125
|
+
// same kill, forwarded onto the controller the runOnce request carries.
|
|
4126
|
+
const onShellAbort = () => abortController.abort();
|
|
4127
|
+
opts.signal?.addEventListener('abort', onShellAbort, { once: true });
|
|
3873
4128
|
try {
|
|
3874
|
-
const result = await this.
|
|
4129
|
+
const result = await this._execViaLoader(code, opts, entry, vfsState, abortController.signal, diagSink);
|
|
3875
4130
|
this._flushVfsWrites(result, entry.pid);
|
|
3876
4131
|
this._recordResidencyMisses(vfsState.bundleKey, result.residencyMisses);
|
|
3877
4132
|
this.processes.exit(entry.pid, result.exitCode);
|
|
@@ -3892,6 +4147,7 @@ export class FacetManager {
|
|
|
3892
4147
|
rpcWrites: result.diag?.rpcWrites ?? 0,
|
|
3893
4148
|
fsRpcReads: result.diag?.fsRpcReads ?? 0,
|
|
3894
4149
|
cacheHit: vfsState.cacheHit ?? false,
|
|
4150
|
+
turns: pacer.chunks,
|
|
3895
4151
|
exitCode: result.exitCode,
|
|
3896
4152
|
at: Date.now(),
|
|
3897
4153
|
});
|
|
@@ -3899,31 +4155,27 @@ export class FacetManager {
|
|
|
3899
4155
|
return result;
|
|
3900
4156
|
}
|
|
3901
4157
|
catch (err) {
|
|
3902
|
-
//
|
|
3903
|
-
//
|
|
3904
|
-
//
|
|
3905
|
-
//
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4158
|
+
// The abort controller firing means this run ended BY a kill — the
|
|
4159
|
+
// shell's Ctrl+C or `kill <pid>`. The process was not a crash and the
|
|
4160
|
+
// abort text is not its stderr: mark it killed and hand back 130 (the
|
|
4161
|
+
// shell's signalled status) with nothing for the terminal to print.
|
|
4162
|
+
if (abortController.signal.aborted) {
|
|
4163
|
+
this.processes.kill(entry.pid);
|
|
4164
|
+
return { exitCode: 130, stdout: '', stderr: '' };
|
|
4165
|
+
}
|
|
4166
|
+
const exitCode = 1;
|
|
4167
|
+
const reason = `runtime worker error: ${errorMessage(err)}`;
|
|
3910
4168
|
this.processes.exit(entry.pid, exitCode);
|
|
3911
|
-
|
|
3912
|
-
|
|
3913
|
-
|
|
3914
|
-
// store marks exit AND the tabs-UI structured event fires. The
|
|
3915
|
-
// timeout path already called onExternalExit from the timeout
|
|
3916
|
-
// handler; _reportExternalExit's getExit() guard dedupes.
|
|
3917
|
-
if (!timedOut) {
|
|
3918
|
-
try {
|
|
3919
|
-
this.hooks.onExternalExit?.(entry.pid, exitCode, reason);
|
|
3920
|
-
}
|
|
3921
|
-
catch { }
|
|
4169
|
+
this._w5RecordTermination(entry.pid, exitCode, 'runtime-worker', reason);
|
|
4170
|
+
try {
|
|
4171
|
+
this.hooks.onExternalExit?.(entry.pid, exitCode, reason);
|
|
3922
4172
|
}
|
|
4173
|
+
catch { }
|
|
3923
4174
|
return { exitCode, stdout: '', stderr: errorMessage(err) };
|
|
3924
4175
|
}
|
|
3925
4176
|
finally {
|
|
3926
|
-
|
|
4177
|
+
opts.signal?.removeEventListener('abort', onShellAbort);
|
|
4178
|
+
pacer.settle();
|
|
3927
4179
|
}
|
|
3928
4180
|
}
|
|
3929
4181
|
/**
|
|
@@ -3963,7 +4215,7 @@ export class FacetManager {
|
|
|
3963
4215
|
}
|
|
3964
4216
|
// ── One-shot dynamic Worker entrypoint ────────────────────────────────
|
|
3965
4217
|
async _execViaLoader(code, opts, entry, vfsState, signal, diagSink) {
|
|
3966
|
-
// Answered by
|
|
4218
|
+
// Answered by _buildProcessBundle while the raw cells were still in
|
|
3967
4219
|
// hand; re-deriving it here is what forced them to be retained.
|
|
3968
4220
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
3969
4221
|
const [sqliteModules, shims] = await Promise.all([
|
|
@@ -3983,13 +4235,6 @@ export class FacetManager {
|
|
|
3983
4235
|
stdin: opts.stdin || '',
|
|
3984
4236
|
captureOutput: !!opts.captureOutput,
|
|
3985
4237
|
cred: { ...entry.cred, groups: [...entry.cred.groups] },
|
|
3986
|
-
// Absolute wall-clock instant the entry drain must stop at, derived
|
|
3987
|
-
// from the same FACET_TIMEOUT_MS the supervisor's kill timer uses. It
|
|
3988
|
-
// is stamped here rather than computed facet-side so the budget tracks
|
|
3989
|
-
// the real remaining lifetime; everything still to happen before the
|
|
3990
|
-
// facet runs (module map build, LOADER.load, the RPC hop) only makes
|
|
3991
|
-
// this earlier than the kill, which is the safe direction.
|
|
3992
|
-
entryDeadlineAt: Date.now() + FACET_TIMEOUT_MS - ONE_SHOT_EXIT_RESERVE_MS,
|
|
3993
4238
|
vfsCursor: vfsState.cursor,
|
|
3994
4239
|
...(diagSink ? { diag: true } : {}),
|
|
3995
4240
|
});
|
|
@@ -4003,16 +4248,21 @@ export class FacetManager {
|
|
|
4003
4248
|
// here rather than named by the enclosing frame, and the serialized
|
|
4004
4249
|
// forms it was built from are released unless the prefetch cache is
|
|
4005
4250
|
// keeping them. Otherwise the coordinator carries a second full copy
|
|
4006
|
-
// of the program for the whole
|
|
4007
|
-
//
|
|
4251
|
+
// of the program for the whole run the facet then performs,
|
|
4252
|
+
// which is the window the isolate was being reset in.
|
|
4008
4253
|
code: async () => {
|
|
4009
|
-
|
|
4254
|
+
// A one-shot has no disk reader at load, so the closure's wasm
|
|
4255
|
+
// images ride by value: read here, inside the scope that holds
|
|
4256
|
+
// the map, and compiled by the loader like the sqlite sidecar.
|
|
4257
|
+
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
4258
|
+
const wasmModules = this._wasmModulesByValue(entry, wasmImports);
|
|
4259
|
+
const generatedWorker = await generateEntrypointCode(code, vfsState, usesSqlite, shims, wasmImports);
|
|
4010
4260
|
if (diagSink) {
|
|
4011
4261
|
diagSink.moduleMapBytes = _encodedSourceBytes(generatedWorker.code);
|
|
4012
4262
|
for (const source of Object.values(generatedWorker.modules)) {
|
|
4013
4263
|
diagSink.moduleMapBytes += _encodedSourceBytes(source);
|
|
4014
4264
|
}
|
|
4015
|
-
for (const m of Object.values(sqliteModules)) {
|
|
4265
|
+
for (const m of [...Object.values(sqliteModules), ...Object.values(wasmModules)]) {
|
|
4016
4266
|
diagSink.moduleMapBytes += m.wasm.byteLength;
|
|
4017
4267
|
}
|
|
4018
4268
|
// Read before the release below, which is the last moment the
|
|
@@ -4032,7 +4282,7 @@ export class FacetManager {
|
|
|
4032
4282
|
compatibilityDate: CF_COMPAT_DATE,
|
|
4033
4283
|
compatibilityFlags: ['nodejs_compat', 'nodejs_compat_v2'],
|
|
4034
4284
|
mainModule: 'runner.js',
|
|
4035
|
-
modules: { 'runner.js': generatedWorker.code, ...generatedWorker.modules, ...sqliteModules },
|
|
4285
|
+
modules: { 'runner.js': generatedWorker.code, ...generatedWorker.modules, ...sqliteModules, ...wasmModules },
|
|
4036
4286
|
};
|
|
4037
4287
|
},
|
|
4038
4288
|
request: new Request('http://nimbus-runtime.local/run', {
|
|
@@ -4440,7 +4690,7 @@ export class FacetManager {
|
|
|
4440
4690
|
_assertLaunchStillOwned(pid) {
|
|
4441
4691
|
if (this.processes.get(pid)?.state === 'running')
|
|
4442
4692
|
return;
|
|
4443
|
-
throw new Error(`Nimbus:
|
|
4693
|
+
throw new Error(`Nimbus: the launch for pid ${pid} was cancelled while it was suspended`);
|
|
4444
4694
|
}
|
|
4445
4695
|
/**
|
|
4446
4696
|
* The one way this manager boots a resident process. Every resident process
|
|
@@ -4630,34 +4880,6 @@ export class FacetManager {
|
|
|
4630
4880
|
vfs.writeFile(path, restored);
|
|
4631
4881
|
}
|
|
4632
4882
|
}
|
|
4633
|
-
/** Execution timeout. */
|
|
4634
|
-
async _execWithTimeout(promise, entry, abort) {
|
|
4635
|
-
let timer;
|
|
4636
|
-
const timeout = new Promise((_, reject) => {
|
|
4637
|
-
timer = setTimeout(() => {
|
|
4638
|
-
this.timedOutProcessIds.add(entry.pid);
|
|
4639
|
-
abort();
|
|
4640
|
-
// The process cannot report exit after the request is cancelled, so
|
|
4641
|
-
// notify the session explicitly.
|
|
4642
|
-
try {
|
|
4643
|
-
this.hooks.onExternalExit?.(entry.pid, 124, // conventional timeout exit code
|
|
4644
|
-
`timeout after ${FACET_TIMEOUT_MS / 1000}s`);
|
|
4645
|
-
}
|
|
4646
|
-
catch { }
|
|
4647
|
-
reject(new Error(`Process timed out after ${FACET_TIMEOUT_MS / 1000}s`));
|
|
4648
|
-
}, FACET_TIMEOUT_MS);
|
|
4649
|
-
});
|
|
4650
|
-
// Always clear the timer; otherwise a successful run would still
|
|
4651
|
-
// trigger the timeout callback at FACET_TIMEOUT_MS, spuriously
|
|
4652
|
-
// marking the exit code as 124.
|
|
4653
|
-
try {
|
|
4654
|
-
return await Promise.race([promise, timeout]);
|
|
4655
|
-
}
|
|
4656
|
-
finally {
|
|
4657
|
-
if (timer !== undefined)
|
|
4658
|
-
clearTimeout(timer);
|
|
4659
|
-
}
|
|
4660
|
-
}
|
|
4661
4883
|
/**
|
|
4662
4884
|
* Re-drive a journalled launch after an instance reset. What the journal
|
|
4663
4885
|
* row carries is the recipe and nothing else: env and credentials are never
|
|
@@ -4669,9 +4891,15 @@ export class FacetManager {
|
|
|
4669
4891
|
switch (recipe.kind) {
|
|
4670
4892
|
case 'node': return this._spawnResident(recipe.code, recipe.opts, attempt);
|
|
4671
4893
|
case 'worker': {
|
|
4672
|
-
//
|
|
4673
|
-
//
|
|
4674
|
-
//
|
|
4894
|
+
// Which resolver a worker recipe re-drives through is decided by
|
|
4895
|
+
// `recipe.resident`, and it is not a flag: it is the interpreter
|
|
4896
|
+
// image (`{ runtime, argv }`) of a python/ruby socket server the
|
|
4897
|
+
// SESSION launched for itself. Only those recipes take the
|
|
4898
|
+
// image-store fallback, because the session persisted that image and
|
|
4899
|
+
// no embedder was ever asked about the launch. Every other worker
|
|
4900
|
+
// recipe — an embedder's `spawnWorker`, durable or not — re-drives
|
|
4901
|
+
// through the embedder's `resolveWorkerLaunch`, and falls back to the
|
|
4902
|
+
// image store only when no embedder hook is composed at all.
|
|
4675
4903
|
const resolve = recipe.resident
|
|
4676
4904
|
? this.hooks.resolveWorkerLaunchFallback
|
|
4677
4905
|
: this.hooks.resolveWorkerLaunch ?? this.hooks.resolveWorkerLaunchFallback;
|
|
@@ -4680,15 +4908,18 @@ export class FacetManager {
|
|
|
4680
4908
|
const resolved = await resolve(recipe);
|
|
4681
4909
|
if (resolved === null)
|
|
4682
4910
|
return undefined;
|
|
4683
|
-
const
|
|
4911
|
+
const mainModule = resolved.mainModule ?? recipe.mainModule ?? DEFAULT_WORKER_MAIN_MODULE;
|
|
4912
|
+
const { [mainModule]: workerCode, ...modules } = resolved.modules;
|
|
4684
4913
|
if (workerCode === undefined)
|
|
4685
|
-
throw new Error(
|
|
4914
|
+
throw new Error(`resolved worker launch carries no ${mainModule} main module`);
|
|
4686
4915
|
return this._spawnWorker(workerCode, record.command, recipe.cwd, {
|
|
4687
4916
|
port: recipe.resident ? record.port : recipe.port > 0 ? recipe.port : undefined, modules, compatibilityDate: recipe.compatibilityDate,
|
|
4688
4917
|
compatibilityFlags: recipe.compatibilityFlags, startArgs: resolved.startArgs ?? recipe.startArgs,
|
|
4689
4918
|
resident: recipe.resident, restart: record.restart,
|
|
4690
4919
|
env: resolved.env ?? undefined, globalOutbound: resolved.globalOutbound,
|
|
4691
4920
|
vfsWasmModules: resolved.vfsWasmModules,
|
|
4921
|
+
vfsTextModules: resolved.vfsTextModules,
|
|
4922
|
+
mainModule,
|
|
4692
4923
|
durable: { owner: residentOwner(record) ?? recipe.owner, image: recipe.image },
|
|
4693
4924
|
}, attempt);
|
|
4694
4925
|
}
|
|
@@ -4769,7 +5000,7 @@ export class FacetManager {
|
|
|
4769
5000
|
*/
|
|
4770
5001
|
async _runResidentLaunch(entry, code, command, opts, attempt) {
|
|
4771
5002
|
const cwd = opts.cwd || '/home/user';
|
|
4772
|
-
const pacer =
|
|
5003
|
+
const pacer = this._launchPacer(entry.pid);
|
|
4773
5004
|
// Journalled before the first byte of work. A launch that FAILS deletes
|
|
4774
5005
|
// its row on the way out — its process has already been exited and the
|
|
4775
5006
|
// user notified, so there is nothing left to owe. A launch that SETTLES
|
|
@@ -4881,17 +5112,9 @@ export class FacetManager {
|
|
|
4881
5112
|
}
|
|
4882
5113
|
}
|
|
4883
5114
|
async _residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName, launchEnv) {
|
|
4884
|
-
if (this.vfs && !this.esbuild) {
|
|
4885
|
-
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
4886
|
-
}
|
|
4887
|
-
if (this.vfs)
|
|
4888
|
-
this.imageStore.ensureDir();
|
|
4889
5115
|
const diagOn = isExecDiagEnabled();
|
|
4890
5116
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
4891
|
-
const
|
|
4892
|
-
const vfsState = processVfs
|
|
4893
|
-
? await buildPrefetchBundle(processVfs, opts.filename, cwd, code, this.esbuild || undefined, opts.bundleProfile, undefined, pacer, this.hooks.transformLargeEsm)
|
|
4894
|
-
: { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
|
|
5117
|
+
const vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd, entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
|
|
4895
5118
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
4896
5119
|
// The launch-time overlay (`$PORT`, `$NIMBUS_APP` under a reservation)
|
|
4897
5120
|
// rides on top of the recipe's env and is never journalled: it is
|
|
@@ -4910,12 +5133,15 @@ export class FacetManager {
|
|
|
4910
5133
|
FORCE_COLOR: opts.env?.FORCE_COLOR || '1',
|
|
4911
5134
|
}
|
|
4912
5135
|
: spawnEnv;
|
|
4913
|
-
|
|
5136
|
+
// Answered by _buildProcessBundle while the raw cells were still in hand.
|
|
5137
|
+
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
4914
5138
|
const [sqliteModules, shims] = await Promise.all([
|
|
4915
5139
|
this.sqliteModuleEntry(usesSqlite),
|
|
4916
5140
|
fetchNodeShimsCode(this.env),
|
|
4917
5141
|
]);
|
|
4918
|
-
|
|
5142
|
+
// Each image is read by path when the facet loads, never by value here.
|
|
5143
|
+
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
5144
|
+
let generatedWorker = await generateLongRunningNodeCode(code, vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite, shims, pacer);
|
|
4919
5145
|
// Sized here, while the map is still in hand. Reading these after the load
|
|
4920
5146
|
// would itself be what keeps the map alive, and the whole point of the
|
|
4921
5147
|
// scoping below is that nothing does.
|
|
@@ -4927,21 +5153,31 @@ export class FacetManager {
|
|
|
4927
5153
|
}
|
|
4928
5154
|
moduleMapBytes = _encodedSourceBytes(generatedWorker.code) + bundleBytes;
|
|
4929
5155
|
}
|
|
4930
|
-
const manifestBytes = diagOn ? _encodedSourceBytes(
|
|
4931
|
-
const metadataBytes = diagOn ? _encodedSourceBytes(
|
|
5156
|
+
const manifestBytes = diagOn ? _encodedSourceBytes(vfsState.serializedManifest ?? '') : 0;
|
|
5157
|
+
const metadataBytes = diagOn ? _encodedSourceBytes(vfsState.serializedMetadata ?? '') : 0;
|
|
4932
5158
|
const cacheHit = vfsState.cacheHit ?? false;
|
|
4933
5159
|
const vfsCursor = vfsState.cursor;
|
|
4934
|
-
|
|
5160
|
+
// The map is generated; the state's only remaining job is its cursor.
|
|
5161
|
+
// Released here, before the boot — releasing afterwards keeps the copy
|
|
5162
|
+
// alive for exactly the window that was resetting the isolate. A state
|
|
5163
|
+
// the cache retained is the cache's to keep, and is served to the next
|
|
5164
|
+
// launch of the same entry.
|
|
5165
|
+
if (!vfsState.cacheRetained)
|
|
5166
|
+
releaseGeneratedSources(vfsState);
|
|
4935
5167
|
let handle;
|
|
4936
5168
|
let resourcesTracked = false;
|
|
4937
5169
|
try {
|
|
4938
|
-
|
|
5170
|
+
// Assembled as one record of the SAME string objects the generator
|
|
5171
|
+
// produced — no copy — and then handed over: this frame drops its own
|
|
5172
|
+
// reference before materialize runs, and the sequence below drops each
|
|
5173
|
+
// source as the store takes it, so one image's text is resident rather
|
|
5174
|
+
// than every image's twice over.
|
|
5175
|
+
const sources = {
|
|
4939
5176
|
'worker.js': generatedWorker.code,
|
|
4940
5177
|
...generatedWorker.modules,
|
|
4941
|
-
}
|
|
4942
|
-
// The image store has taken it; this frame must not be what holds the
|
|
4943
|
-
// only other copy while the facet boots.
|
|
5178
|
+
};
|
|
4944
5179
|
generatedWorker = undefined;
|
|
5180
|
+
const vfsTextModules = await this.imageStore.materialize(entry.pid, drainSources(sources), pacer);
|
|
4945
5181
|
// Last gate before the facet exists. A launch now spans many turns, so
|
|
4946
5182
|
// a kill can land anywhere inside it; booting a process the table has
|
|
4947
5183
|
// already exited would leave a facet nothing owns, running against a
|
|
@@ -4992,6 +5228,7 @@ export class FacetManager {
|
|
|
4992
5228
|
rpcWrites: 0,
|
|
4993
5229
|
fsRpcReads: 0,
|
|
4994
5230
|
cacheHit,
|
|
5231
|
+
turns: pacer.chunks,
|
|
4995
5232
|
exitCode: 0,
|
|
4996
5233
|
at: Date.now(),
|
|
4997
5234
|
});
|
|
@@ -5035,20 +5272,43 @@ export class FacetManager {
|
|
|
5035
5272
|
}
|
|
5036
5273
|
}
|
|
5037
5274
|
catch (e) {
|
|
5275
|
+
// A program classified as long-running that ended on its own during
|
|
5276
|
+
// its boot — `json-server --version` prints and exits 0 — reported its
|
|
5277
|
+
// exit through the supervisor, which released the facet and rejected
|
|
5278
|
+
// the boot handshake with 'resident process released'. That is a
|
|
5279
|
+
// completed run, not a failed launch: the process table already holds
|
|
5280
|
+
// its real exit code, and the caller reports that code.
|
|
5038
5281
|
this.portRegistry.unregisterByPid(entry.pid);
|
|
5039
5282
|
if (resourcesTracked)
|
|
5040
5283
|
this.releaseProcessRpcResources(entry.pid);
|
|
5041
|
-
|
|
5042
|
-
|
|
5284
|
+
if (this.processExitCode(entry.pid) !== null)
|
|
5285
|
+
return;
|
|
5286
|
+
handle?.kill();
|
|
5043
5287
|
this._failLaunch(entry.pid, 'long-running node boot failed: ' + errorMessage(e));
|
|
5044
5288
|
throw e;
|
|
5045
5289
|
}
|
|
5046
5290
|
}
|
|
5047
5291
|
/**
|
|
5048
|
-
*
|
|
5292
|
+
* The exit code of a launched process that has already ended, or null
|
|
5293
|
+
* while it runs. What a caller that started a resident reads to tell a
|
|
5294
|
+
* server that is up from a program that finished during its boot.
|
|
5295
|
+
*/
|
|
5296
|
+
processExitCode(pid) {
|
|
5297
|
+
const entry = this.processes.get(pid);
|
|
5298
|
+
if (entry === undefined || entry.state === 'running')
|
|
5299
|
+
return null;
|
|
5300
|
+
return entry.exitCode ?? 0;
|
|
5301
|
+
}
|
|
5302
|
+
/**
|
|
5303
|
+
* Spawn a long-running dynamic Worker, boot it, and return its boot payload
|
|
5304
|
+
* beside the pid and the process's own inbound facet.
|
|
5049
5305
|
*
|
|
5050
5306
|
* The shared primitive for any runtime that serves over
|
|
5051
|
-
* handleHttpRequest(Request) — the python and ruby socket servers today
|
|
5307
|
+
* handleHttpRequest(Request) — the python and ruby socket servers today —
|
|
5308
|
+
* and for an embedder's own Worker-class program: `workerCode` boots as
|
|
5309
|
+
* `opts.mainModule` (default `worker.js`), `opts.modules` ride inline,
|
|
5310
|
+
* `opts.vfsTextModules` and `opts.vfsWasmModules` are read by path when the
|
|
5311
|
+
* facet loads.
|
|
5052
5312
|
*
|
|
5053
5313
|
* The interpreter image it carries is the memory that should not sit in the
|
|
5054
5314
|
* session's own isolate — ruby's interpreter+stdlib alone is 34.3 MiB — and
|
|
@@ -5056,6 +5316,11 @@ export class FacetManager {
|
|
|
5056
5316
|
* no readiness coupling back into the session: the runner answers
|
|
5057
5317
|
* startProcess with its boot payload and the caller waits on that one
|
|
5058
5318
|
* promise, so nothing polls the port to decide the process is up.
|
|
5319
|
+
*
|
|
5320
|
+
* The returned `facet` is bound to the resident handle's route target — the
|
|
5321
|
+
* same target a registered port routes to — so a port-less process can be
|
|
5322
|
+
* invoked directly. It has no release: `kill(pid)` is the one lifecycle
|
|
5323
|
+
* owner, and the facet is dead once the pid is.
|
|
5059
5324
|
*/
|
|
5060
5325
|
async spawnWorker(workerCode, command, cwd, opts = {}) {
|
|
5061
5326
|
return this._spawnWorker(workerCode, command, cwd, opts, 0);
|
|
@@ -5066,7 +5331,9 @@ export class FacetManager {
|
|
|
5066
5331
|
opts = { ...opts, durable: { owner: await deriveResidentOwner(cwd, opts.resident.argv) } };
|
|
5067
5332
|
}
|
|
5068
5333
|
this.processes.reap();
|
|
5069
|
-
|
|
5334
|
+
// The table entry carries the same argv the identity is derived from, so
|
|
5335
|
+
// a runtime resident reads the same way through either path.
|
|
5336
|
+
const entry = this.processes.spawn(command, opts.resident?.argv ?? [], cwd);
|
|
5070
5337
|
// Stamp the process-table entry so /api/processes exposes this as a
|
|
5071
5338
|
// long-running process.
|
|
5072
5339
|
this.processes.setLongRunning(entry.pid);
|
|
@@ -5078,6 +5345,10 @@ export class FacetManager {
|
|
|
5078
5345
|
catch { }
|
|
5079
5346
|
const compatibilityDate = opts.compatibilityDate ?? CF_COMPAT_DATE;
|
|
5080
5347
|
const compatibilityFlags = opts.compatibilityFlags || ['nodejs_compat'];
|
|
5348
|
+
const mainModule = opts.mainModule ?? DEFAULT_WORKER_MAIN_MODULE;
|
|
5349
|
+
if (opts.modules && Object.hasOwn(opts.modules, mainModule)) {
|
|
5350
|
+
throw new Error(`Nimbus: spawnWorker main module '${mainModule}' is also an inline module; workerCode is the main module`);
|
|
5351
|
+
}
|
|
5081
5352
|
let handle;
|
|
5082
5353
|
let resourcesTracked = false;
|
|
5083
5354
|
let record;
|
|
@@ -5115,6 +5386,8 @@ export class FacetManager {
|
|
|
5115
5386
|
modules: opts.modules ?? {},
|
|
5116
5387
|
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
5117
5388
|
vfsWasmModules: opts.vfsWasmModules,
|
|
5389
|
+
vfsTextModules: opts.vfsTextModules,
|
|
5390
|
+
...(opts.mainModule !== undefined ? { mainModule: opts.mainModule } : {}),
|
|
5118
5391
|
startArgs: opts.startArgs,
|
|
5119
5392
|
});
|
|
5120
5393
|
await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -5141,6 +5414,7 @@ export class FacetManager {
|
|
|
5141
5414
|
compatibilityFlags,
|
|
5142
5415
|
...(opts.durable.image && !opts.resident ? { startArgs: opts.startArgs } : {}),
|
|
5143
5416
|
...(opts.resident ? { resident: opts.resident } : {}),
|
|
5417
|
+
...(opts.mainModule !== undefined ? { mainModule: opts.mainModule } : {}),
|
|
5144
5418
|
},
|
|
5145
5419
|
// The row-level fields `ensureDurableAppOnPort` reads — same values
|
|
5146
5420
|
// the recipe carries, stamped so port/owner lookup never depends on
|
|
@@ -5185,9 +5459,10 @@ export class FacetManager {
|
|
|
5185
5459
|
code: {
|
|
5186
5460
|
compatibilityDate,
|
|
5187
5461
|
compatibilityFlags,
|
|
5188
|
-
mainModule
|
|
5189
|
-
modules: {
|
|
5462
|
+
mainModule,
|
|
5463
|
+
modules: { ...(opts.modules || {}), [mainModule]: workerCode },
|
|
5190
5464
|
vfsWasmModules: opts.vfsWasmModules,
|
|
5465
|
+
...(opts.vfsTextModules !== undefined ? { vfsTextModules: opts.vfsTextModules } : {}),
|
|
5191
5466
|
...(opts.env !== undefined || launchEnv !== undefined ? { env: { ...opts.env, ...launchEnv } } : {}),
|
|
5192
5467
|
...(opts.globalOutbound !== undefined ? { globalOutbound: opts.globalOutbound } : {}),
|
|
5193
5468
|
},
|
|
@@ -5218,7 +5493,20 @@ export class FacetManager {
|
|
|
5218
5493
|
}
|
|
5219
5494
|
await this._registerResidentPort(entry.pid, opts.port);
|
|
5220
5495
|
}
|
|
5221
|
-
|
|
5496
|
+
const target = handle.routeTarget;
|
|
5497
|
+
return {
|
|
5498
|
+
pid: entry.pid,
|
|
5499
|
+
boot,
|
|
5500
|
+
facet: {
|
|
5501
|
+
fetch: (request) => target.handleHttpRequest(request),
|
|
5502
|
+
connect: async (request) => {
|
|
5503
|
+
if (typeof target.handleWebSocketRequest !== 'function') {
|
|
5504
|
+
throw new Error(`Nimbus: worker pid ${entry.pid} accepts no WebSocket upgrades`);
|
|
5505
|
+
}
|
|
5506
|
+
return target.handleWebSocketRequest(request);
|
|
5507
|
+
},
|
|
5508
|
+
},
|
|
5509
|
+
};
|
|
5222
5510
|
}
|
|
5223
5511
|
catch (e) {
|
|
5224
5512
|
this.portRegistry.unregisterByPid(entry.pid);
|
|
@@ -5294,7 +5582,14 @@ export class FacetManager {
|
|
|
5294
5582
|
this.hooks.notify?.(`\x1b[2m[nimbus: "${row.command}" listened on ${port} but its reservation owns `
|
|
5295
5583
|
+ `${row.portMismatch.reserved} ($PORT) — the app's URL will not reach it]\x1b[0m\r\n`);
|
|
5296
5584
|
}
|
|
5297
|
-
|
|
5585
|
+
// A pid nothing journalled — the in-process dev servers, a builtin's
|
|
5586
|
+
// adopted wrapper pid — still has an identity: the process table's. It
|
|
5587
|
+
// re-adopts a reservation it owns exactly as a resident does; the one
|
|
5588
|
+
// thing it cannot do is adopt an EXPLICIT owner, because there is no row
|
|
5589
|
+
// to stamp the adoption on, so it registers ephemeral on that port.
|
|
5590
|
+
const owner = row === undefined
|
|
5591
|
+
? (await this.residentIdentity(pid))?.owner
|
|
5592
|
+
: residentOwner(row);
|
|
5298
5593
|
if (claimedBy !== undefined && row !== undefined && this.residentClaims.get(pid) !== claimedBy) {
|
|
5299
5594
|
const previousOwner = this.residentClaims.get(pid);
|
|
5300
5595
|
await this._releaseResidentClaim(pid);
|
|
@@ -5338,15 +5633,33 @@ export class FacetManager {
|
|
|
5338
5633
|
await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port);
|
|
5339
5634
|
this.portRegistry.register(port, pid);
|
|
5340
5635
|
}
|
|
5341
|
-
/**
|
|
5636
|
+
/**
|
|
5637
|
+
* Who a pid is. One resolver, in precedence order: the ephemeral-duplicate
|
|
5638
|
+
* mark, the journal row (the owner a launch this manager made was stamped
|
|
5639
|
+
* with — derived from the launch's own cwd+argv, or adopted from an
|
|
5640
|
+
* explicit reservation), and finally the process table. The table knows
|
|
5641
|
+
* cwd and argv for every pid, so a serving process nothing journalled — the
|
|
5642
|
+
* in-process Vite dev server, real-vite, a staged artifact, any wrapper pid
|
|
5643
|
+
* a builtin adopted — has the same derived identity shape as a resident
|
|
5644
|
+
* and answers to the same app verbs. It just cannot be re-driven after a
|
|
5645
|
+
* reset: only a journal row carries a recipe. Null only for a pid that is
|
|
5646
|
+
* neither journalled nor running.
|
|
5647
|
+
*/
|
|
5342
5648
|
async residentIdentity(pid) {
|
|
5343
5649
|
const duplicated = this.ephemeralPids.get(pid);
|
|
5344
5650
|
if (duplicated !== undefined)
|
|
5345
5651
|
return { owner: duplicated, ephemeral: true, port: undefined };
|
|
5346
5652
|
const row = (await this.launchJournal.rows()).get(`${FENCED_WORK_KEY_PREFIX}${pid}`);
|
|
5347
|
-
if (row
|
|
5653
|
+
if (row !== undefined)
|
|
5654
|
+
return { owner: residentOwner(row), ephemeral: false, port: row.port };
|
|
5655
|
+
const entry = this.processes.get(pid);
|
|
5656
|
+
if (entry === undefined || entry.state !== 'running')
|
|
5348
5657
|
return null;
|
|
5349
|
-
return {
|
|
5658
|
+
return {
|
|
5659
|
+
owner: await deriveResidentOwner(entry.cwd, entry.argv),
|
|
5660
|
+
ephemeral: false,
|
|
5661
|
+
port: this.portRegistry.getAll().find((live) => live.pid === pid)?.port,
|
|
5662
|
+
};
|
|
5350
5663
|
}
|
|
5351
5664
|
/**
|
|
5352
5665
|
* Every stamped identity — the reservations, and the journal rows that
|
|
@@ -5398,6 +5711,28 @@ export class FacetManager {
|
|
|
5398
5711
|
}
|
|
5399
5712
|
apps.set(owner, app);
|
|
5400
5713
|
}
|
|
5714
|
+
// Every process serving a port is an application whether or not a
|
|
5715
|
+
// journal row backs it — the identity comes from the same resolver the
|
|
5716
|
+
// verbs use, so what `list` shows is what `expose` will bind. A row-backed
|
|
5717
|
+
// app already carries its pid from above; this only adds the servers
|
|
5718
|
+
// nothing journalled (the dev servers, adopted wrapper pids).
|
|
5719
|
+
for (const live of this.portRegistry.getAll()) {
|
|
5720
|
+
const identity = await this.residentIdentity(live.pid);
|
|
5721
|
+
if (identity === null || identity.ephemeral || identity.owner === undefined)
|
|
5722
|
+
continue;
|
|
5723
|
+
const app = apps.get(identity.owner) ?? {
|
|
5724
|
+
owner: identity.owner, name: null, port: null, pid: null, status: 'stopped',
|
|
5725
|
+
visibility: 'scoped', capability: null, restart: 'never', diagnostic: null,
|
|
5726
|
+
};
|
|
5727
|
+
if (app.pid !== null)
|
|
5728
|
+
continue;
|
|
5729
|
+
app.pid = live.pid;
|
|
5730
|
+
if (app.port === null)
|
|
5731
|
+
app.port = live.port;
|
|
5732
|
+
if (app.status === 'stopped')
|
|
5733
|
+
app.status = 'running';
|
|
5734
|
+
apps.set(identity.owner, app);
|
|
5735
|
+
}
|
|
5401
5736
|
return [...apps.values()];
|
|
5402
5737
|
}
|
|
5403
5738
|
async registerPort(pid, port) {
|