@nimbus-sh/worker 0.3.0 → 0.5.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 +46 -43
- package/dist/_shared/preview-host.d.ts +14 -1
- package/dist/_shared/preview-host.d.ts.map +1 -1
- package/dist/_shared/preview-host.js +28 -3
- package/dist/_shared/session-router.d.ts +43 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +59 -12
- package/dist/esbuild-wasm-bundle.generated.d.ts +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +1 -1
- package/dist/facets/cirrus-real.js +1 -1
- package/dist/facets/durable-images.d.ts +50 -0
- package/dist/facets/durable-images.d.ts.map +1 -0
- package/dist/facets/durable-images.js +75 -0
- package/dist/facets/durable-slots.d.ts +45 -0
- package/dist/facets/durable-slots.d.ts.map +1 -0
- package/dist/facets/durable-slots.js +94 -0
- package/dist/facets/esbuild-transform.d.ts +10 -0
- package/dist/facets/esbuild-transform.d.ts.map +1 -0
- package/dist/facets/esbuild-transform.js +36 -0
- package/dist/facets/manager.d.ts +167 -12
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +575 -113
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +3 -4
- package/dist/facets/real-vite-hmr.js +1 -1
- package/dist/facets/vite-dev-server.d.ts +4 -4
- package/dist/facets/vite-dev-server.js +5 -5
- package/dist/git/commands.d.ts +22 -0
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +431 -424
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +6 -5
- package/dist/git-bundle.generated.d.ts +2 -2
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +3 -3
- package/dist/index.d.ts +2 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +11 -2
- package/dist/loaders/child-process/spawn-facet.js +1 -1
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -1
- package/dist/loaders/child-process/spawn-pool.js +2 -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 -3
- package/dist/loaders/npm-resolve-preamble.js +3 -3
- package/dist/loaders/pre-bundle-preamble.d.ts +5 -5
- package/dist/loaders/pre-bundle-preamble.js +6 -6
- package/dist/node-shims-artifact.generated.js +3 -3
- package/dist/npm/install-batch-facet.d.ts +1 -1
- package/dist/npm/install-batch-facet.js +1 -1
- package/dist/npm/installer.d.ts +6 -6
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +28 -27
- package/dist/npm/pre-bundle-facet.d.ts +2 -2
- package/dist/npm/pre-bundle-facet.js +5 -5
- package/dist/npm/r2-cache.d.ts +1 -1
- package/dist/npm/resolve-facet.d.ts +1 -1
- package/dist/npm/resolve-facet.js +1 -1
- package/dist/npm/resolve-one-facet.d.ts +5 -5
- package/dist/npm/resolve-one-facet.js +5 -5
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +61 -4
- package/dist/router/public-directory-do.d.ts +17 -0
- package/dist/router/public-directory-do.d.ts.map +1 -0
- package/dist/router/public-directory-do.js +16 -0
- package/dist/router/public-directory.d.ts +75 -0
- package/dist/router/public-directory.d.ts.map +1 -0
- package/dist/router/public-directory.js +115 -0
- package/dist/router/remote-api.d.ts +9 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +20 -3
- package/dist/runtime/bun-repl.d.ts +1 -1
- package/dist/runtime/bun-repl.js +3 -3
- package/dist/runtime/esbuild-wasm-bytes.js +1 -1
- package/dist/runtime/facet-loader-host.d.ts +2 -2
- package/dist/runtime/facet-loader-host.js +3 -3
- package/dist/runtime/node-repl.js +2 -2
- package/dist/runtime/node-shims-artifact.js +1 -1
- package/dist/runtime/node-shims.d.ts +4 -1
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +326 -7
- package/dist/runtime/opencode-artifact.js +1 -1
- package/dist/runtime/opentui-wasm-bytes.js +1 -1
- package/dist/runtime/python-repl.js +3 -3
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/sqlite-wasm-bytes.js +1 -1
- package/dist/session/agent.d.ts +11 -0
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/diag.js +1 -1
- package/dist/session/helpers.js +1 -1
- package/dist/session/hibernation.d.ts +8 -8
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +15 -15
- package/dist/session/init-phases.d.ts +19 -26
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +20 -43
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +24 -19
- package/dist/session/keys.d.ts +8 -0
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +12 -3
- package/dist/session/nimbus-session.d.ts +33 -19
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +78 -37
- package/dist/session/port-capability.d.ts +95 -8
- package/dist/session/port-capability.d.ts.map +1 -1
- package/dist/session/port-capability.js +165 -8
- package/dist/session/programmatic.d.ts +50 -4
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +112 -15
- package/dist/session/routes.d.ts +17 -0
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +111 -43
- package/dist/session/rpc.d.ts +7 -7
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +34 -23
- package/dist/session/shell-socket.d.ts +76 -0
- package/dist/session/shell-socket.d.ts.map +1 -0
- package/dist/session/shell-socket.js +172 -0
- package/dist/session/start-real-vite.js +2 -2
- package/dist/session/supervisor-op.d.ts +5 -0
- package/dist/session/supervisor-op.d.ts.map +1 -0
- package/dist/session/supervisor-op.js +74 -0
- package/dist/session/supervisor-rpc.d.ts +6 -10
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +98 -79
- package/dist/session/ws.d.ts +1 -3
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +23 -7
- package/package.json +4 -3
- package/public/_assets/runtime/{node-shims-fea906843f6d594e.js → node-shims-9e1380d9ee430fcc.js} +320 -5
- package/public/s/index.html +101 -5
- package/scripts/bundle-esbuild-wasm.mjs +2 -2
- package/scripts/bundle-facet-workers.mjs +5 -7
- package/scripts/bundle-sqlite-wasm.mjs +1 -1
package/dist/facets/manager.js
CHANGED
|
@@ -22,28 +22,35 @@ 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
24
|
import { vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
|
|
25
|
-
import { clearPortCapability } from '../session/port-capability.js';
|
|
25
|
+
import { clearPortCapability, readPortReservation, releasePortReservation, restoreReservedPortCapability } from '../session/port-capability.js';
|
|
26
|
+
import { PORT_CAPABILITY_KEY_PREFIX } from '../session/keys.js';
|
|
27
|
+
import { unbindPublicPortCapability } from '../router/public-directory.js';
|
|
26
28
|
import { prefetchForRequire } from '@nimbus-sh/core/runtime/require-resolver.js';
|
|
27
29
|
import { hasTopLevelModuleSyntax } from '@nimbus-sh/core/runtime/javascript-ast.js';
|
|
28
30
|
import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
|
|
29
|
-
import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/
|
|
30
|
-
import { classifyError } from '@nimbus-sh/
|
|
31
|
-
import {
|
|
32
|
-
import {
|
|
33
|
-
import {
|
|
31
|
+
import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
32
|
+
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
33
|
+
import { TurnBudget, PacedWork, turnChunkMaxBytes, withResolvers } from '@nimbus-sh/fabric/turn-budget.js';
|
|
34
|
+
import { onColdStart } from '@nimbus-sh/fabric/generation.js';
|
|
35
|
+
import { FencedWork, FENCED_WORK_KEY_PREFIX, } from '@nimbus-sh/fabric/fenced-work.js';
|
|
36
|
+
import { EsbuildService, rewriteBundledEsmToCjs, } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
34
37
|
import { isExecDiagEnabled, recordExecTelemetry } from './exec-telemetry.js';
|
|
35
|
-
import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/
|
|
38
|
+
import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
36
39
|
import { sqliteWasmModuleEntry } from './opencode-staging.js';
|
|
37
|
-
import {
|
|
40
|
+
import { ImageStore, } from '@nimbus-sh/fabric/image-store.js';
|
|
38
41
|
import { ProcessFabric, } from '@nimbus-sh/fabric/process-fabric.js';
|
|
39
|
-
import { createLoadedWorkerEntrypoint, getNimbusCtxExports, } from '@nimbus-sh/fabric/workerd-facet-host.js';
|
|
42
|
+
import { createLoadedWorkerEntrypoint, getNimbusCtxExports, deleteFacetStorage, } from '@nimbus-sh/fabric/workerd-facet-host.js';
|
|
43
|
+
import { acquireDurableFacetSlot, freeDurableFacetSlot, } from './durable-slots.js';
|
|
44
|
+
import { persistDurableWorkerImage, } from './durable-images.js';
|
|
40
45
|
import { SQLITE_WASM_MODULE_NAME, } from '../runtime/opencode-facet-runner.js';
|
|
41
46
|
import { parsePortFromArgv, resolveLongRunningPort } from '@nimbus-sh/core/runtime/long-running-handle.js';
|
|
42
47
|
import { DEFAULT_FACET_BUNDLE_PROFILE, } from '@nimbus-sh/core/runtime/bundle-profile.js';
|
|
43
|
-
import { BUNDLE_BUILD_DEADLINE_MS, CF_COMPAT_DATE, FACET_TIMEOUT_MS, VFS_BUNDLE_MAX_FILES, VFS_BUNDLE_MAX_BYTES, CWD_SNAPSHOT_MAX_FILE_BYTES, BUNDLE_MAX_ENCODED_BYTES,
|
|
48
|
+
import { BUNDLE_BUILD_DEADLINE_MS, CF_COMPAT_DATE, FACET_TIMEOUT_MS, 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';
|
|
49
|
+
import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
|
|
44
50
|
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
45
|
-
import { acquireSupervisorAllocation } from '@nimbus-sh/
|
|
46
|
-
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/
|
|
51
|
+
import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
|
|
52
|
+
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
|
|
53
|
+
const ISOLATED_ESM_TRANSFORM_MIN_BYTES = 512 * 1024;
|
|
47
54
|
/**
|
|
48
55
|
* Detect & restore a Uint8Array that's been JSON-mangled to a
|
|
49
56
|
* {"0":n,"1":n,...} object during the result-envelope round-trip.
|
|
@@ -118,6 +125,7 @@ function parseFacetManagerEnv(env) {
|
|
|
118
125
|
LOADER: loader,
|
|
119
126
|
ASSETS: assets,
|
|
120
127
|
NIMBUS_LAUNCH_CHUNK_BYTES: typeof chunkBytes === 'string' ? chunkBytes : undefined,
|
|
128
|
+
rawEnv: env,
|
|
121
129
|
};
|
|
122
130
|
}
|
|
123
131
|
/**
|
|
@@ -406,11 +414,11 @@ function __mkCompiledFn(code) {
|
|
|
406
414
|
code = __nl >= 0 ? code.slice(__nl + 1) : "";
|
|
407
415
|
}
|
|
408
416
|
function renameIfDeclared(name) {
|
|
409
|
-
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "
|
|
417
|
+
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
|
|
410
418
|
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
411
419
|
}
|
|
412
420
|
const baseParams = [
|
|
413
|
-
"exports", "require", "module",
|
|
421
|
+
"exports", renameIfDeclared("require"), "module",
|
|
414
422
|
renameIfDeclared("__filename"),
|
|
415
423
|
renameIfDeclared("__dirname"),
|
|
416
424
|
];
|
|
@@ -431,23 +439,10 @@ const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
|
431
439
|
const __MODULE_VFS_METADATA = ${safeMetadata};
|
|
432
440
|
const __compiledModules = new Map();
|
|
433
441
|
const __compileFailures = new Map();
|
|
434
|
-
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
// .json is data; skip it (it loads via JSON.parse, not as a function).
|
|
439
|
-
const __base = __p.slice(__p.lastIndexOf("/") + 1);
|
|
440
|
-
const __dot = __base.lastIndexOf(".");
|
|
441
|
-
const __ext = __dot > 0 ? __base.slice(__dot) : "";
|
|
442
|
-
if (__ext === ".js" || __ext === ".mjs" || __ext === ".cjs" || __ext === "") {
|
|
443
|
-
if (typeof __c !== "string") continue;
|
|
444
|
-
try {
|
|
445
|
-
__compiledModules.set(__p, __mkCompiledFn(__c));
|
|
446
|
-
} catch (__e) {
|
|
447
|
-
__compileFailures.set(__p, __e && __e.message ? __e.message : String(__e));
|
|
448
|
-
}
|
|
449
|
-
}
|
|
450
|
-
}
|
|
442
|
+
// Precompile JS modules AND extensionless CJS entries (bin scripts and
|
|
443
|
+
// shims). workerd forbids new Function at request time, so anything not
|
|
444
|
+
// precompiled here surfaces the misleading "file was not pre-bundled".
|
|
445
|
+
${BUNDLE_PRECOMPILE_LOOP}
|
|
451
446
|
|
|
452
447
|
class __ProcessExit extends Error {
|
|
453
448
|
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
@@ -685,11 +680,11 @@ const __NimbusHostResponse = globalThis.Response;
|
|
|
685
680
|
|
|
686
681
|
function __mkCompiledFn(code) {
|
|
687
682
|
function renameIfDeclared(name) {
|
|
688
|
-
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "
|
|
683
|
+
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
|
|
689
684
|
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
690
685
|
}
|
|
691
686
|
const baseParams = [
|
|
692
|
-
"exports", "require", "module",
|
|
687
|
+
"exports", renameIfDeclared("require"), "module",
|
|
693
688
|
renameIfDeclared("__filename"),
|
|
694
689
|
renameIfDeclared("__dirname"),
|
|
695
690
|
];
|
|
@@ -717,15 +712,7 @@ const __compileFailures = new Map();
|
|
|
717
712
|
// \`new Function\` throws "Code generation from strings disallowed" in the DO
|
|
718
713
|
// constructor and at request time — so the require closure must be compiled
|
|
719
714
|
// here, off the module map. That is why code cannot come from the store.
|
|
720
|
-
|
|
721
|
-
if (__p.endsWith(".js") || __p.endsWith(".mjs") || __p.endsWith(".cjs")) {
|
|
722
|
-
try {
|
|
723
|
-
__compiledModules.set(__p, __mkCompiledFn(__c));
|
|
724
|
-
} catch (__e) {
|
|
725
|
-
__compileFailures.set(__p, __e && __e.message ? __e.message : String(__e));
|
|
726
|
-
}
|
|
727
|
-
}
|
|
728
|
-
}
|
|
715
|
+
${BUNDLE_PRECOMPILE_LOOP}
|
|
729
716
|
|
|
730
717
|
${FACET_RESIDENT_STORE_SOURCE}
|
|
731
718
|
|
|
@@ -1340,8 +1327,10 @@ export function encodedBundleSize(bundle, manifest) {
|
|
|
1340
1327
|
function describeBundleCells(cells, limit = 8) {
|
|
1341
1328
|
const shown = [...cells].sort((a, b) => b[1] - a[1]).slice(0, limit);
|
|
1342
1329
|
const rest = cells.length - shown.length;
|
|
1343
|
-
return shown.map(([path, bytes]) =>
|
|
1344
|
-
|
|
1330
|
+
return shown.map(([path, bytes]) => {
|
|
1331
|
+
const source = compiledCellPath(path);
|
|
1332
|
+
return source === null ? `${path} (${bytes} bytes)` : `${source} [compiled] (${bytes} bytes)`;
|
|
1333
|
+
}).join(', ') + (rest > 0 ? `, +${rest} more` : '');
|
|
1345
1334
|
}
|
|
1346
1335
|
/**
|
|
1347
1336
|
* hardening-r5: emit a JS expression that revives binary cells from base64
|
|
@@ -1727,7 +1716,7 @@ function buildManifest(vfs, cwd, scriptPath) {
|
|
|
1727
1716
|
return manifest;
|
|
1728
1717
|
}
|
|
1729
1718
|
function buildVfsMetadata(vfs, manifest, bundle) {
|
|
1730
|
-
const paths = new Set(Object.keys(bundle));
|
|
1719
|
+
const paths = new Set(Object.keys(bundle).filter((path) => compiledCellPath(path) === null));
|
|
1731
1720
|
for (const [directory, children] of Object.entries(manifest)) {
|
|
1732
1721
|
paths.add(directory);
|
|
1733
1722
|
for (const child of children) {
|
|
@@ -1801,6 +1790,16 @@ export function greedyAddMainEntries(vfs, cwd, bundle, budgetState) {
|
|
|
1801
1790
|
try {
|
|
1802
1791
|
if (!vfs.exists(stripped) || vfs.isDirectory(stripped))
|
|
1803
1792
|
return false;
|
|
1793
|
+
// This is a guess at what a program might require, and the same
|
|
1794
|
+
// per-file ceiling the entry-package walk applies bounds it: a
|
|
1795
|
+
// multi-MiB main entry is an alternative bundle (typescript's 8.69 MiB
|
|
1796
|
+
// `lib/typescript.js` beside the `lib/tsc.js` that actually runs), and
|
|
1797
|
+
// one guess must not spend a third of the budget — and a third of the
|
|
1798
|
+
// supervisor's headroom — on every invocation that never reads it.
|
|
1799
|
+
// Anything the program really requires arrives through the closure,
|
|
1800
|
+
// which is uncapped.
|
|
1801
|
+
if (vfs.lstat(stripped).size > BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)
|
|
1802
|
+
return false;
|
|
1804
1803
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
1805
1804
|
const content = _readBundleCell(vfs, stripped);
|
|
1806
1805
|
const cellLen = _bundleCellLength(content);
|
|
@@ -2528,18 +2527,22 @@ const RUNTIME_PACKAGE_EXCLUDED_FILE_SUFFIXES = [
|
|
|
2528
2527
|
'.webm',
|
|
2529
2528
|
];
|
|
2530
2529
|
/**
|
|
2531
|
-
* Per-file ceiling for the speculative
|
|
2530
|
+
* Per-file ceiling for the speculative passes over installed packages: the
|
|
2531
|
+
* entry-package walk (`addBinTargetSiblings`) and the main-entry oversample
|
|
2532
|
+
* (`greedyAddMainEntries`).
|
|
2532
2533
|
*
|
|
2533
2534
|
* Everything the entry package needs in order to *run* arrives through the
|
|
2534
|
-
* require closure, which is uncapped and never evicted.
|
|
2535
|
-
* to catch
|
|
2536
|
-
*
|
|
2537
|
-
*
|
|
2538
|
-
* than data.
|
|
2535
|
+
* require closure, which is uncapped and never evicted. These passes exist
|
|
2536
|
+
* only to catch what the static walker cannot see — data files, and modules
|
|
2537
|
+
* reached by a computed require — and those are small. Multi-MiB cells in a
|
|
2538
|
+
* package tree are overwhelmingly alternative bundles — typescript ships an
|
|
2539
|
+
* 8.69 MiB `lib/typescript.js` that `tsc` never reads — rather than data.
|
|
2539
2540
|
*
|
|
2540
2541
|
* So one speculative guess must not spend the budget every later invocation
|
|
2541
2542
|
* then carries: the same reasoning as `CWD_SNAPSHOT_MAX_FILE_BYTES`, applied
|
|
2542
|
-
* to the package tree.
|
|
2543
|
+
* to the package tree. A miss this causes is loud and self-repairing: the
|
|
2544
|
+
* facet reports the unstaged read and the next build stages it from the
|
|
2545
|
+
* residency ledger, which has no per-file rule.
|
|
2543
2546
|
*/
|
|
2544
2547
|
const BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES = 4 * 1024 * 1024;
|
|
2545
2548
|
function shouldIncludeBinPackageFile(pkgRoot, path, bundleProfile) {
|
|
@@ -2769,15 +2772,20 @@ function __cacheKey(src) {
|
|
|
2769
2772
|
*/
|
|
2770
2773
|
function _markBundleEsmAsFailed(bundle, reason) {
|
|
2771
2774
|
for (const path of Object.keys(bundle)) {
|
|
2775
|
+
if (compiledCellPath(path) !== null)
|
|
2776
|
+
continue;
|
|
2772
2777
|
if (!isBundleModuleCandidate(path))
|
|
2773
2778
|
continue;
|
|
2774
2779
|
const src = bundle[path];
|
|
2775
2780
|
if (typeof src !== 'string')
|
|
2776
2781
|
continue;
|
|
2777
|
-
|
|
2782
|
+
// A TypeScript source is never runnable as staged, so it always needs
|
|
2783
|
+
// the emit it cannot get; a JavaScript file only if it is ESM.
|
|
2784
|
+
const typescript = bundleTypescriptLoader(path) !== null;
|
|
2785
|
+
if (!typescript && !looksLikeEsm(src))
|
|
2778
2786
|
continue;
|
|
2779
2787
|
const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
|
|
2780
|
-
bundle[path] =
|
|
2788
|
+
bundle[typescript ? compiledCellKey(path) : path] =
|
|
2781
2789
|
'// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
2782
2790
|
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
2783
2791
|
}
|
|
@@ -2808,13 +2816,89 @@ export function isBundleModuleCandidate(path) {
|
|
|
2808
2816
|
* EXTENSION, where `.js` files transform on their content — `looksLikeEsm` is
|
|
2809
2817
|
* the right question for a file that is already valid JS either way, and the
|
|
2810
2818
|
* wrong one for a file that is never valid JS.
|
|
2819
|
+
*
|
|
2820
|
+
* A declaration file (`.d.ts`, `.d.mts`, `.d.cts`) is not a source: it has
|
|
2821
|
+
* no runtime form, nothing `require()`s one, and esbuild's output for it is
|
|
2822
|
+
* empty by definition. It is DATA — read by the program that ships it, which
|
|
2823
|
+
* is exactly typescript: `tsc` reads its own `lib/lib.*.d.ts` with
|
|
2824
|
+
* `readFileSync`, and every declaration it type-checks against comes from
|
|
2825
|
+
* those bytes. Transforming them handed the compiler an 811-byte license
|
|
2826
|
+
* comment where `lib.es5.d.ts` (217 KB) had been, and every global type was
|
|
2827
|
+
* gone. So a declaration file is left exactly as it was staged.
|
|
2811
2828
|
*/
|
|
2812
2829
|
export function bundleTypescriptLoader(path) {
|
|
2830
|
+
if (isTypescriptDeclarationFile(path))
|
|
2831
|
+
return null;
|
|
2813
2832
|
const ext = vfsPathExtension(path);
|
|
2814
2833
|
if (ext === '.tsx')
|
|
2815
2834
|
return 'tsx';
|
|
2816
2835
|
return ext === '.ts' || ext === '.mts' || ext === '.cts' ? 'ts' : null;
|
|
2817
2836
|
}
|
|
2837
|
+
/** `name.d.ts` / `name.d.mts` / `name.d.cts`, by TypeScript's own rule. */
|
|
2838
|
+
export function isTypescriptDeclarationFile(path) {
|
|
2839
|
+
const base = path.slice(path.lastIndexOf('/') + 1);
|
|
2840
|
+
return /\.d\.[mc]?ts$/.test(base);
|
|
2841
|
+
}
|
|
2842
|
+
/**
|
|
2843
|
+
* Where a bundle carries the EXECUTABLE form of a cell whose staged bytes are
|
|
2844
|
+
* not JavaScript — a TypeScript source.
|
|
2845
|
+
*
|
|
2846
|
+
* A bundle cell is two things to the facet: what `readFileSync` returns for
|
|
2847
|
+
* the path, and what the startup pre-compile loop turns into the function
|
|
2848
|
+
* `require` runs. For a JavaScript file those are the same bytes, so the
|
|
2849
|
+
* ESM→CJS pass rewrites the cell in place and both readers see JavaScript.
|
|
2850
|
+
* For a TypeScript source they are NOT the same: the bytes a program reads
|
|
2851
|
+
* are the source (tsc compiling its project, a test runner, a linter), and
|
|
2852
|
+
* the bytes `require` needs are esbuild's emit. Rewriting the cell in place
|
|
2853
|
+
* handed tsc esbuild's CommonJS rendering of its own `src/index.ts` — it
|
|
2854
|
+
* compiled that, and reported errors on lines the file never had.
|
|
2855
|
+
*
|
|
2856
|
+
* So the source cell stays exactly as staged, and the compiled form travels
|
|
2857
|
+
* under this key: a NUL-framed prefix no filesystem path can contain, in the
|
|
2858
|
+
* same map, so it is sized, split, cached, released and shipped by every
|
|
2859
|
+
* mechanism the bundle already has. The facet's pre-compile loop takes these
|
|
2860
|
+
* cells OFF the map as it compiles them, keyed by the real path, before any
|
|
2861
|
+
* read can see them.
|
|
2862
|
+
*/
|
|
2863
|
+
const COMPILED_CELL_KEY_PREFIX = '\0compiled\0';
|
|
2864
|
+
export function compiledCellKey(path) {
|
|
2865
|
+
return COMPILED_CELL_KEY_PREFIX + path;
|
|
2866
|
+
}
|
|
2867
|
+
/** The source path a compiled-cell key stands for, or null for a plain path. */
|
|
2868
|
+
export function compiledCellPath(key) {
|
|
2869
|
+
return key.startsWith(COMPILED_CELL_KEY_PREFIX) ? key.slice(COMPILED_CELL_KEY_PREFIX.length) : null;
|
|
2870
|
+
}
|
|
2871
|
+
/**
|
|
2872
|
+
* The pre-compile loop both generated facets run at module evaluation, the
|
|
2873
|
+
* only moment workerd lets a string become code. One definition so the two
|
|
2874
|
+
* facets cannot drift on what they compile: every JavaScript-shaped cell
|
|
2875
|
+
* (`.js`, `.mjs`, `.cjs` and the extensionless bin scripts) from its own
|
|
2876
|
+
* bytes, and every TypeScript source from its compiled cell — which is
|
|
2877
|
+
* removed from the bundle here, so a `readFileSync` of the source path
|
|
2878
|
+
* returns the source and a directory listing never shows the key.
|
|
2879
|
+
*/
|
|
2880
|
+
export const BUNDLE_PRECOMPILE_LOOP = `
|
|
2881
|
+
const __NIMBUS_COMPILED_PREFIX = ${JSON.stringify(COMPILED_CELL_KEY_PREFIX)};
|
|
2882
|
+
for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
2883
|
+
let __modulePath = __p;
|
|
2884
|
+
if (__p.startsWith(__NIMBUS_COMPILED_PREFIX)) {
|
|
2885
|
+
__modulePath = __p.slice(__NIMBUS_COMPILED_PREFIX.length);
|
|
2886
|
+
delete __MODULE_VFS_BUNDLE[__p];
|
|
2887
|
+
} else {
|
|
2888
|
+
// .json is data; skip it (it loads via JSON.parse, not as a function).
|
|
2889
|
+
const __base = __p.slice(__p.lastIndexOf("/") + 1);
|
|
2890
|
+
const __dot = __base.lastIndexOf(".");
|
|
2891
|
+
const __ext = __dot > 0 ? __base.slice(__dot) : "";
|
|
2892
|
+
if (__ext !== ".js" && __ext !== ".mjs" && __ext !== ".cjs" && __ext !== "") continue;
|
|
2893
|
+
}
|
|
2894
|
+
if (typeof __c !== "string") continue;
|
|
2895
|
+
try {
|
|
2896
|
+
__compiledModules.set(__modulePath, __mkCompiledFn(__c));
|
|
2897
|
+
} catch (__e) {
|
|
2898
|
+
__compileFailures.set(__modulePath, __e && __e.message ? __e.message : String(__e));
|
|
2899
|
+
}
|
|
2900
|
+
}
|
|
2901
|
+
`;
|
|
2818
2902
|
/**
|
|
2819
2903
|
* Transform every ESM-shaped file in the bundle to CJS via esbuild.
|
|
2820
2904
|
* Mutates `bundle` in place. Errors are swallowed (the file is left as
|
|
@@ -2824,14 +2908,19 @@ export function bundleTypescriptLoader(path) {
|
|
|
2824
2908
|
* Candidate set is `isBundleModuleCandidate`; within it, files with no
|
|
2825
2909
|
* top-level import/export are already CJS-shaped and left alone.
|
|
2826
2910
|
*
|
|
2911
|
+
* A JavaScript cell is rewritten in place. A TypeScript source keeps its
|
|
2912
|
+
* bytes and gets a compiled cell beside it — see `compiledCellKey`.
|
|
2913
|
+
*
|
|
2827
2914
|
* Returns the count of files transformed (for diagnostics).
|
|
2828
2915
|
*/
|
|
2829
|
-
async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
2916
|
+
async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
2830
2917
|
let transformed = 0;
|
|
2831
2918
|
let failed = 0;
|
|
2832
2919
|
// Snapshot the keys first — esbuild calls await; never iterate-and-mutate.
|
|
2833
2920
|
const candidates = [];
|
|
2834
2921
|
for (const path of Object.keys(bundle)) {
|
|
2922
|
+
if (compiledCellPath(path) !== null)
|
|
2923
|
+
continue;
|
|
2835
2924
|
if (!isBundleModuleCandidate(path))
|
|
2836
2925
|
continue;
|
|
2837
2926
|
const src = bundle[path];
|
|
@@ -2839,14 +2928,19 @@ async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
|
2839
2928
|
// esbuild.transform expect strings.
|
|
2840
2929
|
if (typeof src !== 'string')
|
|
2841
2930
|
continue;
|
|
2842
|
-
if (bundleTypescriptLoader(path) === null
|
|
2843
|
-
|
|
2931
|
+
if (bundleTypescriptLoader(path) === null) {
|
|
2932
|
+
const esm = looksLikeEsm(src);
|
|
2933
|
+
if (!esm)
|
|
2934
|
+
continue;
|
|
2935
|
+
}
|
|
2844
2936
|
candidates.push(path);
|
|
2845
2937
|
}
|
|
2846
2938
|
for (const path of candidates) {
|
|
2847
2939
|
const src = bundle[path];
|
|
2848
2940
|
if (typeof src !== 'string')
|
|
2849
2941
|
continue;
|
|
2942
|
+
// A TypeScript source keeps its bytes; its emit lands beside it.
|
|
2943
|
+
const target = bundleTypescriptLoader(path) === null ? path : compiledCellKey(path);
|
|
2850
2944
|
// esbuild-wasm runs inside this isolate, so a transform is computation
|
|
2851
2945
|
// like any other pass — the await above it yields nothing on its own.
|
|
2852
2946
|
if (pacer)
|
|
@@ -2869,19 +2963,25 @@ async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
|
2869
2963
|
const key = __cacheKey(src + '\0' + absUrl);
|
|
2870
2964
|
const cached = __esmTransformCache.get(key);
|
|
2871
2965
|
if (cached) {
|
|
2872
|
-
bundle[
|
|
2966
|
+
bundle[target] = cached;
|
|
2873
2967
|
transformed++;
|
|
2874
2968
|
continue;
|
|
2875
2969
|
}
|
|
2876
2970
|
try {
|
|
2877
|
-
const
|
|
2971
|
+
const transformOptions = {
|
|
2878
2972
|
loader: bundleTypescriptLoader(path) ?? 'js',
|
|
2879
2973
|
format: 'cjs',
|
|
2880
2974
|
target: 'esnext',
|
|
2881
2975
|
define: importMetaDefines(absUrl),
|
|
2882
|
-
}
|
|
2976
|
+
};
|
|
2977
|
+
const bounded = transformOptions.loader === 'js' && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
2978
|
+
? rewriteBundledEsmToCjs(src, absUrl)
|
|
2979
|
+
: null;
|
|
2980
|
+
const t = bounded ?? (isolatedTransform && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
2981
|
+
? await isolatedTransform(src, transformOptions)
|
|
2982
|
+
: await esbuild.transform(src, transformOptions));
|
|
2883
2983
|
const code = bindImportMetaResolve(t.code, absUrl);
|
|
2884
|
-
bundle[
|
|
2984
|
+
bundle[target] = code;
|
|
2885
2985
|
__esmTransformCache.set(key, code);
|
|
2886
2986
|
transformed++;
|
|
2887
2987
|
}
|
|
@@ -2903,7 +3003,6 @@ async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
|
2903
3003
|
// __compiledModules, and the failure surfaces at require time
|
|
2904
3004
|
// with FULL diagnostic context. Net effect for users: the same
|
|
2905
3005
|
// upstream tool still fails, but they now see WHY.
|
|
2906
|
-
//
|
|
2907
3006
|
// Probe: tests/behavioral/npm-create/new/create-astro-diagnostic.mjs
|
|
2908
3007
|
// asserts the user-visible error contains "esbuild transform" so
|
|
2909
3008
|
// future diagnostic regressions are caught.
|
|
@@ -2914,7 +3013,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
|
2914
3013
|
// this so repeated submits don't re-run the same transform.
|
|
2915
3014
|
const diagnosticSrc = '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
2916
3015
|
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
2917
|
-
bundle[
|
|
3016
|
+
bundle[target] = diagnosticSrc;
|
|
2918
3017
|
__esmTransformCache.set(key, diagnosticSrc);
|
|
2919
3018
|
failed++;
|
|
2920
3019
|
}
|
|
@@ -2937,7 +3036,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
|
2937
3036
|
* behaviour for code paths that don't have esbuild handy).
|
|
2938
3037
|
*
|
|
2939
3038
|
*/
|
|
2940
|
-
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer) {
|
|
3039
|
+
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
2941
3040
|
// This build accumulates raw VFS contents in the supervisor heap, and did it
|
|
2942
3041
|
// with nothing watching: the estimator read 9.4 MiB while these bytes were
|
|
2943
3042
|
// resetting the DO three times. Take the budget the enrichment passes are
|
|
@@ -2947,14 +3046,14 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
|
|
|
2947
3046
|
const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
|
|
2948
3047
|
prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
|
|
2949
3048
|
try {
|
|
2950
|
-
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer);
|
|
3049
|
+
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, isolatedTransform);
|
|
2951
3050
|
}
|
|
2952
3051
|
finally {
|
|
2953
3052
|
prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
|
|
2954
3053
|
lease.release();
|
|
2955
3054
|
}
|
|
2956
3055
|
}
|
|
2957
|
-
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer) {
|
|
3056
|
+
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, isolatedTransform) {
|
|
2958
3057
|
// Read the cursor BEFORE the walk: a mutation that lands while the bundle
|
|
2959
3058
|
// is being assembled must be reported as invalidated, not silently missed.
|
|
2960
3059
|
const cursor = { epoch: vfs.epoch, rev: vfs.revision() };
|
|
@@ -3069,7 +3168,7 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3069
3168
|
// "file was not pre-bundled" at request time.
|
|
3070
3169
|
if (esbuild) {
|
|
3071
3170
|
try {
|
|
3072
|
-
await transformEsmInBundle(bundle, esbuild, pacer);
|
|
3171
|
+
await transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform);
|
|
3073
3172
|
// Recount bytes after the transform — CJS rebuilds can be larger
|
|
3074
3173
|
// OR smaller than the ESM source. We don't try to thread totalBytes
|
|
3075
3174
|
// through the transform because the eviction loop below recomputes
|
|
@@ -3108,8 +3207,9 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3108
3207
|
// went are named rather than silently dropped.
|
|
3109
3208
|
const size = encodedBundleSize(bundle, manifest);
|
|
3110
3209
|
if (size.bytes > BUNDLE_MAX_ENCODED_BYTES) {
|
|
3210
|
+
// A compiled cell goes with its source: required when the source is.
|
|
3111
3211
|
const evictable = Object.keys(bundle)
|
|
3112
|
-
.filter((path) => !requiredPaths.has(path))
|
|
3212
|
+
.filter((path) => !requiredPaths.has(compiledCellPath(path) ?? path))
|
|
3113
3213
|
.sort((a, b) => _bundleCellLength(bundle[b]) - _bundleCellLength(bundle[a]));
|
|
3114
3214
|
const evicted = [];
|
|
3115
3215
|
for (const k of evictable) {
|
|
@@ -3149,6 +3249,16 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3149
3249
|
};
|
|
3150
3250
|
}
|
|
3151
3251
|
const ROUTEABLE_PORT_ATTACH_TIMEOUT_MS = 1_000;
|
|
3252
|
+
/** A port request may wait this long for a durable app's re-drive to boot. */
|
|
3253
|
+
const DURABLE_ENSURE_BOOT_BUDGET_MS = 12_000;
|
|
3254
|
+
/**
|
|
3255
|
+
* The durable owner a journal row serves: the row's stamped field for
|
|
3256
|
+
* reservation-claimed residents, falling back to the worker recipe's own
|
|
3257
|
+
* owner for rows written before the stamping rule landed.
|
|
3258
|
+
*/
|
|
3259
|
+
function residentOwner(record) {
|
|
3260
|
+
return record.owner ?? (record.recipe.kind === 'worker' ? record.recipe.owner : undefined);
|
|
3261
|
+
}
|
|
3152
3262
|
/** 'running' is the measured common case: the platform's reset strikes
|
|
3153
3263
|
* seconds after a launch settles, while the resident runs. */
|
|
3154
3264
|
function residentLaunchDoing(record) {
|
|
@@ -3178,20 +3288,20 @@ export class FacetManager {
|
|
|
3178
3288
|
debugEnabled = false;
|
|
3179
3289
|
processRpcResources = new Map();
|
|
3180
3290
|
/**
|
|
3181
|
-
* The content-addressed boot-image store (fabric's
|
|
3291
|
+
* The content-addressed boot-image store (fabric's image-store.ts),
|
|
3182
3292
|
* writing through this session's kernel-credentialed VFS and rooted off the
|
|
3183
3293
|
* live process table.
|
|
3184
3294
|
*/
|
|
3185
|
-
imageStore = new
|
|
3295
|
+
imageStore = new ImageStore(() => this._imageBlobs(), (pid) => this.processes.get(pid)?.state === 'running');
|
|
3186
3296
|
/**
|
|
3187
|
-
* The resident-launch journal (fabric's
|
|
3297
|
+
* The resident-launch journal (fabric's fenced-work.ts): the durable
|
|
3188
3298
|
* record of every resident this session owes the user, and its recovery
|
|
3189
3299
|
* after an instance reset. This manager supplies what a launch IS — the
|
|
3190
|
-
*
|
|
3300
|
+
* recipe `_redrive` re-drives from — and how its loss is reported.
|
|
3191
3301
|
*/
|
|
3192
3302
|
launchJournal;
|
|
3193
3303
|
/**
|
|
3194
|
-
* The granting side of the launch
|
|
3304
|
+
* The granting side of the launch budget (fabric's turn-budget.ts). The
|
|
3195
3305
|
* session's alarm re-enters the object through `pumpResidentLaunches`;
|
|
3196
3306
|
* journal recovery rides the first pump.
|
|
3197
3307
|
*/
|
|
@@ -3234,12 +3344,13 @@ export class FacetManager {
|
|
|
3234
3344
|
* prefetch cache above so a profile can only ever seed the bundle it was
|
|
3235
3345
|
* measured against.
|
|
3236
3346
|
*
|
|
3237
|
-
* Lifetime is the supervisor incarnation's, same as the cache — a restart
|
|
3238
3347
|
* costs one more loud failure and then relearns. Persisting it would be a
|
|
3239
3348
|
* schema and a migration bought with nothing the in-memory form does not
|
|
3240
3349
|
* already deliver for the case that matters: running the command again.
|
|
3241
3350
|
*/
|
|
3242
3351
|
residencyProfiles = new Map();
|
|
3352
|
+
/** In-flight request-driven durable-app ensures, single-flight per port. */
|
|
3353
|
+
ensureInflight = new Map();
|
|
3243
3354
|
static RESIDENCY_PROFILE_MAX_ENTRIES = 16;
|
|
3244
3355
|
/**
|
|
3245
3356
|
* A program that reads a directory of data files misses once per file, so
|
|
@@ -3266,10 +3377,10 @@ export class FacetManager {
|
|
|
3266
3377
|
? Reflect.get(env, 'NIMBUS_DEBUG')
|
|
3267
3378
|
: undefined;
|
|
3268
3379
|
this.debugEnabled = debugVar === '1' || debugVar === 'true';
|
|
3269
|
-
this.launchJournal = new
|
|
3380
|
+
this.launchJournal = new FencedWork(ctx.storage, {
|
|
3270
3381
|
generationBase: () => this.processes.pidBase,
|
|
3271
3382
|
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
3272
|
-
redrive: (record, attempt) => this.
|
|
3383
|
+
redrive: (record, attempt) => this._redrive(record, attempt),
|
|
3273
3384
|
onRedrive: (record) => this.hooks.notify?.('\x1b[2m[nimbus: the session restarted while '
|
|
3274
3385
|
+ `"${record.command}" was ${residentLaunchDoing(record)} — restarting it]\x1b[0m\r\n`),
|
|
3275
3386
|
onAbandoned: (record) => this.hooks.notify?.('\x1b[2m[nimbus: the session restarted again while '
|
|
@@ -3277,10 +3388,13 @@ export class FacetManager {
|
|
|
3277
3388
|
onRedriveFailed: (record, e) => this.hooks.notify?.(`\x1b[2m[nimbus: "${record.command}" could not be restarted: `
|
|
3278
3389
|
+ `${errorMessage(e)}]\x1b[0m\r\n`),
|
|
3279
3390
|
});
|
|
3280
|
-
this.launchPump = new
|
|
3391
|
+
this.launchPump = new PacedWork(ctx, {
|
|
3281
3392
|
requestTurn: hooks.requestLaunchTurn?.bind(hooks),
|
|
3282
|
-
recover: () => this.launchJournal.recoverInterrupted(),
|
|
3283
3393
|
});
|
|
3394
|
+
// A reset that killed a resident launch left journal rows behind; the
|
|
3395
|
+
// first pump after the reset drains this reconciliation before any
|
|
3396
|
+
// waiter resumes, OFF the constructor's init gate.
|
|
3397
|
+
onColdStart(ctx, () => this.launchJournal.recoverInterrupted());
|
|
3284
3398
|
// The journal row of a resident lives for the PROCESS's lifetime, so its
|
|
3285
3399
|
// release belongs on the one seam every end-of-life passes through —
|
|
3286
3400
|
// exit, kill, self-reported exit and timeout abort all mark the table.
|
|
@@ -3322,6 +3436,17 @@ export class FacetManager {
|
|
|
3322
3436
|
unlink: (path) => fs.unlink(path),
|
|
3323
3437
|
};
|
|
3324
3438
|
}
|
|
3439
|
+
/**
|
|
3440
|
+
* The kernel-scoped VFS the durable image store reads and writes through —
|
|
3441
|
+
* `.nimbus/images/<sha256>` is session kernel data, not user content.
|
|
3442
|
+
*/
|
|
3443
|
+
_imageVfs() {
|
|
3444
|
+
const vfs = this.vfs;
|
|
3445
|
+
if (!vfs) {
|
|
3446
|
+
throw new Error('Nimbus: a durable spawn needs a session filesystem to persist its launch image');
|
|
3447
|
+
}
|
|
3448
|
+
return vfs;
|
|
3449
|
+
}
|
|
3325
3450
|
/**
|
|
3326
3451
|
* W3.5 Fix B: hand the FacetManager a pre-warmed EsbuildService for
|
|
3327
3452
|
* the ESM→CJS bundle pre-pass. NimbusSession already lazy-creates one
|
|
@@ -3408,7 +3533,7 @@ export class FacetManager {
|
|
|
3408
3533
|
this.prefetchBundleCache.set(key, cached);
|
|
3409
3534
|
return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
|
|
3410
3535
|
}
|
|
3411
|
-
const vfsState = await buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, this.esbuild || undefined, bundleProfile, this.residencyProfiles.get(key));
|
|
3536
|
+
const vfsState = await buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, this.esbuild || undefined, bundleProfile, this.residencyProfiles.get(key), undefined, this.hooks.transformLargeEsm);
|
|
3412
3537
|
vfsState.bundleKey = key;
|
|
3413
3538
|
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired);
|
|
3414
3539
|
vfsState.serializedManifest = JSON.stringify(vfsState.manifest);
|
|
@@ -3619,12 +3744,7 @@ export class FacetManager {
|
|
|
3619
3744
|
// CJS before they hit the facet's `new Function` pre-compile loop.
|
|
3620
3745
|
// Lazy-create one if NimbusSession didn't share its own.
|
|
3621
3746
|
if (this.vfs && !this.esbuild) {
|
|
3622
|
-
|
|
3623
|
-
this.esbuild = new EsbuildService(this.vfs);
|
|
3624
|
-
}
|
|
3625
|
-
catch {
|
|
3626
|
-
this.esbuild = null;
|
|
3627
|
-
}
|
|
3747
|
+
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
3628
3748
|
}
|
|
3629
3749
|
const diagOn = isExecDiagEnabled();
|
|
3630
3750
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
@@ -3942,7 +4062,7 @@ export class FacetManager {
|
|
|
3942
4062
|
this.imageStore.ensureDir();
|
|
3943
4063
|
const processVfs = this.vfs?.as(entry.cred);
|
|
3944
4064
|
const vfsState = processVfs
|
|
3945
|
-
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined)
|
|
4065
|
+
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE, undefined, undefined, this.hooks.transformLargeEsm)
|
|
3946
4066
|
: { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
|
|
3947
4067
|
const vfsBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
3948
4068
|
assertStagedBundleFitsRpcPayload(vfsBundle, vfsState.bundle);
|
|
@@ -4244,7 +4364,7 @@ export class FacetManager {
|
|
|
4244
4364
|
/**
|
|
4245
4365
|
* Grant every suspended launch a chunk of this turn — the session's alarm
|
|
4246
4366
|
* calls this, and journal recovery rides the first pump. See fabric's
|
|
4247
|
-
* `
|
|
4367
|
+
* `PacedWork.pump` for the ownership argument.
|
|
4248
4368
|
*/
|
|
4249
4369
|
pumpResidentLaunches() {
|
|
4250
4370
|
return this.launchPump.pump();
|
|
@@ -4413,6 +4533,39 @@ export class FacetManager {
|
|
|
4413
4533
|
clearTimeout(timer);
|
|
4414
4534
|
}
|
|
4415
4535
|
}
|
|
4536
|
+
/**
|
|
4537
|
+
* Re-drive a journalled launch after an instance reset. What the journal
|
|
4538
|
+
* row carries is the recipe and nothing else: env and credentials are never
|
|
4539
|
+
* written to storage, so a worker launch's are re-resolved by the embedder
|
|
4540
|
+
* through `hooks.resolveWorkerLaunch`.
|
|
4541
|
+
*/
|
|
4542
|
+
async _redrive(record, attempt) {
|
|
4543
|
+
const { recipe } = record;
|
|
4544
|
+
switch (recipe.kind) {
|
|
4545
|
+
case 'node': return this._spawnResident(recipe.code, recipe.opts, attempt);
|
|
4546
|
+
case 'worker': {
|
|
4547
|
+
// The embedder's resolver decides everything; only a self-owned
|
|
4548
|
+
// launch — nobody composed an embedder hook — falls through to the
|
|
4549
|
+
// session's image-store default.
|
|
4550
|
+
const resolve = this.hooks.resolveWorkerLaunch ?? this.hooks.resolveWorkerLaunchFallback;
|
|
4551
|
+
if (!resolve)
|
|
4552
|
+
throw new Error('resident launch journal holds a worker launch but no resolveWorkerLaunch hook is composed');
|
|
4553
|
+
const resolved = await resolve(recipe);
|
|
4554
|
+
if (resolved === null)
|
|
4555
|
+
return undefined;
|
|
4556
|
+
const { 'worker.js': workerCode, ...modules } = resolved.modules;
|
|
4557
|
+
if (workerCode === undefined)
|
|
4558
|
+
throw new Error('resolved worker launch carries no worker.js module');
|
|
4559
|
+
return this._spawnWorker(workerCode, record.command, recipe.cwd, {
|
|
4560
|
+
port: recipe.port > 0 ? recipe.port : undefined, modules, compatibilityDate: recipe.compatibilityDate,
|
|
4561
|
+
compatibilityFlags: recipe.compatibilityFlags, startArgs: recipe.startArgs,
|
|
4562
|
+
env: resolved.env ?? undefined, globalOutbound: resolved.globalOutbound,
|
|
4563
|
+
vfsWasmModules: resolved.vfsWasmModules,
|
|
4564
|
+
durable: { owner: recipe.owner, image: recipe.image },
|
|
4565
|
+
}, attempt);
|
|
4566
|
+
}
|
|
4567
|
+
}
|
|
4568
|
+
}
|
|
4416
4569
|
/**
|
|
4417
4570
|
* Spawn a long-running Node process with the same shimmed require/fs/http
|
|
4418
4571
|
* environment used by foreground `node <script>` execution.
|
|
@@ -4488,7 +4641,7 @@ export class FacetManager {
|
|
|
4488
4641
|
*/
|
|
4489
4642
|
async _runResidentLaunch(entry, code, command, opts, attempt) {
|
|
4490
4643
|
const cwd = opts.cwd || '/home/user';
|
|
4491
|
-
const pacer = new
|
|
4644
|
+
const pacer = new TurnBudget(this.launchPump, turnChunkMaxBytes(this.env), () => this._assertLaunchStillOwned(entry.pid));
|
|
4492
4645
|
// Journalled before the first byte of work. A launch that FAILS deletes
|
|
4493
4646
|
// its row on the way out — its process has already been exited and the
|
|
4494
4647
|
// user notified, so there is nothing left to owe. A launch that SETTLES
|
|
@@ -4498,17 +4651,51 @@ export class FacetManager {
|
|
|
4498
4651
|
// sessions kept dying. The row is finally released by the supervisor's
|
|
4499
4652
|
// terminal hook when the process ends. A re-drive owns its own process,
|
|
4500
4653
|
// so the caller's pid goes with the instance that had it.
|
|
4654
|
+
//
|
|
4655
|
+
// Durability is a property of the RESERVATION, not the recipe: a resident
|
|
4656
|
+
// that declares a port at spawn claims the durable contract the port's
|
|
4657
|
+
// reservation declares — the owner's durable slot for a store and the
|
|
4658
|
+
// row's port/owner for `ensureDurableAppOnPort`. A resident whose port is
|
|
4659
|
+
// only learned at listen() gets the same journal+capability durability at
|
|
4660
|
+
// registration (see `registerPort`), but keeps its ephemeral facet name —
|
|
4661
|
+
// the store cannot be re-rooted under a running facet.
|
|
4662
|
+
let durableFacetName;
|
|
4663
|
+
let durablePort;
|
|
4664
|
+
let durableOwner;
|
|
4665
|
+
if (opts.port !== undefined && opts.port > 0 && opts.port < 65536) {
|
|
4666
|
+
const reservation = await readPortReservation(this.ctx, opts.port);
|
|
4667
|
+
if (reservation !== null && reservation.owner !== null) {
|
|
4668
|
+
durableOwner = reservation.owner;
|
|
4669
|
+
durablePort = opts.port;
|
|
4670
|
+
durableFacetName = await acquireDurableFacetSlot(this.ctx, durableOwner);
|
|
4671
|
+
}
|
|
4672
|
+
}
|
|
4501
4673
|
const record = {
|
|
4502
4674
|
pid: entry.pid,
|
|
4503
4675
|
command,
|
|
4504
|
-
code,
|
|
4505
|
-
opts: { ...opts, skipSpawn: undefined, callerPid: undefined },
|
|
4506
4676
|
attempt,
|
|
4507
4677
|
phase: 'starting',
|
|
4678
|
+
recipe: { kind: 'node', code, opts: { ...opts, skipSpawn: undefined, callerPid: undefined } },
|
|
4679
|
+
...(durablePort !== undefined ? { port: durablePort, owner: durableOwner } : {}),
|
|
4508
4680
|
};
|
|
4509
|
-
await this.launchJournal.journal(record);
|
|
4510
4681
|
try {
|
|
4511
|
-
await this.
|
|
4682
|
+
await this.launchJournal.journal(record);
|
|
4683
|
+
}
|
|
4684
|
+
catch (e) {
|
|
4685
|
+
// A resident that cannot be journalled does not start; the failure is
|
|
4686
|
+
// reported the way a boot failure is.
|
|
4687
|
+
pacer.settle();
|
|
4688
|
+
this.processes.exit(entry.pid, 1);
|
|
4689
|
+
const reason = 'long-running node launch failed: ' + errorMessage(e);
|
|
4690
|
+
this._w5RecordTermination(entry.pid, 1, 'facet', reason);
|
|
4691
|
+
try {
|
|
4692
|
+
this.hooks.onExternalExit?.(entry.pid, 1, reason);
|
|
4693
|
+
}
|
|
4694
|
+
catch { }
|
|
4695
|
+
throw e;
|
|
4696
|
+
}
|
|
4697
|
+
try {
|
|
4698
|
+
await this._residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName);
|
|
4512
4699
|
}
|
|
4513
4700
|
catch (e) {
|
|
4514
4701
|
await this.launchJournal.release(entry.pid);
|
|
@@ -4525,14 +4712,9 @@ export class FacetManager {
|
|
|
4525
4712
|
await this.launchJournal.journal({ ...record, attempt: 0, phase: 'running' });
|
|
4526
4713
|
}
|
|
4527
4714
|
}
|
|
4528
|
-
async _residentLaunchBody(entry, code, command, cwd, opts, pacer) {
|
|
4715
|
+
async _residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName) {
|
|
4529
4716
|
if (this.vfs && !this.esbuild) {
|
|
4530
|
-
|
|
4531
|
-
this.esbuild = new EsbuildService(this.vfs);
|
|
4532
|
-
}
|
|
4533
|
-
catch {
|
|
4534
|
-
this.esbuild = null;
|
|
4535
|
-
}
|
|
4717
|
+
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
4536
4718
|
}
|
|
4537
4719
|
if (this.vfs)
|
|
4538
4720
|
this.imageStore.ensureDir();
|
|
@@ -4540,7 +4722,7 @@ export class FacetManager {
|
|
|
4540
4722
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
4541
4723
|
const processVfs = this.vfs?.as(entry.cred);
|
|
4542
4724
|
const vfsState = processVfs
|
|
4543
|
-
? await buildPrefetchBundle(processVfs, opts.filename, cwd, code, this.esbuild || undefined, opts.bundleProfile, undefined, pacer)
|
|
4725
|
+
? await buildPrefetchBundle(processVfs, opts.filename, cwd, code, this.esbuild || undefined, opts.bundleProfile, undefined, pacer, this.hooks.transformLargeEsm)
|
|
4544
4726
|
: { bundle: {}, manifest: {}, metadata: {}, reachableCount: 0, truncated: false };
|
|
4545
4727
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
4546
4728
|
const processEnv = opts.attachedTty
|
|
@@ -4597,6 +4779,12 @@ export class FacetManager {
|
|
|
4597
4779
|
// life; the server/watch runner returns once it is up.
|
|
4598
4780
|
startContract: opts.attachedTty ? 'lifetime' : 'boot',
|
|
4599
4781
|
startArgs: { vfsCursor },
|
|
4782
|
+
// A resident whose declared port is reserved binds the owner's
|
|
4783
|
+
// durable slot — the same store a durable worker spawn takes — so the
|
|
4784
|
+
// reservation's durability reaches this process's storage too.
|
|
4785
|
+
...(durableFacetName !== undefined
|
|
4786
|
+
? { facet: { name: durableFacetName, durable: true } }
|
|
4787
|
+
: {}),
|
|
4600
4788
|
boot: {
|
|
4601
4789
|
kind: 'code',
|
|
4602
4790
|
code: {
|
|
@@ -4670,10 +4858,7 @@ export class FacetManager {
|
|
|
4670
4858
|
await handle.booted();
|
|
4671
4859
|
}
|
|
4672
4860
|
if (opts.port && opts.port > 0 && opts.port < 65536) {
|
|
4673
|
-
|
|
4674
|
-
// capability, so a URL handed out for that one cannot reach this one.
|
|
4675
|
-
await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, opts.port);
|
|
4676
|
-
this.portRegistry.register(opts.port, entry.pid);
|
|
4861
|
+
await this._registerResidentPort(entry.pid, opts.port);
|
|
4677
4862
|
}
|
|
4678
4863
|
}
|
|
4679
4864
|
catch (e) {
|
|
@@ -4706,6 +4891,10 @@ export class FacetManager {
|
|
|
4706
4891
|
* promise, so nothing polls the port to decide the process is up.
|
|
4707
4892
|
*/
|
|
4708
4893
|
async spawnWorker(workerCode, command, cwd, opts = {}) {
|
|
4894
|
+
return this._spawnWorker(workerCode, command, cwd, opts, 0);
|
|
4895
|
+
}
|
|
4896
|
+
/** `attempt` is the journal's re-drive budget, as `_spawnResident` carries it. */
|
|
4897
|
+
async _spawnWorker(workerCode, command, cwd, opts, attempt) {
|
|
4709
4898
|
this.processes.reap();
|
|
4710
4899
|
const entry = this.processes.spawn(command, [], cwd);
|
|
4711
4900
|
// Stamp the process-table entry so /api/processes exposes this as a
|
|
@@ -4717,35 +4906,130 @@ export class FacetManager {
|
|
|
4717
4906
|
this.hooks.onSpawn?.(entry.pid, command, true);
|
|
4718
4907
|
}
|
|
4719
4908
|
catch { }
|
|
4909
|
+
const compatibilityDate = opts.compatibilityDate ?? CF_COMPAT_DATE;
|
|
4910
|
+
const compatibilityFlags = opts.compatibilityFlags || ['nodejs_compat'];
|
|
4720
4911
|
let handle;
|
|
4721
4912
|
let resourcesTracked = false;
|
|
4913
|
+
let record;
|
|
4914
|
+
let durableFacetName;
|
|
4722
4915
|
try {
|
|
4916
|
+
if (opts.durable) {
|
|
4917
|
+
// A durable spawn on a declared port starts only when a reservation the
|
|
4918
|
+
// owner already holds is persisted. Validated before the journal row is
|
|
4919
|
+
// written and before the process boots, so a foreign or absent
|
|
4920
|
+
// reservation refuses the launch rather than stealing the exposure.
|
|
4921
|
+
if (opts.port && opts.port > 0 && opts.port < 65536) {
|
|
4922
|
+
const preFlight = await readPortReservation(this.ctx, opts.port);
|
|
4923
|
+
if (preFlight === null || preFlight.owner !== opts.durable.owner) {
|
|
4924
|
+
throw new Error('port reservation conflict: durable worker does not own port ' + opts.port);
|
|
4925
|
+
}
|
|
4926
|
+
}
|
|
4927
|
+
// A live outbound binding cannot be journalled — a reset would re-drive
|
|
4928
|
+
// the recipe with nothing to stand in for it. Only the EMBEDDER's
|
|
4929
|
+
// resolveWorkerLaunch can re-mint one: the session's fallback reads
|
|
4930
|
+
// the image store, which never carries a binding, so a durable spawn
|
|
4931
|
+
// carrying globalOutbound is refused where no embedder hook exists.
|
|
4932
|
+
if (opts.globalOutbound !== undefined && opts.globalOutbound !== null
|
|
4933
|
+
&& this.hooks.resolveWorkerLaunch === undefined) {
|
|
4934
|
+
throw new Error('Nimbus: a durable spawn carrying a live globalOutbound binding '
|
|
4935
|
+
+ 'cannot be journalled without an embedder resolveWorkerLaunch hook');
|
|
4936
|
+
}
|
|
4937
|
+
// Self-owned applications persist their launch inputs as image blobs —
|
|
4938
|
+
// a runner blob for worker.js, an application blob for modules + env —
|
|
4939
|
+
// under .nimbus/images/<sha256>, and the journal row names the digests
|
|
4940
|
+
// of what was written, never placeholder strings. An embedder-owned
|
|
4941
|
+
// spawn is given digests by its own bookkeeping instead.
|
|
4942
|
+
const image = opts.durable.image
|
|
4943
|
+
?? await persistDurableWorkerImage(this._imageVfs(), workerCode, {
|
|
4944
|
+
modules: opts.modules ?? {},
|
|
4945
|
+
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
4946
|
+
vfsWasmModules: opts.vfsWasmModules,
|
|
4947
|
+
});
|
|
4948
|
+
// Journalled before the launch's first byte of work, so a row a
|
|
4949
|
+
// later instance reads proves this process never ended.
|
|
4950
|
+
record = {
|
|
4951
|
+
pid: entry.pid,
|
|
4952
|
+
command,
|
|
4953
|
+
attempt,
|
|
4954
|
+
phase: 'starting',
|
|
4955
|
+
recipe: {
|
|
4956
|
+
kind: 'worker',
|
|
4957
|
+
owner: opts.durable.owner,
|
|
4958
|
+
image,
|
|
4959
|
+
port: opts.port ?? 0,
|
|
4960
|
+
cwd,
|
|
4961
|
+
compatibilityDate,
|
|
4962
|
+
compatibilityFlags,
|
|
4963
|
+
startArgs: opts.startArgs,
|
|
4964
|
+
},
|
|
4965
|
+
// The row-level fields `ensureDurableAppOnPort` reads — same values
|
|
4966
|
+
// the recipe carries, stamped so port/owner lookup never depends on
|
|
4967
|
+
// the recipe's kind.
|
|
4968
|
+
...(opts.port !== undefined && opts.port > 0
|
|
4969
|
+
? { port: opts.port, owner: opts.durable.owner }
|
|
4970
|
+
: {}),
|
|
4971
|
+
};
|
|
4972
|
+
// The durable facet name is claimed once, ever, from DO storage — a
|
|
4973
|
+
// re-drive after a reset, an eviction's re-attach and a relaunch all
|
|
4974
|
+
// land on the same `app-slot-<n>`, which is the only thing that keeps
|
|
4975
|
+
// the retained SQLite bound to this application.
|
|
4976
|
+
durableFacetName = await acquireDurableFacetSlot(this.ctx, opts.durable.owner);
|
|
4977
|
+
await this.launchJournal.journal(record);
|
|
4978
|
+
}
|
|
4723
4979
|
handle = await this._startResidentProcess(entry.pid, {
|
|
4724
4980
|
// These runners answer startProcess with a boot payload (listening
|
|
4725
4981
|
// port, or a completed non-server run) and stay resident after it.
|
|
4726
4982
|
startContract: 'boot',
|
|
4727
4983
|
startArgs: opts.startArgs,
|
|
4984
|
+
...(durableFacetName !== undefined
|
|
4985
|
+
? { facet: { name: durableFacetName, durable: true } }
|
|
4986
|
+
: {}),
|
|
4728
4987
|
boot: {
|
|
4729
4988
|
kind: 'code',
|
|
4730
4989
|
code: {
|
|
4731
|
-
compatibilityDate
|
|
4732
|
-
compatibilityFlags
|
|
4990
|
+
compatibilityDate,
|
|
4991
|
+
compatibilityFlags,
|
|
4733
4992
|
mainModule: 'worker.js',
|
|
4734
4993
|
modules: { 'worker.js': workerCode, ...(opts.modules || {}) },
|
|
4735
4994
|
vfsWasmModules: opts.vfsWasmModules,
|
|
4995
|
+
...(opts.env !== undefined ? { env: opts.env } : {}),
|
|
4996
|
+
...(opts.globalOutbound !== undefined ? { globalOutbound: opts.globalOutbound } : {}),
|
|
4736
4997
|
},
|
|
4737
4998
|
},
|
|
4738
4999
|
});
|
|
4739
5000
|
this.trackProcessRpcResources(entry.pid, [handle]);
|
|
4740
5001
|
resourcesTracked = true;
|
|
4741
5002
|
this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
|
|
5003
|
+
const boot = await handle.booted();
|
|
5004
|
+
if (opts.durable && this.launchJournal.has(entry.pid) && record) {
|
|
5005
|
+
// Booted and running: the launch proved itself, so the resident
|
|
5006
|
+
// starts its running life with a fresh re-drive budget.
|
|
5007
|
+
await this.launchJournal.journal({ ...record, attempt: 0, phase: 'running' });
|
|
5008
|
+
}
|
|
4742
5009
|
if (opts.port && opts.port > 0 && opts.port < 65536) {
|
|
4743
|
-
|
|
4744
|
-
|
|
4745
|
-
|
|
4746
|
-
|
|
5010
|
+
if (opts.durable) {
|
|
5011
|
+
// Re-read after the boot: a release or reassignment during it wins.
|
|
5012
|
+
// The durable launch registers only under a reservation it still
|
|
5013
|
+
// owns; it never clears or takes a foreign exposure.
|
|
5014
|
+
const reservation = await readPortReservation(this.ctx, opts.port);
|
|
5015
|
+
if (reservation === null || reservation.owner !== opts.durable.owner) {
|
|
5016
|
+
throw new Error('port reservation conflict: durable worker does not own port ' + opts.port);
|
|
5017
|
+
}
|
|
5018
|
+
// The owner's hold on the port survives the instance reset that
|
|
5019
|
+
// re-drove this launch, and preview URLs minted against it stay
|
|
5020
|
+
// valid: the durable capability is re-adopted through the
|
|
5021
|
+
// reservation's own path, gated on the stored owner.
|
|
5022
|
+
this.portRegistry.register(opts.port, entry.pid);
|
|
5023
|
+
await restoreReservedPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, opts.port, opts.durable.owner);
|
|
5024
|
+
}
|
|
5025
|
+
else {
|
|
5026
|
+
// A new process on a port retires the previous occupant's preview
|
|
5027
|
+
// capability, so a URL handed out for that one cannot reach this one.
|
|
5028
|
+
await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, opts.port);
|
|
5029
|
+
this.portRegistry.register(opts.port, entry.pid);
|
|
5030
|
+
}
|
|
4747
5031
|
}
|
|
4748
|
-
return { pid: entry.pid, boot
|
|
5032
|
+
return { pid: entry.pid, boot };
|
|
4749
5033
|
}
|
|
4750
5034
|
catch (e) {
|
|
4751
5035
|
this.portRegistry.unregisterByPid(entry.pid);
|
|
@@ -4763,10 +5047,44 @@ export class FacetManager {
|
|
|
4763
5047
|
throw e;
|
|
4764
5048
|
}
|
|
4765
5049
|
}
|
|
5050
|
+
/**
|
|
5051
|
+
* A resident process announcing it bound `port`. When the port is reserved,
|
|
5052
|
+
* the reservation's owner is stamped onto this pid's journal row — the
|
|
5053
|
+
* reservation is what declares which application the port serves, and a
|
|
5054
|
+
* resident that binds it inherits the whole durable contract: the row names
|
|
5055
|
+
* the port `ensureDurableAppOnPort` looks up, the minted capability is
|
|
5056
|
+
* re-adopted rather than retired, and `removeDurableApp` can find the launch
|
|
5057
|
+
* by owner.
|
|
5058
|
+
*
|
|
5059
|
+
* A pid with no journal row — a process outside the resident lifecycle —
|
|
5060
|
+
* registering on a reserved port takes it as today: the stored capability
|
|
5061
|
+
* is retired and a fresh one is minted, the reservation stays with the
|
|
5062
|
+
* owner. An accidental port reuse inside one session therefore cannot
|
|
5063
|
+
* inherit the public capability; same-session processes are one trust
|
|
5064
|
+
* domain, so this is hygiene, not a security boundary.
|
|
5065
|
+
*/
|
|
5066
|
+
async _registerResidentPort(pid, port) {
|
|
5067
|
+
const reservation = await readPortReservation(this.ctx, port);
|
|
5068
|
+
const rowKey = `${FENCED_WORK_KEY_PREFIX}${pid}`;
|
|
5069
|
+
const rows = await this.launchJournal.rows();
|
|
5070
|
+
const row = rows.get(rowKey);
|
|
5071
|
+
if (reservation !== null && reservation.owner !== null && row !== undefined) {
|
|
5072
|
+
if (row.port !== port || residentOwner(row) !== reservation.owner) {
|
|
5073
|
+
await this.launchJournal.journal({ ...row, port, owner: reservation.owner });
|
|
5074
|
+
}
|
|
5075
|
+
this.portRegistry.register(port, pid);
|
|
5076
|
+
await restoreReservedPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port, reservation.owner);
|
|
5077
|
+
return;
|
|
5078
|
+
}
|
|
5079
|
+
// A new process on an unreserved port — or one the journal does not know —
|
|
5080
|
+
// retires the previous occupant's preview capability, so a URL handed out
|
|
5081
|
+
// for that one cannot reach this one.
|
|
5082
|
+
await clearPortCapability({ ctx: this.ctx, portRegistry: this.portRegistry }, port);
|
|
5083
|
+
this.portRegistry.register(port, pid);
|
|
5084
|
+
}
|
|
4766
5085
|
async registerPort(pid, port) {
|
|
4767
5086
|
if (port > 0 && port < 65536) {
|
|
4768
|
-
await
|
|
4769
|
-
this.portRegistry.register(port, pid);
|
|
5087
|
+
await this._registerResidentPort(pid, port);
|
|
4770
5088
|
}
|
|
4771
5089
|
}
|
|
4772
5090
|
waitForRouteablePorts(pid, timeoutMs = ROUTEABLE_PORT_ATTACH_TIMEOUT_MS) {
|
|
@@ -4804,5 +5122,149 @@ export class FacetManager {
|
|
|
4804
5122
|
this._teardownPairedServeFacet(pid);
|
|
4805
5123
|
return result;
|
|
4806
5124
|
}
|
|
5125
|
+
/**
|
|
5126
|
+
* Remove a durable application: the ONLY path that deletes durable facet
|
|
5127
|
+
* storage. Owner-checked by construction — `freeDurableFacetSlot` answers
|
|
5128
|
+
* only a slot the owner actually holds, and the reservation release refuses
|
|
5129
|
+
* a foreign owner's record — so another owner's name cannot be reached.
|
|
5130
|
+
*
|
|
5131
|
+
* One ordered teardown: the live process is killed first (a released
|
|
5132
|
+
* durable facet only aborts, so nothing else ends it), the port reservation
|
|
5133
|
+
* is released, the journal rows for the owner are purged (nothing is owed a
|
|
5134
|
+
* removed application), the facet's SQLite is deleted, and the slot row is
|
|
5135
|
+
* freed last — so a crash mid-removal leaves a name still claimed rather
|
|
5136
|
+
* than a store nobody can re-drive.
|
|
5137
|
+
*/
|
|
5138
|
+
async removeDurableApp(owner) {
|
|
5139
|
+
// Kill any live process this owner still has — a durable release aborts
|
|
5140
|
+
// without deleting, so the store outlives the process unless removal
|
|
5141
|
+
// ends it here.
|
|
5142
|
+
const journalRows = await this.ctx.storage.list({ prefix: 'resident-launch:' });
|
|
5143
|
+
const ownedPids = [];
|
|
5144
|
+
const ownedPorts = new Set();
|
|
5145
|
+
for (const [, record] of journalRows) {
|
|
5146
|
+
// Owner-stamped rows cover every kind the reservation claims — a node
|
|
5147
|
+
// resident that bound the port is as much the application as a durable
|
|
5148
|
+
// worker spawn is.
|
|
5149
|
+
if (residentOwner(record) !== owner)
|
|
5150
|
+
continue;
|
|
5151
|
+
ownedPids.push(record.pid);
|
|
5152
|
+
const rowPort = record.port ?? (record.recipe.kind === 'worker' ? record.recipe.port : 0);
|
|
5153
|
+
if (rowPort > 0)
|
|
5154
|
+
ownedPorts.add(rowPort);
|
|
5155
|
+
}
|
|
5156
|
+
for (const pid of ownedPids)
|
|
5157
|
+
this.kill(pid);
|
|
5158
|
+
// The reservation may outlive every journal row (spawn never finished
|
|
5159
|
+
// writing one), so scan the port records for the owner too.
|
|
5160
|
+
const portRows = await this.ctx.storage.list({ prefix: PORT_CAPABILITY_KEY_PREFIX });
|
|
5161
|
+
for (const [key, record] of portRows) {
|
|
5162
|
+
if (record?.owner === owner)
|
|
5163
|
+
ownedPorts.add(Number(key.slice(PORT_CAPABILITY_KEY_PREFIX.length)));
|
|
5164
|
+
// A public capability leaves the routing directory with the row —
|
|
5165
|
+
// before the reservation is released so a mid-removal crash leaves a
|
|
5166
|
+
// dead URL rather than a dangling route to this session.
|
|
5167
|
+
if (record?.owner === owner && record.visibility === 'public' && typeof record.capability === 'string') {
|
|
5168
|
+
await unbindPublicPortCapability(this._publicDirectoryHost(), record.capability);
|
|
5169
|
+
}
|
|
5170
|
+
}
|
|
5171
|
+
for (const port of ownedPorts) {
|
|
5172
|
+
await releasePortReservation(this.ctx, { owner, port });
|
|
5173
|
+
this.portRegistry.unregister(port);
|
|
5174
|
+
}
|
|
5175
|
+
const purged = await this.launchJournal.purgeWhere((record) => residentOwner(record) === owner);
|
|
5176
|
+
const name = await freeDurableFacetSlot(this.ctx, owner);
|
|
5177
|
+
if (name !== null) {
|
|
5178
|
+
try {
|
|
5179
|
+
deleteFacetStorage(this.ctx, name);
|
|
5180
|
+
}
|
|
5181
|
+
catch { /* already gone */ }
|
|
5182
|
+
}
|
|
5183
|
+
return name !== null || purged > 0 || ownedPorts.size > 0;
|
|
5184
|
+
}
|
|
5185
|
+
/** The session-shaped view the public-directory helpers read env from. */
|
|
5186
|
+
_publicDirectoryHost() {
|
|
5187
|
+
return { env: this.env.rawEnv, ctx: this.ctx };
|
|
5188
|
+
}
|
|
5189
|
+
/**
|
|
5190
|
+
* Whether a request addressed to `port` can reach a durable application —
|
|
5191
|
+
* and, when the application is journaled but dead, drive its re-drive and
|
|
5192
|
+
* wait for the boot, bounded.
|
|
5193
|
+
*
|
|
5194
|
+
* The port request is the one surface a reset leaves dark: the alarm pump
|
|
5195
|
+
* re-drives journaled launches eventually, but a URL a user is holding
|
|
5196
|
+
* cannot wait for an alarm that may never fire. 'started' means a live
|
|
5197
|
+
* process owns the port now; 'absent' means nothing durable claims it
|
|
5198
|
+
* (the caller answers 502 as it always has); 'failed' means a re-drive
|
|
5199
|
+
* ran and lost, or outlived its bound — the caller answers 503 and lets
|
|
5200
|
+
* the page re-ask.
|
|
5201
|
+
*
|
|
5202
|
+
* Single-flight per port: parallel requests on a woken page share one
|
|
5203
|
+
* ensure, which shares the journal's per-row drive with recovery — a
|
|
5204
|
+
* request that lands mid-recovery waits on that boot, it never boots a
|
|
5205
|
+
* second process.
|
|
5206
|
+
*/
|
|
5207
|
+
ensureDurableAppOnPort(port) {
|
|
5208
|
+
let inflight = this.ensureInflight.get(port);
|
|
5209
|
+
if (inflight === undefined) {
|
|
5210
|
+
inflight = this._ensureDurableAppOnPort(port);
|
|
5211
|
+
this.ensureInflight.set(port, inflight);
|
|
5212
|
+
inflight.finally(() => {
|
|
5213
|
+
if (this.ensureInflight.get(port) === inflight)
|
|
5214
|
+
this.ensureInflight.delete(port);
|
|
5215
|
+
});
|
|
5216
|
+
}
|
|
5217
|
+
return inflight;
|
|
5218
|
+
}
|
|
5219
|
+
async _ensureDurableAppOnPort(port) {
|
|
5220
|
+
if (this.portRegistry.has(port))
|
|
5221
|
+
return 'started';
|
|
5222
|
+
// The reservation says the port means something to someone; the journal
|
|
5223
|
+
// row is what makes it a launch this instance owes. A reservation with
|
|
5224
|
+
// no row (the spawn died before journalling, or the app was removed and
|
|
5225
|
+
// the release already ran) answers absent — nothing here can bring back
|
|
5226
|
+
// an application with no recipe.
|
|
5227
|
+
const reservation = await readPortReservation(this.ctx, port);
|
|
5228
|
+
if (reservation === null || reservation.owner === null)
|
|
5229
|
+
return 'absent';
|
|
5230
|
+
const rows = await this.launchJournal.rows();
|
|
5231
|
+
// The reservation names the owner; the row's stamped port names which
|
|
5232
|
+
// resident serves it — kind-agnostic, so a node resident that bound the
|
|
5233
|
+
// port re-drives the same way a durable worker launch does.
|
|
5234
|
+
const entry = [...rows].find(([, record]) => record.port === port && residentOwner(record) === reservation.owner);
|
|
5235
|
+
if (entry === undefined)
|
|
5236
|
+
return 'absent';
|
|
5237
|
+
const [rowKey, record] = entry;
|
|
5238
|
+
if (record.pid > this.processes.pidBase) {
|
|
5239
|
+
// This instance's own row: the launch is already building — waiting
|
|
5240
|
+
// for its port registration is the entire ask, and driving the row
|
|
5241
|
+
// again would boot a second copy.
|
|
5242
|
+
return (await this._waitForPort(port, DURABLE_ENSURE_BOOT_BUDGET_MS))
|
|
5243
|
+
? 'started' : 'failed';
|
|
5244
|
+
}
|
|
5245
|
+
const { promise: boundHit, resolve: markBound } = withResolvers();
|
|
5246
|
+
setTimeout(() => markBound(true), DURABLE_ENSURE_BOOT_BUDGET_MS);
|
|
5247
|
+
const failed = await Promise.race([
|
|
5248
|
+
this.launchJournal.drive(rowKey, record),
|
|
5249
|
+
boundHit,
|
|
5250
|
+
]);
|
|
5251
|
+
if (failed)
|
|
5252
|
+
return 'failed';
|
|
5253
|
+
// A settled drive either owns the port or deliberately answered 'gone':
|
|
5254
|
+
// a resolver's null superseded the row, which is absent, not failed.
|
|
5255
|
+
return this.portRegistry.has(port) ? 'started' : 'absent';
|
|
5256
|
+
}
|
|
5257
|
+
/** Poll for a port registration the in-flight launch has not made yet. */
|
|
5258
|
+
async _waitForPort(port, budgetMs) {
|
|
5259
|
+
const deadline = Date.now() + budgetMs;
|
|
5260
|
+
while (Date.now() < deadline) {
|
|
5261
|
+
if (this.portRegistry.has(port))
|
|
5262
|
+
return true;
|
|
5263
|
+
const { promise: tick, resolve: tickDone } = withResolvers();
|
|
5264
|
+
setTimeout(tickDone, 50);
|
|
5265
|
+
await tick;
|
|
5266
|
+
}
|
|
5267
|
+
return this.portRegistry.has(port);
|
|
5268
|
+
}
|
|
4807
5269
|
get stats() { return this.processes.stats; }
|
|
4808
5270
|
}
|