@nimbus-sh/worker 0.6.0 → 0.7.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 +661 -337
- 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/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/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +6 -5
- 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/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 +3 -0
- package/dist/runtime/node-runner.d.ts.map +1 -1
- package/dist/runtime/node-runner.js +7 -0
- 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 +2 -2
- 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/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 -1
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +82 -603
- package/dist/session/nimbus-session.d.ts +56 -22
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +130 -101
- package/dist/session/npm-install-port.d.ts +16 -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 +31 -5
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +94 -68
- 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 +97 -47
- 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 +2 -2
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +13 -7
- 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/package.json +24 -4
- 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';
|
|
@@ -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,7 +369,7 @@ 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) {
|
|
372
|
+
export async function generateEntrypointCode(userCode, vfsState, usesSqlite, shims, wasmImports = []) {
|
|
399
373
|
const safeCode = JSON.stringify(userCode);
|
|
400
374
|
const bundleSource = await facetVfsBundleSourceFor(vfsState);
|
|
401
375
|
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
@@ -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,6 +617,61 @@ ${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
676
|
const safeCode = JSON.stringify(userCode);
|
|
661
677
|
const safeArgs = JSON.stringify({
|
|
@@ -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);
|
|
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);
|
|
1959
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);
|
|
@@ -3039,7 +3210,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3039
3210
|
* behaviour for code paths that don't have esbuild handy).
|
|
3040
3211
|
*
|
|
3041
3212
|
*/
|
|
3042
|
-
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
3213
|
+
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes) {
|
|
3043
3214
|
// This build accumulates raw VFS contents in the supervisor heap, and did it
|
|
3044
3215
|
// with nothing watching: the estimator read 9.4 MiB while these bytes were
|
|
3045
3216
|
// resetting the DO three times. Take the budget the enrichment passes are
|
|
@@ -3049,19 +3220,25 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
|
|
|
3049
3220
|
const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
|
|
3050
3221
|
prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
|
|
3051
3222
|
try {
|
|
3052
|
-
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform);
|
|
3223
|
+
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform, maxBundleBytes);
|
|
3053
3224
|
}
|
|
3054
3225
|
finally {
|
|
3055
3226
|
prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
|
|
3056
3227
|
lease.release();
|
|
3057
3228
|
}
|
|
3058
3229
|
}
|
|
3059
|
-
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
3230
|
+
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform, maxBundleBytes = VFS_BUNDLE_MAX_BYTES) {
|
|
3060
3231
|
// Read the cursor BEFORE the walk: a mutation that lands while the bundle
|
|
3061
3232
|
// is being assembled must be reported as invalidated, not silently missed.
|
|
3062
3233
|
const cursor = { epoch: vfs.epoch, rev: vfs.revision() };
|
|
3063
3234
|
// 1. Static reachable-set walk from entry.
|
|
3064
|
-
const prefetch = prefetchForRequire(vfs, entryCode || '', cwd, scriptPath);
|
|
3235
|
+
const prefetch = prefetchForRequire(vfs, entryCode || '', cwd, scriptPath, maxBundleBytes);
|
|
3236
|
+
if ('kind' in prefetch) {
|
|
3237
|
+
// A required closure larger than the bound can never launch as a
|
|
3238
|
+
// snapshot. Surface it as the process's own failure rather than a
|
|
3239
|
+
// build error: the caller maps it to exit 1 + stderr.
|
|
3240
|
+
throw new ClosureBoundExceededError(prefetch);
|
|
3241
|
+
}
|
|
3065
3242
|
const bundle = { ...prefetch.bundle };
|
|
3066
3243
|
const requiredPaths = new Set(Object.keys(prefetch.bundle));
|
|
3067
3244
|
let truncated = false;
|
|
@@ -3092,7 +3269,7 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3092
3269
|
// Catches dynamic-require / `bindings()` / plugin-loader cases the
|
|
3093
3270
|
// regex prefetch misses. Its budget is independent from the complete
|
|
3094
3271
|
// static require closure, which is correctness-critical.
|
|
3095
|
-
const greedy = greedyAddMainEntries(vfs, cwd, bundle, budgetState);
|
|
3272
|
+
const greedy = greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths);
|
|
3096
3273
|
await paceAfterPass();
|
|
3097
3274
|
// 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
|
|
3098
3275
|
// bundle .js/.mjs/.cjs source for the canonical jsdom shape:
|
|
@@ -3125,7 +3302,6 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3125
3302
|
// as data, etc.) without needing a per-pkg whitelist.
|
|
3126
3303
|
// No-op when entry isn't inside node_modules.
|
|
3127
3304
|
const binSiblingAdd = addBinTargetSiblings(vfs, scriptPath, bundle, budgetState, bundleProfile);
|
|
3128
|
-
void binSiblingAdd;
|
|
3129
3305
|
await paceAfterPass();
|
|
3130
3306
|
// 2.34 project-data snapshot: sync Node fs cannot await the
|
|
3131
3307
|
// supervisor. Include a bounded snapshot of the current working tree
|
|
@@ -3241,6 +3417,7 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3241
3417
|
const bundleSideModulesRequired = size.bytes > BUNDLE_MAX_ENCODED_BYTES;
|
|
3242
3418
|
// Suppress lint: `greedy.added` is observed only via diagnostics.
|
|
3243
3419
|
void greedy;
|
|
3420
|
+
const wasmImages = collectClosureWasmImages(vfs, bundle, binSiblingAdd.wasmPaths);
|
|
3244
3421
|
return {
|
|
3245
3422
|
bundle,
|
|
3246
3423
|
manifest,
|
|
@@ -3249,8 +3426,11 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3249
3426
|
reachableCount: fileCount,
|
|
3250
3427
|
truncated,
|
|
3251
3428
|
bundleSideModulesRequired,
|
|
3429
|
+
...(wasmImages.length > 0 ? { wasmImages } : {}),
|
|
3252
3430
|
};
|
|
3253
3431
|
}
|
|
3432
|
+
/** The main module name a worker launch boots from unless told otherwise. */
|
|
3433
|
+
export const DEFAULT_WORKER_MAIN_MODULE = 'worker.js';
|
|
3254
3434
|
const ROUTEABLE_PORT_ATTACH_TIMEOUT_MS = 1_000;
|
|
3255
3435
|
/** A port request may wait this long for a durable app's re-drive to boot. */
|
|
3256
3436
|
const DURABLE_ENSURE_BOOT_BUDGET_MS = 12_000;
|
|
@@ -3316,7 +3496,6 @@ export class FacetManager {
|
|
|
3316
3496
|
* journal recovery rides the first pump.
|
|
3317
3497
|
*/
|
|
3318
3498
|
launchPump;
|
|
3319
|
-
timedOutProcessIds = new Set();
|
|
3320
3499
|
// attach-pid → serve-pid: the resident serve facet a bare-`opencode` dual
|
|
3321
3500
|
// spawn created as an OS-child of the attach TUI. When the attach process
|
|
3322
3501
|
// exits (reported / killed), its serve facet is torn down with it.
|
|
@@ -3564,61 +3743,92 @@ export class FacetManager {
|
|
|
3564
3743
|
*/
|
|
3565
3744
|
setEsbuildService(esbuild) { this.esbuild = esbuild; }
|
|
3566
3745
|
/**
|
|
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.
|
|
3746
|
+
* The pacer every launch is built under: the session's alarm-driven turn
|
|
3747
|
+
* pump, the deployment's chunk bound, and the one check a suspended launch
|
|
3748
|
+
* makes when it resumes — that the process it is building for still exists.
|
|
3575
3749
|
*/
|
|
3750
|
+
_launchPacer(pid) {
|
|
3751
|
+
return new TurnBudget(this.launchPump, turnChunkMaxBytes(this.env), () => this._assertLaunchStillOwned(pid));
|
|
3752
|
+
}
|
|
3576
3753
|
/**
|
|
3577
|
-
*
|
|
3754
|
+
* Assemble the filesystem bundle a process boots on, across as many
|
|
3755
|
+
* Durable Object turns as it takes.
|
|
3756
|
+
*
|
|
3757
|
+
* The one builder for every Node process this manager starts. A one-shot
|
|
3758
|
+
* exec and a resident launch used to own two copies of this: exec's was
|
|
3759
|
+
* memoized behind the prefetch cache and raced a wall-clock deadline in a
|
|
3760
|
+
* single turn; the resident's was paged with a TurnBudget and never cached.
|
|
3761
|
+
* Two paths, one job — and a tree large enough to page on one path hit the
|
|
3762
|
+
* deadline on the other, failing every `node -e` in it with "assembling the
|
|
3763
|
+
* filesystem bundle … exceeded". What the two callers genuinely differ in is
|
|
3764
|
+
* the entry, the working directory and the process they build for; that is
|
|
3765
|
+
* all they supply. Everything else — the revision-keyed cache and its stale
|
|
3766
|
+
* eviction, the residency profile a previous miss learned, the reachable-set
|
|
3767
|
+
* walk and its enrichment passes, the ESM→CJS transform, the manifest and
|
|
3768
|
+
* metadata, the module-map serialization with its side-module split, the
|
|
3769
|
+
* `node:sqlite` answer, and the release of the raw cells once they are
|
|
3770
|
+
* serialized — happens here, once, and yields the turn whenever a chunk's
|
|
3771
|
+
* worth of it has been done.
|
|
3578
3772
|
*
|
|
3579
|
-
*
|
|
3580
|
-
*
|
|
3581
|
-
*
|
|
3582
|
-
*
|
|
3583
|
-
*
|
|
3584
|
-
* never returns, with no exit record for the process.
|
|
3773
|
+
* There is no deadline. A launch that spans turns costs turns, not a held
|
|
3774
|
+
* thread, so a large tree is not a defect to be reported at N seconds; the
|
|
3775
|
+
* pacer's `stillWanted` check is what ends a build nothing will use — a
|
|
3776
|
+
* process killed while its build was suspended throws from the next resume,
|
|
3777
|
+
* and the caller reports that as it reports any other launch failure.
|
|
3585
3778
|
*
|
|
3586
|
-
*
|
|
3587
|
-
*
|
|
3588
|
-
*
|
|
3589
|
-
*
|
|
3590
|
-
*
|
|
3591
|
-
*
|
|
3592
|
-
*
|
|
3593
|
-
*
|
|
3594
|
-
*
|
|
3595
|
-
*
|
|
3596
|
-
* makes the remaining class the only one left to explain.
|
|
3779
|
+
* The returned state carries its serialized forms (`bundleSource`,
|
|
3780
|
+
* `serializedManifest`, `serializedMetadata`) and has already released the
|
|
3781
|
+
* raw ones: `generateEntrypointCode` and `generateLongRunningNodeCode` read
|
|
3782
|
+
* the serialized forms and nothing else. A state the cache retained belongs
|
|
3783
|
+
* to the cache — a caller must not release its serialized forms either
|
|
3784
|
+
* (`cacheRetained` says which); one the cache refused belongs to the caller
|
|
3785
|
+
* alone, and `releaseGeneratedSources` drops it once a map is generated.
|
|
3786
|
+
*
|
|
3787
|
+
* A session without a filesystem gets an empty state: there is nothing to
|
|
3788
|
+
* stage and nothing to yield for.
|
|
3597
3789
|
*/
|
|
3598
|
-
|
|
3599
|
-
|
|
3600
|
-
|
|
3601
|
-
|
|
3602
|
-
|
|
3603
|
-
|
|
3604
|
-
|
|
3605
|
-
|
|
3606
|
-
|
|
3607
|
-
|
|
3608
|
-
|
|
3609
|
-
|
|
3610
|
-
|
|
3611
|
-
|
|
3612
|
-
|
|
3613
|
-
|
|
3614
|
-
|
|
3615
|
-
|
|
3616
|
-
|
|
3790
|
+
/**
|
|
3791
|
+
* The closure's wasm images as module-map members for a one-shot facet,
|
|
3792
|
+
* read by path under the process's own credential. An image that cannot
|
|
3793
|
+
* be read is left out; the program's compile then meets the seam's own
|
|
3794
|
+
* refusal, which names the module.
|
|
3795
|
+
*/
|
|
3796
|
+
_wasmModulesByValue(entry, wasmImports) {
|
|
3797
|
+
const modules = {};
|
|
3798
|
+
if (!this.vfs || wasmImports.length === 0)
|
|
3799
|
+
return modules;
|
|
3800
|
+
const vfs = this.vfs.as(entry.cred);
|
|
3801
|
+
for (const image of wasmImports) {
|
|
3802
|
+
let bytes;
|
|
3803
|
+
try {
|
|
3804
|
+
bytes = vfs.readFile(stripLeadingSlashes(image.vfsPath));
|
|
3805
|
+
}
|
|
3806
|
+
catch {
|
|
3807
|
+
continue;
|
|
3808
|
+
}
|
|
3809
|
+
modules[image.moduleName] = {
|
|
3810
|
+
wasm: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
|
3811
|
+
};
|
|
3617
3812
|
}
|
|
3813
|
+
return modules;
|
|
3618
3814
|
}
|
|
3619
|
-
async
|
|
3620
|
-
|
|
3621
|
-
|
|
3815
|
+
async _buildProcessBundle(entry, spec, pacer) {
|
|
3816
|
+
if (!this.vfs) {
|
|
3817
|
+
return { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
|
|
3818
|
+
}
|
|
3819
|
+
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so ESM
|
|
3820
|
+
// source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
3821
|
+
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to CJS
|
|
3822
|
+
// before they hit the facet's `new Function` pre-compile loop. Lazy-create
|
|
3823
|
+
// one if NimbusSession didn't share its own.
|
|
3824
|
+
if (!this.esbuild)
|
|
3825
|
+
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
3826
|
+
this.imageStore.ensureDir();
|
|
3827
|
+
const vfs = this.vfs.as(entry.cred);
|
|
3828
|
+
const { cred } = entry;
|
|
3829
|
+
const profile = spec.bundleProfile ?? DEFAULT_FACET_BUNDLE_PROFILE;
|
|
3830
|
+
const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
|
|
3831
|
+
const key = `${profile}\x00${credKey}\x00${spec.cwd}\x00${spec.scriptPath ?? ''}\x00${_fnv1a(spec.entryCode)}`;
|
|
3622
3832
|
const revision = vfs.revision();
|
|
3623
3833
|
// An entry built at an older revision can never be SERVED again — the
|
|
3624
3834
|
// lookup below requires an exact match — so from the first write after it
|
|
@@ -3627,11 +3837,11 @@ export class FacetManager {
|
|
|
3627
3837
|
// replaces it allocates, so the stale filesystem graph and the new one
|
|
3628
3838
|
// never co-reside.
|
|
3629
3839
|
let evictedStale = false;
|
|
3630
|
-
for (const [staleKey,
|
|
3631
|
-
if (
|
|
3840
|
+
for (const [staleKey, stale] of this.prefetchBundleCache) {
|
|
3841
|
+
if (stale.revision === revision)
|
|
3632
3842
|
continue;
|
|
3633
3843
|
this.prefetchBundleCache.delete(staleKey);
|
|
3634
|
-
this.prefetchCacheBytes -=
|
|
3844
|
+
this.prefetchCacheBytes -= stale.bytes;
|
|
3635
3845
|
evictedStale = true;
|
|
3636
3846
|
}
|
|
3637
3847
|
if (evictedStale)
|
|
@@ -3643,24 +3853,26 @@ export class FacetManager {
|
|
|
3643
3853
|
this.prefetchBundleCache.set(key, cached);
|
|
3644
3854
|
return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
|
|
3645
3855
|
}
|
|
3646
|
-
const vfsState = await buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, this.esbuild
|
|
3856
|
+
const vfsState = await buildPrefetchBundle(vfs, spec.scriptPath, spec.cwd, spec.entryCode, this.esbuild, profile, this.residencyProfiles.get(key), pacer, this.hooks.transformLargeEsm);
|
|
3647
3857
|
vfsState.bundleKey = key;
|
|
3648
|
-
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired);
|
|
3858
|
+
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired, pacer);
|
|
3649
3859
|
vfsState.serializedManifest = JSON.stringify(vfsState.manifest);
|
|
3650
3860
|
vfsState.serializedMetadata = JSON.stringify(vfsState.metadata);
|
|
3861
|
+
// Two more serializations of the same weight as the passes before them.
|
|
3862
|
+
await pacer.spend(vfsState.serializedManifest.length + vfsState.serializedMetadata.length);
|
|
3651
3863
|
// The only consumer of the raw cells past this point is a single boolean,
|
|
3652
3864
|
// so answer it now rather than hold ~17 MB (pi) to answer it later.
|
|
3653
|
-
vfsState.usesNodeSqlite = bundleUsesNodeSqlite(entryCode, vfsState.bundle);
|
|
3865
|
+
vfsState.usesNodeSqlite = bundleUsesNodeSqlite(spec.entryCode, vfsState.bundle);
|
|
3654
3866
|
vfsState.cacheHit = false;
|
|
3655
3867
|
// Serialization is total: bundleSource/serializedManifest/serializedMetadata
|
|
3656
|
-
// carry every byte the raw cells and objects do, and
|
|
3657
|
-
//
|
|
3658
|
-
//
|
|
3659
|
-
//
|
|
3660
|
-
//
|
|
3661
|
-
//
|
|
3662
|
-
//
|
|
3663
|
-
//
|
|
3868
|
+
// carry every byte the raw cells and objects do, and both generators read
|
|
3869
|
+
// only the serialized forms. Retaining both doubled what an entry costs
|
|
3870
|
+
// for its whole lifetime — measured for pi at 502af77, per entry: raw
|
|
3871
|
+
// 17,253,610 + source 18,262,324 + manifest 600,060 + metadata 3,841,244
|
|
3872
|
+
// = 39,957,238 B, of which the raw halves are 21,694,914 B held to answer
|
|
3873
|
+
// `usesNodeSqlite`. Dropping them is a pure release: nothing downstream of
|
|
3874
|
+
// this method reads them, and a cache MISS rebuilds from the VFS rather
|
|
3875
|
+
// than from anything discarded here.
|
|
3664
3876
|
//
|
|
3665
3877
|
// Released BEFORE admission so the byte bound prices what the entry costs
|
|
3666
3878
|
// from here on, not the peak it passed through on the way in.
|
|
@@ -3823,7 +4035,7 @@ export class FacetManager {
|
|
|
3823
4035
|
if (opts.skipSpawn && opts.callerPid != null) {
|
|
3824
4036
|
// The caller already allocated the PID via the supervisor
|
|
3825
4037
|
// (with their own user-facing label). Look up the full entry
|
|
3826
|
-
// from the table —
|
|
4038
|
+
// from the table — the exec path needs the canonical
|
|
3827
4039
|
// ProcessEntry shape. Do NOT reap() either: reaping would
|
|
3828
4040
|
// clear the caller's just-spawned entry because its startTime
|
|
3829
4041
|
// is recent (< 60s) but reap() ALSO drops 'running' entries
|
|
@@ -3848,30 +4060,62 @@ export class FacetManager {
|
|
|
3848
4060
|
catch { }
|
|
3849
4061
|
}
|
|
3850
4062
|
}
|
|
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
4063
|
const diagOn = isExecDiagEnabled();
|
|
3860
4064
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
3861
|
-
|
|
3862
|
-
|
|
3863
|
-
|
|
3864
|
-
|
|
3865
|
-
|
|
3866
|
-
|
|
3867
|
-
|
|
4065
|
+
// Paced like a resident launch: a tree too large for one turn costs
|
|
4066
|
+
// turns, and the pacer's stillWanted check ends a build whose process was
|
|
4067
|
+
// killed while it was suspended. The pacer is settled in the finally
|
|
4068
|
+
// below and not before, because the invocation that granted the last
|
|
4069
|
+
// chunk awaits `chunkEnded` (PacedWork.pump) and has to stay the one
|
|
4070
|
+
// that owns the run: settling at the end of the build would release that
|
|
4071
|
+
// turn with the facet still to load and run on it.
|
|
4072
|
+
const pacer = this._launchPacer(entry.pid);
|
|
4073
|
+
let vfsState;
|
|
4074
|
+
try {
|
|
4075
|
+
vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd: opts.cwd || '/home/user', entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
|
|
4076
|
+
}
|
|
4077
|
+
catch (err) {
|
|
4078
|
+
// The require closure that cannot fit the snapshot bound is the
|
|
4079
|
+
// process's own answer, not a build failure: exit 1 with a stderr
|
|
4080
|
+
// that names the entry, the staged bytes at the stop, the bound,
|
|
4081
|
+
// and the remedy — the same shape the got/next guards print.
|
|
4082
|
+
pacer.settle();
|
|
4083
|
+
if (err instanceof ClosureBoundExceededError) {
|
|
4084
|
+
const o = err.outcome;
|
|
4085
|
+
const mib = (n) => `${(n / 1048576).toFixed(1)} MiB`;
|
|
4086
|
+
const stderr = `require closure for ${o.entry} exceeds the snapshot bound: ` +
|
|
4087
|
+
`${mib(o.bytesSeen)} staged when ${o.lastPath} crossed the ` +
|
|
4088
|
+
`${mib(o.bound)} bound; the process was not started.\n` +
|
|
4089
|
+
`Run it as a server or split the entry; there is no flag to bypass.`;
|
|
4090
|
+
if (this.processes.get(entry.pid)?.state === 'running') {
|
|
4091
|
+
this._failLaunch(entry.pid, stderr);
|
|
4092
|
+
}
|
|
4093
|
+
return { exitCode: 1, stdout: '', stderr };
|
|
4094
|
+
}
|
|
4095
|
+
// A failed build is thrown to the caller exactly as before. What must
|
|
4096
|
+
// not be left behind is the process entry: it was spawned above and
|
|
4097
|
+
// would otherwise sit 'running' forever for a process that never
|
|
4098
|
+
// started. A build ended by a kill finds its entry already exited and
|
|
4099
|
+
// reports nothing twice.
|
|
4100
|
+
if (this.processes.get(entry.pid)?.state === 'running') {
|
|
4101
|
+
this._failLaunch(entry.pid, `assembling the filesystem bundle for \`${command}\` failed: ${errorMessage(err)}`);
|
|
4102
|
+
}
|
|
4103
|
+
throw err;
|
|
4104
|
+
}
|
|
3868
4105
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
3869
4106
|
const diagSink = diagOn
|
|
3870
4107
|
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0, manifestBytes: 0, metadataBytes: 0 }
|
|
3871
4108
|
: undefined;
|
|
3872
4109
|
const abortController = new AbortController();
|
|
4110
|
+
// Ctrl-C / kill on a user program has to end the in-flight run, not just
|
|
4111
|
+
// mark the table row: the runOnce request carries this signal.
|
|
4112
|
+
this.processes.setTerminator(entry.pid, () => abortController.abort());
|
|
4113
|
+
// A shell Ctrl+C arrives as an abort on the command's signal — it is the
|
|
4114
|
+
// same kill, forwarded onto the controller the runOnce request carries.
|
|
4115
|
+
const onShellAbort = () => abortController.abort();
|
|
4116
|
+
opts.signal?.addEventListener('abort', onShellAbort, { once: true });
|
|
3873
4117
|
try {
|
|
3874
|
-
const result = await this.
|
|
4118
|
+
const result = await this._execViaLoader(code, opts, entry, vfsState, abortController.signal, diagSink);
|
|
3875
4119
|
this._flushVfsWrites(result, entry.pid);
|
|
3876
4120
|
this._recordResidencyMisses(vfsState.bundleKey, result.residencyMisses);
|
|
3877
4121
|
this.processes.exit(entry.pid, result.exitCode);
|
|
@@ -3892,6 +4136,7 @@ export class FacetManager {
|
|
|
3892
4136
|
rpcWrites: result.diag?.rpcWrites ?? 0,
|
|
3893
4137
|
fsRpcReads: result.diag?.fsRpcReads ?? 0,
|
|
3894
4138
|
cacheHit: vfsState.cacheHit ?? false,
|
|
4139
|
+
turns: pacer.chunks,
|
|
3895
4140
|
exitCode: result.exitCode,
|
|
3896
4141
|
at: Date.now(),
|
|
3897
4142
|
});
|
|
@@ -3899,31 +4144,27 @@ export class FacetManager {
|
|
|
3899
4144
|
return result;
|
|
3900
4145
|
}
|
|
3901
4146
|
catch (err) {
|
|
3902
|
-
//
|
|
3903
|
-
//
|
|
3904
|
-
//
|
|
3905
|
-
//
|
|
3906
|
-
|
|
3907
|
-
|
|
3908
|
-
|
|
3909
|
-
|
|
4147
|
+
// The abort controller firing means this run ended BY a kill — the
|
|
4148
|
+
// shell's Ctrl+C or `kill <pid>`. The process was not a crash and the
|
|
4149
|
+
// abort text is not its stderr: mark it killed and hand back 130 (the
|
|
4150
|
+
// shell's signalled status) with nothing for the terminal to print.
|
|
4151
|
+
if (abortController.signal.aborted) {
|
|
4152
|
+
this.processes.kill(entry.pid);
|
|
4153
|
+
return { exitCode: 130, stdout: '', stderr: '' };
|
|
4154
|
+
}
|
|
4155
|
+
const exitCode = 1;
|
|
4156
|
+
const reason = `runtime worker error: ${errorMessage(err)}`;
|
|
3910
4157
|
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 { }
|
|
4158
|
+
this._w5RecordTermination(entry.pid, exitCode, 'runtime-worker', reason);
|
|
4159
|
+
try {
|
|
4160
|
+
this.hooks.onExternalExit?.(entry.pid, exitCode, reason);
|
|
3922
4161
|
}
|
|
4162
|
+
catch { }
|
|
3923
4163
|
return { exitCode, stdout: '', stderr: errorMessage(err) };
|
|
3924
4164
|
}
|
|
3925
4165
|
finally {
|
|
3926
|
-
|
|
4166
|
+
opts.signal?.removeEventListener('abort', onShellAbort);
|
|
4167
|
+
pacer.settle();
|
|
3927
4168
|
}
|
|
3928
4169
|
}
|
|
3929
4170
|
/**
|
|
@@ -3963,7 +4204,7 @@ export class FacetManager {
|
|
|
3963
4204
|
}
|
|
3964
4205
|
// ── One-shot dynamic Worker entrypoint ────────────────────────────────
|
|
3965
4206
|
async _execViaLoader(code, opts, entry, vfsState, signal, diagSink) {
|
|
3966
|
-
// Answered by
|
|
4207
|
+
// Answered by _buildProcessBundle while the raw cells were still in
|
|
3967
4208
|
// hand; re-deriving it here is what forced them to be retained.
|
|
3968
4209
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
3969
4210
|
const [sqliteModules, shims] = await Promise.all([
|
|
@@ -3983,13 +4224,6 @@ export class FacetManager {
|
|
|
3983
4224
|
stdin: opts.stdin || '',
|
|
3984
4225
|
captureOutput: !!opts.captureOutput,
|
|
3985
4226
|
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
4227
|
vfsCursor: vfsState.cursor,
|
|
3994
4228
|
...(diagSink ? { diag: true } : {}),
|
|
3995
4229
|
});
|
|
@@ -4003,16 +4237,21 @@ export class FacetManager {
|
|
|
4003
4237
|
// here rather than named by the enclosing frame, and the serialized
|
|
4004
4238
|
// forms it was built from are released unless the prefetch cache is
|
|
4005
4239
|
// keeping them. Otherwise the coordinator carries a second full copy
|
|
4006
|
-
// of the program for the whole
|
|
4007
|
-
//
|
|
4240
|
+
// of the program for the whole run the facet then performs,
|
|
4241
|
+
// which is the window the isolate was being reset in.
|
|
4008
4242
|
code: async () => {
|
|
4009
|
-
|
|
4243
|
+
// A one-shot has no disk reader at load, so the closure's wasm
|
|
4244
|
+
// images ride by value: read here, inside the scope that holds
|
|
4245
|
+
// the map, and compiled by the loader like the sqlite sidecar.
|
|
4246
|
+
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
4247
|
+
const wasmModules = this._wasmModulesByValue(entry, wasmImports);
|
|
4248
|
+
const generatedWorker = await generateEntrypointCode(code, vfsState, usesSqlite, shims, wasmImports);
|
|
4010
4249
|
if (diagSink) {
|
|
4011
4250
|
diagSink.moduleMapBytes = _encodedSourceBytes(generatedWorker.code);
|
|
4012
4251
|
for (const source of Object.values(generatedWorker.modules)) {
|
|
4013
4252
|
diagSink.moduleMapBytes += _encodedSourceBytes(source);
|
|
4014
4253
|
}
|
|
4015
|
-
for (const m of Object.values(sqliteModules)) {
|
|
4254
|
+
for (const m of [...Object.values(sqliteModules), ...Object.values(wasmModules)]) {
|
|
4016
4255
|
diagSink.moduleMapBytes += m.wasm.byteLength;
|
|
4017
4256
|
}
|
|
4018
4257
|
// Read before the release below, which is the last moment the
|
|
@@ -4032,7 +4271,7 @@ export class FacetManager {
|
|
|
4032
4271
|
compatibilityDate: CF_COMPAT_DATE,
|
|
4033
4272
|
compatibilityFlags: ['nodejs_compat', 'nodejs_compat_v2'],
|
|
4034
4273
|
mainModule: 'runner.js',
|
|
4035
|
-
modules: { 'runner.js': generatedWorker.code, ...generatedWorker.modules, ...sqliteModules },
|
|
4274
|
+
modules: { 'runner.js': generatedWorker.code, ...generatedWorker.modules, ...sqliteModules, ...wasmModules },
|
|
4036
4275
|
};
|
|
4037
4276
|
},
|
|
4038
4277
|
request: new Request('http://nimbus-runtime.local/run', {
|
|
@@ -4440,7 +4679,7 @@ export class FacetManager {
|
|
|
4440
4679
|
_assertLaunchStillOwned(pid) {
|
|
4441
4680
|
if (this.processes.get(pid)?.state === 'running')
|
|
4442
4681
|
return;
|
|
4443
|
-
throw new Error(`Nimbus:
|
|
4682
|
+
throw new Error(`Nimbus: the launch for pid ${pid} was cancelled while it was suspended`);
|
|
4444
4683
|
}
|
|
4445
4684
|
/**
|
|
4446
4685
|
* The one way this manager boots a resident process. Every resident process
|
|
@@ -4630,34 +4869,6 @@ export class FacetManager {
|
|
|
4630
4869
|
vfs.writeFile(path, restored);
|
|
4631
4870
|
}
|
|
4632
4871
|
}
|
|
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
4872
|
/**
|
|
4662
4873
|
* Re-drive a journalled launch after an instance reset. What the journal
|
|
4663
4874
|
* row carries is the recipe and nothing else: env and credentials are never
|
|
@@ -4669,9 +4880,15 @@ export class FacetManager {
|
|
|
4669
4880
|
switch (recipe.kind) {
|
|
4670
4881
|
case 'node': return this._spawnResident(recipe.code, recipe.opts, attempt);
|
|
4671
4882
|
case 'worker': {
|
|
4672
|
-
//
|
|
4673
|
-
//
|
|
4674
|
-
//
|
|
4883
|
+
// Which resolver a worker recipe re-drives through is decided by
|
|
4884
|
+
// `recipe.resident`, and it is not a flag: it is the interpreter
|
|
4885
|
+
// image (`{ runtime, argv }`) of a python/ruby socket server the
|
|
4886
|
+
// SESSION launched for itself. Only those recipes take the
|
|
4887
|
+
// image-store fallback, because the session persisted that image and
|
|
4888
|
+
// no embedder was ever asked about the launch. Every other worker
|
|
4889
|
+
// recipe — an embedder's `spawnWorker`, durable or not — re-drives
|
|
4890
|
+
// through the embedder's `resolveWorkerLaunch`, and falls back to the
|
|
4891
|
+
// image store only when no embedder hook is composed at all.
|
|
4675
4892
|
const resolve = recipe.resident
|
|
4676
4893
|
? this.hooks.resolveWorkerLaunchFallback
|
|
4677
4894
|
: this.hooks.resolveWorkerLaunch ?? this.hooks.resolveWorkerLaunchFallback;
|
|
@@ -4680,15 +4897,18 @@ export class FacetManager {
|
|
|
4680
4897
|
const resolved = await resolve(recipe);
|
|
4681
4898
|
if (resolved === null)
|
|
4682
4899
|
return undefined;
|
|
4683
|
-
const
|
|
4900
|
+
const mainModule = resolved.mainModule ?? recipe.mainModule ?? DEFAULT_WORKER_MAIN_MODULE;
|
|
4901
|
+
const { [mainModule]: workerCode, ...modules } = resolved.modules;
|
|
4684
4902
|
if (workerCode === undefined)
|
|
4685
|
-
throw new Error(
|
|
4903
|
+
throw new Error(`resolved worker launch carries no ${mainModule} main module`);
|
|
4686
4904
|
return this._spawnWorker(workerCode, record.command, recipe.cwd, {
|
|
4687
4905
|
port: recipe.resident ? record.port : recipe.port > 0 ? recipe.port : undefined, modules, compatibilityDate: recipe.compatibilityDate,
|
|
4688
4906
|
compatibilityFlags: recipe.compatibilityFlags, startArgs: resolved.startArgs ?? recipe.startArgs,
|
|
4689
4907
|
resident: recipe.resident, restart: record.restart,
|
|
4690
4908
|
env: resolved.env ?? undefined, globalOutbound: resolved.globalOutbound,
|
|
4691
4909
|
vfsWasmModules: resolved.vfsWasmModules,
|
|
4910
|
+
vfsTextModules: resolved.vfsTextModules,
|
|
4911
|
+
mainModule,
|
|
4692
4912
|
durable: { owner: residentOwner(record) ?? recipe.owner, image: recipe.image },
|
|
4693
4913
|
}, attempt);
|
|
4694
4914
|
}
|
|
@@ -4769,7 +4989,7 @@ export class FacetManager {
|
|
|
4769
4989
|
*/
|
|
4770
4990
|
async _runResidentLaunch(entry, code, command, opts, attempt) {
|
|
4771
4991
|
const cwd = opts.cwd || '/home/user';
|
|
4772
|
-
const pacer =
|
|
4992
|
+
const pacer = this._launchPacer(entry.pid);
|
|
4773
4993
|
// Journalled before the first byte of work. A launch that FAILS deletes
|
|
4774
4994
|
// its row on the way out — its process has already been exited and the
|
|
4775
4995
|
// user notified, so there is nothing left to owe. A launch that SETTLES
|
|
@@ -4881,17 +5101,9 @@ export class FacetManager {
|
|
|
4881
5101
|
}
|
|
4882
5102
|
}
|
|
4883
5103
|
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
5104
|
const diagOn = isExecDiagEnabled();
|
|
4890
5105
|
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 };
|
|
5106
|
+
const vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd, entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
|
|
4895
5107
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
4896
5108
|
// The launch-time overlay (`$PORT`, `$NIMBUS_APP` under a reservation)
|
|
4897
5109
|
// rides on top of the recipe's env and is never journalled: it is
|
|
@@ -4910,12 +5122,15 @@ export class FacetManager {
|
|
|
4910
5122
|
FORCE_COLOR: opts.env?.FORCE_COLOR || '1',
|
|
4911
5123
|
}
|
|
4912
5124
|
: spawnEnv;
|
|
4913
|
-
|
|
5125
|
+
// Answered by _buildProcessBundle while the raw cells were still in hand.
|
|
5126
|
+
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
4914
5127
|
const [sqliteModules, shims] = await Promise.all([
|
|
4915
5128
|
this.sqliteModuleEntry(usesSqlite),
|
|
4916
5129
|
fetchNodeShimsCode(this.env),
|
|
4917
5130
|
]);
|
|
4918
|
-
|
|
5131
|
+
// Each image is read by path when the facet loads, never by value here.
|
|
5132
|
+
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
5133
|
+
let generatedWorker = await generateLongRunningNodeCode(code, vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite, shims, pacer);
|
|
4919
5134
|
// Sized here, while the map is still in hand. Reading these after the load
|
|
4920
5135
|
// would itself be what keeps the map alive, and the whole point of the
|
|
4921
5136
|
// scoping below is that nothing does.
|
|
@@ -4927,21 +5142,31 @@ export class FacetManager {
|
|
|
4927
5142
|
}
|
|
4928
5143
|
moduleMapBytes = _encodedSourceBytes(generatedWorker.code) + bundleBytes;
|
|
4929
5144
|
}
|
|
4930
|
-
const manifestBytes = diagOn ? _encodedSourceBytes(
|
|
4931
|
-
const metadataBytes = diagOn ? _encodedSourceBytes(
|
|
5145
|
+
const manifestBytes = diagOn ? _encodedSourceBytes(vfsState.serializedManifest ?? '') : 0;
|
|
5146
|
+
const metadataBytes = diagOn ? _encodedSourceBytes(vfsState.serializedMetadata ?? '') : 0;
|
|
4932
5147
|
const cacheHit = vfsState.cacheHit ?? false;
|
|
4933
5148
|
const vfsCursor = vfsState.cursor;
|
|
4934
|
-
|
|
5149
|
+
// The map is generated; the state's only remaining job is its cursor.
|
|
5150
|
+
// Released here, before the boot — releasing afterwards keeps the copy
|
|
5151
|
+
// alive for exactly the window that was resetting the isolate. A state
|
|
5152
|
+
// the cache retained is the cache's to keep, and is served to the next
|
|
5153
|
+
// launch of the same entry.
|
|
5154
|
+
if (!vfsState.cacheRetained)
|
|
5155
|
+
releaseGeneratedSources(vfsState);
|
|
4935
5156
|
let handle;
|
|
4936
5157
|
let resourcesTracked = false;
|
|
4937
5158
|
try {
|
|
4938
|
-
|
|
5159
|
+
// Assembled as one record of the SAME string objects the generator
|
|
5160
|
+
// produced — no copy — and then handed over: this frame drops its own
|
|
5161
|
+
// reference before materialize runs, and the sequence below drops each
|
|
5162
|
+
// source as the store takes it, so one image's text is resident rather
|
|
5163
|
+
// than every image's twice over.
|
|
5164
|
+
const sources = {
|
|
4939
5165
|
'worker.js': generatedWorker.code,
|
|
4940
5166
|
...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.
|
|
5167
|
+
};
|
|
4944
5168
|
generatedWorker = undefined;
|
|
5169
|
+
const vfsTextModules = await this.imageStore.materialize(entry.pid, drainSources(sources), pacer);
|
|
4945
5170
|
// Last gate before the facet exists. A launch now spans many turns, so
|
|
4946
5171
|
// a kill can land anywhere inside it; booting a process the table has
|
|
4947
5172
|
// already exited would leave a facet nothing owns, running against a
|
|
@@ -4992,6 +5217,7 @@ export class FacetManager {
|
|
|
4992
5217
|
rpcWrites: 0,
|
|
4993
5218
|
fsRpcReads: 0,
|
|
4994
5219
|
cacheHit,
|
|
5220
|
+
turns: pacer.chunks,
|
|
4995
5221
|
exitCode: 0,
|
|
4996
5222
|
at: Date.now(),
|
|
4997
5223
|
});
|
|
@@ -5035,20 +5261,43 @@ export class FacetManager {
|
|
|
5035
5261
|
}
|
|
5036
5262
|
}
|
|
5037
5263
|
catch (e) {
|
|
5264
|
+
// A program classified as long-running that ended on its own during
|
|
5265
|
+
// its boot — `json-server --version` prints and exits 0 — reported its
|
|
5266
|
+
// exit through the supervisor, which released the facet and rejected
|
|
5267
|
+
// the boot handshake with 'resident process released'. That is a
|
|
5268
|
+
// completed run, not a failed launch: the process table already holds
|
|
5269
|
+
// its real exit code, and the caller reports that code.
|
|
5038
5270
|
this.portRegistry.unregisterByPid(entry.pid);
|
|
5039
5271
|
if (resourcesTracked)
|
|
5040
5272
|
this.releaseProcessRpcResources(entry.pid);
|
|
5041
|
-
|
|
5042
|
-
|
|
5273
|
+
if (this.processExitCode(entry.pid) !== null)
|
|
5274
|
+
return;
|
|
5275
|
+
handle?.kill();
|
|
5043
5276
|
this._failLaunch(entry.pid, 'long-running node boot failed: ' + errorMessage(e));
|
|
5044
5277
|
throw e;
|
|
5045
5278
|
}
|
|
5046
5279
|
}
|
|
5047
5280
|
/**
|
|
5048
|
-
*
|
|
5281
|
+
* The exit code of a launched process that has already ended, or null
|
|
5282
|
+
* while it runs. What a caller that started a resident reads to tell a
|
|
5283
|
+
* server that is up from a program that finished during its boot.
|
|
5284
|
+
*/
|
|
5285
|
+
processExitCode(pid) {
|
|
5286
|
+
const entry = this.processes.get(pid);
|
|
5287
|
+
if (entry === undefined || entry.state === 'running')
|
|
5288
|
+
return null;
|
|
5289
|
+
return entry.exitCode ?? 0;
|
|
5290
|
+
}
|
|
5291
|
+
/**
|
|
5292
|
+
* Spawn a long-running dynamic Worker, boot it, and return its boot payload
|
|
5293
|
+
* beside the pid and the process's own inbound facet.
|
|
5049
5294
|
*
|
|
5050
5295
|
* The shared primitive for any runtime that serves over
|
|
5051
|
-
* handleHttpRequest(Request) — the python and ruby socket servers today
|
|
5296
|
+
* handleHttpRequest(Request) — the python and ruby socket servers today —
|
|
5297
|
+
* and for an embedder's own Worker-class program: `workerCode` boots as
|
|
5298
|
+
* `opts.mainModule` (default `worker.js`), `opts.modules` ride inline,
|
|
5299
|
+
* `opts.vfsTextModules` and `opts.vfsWasmModules` are read by path when the
|
|
5300
|
+
* facet loads.
|
|
5052
5301
|
*
|
|
5053
5302
|
* The interpreter image it carries is the memory that should not sit in the
|
|
5054
5303
|
* session's own isolate — ruby's interpreter+stdlib alone is 34.3 MiB — and
|
|
@@ -5056,6 +5305,11 @@ export class FacetManager {
|
|
|
5056
5305
|
* no readiness coupling back into the session: the runner answers
|
|
5057
5306
|
* startProcess with its boot payload and the caller waits on that one
|
|
5058
5307
|
* promise, so nothing polls the port to decide the process is up.
|
|
5308
|
+
*
|
|
5309
|
+
* The returned `facet` is bound to the resident handle's route target — the
|
|
5310
|
+
* same target a registered port routes to — so a port-less process can be
|
|
5311
|
+
* invoked directly. It has no release: `kill(pid)` is the one lifecycle
|
|
5312
|
+
* owner, and the facet is dead once the pid is.
|
|
5059
5313
|
*/
|
|
5060
5314
|
async spawnWorker(workerCode, command, cwd, opts = {}) {
|
|
5061
5315
|
return this._spawnWorker(workerCode, command, cwd, opts, 0);
|
|
@@ -5066,7 +5320,9 @@ export class FacetManager {
|
|
|
5066
5320
|
opts = { ...opts, durable: { owner: await deriveResidentOwner(cwd, opts.resident.argv) } };
|
|
5067
5321
|
}
|
|
5068
5322
|
this.processes.reap();
|
|
5069
|
-
|
|
5323
|
+
// The table entry carries the same argv the identity is derived from, so
|
|
5324
|
+
// a runtime resident reads the same way through either path.
|
|
5325
|
+
const entry = this.processes.spawn(command, opts.resident?.argv ?? [], cwd);
|
|
5070
5326
|
// Stamp the process-table entry so /api/processes exposes this as a
|
|
5071
5327
|
// long-running process.
|
|
5072
5328
|
this.processes.setLongRunning(entry.pid);
|
|
@@ -5078,6 +5334,10 @@ export class FacetManager {
|
|
|
5078
5334
|
catch { }
|
|
5079
5335
|
const compatibilityDate = opts.compatibilityDate ?? CF_COMPAT_DATE;
|
|
5080
5336
|
const compatibilityFlags = opts.compatibilityFlags || ['nodejs_compat'];
|
|
5337
|
+
const mainModule = opts.mainModule ?? DEFAULT_WORKER_MAIN_MODULE;
|
|
5338
|
+
if (opts.modules && Object.hasOwn(opts.modules, mainModule)) {
|
|
5339
|
+
throw new Error(`Nimbus: spawnWorker main module '${mainModule}' is also an inline module; workerCode is the main module`);
|
|
5340
|
+
}
|
|
5081
5341
|
let handle;
|
|
5082
5342
|
let resourcesTracked = false;
|
|
5083
5343
|
let record;
|
|
@@ -5115,6 +5375,8 @@ export class FacetManager {
|
|
|
5115
5375
|
modules: opts.modules ?? {},
|
|
5116
5376
|
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
5117
5377
|
vfsWasmModules: opts.vfsWasmModules,
|
|
5378
|
+
vfsTextModules: opts.vfsTextModules,
|
|
5379
|
+
...(opts.mainModule !== undefined ? { mainModule: opts.mainModule } : {}),
|
|
5118
5380
|
startArgs: opts.startArgs,
|
|
5119
5381
|
});
|
|
5120
5382
|
await this.ctx.storage.transaction(async (txn) => {
|
|
@@ -5141,6 +5403,7 @@ export class FacetManager {
|
|
|
5141
5403
|
compatibilityFlags,
|
|
5142
5404
|
...(opts.durable.image && !opts.resident ? { startArgs: opts.startArgs } : {}),
|
|
5143
5405
|
...(opts.resident ? { resident: opts.resident } : {}),
|
|
5406
|
+
...(opts.mainModule !== undefined ? { mainModule: opts.mainModule } : {}),
|
|
5144
5407
|
},
|
|
5145
5408
|
// The row-level fields `ensureDurableAppOnPort` reads — same values
|
|
5146
5409
|
// the recipe carries, stamped so port/owner lookup never depends on
|
|
@@ -5185,9 +5448,10 @@ export class FacetManager {
|
|
|
5185
5448
|
code: {
|
|
5186
5449
|
compatibilityDate,
|
|
5187
5450
|
compatibilityFlags,
|
|
5188
|
-
mainModule
|
|
5189
|
-
modules: {
|
|
5451
|
+
mainModule,
|
|
5452
|
+
modules: { ...(opts.modules || {}), [mainModule]: workerCode },
|
|
5190
5453
|
vfsWasmModules: opts.vfsWasmModules,
|
|
5454
|
+
...(opts.vfsTextModules !== undefined ? { vfsTextModules: opts.vfsTextModules } : {}),
|
|
5191
5455
|
...(opts.env !== undefined || launchEnv !== undefined ? { env: { ...opts.env, ...launchEnv } } : {}),
|
|
5192
5456
|
...(opts.globalOutbound !== undefined ? { globalOutbound: opts.globalOutbound } : {}),
|
|
5193
5457
|
},
|
|
@@ -5218,7 +5482,20 @@ export class FacetManager {
|
|
|
5218
5482
|
}
|
|
5219
5483
|
await this._registerResidentPort(entry.pid, opts.port);
|
|
5220
5484
|
}
|
|
5221
|
-
|
|
5485
|
+
const target = handle.routeTarget;
|
|
5486
|
+
return {
|
|
5487
|
+
pid: entry.pid,
|
|
5488
|
+
boot,
|
|
5489
|
+
facet: {
|
|
5490
|
+
fetch: (request) => target.handleHttpRequest(request),
|
|
5491
|
+
connect: async (request) => {
|
|
5492
|
+
if (typeof target.handleWebSocketRequest !== 'function') {
|
|
5493
|
+
throw new Error(`Nimbus: worker pid ${entry.pid} accepts no WebSocket upgrades`);
|
|
5494
|
+
}
|
|
5495
|
+
return target.handleWebSocketRequest(request);
|
|
5496
|
+
},
|
|
5497
|
+
},
|
|
5498
|
+
};
|
|
5222
5499
|
}
|
|
5223
5500
|
catch (e) {
|
|
5224
5501
|
this.portRegistry.unregisterByPid(entry.pid);
|
|
@@ -5294,7 +5571,14 @@ export class FacetManager {
|
|
|
5294
5571
|
this.hooks.notify?.(`\x1b[2m[nimbus: "${row.command}" listened on ${port} but its reservation owns `
|
|
5295
5572
|
+ `${row.portMismatch.reserved} ($PORT) — the app's URL will not reach it]\x1b[0m\r\n`);
|
|
5296
5573
|
}
|
|
5297
|
-
|
|
5574
|
+
// A pid nothing journalled — the in-process dev servers, a builtin's
|
|
5575
|
+
// adopted wrapper pid — still has an identity: the process table's. It
|
|
5576
|
+
// re-adopts a reservation it owns exactly as a resident does; the one
|
|
5577
|
+
// thing it cannot do is adopt an EXPLICIT owner, because there is no row
|
|
5578
|
+
// to stamp the adoption on, so it registers ephemeral on that port.
|
|
5579
|
+
const owner = row === undefined
|
|
5580
|
+
? (await this.residentIdentity(pid))?.owner
|
|
5581
|
+
: residentOwner(row);
|
|
5298
5582
|
if (claimedBy !== undefined && row !== undefined && this.residentClaims.get(pid) !== claimedBy) {
|
|
5299
5583
|
const previousOwner = this.residentClaims.get(pid);
|
|
5300
5584
|
await this._releaseResidentClaim(pid);
|
|
@@ -5338,15 +5622,33 @@ export class FacetManager {
|
|
|
5338
5622
|
await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port);
|
|
5339
5623
|
this.portRegistry.register(port, pid);
|
|
5340
5624
|
}
|
|
5341
|
-
/**
|
|
5625
|
+
/**
|
|
5626
|
+
* Who a pid is. One resolver, in precedence order: the ephemeral-duplicate
|
|
5627
|
+
* mark, the journal row (the owner a launch this manager made was stamped
|
|
5628
|
+
* with — derived from the launch's own cwd+argv, or adopted from an
|
|
5629
|
+
* explicit reservation), and finally the process table. The table knows
|
|
5630
|
+
* cwd and argv for every pid, so a serving process nothing journalled — the
|
|
5631
|
+
* in-process Vite dev server, real-vite, a staged artifact, any wrapper pid
|
|
5632
|
+
* a builtin adopted — has the same derived identity shape as a resident
|
|
5633
|
+
* and answers to the same app verbs. It just cannot be re-driven after a
|
|
5634
|
+
* reset: only a journal row carries a recipe. Null only for a pid that is
|
|
5635
|
+
* neither journalled nor running.
|
|
5636
|
+
*/
|
|
5342
5637
|
async residentIdentity(pid) {
|
|
5343
5638
|
const duplicated = this.ephemeralPids.get(pid);
|
|
5344
5639
|
if (duplicated !== undefined)
|
|
5345
5640
|
return { owner: duplicated, ephemeral: true, port: undefined };
|
|
5346
5641
|
const row = (await this.launchJournal.rows()).get(`${FENCED_WORK_KEY_PREFIX}${pid}`);
|
|
5347
|
-
if (row
|
|
5642
|
+
if (row !== undefined)
|
|
5643
|
+
return { owner: residentOwner(row), ephemeral: false, port: row.port };
|
|
5644
|
+
const entry = this.processes.get(pid);
|
|
5645
|
+
if (entry === undefined || entry.state !== 'running')
|
|
5348
5646
|
return null;
|
|
5349
|
-
return {
|
|
5647
|
+
return {
|
|
5648
|
+
owner: await deriveResidentOwner(entry.cwd, entry.argv),
|
|
5649
|
+
ephemeral: false,
|
|
5650
|
+
port: this.portRegistry.getAll().find((live) => live.pid === pid)?.port,
|
|
5651
|
+
};
|
|
5350
5652
|
}
|
|
5351
5653
|
/**
|
|
5352
5654
|
* Every stamped identity — the reservations, and the journal rows that
|
|
@@ -5398,6 +5700,28 @@ export class FacetManager {
|
|
|
5398
5700
|
}
|
|
5399
5701
|
apps.set(owner, app);
|
|
5400
5702
|
}
|
|
5703
|
+
// Every process serving a port is an application whether or not a
|
|
5704
|
+
// journal row backs it — the identity comes from the same resolver the
|
|
5705
|
+
// verbs use, so what `list` shows is what `expose` will bind. A row-backed
|
|
5706
|
+
// app already carries its pid from above; this only adds the servers
|
|
5707
|
+
// nothing journalled (the dev servers, adopted wrapper pids).
|
|
5708
|
+
for (const live of this.portRegistry.getAll()) {
|
|
5709
|
+
const identity = await this.residentIdentity(live.pid);
|
|
5710
|
+
if (identity === null || identity.ephemeral || identity.owner === undefined)
|
|
5711
|
+
continue;
|
|
5712
|
+
const app = apps.get(identity.owner) ?? {
|
|
5713
|
+
owner: identity.owner, name: null, port: null, pid: null, status: 'stopped',
|
|
5714
|
+
visibility: 'scoped', capability: null, restart: 'never', diagnostic: null,
|
|
5715
|
+
};
|
|
5716
|
+
if (app.pid !== null)
|
|
5717
|
+
continue;
|
|
5718
|
+
app.pid = live.pid;
|
|
5719
|
+
if (app.port === null)
|
|
5720
|
+
app.port = live.port;
|
|
5721
|
+
if (app.status === 'stopped')
|
|
5722
|
+
app.status = 'running';
|
|
5723
|
+
apps.set(identity.owner, app);
|
|
5724
|
+
}
|
|
5401
5725
|
return [...apps.values()];
|
|
5402
5726
|
}
|
|
5403
5727
|
async registerPort(pid, port) {
|