@nimbus-sh/worker 0.9.0 → 0.11.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/NOTICE.md +5 -0
- package/README.md +10 -0
- package/dist/_shared/session-router.d.ts +7 -0
- package/dist/_shared/session-router.d.ts.map +1 -1
- package/dist/_shared/session-router.js +7 -0
- package/dist/bindings/body.d.ts +6 -0
- package/dist/bindings/body.d.ts.map +1 -0
- package/dist/bindings/body.js +37 -0
- package/dist/bindings/kv.d.ts +0 -2
- package/dist/bindings/kv.d.ts.map +1 -1
- package/dist/bindings/kv.js +3 -42
- package/dist/bindings/r2.d.ts +0 -2
- package/dist/bindings/r2.d.ts.map +1 -1
- package/dist/bindings/r2.js +4 -44
- package/dist/esbuild-cli-artifact.generated.d.ts +18 -0
- package/dist/esbuild-cli-artifact.generated.d.ts.map +1 -0
- package/dist/esbuild-cli-artifact.generated.js +17 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +24 -24
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -1
- package/dist/esbuild-wasm-bundle.generated.js +24 -24
- package/dist/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +11 -6
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -31
- package/dist/facets/data-plan.d.ts +79 -0
- package/dist/facets/data-plan.d.ts.map +1 -0
- package/dist/facets/data-plan.js +477 -0
- package/dist/facets/esbuild-bundle-pool.d.ts +1 -1
- package/dist/facets/esbuild-bundle-pool.js +3 -3
- package/dist/facets/esbuild-transform.d.ts +38 -8
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +219 -19
- package/dist/facets/exec-telemetry.d.ts +7 -3
- package/dist/facets/exec-telemetry.d.ts.map +1 -1
- package/dist/facets/manager.d.ts +198 -69
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1276 -652
- package/dist/facets/opencode-staging.d.ts +0 -2
- package/dist/facets/opencode-staging.d.ts.map +1 -1
- package/dist/facets/opencode-staging.js +5 -11
- package/dist/facets/process.d.ts +4 -0
- package/dist/facets/process.d.ts.map +1 -1
- package/dist/facets/process.js +3 -0
- package/dist/facets/read-profile.d.ts +149 -0
- package/dist/facets/read-profile.d.ts.map +1 -0
- package/dist/facets/read-profile.js +483 -0
- package/dist/facets/real-vite-hmr.d.ts +2 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -1
- package/dist/facets/real-vite-hmr.js +3 -2
- package/dist/facets/resident-identity.d.ts +0 -1
- package/dist/facets/resident-identity.d.ts.map +1 -1
- package/dist/facets/resident-identity.js +0 -3
- package/dist/facets/wasm-swap-registry.d.ts +34 -25
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -1
- package/dist/facets/wasm-swap-registry.js +62 -56
- package/dist/facets/ws-terminal.d.ts +4 -4
- package/dist/facets/ws-terminal.d.ts.map +1 -1
- package/dist/facets/ws-terminal.js +4 -5
- package/dist/git/commands.d.ts +33 -10
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1891 -197
- package/dist/git/network-facet.d.ts +12 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +69 -18
- package/dist/git/unified-diff.d.ts +74 -0
- package/dist/git/unified-diff.d.ts.map +1 -0
- package/dist/git/unified-diff.js +676 -0
- package/dist/git-bundle.generated.d.ts +7 -11
- package/dist/git-bundle.generated.d.ts.map +1 -1
- package/dist/git-bundle.generated.js +7 -11
- package/dist/hosted/commands.d.ts.map +1 -1
- package/dist/hosted/commands.js +64 -245
- package/dist/hosted/runtime.d.ts +22 -15
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +72 -14
- package/dist/hosted/services.d.ts +8 -1
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +12 -13
- package/dist/hosted/session.d.ts +165 -0
- package/dist/hosted/session.d.ts.map +1 -0
- package/dist/hosted/session.js +132 -0
- package/dist/loaders/generated-workers.d.ts +6 -3
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +8 -5
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- package/dist/loaders/pre-bundle-preamble.d.ts +6 -3
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -1
- package/dist/loaders/pre-bundle-preamble.js +13 -8
- package/dist/node-shims-artifact.generated.d.ts +13 -4
- package/dist/node-shims-artifact.generated.d.ts.map +1 -1
- package/dist/node-shims-artifact.generated.js +16 -7
- package/dist/npm/bin-links.d.ts +3 -1
- package/dist/npm/bin-links.d.ts.map +1 -1
- package/dist/npm/bin-links.js +32 -8
- package/dist/npm/cache.d.ts +11 -1
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +21 -7
- package/dist/npm/install-batch-facet.d.ts +4 -2
- package/dist/npm/install-batch-facet.d.ts.map +1 -1
- package/dist/npm/install-batch-facet.js +32 -18
- package/dist/npm/installer.d.ts +23 -3
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +330 -131
- package/dist/npm/npx-install.d.ts +3 -1
- package/dist/npm/npx-install.d.ts.map +1 -1
- package/dist/npm/npx-install.js +4 -1
- package/dist/npm/package-lock.d.ts +32 -0
- package/dist/npm/package-lock.d.ts.map +1 -0
- package/dist/npm/package-lock.js +65 -0
- package/dist/npm/placement.d.ts +16 -0
- package/dist/npm/placement.d.ts.map +1 -0
- package/dist/npm/placement.js +33 -0
- package/dist/npm/pre-bundle-facet.d.ts +1 -30
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -1
- package/dist/npm/pre-bundle-facet.js +3 -5
- package/dist/npm/r2-cache.d.ts +12 -6
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +34 -16
- package/dist/npm/resolve-one-facet.d.ts +3 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +15 -3
- package/dist/npm/resolver.d.ts +11 -13
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +12 -25
- package/dist/opencode-artifact.generated.js +2 -2
- package/dist/replica/routing.d.ts +2 -38
- package/dist/replica/routing.d.ts.map +1 -1
- package/dist/replica/routing.js +3 -55
- package/dist/router/index.d.ts.map +1 -1
- package/dist/router/index.js +15 -2
- package/dist/router/public-directory.d.ts +18 -0
- package/dist/router/public-directory.d.ts.map +1 -1
- package/dist/router/public-directory.js +1 -1
- package/dist/router/remote-api.d.ts +3 -1
- package/dist/router/remote-api.d.ts.map +1 -1
- package/dist/router/remote-api.js +42 -79
- package/dist/runtime/bash-repl.js +2 -2
- package/dist/runtime/bun-repl.d.ts.map +1 -1
- package/dist/runtime/bun-repl.js +2 -13
- package/dist/runtime/cpython-resident.d.ts.map +1 -1
- package/dist/runtime/cpython-resident.js +2 -10
- package/dist/runtime/esbuild-wasm-bytes.d.ts +37 -18
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +66 -92
- package/dist/runtime/facet-loader-host.d.ts.map +1 -1
- package/dist/runtime/facet-loader-host.js +0 -7
- package/dist/runtime/git-bundle-artifact.d.ts +17 -0
- package/dist/runtime/git-bundle-artifact.d.ts.map +1 -0
- package/dist/runtime/git-bundle-artifact.js +25 -0
- package/dist/runtime/node-repl.d.ts.map +1 -1
- package/dist/runtime/node-repl.js +2 -13
- package/dist/runtime/node-shims-artifact.d.ts +22 -23
- package/dist/runtime/node-shims-artifact.d.ts.map +1 -1
- package/dist/runtime/node-shims-artifact.js +48 -78
- package/dist/runtime/node-shims.d.ts.map +1 -1
- package/dist/runtime/node-shims.js +2358 -996
- package/dist/runtime/opencode-artifact.d.ts.map +1 -1
- package/dist/runtime/opencode-artifact.js +17 -53
- package/dist/runtime/opencode-facet-runner.d.ts +3 -6
- package/dist/runtime/opencode-facet-runner.d.ts.map +1 -1
- package/dist/runtime/opencode-facet-runner.js +45 -10
- package/dist/runtime/opentui-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/opentui-wasm-bytes.js +16 -49
- package/dist/runtime/package-manager.d.ts +0 -2
- package/dist/runtime/package-manager.d.ts.map +1 -1
- package/dist/runtime/package-manager.js +0 -3
- package/dist/runtime/python-repl.d.ts.map +1 -1
- package/dist/runtime/python-repl.js +4 -35
- package/dist/runtime/repl-session.d.ts +10 -0
- package/dist/runtime/repl-session.d.ts.map +1 -1
- package/dist/runtime/repl-session.js +7 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -1
- package/dist/runtime/ruby-repl.js +2 -2
- package/dist/runtime/ruby-resident.d.ts.map +1 -1
- package/dist/runtime/ruby-resident.js +41 -51
- package/dist/runtime/runtime-catalog.d.ts +10 -3
- package/dist/runtime/runtime-catalog.d.ts.map +1 -1
- package/dist/runtime/runtime-catalog.js +125 -15
- package/dist/runtime/sqlite-wasm-bytes.d.ts +4 -4
- package/dist/runtime/sqlite-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/sqlite-wasm-bytes.js +21 -57
- package/dist/runtime/staged-source.d.ts +67 -0
- package/dist/runtime/staged-source.d.ts.map +1 -0
- package/dist/runtime/staged-source.js +113 -0
- package/dist/runtime/wasi/preamble.d.ts +0 -22
- package/dist/runtime/wasi/preamble.d.ts.map +1 -1
- package/dist/runtime/wasi/preamble.js +98 -1425
- package/dist/runtime-catalog.generated.js +1 -1
- package/dist/session/agent.d.ts +14 -2
- package/dist/session/agent.d.ts.map +1 -1
- package/dist/session/agent.js +57 -19
- package/dist/session/ai.d.ts +0 -11
- package/dist/session/ai.d.ts.map +1 -1
- package/dist/session/ai.js +0 -4
- package/dist/session/fs-watch.d.ts +0 -11
- package/dist/session/fs-watch.d.ts.map +1 -1
- package/dist/session/fs-watch.js +0 -21
- package/dist/session/hibernation.d.ts +68 -2
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +95 -1
- package/dist/session/init-phases.d.ts +1 -2
- package/dist/session/init-phases.d.ts.map +1 -1
- package/dist/session/init-phases.js +1 -2
- package/dist/session/init.d.ts.map +1 -1
- package/dist/session/init.js +7 -20
- package/dist/session/keys.d.ts +0 -3
- package/dist/session/keys.d.ts.map +1 -1
- package/dist/session/keys.js +0 -3
- package/dist/session/legacy-reset.d.ts +11 -0
- package/dist/session/legacy-reset.d.ts.map +1 -0
- package/dist/session/legacy-reset.js +16 -0
- package/dist/session/nimbus-session.d.ts +50 -16
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +77 -71
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +2 -0
- package/dist/session/programmatic.d.ts +13 -18
- package/dist/session/programmatic.d.ts.map +1 -1
- package/dist/session/programmatic.js +53 -39
- package/dist/session/replica-routes.d.ts +2 -3
- package/dist/session/replica-routes.d.ts.map +1 -1
- package/dist/session/replica-routes.js +3 -10
- package/dist/session/routes.d.ts.map +1 -1
- package/dist/session/routes.js +11 -12
- package/dist/session/rpc.d.ts +108 -21
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +194 -39
- package/dist/session/start-real-vite.js +2 -2
- package/dist/session/state-store.d.ts +1 -17
- package/dist/session/state-store.d.ts.map +1 -1
- package/dist/session/state-store.js +3 -53
- package/dist/session/supervisor-op.d.ts +7 -0
- package/dist/session/supervisor-op.d.ts.map +1 -1
- package/dist/session/supervisor-op.js +6 -1
- package/dist/session/supervisor-rpc.d.ts +116 -19
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +194 -84
- package/dist/session/vite-command.d.ts.map +1 -1
- package/dist/session/vite-command.js +17 -14
- package/dist/session/ws.d.ts.map +1 -1
- package/dist/session/ws.js +20 -8
- package/dist/shell/npm-bin-entrypoints.d.ts.map +1 -1
- package/dist/shell/npm-bin-entrypoints.js +30 -5
- package/dist/vfs/facet-resident-limits.d.ts +33 -0
- package/dist/vfs/facet-resident-limits.d.ts.map +1 -0
- package/dist/vfs/facet-resident-limits.js +33 -0
- package/dist/vfs/facet-resident-store.d.ts +2 -10
- package/dist/vfs/facet-resident-store.d.ts.map +1 -1
- package/dist/vfs/facet-resident-store.js +1594 -219
- package/dist/workspace-host.d.ts +3 -1
- package/dist/workspace-host.d.ts.map +1 -1
- package/dist/workspace-host.js +2 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +0 -2
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -1
- package/dist/wrangler/nimbus-wrangler.js +8 -22
- package/package.json +34 -7
- package/public/_assets/esbuild-0.24.2.js +2380 -0
- package/public/_assets/opencode/1.16.2/index-attach.js +2 -2
- package/public/_assets/runtime/esbuild-cli-c4fda380d52befc1.js +6892 -0
- package/public/_assets/runtime/git-de3e60c9f3ff006a.js +103 -0
- package/public/_assets/runtime/{node-shims-acaa037e6d26ec0d.js → node-shims-06565b121555c194.js} +2879 -992
- package/public/_assets/runtime/resident-store-6ffb1939c53d982f.js +2264 -0
- package/public/_assets/runtime/vfs-write-ledger-e501e8fc793bc2c1.js +889 -0
- package/scripts/build-opencode-attach-entry.mjs +51 -1
- package/scripts/bundle-esbuild-wasm.mjs +55 -39
- package/scripts/bundle-facet-workers.mjs +159 -10
- package/scripts/bundle-git.mjs +88 -27
- package/scripts/bundle-node-shims.mjs +72 -46
- package/scripts/bundle-runtime.mjs +18 -421
- package/scripts/cf-git-patch.mjs +137 -0
- package/scripts/patch-install-deps.mjs +49 -96
- package/scripts/runtime-specs.mjs +435 -0
- package/vendor/git-http.generated.d.ts +82 -0
- package/vendor/git-upstream.generated.d.ts +4378 -0
- package/vendor/git.LICENSE +7 -0
- package/vendor/git.generated.d.mts +21 -0
- package/vendor/git.generated.mjs +103 -0
- package/dist/replica/suspension.d.ts +0 -36
- package/dist/replica/suspension.d.ts.map +0 -1
- package/dist/replica/suspension.js +0 -49
- package/dist/runtime/static-server.d.ts +0 -3
- package/dist/runtime/static-server.d.ts.map +0 -1
- package/dist/runtime/static-server.js +0 -133
- package/scripts/clean-dist.mjs +0 -7
package/dist/facets/manager.js
CHANGED
|
@@ -15,29 +15,38 @@
|
|
|
15
15
|
* Long-running processes use a dynamic Worker entrypoint that stays
|
|
16
16
|
* registered in ProcessTable and PortRegistry until exit or kill.
|
|
17
17
|
*/
|
|
18
|
-
import {
|
|
18
|
+
import { MK_COMPILED_FN_SOURCE } from '@nimbus-sh/core/_shared/compiled-fn.js';
|
|
19
|
+
import { exitCodeForSignal } from '@nimbus-sh/core/substrate/lifo/shell/signals.js';
|
|
20
|
+
import { fetchNodeFacetSources } from '../runtime/node-shims-artifact.js';
|
|
19
21
|
import { generateSqliteFacetPreamble } from '../runtime/sqlite-shim.js';
|
|
20
22
|
import { getRealNodeImportsCode } from '@nimbus-sh/core/_shared/real-node-imports.js';
|
|
21
|
-
import { FACET_RESIDENT_STORE_SOURCE } from '../vfs/facet-resident-store.js';
|
|
22
23
|
import { VFS_CURSOR_SEED_SOURCE, serializeFacetVfsCursor, } from '@nimbus-sh/core/_shared/facet-vfs-cursor.js';
|
|
23
|
-
import {
|
|
24
|
-
import { ExecutionFs } from '@nimbus-sh/core/shell/execution-fs.js';
|
|
24
|
+
import { typescriptLoader } from '@nimbus-sh/core/_shared/typescript-specifiers.js';
|
|
25
25
|
import { stripLeadingSlashes, vfsPathExtension } from '@nimbus-sh/core/vfs/path.js';
|
|
26
26
|
import { clearPortCapability, listPortReservations, readPortReservation, readPortReservationByOwner, releasePortReservation, restoreReservedPortCapability, } from '../session/port-capability.js';
|
|
27
27
|
import { deriveResidentOwner } from './resident-identity.js';
|
|
28
28
|
import { z } from 'zod/v4';
|
|
29
29
|
import { RESIDENT_OWNER_KEY_PREFIX, DURABLE_IMAGES_KEY_PREFIX } from '../session/keys.js';
|
|
30
30
|
import { PORT_CAPABILITY_KEY_PREFIX } from '../session/keys.js';
|
|
31
|
-
import { unbindPublicPortCapability } from '../router/public-directory.js';
|
|
32
|
-
import { prefetchForRequire, ClosureBoundExceededError } from '@nimbus-sh/core/runtime/require-resolver.js';
|
|
33
|
-
import { hasTopLevelModuleSyntax } from '@nimbus-sh/core/runtime/javascript-ast.js';
|
|
31
|
+
import { sessionIdentity, unbindPublicPortCapability } from '../router/public-directory.js';
|
|
32
|
+
import { prefetchForRequire, requireFsOverBridge, ClosureBoundExceededError } from '@nimbus-sh/core/runtime/require-resolver.js';
|
|
33
|
+
import { hasTopLevelModuleSyntax, parseJavaScriptModule } from '@nimbus-sh/core/runtime/javascript-ast.js';
|
|
34
|
+
import { findStaticFsReferences } from '@nimbus-sh/core/runtime/static-fs-refs.js';
|
|
35
|
+
import { planFacetData } from './data-plan.js';
|
|
36
|
+
import { principalTag, profilePrincipal, ReadProfile, verifiedEvidence, } from './read-profile.js';
|
|
37
|
+
/** What the shared read profile may add to one launch: an eighth of its module map's bytes. */
|
|
38
|
+
const READ_PROFILE_LAUNCH_BYTES = Math.floor(VFS_BUNDLE_MAX_BYTES / 8);
|
|
39
|
+
import { NpmCache } from '../npm/cache.js';
|
|
34
40
|
import { bindImportMetaResolve, importMetaDefines } from '@nimbus-sh/core/runtime/import-meta-transform.js';
|
|
41
|
+
import { mayHaveDynamicImport } from '@nimbus-sh/core/runtime/dynamic-import-rewrite.js';
|
|
35
42
|
import { recordFailure, getLastRpcFrame, getLastFacetId } from '@nimbus-sh/platform/oom-discriminator.js';
|
|
36
43
|
import { classifyError } from '@nimbus-sh/platform/oom-classify.js';
|
|
37
44
|
import { TurnBudget, PacedWork, turnChunkMaxBytes, withResolvers } from '@nimbus-sh/fabric/turn-budget.js';
|
|
38
45
|
import { onColdStart } from '@nimbus-sh/fabric/generation.js';
|
|
39
46
|
import { FencedWork, FENCED_WORK_KEY_PREFIX, } from '@nimbus-sh/fabric/fenced-work.js';
|
|
40
|
-
import {
|
|
47
|
+
import { rewriteBundledEsmToCjs, rewriteProvidedCommonJsModules, } from '@nimbus-sh/core/runtime/esbuild-service.js';
|
|
48
|
+
import { errorText } from '@nimbus-sh/core/_shared/error-text.js';
|
|
49
|
+
import { DEFAULT_CJS_CONDITIONS, DEFAULT_ESM_CONDITIONS, resolvePackageEntry, parseResolvablePackageJson, } from '@nimbus-sh/core/_shared/exports-resolver.js';
|
|
41
50
|
import { isExecDiagEnabled, recordExecTelemetry } from './exec-telemetry.js';
|
|
42
51
|
import { disposeRpcResource, disposeRpcResources } from '@nimbus-sh/platform/rpc-dispose.js';
|
|
43
52
|
import { sqliteWasmModuleEntry } from './opencode-staging.js';
|
|
@@ -49,13 +58,26 @@ import { persistDurableWorkerImage, purgeDurableWorkerImages, } from './durable-
|
|
|
49
58
|
import { SQLITE_WASM_MODULE_NAME, } from '../runtime/opencode-facet-runner.js';
|
|
50
59
|
import { parsePortFromArgv, resolveLongRunningPort } from '@nimbus-sh/core/runtime/long-running-handle.js';
|
|
51
60
|
import { DEFAULT_FACET_BUNDLE_PROFILE, } from '@nimbus-sh/core/runtime/bundle-profile.js';
|
|
52
|
-
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';
|
|
61
|
+
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, ESM_TRANSFORM_CACHE_MAX_BYTES, FS_LIST_PAGE_LIMIT, } from '@nimbus-sh/core/constants.js';
|
|
62
|
+
import { LEDGER_ROW_BYTES } from '@nimbus-sh/core/runtime/storage-ledger.js';
|
|
63
|
+
import { ONE_SHOT_STORE_MEMORY_BYTES, RESIDENT_CHUNK_BYTES } from '../vfs/facet-resident-limits.js';
|
|
53
64
|
import { MAX_RPC_SAFE_PAYLOAD_BYTES } from '@nimbus-sh/platform/limits.js';
|
|
54
|
-
import { CRED_KERNEL } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
65
|
+
import { CRED_KERNEL, isNativeBinPath } from '@nimbus-sh/core/runtime/os-contracts.js';
|
|
55
66
|
import { acquireSupervisorAllocation } from '@nimbus-sh/platform/heavy-alloc-coord.js';
|
|
67
|
+
import { supervisorBindingProps } from '@nimbus-sh/fabric/supervisor-props.js';
|
|
56
68
|
import { wasmImageDigest } from './wasm-image-digest.js';
|
|
57
|
-
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
|
|
58
|
-
const
|
|
69
|
+
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, setTransformCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
|
|
70
|
+
const launchAdapters = new WeakMap();
|
|
71
|
+
function filesOf(fs) {
|
|
72
|
+
let files = launchAdapters.get(fs);
|
|
73
|
+
if (files === undefined) {
|
|
74
|
+
files = requireFsOverBridge(fs);
|
|
75
|
+
launchAdapters.set(fs, files);
|
|
76
|
+
}
|
|
77
|
+
return files;
|
|
78
|
+
}
|
|
79
|
+
/** A bundled ESM file this large is rewritten to CJS without esbuild when its shape allows. */
|
|
80
|
+
const BUNDLED_ESM_REWRITE_MIN_BYTES = 512 * 1024;
|
|
59
81
|
/**
|
|
60
82
|
* Detect & restore a Uint8Array that's been JSON-mangled to a
|
|
61
83
|
* {"0":n,"1":n,...} object during the result-envelope round-trip.
|
|
@@ -281,6 +303,14 @@ const ENTRYPOINT_TIMER_TRACKER = `
|
|
|
281
303
|
g.setInterval = function(fn, ms, ...a){ const id = si(fn, ms, ...a); iv.add(id); g.__nimbusPendingTimers++; return id; };
|
|
282
304
|
g.clearInterval = function(id){ if (iv.delete(id)) g.__nimbusPendingTimers--; return ci(id); };
|
|
283
305
|
}
|
|
306
|
+
// process.exit: the program's pending timers and intervals never fire again.
|
|
307
|
+
g.__nimbusStopProgramTimers = function(){
|
|
308
|
+
for (const id of one) ct(id);
|
|
309
|
+
one.clear();
|
|
310
|
+
if (typeof ci === "function") for (const id of iv) ci(id);
|
|
311
|
+
iv.clear();
|
|
312
|
+
g.__nimbusPendingTimers = 0;
|
|
313
|
+
};
|
|
284
314
|
})(globalThis);
|
|
285
315
|
`;
|
|
286
316
|
/**
|
|
@@ -304,6 +334,11 @@ const ENTRYPOINT_TIMER_TRACKER = `
|
|
|
304
334
|
*/
|
|
305
335
|
const RESIDENCY_MISS_REPORT = `
|
|
306
336
|
const __NIMBUS_RESIDENCY_NAMED_MAX = 20;
|
|
337
|
+
// Everything the next launch of this entry must stage: unanswered reads and
|
|
338
|
+
// modules whose text arrived after boot, too late to compile.
|
|
339
|
+
function __nimbusStagingMisses() {
|
|
340
|
+
return [...(globalThis.__nimbusVfsResidencyMisses || []), ...(globalThis.__nimbusModuleMisses || [])];
|
|
341
|
+
}
|
|
307
342
|
function __nimbusResidencyMissReport() {
|
|
308
343
|
const __missed = globalThis.__nimbusVfsResidencyMisses;
|
|
309
344
|
if (!__missed || __missed.size === 0) return "";
|
|
@@ -370,11 +405,9 @@ const SQLITE_FACET_IMPORT = `import __nimbusSqliteWasmModule from "${SQLITE_WASM
|
|
|
370
405
|
/**
|
|
371
406
|
* Generate one-shot runtime code with a plain fetch handler.
|
|
372
407
|
*/
|
|
373
|
-
export async function generateEntrypointCode(userCode, vfsState, usesSqlite,
|
|
408
|
+
export async function generateEntrypointCode(userCode, vfsState, usesSqlite, sources, wasmImports = []) {
|
|
374
409
|
const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
|
|
375
410
|
const bundleSource = await facetVfsBundleSourceFor(vfsState);
|
|
376
|
-
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
377
|
-
const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
|
|
378
411
|
return {
|
|
379
412
|
code: `
|
|
380
413
|
${bundleSource.imports}
|
|
@@ -384,25 +417,7 @@ ${facetWasmImportsSource(wasmImports)}
|
|
|
384
417
|
const USER_CODE = ${safeCode};
|
|
385
418
|
const __NimbusHostResponse = globalThis.Response;
|
|
386
419
|
|
|
387
|
-
|
|
388
|
-
// Node strips a leading shebang from every module before evaluation;
|
|
389
|
-
// bin scripts are commonly bundled verbatim with their
|
|
390
|
-
// "#!/usr/bin/env node" line, which is a SyntaxError under new Function.
|
|
391
|
-
if (typeof code === "string" && code.charCodeAt(0) === 35 && code.charCodeAt(1) === 33) {
|
|
392
|
-
const __nl = code.indexOf("\\n");
|
|
393
|
-
code = __nl >= 0 ? code.slice(__nl + 1) : "";
|
|
394
|
-
}
|
|
395
|
-
function renameIfDeclared(name) {
|
|
396
|
-
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
|
|
397
|
-
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
398
|
-
}
|
|
399
|
-
const baseParams = [
|
|
400
|
-
"exports", renameIfDeclared("require"), "module",
|
|
401
|
-
renameIfDeclared("__filename"),
|
|
402
|
-
renameIfDeclared("__dirname"),
|
|
403
|
-
];
|
|
404
|
-
return new Function(...baseParams, code);
|
|
405
|
-
}
|
|
420
|
+
${MK_COMPILED_FN_SOURCE}
|
|
406
421
|
|
|
407
422
|
let __compiledFn = null;
|
|
408
423
|
let __entryCompileFailure = null;
|
|
@@ -412,15 +427,13 @@ try {
|
|
|
412
427
|
__entryCompileFailure = (__e && __e.stack) || (__e && __e.message) || String(__e);
|
|
413
428
|
}
|
|
414
429
|
|
|
415
|
-
//
|
|
430
|
+
// The module bundle and its pre-compiled modules, at module level (startup time).
|
|
416
431
|
const __MODULE_VFS_BUNDLE = ${bundleSource.expression};
|
|
417
|
-
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
418
|
-
const __MODULE_VFS_METADATA = ${safeMetadata};
|
|
419
432
|
const __compiledModules = new Map();
|
|
420
433
|
const __compileFailures = new Map();
|
|
421
434
|
// Precompile JS modules AND extensionless CJS entries (bin scripts and
|
|
422
435
|
// shims). workerd forbids new Function at request time, so anything not
|
|
423
|
-
// precompiled here surfaces the misleading "
|
|
436
|
+
// precompiled here surfaces the misleading "not in this launch's module map".
|
|
424
437
|
${BUNDLE_PRECOMPILE_LOOP}
|
|
425
438
|
|
|
426
439
|
class __ProcessExit extends Error {
|
|
@@ -430,7 +443,7 @@ class __ProcessExit extends Error {
|
|
|
430
443
|
export default {
|
|
431
444
|
async fetch(request, workerEnv) {
|
|
432
445
|
const args = await request.json();
|
|
433
|
-
const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag, vfsCursor } = args;
|
|
446
|
+
const { argv, env, cwd: _cwd, filename, dirname, stdin, captureOutput, cred, diag: __diag, vfsCursor, dataPlan } = args;
|
|
434
447
|
// Per invocation, not per module: this body is cached on
|
|
435
448
|
// hash(code + bundle + manifest) and reused by any session whose snapshot
|
|
436
449
|
// hashes the same, and epochs are per supervisor incarnation.
|
|
@@ -441,10 +454,30 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
441
454
|
// ends it early.
|
|
442
455
|
const __entryBudgetMs = Infinity;
|
|
443
456
|
let __drainPasses = 0;
|
|
444
|
-
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
445
|
-
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
446
|
-
const __vfsMetadata = __MODULE_VFS_METADATA;
|
|
447
457
|
const __supervisor = workerEnv?.SUPERVISOR || null;
|
|
458
|
+
// The same store, namespace and data plan a resident boots on, backed by
|
|
459
|
+
// this run's heap (runOnce hosts no SQLite; vfs/facet-resident-store.ts).
|
|
460
|
+
// Declared inside the request, beside the shims, so a loader that reuses
|
|
461
|
+
// this isolate for another run starts it from an empty store.
|
|
462
|
+
${sources.residentStore}
|
|
463
|
+
__residentBindInMemory(${ONE_SHOT_STORE_MEMORY_BYTES});
|
|
464
|
+
__residentSetStorage(undefined, __supervisor);
|
|
465
|
+
__nsSetCred(cred);
|
|
466
|
+
__residentSetPlan(dataPlan);
|
|
467
|
+
__residentSetPushRoots([_cwd || "/home/user", "/tmp"]);
|
|
468
|
+
// The module bundle stays at module scope: a reused isolate adopts it
|
|
469
|
+
// again on its next run, and its text cells are shared, not copied.
|
|
470
|
+
const __residentBooted = await __residentBoot(() => __MODULE_VFS_BUNDLE, __MODULE_VFS_CURSOR, __supervisor);
|
|
471
|
+
if (__residentBooted.cursor) {
|
|
472
|
+
globalThis.__nimbusVfsCursor = { epoch: __residentBooted.cursor.epoch, rev: __residentBooted.cursor.rev };
|
|
473
|
+
}
|
|
474
|
+
// No user code runs before the namespace answers: it is the view every
|
|
475
|
+
// synchronous stat, exists and readdir reads (CUTOVER #13).
|
|
476
|
+
const __namespaceFailure = await __residentRequireNamespace(__supervisor, __residentBooted.failure);
|
|
477
|
+
if (__namespaceFailure) {
|
|
478
|
+
return __NimbusHostResponse.json({ exitCode: 1, stdout: "", stderr: __namespaceFailure + "\\n", vfsWrites: {}, residencyMisses: [] });
|
|
479
|
+
}
|
|
480
|
+
const __vfsBundle = __nimbusResidentBundle;
|
|
448
481
|
const __pendingIO = [];
|
|
449
482
|
// Fix 6 orphan counters (same as NodeProcess.run) — count RPC writes
|
|
450
483
|
// that get dropped during isolate teardown so reportExit can report them.
|
|
@@ -471,24 +504,24 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
471
504
|
let stdout = "", stderr = "";
|
|
472
505
|
let exitCode = 0;
|
|
473
506
|
const __nimbusDeferProcessExitReport = true;
|
|
474
|
-
${
|
|
507
|
+
${sources.ledger}
|
|
475
508
|
const __vfsDirs = {};
|
|
476
509
|
|
|
477
510
|
${ENTRYPOINT_TIMER_TRACKER}
|
|
478
|
-
${shims}
|
|
511
|
+
${sources.shims}
|
|
479
512
|
|
|
480
513
|
${ENTRYPOINT_EVENT_LOOP}
|
|
481
514
|
${RESIDENCY_MISS_REPORT}
|
|
482
515
|
|
|
483
516
|
// Override console AND process.stdout/stderr for live SUPERVISOR streaming
|
|
484
517
|
if (__supervisor && !captureOutput) {
|
|
485
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
486
|
-
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
518
|
+
__consoleMod.log = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
519
|
+
__consoleMod.error = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
487
520
|
__consoleMod.warn = __consoleMod.error;
|
|
488
521
|
__consoleMod.info = __consoleMod.log;
|
|
489
522
|
__consoleMod.debug = __consoleMod.log;
|
|
490
|
-
__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; };
|
|
491
|
-
__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; };
|
|
523
|
+
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
524
|
+
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
492
525
|
}
|
|
493
526
|
|
|
494
527
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -532,6 +565,12 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
532
565
|
}
|
|
533
566
|
}
|
|
534
567
|
}
|
|
568
|
+
// A program that ended without process.exit still gets its 'exit' event.
|
|
569
|
+
if (__nimbusProcessExitCode === null) {
|
|
570
|
+
__nimbusEmitExit(exitCode);
|
|
571
|
+
if (__nimbusProcessExitCode !== null) exitCode = __nimbusProcessExitCode;
|
|
572
|
+
__nimbusProgramStopped = true;
|
|
573
|
+
}
|
|
535
574
|
|
|
536
575
|
async function __drainPendingIO(maxPasses = 12) {
|
|
537
576
|
let __settledIO = 0;
|
|
@@ -602,8 +641,11 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
602
641
|
// Unconditional, unlike diag: the supervisor stages these paths into
|
|
603
642
|
// the next bundle for the same entry, so withholding them behind a
|
|
604
643
|
// debug flag would leave the miss to repeat forever.
|
|
605
|
-
residencyMisses:
|
|
606
|
-
...(__diag ? { diag: {
|
|
644
|
+
residencyMisses: __nimbusStagingMisses(),
|
|
645
|
+
...(__diag ? { diag: {
|
|
646
|
+
drainPasses: __drainPasses, rpcWrites: __rpcWriteCount, fsRpcReads: globalThis.__nimbusFsRpcReads || 0,
|
|
647
|
+
namespaceRefusals: (globalThis.__nimbusVfsCoherence && globalThis.__nimbusVfsCoherence.namespaceRefusals) || 0,
|
|
648
|
+
} } : {}),
|
|
607
649
|
});
|
|
608
650
|
}
|
|
609
651
|
};
|
|
@@ -673,7 +715,7 @@ function facetWasmImportsSource(wasmImports) {
|
|
|
673
715
|
return `${lines.join('\n')}\nglobalThis.__nimbusPrecompiledWasm = new Map([${entries.join(', ')}]);`
|
|
674
716
|
+ `\nglobalThis.__nimbusPrecompiledWasmByDigest = new Map([${byDigest.join(', ')}]);`;
|
|
675
717
|
}
|
|
676
|
-
export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite,
|
|
718
|
+
export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite, sources, pacer) {
|
|
677
719
|
const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
|
|
678
720
|
const safeArgs = JSON.stringify({
|
|
679
721
|
argv: opts.argv || [],
|
|
@@ -686,8 +728,6 @@ export async function generateLongRunningNodeCode(userCode, vfsState, opts, uses
|
|
|
686
728
|
cred: opts.cred,
|
|
687
729
|
});
|
|
688
730
|
const bundleSource = await facetVfsBundleSourceFor(vfsState, pacer);
|
|
689
|
-
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
690
|
-
const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
|
|
691
731
|
return {
|
|
692
732
|
code: `
|
|
693
733
|
${bundleSource.imports}
|
|
@@ -699,18 +739,7 @@ const USER_CODE = ${safeCode};
|
|
|
699
739
|
const __NIMBUS_ARGS = ${safeArgs};
|
|
700
740
|
const __NimbusHostResponse = globalThis.Response;
|
|
701
741
|
|
|
702
|
-
|
|
703
|
-
function renameIfDeclared(name) {
|
|
704
|
-
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
|
|
705
|
-
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
706
|
-
}
|
|
707
|
-
const baseParams = [
|
|
708
|
-
"exports", renameIfDeclared("require"), "module",
|
|
709
|
-
renameIfDeclared("__filename"),
|
|
710
|
-
renameIfDeclared("__dirname"),
|
|
711
|
-
];
|
|
712
|
-
return new Function(...baseParams, code);
|
|
713
|
-
}
|
|
742
|
+
${MK_COMPILED_FN_SOURCE}
|
|
714
743
|
|
|
715
744
|
let __compiledFn = null;
|
|
716
745
|
let __entryCompileFailure = null;
|
|
@@ -725,8 +754,6 @@ try {
|
|
|
725
754
|
// and this object are the same bytes twice, and for pi that is the largest
|
|
726
755
|
// single allocation in the facet before the program starts.
|
|
727
756
|
let __MODULE_VFS_BUNDLE = ${bundleSource.expression};
|
|
728
|
-
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
729
|
-
const __MODULE_VFS_METADATA = ${safeMetadata};
|
|
730
757
|
const __compiledModules = new Map();
|
|
731
758
|
const __compileFailures = new Map();
|
|
732
759
|
// Module evaluation is the ONLY place workerd allows a string to become code —
|
|
@@ -735,7 +762,7 @@ const __compileFailures = new Map();
|
|
|
735
762
|
// here, off the module map. That is why code cannot come from the store.
|
|
736
763
|
${BUNDLE_PRECOMPILE_LOOP}
|
|
737
764
|
|
|
738
|
-
${
|
|
765
|
+
${sources.residentStore}
|
|
739
766
|
|
|
740
767
|
class __ProcessExit extends Error {
|
|
741
768
|
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
@@ -746,6 +773,15 @@ let __nimbusStarting = null;
|
|
|
746
773
|
let __nimbusRuntime = null;
|
|
747
774
|
let __nimbusAttachedLifecycle = null;
|
|
748
775
|
|
|
776
|
+
// The platform's timer, captured at module evaluation, before the shims wrap
|
|
777
|
+
// setTimeout in the resumption barrier. The flush below yields turns for its
|
|
778
|
+
// own bookkeeping; that is not a resumption of the program, and through the
|
|
779
|
+
// wrapped timer it paid a hidden ACQUIRE per yield — the one an inbound
|
|
780
|
+
// request relied on without saying so, and a second one after the response
|
|
781
|
+
// on every request. The request's barrier is taken explicitly, in the shared
|
|
782
|
+
// dispatch (__nimbusServeHttp), exactly once.
|
|
783
|
+
const __nimbusPlatformSetTimeout = setTimeout;
|
|
784
|
+
|
|
749
785
|
async function __nimbusFlushRuntime() {
|
|
750
786
|
const rt = __nimbusRuntime;
|
|
751
787
|
if (!rt) return;
|
|
@@ -765,7 +801,9 @@ async function __nimbusFlushRuntime() {
|
|
|
765
801
|
rt.drainVfsMutations(),
|
|
766
802
|
]);
|
|
767
803
|
for (let pass = 0; pass < 12; pass++) {
|
|
768
|
-
|
|
804
|
+
const turn = Promise.withResolvers();
|
|
805
|
+
__nimbusPlatformSetTimeout(turn.resolve, 0);
|
|
806
|
+
await turn.promise;
|
|
769
807
|
if (rt.pendingIO.length <= rt.settledIO) break;
|
|
770
808
|
const slice = rt.pendingIO.slice(rt.settledIO);
|
|
771
809
|
rt.settledIO = rt.pendingIO.length;
|
|
@@ -794,8 +832,6 @@ async function __nimbusEnsureStarted(workerEnv, workerCtx, __startArgs) {
|
|
|
794
832
|
// time. Same reason argv/env/pid want to move here.
|
|
795
833
|
const __MODULE_VFS_CURSOR = (__startArgs && __startArgs.vfsCursor) || null;
|
|
796
834
|
${VFS_CURSOR_SEED_SOURCE}
|
|
797
|
-
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
798
|
-
const __vfsMetadata = __MODULE_VFS_METADATA;
|
|
799
835
|
const __supervisor = workerEnv?.SUPERVISOR || null;
|
|
800
836
|
// The resident set lives in this facet's own SQLite rather than its heap.
|
|
801
837
|
// A synchronous read cannot block and no JS stack here can be suspended, so
|
|
@@ -803,41 +839,56 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
803
839
|
// \`ctx.storage.sql.exec\` returns a Cursor, not a Promise. See
|
|
804
840
|
// vfs/facet-resident-store.ts.
|
|
805
841
|
__residentBind(workerCtx);
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
//
|
|
810
|
-
//
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
//
|
|
817
|
-
// first synchronous read of an untouched file succeed, and it is the ONLY
|
|
818
|
-
// blocking step: the waiting is done once, here, before the program's
|
|
819
|
-
// first instruction, so that no synchronous read after it ever has to
|
|
820
|
-
// wait.
|
|
842
|
+
__residentSetStorage(__startArgs && __startArgs.storage, __supervisor);
|
|
843
|
+
__nsSetCred(cred);
|
|
844
|
+
__residentSetPlan(__startArgs && __startArgs.dataPlan);
|
|
845
|
+
// The shared read profile's files this launch staged; each is struck off
|
|
846
|
+
// when read, and what is left at exit is reported unread (read-profile.ts).
|
|
847
|
+
globalThis.__nimbusProfileStaged = new Set((__startArgs && __startArgs.profileStaged) || []);
|
|
848
|
+
__residentSetPushRoots([_cwd || "/home/user", "/tmp"]);
|
|
849
|
+
// Bring the store to the authority's current state before the program's
|
|
850
|
+
// first instruction. This is what makes a first synchronous read of an
|
|
851
|
+
// untouched file succeed, and it is the ONLY blocking step: the waiting is
|
|
852
|
+
// done once, here, so that no synchronous read after it ever has to wait.
|
|
821
853
|
//
|
|
822
|
-
//
|
|
823
|
-
//
|
|
824
|
-
//
|
|
825
|
-
//
|
|
826
|
-
//
|
|
827
|
-
//
|
|
828
|
-
|
|
854
|
+
// A cold store adopts this spawn's snapshot and fills the rest. A store
|
|
855
|
+
// that outlived its process — a durable application's, relaunched or
|
|
856
|
+
// re-driven — is cheap to bring current: rows the absolute listing proves
|
|
857
|
+
// current are kept, and only what changed is fetched. It is SERVED only
|
|
858
|
+
// once that listing has vouched for it: rows it cannot vouch for are a
|
|
859
|
+
// previous process's, of unknown age, so the kept store is emptied and
|
|
860
|
+
// this launch boots as a cold one does. See __residentBoot.
|
|
861
|
+
//
|
|
862
|
+
// The thunk hands the parsed bundle over and drops this scope's reference,
|
|
863
|
+
// so a cold boot frees it the moment it is adopted rather than holding it
|
|
864
|
+
// through the fill. A kept store that reconciles never takes it.
|
|
865
|
+
const __residentBootStarted = Date.now();
|
|
866
|
+
const __residentBooted = await __residentBoot(
|
|
867
|
+
() => { const __bundle = __MODULE_VFS_BUNDLE; __MODULE_VFS_BUNDLE = null; return __bundle; },
|
|
868
|
+
__MODULE_VFS_CURSOR,
|
|
869
|
+
__supervisor,
|
|
870
|
+
);
|
|
871
|
+
if (__startArgs && __startArgs.diag && __supervisor) {
|
|
872
|
+
// NIMBUS_DEBUG only: what this process holds, for per-process storage measurement.
|
|
829
873
|
try {
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
}
|
|
834
|
-
} catch
|
|
835
|
-
// A failed pass is a smaller resident set, not a dead process: every
|
|
836
|
-
// path it did not reach reads exactly as it would have without this
|
|
837
|
-
// store. Surfacing beats a silent capability loss.
|
|
838
|
-
try { globalThis.__nimbusResidentFillError = (__e && __e.message) || String(__e); } catch {}
|
|
839
|
-
}
|
|
874
|
+
const __stats = __residentStats();
|
|
875
|
+
await __supervisor.stderr(new TextEncoder().encode("[nimbus-debug] resident boot " + JSON.stringify({
|
|
876
|
+
ms: Date.now() - __residentBootStarted, ...__stats, failure: __residentBooted.failure || null,
|
|
877
|
+
}) + "\\n"));
|
|
878
|
+
} catch {}
|
|
840
879
|
}
|
|
880
|
+
__MODULE_VFS_BUNDLE = null;
|
|
881
|
+
// One cursor, not two. The seed above publishes the cursor this SPAWN
|
|
882
|
+
// staged at, which is right for a cold store and stale for a kept one —
|
|
883
|
+
// the cursor the boot returns describes what the rows actually are.
|
|
884
|
+
if (__residentBooted.cursor) {
|
|
885
|
+
globalThis.__nimbusVfsCursor = { epoch: __residentBooted.cursor.epoch, rev: __residentBooted.cursor.rev };
|
|
886
|
+
}
|
|
887
|
+
// No user code runs before the namespace answers: it is the view every
|
|
888
|
+
// synchronous stat, exists and readdir reads (CUTOVER #13). A launch whose
|
|
889
|
+
// namespace cannot be listed fails here, naming why.
|
|
890
|
+
const __namespaceFailure = await __residentRequireNamespace(__supervisor, __residentBooted.failure);
|
|
891
|
+
if (__namespaceFailure) throw new Error(__namespaceFailure);
|
|
841
892
|
const __vfsBundle = __nimbusResidentBundle;
|
|
842
893
|
const __pendingIO = [];
|
|
843
894
|
let __rpcDrops = 0;
|
|
@@ -863,23 +914,23 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
863
914
|
let stdout = "", stderr = "";
|
|
864
915
|
let exitCode = 0;
|
|
865
916
|
const __nimbusDeferProcessExitReport = true;
|
|
866
|
-
${
|
|
917
|
+
${sources.ledger}
|
|
867
918
|
const __vfsDirs = {};
|
|
868
919
|
|
|
869
920
|
${ENTRYPOINT_TIMER_TRACKER}
|
|
870
|
-
${shims}
|
|
921
|
+
${sources.shims}
|
|
871
922
|
|
|
872
923
|
${ENTRYPOINT_EVENT_LOOP}
|
|
873
924
|
${RESIDENCY_MISS_REPORT}
|
|
874
925
|
|
|
875
926
|
if (__supervisor && !captureOutput) {
|
|
876
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
877
|
-
__consoleMod.error = (...a) => { const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
927
|
+
__consoleMod.log = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
928
|
+
__consoleMod.error = (...a) => { if (__nimbusProgramStopped) return; const s = __utilMod.format(...a) + "\\n"; stderr += s; __queueRpcWrite("stderr", __nimbusOutEnc.encode(s)); };
|
|
878
929
|
__consoleMod.warn = __consoleMod.error;
|
|
879
930
|
__consoleMod.info = __consoleMod.log;
|
|
880
931
|
__consoleMod.debug = __consoleMod.log;
|
|
881
|
-
__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; };
|
|
882
|
-
__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; };
|
|
932
|
+
__processMod.stdout.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stdout += __nimbusOutText("stdout", b); __queueRpcWrite("stdout", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
933
|
+
__processMod.stderr.write = (d, enc, cb) => { if (typeof enc === "function") cb = enc; if (__nimbusProgramStopped) return true; const b = __nimbusOutBytes(d, enc); stderr += __nimbusOutText("stderr", b); __queueRpcWrite("stderr", b); if (typeof cb === "function") queueMicrotask(cb); return true; };
|
|
883
934
|
}
|
|
884
935
|
|
|
885
936
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -964,7 +1015,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
964
1015
|
if (Number(code ?? 0) === 0) code = 1;
|
|
965
1016
|
try { await __supervisor.stderr(__nimbusOutEnc.encode(__residencyReport)); } catch {}
|
|
966
1017
|
}
|
|
967
|
-
await __supervisor.reportExit(code, reason || "");
|
|
1018
|
+
await __supervisor.reportExit(code, reason || "", __nimbusStagingMisses(), [...(globalThis.__nimbusProfileStaged || [])]);
|
|
968
1019
|
__nimbusProcessExitReported = true;
|
|
969
1020
|
};
|
|
970
1021
|
const __nimbusReportLifecycleFailure = async (e) => {
|
|
@@ -996,6 +1047,11 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
996
1047
|
} else {
|
|
997
1048
|
finalCode = Number(await __nimbusProcessExitPromise);
|
|
998
1049
|
}
|
|
1050
|
+
if (__nimbusProcessExitCode === null) {
|
|
1051
|
+
__nimbusEmitExit(finalCode);
|
|
1052
|
+
if (__nimbusProcessExitCode !== null) finalCode = __nimbusProcessExitCode;
|
|
1053
|
+
__nimbusProgramStopped = true;
|
|
1054
|
+
}
|
|
999
1055
|
await __nimbusFlushRuntime();
|
|
1000
1056
|
await __nimbusReportFinalExit(finalCode, "");
|
|
1001
1057
|
})().catch(async (e) => {
|
|
@@ -1023,9 +1079,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
1023
1079
|
try { await __supervisor.stderr(__nimbusOutEnc.encode(tail)); } catch {}
|
|
1024
1080
|
}
|
|
1025
1081
|
if (exitCode !== 0) {
|
|
1026
|
-
|
|
1027
|
-
await __supervisor.reportExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
|
|
1028
|
-
}
|
|
1082
|
+
await __nimbusReportFinalExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
|
|
1029
1083
|
throw new Error(stderr || ("long-running node startup exited " + exitCode));
|
|
1030
1084
|
}
|
|
1031
1085
|
__nimbusStarted = true;
|
|
@@ -1061,7 +1115,13 @@ export class NimbusProcess extends DurableObject {
|
|
|
1061
1115
|
if (startArgs) __nimbusStartArgs = startArgs;
|
|
1062
1116
|
await __nimbusEnsureStarted(this.env, this.ctx, __nimbusStartArgs);
|
|
1063
1117
|
if (__nimbusAttachedLifecycle) await __nimbusAttachedLifecycle;
|
|
1064
|
-
|
|
1118
|
+
// What this facet's database holds now, and the cap its store keeps
|
|
1119
|
+
// under, for the session's storage ledger (N18).
|
|
1120
|
+
let databaseSize;
|
|
1121
|
+
try { databaseSize = this.ctx.storage.sql.databaseSize; } catch {}
|
|
1122
|
+
let storageCap = null;
|
|
1123
|
+
try { storageCap = __residentStorageCap(); } catch {}
|
|
1124
|
+
return { ok: true, databaseSize, storageCap };
|
|
1065
1125
|
}
|
|
1066
1126
|
async fetch(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
|
|
1067
1127
|
async handleHttpRequest(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
|
|
@@ -1070,13 +1130,33 @@ export class NimbusProcess extends DurableObject {
|
|
|
1070
1130
|
modules: bundleSource.modules,
|
|
1071
1131
|
};
|
|
1072
1132
|
}
|
|
1133
|
+
/**
|
|
1134
|
+
* What the facet store charges itself to hold one cell (facet-resident-store's
|
|
1135
|
+
* __residentCellCost, which this must match): its bytes (a text cell's UTF-8
|
|
1136
|
+
* length) plus 1%, a row per chunk and head, and a page of slack.
|
|
1137
|
+
*/
|
|
1138
|
+
/** What a module map costs the facet's store that adopts it (N18). A denial is a head row only. */
|
|
1139
|
+
function moduleMapStorageBytes(bundle) {
|
|
1140
|
+
let bytes = 0;
|
|
1141
|
+
for (const cell of Object.values(bundle)) {
|
|
1142
|
+
bytes += typeof cell === 'string' || cell instanceof Uint8Array ? residentCellCost(cell) : 2 * LEDGER_ROW_BYTES;
|
|
1143
|
+
}
|
|
1144
|
+
return bytes;
|
|
1145
|
+
}
|
|
1146
|
+
function residentCellCost(cell) {
|
|
1147
|
+
const isText = typeof cell === 'string';
|
|
1148
|
+
const size = isText ? cell.length : cell.byteLength;
|
|
1149
|
+
const limit = isText ? Math.floor(RESIDENT_CHUNK_BYTES / 3) : RESIDENT_CHUNK_BYTES;
|
|
1150
|
+
const chunks = Math.max(1, Math.ceil(size / limit));
|
|
1151
|
+
const bytes = isText ? new TextEncoder().encode(cell).byteLength : size;
|
|
1152
|
+
return Math.ceil(bytes * 1.01) + (chunks + 1) * LEDGER_ROW_BYTES + 4096;
|
|
1153
|
+
}
|
|
1073
1154
|
/**
|
|
1074
1155
|
* Drop the raw forms of everything that has been serialized, in place.
|
|
1075
1156
|
*
|
|
1076
|
-
* `bundleSource
|
|
1077
|
-
*
|
|
1078
|
-
*
|
|
1079
|
-
* both facet generators read the serialized halves and nothing else does.
|
|
1157
|
+
* `bundleSource` is a total encoding of `bundle`: no caller can distinguish
|
|
1158
|
+
* a state carrying both from one carrying only the serialized half, because
|
|
1159
|
+
* both facet generators read the serialized half and nothing else does.
|
|
1080
1160
|
* Holding both doubles the cost of a cached entry for its whole lifetime, and
|
|
1081
1161
|
* that lifetime spans execs.
|
|
1082
1162
|
*
|
|
@@ -1089,16 +1169,12 @@ export class NimbusProcess extends DurableObject {
|
|
|
1089
1169
|
export function releaseSerializedSources(vfsState) {
|
|
1090
1170
|
if (vfsState.bundleSource)
|
|
1091
1171
|
vfsState.bundle = {};
|
|
1092
|
-
if (vfsState.serializedManifest !== undefined)
|
|
1093
|
-
vfsState.manifest = {};
|
|
1094
|
-
if (vfsState.serializedMetadata !== undefined)
|
|
1095
|
-
vfsState.metadata = {};
|
|
1096
1172
|
}
|
|
1097
1173
|
/**
|
|
1098
1174
|
* Drop the serialized forms once a module map has been generated from them.
|
|
1099
1175
|
*
|
|
1100
|
-
* The generated source is a total encoding of
|
|
1101
|
-
*
|
|
1176
|
+
* The generated source is a total encoding of it: every byte of the bundle
|
|
1177
|
+
* expression is inside it. Holding them
|
|
1102
1178
|
* afterwards keeps a second copy of the largest thing this DO builds alive for
|
|
1103
1179
|
* as long as the facet runs — for pi, 22.7 MB across the ~20 s window in which
|
|
1104
1180
|
* the isolate was being reset. For the one-shot path that window is the run;
|
|
@@ -1115,8 +1191,6 @@ export function releaseSerializedSources(vfsState) {
|
|
|
1115
1191
|
*/
|
|
1116
1192
|
export function releaseGeneratedSources(vfsState) {
|
|
1117
1193
|
vfsState.bundleSource = undefined;
|
|
1118
|
-
vfsState.serializedManifest = undefined;
|
|
1119
|
-
vfsState.serializedMetadata = undefined;
|
|
1120
1194
|
vfsState.generatedSourcesReleased = true;
|
|
1121
1195
|
}
|
|
1122
1196
|
/**
|
|
@@ -1150,7 +1224,9 @@ async function facetVfsBundleSourceFor(vfsState, pacer) {
|
|
|
1150
1224
|
* pre-fix readFileString contract).
|
|
1151
1225
|
*/
|
|
1152
1226
|
async function _readBundleCell(vfs, path) {
|
|
1153
|
-
const bytes = await vfs.
|
|
1227
|
+
const bytes = await filesOf(vfs).readBytes(path);
|
|
1228
|
+
if (bytes === null)
|
|
1229
|
+
throw Object.assign(new Error(`ENOENT: ${path}`), { code: 'ENOENT', path });
|
|
1154
1230
|
try {
|
|
1155
1231
|
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1156
1232
|
}
|
|
@@ -1163,6 +1239,12 @@ async function _readBundleCell(vfs, path) {
|
|
|
1163
1239
|
* Strings counted as char-length (a slight under-count for non-ASCII
|
|
1164
1240
|
* but matches the pre-fix behaviour); Uint8Array counted as byteLength.
|
|
1165
1241
|
*/
|
|
1242
|
+
/** A cell's raw bytes: UTF-8 for text, as the file is on disk. */
|
|
1243
|
+
function _bundleCellRawBytes(cell) {
|
|
1244
|
+
if (cell instanceof Uint8Array)
|
|
1245
|
+
return cell.byteLength;
|
|
1246
|
+
return typeof cell === 'string' ? _encodedSourceBytes(cell) : 0;
|
|
1247
|
+
}
|
|
1166
1248
|
function _bundleCellLength(cell) {
|
|
1167
1249
|
return typeof cell === 'string' ? cell.length : cell.byteLength;
|
|
1168
1250
|
}
|
|
@@ -1178,13 +1260,10 @@ function _bundleCellLength(cell) {
|
|
|
1178
1260
|
* BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES); omit it for budget-only checks.
|
|
1179
1261
|
*/
|
|
1180
1262
|
async function _bundleAdmits(vfs, path, budgetState, perFileCeiling) {
|
|
1181
|
-
|
|
1182
|
-
|
|
1183
|
-
size = (await vfs.lstat(path)).size;
|
|
1184
|
-
}
|
|
1185
|
-
catch {
|
|
1263
|
+
const stat = await filesOf(vfs).lstat(path).catch(() => null);
|
|
1264
|
+
if (stat === null)
|
|
1186
1265
|
return false;
|
|
1187
|
-
|
|
1266
|
+
const size = stat.size;
|
|
1188
1267
|
if (perFileCeiling !== undefined && size > perFileCeiling)
|
|
1189
1268
|
return false;
|
|
1190
1269
|
return budgetState.totalBytes + size <= VFS_BUNDLE_MAX_BYTES;
|
|
@@ -1218,8 +1297,6 @@ function retainedVfsStateBytes(state) {
|
|
|
1218
1297
|
for (const moduleSource of Object.values(source.modules))
|
|
1219
1298
|
bytes += moduleSource.length;
|
|
1220
1299
|
}
|
|
1221
|
-
bytes += state.serializedManifest?.length ?? 0;
|
|
1222
|
-
bytes += state.serializedMetadata?.length ?? 0;
|
|
1223
1300
|
return bytes;
|
|
1224
1301
|
}
|
|
1225
1302
|
/**
|
|
@@ -1299,8 +1376,8 @@ function _jsonEncodedBytes(value) {
|
|
|
1299
1376
|
}
|
|
1300
1377
|
return bytes;
|
|
1301
1378
|
}
|
|
1302
|
-
/** `{"bundle":` +
|
|
1303
|
-
const ENCODED_PAYLOAD_FRAME_BYTES =
|
|
1379
|
+
/** `{"bundle":` + `}`: the frame around the one member. */
|
|
1380
|
+
const ENCODED_PAYLOAD_FRAME_BYTES = 11;
|
|
1304
1381
|
/**
|
|
1305
1382
|
* Running UTF-8 byte length of `JSON.stringify({ bundle, manifest })`,
|
|
1306
1383
|
* accumulated one cell at a time.
|
|
@@ -1316,10 +1393,9 @@ const ENCODED_PAYLOAD_FRAME_BYTES = 23;
|
|
|
1316
1393
|
* the total is a sum of independent per-cell terms: exact, incremental, and
|
|
1317
1394
|
* never holding more than one cell's worth of scratch.
|
|
1318
1395
|
*/
|
|
1319
|
-
export function encodedBundleSize(bundle
|
|
1396
|
+
export function encodedBundleSize(bundle) {
|
|
1320
1397
|
const cells = new Map();
|
|
1321
1398
|
let cellSum = 0;
|
|
1322
|
-
const manifestBytes = _jsonEncodedBytes(manifest);
|
|
1323
1399
|
const self = {
|
|
1324
1400
|
add(path, cell) {
|
|
1325
1401
|
if (cells.has(path))
|
|
@@ -1337,7 +1413,7 @@ export function encodedBundleSize(bundle, manifest) {
|
|
|
1337
1413
|
},
|
|
1338
1414
|
get bytes() {
|
|
1339
1415
|
const separators = Math.max(0, cells.size - 1);
|
|
1340
|
-
return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators
|
|
1416
|
+
return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators;
|
|
1341
1417
|
},
|
|
1342
1418
|
};
|
|
1343
1419
|
for (const [path, cell] of Object.entries(bundle))
|
|
@@ -1500,7 +1576,13 @@ function _inlineBundleSourceBytes(bundle) {
|
|
|
1500
1576
|
* the merge expression concatenates those fragments back to the original
|
|
1501
1577
|
* string or Uint8Array before module precompilation begins.
|
|
1502
1578
|
*/
|
|
1503
|
-
export async function buildFacetVfsBundleSource(bundle, forceSideModules = false, pacer) {
|
|
1579
|
+
export async function buildFacetVfsBundleSource(bundle, forceSideModules = false, pacer, { consume = false } = {}) {
|
|
1580
|
+
// \`consume\`: the caller releases the raw cells right after (a launch's
|
|
1581
|
+
// build), so each is taken as its side module is serialized and the map is
|
|
1582
|
+
// never held twice. Side modules always: an inline expression is copied
|
|
1583
|
+
// again into the runner's own source.
|
|
1584
|
+
if (consume)
|
|
1585
|
+
forceSideModules = true;
|
|
1504
1586
|
// Size the inline form before building it. A bundle that will be split has
|
|
1505
1587
|
// no use for the whole-bundle expression, and building one to read its
|
|
1506
1588
|
// length off cost a second full copy of the largest string this DO makes.
|
|
@@ -1551,46 +1633,55 @@ export async function buildFacetVfsBundleSource(bundle, forceSideModules = false
|
|
|
1551
1633
|
}
|
|
1552
1634
|
return pieces;
|
|
1553
1635
|
}
|
|
1554
|
-
const chunks = [];
|
|
1555
|
-
let chunk = {};
|
|
1556
|
-
let estimatedBytes = 0;
|
|
1557
|
-
function flushChunk() {
|
|
1558
|
-
if (Object.keys(chunk).length === 0)
|
|
1559
|
-
return;
|
|
1560
|
-
chunks.push(chunk);
|
|
1561
|
-
chunk = {};
|
|
1562
|
-
estimatedBytes = 0;
|
|
1563
|
-
}
|
|
1564
|
-
for (const [path, cell] of Object.entries(bundle)) {
|
|
1565
|
-
for (const [piecePath, pieceCell] of splitCell(path, cell)) {
|
|
1566
|
-
const pieceBytes = sourceBytes(piecePath, pieceCell);
|
|
1567
|
-
if (piecePath in chunk
|
|
1568
|
-
|| (estimatedBytes > 0 && estimatedBytes + pieceBytes > maxModuleBytes)) {
|
|
1569
|
-
flushChunk();
|
|
1570
|
-
}
|
|
1571
|
-
chunk[piecePath] = pieceCell;
|
|
1572
|
-
estimatedBytes += pieceBytes;
|
|
1573
|
-
}
|
|
1574
|
-
}
|
|
1575
|
-
flushChunk();
|
|
1576
1636
|
const modules = {};
|
|
1577
1637
|
const imports = [];
|
|
1578
1638
|
const aliases = [];
|
|
1579
|
-
|
|
1639
|
+
let chunk = {};
|
|
1640
|
+
let chunkPaths = [];
|
|
1641
|
+
let estimatedBytes = 0;
|
|
1642
|
+
// A chunk becomes its side module the moment it is full, so the cells it
|
|
1643
|
+
// carries and their serialization are never both held for the whole map:
|
|
1644
|
+
// consumed, a cell leaves the bundle as its module is written.
|
|
1645
|
+
async function flushChunk() {
|
|
1646
|
+
// Pieces of a split cell fill chunks before the cell's path is complete,
|
|
1647
|
+
// so emptiness is the chunk's own, not its finished paths'.
|
|
1648
|
+
if (estimatedBytes === 0)
|
|
1649
|
+
return;
|
|
1650
|
+
const index = aliases.length;
|
|
1580
1651
|
const moduleName = `${FACET_VFS_MODULE_PREFIX}${index}.js`;
|
|
1581
1652
|
const alias = `__nimbusVfsBundle${index}`;
|
|
1582
|
-
const source = _facetBundleModuleSource(
|
|
1653
|
+
const source = _facetBundleModuleSource(chunk);
|
|
1583
1654
|
if (_encodedSourceBytes(source) > BUNDLE_MAX_ENCODED_BYTES) {
|
|
1584
1655
|
throw new Error(`Nimbus: generated VFS side module exceeds encoded limit: ${moduleName}`);
|
|
1585
1656
|
}
|
|
1586
1657
|
modules[moduleName] = source;
|
|
1587
1658
|
imports.push(`import ${alias} from "${moduleName}";`);
|
|
1588
1659
|
aliases.push(alias);
|
|
1660
|
+
if (consume)
|
|
1661
|
+
for (const path of chunkPaths)
|
|
1662
|
+
delete bundle[path];
|
|
1663
|
+
chunk = {};
|
|
1664
|
+
chunkPaths = [];
|
|
1665
|
+
estimatedBytes = 0;
|
|
1589
1666
|
// Each side module is an independent serialization of its own cells, so
|
|
1590
1667
|
// the turn may end between any two of them.
|
|
1591
1668
|
if (pacer)
|
|
1592
1669
|
await pacer.spend(source.length);
|
|
1593
1670
|
}
|
|
1671
|
+
for (const path of Object.keys(bundle)) {
|
|
1672
|
+
const cell = bundle[path];
|
|
1673
|
+
for (const [piecePath, pieceCell] of splitCell(path, cell)) {
|
|
1674
|
+
const pieceBytes = sourceBytes(piecePath, pieceCell);
|
|
1675
|
+
if (piecePath in chunk
|
|
1676
|
+
|| (estimatedBytes > 0 && estimatedBytes + pieceBytes > maxModuleBytes)) {
|
|
1677
|
+
await flushChunk();
|
|
1678
|
+
}
|
|
1679
|
+
chunk[piecePath] = pieceCell;
|
|
1680
|
+
estimatedBytes += pieceBytes;
|
|
1681
|
+
}
|
|
1682
|
+
chunkPaths.push(path);
|
|
1683
|
+
}
|
|
1684
|
+
await flushChunk();
|
|
1594
1685
|
const expression = `(function(__parts){const __out={};for(const __part of __parts){` +
|
|
1595
1686
|
`for(const [__k,__v] of Object.entries(__part)){const __prev=__out[__k];` +
|
|
1596
1687
|
`if(__prev===undefined){__out[__k]=__v;}` +
|
|
@@ -1645,174 +1736,47 @@ function _fnv1a(s) {
|
|
|
1645
1736
|
return h.toString(16);
|
|
1646
1737
|
}
|
|
1647
1738
|
const MANIFEST_MAX_DEPTH = 12;
|
|
1648
|
-
|
|
1649
|
-
* Build the manifest pass — uncapped path→child-names map. UNCHANGED
|
|
1650
|
-
* from W2.5b; this is the W2.5b root-cause fix and continues to keep
|
|
1651
|
-
* fs.readdirSync / fs.statSync honest regardless of which subset of
|
|
1652
|
-
* file CONTENT we ship.
|
|
1653
|
-
*/
|
|
1654
|
-
async function buildManifest(vfs, cwd, scriptPath) {
|
|
1655
|
-
const manifest = {};
|
|
1656
|
-
async function walk(dirPath, depth = 0) {
|
|
1657
|
-
if (depth > MANIFEST_MAX_DEPTH)
|
|
1658
|
-
return;
|
|
1659
|
-
const stripped = dirPath.replace(/^\/+/, '');
|
|
1660
|
-
if (stripped in manifest)
|
|
1661
|
-
return;
|
|
1662
|
-
let entries;
|
|
1663
|
-
try {
|
|
1664
|
-
entries = (await vfs.readdir(stripped));
|
|
1665
|
-
}
|
|
1666
|
-
catch {
|
|
1667
|
-
return;
|
|
1668
|
-
}
|
|
1669
|
-
manifest[stripped] = entries.map((e) => e.name);
|
|
1670
|
-
for (const entry of entries) {
|
|
1671
|
-
if (entry.type === 'directory') {
|
|
1672
|
-
const childPath = stripped ? stripped + '/' + entry.name : entry.name;
|
|
1673
|
-
(await walk(childPath, depth + 1));
|
|
1674
|
-
}
|
|
1675
|
-
}
|
|
1676
|
-
}
|
|
1677
|
-
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1678
|
-
// ── The path from the root down to the working directory ──────────────
|
|
1679
|
-
//
|
|
1680
|
-
// One level each, before the deep walks, so that every directory on that
|
|
1681
|
-
// chain is ENUMERATED rather than merely mentioned. It is what makes a
|
|
1682
|
-
// synchronous "not there" honest for the shape programs probe most: node's
|
|
1683
|
-
// resolver walks upward asking for `<dir>/node_modules` at every level, and
|
|
1684
|
-
// a config lookup asks for a dotfile directory in $HOME. Those paths mostly
|
|
1685
|
-
// do not exist, and answering that requires having listed the directory
|
|
1686
|
-
// they would be in — otherwise the only honest answer is a refusal, and the
|
|
1687
|
-
// resolver pays a round trip per rung to hear it.
|
|
1688
|
-
//
|
|
1689
|
-
// Cheap by construction: one readdir per component, names only, and the
|
|
1690
|
-
// recursion below each stops immediately.
|
|
1691
|
-
{
|
|
1692
|
-
const segments = cwdStripped ? cwdStripped.split('/') : [];
|
|
1693
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1694
|
-
(await walk(segments.slice(0, i).join('/'), MANIFEST_MAX_DEPTH));
|
|
1695
|
-
}
|
|
1696
|
-
}
|
|
1697
|
-
(await walk(cwdStripped, 0));
|
|
1698
|
-
const nmDir = cwdStripped + '/node_modules';
|
|
1699
|
-
if ((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))) {
|
|
1700
|
-
(await walk(nmDir, 0));
|
|
1701
|
-
}
|
|
1702
|
-
// ── Bin-target package + hoisted dependency roots ─────────────────────
|
|
1703
|
-
//
|
|
1704
|
-
// When the entry script lives in a node_modules outside cwd (npx-cache
|
|
1705
|
-
// packages, globally-installed bins, etc.), buildManifest's cwd walk
|
|
1706
|
-
// misses the package's sibling files. The bin's index.js gets
|
|
1707
|
-
// require-walked + greedy-added to the BUNDLE, but the MANIFEST
|
|
1708
|
-
// (which is the source of truth for `fs.readdirSync`) was empty for
|
|
1709
|
-
// those paths — so `readdirSync('/tmp/.npx-cache/.../template-X')`
|
|
1710
|
-
// returned [] and `create-vite` scaffolded zero files.
|
|
1711
|
-
//
|
|
1712
|
-
// Walk the innermost `node_modules/<pkg>/` of `scriptPath` so its
|
|
1713
|
-
// entire package tree is enumerable via readdir. Also walk that
|
|
1714
|
-
// `node_modules` directory itself: global npm bins resolve hoisted
|
|
1715
|
-
// transitive dependencies as siblings of the bin's own package.
|
|
1716
|
-
// Bounded by MANIFEST_MAX_DEPTH; same depth budget as the cwd walk.
|
|
1717
|
-
if (scriptPath) {
|
|
1718
|
-
const sp = scriptPath.replace(/^\/+/, '');
|
|
1719
|
-
const segs = sp.split('/');
|
|
1720
|
-
let nmIdx = -1;
|
|
1721
|
-
for (let i = segs.length - 1; i >= 0; i--) {
|
|
1722
|
-
if (segs[i] === 'node_modules') {
|
|
1723
|
-
nmIdx = i;
|
|
1724
|
-
break;
|
|
1725
|
-
}
|
|
1726
|
-
}
|
|
1727
|
-
if (nmIdx >= 0) {
|
|
1728
|
-
const isScoped = segs[nmIdx + 1]?.startsWith('@');
|
|
1729
|
-
const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
|
|
1730
|
-
if (pkgEnd <= segs.length) {
|
|
1731
|
-
const pkgRoot = segs.slice(0, pkgEnd).join('/');
|
|
1732
|
-
if ((await vfs.exists(pkgRoot)) && (await vfs.isDirectory(pkgRoot))) {
|
|
1733
|
-
(await walk(pkgRoot, 0));
|
|
1734
|
-
}
|
|
1735
|
-
}
|
|
1736
|
-
const nodeModulesRoot = segs.slice(0, nmIdx + 1).join('/');
|
|
1737
|
-
if ((await vfs.exists(nodeModulesRoot)) && (await vfs.isDirectory(nodeModulesRoot))) {
|
|
1738
|
-
(await walk(nodeModulesRoot, 0));
|
|
1739
|
-
}
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return manifest;
|
|
1743
|
-
}
|
|
1744
|
-
async function buildVfsMetadata(vfs, manifest, bundle) {
|
|
1745
|
-
const paths = new Set(Object.keys(bundle).filter((path) => compiledCellPath(path) === null));
|
|
1746
|
-
for (const [directory, children] of Object.entries(manifest)) {
|
|
1747
|
-
paths.add(directory);
|
|
1748
|
-
for (const child of children) {
|
|
1749
|
-
paths.add(directory ? `${directory}/${child}` : child);
|
|
1750
|
-
}
|
|
1751
|
-
}
|
|
1752
|
-
const metadata = {};
|
|
1753
|
-
for (const path of paths) {
|
|
1754
|
-
try {
|
|
1755
|
-
const stat = await vfs.authority.stat(path, { followSymlinks: false });
|
|
1756
|
-
if (!stat)
|
|
1757
|
-
continue;
|
|
1758
|
-
metadata[path] = {
|
|
1759
|
-
type: stat.type,
|
|
1760
|
-
size: stat.size,
|
|
1761
|
-
mode: stat.mode,
|
|
1762
|
-
uid: stat.uid,
|
|
1763
|
-
gid: stat.gid,
|
|
1764
|
-
};
|
|
1765
|
-
}
|
|
1766
|
-
catch {
|
|
1767
|
-
// The credentialed lookup is authoritative; inaccessible ancestors do
|
|
1768
|
-
// not reveal whether a leaf exists.
|
|
1769
|
-
}
|
|
1770
|
-
}
|
|
1771
|
-
return metadata;
|
|
1772
|
-
}
|
|
1773
|
-
async function addUnreadableDenialCells(vfs, bundle, metadata) {
|
|
1774
|
-
for (const [path, stat] of Object.entries(metadata)) {
|
|
1775
|
-
if (stat.type === 'directory' || path in bundle)
|
|
1776
|
-
continue;
|
|
1777
|
-
try {
|
|
1778
|
-
(await vfs.access(path, 0o4));
|
|
1779
|
-
}
|
|
1780
|
-
catch (error) {
|
|
1781
|
-
if (typeof error === 'object'
|
|
1782
|
-
&& error !== null
|
|
1783
|
-
&& 'code' in error
|
|
1784
|
-
&& error.code === 'EACCES') {
|
|
1785
|
-
bundle[path] = { error: 'EACCES' };
|
|
1786
|
-
}
|
|
1787
|
-
}
|
|
1788
|
-
}
|
|
1789
|
-
}
|
|
1790
|
-
/**
|
|
1791
|
-
* Greedy-oversample every installed package's main entry. The static
|
|
1792
|
-
* prefetch via require-resolver covers the require() chain literally
|
|
1793
|
-
* present in source; greedy oversampling adds a safety net for dynamic
|
|
1794
|
-
* patterns the regex misses (jest/`bindings`/`import-local` style
|
|
1795
|
-
* computed-path requires). Bounded to package.json + 1 main-entry file
|
|
1796
|
-
* per package — sub-agent §Q3 quantified the worst-case cumulative
|
|
1797
|
-
* budget impact (~322 KiB for fastify, ~1.7 MiB for ts-jest).
|
|
1798
|
-
*
|
|
1799
|
-
* `requiredPaths` is the static require closure. A package the closure
|
|
1800
|
-
* already reached — but reached only through a SUBPATH — has its main entry
|
|
1801
|
-
* skipped; see `mainIsSpeculative`.
|
|
1802
|
-
*/
|
|
1803
|
-
// can verify the hash-chunk + shared/ oversample directly. Pre-X.5-C this
|
|
1804
|
-
// was a file-local helper. Adding the named export is a pure surface
|
|
1805
|
-
// addition — no callers other than buildPrefetchBundle (same file) and
|
|
1806
|
-
// the new probe.
|
|
1807
|
-
export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set()) {
|
|
1739
|
+
export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set(), options = {}) {
|
|
1808
1740
|
let added = 0;
|
|
1741
|
+
const groups = [];
|
|
1742
|
+
const bound = Math.min(options.maxBundleBytes ?? VFS_BUNDLE_MAX_BYTES, VFS_BUNDLE_MAX_BYTES);
|
|
1743
|
+
const independent = new Set(Object.keys(bundle));
|
|
1744
|
+
let rawBytes = 0;
|
|
1745
|
+
for (const cell of Object.values(bundle))
|
|
1746
|
+
rawBytes += _bundleCellRawBytes(cell);
|
|
1747
|
+
if (rawBytes >= bound)
|
|
1748
|
+
return { added, groups };
|
|
1809
1749
|
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1810
1750
|
const nmDir = cwdStripped + '/node_modules';
|
|
1811
|
-
if (!((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))))
|
|
1812
|
-
return { added };
|
|
1751
|
+
if (!((await filesOf(vfs).exists(nmDir)) && (await filesOf(vfs).isDirectory(nmDir))))
|
|
1752
|
+
return { added, groups };
|
|
1813
1753
|
const exts = ['', '.js', '.cjs', '.mjs', '/index.js', '/index.cjs'];
|
|
1814
1754
|
async function addOne(path) {
|
|
1815
1755
|
const stripped = path.replace(/^\/+/, '');
|
|
1756
|
+
if ((isBundleModuleCandidate(stripped) || stripped.endsWith('.cjs')) && !isNativeBinPath(stripped)) {
|
|
1757
|
+
if (independent.has(stripped))
|
|
1758
|
+
return false;
|
|
1759
|
+
if (bundle[stripped] === undefined && !(await _bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)))
|
|
1760
|
+
return false;
|
|
1761
|
+
const closure = await prefetchForRequire(requireFsOverBridge(vfs), '', stripped.slice(0, stripped.lastIndexOf('/')), '/' + stripped, bound, options.pacer?.spend.bind(options.pacer), {
|
|
1762
|
+
purpose: 'dependency-closure', held: bundle,
|
|
1763
|
+
maxAdditionalBytes: Math.max(0, bound - rawBytes),
|
|
1764
|
+
maxAdditionalFiles: Math.max(0, VFS_BUNDLE_MAX_FILES - budgetState.fileCount),
|
|
1765
|
+
});
|
|
1766
|
+
if ('kind' in closure || closure.bundle[stripped] === undefined)
|
|
1767
|
+
return false;
|
|
1768
|
+
for (const [member, cell] of Object.entries(closure.bundle)) {
|
|
1769
|
+
if (bundle[member] !== undefined)
|
|
1770
|
+
continue;
|
|
1771
|
+
bundle[member] = cell;
|
|
1772
|
+
rawBytes += _bundleCellRawBytes(cell);
|
|
1773
|
+
budgetState.totalBytes += _bundleCellLength(cell);
|
|
1774
|
+
budgetState.fileCount++;
|
|
1775
|
+
added++;
|
|
1776
|
+
}
|
|
1777
|
+
groups.push({ root: stripped, members: new Set(Object.keys(closure.bundle)) });
|
|
1778
|
+
return true;
|
|
1779
|
+
}
|
|
1816
1780
|
if (stripped in bundle)
|
|
1817
1781
|
return false;
|
|
1818
1782
|
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
@@ -1820,7 +1784,7 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1820
1784
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1821
1785
|
return false;
|
|
1822
1786
|
try {
|
|
1823
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
1787
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
1824
1788
|
return false;
|
|
1825
1789
|
// This is a guess at what a program might require, and the same
|
|
1826
1790
|
// per-file ceiling the entry-package walk applies bounds it: a
|
|
@@ -1828,8 +1792,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1828
1792
|
// `lib/typescript.js` beside the `lib/tsc.js` that actually runs), and
|
|
1829
1793
|
// one guess must not spend a third of the budget — and a third of the
|
|
1830
1794
|
// supervisor's headroom — on every invocation that never reads it.
|
|
1831
|
-
//
|
|
1832
|
-
//
|
|
1795
|
+
// Literal dependencies use the closure's byte allowance, not this
|
|
1796
|
+
// speculative-root per-file ceiling.
|
|
1833
1797
|
if (!(await _bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)))
|
|
1834
1798
|
return false;
|
|
1835
1799
|
// A guess must not be a native binary. Nothing in a Workers isolate can
|
|
@@ -1839,14 +1803,19 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1839
1803
|
// Measured: freeing 1.3 MiB of rollup let `@napi-rs/lzma-linux-x64-gnu`'s
|
|
1840
1804
|
// 1,445,448-byte `.node` in, which the size guard had been evicting.
|
|
1841
1805
|
// Only the guess is filtered; a path the closure requires is untouched.
|
|
1842
|
-
if (
|
|
1806
|
+
if (isNativeBinPath(stripped))
|
|
1843
1807
|
return false;
|
|
1844
1808
|
// hardening-r5: preserve binary content as Uint8Array.
|
|
1809
|
+
const stat = await filesOf(vfs).stat(stripped);
|
|
1810
|
+
if (stat === null || rawBytes + stat.size > bound)
|
|
1811
|
+
return false;
|
|
1845
1812
|
const content = (await _readBundleCell(vfs, stripped));
|
|
1846
1813
|
const cellLen = _bundleCellLength(content);
|
|
1847
|
-
|
|
1814
|
+
const raw = _bundleCellRawBytes(content);
|
|
1815
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES || rawBytes + raw > bound)
|
|
1848
1816
|
return false;
|
|
1849
1817
|
bundle[stripped] = content;
|
|
1818
|
+
rawBytes += raw;
|
|
1850
1819
|
budgetState.totalBytes += cellLen;
|
|
1851
1820
|
budgetState.fileCount++;
|
|
1852
1821
|
added++;
|
|
@@ -1856,26 +1825,6 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1856
1825
|
return false;
|
|
1857
1826
|
}
|
|
1858
1827
|
}
|
|
1859
|
-
// X.5-C Fix #2 helper: walk a (possibly nested) exports value and
|
|
1860
|
-
// collect every string-leaf path. unbuild-shaped packages like pathe
|
|
1861
|
-
// nest two deep — `exports."."`.{require,import}.{types,default} —
|
|
1862
|
-
// and the previous one-level loop only caught the inner string leaves
|
|
1863
|
-
// when default was at the top, missing the unbuild shape entirely.
|
|
1864
|
-
function collectExportLeaves(node, out) {
|
|
1865
|
-
if (typeof node === 'string') {
|
|
1866
|
-
out.add(node);
|
|
1867
|
-
return;
|
|
1868
|
-
}
|
|
1869
|
-
if (!node || typeof node !== 'object')
|
|
1870
|
-
return;
|
|
1871
|
-
// Order matters for the "most likely usable" leaf: prefer require
|
|
1872
|
-
// (most CJS-friendly), then default, then node, then import. We add
|
|
1873
|
-
// ALL of them to the candidate set — addPkgEntry will probe each.
|
|
1874
|
-
for (const k of ['require', 'node', 'default', 'import']) {
|
|
1875
|
-
if (k in node)
|
|
1876
|
-
collectExportLeaves(node[k], out);
|
|
1877
|
-
}
|
|
1878
|
-
}
|
|
1879
1828
|
/**
|
|
1880
1829
|
* Whether guessing at `pkgDir`'s main entry is still a guess.
|
|
1881
1830
|
*
|
|
@@ -1907,47 +1856,52 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1907
1856
|
return !reached;
|
|
1908
1857
|
}
|
|
1909
1858
|
async function addPkgEntry(pkgDir) {
|
|
1910
|
-
|
|
1859
|
+
const manifest = (pkgDir + '/package.json').replace(/^\/+/, '');
|
|
1860
|
+
if (bundle[manifest] === undefined && !(await addOne(manifest)))
|
|
1861
|
+
return;
|
|
1911
1862
|
// A package the closure reached keeps exactly what it reached; only an
|
|
1912
1863
|
// unreached one gets the guess below.
|
|
1913
1864
|
if (!mainIsSpeculative(pkgDir))
|
|
1914
1865
|
return;
|
|
1915
|
-
|
|
1866
|
+
// Malformed JSON, or a package whose entry fields the resolver cannot
|
|
1867
|
+
// read (`main: 7`, `exports: true`) drops that field and keeps the rest;
|
|
1868
|
+
// unparseable JSON is a package with no declared entry. Either way the
|
|
1869
|
+
// next package is unaffected.
|
|
1870
|
+
let pkg;
|
|
1916
1871
|
try {
|
|
1917
|
-
|
|
1872
|
+
const text = bundle[manifest];
|
|
1873
|
+
pkg = typeof text === 'string' ? parseResolvablePackageJson(text) : null;
|
|
1918
1874
|
}
|
|
1919
1875
|
catch {
|
|
1920
|
-
|
|
1921
|
-
}
|
|
1922
|
-
|
|
1923
|
-
|
|
1924
|
-
|
|
1925
|
-
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1935
|
-
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
if (candidates.size === 0)
|
|
1942
|
-
candidates.add('index.js');
|
|
1876
|
+
pkg = null;
|
|
1877
|
+
}
|
|
1878
|
+
// The root entry a bare require of this package resolves to, in the
|
|
1879
|
+
// order the runtime resolves it (require-resolver.ts resolvePkgSubpathEx):
|
|
1880
|
+
// exports under the CJS conditions, then the ESM ones, then legacy main,
|
|
1881
|
+
// then an index probe, each tried until one lands. A denied root
|
|
1882
|
+
// (`"." : null`) resolves to nothing and falls through the same way. A
|
|
1883
|
+
// hand-rolled walk of `exports["."]` read nothing from a root conditional
|
|
1884
|
+
// map (on-change@6: `{ types, default }`, no ".", no main) and guessed
|
|
1885
|
+
// `index.js`, which does not exist.
|
|
1886
|
+
const candidates = [];
|
|
1887
|
+
if (pkg) {
|
|
1888
|
+
let entry = resolvePackageEntry(pkg, '.', DEFAULT_CJS_CONDITIONS);
|
|
1889
|
+
if (entry === null && pkg.exports != null)
|
|
1890
|
+
entry = resolvePackageEntry(pkg, '.', DEFAULT_ESM_CONDITIONS);
|
|
1891
|
+
if (entry !== null)
|
|
1892
|
+
candidates.push(entry);
|
|
1893
|
+
if (pkg.main !== undefined)
|
|
1894
|
+
candidates.push(pkg.main);
|
|
1895
|
+
}
|
|
1896
|
+
candidates.push('index.js');
|
|
1943
1897
|
for (const rel of candidates) {
|
|
1944
1898
|
const norm = rel.replace(/^\.\//, '');
|
|
1945
1899
|
const base = pkgDir + '/' + norm;
|
|
1946
1900
|
let landed = false;
|
|
1947
1901
|
const tries = /\.[a-z]+$/.test(norm) ? [base] : exts.map((e) => base + e);
|
|
1948
1902
|
for (const candidate of tries) {
|
|
1949
|
-
if ((await vfs.exists(candidate.replace(/^\/+/, ''))) &&
|
|
1950
|
-
!(await vfs.isDirectory(candidate.replace(/^\/+/, '')))) {
|
|
1903
|
+
if ((await filesOf(vfs).exists(candidate.replace(/^\/+/, ''))) &&
|
|
1904
|
+
!(await filesOf(vfs).isDirectory(candidate.replace(/^\/+/, '')))) {
|
|
1951
1905
|
if ((await addOne(candidate))) {
|
|
1952
1906
|
landed = true;
|
|
1953
1907
|
break;
|
|
@@ -1965,8 +1919,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1965
1919
|
// The static walker cannot discover computed hash-chunk imports, so
|
|
1966
1920
|
// the greedy oversample is the safety net for their reachability.
|
|
1967
1921
|
const entryDir = base.replace(/\/[^/]+$/, '');
|
|
1968
|
-
|
|
1969
|
-
const sibs =
|
|
1922
|
+
{
|
|
1923
|
+
const sibs = await (async () => vfs.readdir(entryDir))().catch(() => []);
|
|
1970
1924
|
for (const sib of sibs) {
|
|
1971
1925
|
if (sib.type !== 'file')
|
|
1972
1926
|
continue;
|
|
@@ -1994,8 +1948,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1994
1948
|
// typical shared/ dir returns 1-5 files.
|
|
1995
1949
|
const sharedDir = entryDir + '/shared';
|
|
1996
1950
|
const sharedStripped = sharedDir.replace(/^\/+/, '');
|
|
1997
|
-
if (
|
|
1998
|
-
for (const sh of
|
|
1951
|
+
if (await (async () => filesOf(vfs).isDirectory(sharedStripped))().catch(() => false)) {
|
|
1952
|
+
for (const sh of await (async () => vfs.readdir(sharedDir))().catch(() => [])) {
|
|
1999
1953
|
if (sh.type !== 'file')
|
|
2000
1954
|
continue;
|
|
2001
1955
|
if (!/\.(cjs|mjs|js)$/.test(sh.name))
|
|
@@ -2004,22 +1958,20 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
2004
1958
|
}
|
|
2005
1959
|
}
|
|
2006
1960
|
}
|
|
2007
|
-
catch { /* unreadable dir — drop sibling oversample, entry
|
|
2008
|
-
file is enough */
|
|
2009
|
-
}
|
|
2010
1961
|
break;
|
|
2011
1962
|
}
|
|
2012
1963
|
}
|
|
2013
1964
|
}
|
|
2014
1965
|
for (const pkgDir of (await speculativePackageDirs(vfs, cwdStripped, bundle)))
|
|
2015
1966
|
(await addPkgEntry(pkgDir));
|
|
2016
|
-
return { added };
|
|
1967
|
+
return { added, groups };
|
|
2017
1968
|
}
|
|
2018
1969
|
/**
|
|
2019
|
-
* The packages a computed `require(name)
|
|
2020
|
-
*
|
|
2021
|
-
* ONE `dependencies` hop from a package
|
|
2022
|
-
*
|
|
1970
|
+
* The packages a name resolved at runtime — a computed `require(name)`, or a
|
|
1971
|
+
* resolver call like exsolve's `resolveModulePath(name, { from: rootDir })` —
|
|
1972
|
+
* can plausibly reach: every package ONE `dependencies` hop from a package
|
|
1973
|
+
* the project itself declares or that owns a file in the static closure, plus
|
|
1974
|
+
* those roots themselves. Never devDependencies, never a second hop.
|
|
2023
1975
|
*
|
|
2024
1976
|
* Unbounded, the greedy oversample read every installed package's main:
|
|
2025
1977
|
* for `node -e "import('got')"` in got's repo — a one-file static closure —
|
|
@@ -2031,11 +1983,26 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
2031
1983
|
* tree. Computed requires almost always target a declared runtime
|
|
2032
1984
|
* dependency of the package doing the requiring, so the bound is one hop
|
|
2033
1985
|
* over `dependencies` only. Directories, sorted for a stable bundle.
|
|
1986
|
+
*
|
|
1987
|
+
* The project's own dependencies are hop ROOTS and not merely members,
|
|
1988
|
+
* because a bin runs inside the project and resolves from the project root,
|
|
1989
|
+
* where what it names is its host framework's runtime peer rather than
|
|
1990
|
+
* anything its own package declares. Measured on staging, `nuxt dev` on a
|
|
1991
|
+
* `nuxi init` project: npm points `node_modules/.bin/nuxt` at
|
|
1992
|
+
* `@nuxt/cli/bin/nuxi.mjs`, so `@nuxt/cli` owns the entry; `@nuxt/kit` is a
|
|
1993
|
+
* devDependency of `@nuxt/cli` and a dependency of `nuxt`, which the project
|
|
1994
|
+
* declares. Admitting `nuxt` without hopping from it left
|
|
1995
|
+
* `@nuxt/kit/package.json` unstaged, `readFileSync` raised EAGAIN inside
|
|
1996
|
+
* exsolve — which swallows every error — and the CLI reported
|
|
1997
|
+
* `Cannot resolve module "@nuxt/kit" (from: /home/user/nuxt-probe/mvp/)`.
|
|
1998
|
+
* One hop from each project dependency is what the project's own node_modules
|
|
1999
|
+
* was hoisted for; it is the same edge kind and the same single level the
|
|
2000
|
+
* owner hop already spends.
|
|
2034
2001
|
*/
|
|
2035
2002
|
export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
2036
2003
|
const runtimeDeps = async (pkgJsonPath) => {
|
|
2037
2004
|
try {
|
|
2038
|
-
const meta = JSON.parse((await vfs.readFileString(pkgJsonPath)));
|
|
2005
|
+
const meta = JSON.parse((await filesOf(vfs).readFileString(pkgJsonPath)));
|
|
2039
2006
|
const names = new Set();
|
|
2040
2007
|
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
2041
2008
|
const deps = meta?.[field];
|
|
@@ -2064,7 +2031,7 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2064
2031
|
let dir = fromDir;
|
|
2065
2032
|
for (;;) {
|
|
2066
2033
|
const candidate = dir + '/node_modules/' + name;
|
|
2067
|
-
if ((await vfs.exists(candidate + '/package.json')))
|
|
2034
|
+
if ((await filesOf(vfs).exists(candidate + '/package.json')))
|
|
2068
2035
|
return candidate;
|
|
2069
2036
|
const idx = dir.lastIndexOf('/');
|
|
2070
2037
|
if (idx <= 0)
|
|
@@ -2073,14 +2040,24 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2073
2040
|
}
|
|
2074
2041
|
};
|
|
2075
2042
|
const reached = new Set();
|
|
2043
|
+
const hopped = new Set();
|
|
2044
|
+
// Returns what this hop landed on, so a root can be hopped from without
|
|
2045
|
+
// reading its manifest twice and without the caller tracking the edges.
|
|
2076
2046
|
const hop = async (fromDir) => {
|
|
2047
|
+
if (hopped.has(fromDir))
|
|
2048
|
+
return [];
|
|
2049
|
+
hopped.add(fromDir);
|
|
2050
|
+
const landed = [];
|
|
2077
2051
|
for (const name of (await runtimeDeps(fromDir + '/package.json'))) {
|
|
2078
2052
|
const dir = (await resolveDir(name, fromDir));
|
|
2079
|
-
if (dir
|
|
2080
|
-
|
|
2053
|
+
if (dir === null)
|
|
2054
|
+
continue;
|
|
2055
|
+
reached.add(dir);
|
|
2056
|
+
landed.push(dir);
|
|
2081
2057
|
}
|
|
2058
|
+
return landed;
|
|
2082
2059
|
};
|
|
2083
|
-
(await hop(cwdStripped));
|
|
2060
|
+
const projectDeps = (await hop(cwdStripped));
|
|
2084
2061
|
const owners = new Set();
|
|
2085
2062
|
for (const path of Object.keys(bundle)) {
|
|
2086
2063
|
const owner = ownerOf(path);
|
|
@@ -2091,6 +2068,12 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2091
2068
|
reached.add(owner);
|
|
2092
2069
|
(await hop(owner));
|
|
2093
2070
|
}
|
|
2071
|
+
// The second kind of root: what the project declares. A bin resolving from
|
|
2072
|
+
// the project root reaches these packages' dependencies, and nothing else
|
|
2073
|
+
// in this function would — the project's dependency owns no staged file
|
|
2074
|
+
// when the bin that runs belongs to a sibling package.
|
|
2075
|
+
for (const dir of projectDeps)
|
|
2076
|
+
(await hop(dir));
|
|
2094
2077
|
return [...reached].sort();
|
|
2095
2078
|
}
|
|
2096
2079
|
/**
|
|
@@ -2159,7 +2142,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
|
2159
2142
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2160
2143
|
return false;
|
|
2161
2144
|
try {
|
|
2162
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2145
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2163
2146
|
return false;
|
|
2164
2147
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2165
2148
|
return false;
|
|
@@ -2255,7 +2238,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
|
2255
2238
|
* (0, path_1.resolve)(__dirname, '../../../.ts-jest-digest'), 'utf8');
|
|
2256
2239
|
*
|
|
2257
2240
|
* The install pipeline writes `.ts-jest-digest` to VFS correctly
|
|
2258
|
-
* (
|
|
2241
|
+
* (the namespace lists it). But the runtime
|
|
2259
2242
|
* fs shim's readFileSync (`src/node-shims.ts:202-215`) consults
|
|
2260
2243
|
* `__vfsBundle` only, and none of the existing bundle-population
|
|
2261
2244
|
* passes — `prefetchForRequire` (require-graph), `greedyAddMainEntries`
|
|
@@ -2334,7 +2317,7 @@ export async function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, bud
|
|
|
2334
2317
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2335
2318
|
return false;
|
|
2336
2319
|
try {
|
|
2337
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2320
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2338
2321
|
return false;
|
|
2339
2322
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2340
2323
|
return false;
|
|
@@ -2523,13 +2506,10 @@ export async function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState,
|
|
|
2523
2506
|
wasmPaths.push(child);
|
|
2524
2507
|
if (bundle[child] !== undefined)
|
|
2525
2508
|
continue;
|
|
2526
|
-
|
|
2527
|
-
|
|
2528
|
-
size = (await vfs.lstat(child)).size;
|
|
2529
|
-
}
|
|
2530
|
-
catch {
|
|
2509
|
+
const stat = await filesOf(vfs).lstat(child).catch(() => null);
|
|
2510
|
+
if (stat === null)
|
|
2531
2511
|
continue;
|
|
2532
|
-
|
|
2512
|
+
const size = stat.size;
|
|
2533
2513
|
if (size > BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)
|
|
2534
2514
|
continue;
|
|
2535
2515
|
candidates.push({ path: child, size });
|
|
@@ -2595,13 +2575,9 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
|
|
|
2595
2575
|
for (const path of unstagedPaths) {
|
|
2596
2576
|
if (byPath.has(path))
|
|
2597
2577
|
continue;
|
|
2598
|
-
|
|
2599
|
-
|
|
2600
|
-
bytes = await vfs.readFile(path);
|
|
2601
|
-
}
|
|
2602
|
-
catch {
|
|
2578
|
+
const bytes = await filesOf(vfs).readBytes(path).catch(() => null);
|
|
2579
|
+
if (bytes === null)
|
|
2603
2580
|
continue;
|
|
2604
|
-
}
|
|
2605
2581
|
byPath.set(path, { vfsPath: '/' + stripLeadingSlashes(path), digest: wasmImageDigest(bytes) });
|
|
2606
2582
|
}
|
|
2607
2583
|
return [...byPath.values()];
|
|
@@ -2621,21 +2597,29 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
|
|
|
2621
2597
|
* Admitted smallest-first for the same reason as the entry-package walk: the
|
|
2622
2598
|
* budget is shared, so ordering by size maximizes the number of misses a
|
|
2623
2599
|
* fixed number of bytes repairs.
|
|
2600
|
+
*
|
|
2601
|
+
* `room` is what the snapshot bound leaves after what is already committed
|
|
2602
|
+
* (the static closure, and for learned entries this session's own misses), in
|
|
2603
|
+
* raw bytes: evidence fills that room and never pushes the snapshot past the
|
|
2604
|
+
* bound, so it can never be the reason a launch fails. `bytes` is what this
|
|
2605
|
+
* call staged, in the same unit.
|
|
2624
2606
|
*/
|
|
2625
|
-
export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState) {
|
|
2607
|
+
export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState, room = Infinity, pacer) {
|
|
2626
2608
|
if (!observed || observed.size === 0)
|
|
2627
|
-
return { added: 0 };
|
|
2609
|
+
return { added: 0, bytes: 0 };
|
|
2610
|
+
let bytes = 0;
|
|
2628
2611
|
const candidates = [];
|
|
2629
2612
|
for (const path of observed) {
|
|
2630
|
-
if (path === ''
|
|
2613
|
+
if (path === '')
|
|
2631
2614
|
continue;
|
|
2632
|
-
|
|
2633
|
-
|
|
2634
|
-
|
|
2635
|
-
}
|
|
2636
|
-
catch {
|
|
2615
|
+
// Already staged, but evictable: the evidence is what makes it required.
|
|
2616
|
+
if (bundle[path] !== undefined) {
|
|
2617
|
+
requiredPaths.add(path);
|
|
2637
2618
|
continue;
|
|
2638
2619
|
}
|
|
2620
|
+
const stat = await filesOf(vfs).lstat(path).catch(() => null);
|
|
2621
|
+
if (stat === null)
|
|
2622
|
+
continue;
|
|
2639
2623
|
if (stat.type === 'directory')
|
|
2640
2624
|
continue;
|
|
2641
2625
|
candidates.push({ path, size: stat.size });
|
|
@@ -2657,13 +2641,48 @@ export async function addObservedReads(vfs, observed, bundle, requiredPaths, bud
|
|
|
2657
2641
|
const cellLen = _bundleCellLength(content);
|
|
2658
2642
|
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
2659
2643
|
continue;
|
|
2644
|
+
const raw = _bundleCellRawBytes(content);
|
|
2645
|
+
if (bytes + raw > room)
|
|
2646
|
+
continue;
|
|
2660
2647
|
bundle[candidate.path] = content;
|
|
2661
2648
|
requiredPaths.add(candidate.path);
|
|
2649
|
+
bytes += raw;
|
|
2662
2650
|
budgetState.totalBytes += cellLen;
|
|
2663
2651
|
budgetState.fileCount++;
|
|
2664
2652
|
added++;
|
|
2665
2653
|
}
|
|
2666
|
-
|
|
2654
|
+
// A module brings its static imports: learned one miss per launch, nuxt's
|
|
2655
|
+
// on-change alone would have cost a relaunch for each of its files.
|
|
2656
|
+
for (const path of observed) {
|
|
2657
|
+
if (!/\.[cm]?js$/.test(path) || bundle[path] === undefined)
|
|
2658
|
+
continue;
|
|
2659
|
+
const closure = await prefetchForRequire(requireFsOverBridge(vfs), '', path.slice(0, path.lastIndexOf('/')), '/' + path, undefined, pacer?.spend.bind(pacer));
|
|
2660
|
+
if ('kind' in closure)
|
|
2661
|
+
continue;
|
|
2662
|
+
for (const [dep, content] of Object.entries(closure.bundle)) {
|
|
2663
|
+
if (closure.speculative.has(dep))
|
|
2664
|
+
continue;
|
|
2665
|
+
if (bundle[dep] !== undefined) {
|
|
2666
|
+
requiredPaths.add(dep);
|
|
2667
|
+
continue;
|
|
2668
|
+
}
|
|
2669
|
+
const cellLen = _bundleCellLength(content);
|
|
2670
|
+
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
2671
|
+
break;
|
|
2672
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
2673
|
+
continue;
|
|
2674
|
+
const raw = _bundleCellRawBytes(content);
|
|
2675
|
+
if (bytes + raw > room)
|
|
2676
|
+
continue;
|
|
2677
|
+
bundle[dep] = content;
|
|
2678
|
+
requiredPaths.add(dep);
|
|
2679
|
+
bytes += raw;
|
|
2680
|
+
budgetState.totalBytes += cellLen;
|
|
2681
|
+
budgetState.fileCount++;
|
|
2682
|
+
added++;
|
|
2683
|
+
}
|
|
2684
|
+
}
|
|
2685
|
+
return { added, bytes };
|
|
2667
2686
|
}
|
|
2668
2687
|
const RUNTIME_PACKAGE_EXCLUDED_ROOT_DIRS = new Set([
|
|
2669
2688
|
'docs',
|
|
@@ -2706,6 +2725,31 @@ const RUNTIME_PACKAGE_EXCLUDED_FILE_SUFFIXES = [
|
|
|
2706
2725
|
'.mov',
|
|
2707
2726
|
'.webm',
|
|
2708
2727
|
];
|
|
2728
|
+
/**
|
|
2729
|
+
* Whether the facet's require path could ever load this cell AS A MODULE.
|
|
2730
|
+
*
|
|
2731
|
+
* Not a judgement about whether the file is useful — an evicted cell is a
|
|
2732
|
+
* real loss either way, since the synchronous read it exists for raises
|
|
2733
|
+
* EAGAIN. It is a judgement about what the loss can BREAK. A cell the loader
|
|
2734
|
+
* can resolve is one some other module may be importing, and losing it takes
|
|
2735
|
+
* every importer down with it; a cell it can never resolve is only ever read
|
|
2736
|
+
* as data, by whoever asked for that path specifically.
|
|
2737
|
+
*
|
|
2738
|
+
* Declaration files are the clean case, and the reason this is a suffix test
|
|
2739
|
+
* rather than a content one: `foo.d.ts` is consumed by a type checker and is
|
|
2740
|
+
* never the target of a `require`, so shedding one cannot orphan a module.
|
|
2741
|
+
* That does not contradict the list above keeping them admissible — a `.d.ts`
|
|
2742
|
+
* IS a legitimate runtime read for the one package that reads its own (tsc
|
|
2743
|
+
* and `lib.*.d.ts`). This decides only what goes FIRST once a bound has
|
|
2744
|
+
* already been breached and something has to.
|
|
2745
|
+
*/
|
|
2746
|
+
function _isLoadableModuleCell(path) {
|
|
2747
|
+
if (isTypescriptDeclarationFile(path))
|
|
2748
|
+
return false;
|
|
2749
|
+
const ext = vfsPathExtension(path);
|
|
2750
|
+
return ext === '.js' || ext === '.mjs' || ext === '.cjs' || ext === '.json'
|
|
2751
|
+
|| ext === '.wasm' || ext === '' || bundleTypescriptLoader(path) !== null;
|
|
2752
|
+
}
|
|
2709
2753
|
/**
|
|
2710
2754
|
* Per-file ceiling for the speculative passes over installed packages: the
|
|
2711
2755
|
* entry-package walk (`addBinTargetSiblings`) and the main-entry oversample
|
|
@@ -2857,7 +2901,7 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
|
2857
2901
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2858
2902
|
return;
|
|
2859
2903
|
try {
|
|
2860
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2904
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2861
2905
|
return;
|
|
2862
2906
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2863
2907
|
return;
|
|
@@ -2910,8 +2954,19 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
|
2910
2954
|
}
|
|
2911
2955
|
return { added };
|
|
2912
2956
|
}
|
|
2913
|
-
function looksLikeEsm(src) {
|
|
2914
|
-
|
|
2957
|
+
function looksLikeEsm(path, src) {
|
|
2958
|
+
if (!hasTopLevelModuleSyntax(src))
|
|
2959
|
+
return false;
|
|
2960
|
+
if (vfsPathExtension(path) !== '')
|
|
2961
|
+
return true;
|
|
2962
|
+
// No extension: a bin script, or data such as a LICENSE whose prose says "import". Only a parse tells them apart.
|
|
2963
|
+
try {
|
|
2964
|
+
parseJavaScriptModule(src);
|
|
2965
|
+
return true;
|
|
2966
|
+
}
|
|
2967
|
+
catch {
|
|
2968
|
+
return false;
|
|
2969
|
+
}
|
|
2915
2970
|
}
|
|
2916
2971
|
/**
|
|
2917
2972
|
* W3.5 Fix B — module-level cache for ESM→CJS transform results, keyed
|
|
@@ -2922,8 +2977,44 @@ function looksLikeEsm(src) {
|
|
|
2922
2977
|
*
|
|
2923
2978
|
* Lives at module scope so warm exec invocations hit the cache without
|
|
2924
2979
|
* paying the wasm cold-start cost again.
|
|
2980
|
+
*
|
|
2981
|
+
* Bounded by bytes, LRU, and reported to the heap model like the prefetch
|
|
2982
|
+
* cache (see ESM_TRANSFORM_CACHE_MAX_BYTES for the reset it caused
|
|
2983
|
+
* unbounded). An output larger than the whole bound is used for the build
|
|
2984
|
+
* that produced it and not retained: admitting it would evict everything
|
|
2985
|
+
* else to hold something that still does not fit.
|
|
2925
2986
|
*/
|
|
2926
2987
|
const __esmTransformCache = new Map();
|
|
2988
|
+
let __esmTransformCacheBytes = 0;
|
|
2989
|
+
function __esmTransformCacheGet(key) {
|
|
2990
|
+
const code = __esmTransformCache.get(key);
|
|
2991
|
+
if (code === undefined)
|
|
2992
|
+
return undefined;
|
|
2993
|
+
// Refresh recency: a Map iterates in insertion order, so the oldest
|
|
2994
|
+
// entry is the first one.
|
|
2995
|
+
__esmTransformCache.delete(key);
|
|
2996
|
+
__esmTransformCache.set(key, code);
|
|
2997
|
+
return code;
|
|
2998
|
+
}
|
|
2999
|
+
function __esmTransformCacheSet(key, code) {
|
|
3000
|
+
const bytes = key.length + code.length;
|
|
3001
|
+
if (bytes > ESM_TRANSFORM_CACHE_MAX_BYTES)
|
|
3002
|
+
return;
|
|
3003
|
+
const previous = __esmTransformCache.get(key);
|
|
3004
|
+
if (previous !== undefined) {
|
|
3005
|
+
__esmTransformCacheBytes -= key.length + previous.length;
|
|
3006
|
+
__esmTransformCache.delete(key);
|
|
3007
|
+
}
|
|
3008
|
+
__esmTransformCache.set(key, code);
|
|
3009
|
+
__esmTransformCacheBytes += bytes;
|
|
3010
|
+
for (const [oldest, entry] of __esmTransformCache) {
|
|
3011
|
+
if (__esmTransformCacheBytes <= ESM_TRANSFORM_CACHE_MAX_BYTES)
|
|
3012
|
+
break;
|
|
3013
|
+
__esmTransformCache.delete(oldest);
|
|
3014
|
+
__esmTransformCacheBytes -= oldest.length + entry.length;
|
|
3015
|
+
}
|
|
3016
|
+
setTransformCacheBytes(__esmTransformCacheBytes);
|
|
3017
|
+
}
|
|
2927
3018
|
function __cacheKey(src) {
|
|
2928
3019
|
// FNV-1a 32-bit. Only used for cache keys, NEVER for content
|
|
2929
3020
|
// integrity. The ~30-byte string we return is a hex hash + length —
|
|
@@ -2959,14 +3050,21 @@ function _markBundleEsmAsFailed(bundle, reason) {
|
|
|
2959
3050
|
// A TypeScript source is never runnable as staged, so it always needs
|
|
2960
3051
|
// the emit it cannot get; a JavaScript file only if it is ESM.
|
|
2961
3052
|
const typescript = bundleTypescriptLoader(path) !== null;
|
|
2962
|
-
if (!typescript && !looksLikeEsm(src))
|
|
3053
|
+
if (!typescript && !looksLikeEsm(path, src))
|
|
2963
3054
|
continue;
|
|
2964
|
-
|
|
2965
|
-
bundle[typescript ? compiledCellKey(path) : path] =
|
|
2966
|
-
'// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
2967
|
-
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
3055
|
+
bundle[typescript ? compiledCellKey(path) : path] = esbuildDiagnosticShim(path, reason);
|
|
2968
3056
|
}
|
|
2969
3057
|
}
|
|
3058
|
+
/**
|
|
3059
|
+
* Parseable CommonJS standing in for a module esbuild could not transform: it
|
|
3060
|
+
* throws the esbuild reason when required, so the failure surfaces at the
|
|
3061
|
+
* `require` with its cause rather than as a bare "Cannot use import statement".
|
|
3062
|
+
*/
|
|
3063
|
+
function esbuildDiagnosticShim(path, reason) {
|
|
3064
|
+
const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason.replace(/\n/g, ' ')}`);
|
|
3065
|
+
return '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
3066
|
+
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
3067
|
+
}
|
|
2970
3068
|
/**
|
|
2971
3069
|
* The set of bundle entries the facet's startup pre-compile loop turns into
|
|
2972
3070
|
* functions, minus the ones already in the right format. Everything the loop
|
|
@@ -2977,7 +3075,8 @@ function _markBundleEsmAsFailed(bundle, reason) {
|
|
|
2977
3075
|
* Extensionless entries are in the set for the same reason the pre-compile
|
|
2978
3076
|
* loop takes them — that is the shape of nearly every npm `bin` script.
|
|
2979
3077
|
* `.json` is data and `.cjs` is CommonJS by definition; neither needs the
|
|
2980
|
-
* transform. Content
|
|
3078
|
+
* transform. Content decides from here: `looksLikeEsm` sniffs module syntax,
|
|
3079
|
+
* and parses an extensionless file, which may be data rather than a script.
|
|
2981
3080
|
*/
|
|
2982
3081
|
export function isBundleModuleCandidate(path) {
|
|
2983
3082
|
const ext = vfsPathExtension(path);
|
|
@@ -2985,7 +3084,8 @@ export function isBundleModuleCandidate(path) {
|
|
|
2985
3084
|
}
|
|
2986
3085
|
/**
|
|
2987
3086
|
* The esbuild loader for a TypeScript source in the bundle, or null when the
|
|
2988
|
-
* path does not name one.
|
|
3087
|
+
* path does not name one. Which extensions are TypeScript is
|
|
3088
|
+
* `typescriptLoader`'s table, the one a runtime's entry script is decided by.
|
|
2989
3089
|
*
|
|
2990
3090
|
* A resolved `.ts` file reaches the facet as TypeScript, and TypeScript is not
|
|
2991
3091
|
* JavaScript: `new Function` on a type annotation is a SyntaxError whether or
|
|
@@ -3004,12 +3104,7 @@ export function isBundleModuleCandidate(path) {
|
|
|
3004
3104
|
* gone. So a declaration file is left exactly as it was staged.
|
|
3005
3105
|
*/
|
|
3006
3106
|
export function bundleTypescriptLoader(path) {
|
|
3007
|
-
|
|
3008
|
-
return null;
|
|
3009
|
-
const ext = vfsPathExtension(path);
|
|
3010
|
-
if (ext === '.tsx')
|
|
3011
|
-
return 'tsx';
|
|
3012
|
-
return ext === '.ts' || ext === '.mts' || ext === '.cts' ? 'ts' : null;
|
|
3107
|
+
return isTypescriptDeclarationFile(path) ? null : typescriptLoader(path);
|
|
3013
3108
|
}
|
|
3014
3109
|
/** `name.d.ts` / `name.d.mts` / `name.d.cts`, by TypeScript's own rule. */
|
|
3015
3110
|
export function isTypescriptDeclarationFile(path) {
|
|
@@ -3078,19 +3173,22 @@ for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
|
3078
3173
|
`;
|
|
3079
3174
|
/**
|
|
3080
3175
|
* Transform every ESM-shaped file in the bundle to CJS via esbuild.
|
|
3081
|
-
* Mutates `bundle` in place.
|
|
3082
|
-
*
|
|
3083
|
-
* into __compileFailures and __loadModule will surface it (Fix C).
|
|
3176
|
+
* Mutates `bundle` in place. A module esbuild rejects becomes a diagnostic
|
|
3177
|
+
* shim that throws the reason when required (`esbuildDiagnosticShim`).
|
|
3084
3178
|
*
|
|
3085
|
-
*
|
|
3086
|
-
*
|
|
3179
|
+
* ESM/TypeScript candidates are `isBundleModuleCandidate`. CommonJS cells,
|
|
3180
|
+
* including .cjs, also visit the rewrite-only path for dynamic import().
|
|
3087
3181
|
*
|
|
3088
3182
|
* A JavaScript cell is rewritten in place. A TypeScript source keeps its
|
|
3089
3183
|
* bytes and gets a compiled cell beside it — see `compiledCellKey`.
|
|
3090
3184
|
*
|
|
3185
|
+
* When the service transforms in another isolate every cell goes in one
|
|
3186
|
+
* `transformMany`, so a launch costs one round trip and this isolate never
|
|
3187
|
+
* grows esbuild's heap. Transforms that run here are paced like any pass.
|
|
3188
|
+
*
|
|
3091
3189
|
* Returns the count of files transformed (for diagnostics).
|
|
3092
3190
|
*/
|
|
3093
|
-
async function transformEsmInBundle(bundle, esbuild, pacer
|
|
3191
|
+
async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
3094
3192
|
let transformed = 0;
|
|
3095
3193
|
let failed = 0;
|
|
3096
3194
|
// Snapshot the keys first — esbuild calls await; never iterate-and-mutate.
|
|
@@ -3098,104 +3196,140 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3098
3196
|
for (const path of Object.keys(bundle)) {
|
|
3099
3197
|
if (compiledCellPath(path) !== null)
|
|
3100
3198
|
continue;
|
|
3101
|
-
if (!isBundleModuleCandidate(path))
|
|
3199
|
+
if (!isBundleModuleCandidate(path) && !path.endsWith('.cjs'))
|
|
3102
3200
|
continue;
|
|
3103
3201
|
const src = bundle[path];
|
|
3104
3202
|
// hardening-r5: binary cells are not ESM. Skip — looksLikeEsm +
|
|
3105
3203
|
// esbuild.transform expect strings.
|
|
3106
3204
|
if (typeof src !== 'string')
|
|
3107
3205
|
continue;
|
|
3108
|
-
if (
|
|
3109
|
-
|
|
3110
|
-
if (!esm)
|
|
3206
|
+
if (path.endsWith('.cjs')) {
|
|
3207
|
+
if (!mayHaveDynamicImport(src))
|
|
3111
3208
|
continue;
|
|
3112
3209
|
}
|
|
3210
|
+
else if (bundleTypescriptLoader(path) === null && !looksLikeEsm(path, src) && !mayHaveDynamicImport(src))
|
|
3211
|
+
continue;
|
|
3113
3212
|
candidates.push(path);
|
|
3114
3213
|
}
|
|
3214
|
+
const settle = (cell, outcome) => {
|
|
3215
|
+
if ('error' in outcome) {
|
|
3216
|
+
// Infrastructure failure is not source code. Abort before the bundle
|
|
3217
|
+
// or immutable loader image can cache a throw-on-require diagnostic.
|
|
3218
|
+
if (outcome.transient)
|
|
3219
|
+
throw new Error(`esbuild transform unavailable for ${cell.path}: ${outcome.error}`);
|
|
3220
|
+
// esbuild's verdict on this source is cached with it; a host that could
|
|
3221
|
+
// not run the transform this time has no verdict to cache.
|
|
3222
|
+
const shim = esbuildDiagnosticShim(cell.path, outcome.error);
|
|
3223
|
+
bundle[cell.target] = shim;
|
|
3224
|
+
__esmTransformCacheSet(cell.key, shim);
|
|
3225
|
+
failed++;
|
|
3226
|
+
return;
|
|
3227
|
+
}
|
|
3228
|
+
const code = bindImportMetaResolve(outcome.code, cell.absUrl);
|
|
3229
|
+
bundle[cell.target] = code;
|
|
3230
|
+
__esmTransformCacheSet(cell.key, code);
|
|
3231
|
+
transformed++;
|
|
3232
|
+
};
|
|
3233
|
+
const transformCells = async (cells) => {
|
|
3234
|
+
let outcomes;
|
|
3235
|
+
try {
|
|
3236
|
+
outcomes = await esbuild.transformMany(cells.map((cell) => cell.request));
|
|
3237
|
+
}
|
|
3238
|
+
catch (e) {
|
|
3239
|
+
// Publishing a whole-bundle diagnostic would poison the next launch
|
|
3240
|
+
// even when the per-cell transform cache correctly rejected it.
|
|
3241
|
+
throw new Error(`esbuild transform service unavailable: ${errorText(e)}`, { cause: e });
|
|
3242
|
+
}
|
|
3243
|
+
cells.forEach((cell, i) => settle(cell, outcomes[i]));
|
|
3244
|
+
};
|
|
3245
|
+
const batch = [];
|
|
3115
3246
|
for (const path of candidates) {
|
|
3116
3247
|
const original = bundle[path];
|
|
3117
3248
|
if (typeof original !== 'string')
|
|
3118
3249
|
continue;
|
|
3119
|
-
const
|
|
3250
|
+
const loader = bundleTypescriptLoader(path);
|
|
3120
3251
|
// A TypeScript source keeps its bytes; its emit lands beside it.
|
|
3121
|
-
const target =
|
|
3122
|
-
//
|
|
3123
|
-
//
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
// `import.meta.url` substitution mirrors the sibling fix at
|
|
3127
|
-
// src/runtime/runtime-registry.ts:383-389 (framework-gaps-fix P5).
|
|
3128
|
-
// Without `define`, esbuild's CJS transform reduces `import.meta.url`
|
|
3129
|
-
// to undefined (single-pass) or preserves it literally — only to
|
|
3130
|
-
// SyntaxError at `new Function(...)` parse time (two-pass via
|
|
3131
|
-
// EsbuildService.transform's async-IIFE wrap → "Cannot use
|
|
3132
|
-
// 'import.meta' outside a module"). The substitution value is the
|
|
3133
|
-
// real `file:///<absolute-path>` URL — exactly what real Node returns
|
|
3134
|
-
// for an ESM module at that path.
|
|
3135
|
-
//
|
|
3136
|
-
// Note: cache key now incorporates the path because the transformed
|
|
3137
|
-
// output is path-specific (the URL literal is baked in). Two files
|
|
3138
|
-
// with identical source but different paths would otherwise share a
|
|
3139
|
-
// cache entry and the second file would get the first file's URL.
|
|
3252
|
+
const target = loader === null ? path : compiledCellKey(path);
|
|
3253
|
+
// Source is transformed once per path; import.meta reads metadata from
|
|
3254
|
+
// each evaluation's module object, including its query and fragment.
|
|
3255
|
+
// The source URL still keys transforms and supplies the static parent
|
|
3256
|
+
// for rewritten dynamic imports and diagnostics.
|
|
3140
3257
|
const absUrl = 'file:///' + path.replace(/^\/+/, '');
|
|
3141
|
-
|
|
3142
|
-
const
|
|
3143
|
-
|
|
3258
|
+
// Keyed on the staged bytes, so a cell the pre-pass fails has a key too.
|
|
3259
|
+
const key = __cacheKey(original + '\0' + absUrl);
|
|
3260
|
+
const cached = __esmTransformCacheGet(key);
|
|
3261
|
+
if (cached !== undefined) {
|
|
3144
3262
|
bundle[target] = cached;
|
|
3145
3263
|
transformed++;
|
|
3146
3264
|
continue;
|
|
3147
3265
|
}
|
|
3266
|
+
// Every cell's dynamic import() is the process's: the transform keeps
|
|
3267
|
+
// them, and the facet rewrites each to the process's ESM loader.
|
|
3268
|
+
const moduleMetadata = !path.endsWith('.cjs') && (loader !== null || looksLikeEsm(path, original));
|
|
3269
|
+
const cellFor = (code, rewriteOnly = false) => ({
|
|
3270
|
+
path,
|
|
3271
|
+
target,
|
|
3272
|
+
key,
|
|
3273
|
+
absUrl,
|
|
3274
|
+
request: {
|
|
3275
|
+
code,
|
|
3276
|
+
options: rewriteOnly
|
|
3277
|
+
? { rewriteOnly: true, dynamicImportParent: absUrl, moduleMetadata }
|
|
3278
|
+
: { loader: loader ?? 'js', format: 'cjs', target: 'esnext', define: importMetaDefines(absUrl, true), dynamicImportParent: absUrl, moduleMetadata },
|
|
3279
|
+
},
|
|
3280
|
+
});
|
|
3281
|
+
let src;
|
|
3148
3282
|
try {
|
|
3149
|
-
|
|
3150
|
-
loader: bundleTypescriptLoader(path) ?? 'js',
|
|
3151
|
-
format: 'cjs',
|
|
3152
|
-
target: 'esnext',
|
|
3153
|
-
define: importMetaDefines(absUrl),
|
|
3154
|
-
};
|
|
3155
|
-
const bounded = transformOptions.loader === 'js' && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
3156
|
-
? rewriteBundledEsmToCjs(src, absUrl)
|
|
3157
|
-
: null;
|
|
3158
|
-
const t = bounded ?? (isolatedTransform && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
3159
|
-
? await isolatedTransform(src, transformOptions)
|
|
3160
|
-
: await esbuild.transform(src, transformOptions));
|
|
3161
|
-
const code = bindImportMetaResolve(t.code, absUrl);
|
|
3162
|
-
bundle[target] = code;
|
|
3163
|
-
__esmTransformCache.set(key, code);
|
|
3164
|
-
transformed++;
|
|
3283
|
+
src = loader === null ? rewriteProvidedCommonJsModules(original) : original;
|
|
3165
3284
|
}
|
|
3166
3285
|
catch (e) {
|
|
3167
|
-
//
|
|
3168
|
-
|
|
3169
|
-
|
|
3170
|
-
|
|
3171
|
-
|
|
3172
|
-
|
|
3173
|
-
|
|
3174
|
-
//
|
|
3175
|
-
|
|
3176
|
-
|
|
3177
|
-
|
|
3178
|
-
|
|
3179
|
-
|
|
3180
|
-
|
|
3181
|
-
|
|
3182
|
-
|
|
3183
|
-
|
|
3184
|
-
|
|
3185
|
-
|
|
3186
|
-
|
|
3187
|
-
|
|
3188
|
-
|
|
3189
|
-
|
|
3190
|
-
|
|
3191
|
-
|
|
3192
|
-
|
|
3193
|
-
|
|
3194
|
-
|
|
3195
|
-
|
|
3196
|
-
|
|
3286
|
+
// The pre-pass cannot read this cell: a verdict on it alone, like esbuild's.
|
|
3287
|
+
settle(cellFor(original), { error: errorText(e) });
|
|
3288
|
+
continue;
|
|
3289
|
+
}
|
|
3290
|
+
// CommonJS already: only its dynamic import() calls change.
|
|
3291
|
+
const cell = path.endsWith('.cjs') || (loader === null && !looksLikeEsm(path, src)) ? cellFor(src, true) : cellFor(src);
|
|
3292
|
+
if (cell.request.options?.rewriteOnly !== true && loader === null && src.length >= BUNDLED_ESM_REWRITE_MIN_BYTES) {
|
|
3293
|
+
// The bounded rewrite is computation in this isolate, however large.
|
|
3294
|
+
if (pacer)
|
|
3295
|
+
await pacer.spend(src.length);
|
|
3296
|
+
let rewritten;
|
|
3297
|
+
try {
|
|
3298
|
+
rewritten = rewriteBundledEsmToCjs(src, absUrl, true);
|
|
3299
|
+
}
|
|
3300
|
+
catch (e) {
|
|
3301
|
+
rewritten = { error: errorText(e) };
|
|
3302
|
+
}
|
|
3303
|
+
if (rewritten && 'error' in rewritten) {
|
|
3304
|
+
settle(cell, rewritten);
|
|
3305
|
+
continue;
|
|
3306
|
+
}
|
|
3307
|
+
if (rewritten) {
|
|
3308
|
+
// Its declarations are CommonJS now; what import() calls remain go to
|
|
3309
|
+
// the facet like any cell's.
|
|
3310
|
+
if (!mayHaveDynamicImport(rewritten.code) && !rewritten.code.includes('import.meta')) {
|
|
3311
|
+
settle(cell, rewritten);
|
|
3312
|
+
continue;
|
|
3313
|
+
}
|
|
3314
|
+
const rest = cellFor(rewritten.code, true);
|
|
3315
|
+
if (esbuild.transformsInIsolate) {
|
|
3316
|
+
await transformCells([rest]);
|
|
3317
|
+
continue;
|
|
3318
|
+
}
|
|
3319
|
+
batch.push(rest);
|
|
3320
|
+
continue;
|
|
3321
|
+
}
|
|
3197
3322
|
}
|
|
3323
|
+
if (esbuild.transformsInIsolate) {
|
|
3324
|
+
if (pacer)
|
|
3325
|
+
await pacer.spend(src.length);
|
|
3326
|
+
await transformCells([cell]);
|
|
3327
|
+
continue;
|
|
3328
|
+
}
|
|
3329
|
+
batch.push(cell);
|
|
3198
3330
|
}
|
|
3331
|
+
if (batch.length > 0)
|
|
3332
|
+
await transformCells(batch);
|
|
3199
3333
|
return { transformed, failed };
|
|
3200
3334
|
}
|
|
3201
3335
|
/**
|
|
@@ -3214,7 +3348,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3214
3348
|
* behaviour for code paths that don't have esbuild handy).
|
|
3215
3349
|
*
|
|
3216
3350
|
*/
|
|
3217
|
-
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer,
|
|
3351
|
+
export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, maxBundleBytes, learnedFor) {
|
|
3218
3352
|
// This build accumulates raw VFS contents in the supervisor heap, and did it
|
|
3219
3353
|
// with nothing watching: the estimator read 9.4 MiB while these bytes were
|
|
3220
3354
|
// resetting the DO three times. Take the budget the enrichment passes are
|
|
@@ -3224,20 +3358,20 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
|
|
|
3224
3358
|
const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
|
|
3225
3359
|
prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
|
|
3226
3360
|
try {
|
|
3227
|
-
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer,
|
|
3361
|
+
return await _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile, observedReads, pacer, maxBundleBytes, learnedFor);
|
|
3228
3362
|
}
|
|
3229
3363
|
finally {
|
|
3230
3364
|
prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
|
|
3231
3365
|
lease.release();
|
|
3232
3366
|
}
|
|
3233
3367
|
}
|
|
3234
|
-
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer,
|
|
3368
|
+
async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bundleProfile = DEFAULT_FACET_BUNDLE_PROFILE, observedReads, pacer, maxBundleBytes = VFS_BUNDLE_MAX_BYTES, learnedFor) {
|
|
3235
3369
|
// Read the cursor BEFORE the walk: a mutation that lands while the bundle
|
|
3236
3370
|
// is being assembled must be reported as invalidated, not silently missed.
|
|
3237
|
-
const admitted = await vfs.
|
|
3371
|
+
const admitted = await vfs.acquire(null, 0);
|
|
3238
3372
|
const cursor = { epoch: admitted.epoch, rev: admitted.rev };
|
|
3239
3373
|
// 1. Static reachable-set walk from entry.
|
|
3240
|
-
const prefetch = (await prefetchForRequire(vfs, entryCode || '', cwd, scriptPath, maxBundleBytes));
|
|
3374
|
+
const prefetch = (await prefetchForRequire(requireFsOverBridge(vfs), entryCode || '', cwd, scriptPath, maxBundleBytes, pacer?.spend.bind(pacer)));
|
|
3241
3375
|
if ('kind' in prefetch) {
|
|
3242
3376
|
// A required closure larger than the bound can never launch as a
|
|
3243
3377
|
// snapshot. Surface it as the process's own failure rather than a
|
|
@@ -3245,7 +3379,16 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3245
3379
|
throw new ClosureBoundExceededError(prefetch);
|
|
3246
3380
|
}
|
|
3247
3381
|
const bundle = { ...prefetch.bundle };
|
|
3248
|
-
const
|
|
3382
|
+
const closurePaths = new Set(Object.keys(prefetch.bundle).filter((path) => !prefetch.speculative.has(path)));
|
|
3383
|
+
// Evidence is kept apart from the closure: an observed subpath is not the
|
|
3384
|
+
// closure choosing that corner of a package, and only the closure can fail
|
|
3385
|
+
// a launch. The room evidence may fill is what the closure leaves under the
|
|
3386
|
+
// bound, this session's own misses first, then what other sessions learned.
|
|
3387
|
+
const observedPaths = new Set();
|
|
3388
|
+
const learnedPaths = new Set();
|
|
3389
|
+
let room = maxBundleBytes;
|
|
3390
|
+
for (const path of closurePaths)
|
|
3391
|
+
room -= _bundleCellRawBytes(bundle[path]);
|
|
3249
3392
|
let truncated = false;
|
|
3250
3393
|
const budgetState = { totalBytes: 0, fileCount: 0 };
|
|
3251
3394
|
// Each enrichment pass below re-scans the bundle accumulated so far, so a
|
|
@@ -3267,14 +3410,23 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3267
3410
|
// admitted first, ahead of every guess, and they join the required set
|
|
3268
3411
|
// rather than the evictable one, because a file evicted here misses
|
|
3269
3412
|
// again on the next run and the loop never closes.
|
|
3270
|
-
|
|
3271
|
-
|
|
3413
|
+
// Other sessions' misses in the packages this closure can load
|
|
3414
|
+
// (read-profile.ts) are evidence of the same kind: a learned module joins
|
|
3415
|
+
// the module map with its imports, as this session's own misses do.
|
|
3416
|
+
const learned = learnedFor ? await learnedFor(Object.keys(bundle)).catch(() => []) : [];
|
|
3417
|
+
const own = await addObservedReads(vfs, observedReads, bundle, observedPaths, budgetState, Math.max(0, room), pacer);
|
|
3418
|
+
room -= own.bytes;
|
|
3419
|
+
const learnedOnly = new Set(learned.filter((path) => !observedReads?.has(path) && !observedPaths.has(path)));
|
|
3420
|
+
await addObservedReads(vfs, learnedOnly, bundle, learnedPaths, budgetState, Math.max(0, room), pacer);
|
|
3421
|
+
for (const path of observedPaths)
|
|
3422
|
+
learnedPaths.delete(path);
|
|
3272
3423
|
await paceAfterPass();
|
|
3273
3424
|
// 2. Greedy oversample — every installed pkg's pkg.json + main.
|
|
3274
3425
|
// Catches dynamic-require / `bindings()` / plugin-loader cases the
|
|
3275
3426
|
// regex prefetch misses. Its budget is independent from the complete
|
|
3276
3427
|
// static require closure, which is correctness-critical.
|
|
3277
|
-
const
|
|
3428
|
+
const independentBeforeGroups = new Set(Object.keys(bundle));
|
|
3429
|
+
const greedy = (await greedyAddMainEntries(vfs, cwd, bundle, budgetState, closurePaths, { maxBundleBytes, pacer }));
|
|
3278
3430
|
await paceAfterPass();
|
|
3279
3431
|
// 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
|
|
3280
3432
|
// bundle .js/.mjs/.cjs source for the canonical jsdom shape:
|
|
@@ -3349,31 +3501,17 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3349
3501
|
// ESM files (e.g. tldts/dist/es6/index.js, @remix-run/react/dist/esm,
|
|
3350
3502
|
// @tailwindcss/vite, react-remove-scroll, astro) silently fail
|
|
3351
3503
|
// `new Function` at facet startup and surface as the misleading
|
|
3352
|
-
// "
|
|
3504
|
+
// "not in this launch's module map" at request time.
|
|
3353
3505
|
if (esbuild) {
|
|
3354
|
-
|
|
3355
|
-
|
|
3356
|
-
|
|
3357
|
-
|
|
3358
|
-
// through the transform because the eviction loop below recomputes
|
|
3359
|
-
// the encoded size from scratch anyway.
|
|
3360
|
-
}
|
|
3361
|
-
catch (e) {
|
|
3362
|
-
// framework-fixes-F4 (2026-05-12): the prior catch was silent.
|
|
3363
|
-
// Replace every detected-ESM source with the SAME diagnostic
|
|
3364
|
-
// shim transformEsmInBundle's per-file catch installs, so the
|
|
3365
|
-
// user sees "esbuild transform failed (service-level): <reason>"
|
|
3366
|
-
// instead of a bare "Cannot use import statement..." parse
|
|
3367
|
-
// error with no context.
|
|
3368
|
-
const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
|
|
3369
|
-
_markBundleEsmAsFailed(bundle, `esbuild service unavailable: ${reason}`);
|
|
3370
|
-
}
|
|
3506
|
+
// Transient failures propagate through the launch failure path before
|
|
3507
|
+
// serialization/cache/LOADER publication. Per-source verdicts still use
|
|
3508
|
+
// the lazy diagnostic cells installed by transformEsmInBundle.
|
|
3509
|
+
await transformEsmInBundle(bundle, esbuild, pacer);
|
|
3371
3510
|
}
|
|
3372
3511
|
else {
|
|
3373
|
-
//
|
|
3374
|
-
//
|
|
3375
|
-
|
|
3376
|
-
_markBundleEsmAsFailed(bundle, 'esbuild service not initialized (likely lazy-init failure)');
|
|
3512
|
+
// No esbuild service was given: the ESM cells stage as diagnostics that
|
|
3513
|
+
// say so, rather than as source `new Function` rejects without a reason.
|
|
3514
|
+
_markBundleEsmAsFailed(bundle, 'no esbuild service was given to this launch');
|
|
3377
3515
|
}
|
|
3378
3516
|
for (const path of Object.keys(bundle)) {
|
|
3379
3517
|
if (compiledCellPath(path) === null && (!isBundleModuleCandidate(path) || bundleTypescriptLoader(path) !== null))
|
|
@@ -3382,61 +3520,165 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3382
3520
|
if (typeof source !== 'string')
|
|
3383
3521
|
continue;
|
|
3384
3522
|
await pacer?.spend(source.length);
|
|
3385
|
-
|
|
3523
|
+
try {
|
|
3524
|
+
bundle[path] = rewriteProvidedCommonJsModules(source);
|
|
3525
|
+
}
|
|
3526
|
+
catch {
|
|
3527
|
+
// Unparseable, so not a module and no bundled records to bind: data such as a LICENSE.
|
|
3528
|
+
}
|
|
3386
3529
|
}
|
|
3387
3530
|
await paceAfterPass();
|
|
3388
|
-
//
|
|
3389
|
-
//
|
|
3390
|
-
//
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
//
|
|
3395
|
-
// units) because that is what workerd charges against the per-module
|
|
3396
|
-
// text-size budget. Only OPTIONAL enrichment is evictable, largest
|
|
3397
|
-
// first; the manifest and the static require closure stay.
|
|
3531
|
+
// 4. The snapshot's size guard, in the unit the session DO's memory was
|
|
3532
|
+
// measured in and the require walk counts: raw bytes, a cell's UTF-8
|
|
3533
|
+
// length (VFS_BUNDLE_MAX_BYTES, platform/limits.ts, unless the caller
|
|
3534
|
+
// set its own). The per-module text limit is met by side modules and is
|
|
3535
|
+
// not a reason to evict. Only
|
|
3536
|
+
// OPTIONAL enrichment is evictable; the static require closure stays, and
|
|
3537
|
+
// a closure past the bound by itself fails the launch by name.
|
|
3398
3538
|
//
|
|
3399
3539
|
// Evicting an enrichment file is a real loss — the sync fs reads it
|
|
3400
3540
|
// exists for cannot fall back to the supervisor — so the paths that
|
|
3401
3541
|
// went are named rather than silently dropped.
|
|
3402
|
-
const size = encodedBundleSize(bundle
|
|
3403
|
-
|
|
3542
|
+
const size = encodedBundleSize(bundle);
|
|
3543
|
+
const rawBytes = new Map();
|
|
3544
|
+
let rawTotal = 0;
|
|
3545
|
+
for (const [path, cell] of Object.entries(bundle)) {
|
|
3546
|
+
const bytes = _bundleCellRawBytes(cell);
|
|
3547
|
+
rawBytes.set(path, bytes);
|
|
3548
|
+
rawTotal += bytes;
|
|
3549
|
+
}
|
|
3550
|
+
if (rawTotal > maxBundleBytes) {
|
|
3551
|
+
// Only the walk's closure can fail a launch, and the path it names is the
|
|
3552
|
+
// one whose bytes (with its compiled cell) crossed the bound, in walk order.
|
|
3553
|
+
let closureBytes = 0;
|
|
3554
|
+
let crossedAt = null;
|
|
3555
|
+
for (const path of closurePaths) {
|
|
3556
|
+
closureBytes += (rawBytes.get(path) ?? 0) + (rawBytes.get(compiledCellKey(path)) ?? 0);
|
|
3557
|
+
if (crossedAt === null && closureBytes > maxBundleBytes)
|
|
3558
|
+
crossedAt = path;
|
|
3559
|
+
}
|
|
3560
|
+
if (crossedAt !== null) {
|
|
3561
|
+
throw new ClosureBoundExceededError({
|
|
3562
|
+
kind: 'closure-exceeds-bound',
|
|
3563
|
+
entry: scriptPath ?? 'entry code',
|
|
3564
|
+
bytesSeen: closureBytes,
|
|
3565
|
+
bound: maxBundleBytes,
|
|
3566
|
+
lastPath: crossedAt,
|
|
3567
|
+
});
|
|
3568
|
+
}
|
|
3404
3569
|
// A compiled cell goes with its source: required when the source is.
|
|
3405
|
-
|
|
3406
|
-
|
|
3407
|
-
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
|
|
3570
|
+
//
|
|
3571
|
+
// Order matters as much as the bound. Largest-first alone ranks a cell by
|
|
3572
|
+
// what it costs and never by what losing it costs: an admitted module and
|
|
3573
|
+
// the sibling it imports are both "optional", and shedding the sibling
|
|
3574
|
+
// leaves a module in the bundle that cannot load. On `astro dev` the
|
|
3575
|
+
// snapshot breached its bound by 591 files, and among the largest were
|
|
3576
|
+
// modules while 3,291 declaration files (never a require target) stayed.
|
|
3577
|
+
//
|
|
3578
|
+
// Unloadable cells first, then enrichment largest-first, then dynamic-import
|
|
3579
|
+
// subtrees last-discovered first, so a CLI's own `import()` deferral is shed last.
|
|
3580
|
+
const speculativeRank = new Map();
|
|
3581
|
+
for (const path of prefetch.speculative)
|
|
3582
|
+
speculativeRank.set(path, speculativeRank.size);
|
|
3583
|
+
const rankOf = (path) => speculativeRank.get(compiledCellPath(path) ?? path) ?? -1;
|
|
3584
|
+
// Evidence goes after all enrichment: what others learned before this
|
|
3585
|
+
// session's own misses, each tier last-admitted first.
|
|
3586
|
+
const tierOf = (path) => {
|
|
3587
|
+
const source = compiledCellPath(path) ?? path;
|
|
3588
|
+
return closurePaths.has(source) ? 3 : observedPaths.has(source) ? 2 : learnedPaths.has(source) ? 1 : 0;
|
|
3589
|
+
};
|
|
3590
|
+
const members = new Set();
|
|
3591
|
+
for (const group of greedy.groups)
|
|
3592
|
+
for (const path of group.members)
|
|
3593
|
+
members.add(path);
|
|
3594
|
+
const sources = new Set(Object.keys(bundle).map(path => compiledCellPath(path) ?? path));
|
|
3595
|
+
const independent = new Set([...sources].filter(path => !members.has(path) || independentBeforeGroups.has(path)));
|
|
3596
|
+
const keptGroups = new Set(greedy.groups);
|
|
3597
|
+
const weight = (path) => (rawBytes.get(path) ?? 0) + (rawBytes.get(compiledCellKey(path)) ?? 0);
|
|
3598
|
+
const unitBytes = new Map();
|
|
3599
|
+
for (const group of greedy.groups) {
|
|
3600
|
+
let total = 0;
|
|
3601
|
+
for (const member of group.members)
|
|
3602
|
+
if (!independent.has(member))
|
|
3603
|
+
total += weight(member);
|
|
3604
|
+
unitBytes.set(group.root, total);
|
|
3605
|
+
if (pacer)
|
|
3606
|
+
await pacer.spend(group.members.size * 256);
|
|
3607
|
+
}
|
|
3608
|
+
const lastAdmittedFirst = (tier) => [...tier].reverse().filter(path => independent.has(path));
|
|
3609
|
+
const enrichment = [...independent, ...greedy.groups.map(group => group.root)]
|
|
3610
|
+
.filter(path => tierOf(path) === 0)
|
|
3611
|
+
.sort((a, b) => {
|
|
3612
|
+
const loadable = (_isLoadableModuleCell(a) ? 1 : 0) - (_isLoadableModuleCell(b) ? 1 : 0);
|
|
3613
|
+
if (loadable !== 0)
|
|
3614
|
+
return loadable;
|
|
3615
|
+
const rankA = rankOf(a);
|
|
3616
|
+
const rankB = rankOf(b);
|
|
3617
|
+
if (rankA !== rankB)
|
|
3618
|
+
return rankA < 0 ? -1 : rankB < 0 ? 1 : rankB - rankA;
|
|
3619
|
+
return (unitBytes.get(b) ?? weight(b)) - (unitBytes.get(a) ?? weight(a));
|
|
3620
|
+
});
|
|
3621
|
+
const evictable = [...enrichment, ...lastAdmittedFirst(learnedPaths), ...lastAdmittedFirst(observedPaths)];
|
|
3622
|
+
async function retainedPaths() {
|
|
3623
|
+
const retained = new Set(independent);
|
|
3624
|
+
for (const group of keptGroups) {
|
|
3625
|
+
let work = 0;
|
|
3626
|
+
for (const path of group.members) {
|
|
3627
|
+
retained.add(path);
|
|
3628
|
+
work += path.length + 256;
|
|
3629
|
+
}
|
|
3630
|
+
if (pacer)
|
|
3631
|
+
await pacer.spend(work);
|
|
3632
|
+
}
|
|
3633
|
+
for (const source of [...retained]) {
|
|
3634
|
+
const compiled = compiledCellKey(source);
|
|
3635
|
+
if (bundle[compiled] !== undefined)
|
|
3636
|
+
retained.add(compiled);
|
|
3637
|
+
}
|
|
3638
|
+
return retained;
|
|
3639
|
+
}
|
|
3640
|
+
let retained = await retainedPaths();
|
|
3641
|
+
for (const path of evictable) {
|
|
3642
|
+
if (rawTotal <= maxBundleBytes)
|
|
3411
3643
|
break;
|
|
3412
|
-
|
|
3644
|
+
if (independent.delete(path)) {
|
|
3645
|
+
for (const group of keptGroups)
|
|
3646
|
+
if (group.members.has(path))
|
|
3647
|
+
keptGroups.delete(group);
|
|
3648
|
+
}
|
|
3649
|
+
else {
|
|
3650
|
+
for (const group of keptGroups)
|
|
3651
|
+
if (group.root === path)
|
|
3652
|
+
keptGroups.delete(group);
|
|
3653
|
+
}
|
|
3654
|
+
retained = await retainedPaths();
|
|
3655
|
+
rawTotal = 0;
|
|
3656
|
+
for (const path of retained)
|
|
3657
|
+
rawTotal += rawBytes.get(path) ?? 0;
|
|
3658
|
+
}
|
|
3659
|
+
const evicted = [];
|
|
3660
|
+
for (const k of Object.keys(bundle)) {
|
|
3661
|
+
if (retained.has(k))
|
|
3662
|
+
continue;
|
|
3663
|
+
evicted.push([k, rawBytes.get(k) ?? 0]);
|
|
3413
3664
|
delete bundle[k];
|
|
3414
3665
|
size.remove(k);
|
|
3415
3666
|
}
|
|
3416
3667
|
if (evicted.length > 0) {
|
|
3417
3668
|
truncated = true;
|
|
3418
|
-
console.warn(`[facet-manager] prefetch snapshot exceeded ${
|
|
3419
|
-
+ `
|
|
3669
|
+
console.warn(`[facet-manager] prefetch snapshot exceeded ${maxBundleBytes} bytes; `
|
|
3670
|
+
+ `evicted ${evicted.length} optional file(s). They still exist and `
|
|
3420
3671
|
+ `async reads still return them; synchronous reads raise EAGAIN: `
|
|
3421
3672
|
+ `${describeBundleCells(evicted)}`);
|
|
3422
3673
|
}
|
|
3423
3674
|
}
|
|
3424
3675
|
const fileCount = Object.keys(bundle).length;
|
|
3425
|
-
const metadata = (await buildVfsMetadata(vfs, manifest, bundle));
|
|
3426
|
-
(await addUnreadableDenialCells(vfs, bundle, metadata));
|
|
3427
|
-
await paceAfterPass();
|
|
3428
|
-
// Denial cells land after the eviction pass, so account for them before
|
|
3429
|
-
// deciding where the bundle has to live.
|
|
3430
|
-
for (const [path, cell] of Object.entries(bundle))
|
|
3431
|
-
size.add(path, cell);
|
|
3432
3676
|
const bundleSideModulesRequired = size.bytes > BUNDLE_MAX_ENCODED_BYTES;
|
|
3433
3677
|
// Suppress lint: `greedy.added` is observed only via diagnostics.
|
|
3434
3678
|
void greedy;
|
|
3435
3679
|
const wasmImages = (await collectClosureWasmImages(vfs, bundle, binSiblingAdd.wasmPaths));
|
|
3436
3680
|
return {
|
|
3437
3681
|
bundle,
|
|
3438
|
-
manifest,
|
|
3439
|
-
metadata,
|
|
3440
3682
|
cursor,
|
|
3441
3683
|
reachableCount: fileCount,
|
|
3442
3684
|
truncated,
|
|
@@ -3518,24 +3760,27 @@ export class FacetManager {
|
|
|
3518
3760
|
// spawn created as an OS-child of the attach TUI. When the attach process
|
|
3519
3761
|
// exits (reported / killed), its serve facet is torn down with it.
|
|
3520
3762
|
_pairedServeFacet = new Map();
|
|
3763
|
+
// The bundle a resident pid booted from, so the misses it reports at exit
|
|
3764
|
+
// stage into the next launch of the same entry, as a one-shot's do.
|
|
3765
|
+
residentBundleKeys = new Map();
|
|
3766
|
+
/** Per resident pid: the shared read profile's entries its launch staged, settled at its exit. */
|
|
3767
|
+
residentProfileOffers = new Map();
|
|
3521
3768
|
/**
|
|
3522
|
-
*
|
|
3523
|
-
*
|
|
3524
|
-
*
|
|
3525
|
-
* `setEsbuildService` lets NimbusSession share its existing instance
|
|
3526
|
-
* to avoid double-init.
|
|
3769
|
+
* The esbuild the bundle's ESM→CJS pass transforms with. composeFacetManager
|
|
3770
|
+
* sets it: the host's own, or one whose transforms run in the session's
|
|
3771
|
+
* esbuild facet. Never one of this isolate: esbuild-wasm's heap only grows.
|
|
3527
3772
|
*/
|
|
3528
3773
|
esbuild = null;
|
|
3529
3774
|
/**
|
|
3530
3775
|
* Prefetch-bundle cache. buildPrefetchBundle does a full VFS reachable-set
|
|
3531
3776
|
* walk + greedy oversample + esbuild ESM→CJS pass on EVERY foreground
|
|
3532
3777
|
* exec — dominant wall-clock on large node_modules. This memoizes the
|
|
3533
|
-
* result (including the serialized facet bundle
|
|
3778
|
+
* result (including the serialized facet bundle) keyed on
|
|
3534
3779
|
* (bundleProfile, cwd, scriptPath, entryCode identity).
|
|
3535
3780
|
*
|
|
3536
3781
|
* Correctness watermark: the GLOBAL SqliteVFS revision. buildPrefetchBundle
|
|
3537
3782
|
* reads from paths that can lie anywhere in the VFS (addEntryAbsPathReads
|
|
3538
|
-
* pulls absolute-path literals like /tmp/x
|
|
3783
|
+
* pulls absolute-path literals like /tmp/x),
|
|
3539
3784
|
* so a cwd-scoped subtree revision cannot guarantee invalidation. The
|
|
3540
3785
|
* global revision bumps on ANY write, so the cache invalidates on every
|
|
3541
3786
|
* mutation that could change any file the bundle reads — provably
|
|
@@ -3556,6 +3801,35 @@ export class FacetManager {
|
|
|
3556
3801
|
* already deliver for the case that matters: running the command again.
|
|
3557
3802
|
*/
|
|
3558
3803
|
residencyProfiles = new Map();
|
|
3804
|
+
/**
|
|
3805
|
+
* Misses shared across sessions per installed package (read-profile.ts),
|
|
3806
|
+
* kept in the npm tarball cache bucket (NPM_TARBALL_CACHE) beside the
|
|
3807
|
+
* tarballs. Unbound, a miss is learned for this session only
|
|
3808
|
+
* (residencyProfiles).
|
|
3809
|
+
*/
|
|
3810
|
+
readProfile = null;
|
|
3811
|
+
/** Read-profile changes dropped after losing every write race. */
|
|
3812
|
+
readProfileConflicts = 0;
|
|
3813
|
+
/** Per module path: its static references at a revision (see _closureStaticRefs). */
|
|
3814
|
+
staticRefsMemo = new Map();
|
|
3815
|
+
/** Modules whose references are remembered: a few programs' closures. */
|
|
3816
|
+
static STATIC_REFS_MEMO_MAX = 16_384;
|
|
3817
|
+
/**
|
|
3818
|
+
* What the prefetch cache holds right now, for /api/_diag/memory: each
|
|
3819
|
+
* entry's key, the revision it was built at, and its retained bytes, next
|
|
3820
|
+
* to the filesystem's live revision and the residency profiles. A launch
|
|
3821
|
+
* that rebuilds where a hit was expected is explained by exactly these:
|
|
3822
|
+
* the revision moved, the key changed, or a miss profile dropped the entry.
|
|
3823
|
+
*/
|
|
3824
|
+
prefetchCacheDiag() {
|
|
3825
|
+
return {
|
|
3826
|
+
revision: this.vfs ? this.vfs.revision() : null,
|
|
3827
|
+
entries: [...this.prefetchBundleCache].map(([key, entry]) => ({
|
|
3828
|
+
key, revision: entry.revision, bytes: entry.bytes,
|
|
3829
|
+
})),
|
|
3830
|
+
residencyProfiles: [...this.residencyProfiles].map(([key, paths]) => ({ key, paths: [...paths] })),
|
|
3831
|
+
};
|
|
3832
|
+
}
|
|
3559
3833
|
/** In-flight request-driven durable-app ensures, single-flight per port. */
|
|
3560
3834
|
ensureInflight = new Map();
|
|
3561
3835
|
/** Per-pid chain of journal-row amendments; see `_amendRow`. */
|
|
@@ -3593,6 +3867,12 @@ export class FacetManager {
|
|
|
3593
3867
|
? Reflect.get(env, 'NIMBUS_DEBUG')
|
|
3594
3868
|
: undefined;
|
|
3595
3869
|
this.debugEnabled = debugVar === '1' || debugVar === 'true';
|
|
3870
|
+
const profiles = ((typeof env === 'object' || typeof env === 'function') && env !== null)
|
|
3871
|
+
? Reflect.get(env, 'NPM_TARBALL_CACHE')
|
|
3872
|
+
: undefined;
|
|
3873
|
+
this.readProfile = profiles && typeof profiles === 'object'
|
|
3874
|
+
? new ReadProfile(profiles, Date.now, () => { this.readProfileConflicts++; })
|
|
3875
|
+
: null;
|
|
3596
3876
|
this.launchJournal = new FencedWork(ctx.storage, {
|
|
3597
3877
|
generationBase: () => this.processes.pidBase,
|
|
3598
3878
|
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
@@ -3617,8 +3897,31 @@ export class FacetManager {
|
|
|
3617
3897
|
// Rooted on waitUntil: the hook fires synchronously inside whatever turn
|
|
3618
3898
|
// ended the process, and the delete must not be a floating promise there.
|
|
3619
3899
|
this.processes.setOnTerminal((pid) => {
|
|
3900
|
+
this.residentBundleKeys.delete(pid);
|
|
3901
|
+
this.residentProfileOffers.delete(pid);
|
|
3620
3902
|
this.ctx.waitUntil(this.trackLaunchTask(this._onResidentTerminal(pid)));
|
|
3621
3903
|
});
|
|
3904
|
+
this.processes.setDefaultSignalAction((pid, code, signal) => this._endBySignal(pid, code, signal));
|
|
3905
|
+
}
|
|
3906
|
+
/**
|
|
3907
|
+
* A signal's default action: the process ends with 128+signo whether its
|
|
3908
|
+
* facet is still being built (the launch stops at its next ownership gate)
|
|
3909
|
+
* or already booted (its resources are released like a kill).
|
|
3910
|
+
*/
|
|
3911
|
+
_endBySignal(pid, code, signal) {
|
|
3912
|
+
if (this.processes.get(pid)?.state !== 'running')
|
|
3913
|
+
return;
|
|
3914
|
+
this.portRegistry.unregisterByPid(pid);
|
|
3915
|
+
this.releaseProcessRpcResources(pid);
|
|
3916
|
+
this.revokeProcessVfsWriters(pid);
|
|
3917
|
+
this.processes.exit(pid, code);
|
|
3918
|
+
this.processes.markExit(pid, code, signal);
|
|
3919
|
+
this.processes.closeInput(pid);
|
|
3920
|
+
try {
|
|
3921
|
+
this.hooks.onExternalExit?.(pid, code, signal);
|
|
3922
|
+
}
|
|
3923
|
+
catch { }
|
|
3924
|
+
this._teardownPairedServeFacet(pid);
|
|
3622
3925
|
}
|
|
3623
3926
|
/**
|
|
3624
3927
|
* The process is over. Every end-of-life passes through here: a clean
|
|
@@ -3757,12 +4060,42 @@ export class FacetManager {
|
|
|
3757
4060
|
}
|
|
3758
4061
|
return vfs;
|
|
3759
4062
|
}
|
|
4063
|
+
/** Give the bundle's ESM→CJS pass the host's esbuild, as composeFacetManager does. */
|
|
4064
|
+
setEsbuildService(esbuild) { this.esbuild = esbuild; }
|
|
3760
4065
|
/**
|
|
3761
|
-
*
|
|
3762
|
-
* the ESM
|
|
3763
|
-
*
|
|
4066
|
+
* The entry script as the facet compiles it: each dynamic `import()` routed
|
|
4067
|
+
* to the process's ESM loader, with the entry's own URL as the parent (Node
|
|
4068
|
+
* names `-e` code `<cwd>/[eval]` and stdin `<cwd>/[stdin]`). The parse runs
|
|
4069
|
+
* in the esbuild facet like every cell's, and is cached by content. The
|
|
4070
|
+
* module-map walk reads the script as written, before this.
|
|
3764
4071
|
*/
|
|
3765
|
-
|
|
4072
|
+
async _entryDynamicImports(code, filename, cwd) {
|
|
4073
|
+
if (!mayHaveDynamicImport(code))
|
|
4074
|
+
return code;
|
|
4075
|
+
if (this.esbuild === null)
|
|
4076
|
+
throw new Error('entry dynamic import requires the transform service');
|
|
4077
|
+
const base = cwd.replace(/\/+$/, '') || '/';
|
|
4078
|
+
const path = filename === undefined || filename === '<eval>'
|
|
4079
|
+
? `${base}/[eval]`
|
|
4080
|
+
: filename === '[stdin]' ? `${base}/[stdin]` : filename;
|
|
4081
|
+
const parentUrl = 'file:///' + path.replace(/^\/+/, '');
|
|
4082
|
+
const key = __cacheKey(code + '\0' + parentUrl + '\0entry');
|
|
4083
|
+
const cached = __esmTransformCacheGet(key);
|
|
4084
|
+
if (cached !== undefined)
|
|
4085
|
+
return cached;
|
|
4086
|
+
const [outcome] = await this.esbuild.transformMany([
|
|
4087
|
+
{ code, options: { rewriteOnly: true, dynamicImportParent: parentUrl } },
|
|
4088
|
+
]);
|
|
4089
|
+
// A failed entry rewrite must not reach a Worker Loader as native host
|
|
4090
|
+
// import(), or become an immutable cached image. Preserve the reported
|
|
4091
|
+
// reason for permanent source errors as well as transient failures.
|
|
4092
|
+
if (outcome === undefined)
|
|
4093
|
+
throw new Error('entry transform service returned no outcome');
|
|
4094
|
+
if ('error' in outcome)
|
|
4095
|
+
throw new Error(`entry dynamic import transform failed: ${outcome.error}`);
|
|
4096
|
+
__esmTransformCacheSet(key, outcome.code);
|
|
4097
|
+
return outcome.code;
|
|
4098
|
+
}
|
|
3766
4099
|
/**
|
|
3767
4100
|
* The pacer every launch is built under: the session's alarm-driven turn
|
|
3768
4101
|
* pump, the deployment's chunk bound, and the one check a suspended launch
|
|
@@ -3820,36 +4153,211 @@ export class FacetManager {
|
|
|
3820
4153
|
return modules;
|
|
3821
4154
|
if (!this.filesystem)
|
|
3822
4155
|
throw new Error('Process filesystem authority is not initialized');
|
|
3823
|
-
const vfs =
|
|
4156
|
+
const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
|
|
3824
4157
|
for (const image of wasmImports) {
|
|
3825
|
-
|
|
3826
|
-
|
|
3827
|
-
bytes = (await vfs.readFile(stripLeadingSlashes(image.vfsPath)));
|
|
3828
|
-
}
|
|
3829
|
-
catch {
|
|
4158
|
+
const bytes = await filesOf(vfs).readBytes(stripLeadingSlashes(image.vfsPath)).catch(() => null);
|
|
4159
|
+
if (bytes === null)
|
|
3830
4160
|
continue;
|
|
3831
|
-
}
|
|
3832
4161
|
modules[image.moduleName] = {
|
|
3833
4162
|
wasm: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
|
3834
4163
|
};
|
|
3835
4164
|
}
|
|
3836
4165
|
return modules;
|
|
3837
4166
|
}
|
|
4167
|
+
/**
|
|
4168
|
+
* Which contents a resident process holds from its first instruction,
|
|
4169
|
+
* beyond its module map: data-plan.ts over this process's view of the
|
|
4170
|
+
* namespace. A path the plan leaves out is still named and stat-able; a
|
|
4171
|
+
* synchronous read of it is the one honest miss.
|
|
4172
|
+
*/
|
|
4173
|
+
async _planResidentData(entry, vfsState, cwd, home, pacer) {
|
|
4174
|
+
if (!this.vfs || !this.filesystem)
|
|
4175
|
+
return { paths: [], storageBytes: 0 };
|
|
4176
|
+
const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
|
|
4177
|
+
const started = Date.now();
|
|
4178
|
+
const trace = (what) => {
|
|
4179
|
+
if (!this.debugEnabled)
|
|
4180
|
+
return;
|
|
4181
|
+
this.processes.appendOutput(entry.pid, 'stderr', `[nimbus-debug] data plan: ${what} at ${Date.now() - started} ms\n`);
|
|
4182
|
+
};
|
|
4183
|
+
trace('static references');
|
|
4184
|
+
const refs = await this._closureStaticRefs(vfs, vfsState.bundlePaths ?? [], pacer);
|
|
4185
|
+
trace(`${refs.length} modules name paths; learned reads`);
|
|
4186
|
+
const learned = await this._learnedReads(vfsState, entry.cred);
|
|
4187
|
+
trace(`${learned.length} learned; listing`);
|
|
4188
|
+
let pages = 0;
|
|
4189
|
+
let names = 0;
|
|
4190
|
+
const plan = await planFacetData({
|
|
4191
|
+
list: async (after) => {
|
|
4192
|
+
const page = await vfs.list(after, FS_LIST_PAGE_LIMIT);
|
|
4193
|
+
names += page.entries.length;
|
|
4194
|
+
trace(`page ${++pages}: ${page.entries.length} entries`);
|
|
4195
|
+
return { entries: page.entries, next: page.next };
|
|
4196
|
+
},
|
|
4197
|
+
readText: async (path) => {
|
|
4198
|
+
try {
|
|
4199
|
+
return await filesOf(vfs).readFileString(path);
|
|
4200
|
+
}
|
|
4201
|
+
catch {
|
|
4202
|
+
return null;
|
|
4203
|
+
}
|
|
4204
|
+
},
|
|
4205
|
+
// A missing or unreadable component is not a link: the lookup ends there.
|
|
4206
|
+
readlink: async (path) => {
|
|
4207
|
+
try {
|
|
4208
|
+
return await vfs.readlink(path);
|
|
4209
|
+
}
|
|
4210
|
+
catch {
|
|
4211
|
+
return null;
|
|
4212
|
+
}
|
|
4213
|
+
},
|
|
4214
|
+
stat: async (path) => {
|
|
4215
|
+
const st = await filesOf(vfs).stat(path).catch(() => null);
|
|
4216
|
+
return st && { kind: st.type, size: st.size };
|
|
4217
|
+
},
|
|
4218
|
+
}, {
|
|
4219
|
+
cwd,
|
|
4220
|
+
home: home || '/home/user',
|
|
4221
|
+
closure: vfsState.bundlePaths ?? [],
|
|
4222
|
+
refs,
|
|
4223
|
+
learned,
|
|
4224
|
+
spend: (units) => pacer.spend(units),
|
|
4225
|
+
});
|
|
4226
|
+
trace(`planned ${plan.paths.length}`);
|
|
4227
|
+
// What the facet's store will hold (N18), in the cost its store charges
|
|
4228
|
+
// itself (facet-resident-store's __residentCellCost): the planned bytes on
|
|
4229
|
+
// overflow pages (1% over), a head and chunk rows per file, a namespace
|
|
4230
|
+
// row per name, and slack for the last write's page.
|
|
4231
|
+
const rows = plan.paths.length * 2 + Math.ceil(plan.bytes / RESIDENT_CHUNK_BYTES) + names;
|
|
4232
|
+
// And the module map, which the store adopts at boot: the process's code,
|
|
4233
|
+
// costed when the map was built (its cells are released once serialized).
|
|
4234
|
+
const moduleBytes = vfsState.moduleStorageBytes ?? moduleMapStorageBytes(vfsState.bundle);
|
|
4235
|
+
const storageBytes = Math.ceil(plan.bytes * 1.01) + rows * LEDGER_ROW_BYTES + moduleBytes + 65_536;
|
|
4236
|
+
return { paths: plan.paths, storageBytes };
|
|
4237
|
+
}
|
|
4238
|
+
/**
|
|
4239
|
+
* Paths earlier launches of the same build missed in this session. Other
|
|
4240
|
+
* sessions' misses (the shared read profile) join the module map instead,
|
|
4241
|
+
* in _buildProcessBundle, where a learned module brings its imports.
|
|
4242
|
+
*/
|
|
4243
|
+
async _learnedReads(vfsState, _cred) {
|
|
4244
|
+
return vfsState.bundleKey ? [...(this.residencyProfiles.get(vfsState.bundleKey) ?? [])] : [];
|
|
4245
|
+
}
|
|
4246
|
+
/**
|
|
4247
|
+
* The installed packages a closure can load: every package the lockfiles
|
|
4248
|
+
* pin whose node_modules directory is on the resolution path of the cwd or
|
|
4249
|
+
* of a closure file. A first miss is, by definition, in a package the
|
|
4250
|
+
* closure did not already load, so its package's root is found this way.
|
|
4251
|
+
*/
|
|
4252
|
+
_profileRoots(closure, cwd) {
|
|
4253
|
+
const sql = this.ctx.storage.sql;
|
|
4254
|
+
if (!sql)
|
|
4255
|
+
return [];
|
|
4256
|
+
const integrities = new NpmCache(sql).installedIntegrities();
|
|
4257
|
+
if (integrities.size === 0)
|
|
4258
|
+
return [];
|
|
4259
|
+
const dirs = new Set();
|
|
4260
|
+
const ancestors = (dir) => {
|
|
4261
|
+
for (let d = dir;;) {
|
|
4262
|
+
const nm = d === '' ? 'node_modules' : `${d}/node_modules`;
|
|
4263
|
+
if (dirs.has(nm))
|
|
4264
|
+
return;
|
|
4265
|
+
dirs.add(nm);
|
|
4266
|
+
if (d === '')
|
|
4267
|
+
return;
|
|
4268
|
+
d = d.slice(0, Math.max(0, d.lastIndexOf('/')));
|
|
4269
|
+
}
|
|
4270
|
+
};
|
|
4271
|
+
ancestors(cwd.replace(/^\/+|\/+$/g, ''));
|
|
4272
|
+
for (const path of closure) {
|
|
4273
|
+
const k = path.replace(/^\/+/, '');
|
|
4274
|
+
ancestors(k.slice(0, Math.max(0, k.lastIndexOf('/'))));
|
|
4275
|
+
}
|
|
4276
|
+
return [...integrities.keys()].filter((root) => {
|
|
4277
|
+
const i = root.lastIndexOf('/node_modules/');
|
|
4278
|
+
const parent = i === -1 ? (root.startsWith('node_modules/') ? 'node_modules' : null) : root.slice(0, i + '/node_modules'.length);
|
|
4279
|
+
return parent !== null && dirs.has(parent);
|
|
4280
|
+
});
|
|
4281
|
+
}
|
|
4282
|
+
/**
|
|
4283
|
+
* A package directory's identity for the shared read profile: the tarball
|
|
4284
|
+
* integrity the session's lockfiles pin, or, for a package no lockfile
|
|
4285
|
+
* pins (a link, a git or file dependency), the content key of its
|
|
4286
|
+
* package.json as this credential reads it.
|
|
4287
|
+
*/
|
|
4288
|
+
_packageIdentity(cred) {
|
|
4289
|
+
const sql = this.ctx.storage.sql;
|
|
4290
|
+
const integrities = sql ? new NpmCache(sql).installedIntegrities() : new Map();
|
|
4291
|
+
const view = this.vfs?.as(cred) ?? null;
|
|
4292
|
+
return (root) => {
|
|
4293
|
+
const integrity = integrities.get(root);
|
|
4294
|
+
if (integrity !== undefined)
|
|
4295
|
+
return integrity;
|
|
4296
|
+
if (view === null)
|
|
4297
|
+
return null;
|
|
4298
|
+
try {
|
|
4299
|
+
return `pkgjson:${view.contentKey(root + '/package.json')}`;
|
|
4300
|
+
}
|
|
4301
|
+
catch {
|
|
4302
|
+
return null;
|
|
4303
|
+
}
|
|
4304
|
+
};
|
|
4305
|
+
}
|
|
4306
|
+
/**
|
|
4307
|
+
* What the closure's JavaScript names by a foldable path (static-fs-refs.ts),
|
|
4308
|
+
* read from the VFS as written rather than from the module map, whose ESM
|
|
4309
|
+
* cells were rewritten and lost their import.meta. Each module is parsed once
|
|
4310
|
+
* per revision of it, in this session.
|
|
4311
|
+
*/
|
|
4312
|
+
async _closureStaticRefs(vfs, paths, pacer) {
|
|
4313
|
+
const out = [];
|
|
4314
|
+
for (const path of paths) {
|
|
4315
|
+
if (!/\.(?:c|m)?js$/.test(path))
|
|
4316
|
+
continue;
|
|
4317
|
+
let rev;
|
|
4318
|
+
try {
|
|
4319
|
+
rev = await vfs.revision(path);
|
|
4320
|
+
}
|
|
4321
|
+
catch {
|
|
4322
|
+
continue;
|
|
4323
|
+
}
|
|
4324
|
+
const memo = this.staticRefsMemo.get(path);
|
|
4325
|
+
let refs;
|
|
4326
|
+
if (memo && memo.rev === rev) {
|
|
4327
|
+
refs = memo.refs;
|
|
4328
|
+
}
|
|
4329
|
+
else {
|
|
4330
|
+
let source;
|
|
4331
|
+
try {
|
|
4332
|
+
source = await filesOf(vfs).readFileString(path);
|
|
4333
|
+
}
|
|
4334
|
+
catch {
|
|
4335
|
+
continue;
|
|
4336
|
+
}
|
|
4337
|
+
await pacer.spend(source.length);
|
|
4338
|
+
refs = findStaticFsReferences(source, '/' + path);
|
|
4339
|
+
this.staticRefsMemo.delete(path);
|
|
4340
|
+
this.staticRefsMemo.set(path, { rev, refs });
|
|
4341
|
+
for (const oldest of this.staticRefsMemo.keys()) {
|
|
4342
|
+
if (this.staticRefsMemo.size <= FacetManager.STATIC_REFS_MEMO_MAX)
|
|
4343
|
+
break;
|
|
4344
|
+
this.staticRefsMemo.delete(oldest);
|
|
4345
|
+
}
|
|
4346
|
+
}
|
|
4347
|
+
if (refs.exact.length + refs.listed.length + refs.patterns.length + refs.cwdRelative.length + refs.resolves.length > 0) {
|
|
4348
|
+
out.push(refs);
|
|
4349
|
+
}
|
|
4350
|
+
}
|
|
4351
|
+
return out;
|
|
4352
|
+
}
|
|
3838
4353
|
async _buildProcessBundle(entry, spec, pacer) {
|
|
3839
4354
|
if (!this.vfs) {
|
|
3840
|
-
return { bundle: {},
|
|
3841
|
-
}
|
|
3842
|
-
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so ESM
|
|
3843
|
-
// source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
3844
|
-
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to CJS
|
|
3845
|
-
// before they hit the facet's `new Function` pre-compile loop. Lazy-create
|
|
3846
|
-
// one if NimbusSession didn't share its own.
|
|
3847
|
-
if (!this.esbuild)
|
|
3848
|
-
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
4355
|
+
return { bundle: {}, reachableCount: 0, truncated: false };
|
|
4356
|
+
}
|
|
3849
4357
|
this.imageStore.ensureDir();
|
|
3850
4358
|
if (!this.filesystem)
|
|
3851
4359
|
throw new Error('Process filesystem authority is not initialized');
|
|
3852
|
-
const vfs =
|
|
4360
|
+
const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
|
|
3853
4361
|
const { cred } = entry;
|
|
3854
4362
|
const profile = spec.bundleProfile ?? DEFAULT_FACET_BUNDLE_PROFILE;
|
|
3855
4363
|
const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
|
|
@@ -3878,16 +4386,41 @@ export class FacetManager {
|
|
|
3878
4386
|
this.prefetchBundleCache.set(key, cached);
|
|
3879
4387
|
return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
|
|
3880
4388
|
}
|
|
3881
|
-
const
|
|
4389
|
+
const readProfile = this.readProfile;
|
|
4390
|
+
const offered = [];
|
|
4391
|
+
const learnedFor = readProfile === null ? undefined : async (closure) => {
|
|
4392
|
+
const roots = this._profileRoots(closure, spec.cwd);
|
|
4393
|
+
if (roots.length === 0)
|
|
4394
|
+
return [];
|
|
4395
|
+
offered.push(...await readProfile.lookup(roots, this._packageIdentity(cred), READ_PROFILE_LAUNCH_BYTES));
|
|
4396
|
+
return offered.map((entry) => entry.path);
|
|
4397
|
+
};
|
|
4398
|
+
const vfsState = await buildPrefetchBundle(vfs, spec.scriptPath, spec.cwd, spec.entryCode, this.esbuild ?? undefined, profile, this.residencyProfiles.get(key), pacer, undefined, learnedFor);
|
|
4399
|
+
if (offered.length > 0) {
|
|
4400
|
+
const staged = [];
|
|
4401
|
+
const unresolved = [];
|
|
4402
|
+
for (const entry of offered) {
|
|
4403
|
+
if (vfsState.bundle[entry.path] !== undefined) {
|
|
4404
|
+
staged.push(entry);
|
|
4405
|
+
continue;
|
|
4406
|
+
}
|
|
4407
|
+
// Not staged: over the budget, or nothing a regular file answers (then it is pruned).
|
|
4408
|
+
const stat = await Promise.resolve(vfs.stat('/' + entry.path, { followSymlinks: false })).catch(() => null);
|
|
4409
|
+
if (stat === null || stat.type !== 'file')
|
|
4410
|
+
unresolved.push(entry.path);
|
|
4411
|
+
}
|
|
4412
|
+
vfsState.profileOffer = { staged, unresolved };
|
|
4413
|
+
}
|
|
3882
4414
|
vfsState.bundleKey = key;
|
|
3883
|
-
vfsState.
|
|
3884
|
-
|
|
3885
|
-
|
|
3886
|
-
// Two more serializations of the same weight as the passes before them.
|
|
3887
|
-
await pacer.spend(vfsState.serializedManifest.length + vfsState.serializedMetadata.length);
|
|
3888
|
-
// The only consumer of the raw cells past this point is a single boolean,
|
|
3889
|
-
// so answer it now rather than hold ~17 MB (pi) to answer it later.
|
|
4415
|
+
vfsState.bundlePaths = Object.keys(vfsState.bundle);
|
|
4416
|
+
// The only consumer of the raw cells past serialization is a single
|
|
4417
|
+
// boolean, so it is answered first; the serialization then consumes them.
|
|
3890
4418
|
vfsState.usesNodeSqlite = bundleUsesNodeSqlite(spec.entryCode, vfsState.bundle);
|
|
4419
|
+
// And what the module map costs the facet's store, which adopts it at boot
|
|
4420
|
+
// (N18): taken now, while the cells exist, for every launch this state
|
|
4421
|
+
// serves (a cache hit included).
|
|
4422
|
+
vfsState.moduleStorageBytes = moduleMapStorageBytes(vfsState.bundle);
|
|
4423
|
+
vfsState.bundleSource = await buildFacetVfsBundleSource(vfsState.bundle, vfsState.bundleSideModulesRequired, pacer, { consume: true });
|
|
3891
4424
|
vfsState.cacheHit = false;
|
|
3892
4425
|
// Serialization is total: bundleSource/serializedManifest/serializedMetadata
|
|
3893
4426
|
// carry every byte the raw cells and objects do, and both generators read
|
|
@@ -4023,7 +4556,43 @@ export class FacetManager {
|
|
|
4023
4556
|
hasResidentProcess(pid) {
|
|
4024
4557
|
return this.processRpcResources.has(pid);
|
|
4025
4558
|
}
|
|
4026
|
-
noteProcessReportedExit(pid, exitCode) {
|
|
4559
|
+
noteProcessReportedExit(pid, exitCode, residencyMisses, evidence) {
|
|
4560
|
+
// Filed before the exit marks the table: the terminal hook forgets the key.
|
|
4561
|
+
this._recordResidencyMisses(this.residentBundleKeys.get(pid), residencyMisses);
|
|
4562
|
+
const exiting = this.processes.get(pid);
|
|
4563
|
+
const offer = this.residentProfileOffers.get(pid);
|
|
4564
|
+
this.residentProfileOffers.delete(pid);
|
|
4565
|
+
if (this.readProfile && exiting) {
|
|
4566
|
+
// The shared profile learns only what this supervisor served the process
|
|
4567
|
+
// after a miss, as the process's own credential sees it (read-profile.ts).
|
|
4568
|
+
const profile = this.readProfile;
|
|
4569
|
+
const identity = this._packageIdentity(exiting.cred);
|
|
4570
|
+
const view = this.vfs?.as(exiting.cred) ?? null;
|
|
4571
|
+
const served = evidence?.served ?? new Set();
|
|
4572
|
+
// Observers and vouchers are principals the router verified, read off
|
|
4573
|
+
// this session's Durable Object name; an anonymous session only reads.
|
|
4574
|
+
const principal = profilePrincipal(sessionIdentity({ ctx: this.ctx })?.tenantSegment);
|
|
4575
|
+
this.ctx.waitUntil((async () => {
|
|
4576
|
+
const tag = principal === null ? null : await principalTag(principal);
|
|
4577
|
+
if (tag !== null && view !== null && residencyMisses && residencyMisses.length > 0) {
|
|
4578
|
+
const found = await verifiedEvidence(residencyMisses, served, async (path) => {
|
|
4579
|
+
try {
|
|
4580
|
+
const st = view.lstat(path.replace(/^\/+/, ''));
|
|
4581
|
+
return { type: st.type, size: st.size };
|
|
4582
|
+
}
|
|
4583
|
+
catch {
|
|
4584
|
+
return null;
|
|
4585
|
+
}
|
|
4586
|
+
});
|
|
4587
|
+
if (found.length > 0)
|
|
4588
|
+
await profile.observe(found, tag, identity);
|
|
4589
|
+
}
|
|
4590
|
+
if (tag !== null && offer !== undefined) {
|
|
4591
|
+
const unread = evidence?.profileUnread ? new Set(evidence.profileUnread) : null;
|
|
4592
|
+
await profile.settle(offer.staged, unread, served, new Set(offer.unresolved), tag);
|
|
4593
|
+
}
|
|
4594
|
+
})().catch(() => undefined));
|
|
4595
|
+
}
|
|
4027
4596
|
this.portRegistry.unregisterByPid(pid);
|
|
4028
4597
|
this.processes.exit(pid, exitCode);
|
|
4029
4598
|
const tracked = this.processRpcResources.get(pid);
|
|
@@ -4129,7 +4698,7 @@ export class FacetManager {
|
|
|
4129
4698
|
}
|
|
4130
4699
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
4131
4700
|
const diagSink = diagOn
|
|
4132
|
-
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0
|
|
4701
|
+
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0 }
|
|
4133
4702
|
: undefined;
|
|
4134
4703
|
const abortController = new AbortController();
|
|
4135
4704
|
// Ctrl-C / kill on a user program has to end the in-flight run, not just
|
|
@@ -4140,7 +4709,11 @@ export class FacetManager {
|
|
|
4140
4709
|
const onShellAbort = () => abortController.abort();
|
|
4141
4710
|
opts.signal?.addEventListener('abort', onShellAbort, { once: true });
|
|
4142
4711
|
try {
|
|
4143
|
-
|
|
4712
|
+
// A one-shot holds its module map and what it was seen to read (both in
|
|
4713
|
+
// the bundle), and plans nothing beyond them: a data plan is chosen from
|
|
4714
|
+
// a listing of the namespace, and a one-shot takes no listing of its own
|
|
4715
|
+
// (§2.8: the principal's image is where that comes from).
|
|
4716
|
+
const result = await this._execViaLoader(code, opts, entry, vfsState, [], abortController.signal, diagSink);
|
|
4144
4717
|
this._flushVfsWrites(result, entry.pid);
|
|
4145
4718
|
this._recordResidencyMisses(vfsState.bundleKey, result.residencyMisses);
|
|
4146
4719
|
this.processes.exit(entry.pid, result.exitCode);
|
|
@@ -4156,8 +4729,7 @@ export class FacetManager {
|
|
|
4156
4729
|
drainPasses: result.diag?.drainPasses ?? 0,
|
|
4157
4730
|
moduleMapBytes: diagSink.moduleMapBytes,
|
|
4158
4731
|
bundleBytes: diagSink.bundleBytes,
|
|
4159
|
-
|
|
4160
|
-
metadataBytes: diagSink.metadataBytes,
|
|
4732
|
+
namespaceRefusals: result.diag?.namespaceRefusals ?? 0,
|
|
4161
4733
|
rpcWrites: result.diag?.rpcWrites ?? 0,
|
|
4162
4734
|
fsRpcReads: result.diag?.fsRpcReads ?? 0,
|
|
4163
4735
|
cacheHit: vfsState.cacheHit ?? false,
|
|
@@ -4228,13 +4800,13 @@ export class FacetManager {
|
|
|
4228
4800
|
}
|
|
4229
4801
|
}
|
|
4230
4802
|
// ── One-shot dynamic Worker entrypoint ────────────────────────────────
|
|
4231
|
-
async _execViaLoader(code, opts, entry, vfsState, signal, diagSink) {
|
|
4803
|
+
async _execViaLoader(code, opts, entry, vfsState, dataPlan, signal, diagSink) {
|
|
4232
4804
|
// Answered by _buildProcessBundle while the raw cells were still in
|
|
4233
4805
|
// hand; re-deriving it here is what forced them to be retained.
|
|
4234
4806
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
4235
|
-
const [sqliteModules,
|
|
4807
|
+
const [sqliteModules, sources] = await Promise.all([
|
|
4236
4808
|
this.sqliteModuleEntry(usesSqlite),
|
|
4237
|
-
|
|
4809
|
+
fetchNodeFacetSources(this.env),
|
|
4238
4810
|
]);
|
|
4239
4811
|
const writerId = crypto.randomUUID();
|
|
4240
4812
|
let writerActivated = false;
|
|
@@ -4250,6 +4822,7 @@ export class FacetManager {
|
|
|
4250
4822
|
captureOutput: !!opts.captureOutput,
|
|
4251
4823
|
cred: { ...entry.cred, groups: [...entry.cred.groups] },
|
|
4252
4824
|
vfsCursor: vfsState.cursor,
|
|
4825
|
+
dataPlan,
|
|
4253
4826
|
...(diagSink ? { diag: true } : {}),
|
|
4254
4827
|
});
|
|
4255
4828
|
try {
|
|
@@ -4270,7 +4843,8 @@ export class FacetManager {
|
|
|
4270
4843
|
// the map, and compiled by the loader like the sqlite sidecar.
|
|
4271
4844
|
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
4272
4845
|
const wasmModules = (await this._wasmModulesByValue(entry, wasmImports));
|
|
4273
|
-
const
|
|
4846
|
+
const entryCode = await this._entryDynamicImports(code, opts.filename, opts.cwd || '/home/user');
|
|
4847
|
+
const generatedWorker = await generateEntrypointCode(entryCode, vfsState, usesSqlite, sources, wasmImports);
|
|
4274
4848
|
if (diagSink) {
|
|
4275
4849
|
diagSink.moduleMapBytes = _encodedSourceBytes(generatedWorker.code);
|
|
4276
4850
|
for (const source of Object.values(generatedWorker.modules)) {
|
|
@@ -4285,8 +4859,6 @@ export class FacetManager {
|
|
|
4285
4859
|
for (const source of Object.values(vfsState.bundleSource?.modules ?? {})) {
|
|
4286
4860
|
diagSink.bundleBytes += _encodedSourceBytes(source);
|
|
4287
4861
|
}
|
|
4288
|
-
diagSink.manifestBytes = _encodedSourceBytes(vfsState.serializedManifest ?? '');
|
|
4289
|
-
diagSink.metadataBytes = _encodedSourceBytes(vfsState.serializedMetadata ?? '');
|
|
4290
4862
|
}
|
|
4291
4863
|
if (!vfsState.cacheRetained)
|
|
4292
4864
|
releaseGeneratedSources(vfsState);
|
|
@@ -4353,7 +4925,7 @@ export class FacetManager {
|
|
|
4353
4925
|
// the Worker-Loader cache-miss path (with SUPERVISOR bound to THIS call's
|
|
4354
4926
|
// context, which stays open for the whole run), never in this DO.
|
|
4355
4927
|
const writerId = crypto.randomUUID();
|
|
4356
|
-
const supervisor = {
|
|
4928
|
+
const supervisor = { ...supervisorBindingProps(this.ctx, staged.pid), writerId };
|
|
4357
4929
|
const ctxExports = getNimbusCtxExports();
|
|
4358
4930
|
let entrypoint;
|
|
4359
4931
|
let writerActivated = false;
|
|
@@ -4434,10 +5006,10 @@ export class FacetManager {
|
|
|
4434
5006
|
// other writes flush live through the SUPERVISOR RPC bridge.
|
|
4435
5007
|
if (this.vfs)
|
|
4436
5008
|
this.imageStore.ensureDir();
|
|
4437
|
-
const processVfs = this.filesystem ?
|
|
5009
|
+
const processVfs = this.filesystem ? this.filesystem.bind({ pid: entry.pid, cred: entry.cred }) : null;
|
|
4438
5010
|
const vfsState = processVfs
|
|
4439
|
-
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE
|
|
4440
|
-
: { bundle: {},
|
|
5011
|
+
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE)
|
|
5012
|
+
: { bundle: {}, reachableCount: 0, truncated: false };
|
|
4441
5013
|
const vfsBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
4442
5014
|
assertStagedBundleFitsRpcPayload(vfsBundle, vfsState.bundle);
|
|
4443
5015
|
const stageSpec = {
|
|
@@ -4448,8 +5020,6 @@ export class FacetManager {
|
|
|
4448
5020
|
cwd: opts.cwd,
|
|
4449
5021
|
stdin: opts.stdin ?? '',
|
|
4450
5022
|
vfsBundle,
|
|
4451
|
-
vfsManifest: JSON.stringify(vfsState.manifest),
|
|
4452
|
-
vfsMetadata: JSON.stringify(vfsState.metadata),
|
|
4453
5023
|
vfsCursor: serializeFacetVfsCursor(vfsState.cursor),
|
|
4454
5024
|
};
|
|
4455
5025
|
return { pid, command, stageSpec };
|
|
@@ -4617,7 +5187,8 @@ export class FacetManager {
|
|
|
4617
5187
|
// The handle's route target resolves the RUNNING facet wherever it is
|
|
4618
5188
|
// hosted; binding it for the pid before the port is announced is what
|
|
4619
5189
|
// lets the shim's listen()→SUPERVISOR.registerPort back-fill.
|
|
4620
|
-
|
|
5190
|
+
// The opencode runner dispatches through node-shims' __nimbusServeHttp, which strips the ACQUIRE.
|
|
5191
|
+
this.portRegistry.bindFacetStub(pid, handle.routeTarget, { deliversAcquire: true });
|
|
4621
5192
|
this.trackProcessRpcResources(pid, [handle], { releaseOnReportExit: false });
|
|
4622
5193
|
resourcesTracked = true;
|
|
4623
5194
|
this.ctx.waitUntil(handle.done
|
|
@@ -4755,10 +5326,6 @@ export class FacetManager {
|
|
|
4755
5326
|
await Promise.allSettled([...this.launchTasks]);
|
|
4756
5327
|
await Promise.all(this.rowAmendments.values());
|
|
4757
5328
|
}
|
|
4758
|
-
/** Whether any launch is suspended waiting for a turn. */
|
|
4759
|
-
get hasPendingLaunchTurns() {
|
|
4760
|
-
return this.launchPump.hasPending;
|
|
4761
|
-
}
|
|
4762
5329
|
/** Allocate a free loopback port for a resident server facet (from 4096 up). */
|
|
4763
5330
|
_allocateLoopbackPort() {
|
|
4764
5331
|
for (let port = 4096; port < 4096 + 4096; port++) {
|
|
@@ -5145,7 +5712,14 @@ export class FacetManager {
|
|
|
5145
5712
|
async _residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName, launchEnv) {
|
|
5146
5713
|
const diagOn = isExecDiagEnabled();
|
|
5147
5714
|
const __bundleStart = diagOn ? Date.now() : 0;
|
|
5715
|
+
if (this.debugEnabled)
|
|
5716
|
+
this.processes.appendOutput(entry.pid, 'stderr', '[nimbus-debug] launch: building the module map\n');
|
|
5148
5717
|
const vfsState = await this._buildProcessBundle(entry, { scriptPath: opts.filename, cwd, entryCode: code, bundleProfile: opts.bundleProfile }, pacer);
|
|
5718
|
+
const planStart = Date.now();
|
|
5719
|
+
const { paths: dataPlan, storageBytes } = await this._planResidentData(entry, vfsState, cwd, opts.env?.HOME, pacer);
|
|
5720
|
+
if (this.debugEnabled) {
|
|
5721
|
+
this.processes.appendOutput(entry.pid, 'stderr', `[nimbus-debug] data plan: ${dataPlan.length} paths in ${Date.now() - planStart} ms (${pacer.chunks} turns so far)\n`);
|
|
5722
|
+
}
|
|
5149
5723
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
5150
5724
|
// The launch-time overlay (`$PORT`, `$NIMBUS_APP` under a reservation)
|
|
5151
5725
|
// rides on top of the recipe's env and is never journalled: it is
|
|
@@ -5166,13 +5740,13 @@ export class FacetManager {
|
|
|
5166
5740
|
: spawnEnv;
|
|
5167
5741
|
// Answered by _buildProcessBundle while the raw cells were still in hand.
|
|
5168
5742
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
5169
|
-
const [sqliteModules,
|
|
5743
|
+
const [sqliteModules, sources] = await Promise.all([
|
|
5170
5744
|
this.sqliteModuleEntry(usesSqlite),
|
|
5171
|
-
|
|
5745
|
+
fetchNodeFacetSources(this.env),
|
|
5172
5746
|
]);
|
|
5173
5747
|
// Each image is read by path when the facet loads, never by value here.
|
|
5174
5748
|
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
5175
|
-
let generatedWorker = await generateLongRunningNodeCode(code, vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite,
|
|
5749
|
+
let generatedWorker = await generateLongRunningNodeCode(await this._entryDynamicImports(code, opts.filename, cwd), vfsState, { ...opts, env: processEnv, cred: entry.cred, wasmImports }, usesSqlite, sources, pacer);
|
|
5176
5750
|
// Sized here, while the map is still in hand. Reading these after the load
|
|
5177
5751
|
// would itself be what keeps the map alive, and the whole point of the
|
|
5178
5752
|
// scoping below is that nothing does.
|
|
@@ -5184,10 +5758,13 @@ export class FacetManager {
|
|
|
5184
5758
|
}
|
|
5185
5759
|
moduleMapBytes = _encodedSourceBytes(generatedWorker.code) + bundleBytes;
|
|
5186
5760
|
}
|
|
5187
|
-
const manifestBytes = diagOn ? _encodedSourceBytes(vfsState.serializedManifest ?? '') : 0;
|
|
5188
|
-
const metadataBytes = diagOn ? _encodedSourceBytes(vfsState.serializedMetadata ?? '') : 0;
|
|
5189
5761
|
const cacheHit = vfsState.cacheHit ?? false;
|
|
5190
5762
|
const vfsCursor = vfsState.cursor;
|
|
5763
|
+
if (vfsState.bundleKey)
|
|
5764
|
+
this.residentBundleKeys.set(entry.pid, vfsState.bundleKey);
|
|
5765
|
+
const profileOffer = vfsState.profileOffer;
|
|
5766
|
+
if (profileOffer !== undefined)
|
|
5767
|
+
this.residentProfileOffers.set(entry.pid, profileOffer);
|
|
5191
5768
|
// The map is generated; the state's only remaining job is its cursor.
|
|
5192
5769
|
// Released here, before the boot — releasing afterwards keeps the copy
|
|
5193
5770
|
// alive for exactly the window that was resetting the isolate. A state
|
|
@@ -5209,6 +5786,8 @@ export class FacetManager {
|
|
|
5209
5786
|
};
|
|
5210
5787
|
generatedWorker = undefined;
|
|
5211
5788
|
const vfsTextModules = await this.imageStore.materialize(entry.pid, drainSources(sources), pacer);
|
|
5789
|
+
if (this.debugEnabled)
|
|
5790
|
+
this.processes.appendOutput(entry.pid, 'stderr', '[nimbus-debug] launch: images stored, starting the facet\n');
|
|
5212
5791
|
// Last gate before the facet exists. A launch now spans many turns, so
|
|
5213
5792
|
// a kill can land anywhere inside it; booting a process the table has
|
|
5214
5793
|
// already exited would leave a facet nothing owns, running against a
|
|
@@ -5218,7 +5797,12 @@ export class FacetManager {
|
|
|
5218
5797
|
// The attached-TTY runner holds startProcess open for the process's
|
|
5219
5798
|
// life; the server/watch runner returns once it is up.
|
|
5220
5799
|
startContract: opts.attachedTty ? 'lifetime' : 'boot',
|
|
5221
|
-
startArgs: {
|
|
5800
|
+
startArgs: {
|
|
5801
|
+
vfsCursor, dataPlan,
|
|
5802
|
+
...(profileOffer !== undefined && profileOffer.staged.length > 0 ? { profileStaged: profileOffer.staged.map((e) => e.path) } : {}),
|
|
5803
|
+
...(this.debugEnabled ? { diag: true } : {}),
|
|
5804
|
+
},
|
|
5805
|
+
storageBytes,
|
|
5222
5806
|
// A resident whose declared port is reserved binds the owner's
|
|
5223
5807
|
// durable slot — the same store a durable worker spawn takes — so the
|
|
5224
5808
|
// reservation's durability reaches this process's storage too.
|
|
@@ -5238,6 +5822,11 @@ export class FacetManager {
|
|
|
5238
5822
|
},
|
|
5239
5823
|
},
|
|
5240
5824
|
});
|
|
5825
|
+
// Ended while its facet was being created (a signal's default action).
|
|
5826
|
+
if (this.processes.get(entry.pid)?.state !== 'running') {
|
|
5827
|
+
handle.kill();
|
|
5828
|
+
return;
|
|
5829
|
+
}
|
|
5241
5830
|
if (diagOn) {
|
|
5242
5831
|
recordExecTelemetry({
|
|
5243
5832
|
command,
|
|
@@ -5254,8 +5843,7 @@ export class FacetManager {
|
|
|
5254
5843
|
// when it stayed inline it rides inside worker.js and shows up only
|
|
5255
5844
|
// in the total.
|
|
5256
5845
|
bundleBytes,
|
|
5257
|
-
|
|
5258
|
-
metadataBytes,
|
|
5846
|
+
namespaceRefusals: 0,
|
|
5259
5847
|
rpcWrites: 0,
|
|
5260
5848
|
fsRpcReads: 0,
|
|
5261
5849
|
cacheHit,
|
|
@@ -5266,7 +5854,8 @@ export class FacetManager {
|
|
|
5266
5854
|
}
|
|
5267
5855
|
this.trackProcessRpcResources(entry.pid, [handle], { releaseOnReportExit: !opts.attachedTty });
|
|
5268
5856
|
resourcesTracked = true;
|
|
5269
|
-
|
|
5857
|
+
// A node-shims resident: its __nimbusServeHttp takes the delivered ACQUIRE off each request.
|
|
5858
|
+
this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget, { deliversAcquire: true });
|
|
5270
5859
|
if (opts.attachedTty) {
|
|
5271
5860
|
this.ctx.waitUntil(handle.done
|
|
5272
5861
|
.catch((e) => {
|
|
@@ -5385,6 +5974,7 @@ export class FacetManager {
|
|
|
5385
5974
|
let record;
|
|
5386
5975
|
let durableFacetName;
|
|
5387
5976
|
let launchEnv;
|
|
5977
|
+
const foreground = opts.foreground ? this._holdForeground(entry.pid, opts.foreground) : null;
|
|
5388
5978
|
try {
|
|
5389
5979
|
if (opts.durable) {
|
|
5390
5980
|
// A durable spawn on a declared port starts only when a reservation the
|
|
@@ -5502,7 +6092,7 @@ export class FacetManager {
|
|
|
5502
6092
|
this.trackProcessRpcResources(entry.pid, [handle]);
|
|
5503
6093
|
resourcesTracked = true;
|
|
5504
6094
|
this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
|
|
5505
|
-
const boot = await handle.booted();
|
|
6095
|
+
const boot = foreground ? await Promise.race([handle.booted(), foreground.interrupted]) : await handle.booted();
|
|
5506
6096
|
if (record && this.launchJournal.has(entry.pid)) {
|
|
5507
6097
|
// Booted and running: the launch proved itself, so the resident
|
|
5508
6098
|
// starts its running life with a fresh re-drive budget.
|
|
@@ -5545,9 +6135,38 @@ export class FacetManager {
|
|
|
5545
6135
|
this.releaseProcessRpcResources(entry.pid);
|
|
5546
6136
|
else
|
|
5547
6137
|
handle?.kill();
|
|
5548
|
-
|
|
6138
|
+
// An interrupt already killed the process; it did not fail.
|
|
6139
|
+
if (!opts.foreground?.signal.aborted)
|
|
6140
|
+
this._failLaunch(entry.pid, 'long-running worker boot failed: ' + errorMessage(e));
|
|
5549
6141
|
throw e;
|
|
5550
6142
|
}
|
|
6143
|
+
finally {
|
|
6144
|
+
foreground?.release();
|
|
6145
|
+
}
|
|
6146
|
+
}
|
|
6147
|
+
_holdForeground(pid, launch) {
|
|
6148
|
+
this.processes.setForeground(pid, true);
|
|
6149
|
+
const unsubscribe = this.processes.subscribeLogs(pid, (chunk) => launch.write(chunk.stream, chunk.data));
|
|
6150
|
+
let onAbort = () => { };
|
|
6151
|
+
const interrupted = new Promise((_, reject) => {
|
|
6152
|
+
onAbort = () => {
|
|
6153
|
+
this.kill(pid);
|
|
6154
|
+
reject(new DOMException('the launching command was interrupted', 'AbortError'));
|
|
6155
|
+
};
|
|
6156
|
+
});
|
|
6157
|
+
interrupted.catch(() => { });
|
|
6158
|
+
if (launch.signal.aborted)
|
|
6159
|
+
onAbort();
|
|
6160
|
+
else
|
|
6161
|
+
launch.signal.addEventListener('abort', onAbort, { once: true });
|
|
6162
|
+
return {
|
|
6163
|
+
interrupted,
|
|
6164
|
+
release: () => {
|
|
6165
|
+
launch.signal.removeEventListener('abort', onAbort);
|
|
6166
|
+
unsubscribe();
|
|
6167
|
+
this.processes.setForeground(pid, false);
|
|
6168
|
+
},
|
|
6169
|
+
};
|
|
5551
6170
|
}
|
|
5552
6171
|
/**
|
|
5553
6172
|
* A resident process announcing it bound `port`.
|
|
@@ -5788,18 +6407,23 @@ export class FacetManager {
|
|
|
5788
6407
|
catch { }
|
|
5789
6408
|
}
|
|
5790
6409
|
}
|
|
5791
|
-
/**
|
|
5792
|
-
|
|
6410
|
+
/**
|
|
6411
|
+
* Kill a running process by PID. Given the signal that ends it (a name
|
|
6412
|
+
* without `SIG`), it exits with that signal's status, 128+signo, and its
|
|
6413
|
+
* exit names `SIG<name>`; without one it is SIGKILL's 137, `killed`.
|
|
6414
|
+
*/
|
|
6415
|
+
kill(pid, signal) {
|
|
5793
6416
|
const entry = this.processes.get(pid);
|
|
5794
6417
|
if (!entry || entry.state !== 'running')
|
|
5795
6418
|
return false;
|
|
6419
|
+
const code = signal === undefined ? 137 : exitCodeForSignal(signal);
|
|
5796
6420
|
this.portRegistry.unregisterByPid(pid);
|
|
5797
6421
|
this.releaseProcessRpcResources(pid);
|
|
5798
6422
|
this.revokeProcessVfsWriters(pid);
|
|
5799
|
-
const result = this.processes.kill(pid);
|
|
6423
|
+
const result = this.processes.kill(pid, code);
|
|
5800
6424
|
if (result) {
|
|
5801
6425
|
try {
|
|
5802
|
-
this.hooks.onExternalExit?.(pid,
|
|
6426
|
+
this.hooks.onExternalExit?.(pid, code, signal === undefined ? 'killed' : `SIG${signal}`);
|
|
5803
6427
|
}
|
|
5804
6428
|
catch { }
|
|
5805
6429
|
}
|
|
@@ -5959,5 +6583,5 @@ export class FacetManager {
|
|
|
5959
6583
|
}
|
|
5960
6584
|
return this.portRegistry.has(port);
|
|
5961
6585
|
}
|
|
5962
|
-
get stats() { return this.processes.stats; }
|
|
6586
|
+
get stats() { return { ...this.processes.stats, readProfileConflicts: this.readProfileConflicts }; }
|
|
5963
6587
|
}
|