@nimbus-sh/worker 0.10.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/facets/cirrus-real.d.ts.map +1 -1
- package/dist/facets/cirrus-real.js +9 -4
- package/dist/facets/compose.d.ts +11 -7
- package/dist/facets/compose.d.ts.map +1 -1
- package/dist/facets/compose.js +8 -38
- 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-transform.d.ts +36 -10
- package/dist/facets/esbuild-transform.d.ts.map +1 -1
- package/dist/facets/esbuild-transform.js +216 -20
- 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 +179 -78
- package/dist/facets/manager.d.ts.map +1 -1
- package/dist/facets/manager.js +1221 -649
- 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/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 +16 -9
- package/dist/git/commands.d.ts.map +1 -1
- package/dist/git/commands.js +1818 -188
- package/dist/git/network-facet.d.ts +10 -2
- package/dist/git/network-facet.d.ts.map +1 -1
- package/dist/git/network-facet.js +62 -16
- 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 +62 -243
- package/dist/hosted/runtime.d.ts +19 -13
- package/dist/hosted/runtime.d.ts.map +1 -1
- package/dist/hosted/runtime.js +71 -14
- package/dist/hosted/services.d.ts +6 -7
- package/dist/hosted/services.d.ts.map +1 -1
- package/dist/hosted/services.js +10 -14
- 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 +5 -2
- package/dist/loaders/generated-workers.d.ts.map +1 -1
- package/dist/loaders/generated-workers.js +6 -3
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -1
- package/dist/loaders/npm-resolve-preamble.js +0 -27
- 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 +5 -0
- package/dist/npm/cache.d.ts.map +1 -1
- package/dist/npm/cache.js +13 -1
- package/dist/npm/install-batch-facet.d.ts +2 -2
- package/dist/npm/install-batch-facet.js +3 -3
- package/dist/npm/installer.d.ts +17 -1
- package/dist/npm/installer.d.ts.map +1 -1
- package/dist/npm/installer.js +159 -20
- 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/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 +2 -0
- package/dist/npm/r2-cache.d.ts.map +1 -1
- package/dist/npm/r2-cache.js +2 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -1
- package/dist/npm/resolve-one-facet.js +14 -3
- package/dist/npm/resolver.d.ts +0 -7
- package/dist/npm/resolver.d.ts.map +1 -1
- package/dist/npm/resolver.js +0 -22
- 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/esbuild-wasm-bytes.d.ts +17 -5
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -1
- package/dist/runtime/esbuild-wasm-bytes.js +43 -94
- 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 +1567 -864
- 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 +2 -2
- 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 +36 -35
- 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.map +1 -1
- package/dist/runtime/wasi/preamble.js +0 -24
- 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 +34 -11
- package/dist/session/hibernation.d.ts.map +1 -1
- package/dist/session/hibernation.js +46 -29
- 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 +41 -15
- package/dist/session/nimbus-session.d.ts.map +1 -1
- package/dist/session/nimbus-session.js +64 -70
- package/dist/session/npm-install-port.d.ts.map +1 -1
- package/dist/session/npm-install-port.js +1 -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 +7 -10
- package/dist/session/rpc.d.ts +104 -20
- package/dist/session/rpc.d.ts.map +1 -1
- package/dist/session/rpc.js +184 -37
- 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 +104 -15
- package/dist/session/supervisor-rpc.d.ts.map +1 -1
- package/dist/session/supervisor-rpc.js +188 -71
- 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 +2 -0
- 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 +5 -22
- package/package.json +8 -7
- 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-7b4dd7a76d95d21e.js → node-shims-06565b121555c194.js} +2089 -861
- 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-facet-workers.mjs +114 -3
- 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, ESM_TRANSFORM_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
69
|
import { prefetchBundleStart, prefetchBundleEnd, setPrefetchCacheBytes, setTransformCacheBytes, } from '@nimbus-sh/platform/diag-counters.js';
|
|
58
|
-
const
|
|
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,7 +417,7 @@ ${facetWasmImportsSource(wasmImports)}
|
|
|
384
417
|
const USER_CODE = ${safeCode};
|
|
385
418
|
const __NimbusHostResponse = globalThis.Response;
|
|
386
419
|
|
|
387
|
-
${
|
|
420
|
+
${MK_COMPILED_FN_SOURCE}
|
|
388
421
|
|
|
389
422
|
let __compiledFn = null;
|
|
390
423
|
let __entryCompileFailure = null;
|
|
@@ -394,15 +427,13 @@ try {
|
|
|
394
427
|
__entryCompileFailure = (__e && __e.stack) || (__e && __e.message) || String(__e);
|
|
395
428
|
}
|
|
396
429
|
|
|
397
|
-
//
|
|
430
|
+
// The module bundle and its pre-compiled modules, at module level (startup time).
|
|
398
431
|
const __MODULE_VFS_BUNDLE = ${bundleSource.expression};
|
|
399
|
-
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
400
|
-
const __MODULE_VFS_METADATA = ${safeMetadata};
|
|
401
432
|
const __compiledModules = new Map();
|
|
402
433
|
const __compileFailures = new Map();
|
|
403
434
|
// Precompile JS modules AND extensionless CJS entries (bin scripts and
|
|
404
435
|
// shims). workerd forbids new Function at request time, so anything not
|
|
405
|
-
// precompiled here surfaces the misleading "
|
|
436
|
+
// precompiled here surfaces the misleading "not in this launch's module map".
|
|
406
437
|
${BUNDLE_PRECOMPILE_LOOP}
|
|
407
438
|
|
|
408
439
|
class __ProcessExit extends Error {
|
|
@@ -412,7 +443,7 @@ class __ProcessExit extends Error {
|
|
|
412
443
|
export default {
|
|
413
444
|
async fetch(request, workerEnv) {
|
|
414
445
|
const args = await request.json();
|
|
415
|
-
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;
|
|
416
447
|
// Per invocation, not per module: this body is cached on
|
|
417
448
|
// hash(code + bundle + manifest) and reused by any session whose snapshot
|
|
418
449
|
// hashes the same, and epochs are per supervisor incarnation.
|
|
@@ -423,10 +454,30 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
423
454
|
// ends it early.
|
|
424
455
|
const __entryBudgetMs = Infinity;
|
|
425
456
|
let __drainPasses = 0;
|
|
426
|
-
const __vfsBundle = __MODULE_VFS_BUNDLE;
|
|
427
|
-
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
428
|
-
const __vfsMetadata = __MODULE_VFS_METADATA;
|
|
429
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;
|
|
430
481
|
const __pendingIO = [];
|
|
431
482
|
// Fix 6 orphan counters (same as NodeProcess.run) — count RPC writes
|
|
432
483
|
// that get dropped during isolate teardown so reportExit can report them.
|
|
@@ -453,24 +504,24 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
453
504
|
let stdout = "", stderr = "";
|
|
454
505
|
let exitCode = 0;
|
|
455
506
|
const __nimbusDeferProcessExitReport = true;
|
|
456
|
-
${
|
|
507
|
+
${sources.ledger}
|
|
457
508
|
const __vfsDirs = {};
|
|
458
509
|
|
|
459
510
|
${ENTRYPOINT_TIMER_TRACKER}
|
|
460
|
-
${shims}
|
|
511
|
+
${sources.shims}
|
|
461
512
|
|
|
462
513
|
${ENTRYPOINT_EVENT_LOOP}
|
|
463
514
|
${RESIDENCY_MISS_REPORT}
|
|
464
515
|
|
|
465
516
|
// Override console AND process.stdout/stderr for live SUPERVISOR streaming
|
|
466
517
|
if (__supervisor && !captureOutput) {
|
|
467
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
468
|
-
__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)); };
|
|
469
520
|
__consoleMod.warn = __consoleMod.error;
|
|
470
521
|
__consoleMod.info = __consoleMod.log;
|
|
471
522
|
__consoleMod.debug = __consoleMod.log;
|
|
472
|
-
__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; };
|
|
473
|
-
__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; };
|
|
474
525
|
}
|
|
475
526
|
|
|
476
527
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -514,6 +565,12 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
514
565
|
}
|
|
515
566
|
}
|
|
516
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
|
+
}
|
|
517
574
|
|
|
518
575
|
async function __drainPendingIO(maxPasses = 12) {
|
|
519
576
|
let __settledIO = 0;
|
|
@@ -584,8 +641,11 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
584
641
|
// Unconditional, unlike diag: the supervisor stages these paths into
|
|
585
642
|
// the next bundle for the same entry, so withholding them behind a
|
|
586
643
|
// debug flag would leave the miss to repeat forever.
|
|
587
|
-
residencyMisses:
|
|
588
|
-
...(__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
|
+
} } : {}),
|
|
589
649
|
});
|
|
590
650
|
}
|
|
591
651
|
};
|
|
@@ -655,7 +715,7 @@ function facetWasmImportsSource(wasmImports) {
|
|
|
655
715
|
return `${lines.join('\n')}\nglobalThis.__nimbusPrecompiledWasm = new Map([${entries.join(', ')}]);`
|
|
656
716
|
+ `\nglobalThis.__nimbusPrecompiledWasmByDigest = new Map([${byDigest.join(', ')}]);`;
|
|
657
717
|
}
|
|
658
|
-
export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite,
|
|
718
|
+
export async function generateLongRunningNodeCode(userCode, vfsState, opts, usesSqlite, sources, pacer) {
|
|
659
719
|
const safeCode = JSON.stringify(rewriteProvidedCommonJsModules(userCode));
|
|
660
720
|
const safeArgs = JSON.stringify({
|
|
661
721
|
argv: opts.argv || [],
|
|
@@ -668,8 +728,6 @@ export async function generateLongRunningNodeCode(userCode, vfsState, opts, uses
|
|
|
668
728
|
cred: opts.cred,
|
|
669
729
|
});
|
|
670
730
|
const bundleSource = await facetVfsBundleSourceFor(vfsState, pacer);
|
|
671
|
-
const safeManifest = vfsState.serializedManifest ?? JSON.stringify(vfsState.manifest);
|
|
672
|
-
const safeMetadata = vfsState.serializedMetadata ?? JSON.stringify(vfsState.metadata);
|
|
673
731
|
return {
|
|
674
732
|
code: `
|
|
675
733
|
${bundleSource.imports}
|
|
@@ -681,7 +739,7 @@ const USER_CODE = ${safeCode};
|
|
|
681
739
|
const __NIMBUS_ARGS = ${safeArgs};
|
|
682
740
|
const __NimbusHostResponse = globalThis.Response;
|
|
683
741
|
|
|
684
|
-
${
|
|
742
|
+
${MK_COMPILED_FN_SOURCE}
|
|
685
743
|
|
|
686
744
|
let __compiledFn = null;
|
|
687
745
|
let __entryCompileFailure = null;
|
|
@@ -696,8 +754,6 @@ try {
|
|
|
696
754
|
// and this object are the same bytes twice, and for pi that is the largest
|
|
697
755
|
// single allocation in the facet before the program starts.
|
|
698
756
|
let __MODULE_VFS_BUNDLE = ${bundleSource.expression};
|
|
699
|
-
const __MODULE_VFS_MANIFEST = ${safeManifest};
|
|
700
|
-
const __MODULE_VFS_METADATA = ${safeMetadata};
|
|
701
757
|
const __compiledModules = new Map();
|
|
702
758
|
const __compileFailures = new Map();
|
|
703
759
|
// Module evaluation is the ONLY place workerd allows a string to become code —
|
|
@@ -706,7 +762,7 @@ const __compileFailures = new Map();
|
|
|
706
762
|
// here, off the module map. That is why code cannot come from the store.
|
|
707
763
|
${BUNDLE_PRECOMPILE_LOOP}
|
|
708
764
|
|
|
709
|
-
${
|
|
765
|
+
${sources.residentStore}
|
|
710
766
|
|
|
711
767
|
class __ProcessExit extends Error {
|
|
712
768
|
constructor(code) { super("process.exit(" + code + ")"); this.code = code; }
|
|
@@ -717,6 +773,15 @@ let __nimbusStarting = null;
|
|
|
717
773
|
let __nimbusRuntime = null;
|
|
718
774
|
let __nimbusAttachedLifecycle = null;
|
|
719
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
|
+
|
|
720
785
|
async function __nimbusFlushRuntime() {
|
|
721
786
|
const rt = __nimbusRuntime;
|
|
722
787
|
if (!rt) return;
|
|
@@ -736,7 +801,9 @@ async function __nimbusFlushRuntime() {
|
|
|
736
801
|
rt.drainVfsMutations(),
|
|
737
802
|
]);
|
|
738
803
|
for (let pass = 0; pass < 12; pass++) {
|
|
739
|
-
|
|
804
|
+
const turn = Promise.withResolvers();
|
|
805
|
+
__nimbusPlatformSetTimeout(turn.resolve, 0);
|
|
806
|
+
await turn.promise;
|
|
740
807
|
if (rt.pendingIO.length <= rt.settledIO) break;
|
|
741
808
|
const slice = rt.pendingIO.slice(rt.settledIO);
|
|
742
809
|
rt.settledIO = rt.pendingIO.length;
|
|
@@ -765,8 +832,6 @@ async function __nimbusEnsureStarted(workerEnv, workerCtx, __startArgs) {
|
|
|
765
832
|
// time. Same reason argv/env/pid want to move here.
|
|
766
833
|
const __MODULE_VFS_CURSOR = (__startArgs && __startArgs.vfsCursor) || null;
|
|
767
834
|
${VFS_CURSOR_SEED_SOURCE}
|
|
768
|
-
const __vfsManifest = __MODULE_VFS_MANIFEST;
|
|
769
|
-
const __vfsMetadata = __MODULE_VFS_METADATA;
|
|
770
835
|
const __supervisor = workerEnv?.SUPERVISOR || null;
|
|
771
836
|
// The resident set lives in this facet's own SQLite rather than its heap.
|
|
772
837
|
// A synchronous read cannot block and no JS stack here can be suspended, so
|
|
@@ -774,41 +839,56 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
774
839
|
// \`ctx.storage.sql.exec\` returns a Cursor, not a Promise. See
|
|
775
840
|
// vfs/facet-resident-store.ts.
|
|
776
841
|
__residentBind(workerCtx);
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
//
|
|
781
|
-
//
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
786
|
-
|
|
787
|
-
//
|
|
788
|
-
// first synchronous read of an untouched file succeed, and it is the ONLY
|
|
789
|
-
// blocking step: the waiting is done once, here, before the program's
|
|
790
|
-
// first instruction, so that no synchronous read after it ever has to
|
|
791
|
-
// 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.
|
|
792
853
|
//
|
|
793
|
-
//
|
|
794
|
-
//
|
|
795
|
-
//
|
|
796
|
-
//
|
|
797
|
-
//
|
|
798
|
-
//
|
|
799
|
-
|
|
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.
|
|
800
873
|
try {
|
|
801
|
-
const
|
|
802
|
-
|
|
803
|
-
|
|
804
|
-
}
|
|
805
|
-
} catch
|
|
806
|
-
// A failed pass is a smaller resident set, not a dead process: every
|
|
807
|
-
// path it did not reach reads exactly as it would have without this
|
|
808
|
-
// store. Surfacing beats a silent capability loss.
|
|
809
|
-
try { globalThis.__nimbusResidentFillError = (__e && __e.message) || String(__e); } catch {}
|
|
810
|
-
}
|
|
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 {}
|
|
811
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);
|
|
812
892
|
const __vfsBundle = __nimbusResidentBundle;
|
|
813
893
|
const __pendingIO = [];
|
|
814
894
|
let __rpcDrops = 0;
|
|
@@ -834,23 +914,23 @@ ${VFS_CURSOR_SEED_SOURCE}
|
|
|
834
914
|
let stdout = "", stderr = "";
|
|
835
915
|
let exitCode = 0;
|
|
836
916
|
const __nimbusDeferProcessExitReport = true;
|
|
837
|
-
${
|
|
917
|
+
${sources.ledger}
|
|
838
918
|
const __vfsDirs = {};
|
|
839
919
|
|
|
840
920
|
${ENTRYPOINT_TIMER_TRACKER}
|
|
841
|
-
${shims}
|
|
921
|
+
${sources.shims}
|
|
842
922
|
|
|
843
923
|
${ENTRYPOINT_EVENT_LOOP}
|
|
844
924
|
${RESIDENCY_MISS_REPORT}
|
|
845
925
|
|
|
846
926
|
if (__supervisor && !captureOutput) {
|
|
847
|
-
__consoleMod.log = (...a) => { const s = __utilMod.format(...a) + "\\n"; stdout += s; __queueRpcWrite("stdout", __nimbusOutEnc.encode(s)); };
|
|
848
|
-
__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)); };
|
|
849
929
|
__consoleMod.warn = __consoleMod.error;
|
|
850
930
|
__consoleMod.info = __consoleMod.log;
|
|
851
931
|
__consoleMod.debug = __consoleMod.log;
|
|
852
|
-
__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; };
|
|
853
|
-
__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; };
|
|
854
934
|
}
|
|
855
935
|
|
|
856
936
|
try { globalThis.console = __consoleMod; } catch {}
|
|
@@ -935,7 +1015,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
935
1015
|
if (Number(code ?? 0) === 0) code = 1;
|
|
936
1016
|
try { await __supervisor.stderr(__nimbusOutEnc.encode(__residencyReport)); } catch {}
|
|
937
1017
|
}
|
|
938
|
-
await __supervisor.reportExit(code, reason || "");
|
|
1018
|
+
await __supervisor.reportExit(code, reason || "", __nimbusStagingMisses(), [...(globalThis.__nimbusProfileStaged || [])]);
|
|
939
1019
|
__nimbusProcessExitReported = true;
|
|
940
1020
|
};
|
|
941
1021
|
const __nimbusReportLifecycleFailure = async (e) => {
|
|
@@ -967,6 +1047,11 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
967
1047
|
} else {
|
|
968
1048
|
finalCode = Number(await __nimbusProcessExitPromise);
|
|
969
1049
|
}
|
|
1050
|
+
if (__nimbusProcessExitCode === null) {
|
|
1051
|
+
__nimbusEmitExit(finalCode);
|
|
1052
|
+
if (__nimbusProcessExitCode !== null) finalCode = __nimbusProcessExitCode;
|
|
1053
|
+
__nimbusProgramStopped = true;
|
|
1054
|
+
}
|
|
970
1055
|
await __nimbusFlushRuntime();
|
|
971
1056
|
await __nimbusReportFinalExit(finalCode, "");
|
|
972
1057
|
})().catch(async (e) => {
|
|
@@ -994,9 +1079,7 @@ ${RESIDENCY_MISS_REPORT}
|
|
|
994
1079
|
try { await __supervisor.stderr(__nimbusOutEnc.encode(tail)); } catch {}
|
|
995
1080
|
}
|
|
996
1081
|
if (exitCode !== 0) {
|
|
997
|
-
|
|
998
|
-
await __supervisor.reportExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
|
|
999
|
-
}
|
|
1082
|
+
await __nimbusReportFinalExit(exitCode, stderr || ("exit " + exitCode + "\\n"));
|
|
1000
1083
|
throw new Error(stderr || ("long-running node startup exited " + exitCode));
|
|
1001
1084
|
}
|
|
1002
1085
|
__nimbusStarted = true;
|
|
@@ -1032,7 +1115,13 @@ export class NimbusProcess extends DurableObject {
|
|
|
1032
1115
|
if (startArgs) __nimbusStartArgs = startArgs;
|
|
1033
1116
|
await __nimbusEnsureStarted(this.env, this.ctx, __nimbusStartArgs);
|
|
1034
1117
|
if (__nimbusAttachedLifecycle) await __nimbusAttachedLifecycle;
|
|
1035
|
-
|
|
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 };
|
|
1036
1125
|
}
|
|
1037
1126
|
async fetch(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
|
|
1038
1127
|
async handleHttpRequest(req) { return __nimbusDispatchHttp(req, this.env, this.ctx); }
|
|
@@ -1041,13 +1130,33 @@ export class NimbusProcess extends DurableObject {
|
|
|
1041
1130
|
modules: bundleSource.modules,
|
|
1042
1131
|
};
|
|
1043
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
|
+
}
|
|
1044
1154
|
/**
|
|
1045
1155
|
* Drop the raw forms of everything that has been serialized, in place.
|
|
1046
1156
|
*
|
|
1047
|
-
* `bundleSource
|
|
1048
|
-
*
|
|
1049
|
-
*
|
|
1050
|
-
* 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.
|
|
1051
1160
|
* Holding both doubles the cost of a cached entry for its whole lifetime, and
|
|
1052
1161
|
* that lifetime spans execs.
|
|
1053
1162
|
*
|
|
@@ -1060,16 +1169,12 @@ export class NimbusProcess extends DurableObject {
|
|
|
1060
1169
|
export function releaseSerializedSources(vfsState) {
|
|
1061
1170
|
if (vfsState.bundleSource)
|
|
1062
1171
|
vfsState.bundle = {};
|
|
1063
|
-
if (vfsState.serializedManifest !== undefined)
|
|
1064
|
-
vfsState.manifest = {};
|
|
1065
|
-
if (vfsState.serializedMetadata !== undefined)
|
|
1066
|
-
vfsState.metadata = {};
|
|
1067
1172
|
}
|
|
1068
1173
|
/**
|
|
1069
1174
|
* Drop the serialized forms once a module map has been generated from them.
|
|
1070
1175
|
*
|
|
1071
|
-
* The generated source is a total encoding of
|
|
1072
|
-
*
|
|
1176
|
+
* The generated source is a total encoding of it: every byte of the bundle
|
|
1177
|
+
* expression is inside it. Holding them
|
|
1073
1178
|
* afterwards keeps a second copy of the largest thing this DO builds alive for
|
|
1074
1179
|
* as long as the facet runs — for pi, 22.7 MB across the ~20 s window in which
|
|
1075
1180
|
* the isolate was being reset. For the one-shot path that window is the run;
|
|
@@ -1086,8 +1191,6 @@ export function releaseSerializedSources(vfsState) {
|
|
|
1086
1191
|
*/
|
|
1087
1192
|
export function releaseGeneratedSources(vfsState) {
|
|
1088
1193
|
vfsState.bundleSource = undefined;
|
|
1089
|
-
vfsState.serializedManifest = undefined;
|
|
1090
|
-
vfsState.serializedMetadata = undefined;
|
|
1091
1194
|
vfsState.generatedSourcesReleased = true;
|
|
1092
1195
|
}
|
|
1093
1196
|
/**
|
|
@@ -1121,7 +1224,9 @@ async function facetVfsBundleSourceFor(vfsState, pacer) {
|
|
|
1121
1224
|
* pre-fix readFileString contract).
|
|
1122
1225
|
*/
|
|
1123
1226
|
async function _readBundleCell(vfs, path) {
|
|
1124
|
-
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 });
|
|
1125
1230
|
try {
|
|
1126
1231
|
return new TextDecoder('utf-8', { fatal: true }).decode(bytes);
|
|
1127
1232
|
}
|
|
@@ -1134,6 +1239,12 @@ async function _readBundleCell(vfs, path) {
|
|
|
1134
1239
|
* Strings counted as char-length (a slight under-count for non-ASCII
|
|
1135
1240
|
* but matches the pre-fix behaviour); Uint8Array counted as byteLength.
|
|
1136
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
|
+
}
|
|
1137
1248
|
function _bundleCellLength(cell) {
|
|
1138
1249
|
return typeof cell === 'string' ? cell.length : cell.byteLength;
|
|
1139
1250
|
}
|
|
@@ -1149,13 +1260,10 @@ function _bundleCellLength(cell) {
|
|
|
1149
1260
|
* BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES); omit it for budget-only checks.
|
|
1150
1261
|
*/
|
|
1151
1262
|
async function _bundleAdmits(vfs, path, budgetState, perFileCeiling) {
|
|
1152
|
-
|
|
1153
|
-
|
|
1154
|
-
size = (await vfs.lstat(path)).size;
|
|
1155
|
-
}
|
|
1156
|
-
catch {
|
|
1263
|
+
const stat = await filesOf(vfs).lstat(path).catch(() => null);
|
|
1264
|
+
if (stat === null)
|
|
1157
1265
|
return false;
|
|
1158
|
-
|
|
1266
|
+
const size = stat.size;
|
|
1159
1267
|
if (perFileCeiling !== undefined && size > perFileCeiling)
|
|
1160
1268
|
return false;
|
|
1161
1269
|
return budgetState.totalBytes + size <= VFS_BUNDLE_MAX_BYTES;
|
|
@@ -1189,8 +1297,6 @@ function retainedVfsStateBytes(state) {
|
|
|
1189
1297
|
for (const moduleSource of Object.values(source.modules))
|
|
1190
1298
|
bytes += moduleSource.length;
|
|
1191
1299
|
}
|
|
1192
|
-
bytes += state.serializedManifest?.length ?? 0;
|
|
1193
|
-
bytes += state.serializedMetadata?.length ?? 0;
|
|
1194
1300
|
return bytes;
|
|
1195
1301
|
}
|
|
1196
1302
|
/**
|
|
@@ -1270,8 +1376,8 @@ function _jsonEncodedBytes(value) {
|
|
|
1270
1376
|
}
|
|
1271
1377
|
return bytes;
|
|
1272
1378
|
}
|
|
1273
|
-
/** `{"bundle":` +
|
|
1274
|
-
const ENCODED_PAYLOAD_FRAME_BYTES =
|
|
1379
|
+
/** `{"bundle":` + `}`: the frame around the one member. */
|
|
1380
|
+
const ENCODED_PAYLOAD_FRAME_BYTES = 11;
|
|
1275
1381
|
/**
|
|
1276
1382
|
* Running UTF-8 byte length of `JSON.stringify({ bundle, manifest })`,
|
|
1277
1383
|
* accumulated one cell at a time.
|
|
@@ -1287,10 +1393,9 @@ const ENCODED_PAYLOAD_FRAME_BYTES = 23;
|
|
|
1287
1393
|
* the total is a sum of independent per-cell terms: exact, incremental, and
|
|
1288
1394
|
* never holding more than one cell's worth of scratch.
|
|
1289
1395
|
*/
|
|
1290
|
-
export function encodedBundleSize(bundle
|
|
1396
|
+
export function encodedBundleSize(bundle) {
|
|
1291
1397
|
const cells = new Map();
|
|
1292
1398
|
let cellSum = 0;
|
|
1293
|
-
const manifestBytes = _jsonEncodedBytes(manifest);
|
|
1294
1399
|
const self = {
|
|
1295
1400
|
add(path, cell) {
|
|
1296
1401
|
if (cells.has(path))
|
|
@@ -1308,7 +1413,7 @@ export function encodedBundleSize(bundle, manifest) {
|
|
|
1308
1413
|
},
|
|
1309
1414
|
get bytes() {
|
|
1310
1415
|
const separators = Math.max(0, cells.size - 1);
|
|
1311
|
-
return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators
|
|
1416
|
+
return ENCODED_PAYLOAD_FRAME_BYTES + 2 + cellSum + separators;
|
|
1312
1417
|
},
|
|
1313
1418
|
};
|
|
1314
1419
|
for (const [path, cell] of Object.entries(bundle))
|
|
@@ -1471,7 +1576,13 @@ function _inlineBundleSourceBytes(bundle) {
|
|
|
1471
1576
|
* the merge expression concatenates those fragments back to the original
|
|
1472
1577
|
* string or Uint8Array before module precompilation begins.
|
|
1473
1578
|
*/
|
|
1474
|
-
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;
|
|
1475
1586
|
// Size the inline form before building it. A bundle that will be split has
|
|
1476
1587
|
// no use for the whole-bundle expression, and building one to read its
|
|
1477
1588
|
// length off cost a second full copy of the largest string this DO makes.
|
|
@@ -1522,46 +1633,55 @@ export async function buildFacetVfsBundleSource(bundle, forceSideModules = false
|
|
|
1522
1633
|
}
|
|
1523
1634
|
return pieces;
|
|
1524
1635
|
}
|
|
1525
|
-
const chunks = [];
|
|
1526
|
-
let chunk = {};
|
|
1527
|
-
let estimatedBytes = 0;
|
|
1528
|
-
function flushChunk() {
|
|
1529
|
-
if (Object.keys(chunk).length === 0)
|
|
1530
|
-
return;
|
|
1531
|
-
chunks.push(chunk);
|
|
1532
|
-
chunk = {};
|
|
1533
|
-
estimatedBytes = 0;
|
|
1534
|
-
}
|
|
1535
|
-
for (const [path, cell] of Object.entries(bundle)) {
|
|
1536
|
-
for (const [piecePath, pieceCell] of splitCell(path, cell)) {
|
|
1537
|
-
const pieceBytes = sourceBytes(piecePath, pieceCell);
|
|
1538
|
-
if (piecePath in chunk
|
|
1539
|
-
|| (estimatedBytes > 0 && estimatedBytes + pieceBytes > maxModuleBytes)) {
|
|
1540
|
-
flushChunk();
|
|
1541
|
-
}
|
|
1542
|
-
chunk[piecePath] = pieceCell;
|
|
1543
|
-
estimatedBytes += pieceBytes;
|
|
1544
|
-
}
|
|
1545
|
-
}
|
|
1546
|
-
flushChunk();
|
|
1547
1636
|
const modules = {};
|
|
1548
1637
|
const imports = [];
|
|
1549
1638
|
const aliases = [];
|
|
1550
|
-
|
|
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;
|
|
1551
1651
|
const moduleName = `${FACET_VFS_MODULE_PREFIX}${index}.js`;
|
|
1552
1652
|
const alias = `__nimbusVfsBundle${index}`;
|
|
1553
|
-
const source = _facetBundleModuleSource(
|
|
1653
|
+
const source = _facetBundleModuleSource(chunk);
|
|
1554
1654
|
if (_encodedSourceBytes(source) > BUNDLE_MAX_ENCODED_BYTES) {
|
|
1555
1655
|
throw new Error(`Nimbus: generated VFS side module exceeds encoded limit: ${moduleName}`);
|
|
1556
1656
|
}
|
|
1557
1657
|
modules[moduleName] = source;
|
|
1558
1658
|
imports.push(`import ${alias} from "${moduleName}";`);
|
|
1559
1659
|
aliases.push(alias);
|
|
1660
|
+
if (consume)
|
|
1661
|
+
for (const path of chunkPaths)
|
|
1662
|
+
delete bundle[path];
|
|
1663
|
+
chunk = {};
|
|
1664
|
+
chunkPaths = [];
|
|
1665
|
+
estimatedBytes = 0;
|
|
1560
1666
|
// Each side module is an independent serialization of its own cells, so
|
|
1561
1667
|
// the turn may end between any two of them.
|
|
1562
1668
|
if (pacer)
|
|
1563
1669
|
await pacer.spend(source.length);
|
|
1564
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();
|
|
1565
1685
|
const expression = `(function(__parts){const __out={};for(const __part of __parts){` +
|
|
1566
1686
|
`for(const [__k,__v] of Object.entries(__part)){const __prev=__out[__k];` +
|
|
1567
1687
|
`if(__prev===undefined){__out[__k]=__v;}` +
|
|
@@ -1616,174 +1736,47 @@ function _fnv1a(s) {
|
|
|
1616
1736
|
return h.toString(16);
|
|
1617
1737
|
}
|
|
1618
1738
|
const MANIFEST_MAX_DEPTH = 12;
|
|
1619
|
-
|
|
1620
|
-
* Build the manifest pass — uncapped path→child-names map. UNCHANGED
|
|
1621
|
-
* from W2.5b; this is the W2.5b root-cause fix and continues to keep
|
|
1622
|
-
* fs.readdirSync / fs.statSync honest regardless of which subset of
|
|
1623
|
-
* file CONTENT we ship.
|
|
1624
|
-
*/
|
|
1625
|
-
async function buildManifest(vfs, cwd, scriptPath) {
|
|
1626
|
-
const manifest = {};
|
|
1627
|
-
async function walk(dirPath, depth = 0) {
|
|
1628
|
-
if (depth > MANIFEST_MAX_DEPTH)
|
|
1629
|
-
return;
|
|
1630
|
-
const stripped = dirPath.replace(/^\/+/, '');
|
|
1631
|
-
if (stripped in manifest)
|
|
1632
|
-
return;
|
|
1633
|
-
let entries;
|
|
1634
|
-
try {
|
|
1635
|
-
entries = (await vfs.readdir(stripped));
|
|
1636
|
-
}
|
|
1637
|
-
catch {
|
|
1638
|
-
return;
|
|
1639
|
-
}
|
|
1640
|
-
manifest[stripped] = entries.map((e) => e.name);
|
|
1641
|
-
for (const entry of entries) {
|
|
1642
|
-
if (entry.type === 'directory') {
|
|
1643
|
-
const childPath = stripped ? stripped + '/' + entry.name : entry.name;
|
|
1644
|
-
(await walk(childPath, depth + 1));
|
|
1645
|
-
}
|
|
1646
|
-
}
|
|
1647
|
-
}
|
|
1648
|
-
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1649
|
-
// ── The path from the root down to the working directory ──────────────
|
|
1650
|
-
//
|
|
1651
|
-
// One level each, before the deep walks, so that every directory on that
|
|
1652
|
-
// chain is ENUMERATED rather than merely mentioned. It is what makes a
|
|
1653
|
-
// synchronous "not there" honest for the shape programs probe most: node's
|
|
1654
|
-
// resolver walks upward asking for `<dir>/node_modules` at every level, and
|
|
1655
|
-
// a config lookup asks for a dotfile directory in $HOME. Those paths mostly
|
|
1656
|
-
// do not exist, and answering that requires having listed the directory
|
|
1657
|
-
// they would be in — otherwise the only honest answer is a refusal, and the
|
|
1658
|
-
// resolver pays a round trip per rung to hear it.
|
|
1659
|
-
//
|
|
1660
|
-
// Cheap by construction: one readdir per component, names only, and the
|
|
1661
|
-
// recursion below each stops immediately.
|
|
1662
|
-
{
|
|
1663
|
-
const segments = cwdStripped ? cwdStripped.split('/') : [];
|
|
1664
|
-
for (let i = 0; i < segments.length; i++) {
|
|
1665
|
-
(await walk(segments.slice(0, i).join('/'), MANIFEST_MAX_DEPTH));
|
|
1666
|
-
}
|
|
1667
|
-
}
|
|
1668
|
-
(await walk(cwdStripped, 0));
|
|
1669
|
-
const nmDir = cwdStripped + '/node_modules';
|
|
1670
|
-
if ((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))) {
|
|
1671
|
-
(await walk(nmDir, 0));
|
|
1672
|
-
}
|
|
1673
|
-
// ── Bin-target package + hoisted dependency roots ─────────────────────
|
|
1674
|
-
//
|
|
1675
|
-
// When the entry script lives in a node_modules outside cwd (npx-cache
|
|
1676
|
-
// packages, globally-installed bins, etc.), buildManifest's cwd walk
|
|
1677
|
-
// misses the package's sibling files. The bin's index.js gets
|
|
1678
|
-
// require-walked + greedy-added to the BUNDLE, but the MANIFEST
|
|
1679
|
-
// (which is the source of truth for `fs.readdirSync`) was empty for
|
|
1680
|
-
// those paths — so `readdirSync('/tmp/.npx-cache/.../template-X')`
|
|
1681
|
-
// returned [] and `create-vite` scaffolded zero files.
|
|
1682
|
-
//
|
|
1683
|
-
// Walk the innermost `node_modules/<pkg>/` of `scriptPath` so its
|
|
1684
|
-
// entire package tree is enumerable via readdir. Also walk that
|
|
1685
|
-
// `node_modules` directory itself: global npm bins resolve hoisted
|
|
1686
|
-
// transitive dependencies as siblings of the bin's own package.
|
|
1687
|
-
// Bounded by MANIFEST_MAX_DEPTH; same depth budget as the cwd walk.
|
|
1688
|
-
if (scriptPath) {
|
|
1689
|
-
const sp = scriptPath.replace(/^\/+/, '');
|
|
1690
|
-
const segs = sp.split('/');
|
|
1691
|
-
let nmIdx = -1;
|
|
1692
|
-
for (let i = segs.length - 1; i >= 0; i--) {
|
|
1693
|
-
if (segs[i] === 'node_modules') {
|
|
1694
|
-
nmIdx = i;
|
|
1695
|
-
break;
|
|
1696
|
-
}
|
|
1697
|
-
}
|
|
1698
|
-
if (nmIdx >= 0) {
|
|
1699
|
-
const isScoped = segs[nmIdx + 1]?.startsWith('@');
|
|
1700
|
-
const pkgEnd = isScoped ? nmIdx + 3 : nmIdx + 2;
|
|
1701
|
-
if (pkgEnd <= segs.length) {
|
|
1702
|
-
const pkgRoot = segs.slice(0, pkgEnd).join('/');
|
|
1703
|
-
if ((await vfs.exists(pkgRoot)) && (await vfs.isDirectory(pkgRoot))) {
|
|
1704
|
-
(await walk(pkgRoot, 0));
|
|
1705
|
-
}
|
|
1706
|
-
}
|
|
1707
|
-
const nodeModulesRoot = segs.slice(0, nmIdx + 1).join('/');
|
|
1708
|
-
if ((await vfs.exists(nodeModulesRoot)) && (await vfs.isDirectory(nodeModulesRoot))) {
|
|
1709
|
-
(await walk(nodeModulesRoot, 0));
|
|
1710
|
-
}
|
|
1711
|
-
}
|
|
1712
|
-
}
|
|
1713
|
-
return manifest;
|
|
1714
|
-
}
|
|
1715
|
-
async function buildVfsMetadata(vfs, manifest, bundle) {
|
|
1716
|
-
const paths = new Set(Object.keys(bundle).filter((path) => compiledCellPath(path) === null));
|
|
1717
|
-
for (const [directory, children] of Object.entries(manifest)) {
|
|
1718
|
-
paths.add(directory);
|
|
1719
|
-
for (const child of children) {
|
|
1720
|
-
paths.add(directory ? `${directory}/${child}` : child);
|
|
1721
|
-
}
|
|
1722
|
-
}
|
|
1723
|
-
const metadata = {};
|
|
1724
|
-
for (const path of paths) {
|
|
1725
|
-
try {
|
|
1726
|
-
const stat = await vfs.authority.stat(path, { followSymlinks: false });
|
|
1727
|
-
if (!stat)
|
|
1728
|
-
continue;
|
|
1729
|
-
metadata[path] = {
|
|
1730
|
-
type: stat.type,
|
|
1731
|
-
size: stat.size,
|
|
1732
|
-
mode: stat.mode,
|
|
1733
|
-
uid: stat.uid,
|
|
1734
|
-
gid: stat.gid,
|
|
1735
|
-
};
|
|
1736
|
-
}
|
|
1737
|
-
catch {
|
|
1738
|
-
// The credentialed lookup is authoritative; inaccessible ancestors do
|
|
1739
|
-
// not reveal whether a leaf exists.
|
|
1740
|
-
}
|
|
1741
|
-
}
|
|
1742
|
-
return metadata;
|
|
1743
|
-
}
|
|
1744
|
-
async function addUnreadableDenialCells(vfs, bundle, metadata) {
|
|
1745
|
-
for (const [path, stat] of Object.entries(metadata)) {
|
|
1746
|
-
if (stat.type === 'directory' || path in bundle)
|
|
1747
|
-
continue;
|
|
1748
|
-
try {
|
|
1749
|
-
(await vfs.access(path, 0o4));
|
|
1750
|
-
}
|
|
1751
|
-
catch (error) {
|
|
1752
|
-
if (typeof error === 'object'
|
|
1753
|
-
&& error !== null
|
|
1754
|
-
&& 'code' in error
|
|
1755
|
-
&& error.code === 'EACCES') {
|
|
1756
|
-
bundle[path] = { error: 'EACCES' };
|
|
1757
|
-
}
|
|
1758
|
-
}
|
|
1759
|
-
}
|
|
1760
|
-
}
|
|
1761
|
-
/**
|
|
1762
|
-
* Greedy-oversample every installed package's main entry. The static
|
|
1763
|
-
* prefetch via require-resolver covers the require() chain literally
|
|
1764
|
-
* present in source; greedy oversampling adds a safety net for dynamic
|
|
1765
|
-
* patterns the regex misses (jest/`bindings`/`import-local` style
|
|
1766
|
-
* computed-path requires). Bounded to package.json + 1 main-entry file
|
|
1767
|
-
* per package — sub-agent §Q3 quantified the worst-case cumulative
|
|
1768
|
-
* budget impact (~322 KiB for fastify, ~1.7 MiB for ts-jest).
|
|
1769
|
-
*
|
|
1770
|
-
* `requiredPaths` is the static require closure. A package the closure
|
|
1771
|
-
* already reached — but reached only through a SUBPATH — has its main entry
|
|
1772
|
-
* skipped; see `mainIsSpeculative`.
|
|
1773
|
-
*/
|
|
1774
|
-
// can verify the hash-chunk + shared/ oversample directly. Pre-X.5-C this
|
|
1775
|
-
// was a file-local helper. Adding the named export is a pure surface
|
|
1776
|
-
// addition — no callers other than buildPrefetchBundle (same file) and
|
|
1777
|
-
// the new probe.
|
|
1778
|
-
export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set()) {
|
|
1739
|
+
export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requiredPaths = new Set(), options = {}) {
|
|
1779
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 };
|
|
1780
1749
|
const cwdStripped = cwd.replace(/^\/+/, '');
|
|
1781
1750
|
const nmDir = cwdStripped + '/node_modules';
|
|
1782
|
-
if (!((await vfs.exists(nmDir)) && (await vfs.isDirectory(nmDir))))
|
|
1783
|
-
return { added };
|
|
1751
|
+
if (!((await filesOf(vfs).exists(nmDir)) && (await filesOf(vfs).isDirectory(nmDir))))
|
|
1752
|
+
return { added, groups };
|
|
1784
1753
|
const exts = ['', '.js', '.cjs', '.mjs', '/index.js', '/index.cjs'];
|
|
1785
1754
|
async function addOne(path) {
|
|
1786
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
|
+
}
|
|
1787
1780
|
if (stripped in bundle)
|
|
1788
1781
|
return false;
|
|
1789
1782
|
if (budgetState.fileCount >= VFS_BUNDLE_MAX_FILES)
|
|
@@ -1791,7 +1784,7 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1791
1784
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
1792
1785
|
return false;
|
|
1793
1786
|
try {
|
|
1794
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
1787
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
1795
1788
|
return false;
|
|
1796
1789
|
// This is a guess at what a program might require, and the same
|
|
1797
1790
|
// per-file ceiling the entry-package walk applies bounds it: a
|
|
@@ -1799,8 +1792,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1799
1792
|
// `lib/typescript.js` beside the `lib/tsc.js` that actually runs), and
|
|
1800
1793
|
// one guess must not spend a third of the budget — and a third of the
|
|
1801
1794
|
// supervisor's headroom — on every invocation that never reads it.
|
|
1802
|
-
//
|
|
1803
|
-
//
|
|
1795
|
+
// Literal dependencies use the closure's byte allowance, not this
|
|
1796
|
+
// speculative-root per-file ceiling.
|
|
1804
1797
|
if (!(await _bundleAdmits(vfs, stripped, budgetState, BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)))
|
|
1805
1798
|
return false;
|
|
1806
1799
|
// A guess must not be a native binary. Nothing in a Workers isolate can
|
|
@@ -1810,14 +1803,19 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1810
1803
|
// Measured: freeing 1.3 MiB of rollup let `@napi-rs/lzma-linux-x64-gnu`'s
|
|
1811
1804
|
// 1,445,448-byte `.node` in, which the size guard had been evicting.
|
|
1812
1805
|
// Only the guess is filtered; a path the closure requires is untouched.
|
|
1813
|
-
if (
|
|
1806
|
+
if (isNativeBinPath(stripped))
|
|
1814
1807
|
return false;
|
|
1815
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;
|
|
1816
1812
|
const content = (await _readBundleCell(vfs, stripped));
|
|
1817
1813
|
const cellLen = _bundleCellLength(content);
|
|
1818
|
-
|
|
1814
|
+
const raw = _bundleCellRawBytes(content);
|
|
1815
|
+
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES || rawBytes + raw > bound)
|
|
1819
1816
|
return false;
|
|
1820
1817
|
bundle[stripped] = content;
|
|
1818
|
+
rawBytes += raw;
|
|
1821
1819
|
budgetState.totalBytes += cellLen;
|
|
1822
1820
|
budgetState.fileCount++;
|
|
1823
1821
|
added++;
|
|
@@ -1827,26 +1825,6 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1827
1825
|
return false;
|
|
1828
1826
|
}
|
|
1829
1827
|
}
|
|
1830
|
-
// X.5-C Fix #2 helper: walk a (possibly nested) exports value and
|
|
1831
|
-
// collect every string-leaf path. unbuild-shaped packages like pathe
|
|
1832
|
-
// nest two deep — `exports."."`.{require,import}.{types,default} —
|
|
1833
|
-
// and the previous one-level loop only caught the inner string leaves
|
|
1834
|
-
// when default was at the top, missing the unbuild shape entirely.
|
|
1835
|
-
function collectExportLeaves(node, out) {
|
|
1836
|
-
if (typeof node === 'string') {
|
|
1837
|
-
out.add(node);
|
|
1838
|
-
return;
|
|
1839
|
-
}
|
|
1840
|
-
if (!node || typeof node !== 'object')
|
|
1841
|
-
return;
|
|
1842
|
-
// Order matters for the "most likely usable" leaf: prefer require
|
|
1843
|
-
// (most CJS-friendly), then default, then node, then import. We add
|
|
1844
|
-
// ALL of them to the candidate set — addPkgEntry will probe each.
|
|
1845
|
-
for (const k of ['require', 'node', 'default', 'import']) {
|
|
1846
|
-
if (k in node)
|
|
1847
|
-
collectExportLeaves(node[k], out);
|
|
1848
|
-
}
|
|
1849
|
-
}
|
|
1850
1828
|
/**
|
|
1851
1829
|
* Whether guessing at `pkgDir`'s main entry is still a guess.
|
|
1852
1830
|
*
|
|
@@ -1878,47 +1856,52 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1878
1856
|
return !reached;
|
|
1879
1857
|
}
|
|
1880
1858
|
async function addPkgEntry(pkgDir) {
|
|
1881
|
-
|
|
1859
|
+
const manifest = (pkgDir + '/package.json').replace(/^\/+/, '');
|
|
1860
|
+
if (bundle[manifest] === undefined && !(await addOne(manifest)))
|
|
1861
|
+
return;
|
|
1882
1862
|
// A package the closure reached keeps exactly what it reached; only an
|
|
1883
1863
|
// unreached one gets the guess below.
|
|
1884
1864
|
if (!mainIsSpeculative(pkgDir))
|
|
1885
1865
|
return;
|
|
1886
|
-
|
|
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;
|
|
1887
1871
|
try {
|
|
1888
|
-
|
|
1872
|
+
const text = bundle[manifest];
|
|
1873
|
+
pkg = typeof text === 'string' ? parseResolvablePackageJson(text) : null;
|
|
1889
1874
|
}
|
|
1890
1875
|
catch {
|
|
1891
|
-
|
|
1892
|
-
}
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
|
|
1901
|
-
|
|
1902
|
-
|
|
1903
|
-
|
|
1904
|
-
|
|
1905
|
-
|
|
1906
|
-
|
|
1907
|
-
|
|
1908
|
-
|
|
1909
|
-
|
|
1910
|
-
|
|
1911
|
-
|
|
1912
|
-
if (candidates.size === 0)
|
|
1913
|
-
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');
|
|
1914
1897
|
for (const rel of candidates) {
|
|
1915
1898
|
const norm = rel.replace(/^\.\//, '');
|
|
1916
1899
|
const base = pkgDir + '/' + norm;
|
|
1917
1900
|
let landed = false;
|
|
1918
1901
|
const tries = /\.[a-z]+$/.test(norm) ? [base] : exts.map((e) => base + e);
|
|
1919
1902
|
for (const candidate of tries) {
|
|
1920
|
-
if ((await vfs.exists(candidate.replace(/^\/+/, ''))) &&
|
|
1921
|
-
!(await vfs.isDirectory(candidate.replace(/^\/+/, '')))) {
|
|
1903
|
+
if ((await filesOf(vfs).exists(candidate.replace(/^\/+/, ''))) &&
|
|
1904
|
+
!(await filesOf(vfs).isDirectory(candidate.replace(/^\/+/, '')))) {
|
|
1922
1905
|
if ((await addOne(candidate))) {
|
|
1923
1906
|
landed = true;
|
|
1924
1907
|
break;
|
|
@@ -1936,8 +1919,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1936
1919
|
// The static walker cannot discover computed hash-chunk imports, so
|
|
1937
1920
|
// the greedy oversample is the safety net for their reachability.
|
|
1938
1921
|
const entryDir = base.replace(/\/[^/]+$/, '');
|
|
1939
|
-
|
|
1940
|
-
const sibs =
|
|
1922
|
+
{
|
|
1923
|
+
const sibs = await (async () => vfs.readdir(entryDir))().catch(() => []);
|
|
1941
1924
|
for (const sib of sibs) {
|
|
1942
1925
|
if (sib.type !== 'file')
|
|
1943
1926
|
continue;
|
|
@@ -1965,8 +1948,8 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1965
1948
|
// typical shared/ dir returns 1-5 files.
|
|
1966
1949
|
const sharedDir = entryDir + '/shared';
|
|
1967
1950
|
const sharedStripped = sharedDir.replace(/^\/+/, '');
|
|
1968
|
-
if (
|
|
1969
|
-
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(() => [])) {
|
|
1970
1953
|
if (sh.type !== 'file')
|
|
1971
1954
|
continue;
|
|
1972
1955
|
if (!/\.(cjs|mjs|js)$/.test(sh.name))
|
|
@@ -1975,22 +1958,20 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
1975
1958
|
}
|
|
1976
1959
|
}
|
|
1977
1960
|
}
|
|
1978
|
-
catch { /* unreadable dir — drop sibling oversample, entry
|
|
1979
|
-
file is enough */
|
|
1980
|
-
}
|
|
1981
1961
|
break;
|
|
1982
1962
|
}
|
|
1983
1963
|
}
|
|
1984
1964
|
}
|
|
1985
1965
|
for (const pkgDir of (await speculativePackageDirs(vfs, cwdStripped, bundle)))
|
|
1986
1966
|
(await addPkgEntry(pkgDir));
|
|
1987
|
-
return { added };
|
|
1967
|
+
return { added, groups };
|
|
1988
1968
|
}
|
|
1989
1969
|
/**
|
|
1990
|
-
* The packages a computed `require(name)
|
|
1991
|
-
*
|
|
1992
|
-
* ONE `dependencies` hop from a package
|
|
1993
|
-
*
|
|
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.
|
|
1994
1975
|
*
|
|
1995
1976
|
* Unbounded, the greedy oversample read every installed package's main:
|
|
1996
1977
|
* for `node -e "import('got')"` in got's repo — a one-file static closure —
|
|
@@ -2002,11 +1983,26 @@ export async function greedyAddMainEntries(vfs, cwd, bundle, budgetState, requir
|
|
|
2002
1983
|
* tree. Computed requires almost always target a declared runtime
|
|
2003
1984
|
* dependency of the package doing the requiring, so the bound is one hop
|
|
2004
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.
|
|
2005
2001
|
*/
|
|
2006
2002
|
export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
2007
2003
|
const runtimeDeps = async (pkgJsonPath) => {
|
|
2008
2004
|
try {
|
|
2009
|
-
const meta = JSON.parse((await vfs.readFileString(pkgJsonPath)));
|
|
2005
|
+
const meta = JSON.parse((await filesOf(vfs).readFileString(pkgJsonPath)));
|
|
2010
2006
|
const names = new Set();
|
|
2011
2007
|
for (const field of ['dependencies', 'optionalDependencies']) {
|
|
2012
2008
|
const deps = meta?.[field];
|
|
@@ -2035,7 +2031,7 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2035
2031
|
let dir = fromDir;
|
|
2036
2032
|
for (;;) {
|
|
2037
2033
|
const candidate = dir + '/node_modules/' + name;
|
|
2038
|
-
if ((await vfs.exists(candidate + '/package.json')))
|
|
2034
|
+
if ((await filesOf(vfs).exists(candidate + '/package.json')))
|
|
2039
2035
|
return candidate;
|
|
2040
2036
|
const idx = dir.lastIndexOf('/');
|
|
2041
2037
|
if (idx <= 0)
|
|
@@ -2044,14 +2040,24 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2044
2040
|
}
|
|
2045
2041
|
};
|
|
2046
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.
|
|
2047
2046
|
const hop = async (fromDir) => {
|
|
2047
|
+
if (hopped.has(fromDir))
|
|
2048
|
+
return [];
|
|
2049
|
+
hopped.add(fromDir);
|
|
2050
|
+
const landed = [];
|
|
2048
2051
|
for (const name of (await runtimeDeps(fromDir + '/package.json'))) {
|
|
2049
2052
|
const dir = (await resolveDir(name, fromDir));
|
|
2050
|
-
if (dir
|
|
2051
|
-
|
|
2053
|
+
if (dir === null)
|
|
2054
|
+
continue;
|
|
2055
|
+
reached.add(dir);
|
|
2056
|
+
landed.push(dir);
|
|
2052
2057
|
}
|
|
2058
|
+
return landed;
|
|
2053
2059
|
};
|
|
2054
|
-
(await hop(cwdStripped));
|
|
2060
|
+
const projectDeps = (await hop(cwdStripped));
|
|
2055
2061
|
const owners = new Set();
|
|
2056
2062
|
for (const path of Object.keys(bundle)) {
|
|
2057
2063
|
const owner = ownerOf(path);
|
|
@@ -2062,6 +2068,12 @@ export async function speculativePackageDirs(vfs, cwdStripped, bundle) {
|
|
|
2062
2068
|
reached.add(owner);
|
|
2063
2069
|
(await hop(owner));
|
|
2064
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));
|
|
2065
2077
|
return [...reached].sort();
|
|
2066
2078
|
}
|
|
2067
2079
|
/**
|
|
@@ -2130,7 +2142,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
|
2130
2142
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2131
2143
|
return false;
|
|
2132
2144
|
try {
|
|
2133
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2145
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2134
2146
|
return false;
|
|
2135
2147
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2136
2148
|
return false;
|
|
@@ -2226,7 +2238,7 @@ export async function addStaticReadFileAssets(vfs, cwd, bundle, budgetState) {
|
|
|
2226
2238
|
* (0, path_1.resolve)(__dirname, '../../../.ts-jest-digest'), 'utf8');
|
|
2227
2239
|
*
|
|
2228
2240
|
* The install pipeline writes `.ts-jest-digest` to VFS correctly
|
|
2229
|
-
* (
|
|
2241
|
+
* (the namespace lists it). But the runtime
|
|
2230
2242
|
* fs shim's readFileSync (`src/node-shims.ts:202-215`) consults
|
|
2231
2243
|
* `__vfsBundle` only, and none of the existing bundle-population
|
|
2232
2244
|
* passes — `prefetchForRequire` (require-graph), `greedyAddMainEntries`
|
|
@@ -2305,7 +2317,7 @@ export async function addStaticReadFileDotfilesAndCompiled(vfs, cwd, bundle, bud
|
|
|
2305
2317
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2306
2318
|
return false;
|
|
2307
2319
|
try {
|
|
2308
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2320
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2309
2321
|
return false;
|
|
2310
2322
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2311
2323
|
return false;
|
|
@@ -2494,13 +2506,10 @@ export async function addBinTargetSiblings(vfs, scriptPath, bundle, budgetState,
|
|
|
2494
2506
|
wasmPaths.push(child);
|
|
2495
2507
|
if (bundle[child] !== undefined)
|
|
2496
2508
|
continue;
|
|
2497
|
-
|
|
2498
|
-
|
|
2499
|
-
size = (await vfs.lstat(child)).size;
|
|
2500
|
-
}
|
|
2501
|
-
catch {
|
|
2509
|
+
const stat = await filesOf(vfs).lstat(child).catch(() => null);
|
|
2510
|
+
if (stat === null)
|
|
2502
2511
|
continue;
|
|
2503
|
-
|
|
2512
|
+
const size = stat.size;
|
|
2504
2513
|
if (size > BIN_PACKAGE_SPECULATIVE_MAX_FILE_BYTES)
|
|
2505
2514
|
continue;
|
|
2506
2515
|
candidates.push({ path: child, size });
|
|
@@ -2566,13 +2575,9 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
|
|
|
2566
2575
|
for (const path of unstagedPaths) {
|
|
2567
2576
|
if (byPath.has(path))
|
|
2568
2577
|
continue;
|
|
2569
|
-
|
|
2570
|
-
|
|
2571
|
-
bytes = await vfs.readFile(path);
|
|
2572
|
-
}
|
|
2573
|
-
catch {
|
|
2578
|
+
const bytes = await filesOf(vfs).readBytes(path).catch(() => null);
|
|
2579
|
+
if (bytes === null)
|
|
2574
2580
|
continue;
|
|
2575
|
-
}
|
|
2576
2581
|
byPath.set(path, { vfsPath: '/' + stripLeadingSlashes(path), digest: wasmImageDigest(bytes) });
|
|
2577
2582
|
}
|
|
2578
2583
|
return [...byPath.values()];
|
|
@@ -2592,21 +2597,29 @@ export async function collectClosureWasmImages(vfs, bundle, unstagedPaths) {
|
|
|
2592
2597
|
* Admitted smallest-first for the same reason as the entry-package walk: the
|
|
2593
2598
|
* budget is shared, so ordering by size maximizes the number of misses a
|
|
2594
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.
|
|
2595
2606
|
*/
|
|
2596
|
-
export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState) {
|
|
2607
|
+
export async function addObservedReads(vfs, observed, bundle, requiredPaths, budgetState, room = Infinity, pacer) {
|
|
2597
2608
|
if (!observed || observed.size === 0)
|
|
2598
|
-
return { added: 0 };
|
|
2609
|
+
return { added: 0, bytes: 0 };
|
|
2610
|
+
let bytes = 0;
|
|
2599
2611
|
const candidates = [];
|
|
2600
2612
|
for (const path of observed) {
|
|
2601
|
-
if (path === ''
|
|
2613
|
+
if (path === '')
|
|
2602
2614
|
continue;
|
|
2603
|
-
|
|
2604
|
-
|
|
2605
|
-
|
|
2606
|
-
}
|
|
2607
|
-
catch {
|
|
2615
|
+
// Already staged, but evictable: the evidence is what makes it required.
|
|
2616
|
+
if (bundle[path] !== undefined) {
|
|
2617
|
+
requiredPaths.add(path);
|
|
2608
2618
|
continue;
|
|
2609
2619
|
}
|
|
2620
|
+
const stat = await filesOf(vfs).lstat(path).catch(() => null);
|
|
2621
|
+
if (stat === null)
|
|
2622
|
+
continue;
|
|
2610
2623
|
if (stat.type === 'directory')
|
|
2611
2624
|
continue;
|
|
2612
2625
|
candidates.push({ path, size: stat.size });
|
|
@@ -2628,13 +2641,48 @@ export async function addObservedReads(vfs, observed, bundle, requiredPaths, bud
|
|
|
2628
2641
|
const cellLen = _bundleCellLength(content);
|
|
2629
2642
|
if (budgetState.totalBytes + cellLen > VFS_BUNDLE_MAX_BYTES)
|
|
2630
2643
|
continue;
|
|
2644
|
+
const raw = _bundleCellRawBytes(content);
|
|
2645
|
+
if (bytes + raw > room)
|
|
2646
|
+
continue;
|
|
2631
2647
|
bundle[candidate.path] = content;
|
|
2632
2648
|
requiredPaths.add(candidate.path);
|
|
2649
|
+
bytes += raw;
|
|
2633
2650
|
budgetState.totalBytes += cellLen;
|
|
2634
2651
|
budgetState.fileCount++;
|
|
2635
2652
|
added++;
|
|
2636
2653
|
}
|
|
2637
|
-
|
|
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 };
|
|
2638
2686
|
}
|
|
2639
2687
|
const RUNTIME_PACKAGE_EXCLUDED_ROOT_DIRS = new Set([
|
|
2640
2688
|
'docs',
|
|
@@ -2677,6 +2725,31 @@ const RUNTIME_PACKAGE_EXCLUDED_FILE_SUFFIXES = [
|
|
|
2677
2725
|
'.mov',
|
|
2678
2726
|
'.webm',
|
|
2679
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
|
+
}
|
|
2680
2753
|
/**
|
|
2681
2754
|
* Per-file ceiling for the speculative passes over installed packages: the
|
|
2682
2755
|
* entry-package walk (`addBinTargetSiblings`) and the main-entry oversample
|
|
@@ -2828,7 +2901,7 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
|
2828
2901
|
if (budgetState.totalBytes >= VFS_BUNDLE_MAX_BYTES)
|
|
2829
2902
|
return;
|
|
2830
2903
|
try {
|
|
2831
|
-
if (!(await vfs.exists(stripped)) || (await vfs.isDirectory(stripped)))
|
|
2904
|
+
if (!(await filesOf(vfs).exists(stripped)) || (await filesOf(vfs).isDirectory(stripped)))
|
|
2832
2905
|
return;
|
|
2833
2906
|
if (!(await _bundleAdmits(vfs, stripped, budgetState)))
|
|
2834
2907
|
return;
|
|
@@ -2881,8 +2954,19 @@ async function addEntryAbsPathReads(vfs, entryCode, bundle, budgetState) {
|
|
|
2881
2954
|
}
|
|
2882
2955
|
return { added };
|
|
2883
2956
|
}
|
|
2884
|
-
function looksLikeEsm(src) {
|
|
2885
|
-
|
|
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
|
+
}
|
|
2886
2970
|
}
|
|
2887
2971
|
/**
|
|
2888
2972
|
* W3.5 Fix B — module-level cache for ESM→CJS transform results, keyed
|
|
@@ -2966,14 +3050,21 @@ function _markBundleEsmAsFailed(bundle, reason) {
|
|
|
2966
3050
|
// A TypeScript source is never runnable as staged, so it always needs
|
|
2967
3051
|
// the emit it cannot get; a JavaScript file only if it is ESM.
|
|
2968
3052
|
const typescript = bundleTypescriptLoader(path) !== null;
|
|
2969
|
-
if (!typescript && !looksLikeEsm(src))
|
|
3053
|
+
if (!typescript && !looksLikeEsm(path, src))
|
|
2970
3054
|
continue;
|
|
2971
|
-
|
|
2972
|
-
bundle[typescript ? compiledCellKey(path) : path] =
|
|
2973
|
-
'// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
2974
|
-
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
3055
|
+
bundle[typescript ? compiledCellKey(path) : path] = esbuildDiagnosticShim(path, reason);
|
|
2975
3056
|
}
|
|
2976
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
|
+
}
|
|
2977
3068
|
/**
|
|
2978
3069
|
* The set of bundle entries the facet's startup pre-compile loop turns into
|
|
2979
3070
|
* functions, minus the ones already in the right format. Everything the loop
|
|
@@ -2984,7 +3075,8 @@ function _markBundleEsmAsFailed(bundle, reason) {
|
|
|
2984
3075
|
* Extensionless entries are in the set for the same reason the pre-compile
|
|
2985
3076
|
* loop takes them — that is the shape of nearly every npm `bin` script.
|
|
2986
3077
|
* `.json` is data and `.cjs` is CommonJS by definition; neither needs the
|
|
2987
|
-
* 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.
|
|
2988
3080
|
*/
|
|
2989
3081
|
export function isBundleModuleCandidate(path) {
|
|
2990
3082
|
const ext = vfsPathExtension(path);
|
|
@@ -2992,7 +3084,8 @@ export function isBundleModuleCandidate(path) {
|
|
|
2992
3084
|
}
|
|
2993
3085
|
/**
|
|
2994
3086
|
* The esbuild loader for a TypeScript source in the bundle, or null when the
|
|
2995
|
-
* 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.
|
|
2996
3089
|
*
|
|
2997
3090
|
* A resolved `.ts` file reaches the facet as TypeScript, and TypeScript is not
|
|
2998
3091
|
* JavaScript: `new Function` on a type annotation is a SyntaxError whether or
|
|
@@ -3011,12 +3104,7 @@ export function isBundleModuleCandidate(path) {
|
|
|
3011
3104
|
* gone. So a declaration file is left exactly as it was staged.
|
|
3012
3105
|
*/
|
|
3013
3106
|
export function bundleTypescriptLoader(path) {
|
|
3014
|
-
|
|
3015
|
-
return null;
|
|
3016
|
-
const ext = vfsPathExtension(path);
|
|
3017
|
-
if (ext === '.tsx')
|
|
3018
|
-
return 'tsx';
|
|
3019
|
-
return ext === '.ts' || ext === '.mts' || ext === '.cts' ? 'ts' : null;
|
|
3107
|
+
return isTypescriptDeclarationFile(path) ? null : typescriptLoader(path);
|
|
3020
3108
|
}
|
|
3021
3109
|
/** `name.d.ts` / `name.d.mts` / `name.d.cts`, by TypeScript's own rule. */
|
|
3022
3110
|
export function isTypescriptDeclarationFile(path) {
|
|
@@ -3052,35 +3140,6 @@ export function compiledCellKey(path) {
|
|
|
3052
3140
|
export function compiledCellPath(key) {
|
|
3053
3141
|
return key.startsWith(COMPILED_CELL_KEY_PREFIX) ? key.slice(COMPILED_CELL_KEY_PREFIX.length) : null;
|
|
3054
3142
|
}
|
|
3055
|
-
/**
|
|
3056
|
-
* The compile helper both generated facets define: one CommonJS cell to one
|
|
3057
|
-
* function, at module evaluation, the only moment workerd lets a string
|
|
3058
|
-
* become code. One definition so the two facets cannot drift on what a cell
|
|
3059
|
-
* may contain — the long-running facet once lacked the shebang strip, and a
|
|
3060
|
-
* required module that carried one compiled in a short command and failed
|
|
3061
|
-
* in the same tool's attached process.
|
|
3062
|
-
*/
|
|
3063
|
-
export const FACET_COMPILE_HELPER = `
|
|
3064
|
-
function __mkCompiledFn(code) {
|
|
3065
|
-
// Node strips a leading shebang from every module before evaluation, not
|
|
3066
|
-
// only the entry: a required module bundled with its "#!/usr/bin/env node"
|
|
3067
|
-
// line is a SyntaxError under new Function.
|
|
3068
|
-
if (typeof code === "string" && code.charCodeAt(0) === 35 && code.charCodeAt(1) === 33) {
|
|
3069
|
-
const __nl = code.indexOf("\\n");
|
|
3070
|
-
code = __nl >= 0 ? code.slice(__nl + 1) : "";
|
|
3071
|
-
}
|
|
3072
|
-
function renameIfDeclared(name) {
|
|
3073
|
-
const re = new RegExp("(?:^|\\\\n|;)\\\\s*(?:const|let|var|function|class)\\\\s+" + name + "(?![$\\\\w])", "m");
|
|
3074
|
-
return re.test(code) ? name + "__nimbus_unused" : name;
|
|
3075
|
-
}
|
|
3076
|
-
const baseParams = [
|
|
3077
|
-
"exports", renameIfDeclared("require"), "module",
|
|
3078
|
-
renameIfDeclared("__filename"),
|
|
3079
|
-
renameIfDeclared("__dirname"),
|
|
3080
|
-
];
|
|
3081
|
-
return new Function(...baseParams, code);
|
|
3082
|
-
}
|
|
3083
|
-
`;
|
|
3084
3143
|
/**
|
|
3085
3144
|
* The pre-compile loop both generated facets run at module evaluation, the
|
|
3086
3145
|
* only moment workerd lets a string become code. One definition so the two
|
|
@@ -3114,19 +3173,22 @@ for (const [__p, __c] of Object.entries(__MODULE_VFS_BUNDLE)) {
|
|
|
3114
3173
|
`;
|
|
3115
3174
|
/**
|
|
3116
3175
|
* Transform every ESM-shaped file in the bundle to CJS via esbuild.
|
|
3117
|
-
* Mutates `bundle` in place.
|
|
3118
|
-
*
|
|
3119
|
-
* 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`).
|
|
3120
3178
|
*
|
|
3121
|
-
*
|
|
3122
|
-
*
|
|
3179
|
+
* ESM/TypeScript candidates are `isBundleModuleCandidate`. CommonJS cells,
|
|
3180
|
+
* including .cjs, also visit the rewrite-only path for dynamic import().
|
|
3123
3181
|
*
|
|
3124
3182
|
* A JavaScript cell is rewritten in place. A TypeScript source keeps its
|
|
3125
3183
|
* bytes and gets a compiled cell beside it — see `compiledCellKey`.
|
|
3126
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
|
+
*
|
|
3127
3189
|
* Returns the count of files transformed (for diagnostics).
|
|
3128
3190
|
*/
|
|
3129
|
-
async function transformEsmInBundle(bundle, esbuild, pacer
|
|
3191
|
+
async function transformEsmInBundle(bundle, esbuild, pacer) {
|
|
3130
3192
|
let transformed = 0;
|
|
3131
3193
|
let failed = 0;
|
|
3132
3194
|
// Snapshot the keys first — esbuild calls await; never iterate-and-mutate.
|
|
@@ -3134,104 +3196,140 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3134
3196
|
for (const path of Object.keys(bundle)) {
|
|
3135
3197
|
if (compiledCellPath(path) !== null)
|
|
3136
3198
|
continue;
|
|
3137
|
-
if (!isBundleModuleCandidate(path))
|
|
3199
|
+
if (!isBundleModuleCandidate(path) && !path.endsWith('.cjs'))
|
|
3138
3200
|
continue;
|
|
3139
3201
|
const src = bundle[path];
|
|
3140
3202
|
// hardening-r5: binary cells are not ESM. Skip — looksLikeEsm +
|
|
3141
3203
|
// esbuild.transform expect strings.
|
|
3142
3204
|
if (typeof src !== 'string')
|
|
3143
3205
|
continue;
|
|
3144
|
-
if (
|
|
3145
|
-
|
|
3146
|
-
if (!esm)
|
|
3206
|
+
if (path.endsWith('.cjs')) {
|
|
3207
|
+
if (!mayHaveDynamicImport(src))
|
|
3147
3208
|
continue;
|
|
3148
3209
|
}
|
|
3210
|
+
else if (bundleTypescriptLoader(path) === null && !looksLikeEsm(path, src) && !mayHaveDynamicImport(src))
|
|
3211
|
+
continue;
|
|
3149
3212
|
candidates.push(path);
|
|
3150
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 = [];
|
|
3151
3246
|
for (const path of candidates) {
|
|
3152
3247
|
const original = bundle[path];
|
|
3153
3248
|
if (typeof original !== 'string')
|
|
3154
3249
|
continue;
|
|
3155
|
-
const
|
|
3250
|
+
const loader = bundleTypescriptLoader(path);
|
|
3156
3251
|
// A TypeScript source keeps its bytes; its emit lands beside it.
|
|
3157
|
-
const target =
|
|
3158
|
-
//
|
|
3159
|
-
//
|
|
3160
|
-
|
|
3161
|
-
|
|
3162
|
-
// `import.meta.url` substitution mirrors the sibling fix at
|
|
3163
|
-
// src/runtime/runtime-registry.ts:383-389 (framework-gaps-fix P5).
|
|
3164
|
-
// Without `define`, esbuild's CJS transform reduces `import.meta.url`
|
|
3165
|
-
// to undefined (single-pass) or preserves it literally — only to
|
|
3166
|
-
// SyntaxError at `new Function(...)` parse time (two-pass via
|
|
3167
|
-
// EsbuildService.transform's async-IIFE wrap → "Cannot use
|
|
3168
|
-
// 'import.meta' outside a module"). The substitution value is the
|
|
3169
|
-
// real `file:///<absolute-path>` URL — exactly what real Node returns
|
|
3170
|
-
// for an ESM module at that path.
|
|
3171
|
-
//
|
|
3172
|
-
// Note: cache key now incorporates the path because the transformed
|
|
3173
|
-
// output is path-specific (the URL literal is baked in). Two files
|
|
3174
|
-
// with identical source but different paths would otherwise share a
|
|
3175
|
-
// 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.
|
|
3176
3257
|
const absUrl = 'file:///' + path.replace(/^\/+/, '');
|
|
3177
|
-
|
|
3258
|
+
// Keyed on the staged bytes, so a cell the pre-pass fails has a key too.
|
|
3259
|
+
const key = __cacheKey(original + '\0' + absUrl);
|
|
3178
3260
|
const cached = __esmTransformCacheGet(key);
|
|
3179
3261
|
if (cached !== undefined) {
|
|
3180
3262
|
bundle[target] = cached;
|
|
3181
3263
|
transformed++;
|
|
3182
3264
|
continue;
|
|
3183
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;
|
|
3184
3282
|
try {
|
|
3185
|
-
|
|
3186
|
-
loader: bundleTypescriptLoader(path) ?? 'js',
|
|
3187
|
-
format: 'cjs',
|
|
3188
|
-
target: 'esnext',
|
|
3189
|
-
define: importMetaDefines(absUrl),
|
|
3190
|
-
};
|
|
3191
|
-
const bounded = transformOptions.loader === 'js' && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
3192
|
-
? rewriteBundledEsmToCjs(src, absUrl)
|
|
3193
|
-
: null;
|
|
3194
|
-
const t = bounded ?? (isolatedTransform && src.length >= ISOLATED_ESM_TRANSFORM_MIN_BYTES
|
|
3195
|
-
? await isolatedTransform(src, transformOptions)
|
|
3196
|
-
: await esbuild.transform(src, transformOptions));
|
|
3197
|
-
const code = bindImportMetaResolve(t.code, absUrl);
|
|
3198
|
-
bundle[target] = code;
|
|
3199
|
-
__esmTransformCacheSet(key, code);
|
|
3200
|
-
transformed++;
|
|
3283
|
+
src = loader === null ? rewriteProvidedCommonJsModules(original) : original;
|
|
3201
3284
|
}
|
|
3202
3285
|
catch (e) {
|
|
3203
|
-
//
|
|
3204
|
-
|
|
3205
|
-
|
|
3206
|
-
// Pre-fix this catch swallowed the failure silently. The pre-
|
|
3207
|
-
// compile loop then re-saw the ESM source and emitted the
|
|
3208
|
-
// SyntaxError "Cannot use import statement outside a module" into
|
|
3209
|
-
// __compileFailures. __loadModule surfaced that as the user error
|
|
3210
|
-
// — but it told the user nothing about WHY esbuild rejected the
|
|
3211
|
-
// file (was it TLA at module scope? a TypeScript file mistyped
|
|
3212
|
-
// as .js? an unsupported syntax? our esbuild service not init?).
|
|
3213
|
-
//
|
|
3214
|
-
// Post-fix the replacement source is parseable CJS that, when
|
|
3215
|
-
// require()'d, throws a real Error carrying the esbuild reason.
|
|
3216
|
-
// The pre-compile loop succeeds, the file lands in
|
|
3217
|
-
// __compiledModules, and the failure surfaces at require time
|
|
3218
|
-
// with FULL diagnostic context. Net effect for users: the same
|
|
3219
|
-
// upstream tool still fails, but they now see WHY.
|
|
3220
|
-
// Probe: tests/behavioral/npm-create/new/create-astro-diagnostic.mjs
|
|
3221
|
-
// asserts the user-visible error contains "esbuild transform" so
|
|
3222
|
-
// future diagnostic regressions are caught.
|
|
3223
|
-
const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
|
|
3224
|
-
const escapedReason = JSON.stringify(`esbuild transform failed for ${path}: ${reason}`);
|
|
3225
|
-
// Build a valid CJS module that throws on load. Use IIFE to keep
|
|
3226
|
-
// module-scope flat (no TLA, no top-level await). Esbuild-cache
|
|
3227
|
-
// this so repeated submits don't re-run the same transform.
|
|
3228
|
-
const diagnosticSrc = '// framework-fixes-F4 diagnostic shim — esbuild rejected the ESM transform\n' +
|
|
3229
|
-
'(function () { throw new Error(' + escapedReason + '); })();\n';
|
|
3230
|
-
bundle[target] = diagnosticSrc;
|
|
3231
|
-
__esmTransformCacheSet(key, diagnosticSrc);
|
|
3232
|
-
failed++;
|
|
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;
|
|
3233
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
|
+
}
|
|
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);
|
|
3234
3330
|
}
|
|
3331
|
+
if (batch.length > 0)
|
|
3332
|
+
await transformCells(batch);
|
|
3235
3333
|
return { transformed, failed };
|
|
3236
3334
|
}
|
|
3237
3335
|
/**
|
|
@@ -3250,7 +3348,7 @@ async function transformEsmInBundle(bundle, esbuild, pacer, isolatedTransform) {
|
|
|
3250
3348
|
* behaviour for code paths that don't have esbuild handy).
|
|
3251
3349
|
*
|
|
3252
3350
|
*/
|
|
3253
|
-
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) {
|
|
3254
3352
|
// This build accumulates raw VFS contents in the supervisor heap, and did it
|
|
3255
3353
|
// with nothing watching: the estimator read 9.4 MiB while these bytes were
|
|
3256
3354
|
// resetting the DO three times. Take the budget the enrichment passes are
|
|
@@ -3260,20 +3358,20 @@ export async function buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbui
|
|
|
3260
3358
|
const lease = await acquireSupervisorAllocation(VFS_BUNDLE_MAX_BYTES);
|
|
3261
3359
|
prefetchBundleStart(VFS_BUNDLE_MAX_BYTES);
|
|
3262
3360
|
try {
|
|
3263
|
-
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);
|
|
3264
3362
|
}
|
|
3265
3363
|
finally {
|
|
3266
3364
|
prefetchBundleEnd(VFS_BUNDLE_MAX_BYTES);
|
|
3267
3365
|
lease.release();
|
|
3268
3366
|
}
|
|
3269
3367
|
}
|
|
3270
|
-
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) {
|
|
3271
3369
|
// Read the cursor BEFORE the walk: a mutation that lands while the bundle
|
|
3272
3370
|
// is being assembled must be reported as invalidated, not silently missed.
|
|
3273
|
-
const admitted = await vfs.
|
|
3371
|
+
const admitted = await vfs.acquire(null, 0);
|
|
3274
3372
|
const cursor = { epoch: admitted.epoch, rev: admitted.rev };
|
|
3275
3373
|
// 1. Static reachable-set walk from entry.
|
|
3276
|
-
const prefetch = (await prefetchForRequire(vfs, entryCode || '', cwd, scriptPath, maxBundleBytes));
|
|
3374
|
+
const prefetch = (await prefetchForRequire(requireFsOverBridge(vfs), entryCode || '', cwd, scriptPath, maxBundleBytes, pacer?.spend.bind(pacer)));
|
|
3277
3375
|
if ('kind' in prefetch) {
|
|
3278
3376
|
// A required closure larger than the bound can never launch as a
|
|
3279
3377
|
// snapshot. Surface it as the process's own failure rather than a
|
|
@@ -3281,7 +3379,16 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3281
3379
|
throw new ClosureBoundExceededError(prefetch);
|
|
3282
3380
|
}
|
|
3283
3381
|
const bundle = { ...prefetch.bundle };
|
|
3284
|
-
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]);
|
|
3285
3392
|
let truncated = false;
|
|
3286
3393
|
const budgetState = { totalBytes: 0, fileCount: 0 };
|
|
3287
3394
|
// Each enrichment pass below re-scans the bundle accumulated so far, so a
|
|
@@ -3303,14 +3410,23 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3303
3410
|
// admitted first, ahead of every guess, and they join the required set
|
|
3304
3411
|
// rather than the evictable one, because a file evicted here misses
|
|
3305
3412
|
// again on the next run and the loop never closes.
|
|
3306
|
-
|
|
3307
|
-
|
|
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);
|
|
3308
3423
|
await paceAfterPass();
|
|
3309
3424
|
// 2. Greedy oversample — every installed pkg's pkg.json + main.
|
|
3310
3425
|
// Catches dynamic-require / `bindings()` / plugin-loader cases the
|
|
3311
3426
|
// regex prefetch misses. Its budget is independent from the complete
|
|
3312
3427
|
// static require closure, which is correctness-critical.
|
|
3313
|
-
const
|
|
3428
|
+
const independentBeforeGroups = new Set(Object.keys(bundle));
|
|
3429
|
+
const greedy = (await greedyAddMainEntries(vfs, cwd, bundle, budgetState, closurePaths, { maxBundleBytes, pacer }));
|
|
3314
3430
|
await paceAfterPass();
|
|
3315
3431
|
// 2.25 X.5-Z3: static-readFileSync asset prefetch. Scans every
|
|
3316
3432
|
// bundle .js/.mjs/.cjs source for the canonical jsdom shape:
|
|
@@ -3385,31 +3501,17 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3385
3501
|
// ESM files (e.g. tldts/dist/es6/index.js, @remix-run/react/dist/esm,
|
|
3386
3502
|
// @tailwindcss/vite, react-remove-scroll, astro) silently fail
|
|
3387
3503
|
// `new Function` at facet startup and surface as the misleading
|
|
3388
|
-
// "
|
|
3504
|
+
// "not in this launch's module map" at request time.
|
|
3389
3505
|
if (esbuild) {
|
|
3390
|
-
|
|
3391
|
-
|
|
3392
|
-
|
|
3393
|
-
|
|
3394
|
-
// through the transform because the eviction loop below recomputes
|
|
3395
|
-
// the encoded size from scratch anyway.
|
|
3396
|
-
}
|
|
3397
|
-
catch (e) {
|
|
3398
|
-
// framework-fixes-F4 (2026-05-12): the prior catch was silent.
|
|
3399
|
-
// Replace every detected-ESM source with the SAME diagnostic
|
|
3400
|
-
// shim transformEsmInBundle's per-file catch installs, so the
|
|
3401
|
-
// user sees "esbuild transform failed (service-level): <reason>"
|
|
3402
|
-
// instead of a bare "Cannot use import statement..." parse
|
|
3403
|
-
// error with no context.
|
|
3404
|
-
const reason = (e && e.message) ? String(e.message).replace(/\n/g, ' ') : String(e);
|
|
3405
|
-
_markBundleEsmAsFailed(bundle, `esbuild service unavailable: ${reason}`);
|
|
3406
|
-
}
|
|
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);
|
|
3407
3510
|
}
|
|
3408
3511
|
else {
|
|
3409
|
-
//
|
|
3410
|
-
//
|
|
3411
|
-
|
|
3412
|
-
_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');
|
|
3413
3515
|
}
|
|
3414
3516
|
for (const path of Object.keys(bundle)) {
|
|
3415
3517
|
if (compiledCellPath(path) === null && (!isBundleModuleCandidate(path) || bundleTypescriptLoader(path) !== null))
|
|
@@ -3418,61 +3520,165 @@ async function _buildPrefetchBundle(vfs, scriptPath, cwd, entryCode, esbuild, bu
|
|
|
3418
3520
|
if (typeof source !== 'string')
|
|
3419
3521
|
continue;
|
|
3420
3522
|
await pacer?.spend(source.length);
|
|
3421
|
-
|
|
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
|
+
}
|
|
3422
3529
|
}
|
|
3423
3530
|
await paceAfterPass();
|
|
3424
|
-
//
|
|
3425
|
-
//
|
|
3426
|
-
//
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
//
|
|
3431
|
-
// units) because that is what workerd charges against the per-module
|
|
3432
|
-
// text-size budget. Only OPTIONAL enrichment is evictable, largest
|
|
3433
|
-
// 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.
|
|
3434
3538
|
//
|
|
3435
3539
|
// Evicting an enrichment file is a real loss — the sync fs reads it
|
|
3436
3540
|
// exists for cannot fall back to the supervisor — so the paths that
|
|
3437
3541
|
// went are named rather than silently dropped.
|
|
3438
|
-
const size = encodedBundleSize(bundle
|
|
3439
|
-
|
|
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
|
+
}
|
|
3440
3569
|
// A compiled cell goes with its source: required when the source is.
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
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)
|
|
3447
3643
|
break;
|
|
3448
|
-
|
|
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]);
|
|
3449
3664
|
delete bundle[k];
|
|
3450
3665
|
size.remove(k);
|
|
3451
3666
|
}
|
|
3452
3667
|
if (evicted.length > 0) {
|
|
3453
3668
|
truncated = true;
|
|
3454
|
-
console.warn(`[facet-manager] prefetch snapshot exceeded ${
|
|
3455
|
-
+ `
|
|
3669
|
+
console.warn(`[facet-manager] prefetch snapshot exceeded ${maxBundleBytes} bytes; `
|
|
3670
|
+
+ `evicted ${evicted.length} optional file(s). They still exist and `
|
|
3456
3671
|
+ `async reads still return them; synchronous reads raise EAGAIN: `
|
|
3457
3672
|
+ `${describeBundleCells(evicted)}`);
|
|
3458
3673
|
}
|
|
3459
3674
|
}
|
|
3460
3675
|
const fileCount = Object.keys(bundle).length;
|
|
3461
|
-
const metadata = (await buildVfsMetadata(vfs, manifest, bundle));
|
|
3462
|
-
(await addUnreadableDenialCells(vfs, bundle, metadata));
|
|
3463
|
-
await paceAfterPass();
|
|
3464
|
-
// Denial cells land after the eviction pass, so account for them before
|
|
3465
|
-
// deciding where the bundle has to live.
|
|
3466
|
-
for (const [path, cell] of Object.entries(bundle))
|
|
3467
|
-
size.add(path, cell);
|
|
3468
3676
|
const bundleSideModulesRequired = size.bytes > BUNDLE_MAX_ENCODED_BYTES;
|
|
3469
3677
|
// Suppress lint: `greedy.added` is observed only via diagnostics.
|
|
3470
3678
|
void greedy;
|
|
3471
3679
|
const wasmImages = (await collectClosureWasmImages(vfs, bundle, binSiblingAdd.wasmPaths));
|
|
3472
3680
|
return {
|
|
3473
3681
|
bundle,
|
|
3474
|
-
manifest,
|
|
3475
|
-
metadata,
|
|
3476
3682
|
cursor,
|
|
3477
3683
|
reachableCount: fileCount,
|
|
3478
3684
|
truncated,
|
|
@@ -3554,24 +3760,27 @@ export class FacetManager {
|
|
|
3554
3760
|
// spawn created as an OS-child of the attach TUI. When the attach process
|
|
3555
3761
|
// exits (reported / killed), its serve facet is torn down with it.
|
|
3556
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();
|
|
3557
3768
|
/**
|
|
3558
|
-
*
|
|
3559
|
-
*
|
|
3560
|
-
*
|
|
3561
|
-
* `setEsbuildService` lets NimbusSession share its existing instance
|
|
3562
|
-
* 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.
|
|
3563
3772
|
*/
|
|
3564
3773
|
esbuild = null;
|
|
3565
3774
|
/**
|
|
3566
3775
|
* Prefetch-bundle cache. buildPrefetchBundle does a full VFS reachable-set
|
|
3567
3776
|
* walk + greedy oversample + esbuild ESM→CJS pass on EVERY foreground
|
|
3568
3777
|
* exec — dominant wall-clock on large node_modules. This memoizes the
|
|
3569
|
-
* result (including the serialized facet bundle
|
|
3778
|
+
* result (including the serialized facet bundle) keyed on
|
|
3570
3779
|
* (bundleProfile, cwd, scriptPath, entryCode identity).
|
|
3571
3780
|
*
|
|
3572
3781
|
* Correctness watermark: the GLOBAL SqliteVFS revision. buildPrefetchBundle
|
|
3573
3782
|
* reads from paths that can lie anywhere in the VFS (addEntryAbsPathReads
|
|
3574
|
-
* pulls absolute-path literals like /tmp/x
|
|
3783
|
+
* pulls absolute-path literals like /tmp/x),
|
|
3575
3784
|
* so a cwd-scoped subtree revision cannot guarantee invalidation. The
|
|
3576
3785
|
* global revision bumps on ANY write, so the cache invalidates on every
|
|
3577
3786
|
* mutation that could change any file the bundle reads — provably
|
|
@@ -3592,6 +3801,19 @@ export class FacetManager {
|
|
|
3592
3801
|
* already deliver for the case that matters: running the command again.
|
|
3593
3802
|
*/
|
|
3594
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;
|
|
3595
3817
|
/**
|
|
3596
3818
|
* What the prefetch cache holds right now, for /api/_diag/memory: each
|
|
3597
3819
|
* entry's key, the revision it was built at, and its retained bytes, next
|
|
@@ -3645,6 +3867,12 @@ export class FacetManager {
|
|
|
3645
3867
|
? Reflect.get(env, 'NIMBUS_DEBUG')
|
|
3646
3868
|
: undefined;
|
|
3647
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;
|
|
3648
3876
|
this.launchJournal = new FencedWork(ctx.storage, {
|
|
3649
3877
|
generationBase: () => this.processes.pidBase,
|
|
3650
3878
|
waitUntil: (promise) => this.ctx.waitUntil(promise),
|
|
@@ -3669,8 +3897,31 @@ export class FacetManager {
|
|
|
3669
3897
|
// Rooted on waitUntil: the hook fires synchronously inside whatever turn
|
|
3670
3898
|
// ended the process, and the delete must not be a floating promise there.
|
|
3671
3899
|
this.processes.setOnTerminal((pid) => {
|
|
3900
|
+
this.residentBundleKeys.delete(pid);
|
|
3901
|
+
this.residentProfileOffers.delete(pid);
|
|
3672
3902
|
this.ctx.waitUntil(this.trackLaunchTask(this._onResidentTerminal(pid)));
|
|
3673
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);
|
|
3674
3925
|
}
|
|
3675
3926
|
/**
|
|
3676
3927
|
* The process is over. Every end-of-life passes through here: a clean
|
|
@@ -3809,12 +4060,42 @@ export class FacetManager {
|
|
|
3809
4060
|
}
|
|
3810
4061
|
return vfs;
|
|
3811
4062
|
}
|
|
4063
|
+
/** Give the bundle's ESM→CJS pass the host's esbuild, as composeFacetManager does. */
|
|
4064
|
+
setEsbuildService(esbuild) { this.esbuild = esbuild; }
|
|
3812
4065
|
/**
|
|
3813
|
-
*
|
|
3814
|
-
* the ESM
|
|
3815
|
-
*
|
|
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.
|
|
3816
4071
|
*/
|
|
3817
|
-
|
|
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
|
+
}
|
|
3818
4099
|
/**
|
|
3819
4100
|
* The pacer every launch is built under: the session's alarm-driven turn
|
|
3820
4101
|
* pump, the deployment's chunk bound, and the one check a suspended launch
|
|
@@ -3872,36 +4153,211 @@ export class FacetManager {
|
|
|
3872
4153
|
return modules;
|
|
3873
4154
|
if (!this.filesystem)
|
|
3874
4155
|
throw new Error('Process filesystem authority is not initialized');
|
|
3875
|
-
const vfs =
|
|
4156
|
+
const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
|
|
3876
4157
|
for (const image of wasmImports) {
|
|
3877
|
-
|
|
3878
|
-
|
|
3879
|
-
bytes = (await vfs.readFile(stripLeadingSlashes(image.vfsPath)));
|
|
3880
|
-
}
|
|
3881
|
-
catch {
|
|
4158
|
+
const bytes = await filesOf(vfs).readBytes(stripLeadingSlashes(image.vfsPath)).catch(() => null);
|
|
4159
|
+
if (bytes === null)
|
|
3882
4160
|
continue;
|
|
3883
|
-
}
|
|
3884
4161
|
modules[image.moduleName] = {
|
|
3885
4162
|
wasm: bytes.buffer.slice(bytes.byteOffset, bytes.byteOffset + bytes.byteLength),
|
|
3886
4163
|
};
|
|
3887
4164
|
}
|
|
3888
4165
|
return modules;
|
|
3889
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
|
+
}
|
|
3890
4353
|
async _buildProcessBundle(entry, spec, pacer) {
|
|
3891
4354
|
if (!this.vfs) {
|
|
3892
|
-
return { bundle: {},
|
|
3893
|
-
}
|
|
3894
|
-
// W3.5 Fix B: thread an EsbuildService into buildPrefetchBundle so ESM
|
|
3895
|
-
// source files (e.g. tldts/dist/es6/index.js, @remix-run/react,
|
|
3896
|
-
// @tailwindcss/vite, react-remove-scroll, astro) get transformed to CJS
|
|
3897
|
-
// before they hit the facet's `new Function` pre-compile loop. Lazy-create
|
|
3898
|
-
// one if NimbusSession didn't share its own.
|
|
3899
|
-
if (!this.esbuild)
|
|
3900
|
-
this.esbuild = new EsbuildService(this.vfs.as(CRED_KERNEL));
|
|
4355
|
+
return { bundle: {}, reachableCount: 0, truncated: false };
|
|
4356
|
+
}
|
|
3901
4357
|
this.imageStore.ensureDir();
|
|
3902
4358
|
if (!this.filesystem)
|
|
3903
4359
|
throw new Error('Process filesystem authority is not initialized');
|
|
3904
|
-
const vfs =
|
|
4360
|
+
const vfs = this.filesystem.bind({ pid: entry.pid, cred: entry.cred });
|
|
3905
4361
|
const { cred } = entry;
|
|
3906
4362
|
const profile = spec.bundleProfile ?? DEFAULT_FACET_BUNDLE_PROFILE;
|
|
3907
4363
|
const credKey = `${cred.uid}:${cred.gid}:${cred.groups.join(',')}`;
|
|
@@ -3930,16 +4386,41 @@ export class FacetManager {
|
|
|
3930
4386
|
this.prefetchBundleCache.set(key, cached);
|
|
3931
4387
|
return { ...cached.vfsState, cacheHit: true, cacheRetained: true };
|
|
3932
4388
|
}
|
|
3933
|
-
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
|
+
}
|
|
3934
4414
|
vfsState.bundleKey = key;
|
|
3935
|
-
vfsState.
|
|
3936
|
-
|
|
3937
|
-
|
|
3938
|
-
// Two more serializations of the same weight as the passes before them.
|
|
3939
|
-
await pacer.spend(vfsState.serializedManifest.length + vfsState.serializedMetadata.length);
|
|
3940
|
-
// The only consumer of the raw cells past this point is a single boolean,
|
|
3941
|
-
// 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.
|
|
3942
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 });
|
|
3943
4424
|
vfsState.cacheHit = false;
|
|
3944
4425
|
// Serialization is total: bundleSource/serializedManifest/serializedMetadata
|
|
3945
4426
|
// carry every byte the raw cells and objects do, and both generators read
|
|
@@ -4075,7 +4556,43 @@ export class FacetManager {
|
|
|
4075
4556
|
hasResidentProcess(pid) {
|
|
4076
4557
|
return this.processRpcResources.has(pid);
|
|
4077
4558
|
}
|
|
4078
|
-
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
|
+
}
|
|
4079
4596
|
this.portRegistry.unregisterByPid(pid);
|
|
4080
4597
|
this.processes.exit(pid, exitCode);
|
|
4081
4598
|
const tracked = this.processRpcResources.get(pid);
|
|
@@ -4181,7 +4698,7 @@ export class FacetManager {
|
|
|
4181
4698
|
}
|
|
4182
4699
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
4183
4700
|
const diagSink = diagOn
|
|
4184
|
-
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0
|
|
4701
|
+
? { loadMs: 0, runMs: 0, moduleMapBytes: 0, bundleBytes: 0 }
|
|
4185
4702
|
: undefined;
|
|
4186
4703
|
const abortController = new AbortController();
|
|
4187
4704
|
// Ctrl-C / kill on a user program has to end the in-flight run, not just
|
|
@@ -4192,7 +4709,11 @@ export class FacetManager {
|
|
|
4192
4709
|
const onShellAbort = () => abortController.abort();
|
|
4193
4710
|
opts.signal?.addEventListener('abort', onShellAbort, { once: true });
|
|
4194
4711
|
try {
|
|
4195
|
-
|
|
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);
|
|
4196
4717
|
this._flushVfsWrites(result, entry.pid);
|
|
4197
4718
|
this._recordResidencyMisses(vfsState.bundleKey, result.residencyMisses);
|
|
4198
4719
|
this.processes.exit(entry.pid, result.exitCode);
|
|
@@ -4208,8 +4729,7 @@ export class FacetManager {
|
|
|
4208
4729
|
drainPasses: result.diag?.drainPasses ?? 0,
|
|
4209
4730
|
moduleMapBytes: diagSink.moduleMapBytes,
|
|
4210
4731
|
bundleBytes: diagSink.bundleBytes,
|
|
4211
|
-
|
|
4212
|
-
metadataBytes: diagSink.metadataBytes,
|
|
4732
|
+
namespaceRefusals: result.diag?.namespaceRefusals ?? 0,
|
|
4213
4733
|
rpcWrites: result.diag?.rpcWrites ?? 0,
|
|
4214
4734
|
fsRpcReads: result.diag?.fsRpcReads ?? 0,
|
|
4215
4735
|
cacheHit: vfsState.cacheHit ?? false,
|
|
@@ -4280,13 +4800,13 @@ export class FacetManager {
|
|
|
4280
4800
|
}
|
|
4281
4801
|
}
|
|
4282
4802
|
// ── One-shot dynamic Worker entrypoint ────────────────────────────────
|
|
4283
|
-
async _execViaLoader(code, opts, entry, vfsState, signal, diagSink) {
|
|
4803
|
+
async _execViaLoader(code, opts, entry, vfsState, dataPlan, signal, diagSink) {
|
|
4284
4804
|
// Answered by _buildProcessBundle while the raw cells were still in
|
|
4285
4805
|
// hand; re-deriving it here is what forced them to be retained.
|
|
4286
4806
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
4287
|
-
const [sqliteModules,
|
|
4807
|
+
const [sqliteModules, sources] = await Promise.all([
|
|
4288
4808
|
this.sqliteModuleEntry(usesSqlite),
|
|
4289
|
-
|
|
4809
|
+
fetchNodeFacetSources(this.env),
|
|
4290
4810
|
]);
|
|
4291
4811
|
const writerId = crypto.randomUUID();
|
|
4292
4812
|
let writerActivated = false;
|
|
@@ -4302,6 +4822,7 @@ export class FacetManager {
|
|
|
4302
4822
|
captureOutput: !!opts.captureOutput,
|
|
4303
4823
|
cred: { ...entry.cred, groups: [...entry.cred.groups] },
|
|
4304
4824
|
vfsCursor: vfsState.cursor,
|
|
4825
|
+
dataPlan,
|
|
4305
4826
|
...(diagSink ? { diag: true } : {}),
|
|
4306
4827
|
});
|
|
4307
4828
|
try {
|
|
@@ -4322,7 +4843,8 @@ export class FacetManager {
|
|
|
4322
4843
|
// the map, and compiled by the loader like the sqlite sidecar.
|
|
4323
4844
|
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
4324
4845
|
const wasmModules = (await this._wasmModulesByValue(entry, wasmImports));
|
|
4325
|
-
const
|
|
4846
|
+
const entryCode = await this._entryDynamicImports(code, opts.filename, opts.cwd || '/home/user');
|
|
4847
|
+
const generatedWorker = await generateEntrypointCode(entryCode, vfsState, usesSqlite, sources, wasmImports);
|
|
4326
4848
|
if (diagSink) {
|
|
4327
4849
|
diagSink.moduleMapBytes = _encodedSourceBytes(generatedWorker.code);
|
|
4328
4850
|
for (const source of Object.values(generatedWorker.modules)) {
|
|
@@ -4337,8 +4859,6 @@ export class FacetManager {
|
|
|
4337
4859
|
for (const source of Object.values(vfsState.bundleSource?.modules ?? {})) {
|
|
4338
4860
|
diagSink.bundleBytes += _encodedSourceBytes(source);
|
|
4339
4861
|
}
|
|
4340
|
-
diagSink.manifestBytes = _encodedSourceBytes(vfsState.serializedManifest ?? '');
|
|
4341
|
-
diagSink.metadataBytes = _encodedSourceBytes(vfsState.serializedMetadata ?? '');
|
|
4342
4862
|
}
|
|
4343
4863
|
if (!vfsState.cacheRetained)
|
|
4344
4864
|
releaseGeneratedSources(vfsState);
|
|
@@ -4405,7 +4925,7 @@ export class FacetManager {
|
|
|
4405
4925
|
// the Worker-Loader cache-miss path (with SUPERVISOR bound to THIS call's
|
|
4406
4926
|
// context, which stays open for the whole run), never in this DO.
|
|
4407
4927
|
const writerId = crypto.randomUUID();
|
|
4408
|
-
const supervisor = {
|
|
4928
|
+
const supervisor = { ...supervisorBindingProps(this.ctx, staged.pid), writerId };
|
|
4409
4929
|
const ctxExports = getNimbusCtxExports();
|
|
4410
4930
|
let entrypoint;
|
|
4411
4931
|
let writerActivated = false;
|
|
@@ -4486,10 +5006,10 @@ export class FacetManager {
|
|
|
4486
5006
|
// other writes flush live through the SUPERVISOR RPC bridge.
|
|
4487
5007
|
if (this.vfs)
|
|
4488
5008
|
this.imageStore.ensureDir();
|
|
4489
|
-
const processVfs = this.filesystem ?
|
|
5009
|
+
const processVfs = this.filesystem ? this.filesystem.bind({ pid: entry.pid, cred: entry.cred }) : null;
|
|
4490
5010
|
const vfsState = processVfs
|
|
4491
|
-
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE
|
|
4492
|
-
: { bundle: {},
|
|
5011
|
+
? await buildPrefetchBundle(processVfs, undefined, opts.cwd, '', this.esbuild || undefined, DEFAULT_FACET_BUNDLE_PROFILE)
|
|
5012
|
+
: { bundle: {}, reachableCount: 0, truncated: false };
|
|
4493
5013
|
const vfsBundle = _serializeBundleForFacet(vfsState.bundle);
|
|
4494
5014
|
assertStagedBundleFitsRpcPayload(vfsBundle, vfsState.bundle);
|
|
4495
5015
|
const stageSpec = {
|
|
@@ -4500,8 +5020,6 @@ export class FacetManager {
|
|
|
4500
5020
|
cwd: opts.cwd,
|
|
4501
5021
|
stdin: opts.stdin ?? '',
|
|
4502
5022
|
vfsBundle,
|
|
4503
|
-
vfsManifest: JSON.stringify(vfsState.manifest),
|
|
4504
|
-
vfsMetadata: JSON.stringify(vfsState.metadata),
|
|
4505
5023
|
vfsCursor: serializeFacetVfsCursor(vfsState.cursor),
|
|
4506
5024
|
};
|
|
4507
5025
|
return { pid, command, stageSpec };
|
|
@@ -4669,7 +5187,8 @@ export class FacetManager {
|
|
|
4669
5187
|
// The handle's route target resolves the RUNNING facet wherever it is
|
|
4670
5188
|
// hosted; binding it for the pid before the port is announced is what
|
|
4671
5189
|
// lets the shim's listen()→SUPERVISOR.registerPort back-fill.
|
|
4672
|
-
|
|
5190
|
+
// The opencode runner dispatches through node-shims' __nimbusServeHttp, which strips the ACQUIRE.
|
|
5191
|
+
this.portRegistry.bindFacetStub(pid, handle.routeTarget, { deliversAcquire: true });
|
|
4673
5192
|
this.trackProcessRpcResources(pid, [handle], { releaseOnReportExit: false });
|
|
4674
5193
|
resourcesTracked = true;
|
|
4675
5194
|
this.ctx.waitUntil(handle.done
|
|
@@ -4807,10 +5326,6 @@ export class FacetManager {
|
|
|
4807
5326
|
await Promise.allSettled([...this.launchTasks]);
|
|
4808
5327
|
await Promise.all(this.rowAmendments.values());
|
|
4809
5328
|
}
|
|
4810
|
-
/** Whether any launch is suspended waiting for a turn. */
|
|
4811
|
-
get hasPendingLaunchTurns() {
|
|
4812
|
-
return this.launchPump.hasPending;
|
|
4813
|
-
}
|
|
4814
5329
|
/** Allocate a free loopback port for a resident server facet (from 4096 up). */
|
|
4815
5330
|
_allocateLoopbackPort() {
|
|
4816
5331
|
for (let port = 4096; port < 4096 + 4096; port++) {
|
|
@@ -5197,7 +5712,14 @@ export class FacetManager {
|
|
|
5197
5712
|
async _residentLaunchBody(entry, code, command, cwd, opts, pacer, durableFacetName, launchEnv) {
|
|
5198
5713
|
const diagOn = isExecDiagEnabled();
|
|
5199
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');
|
|
5200
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
|
+
}
|
|
5201
5723
|
const bundleMs = diagOn ? Date.now() - __bundleStart : 0;
|
|
5202
5724
|
// The launch-time overlay (`$PORT`, `$NIMBUS_APP` under a reservation)
|
|
5203
5725
|
// rides on top of the recipe's env and is never journalled: it is
|
|
@@ -5218,13 +5740,13 @@ export class FacetManager {
|
|
|
5218
5740
|
: spawnEnv;
|
|
5219
5741
|
// Answered by _buildProcessBundle while the raw cells were still in hand.
|
|
5220
5742
|
const usesSqlite = vfsState.usesNodeSqlite ?? bundleUsesNodeSqlite(code, vfsState.bundle);
|
|
5221
|
-
const [sqliteModules,
|
|
5743
|
+
const [sqliteModules, sources] = await Promise.all([
|
|
5222
5744
|
this.sqliteModuleEntry(usesSqlite),
|
|
5223
|
-
|
|
5745
|
+
fetchNodeFacetSources(this.env),
|
|
5224
5746
|
]);
|
|
5225
5747
|
// Each image is read by path when the facet loads, never by value here.
|
|
5226
5748
|
const wasmImports = facetWasmImports([], vfsState.wasmImages ?? []);
|
|
5227
|
-
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);
|
|
5228
5750
|
// Sized here, while the map is still in hand. Reading these after the load
|
|
5229
5751
|
// would itself be what keeps the map alive, and the whole point of the
|
|
5230
5752
|
// scoping below is that nothing does.
|
|
@@ -5236,10 +5758,13 @@ export class FacetManager {
|
|
|
5236
5758
|
}
|
|
5237
5759
|
moduleMapBytes = _encodedSourceBytes(generatedWorker.code) + bundleBytes;
|
|
5238
5760
|
}
|
|
5239
|
-
const manifestBytes = diagOn ? _encodedSourceBytes(vfsState.serializedManifest ?? '') : 0;
|
|
5240
|
-
const metadataBytes = diagOn ? _encodedSourceBytes(vfsState.serializedMetadata ?? '') : 0;
|
|
5241
5761
|
const cacheHit = vfsState.cacheHit ?? false;
|
|
5242
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);
|
|
5243
5768
|
// The map is generated; the state's only remaining job is its cursor.
|
|
5244
5769
|
// Released here, before the boot — releasing afterwards keeps the copy
|
|
5245
5770
|
// alive for exactly the window that was resetting the isolate. A state
|
|
@@ -5261,6 +5786,8 @@ export class FacetManager {
|
|
|
5261
5786
|
};
|
|
5262
5787
|
generatedWorker = undefined;
|
|
5263
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');
|
|
5264
5791
|
// Last gate before the facet exists. A launch now spans many turns, so
|
|
5265
5792
|
// a kill can land anywhere inside it; booting a process the table has
|
|
5266
5793
|
// already exited would leave a facet nothing owns, running against a
|
|
@@ -5270,7 +5797,12 @@ export class FacetManager {
|
|
|
5270
5797
|
// The attached-TTY runner holds startProcess open for the process's
|
|
5271
5798
|
// life; the server/watch runner returns once it is up.
|
|
5272
5799
|
startContract: opts.attachedTty ? 'lifetime' : 'boot',
|
|
5273
|
-
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,
|
|
5274
5806
|
// A resident whose declared port is reserved binds the owner's
|
|
5275
5807
|
// durable slot — the same store a durable worker spawn takes — so the
|
|
5276
5808
|
// reservation's durability reaches this process's storage too.
|
|
@@ -5290,6 +5822,11 @@ export class FacetManager {
|
|
|
5290
5822
|
},
|
|
5291
5823
|
},
|
|
5292
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
|
+
}
|
|
5293
5830
|
if (diagOn) {
|
|
5294
5831
|
recordExecTelemetry({
|
|
5295
5832
|
command,
|
|
@@ -5306,8 +5843,7 @@ export class FacetManager {
|
|
|
5306
5843
|
// when it stayed inline it rides inside worker.js and shows up only
|
|
5307
5844
|
// in the total.
|
|
5308
5845
|
bundleBytes,
|
|
5309
|
-
|
|
5310
|
-
metadataBytes,
|
|
5846
|
+
namespaceRefusals: 0,
|
|
5311
5847
|
rpcWrites: 0,
|
|
5312
5848
|
fsRpcReads: 0,
|
|
5313
5849
|
cacheHit,
|
|
@@ -5318,7 +5854,8 @@ export class FacetManager {
|
|
|
5318
5854
|
}
|
|
5319
5855
|
this.trackProcessRpcResources(entry.pid, [handle], { releaseOnReportExit: !opts.attachedTty });
|
|
5320
5856
|
resourcesTracked = true;
|
|
5321
|
-
|
|
5857
|
+
// A node-shims resident: its __nimbusServeHttp takes the delivered ACQUIRE off each request.
|
|
5858
|
+
this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget, { deliversAcquire: true });
|
|
5322
5859
|
if (opts.attachedTty) {
|
|
5323
5860
|
this.ctx.waitUntil(handle.done
|
|
5324
5861
|
.catch((e) => {
|
|
@@ -5437,6 +5974,7 @@ export class FacetManager {
|
|
|
5437
5974
|
let record;
|
|
5438
5975
|
let durableFacetName;
|
|
5439
5976
|
let launchEnv;
|
|
5977
|
+
const foreground = opts.foreground ? this._holdForeground(entry.pid, opts.foreground) : null;
|
|
5440
5978
|
try {
|
|
5441
5979
|
if (opts.durable) {
|
|
5442
5980
|
// A durable spawn on a declared port starts only when a reservation the
|
|
@@ -5554,7 +6092,7 @@ export class FacetManager {
|
|
|
5554
6092
|
this.trackProcessRpcResources(entry.pid, [handle]);
|
|
5555
6093
|
resourcesTracked = true;
|
|
5556
6094
|
this.portRegistry.bindFacetStub(entry.pid, handle.routeTarget);
|
|
5557
|
-
const boot = await handle.booted();
|
|
6095
|
+
const boot = foreground ? await Promise.race([handle.booted(), foreground.interrupted]) : await handle.booted();
|
|
5558
6096
|
if (record && this.launchJournal.has(entry.pid)) {
|
|
5559
6097
|
// Booted and running: the launch proved itself, so the resident
|
|
5560
6098
|
// starts its running life with a fresh re-drive budget.
|
|
@@ -5597,9 +6135,38 @@ export class FacetManager {
|
|
|
5597
6135
|
this.releaseProcessRpcResources(entry.pid);
|
|
5598
6136
|
else
|
|
5599
6137
|
handle?.kill();
|
|
5600
|
-
|
|
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));
|
|
5601
6141
|
throw e;
|
|
5602
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
|
+
};
|
|
5603
6170
|
}
|
|
5604
6171
|
/**
|
|
5605
6172
|
* A resident process announcing it bound `port`.
|
|
@@ -5840,18 +6407,23 @@ export class FacetManager {
|
|
|
5840
6407
|
catch { }
|
|
5841
6408
|
}
|
|
5842
6409
|
}
|
|
5843
|
-
/**
|
|
5844
|
-
|
|
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) {
|
|
5845
6416
|
const entry = this.processes.get(pid);
|
|
5846
6417
|
if (!entry || entry.state !== 'running')
|
|
5847
6418
|
return false;
|
|
6419
|
+
const code = signal === undefined ? 137 : exitCodeForSignal(signal);
|
|
5848
6420
|
this.portRegistry.unregisterByPid(pid);
|
|
5849
6421
|
this.releaseProcessRpcResources(pid);
|
|
5850
6422
|
this.revokeProcessVfsWriters(pid);
|
|
5851
|
-
const result = this.processes.kill(pid);
|
|
6423
|
+
const result = this.processes.kill(pid, code);
|
|
5852
6424
|
if (result) {
|
|
5853
6425
|
try {
|
|
5854
|
-
this.hooks.onExternalExit?.(pid,
|
|
6426
|
+
this.hooks.onExternalExit?.(pid, code, signal === undefined ? 'killed' : `SIG${signal}`);
|
|
5855
6427
|
}
|
|
5856
6428
|
catch { }
|
|
5857
6429
|
}
|
|
@@ -6011,5 +6583,5 @@ export class FacetManager {
|
|
|
6011
6583
|
}
|
|
6012
6584
|
return this.portRegistry.has(port);
|
|
6013
6585
|
}
|
|
6014
|
-
get stats() { return this.processes.stats; }
|
|
6586
|
+
get stats() { return { ...this.processes.stats, readProfileConflicts: this.readProfileConflicts }; }
|
|
6015
6587
|
}
|