@nimbus-sh/worker 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +219 -0
- package/dist/_shared/bytes.d.ts +18 -0
- package/dist/_shared/bytes.d.ts.map +1 -0
- package/dist/_shared/bytes.js +17 -0
- package/dist/_shared/cache-stats.d.ts +146 -0
- package/dist/_shared/cache-stats.d.ts.map +1 -0
- package/dist/_shared/cache-stats.js +162 -0
- package/dist/_shared/exports-resolver.d.ts +76 -0
- package/dist/_shared/exports-resolver.d.ts.map +1 -0
- package/dist/_shared/exports-resolver.js +284 -0
- package/dist/_shared/install-phase.d.ts +47 -0
- package/dist/_shared/install-phase.d.ts.map +1 -0
- package/dist/_shared/install-phase.js +1 -0
- package/dist/_shared/real-node-imports.d.ts +38 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -0
- package/dist/_shared/real-node-imports.js +46 -0
- package/dist/_shared/retry.d.ts +93 -0
- package/dist/_shared/retry.d.ts.map +1 -0
- package/dist/_shared/retry.js +161 -0
- package/dist/_shared/session-id.d.ts +39 -0
- package/dist/_shared/session-id.d.ts.map +1 -0
- package/dist/_shared/session-id.js +127 -0
- package/dist/_shared/session-router.d.ts +104 -0
- package/dist/_shared/session-router.d.ts.map +1 -0
- package/dist/_shared/session-router.js +165 -0
- package/dist/_shared/w7-frame.d.ts +93 -0
- package/dist/_shared/w7-frame.d.ts.map +1 -0
- package/dist/_shared/w7-frame.js +402 -0
- package/dist/auth/index.d.ts +16 -0
- package/dist/auth/index.d.ts.map +1 -0
- package/dist/auth/index.js +15 -0
- package/dist/auth/middleware.d.ts +85 -0
- package/dist/auth/middleware.d.ts.map +1 -0
- package/dist/auth/middleware.js +150 -0
- package/dist/auth/token.d.ts +96 -0
- package/dist/auth/token.d.ts.map +1 -0
- package/dist/auth/token.js +226 -0
- package/dist/auth/types.d.ts +141 -0
- package/dist/auth/types.d.ts.map +1 -0
- package/dist/auth/types.js +114 -0
- package/dist/bindings/d1.d.ts +102 -0
- package/dist/bindings/d1.d.ts.map +1 -0
- package/dist/bindings/d1.js +549 -0
- package/dist/bindings/kv.d.ts +82 -0
- package/dist/bindings/kv.d.ts.map +1 -0
- package/dist/bindings/kv.js +290 -0
- package/dist/bindings/r2.d.ts +137 -0
- package/dist/bindings/r2.d.ts.map +1 -0
- package/dist/bindings/r2.js +379 -0
- package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
- package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
- package/dist/cirrus-npm-cjs.generated.js +41 -0
- package/dist/cirrus-plugin-react.generated.d.ts +34 -0
- package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
- package/dist/cirrus-plugin-react.generated.js +33 -0
- package/dist/constants.d.ts +34 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +116 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
- package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
- package/dist/esbuild-wasm-bundle.generated.js +40 -0
- package/dist/facets/cirrus-real.d.ts +147 -0
- package/dist/facets/cirrus-real.d.ts.map +1 -0
- package/dist/facets/cirrus-real.js +981 -0
- package/dist/facets/inner-do-registry.d.ts +41 -0
- package/dist/facets/inner-do-registry.d.ts.map +1 -0
- package/dist/facets/inner-do-registry.js +51 -0
- package/dist/facets/manager.d.ts +314 -0
- package/dist/facets/manager.d.ts.map +1 -0
- package/dist/facets/manager.js +2681 -0
- package/dist/facets/process.d.ts +318 -0
- package/dist/facets/process.d.ts.map +1 -0
- package/dist/facets/process.js +567 -0
- package/dist/facets/real-vite-fs-shim.d.ts +151 -0
- package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
- package/dist/facets/real-vite-fs-shim.js +1279 -0
- package/dist/facets/real-vite-hmr.d.ts +142 -0
- package/dist/facets/real-vite-hmr.d.ts.map +1 -0
- package/dist/facets/real-vite-hmr.js +476 -0
- package/dist/facets/vite-dev-server.d.ts +293 -0
- package/dist/facets/vite-dev-server.d.ts.map +1 -0
- package/dist/facets/vite-dev-server.js +2580 -0
- package/dist/facets/wasm-swap-registry.d.ts +261 -0
- package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
- package/dist/facets/wasm-swap-registry.js +551 -0
- package/dist/facets/ws-terminal.d.ts +107 -0
- package/dist/facets/ws-terminal.d.ts.map +1 -0
- package/dist/facets/ws-terminal.js +207 -0
- package/dist/git/commands.d.ts +13 -0
- package/dist/git/commands.d.ts.map +1 -0
- package/dist/git/commands.js +633 -0
- package/dist/git/network-facet.d.ts +61 -0
- package/dist/git/network-facet.d.ts.map +1 -0
- package/dist/git/network-facet.js +700 -0
- package/dist/git-bundle.generated.d.ts +16 -0
- package/dist/git-bundle.generated.d.ts.map +1 -0
- package/dist/git-bundle.generated.js +15 -0
- package/dist/index.d.ts +41 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +73 -0
- package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
- package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-facet.js +103 -0
- package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
- package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
- package/dist/loaders/child-process/spawn-pool.js +161 -0
- package/dist/loaders/fanout-pool.d.ts +195 -0
- package/dist/loaders/fanout-pool.d.ts.map +1 -0
- package/dist/loaders/fanout-pool.js +326 -0
- package/dist/loaders/generated-workers.d.ts +26 -0
- package/dist/loaders/generated-workers.d.ts.map +1 -0
- package/dist/loaders/generated-workers.js +25 -0
- package/dist/loaders/index.d.ts +15 -0
- package/dist/loaders/index.d.ts.map +1 -0
- package/dist/loaders/index.js +13 -0
- package/dist/loaders/loader-pool.d.ts +273 -0
- package/dist/loaders/loader-pool.d.ts.map +1 -0
- package/dist/loaders/loader-pool.js +624 -0
- package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
- package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
- package/dist/loaders/npm-resolve-preamble.js +237 -0
- package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
- package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
- package/dist/loaders/pre-bundle-preamble.js +114 -0
- package/dist/loaders/vendor/codegen.d.ts +24 -0
- package/dist/loaders/vendor/codegen.d.ts.map +1 -0
- package/dist/loaders/vendor/codegen.js +44 -0
- package/dist/loaders/vendor/errors.d.ts +24 -0
- package/dist/loaders/vendor/errors.d.ts.map +1 -0
- package/dist/loaders/vendor/errors.js +46 -0
- package/dist/loaders/vendor/index.d.ts +16 -0
- package/dist/loaders/vendor/index.d.ts.map +1 -0
- package/dist/loaders/vendor/index.js +11 -0
- package/dist/loaders/vendor/pool.d.ts +88 -0
- package/dist/loaders/vendor/pool.d.ts.map +1 -0
- package/dist/loaders/vendor/pool.js +353 -0
- package/dist/loaders/vendor/primitives.d.ts +11 -0
- package/dist/loaders/vendor/primitives.d.ts.map +1 -0
- package/dist/loaders/vendor/primitives.js +33 -0
- package/dist/loaders/vendor/serialize.d.ts +3 -0
- package/dist/loaders/vendor/serialize.d.ts.map +1 -0
- package/dist/loaders/vendor/serialize.js +25 -0
- package/dist/loaders/vendor/types.d.ts +60 -0
- package/dist/loaders/vendor/types.d.ts.map +1 -0
- package/dist/loaders/vendor/types.js +4 -0
- package/dist/npm/cache.d.ts +136 -0
- package/dist/npm/cache.d.ts.map +1 -0
- package/dist/npm/cache.js +429 -0
- package/dist/npm/install-batch-facet.d.ts +129 -0
- package/dist/npm/install-batch-facet.d.ts.map +1 -0
- package/dist/npm/install-batch-facet.js +598 -0
- package/dist/npm/install-facet.d.ts +29 -0
- package/dist/npm/install-facet.d.ts.map +1 -0
- package/dist/npm/install-facet.js +9 -0
- package/dist/npm/installer.d.ts +239 -0
- package/dist/npm/installer.d.ts.map +1 -0
- package/dist/npm/installer.js +2022 -0
- package/dist/npm/npx-install.d.ts +74 -0
- package/dist/npm/npx-install.d.ts.map +1 -0
- package/dist/npm/npx-install.js +302 -0
- package/dist/npm/pre-bundle-facet.d.ts +168 -0
- package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
- package/dist/npm/pre-bundle-facet.js +653 -0
- package/dist/npm/r2-cache.d.ts +286 -0
- package/dist/npm/r2-cache.d.ts.map +1 -0
- package/dist/npm/r2-cache.js +506 -0
- package/dist/npm/resolve-facet.d.ts +202 -0
- package/dist/npm/resolve-facet.d.ts.map +1 -0
- package/dist/npm/resolve-facet.js +833 -0
- package/dist/npm/resolve-one-facet.d.ts +204 -0
- package/dist/npm/resolve-one-facet.d.ts.map +1 -0
- package/dist/npm/resolve-one-facet.js +437 -0
- package/dist/npm/resolver.d.ts +109 -0
- package/dist/npm/resolver.d.ts.map +1 -0
- package/dist/npm/resolver.js +899 -0
- package/dist/npm/tarball-stream.d.ts +85 -0
- package/dist/npm/tarball-stream.d.ts.map +1 -0
- package/dist/npm/tarball-stream.js +205 -0
- package/dist/npm/tarball.d.ts +51 -0
- package/dist/npm/tarball.d.ts.map +1 -0
- package/dist/npm/tarball.js +185 -0
- package/dist/observability/diag-counters.d.ts +292 -0
- package/dist/observability/diag-counters.d.ts.map +1 -0
- package/dist/observability/diag-counters.js +302 -0
- package/dist/observability/heap-estimate.d.ts +166 -0
- package/dist/observability/heap-estimate.d.ts.map +1 -0
- package/dist/observability/heap-estimate.js +192 -0
- package/dist/observability/heavy-alloc-coord.d.ts +95 -0
- package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
- package/dist/observability/heavy-alloc-coord.js +149 -0
- package/dist/observability/oom-classify.d.ts +56 -0
- package/dist/observability/oom-classify.d.ts.map +1 -0
- package/dist/observability/oom-classify.js +118 -0
- package/dist/observability/oom-discriminator.d.ts +192 -0
- package/dist/observability/oom-discriminator.d.ts.map +1 -0
- package/dist/observability/oom-discriminator.js +233 -0
- package/dist/real-vite-bundle.generated.d.ts +46 -0
- package/dist/real-vite-bundle.generated.d.ts.map +1 -0
- package/dist/real-vite-bundle.generated.js +53 -0
- package/dist/replica/routing.d.ts +171 -0
- package/dist/replica/routing.d.ts.map +1 -0
- package/dist/replica/routing.js +288 -0
- package/dist/replica/suspension.d.ts +36 -0
- package/dist/replica/suspension.d.ts.map +1 -0
- package/dist/replica/suspension.js +49 -0
- package/dist/router/index.d.ts +156 -0
- package/dist/router/index.d.ts.map +1 -0
- package/dist/router/index.js +220 -0
- package/dist/runtime/assets-loader.d.ts +71 -0
- package/dist/runtime/assets-loader.d.ts.map +1 -0
- package/dist/runtime/assets-loader.js +128 -0
- package/dist/runtime/barrel-detect.d.ts +46 -0
- package/dist/runtime/barrel-detect.d.ts.map +1 -0
- package/dist/runtime/barrel-detect.js +73 -0
- package/dist/runtime/barrel-synthesizer.d.ts +181 -0
- package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
- package/dist/runtime/barrel-synthesizer.js +541 -0
- package/dist/runtime/bun-repl.d.ts +46 -0
- package/dist/runtime/bun-repl.d.ts.map +1 -0
- package/dist/runtime/bun-repl.js +326 -0
- package/dist/runtime/bun-runner.d.ts +66 -0
- package/dist/runtime/bun-runner.d.ts.map +1 -0
- package/dist/runtime/bun-runner.js +219 -0
- package/dist/runtime/clang-runner.d.ts +38 -0
- package/dist/runtime/clang-runner.d.ts.map +1 -0
- package/dist/runtime/clang-runner.js +1033 -0
- package/dist/runtime/comment-strip.d.ts +37 -0
- package/dist/runtime/comment-strip.d.ts.map +1 -0
- package/dist/runtime/comment-strip.js +74 -0
- package/dist/runtime/esbuild-service.d.ts +201 -0
- package/dist/runtime/esbuild-service.d.ts.map +1 -0
- package/dist/runtime/esbuild-service.js +1444 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
- package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
- package/dist/runtime/esbuild-wasm-bytes.js +131 -0
- package/dist/runtime/framework-detect.d.ts +71 -0
- package/dist/runtime/framework-detect.d.ts.map +1 -0
- package/dist/runtime/framework-detect.js +178 -0
- package/dist/runtime/long-running-handle.d.ts +133 -0
- package/dist/runtime/long-running-handle.d.ts.map +1 -0
- package/dist/runtime/long-running-handle.js +192 -0
- package/dist/runtime/node-repl.d.ts +36 -0
- package/dist/runtime/node-repl.d.ts.map +1 -0
- package/dist/runtime/node-repl.js +293 -0
- package/dist/runtime/node-runner.d.ts +111 -0
- package/dist/runtime/node-runner.d.ts.map +1 -0
- package/dist/runtime/node-runner.js +194 -0
- package/dist/runtime/node-shims.d.ts +24 -0
- package/dist/runtime/node-shims.d.ts.map +1 -0
- package/dist/runtime/node-shims.js +3019 -0
- package/dist/runtime/package-manager.d.ts +118 -0
- package/dist/runtime/package-manager.d.ts.map +1 -0
- package/dist/runtime/package-manager.js +420 -0
- package/dist/runtime/port-registry.d.ts +80 -0
- package/dist/runtime/port-registry.d.ts.map +1 -0
- package/dist/runtime/port-registry.js +166 -0
- package/dist/runtime/process-logs-api.d.ts +102 -0
- package/dist/runtime/process-logs-api.d.ts.map +1 -0
- package/dist/runtime/process-logs-api.js +253 -0
- package/dist/runtime/process-logs.d.ts +311 -0
- package/dist/runtime/process-logs.d.ts.map +1 -0
- package/dist/runtime/process-logs.js +656 -0
- package/dist/runtime/process-table.d.ts +65 -0
- package/dist/runtime/process-table.d.ts.map +1 -0
- package/dist/runtime/process-table.js +107 -0
- package/dist/runtime/project-detect.d.ts +21 -0
- package/dist/runtime/project-detect.d.ts.map +1 -0
- package/dist/runtime/project-detect.js +46 -0
- package/dist/runtime/python-repl.d.ts +66 -0
- package/dist/runtime/python-repl.d.ts.map +1 -0
- package/dist/runtime/python-repl.js +563 -0
- package/dist/runtime/python-runner.d.ts +50 -0
- package/dist/runtime/python-runner.d.ts.map +1 -0
- package/dist/runtime/python-runner.js +870 -0
- package/dist/runtime/repl-session.d.ts +160 -0
- package/dist/runtime/repl-session.d.ts.map +1 -0
- package/dist/runtime/repl-session.js +393 -0
- package/dist/runtime/require-resolver.d.ts +52 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -0
- package/dist/runtime/require-resolver.js +690 -0
- package/dist/runtime/router-basename.d.ts +36 -0
- package/dist/runtime/router-basename.d.ts.map +1 -0
- package/dist/runtime/router-basename.js +310 -0
- package/dist/runtime/ruby-repl.d.ts +43 -0
- package/dist/runtime/ruby-repl.d.ts.map +1 -0
- package/dist/runtime/ruby-repl.js +229 -0
- package/dist/runtime/ruby-runner.d.ts +62 -0
- package/dist/runtime/ruby-runner.d.ts.map +1 -0
- package/dist/runtime/ruby-runner.js +737 -0
- package/dist/runtime/runtime-catalog.d.ts +99 -0
- package/dist/runtime/runtime-catalog.d.ts.map +1 -0
- package/dist/runtime/runtime-catalog.js +183 -0
- package/dist/runtime/runtime-registry.d.ts +136 -0
- package/dist/runtime/runtime-registry.d.ts.map +1 -0
- package/dist/runtime/runtime-registry.js +318 -0
- package/dist/runtime/streams.d.ts +13 -0
- package/dist/runtime/streams.d.ts.map +1 -0
- package/dist/runtime/streams.js +402 -0
- package/dist/runtime/wasi-instance.d.ts +190 -0
- package/dist/runtime/wasi-instance.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.js +2044 -0
- package/dist/runtime/wasm-runner.d.ts +117 -0
- package/dist/runtime/wasm-runner.d.ts.map +1 -0
- package/dist/runtime/wasm-runner.js +705 -0
- package/dist/session/bindings.d.ts +168 -0
- package/dist/session/bindings.d.ts.map +1 -0
- package/dist/session/bindings.js +524 -0
- package/dist/session/ctx-exports.d.ts +16 -0
- package/dist/session/ctx-exports.d.ts.map +1 -0
- package/dist/session/ctx-exports.js +22 -0
- package/dist/session/diag.d.ts +62 -0
- package/dist/session/diag.d.ts.map +1 -0
- package/dist/session/diag.js +118 -0
- package/dist/session/fs-watch.d.ts +132 -0
- package/dist/session/fs-watch.d.ts.map +1 -0
- package/dist/session/fs-watch.js +255 -0
- package/dist/session/helpers.d.ts +150 -0
- package/dist/session/helpers.d.ts.map +1 -0
- package/dist/session/helpers.js +472 -0
- package/dist/session/hibernation.d.ts +145 -0
- package/dist/session/hibernation.d.ts.map +1 -0
- package/dist/session/hibernation.js +385 -0
- package/dist/session/init-phases.d.ts +87 -0
- package/dist/session/init-phases.d.ts.map +1 -0
- package/dist/session/init-phases.js +137 -0
- package/dist/session/init.d.ts +46 -0
- package/dist/session/init.d.ts.map +1 -0
- package/dist/session/init.js +3579 -0
- package/dist/session/keys.d.ts +56 -0
- package/dist/session/keys.d.ts.map +1 -0
- package/dist/session/keys.js +55 -0
- package/dist/session/nimbus-session.d.ts +434 -0
- package/dist/session/nimbus-session.d.ts.map +1 -0
- package/dist/session/nimbus-session.js +1084 -0
- package/dist/session/programmatic.d.ts +105 -0
- package/dist/session/programmatic.d.ts.map +1 -0
- package/dist/session/programmatic.js +326 -0
- package/dist/session/replica-routes.d.ts +68 -0
- package/dist/session/replica-routes.d.ts.map +1 -0
- package/dist/session/replica-routes.js +77 -0
- package/dist/session/routes.d.ts +29 -0
- package/dist/session/routes.d.ts.map +1 -0
- package/dist/session/routes.js +1331 -0
- package/dist/session/rpc.d.ts +323 -0
- package/dist/session/rpc.d.ts.map +1 -0
- package/dist/session/rpc.js +808 -0
- package/dist/session/state-store.d.ts +210 -0
- package/dist/session/state-store.d.ts.map +1 -0
- package/dist/session/state-store.js +459 -0
- package/dist/session/supervisor-rpc.d.ts +269 -0
- package/dist/session/supervisor-rpc.d.ts.map +1 -0
- package/dist/session/supervisor-rpc.js +437 -0
- package/dist/session/ws-hibernation-config.d.ts +64 -0
- package/dist/session/ws-hibernation-config.d.ts.map +1 -0
- package/dist/session/ws-hibernation-config.js +89 -0
- package/dist/session/ws.d.ts +95 -0
- package/dist/session/ws.d.ts.map +1 -0
- package/dist/session/ws.js +395 -0
- package/dist/shell/features.d.ts +410 -0
- package/dist/shell/features.d.ts.map +1 -0
- package/dist/shell/features.js +1885 -0
- package/dist/shell/unix-commands.d.ts +15 -0
- package/dist/shell/unix-commands.d.ts.map +1 -0
- package/dist/shell/unix-commands.js +3703 -0
- package/dist/stubs/lifo-ui.d.ts +10 -0
- package/dist/stubs/lifo-ui.d.ts.map +1 -0
- package/dist/stubs/lifo-ui.js +1 -0
- package/dist/tailwind-play.generated.d.ts +19 -0
- package/dist/tailwind-play.generated.d.ts.map +1 -0
- package/dist/tailwind-play.generated.js +18 -0
- package/dist/vfs/dev-provider.d.ts +64 -0
- package/dist/vfs/dev-provider.d.ts.map +1 -0
- package/dist/vfs/dev-provider.js +191 -0
- package/dist/vfs/events.d.ts +51 -0
- package/dist/vfs/events.d.ts.map +1 -0
- package/dist/vfs/events.js +127 -0
- package/dist/vfs/path.d.ts +30 -0
- package/dist/vfs/path.d.ts.map +1 -0
- package/dist/vfs/path.js +41 -0
- package/dist/vfs/seed-project.d.ts +62 -0
- package/dist/vfs/seed-project.d.ts.map +1 -0
- package/dist/vfs/seed-project.js +1045 -0
- package/dist/vfs/sqlite-vfs.d.ts +435 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
- package/dist/vfs/sqlite-vfs.js +1670 -0
- package/dist/vfs/symlink-registry.d.ts +64 -0
- package/dist/vfs/symlink-registry.d.ts.map +1 -0
- package/dist/vfs/symlink-registry.js +137 -0
- package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
- package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
- package/dist/wrangler/nimbus-wrangler.js +931 -0
- package/package.json +77 -0
- package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
- package/public/_assets/esbuild-0.24.2.wasm +0 -0
- package/public/_assets/real-vite-bundle.js +101478 -0
- package/public/_assets/rollup.wasm +0 -0
- package/public/favicon.svg +8 -0
- package/public/index.html +565 -0
- package/public/s/index.html +2379 -0
- package/scripts/bundle-esbuild-wasm.mjs +244 -0
- package/scripts/bundle-facet-workers.mjs +137 -0
- package/scripts/bundle-git.mjs +118 -0
- package/scripts/bundle-npm-cjs.mjs +363 -0
- package/scripts/bundle-plugin-react.mjs +480 -0
- package/scripts/bundle-real-vite.mjs +959 -0
- package/scripts/bundle-runtime.mjs +600 -0
- package/scripts/bundle-tailwind-play.mjs +130 -0
- package/scripts/patch-lifo-ui.mjs +131 -0
|
@@ -0,0 +1,1331 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* session/routes.ts — HTTP/WS fetch routing for the supervisor DO.
|
|
3
|
+
*
|
|
4
|
+
* One handleRequest function for everything the DO speaks:
|
|
5
|
+
* /ws upgrade → cold-init / warm-rejoin (B'.5) / 409
|
|
6
|
+
* /preview/* → cirrus-real or vite-dev-server forward
|
|
7
|
+
* /port/:n/* → user http-server proxy via port-registry
|
|
8
|
+
* /worker/* → nimbus-wrangler dev forward
|
|
9
|
+
* /api/_diag/* → forensic surfaces (memory, session, cirrus)
|
|
10
|
+
* /api/_test/* → NIMBUS_DEBUG-gated probe endpoints
|
|
11
|
+
* /api/* (other) → small JSON endpoints (write-file, mkdir, ...)
|
|
12
|
+
*
|
|
13
|
+
* The dispatcher is one big if/else by design — pattern-matching
|
|
14
|
+
* URL paths cleanly is easier to read than a Map-based router for
|
|
15
|
+
* this many one-off shapes, and grep-ability matters when debugging.
|
|
16
|
+
*
|
|
17
|
+
* Surfaces:
|
|
18
|
+
* - handleFetch(self, request) — top-level dispatcher; was _handleFetch.
|
|
19
|
+
*
|
|
20
|
+
* The class retains `fetch` (DO contract) + `_handleFetch` as delegators
|
|
21
|
+
* per plan §IX.4 R1.
|
|
22
|
+
*
|
|
23
|
+
* Per DEFECT-D1: route handlers read self.ctx + self.env extensively
|
|
24
|
+
* (~30 sites). RoutesHost = any pragmatic deviation, like InitHost in S6.
|
|
25
|
+
*/
|
|
26
|
+
import { handleReplicaPreflight as _w12HandleReplicaPreflight } from '../replica/routing.js';
|
|
27
|
+
import { replicasSuspended as _w12ReplicasSuspended } from '../replica/suspension.js';
|
|
28
|
+
import { matchLogsPath, handleLogsWebSocketRequest, handleProcessesListRequest, } from '../runtime/process-logs-api.js';
|
|
29
|
+
import { readDiagCounters } from '../observability/diag-counters.js';
|
|
30
|
+
import { getFailures, getLastRpcFrame, getLastFacetId, getRecoveryEvents, recordRecoveryEvent, resetRecoveryEvents, } from '../observability/oom-discriminator.js';
|
|
31
|
+
import { LRU_MAX_ENTRIES } from '../constants.js';
|
|
32
|
+
import { estimateSupervisorHeap, WORKERD_EVICTION_LABELS } from '../observability/heap-estimate.js';
|
|
33
|
+
import { loadShellState, loadKernelMounts, getScrollbackStats, clearSessionState, appendScrollback, loadScrollback } from './state-store.js';
|
|
34
|
+
import { isWarmRejoin, joinExistingSession } from './init-phases.js';
|
|
35
|
+
import { EsbuildService } from '../runtime/esbuild-service.js';
|
|
36
|
+
import { ViteDevServer } from '../facets/vite-dev-server.js';
|
|
37
|
+
import { ProcessLogStore } from '../runtime/process-logs.js';
|
|
38
|
+
import { notifyTerminalEvent } from '../runtime/process-logs-api.js';
|
|
39
|
+
import { makeLongRunningPortStub } from '../runtime/long-running-handle.js';
|
|
40
|
+
import { getLoadedCodesStats } from './bindings.js';
|
|
41
|
+
import { renderNoDevServerHtml } from './helpers.js';
|
|
42
|
+
// CLN-1 (2026-05-11): also import R2_CACHE_PREFIX + L2_KEY_HOST so the
|
|
43
|
+
// cache-purge helpers below don't hardcode the synthetic key shape.
|
|
44
|
+
// Bumping R2_CACHE_PREFIX 'v1' → 'v2' now invalidates the L2 cache key
|
|
45
|
+
// shape used here automatically.
|
|
46
|
+
import { R2CacheClient, R2_CACHE_PREFIX, L2_KEY_HOST } from '../npm/r2-cache.js';
|
|
47
|
+
import { fetchEsbuildWasmBytes, ESBUILD_WASM_L2_KEY } from '../runtime/esbuild-wasm-bytes.js';
|
|
48
|
+
import { NimbusFanoutPool, IN_DO_THRESHOLD, MAX_PEER_FANOUT, hashKeyToShard } from '../loaders/fanout-pool.js';
|
|
49
|
+
export async function handleFetch(self, request) {
|
|
50
|
+
const url = new URL(request.url);
|
|
51
|
+
// Capture session basePath from the routing header (if forwarded by the
|
|
52
|
+
// Worker's session-router). Threaded through to ViteDevServer so the
|
|
53
|
+
// served app's module URLs, HMR paths, <base href>, and router basename
|
|
54
|
+
// all resolve under `/s/<id>/preview/...`.
|
|
55
|
+
await self.hydrateSessionBasePath(request);
|
|
56
|
+
// W9: bump the isolate generation counter on the FIRST request of a
|
|
57
|
+
// new isolate (cold start or post-hibernation wake). Cheap — one
|
|
58
|
+
// storage.get + one storage.put per isolate. Subsequent calls in the
|
|
59
|
+
// same isolate are a fast no-op (gated by _w9IsolateGenPersisted).
|
|
60
|
+
await self._w9MaybeBumpIsolateGen();
|
|
61
|
+
// ── W12 — DO read replica preflight ─────────────────────────────────
|
|
62
|
+
//
|
|
63
|
+
// If THIS isolate is a replica AND the route policy says delegate, we
|
|
64
|
+
// forward the Request to the primary via `ctx.storage.primary.fetch()`
|
|
65
|
+
// and return the primary's Response. Single intra-region RPC hop:
|
|
66
|
+
// the replica was placed near the primary, so this is fast; the user
|
|
67
|
+
// experiences edge-RTT-to-replica + RPC + primary-handle, which is
|
|
68
|
+
// strictly less than user-RTT-to-far-region for cross-region tenants.
|
|
69
|
+
//
|
|
70
|
+
// On the primary OR for replica-eligible routes (with cold/warm
|
|
71
|
+
// distinction handled), `delegated === false` and we fall through to
|
|
72
|
+
// the existing route handlers unchanged.
|
|
73
|
+
//
|
|
74
|
+
// Graceful-degrade: if `inspectReplicaState` reports isReplica but the
|
|
75
|
+
// primary stub is unusable, `handleReplicaPreflight` returns
|
|
76
|
+
// `delegated: false` and we handle locally — correctness > latency.
|
|
77
|
+
//
|
|
78
|
+
// Performance note: the preflight is <1ms (pure pathname classification
|
|
79
|
+
// + a `typeof` check on `ctx.storage.primary`). Hot path.
|
|
80
|
+
try {
|
|
81
|
+
const w12Pre = await _w12HandleReplicaPreflight(self.ctx, request, {
|
|
82
|
+
isWarm: !!(self.viteDevServer?.isRunning || self.cirrusReal?.isRunning),
|
|
83
|
+
suspended: _w12ReplicasSuspended(),
|
|
84
|
+
});
|
|
85
|
+
if (w12Pre.delegated && w12Pre.response) {
|
|
86
|
+
return w12Pre.response;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
catch (e) {
|
|
90
|
+
// Preflight should never throw, but never let a routing helper kill
|
|
91
|
+
// request handling. Log + continue with local handling.
|
|
92
|
+
console.warn('[nimbus/W12] preflight threw:', e?.message);
|
|
93
|
+
}
|
|
94
|
+
if (url.pathname === '/ws') {
|
|
95
|
+
if (request.headers.get('Upgrade') !== 'websocket') {
|
|
96
|
+
return new Response('Expected WebSocket', { status: 426 });
|
|
97
|
+
}
|
|
98
|
+
// [B'.5] Three-way decision on a /ws upgrade:
|
|
99
|
+
// 1. Warm rejoin: a wsClose/wsError fired earlier and left
|
|
100
|
+
// kernel/shell/terminal alive in-memory. Re-attach the
|
|
101
|
+
// new ws to the existing Shell — Phase B skipped.
|
|
102
|
+
// 2. Cold init: no shell yet (first connect, or post-DO-
|
|
103
|
+
// eviction). Run the full R/B/W/O sequence.
|
|
104
|
+
// 3. Active conflict: Shell is non-null AND phase != drained,
|
|
105
|
+
// meaning some other /ws is already attached. 409 to
|
|
106
|
+
// prevent two-tab cross-wiring (multi-tab share is a
|
|
107
|
+
// separate feature; B'.5 doesn't enable it).
|
|
108
|
+
const pair = new WebSocketPair();
|
|
109
|
+
const [client, server] = Object.values(pair);
|
|
110
|
+
self.ctx.acceptWebSocket(server);
|
|
111
|
+
try {
|
|
112
|
+
server.serializeAttachment?.({ kind: 'shell' });
|
|
113
|
+
}
|
|
114
|
+
catch { }
|
|
115
|
+
if (isWarmRejoin(self)) {
|
|
116
|
+
// Warm rejoin path. The existing Shell is alive; we just
|
|
117
|
+
// swap the WebSocketTerminal's ws ref + replay scrollback.
|
|
118
|
+
try {
|
|
119
|
+
joinExistingSession(self, server, appendScrollback, loadScrollback);
|
|
120
|
+
}
|
|
121
|
+
catch (err) {
|
|
122
|
+
console.error('warm-rejoin error:', err?.message, err?.stack);
|
|
123
|
+
return new Response('Rejoin failed: ' + err?.message, { status: 500 });
|
|
124
|
+
}
|
|
125
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
126
|
+
}
|
|
127
|
+
if (self.shell != null) {
|
|
128
|
+
// Active conflict — Shell exists and isn't drained. Some
|
|
129
|
+
// other /ws is attached; reject this one.
|
|
130
|
+
return new Response(JSON.stringify({
|
|
131
|
+
error: 'session already has active terminal',
|
|
132
|
+
hint: 'open a new /new session',
|
|
133
|
+
}), {
|
|
134
|
+
status: 409,
|
|
135
|
+
headers: { 'Content-Type': 'application/json' },
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
// Cold init path — first ever /ws (or post-DO-eviction).
|
|
139
|
+
try {
|
|
140
|
+
self.initSession(server);
|
|
141
|
+
}
|
|
142
|
+
catch (err) {
|
|
143
|
+
console.error('initSession error:', err?.message, err?.stack);
|
|
144
|
+
return new Response('Init failed: ' + err?.message, { status: 500 });
|
|
145
|
+
}
|
|
146
|
+
return new Response(null, { status: 101, webSocket: client });
|
|
147
|
+
}
|
|
148
|
+
// ── Process log streaming / listing — see src/process-logs-api.ts ──
|
|
149
|
+
const logsPid = matchLogsPath(url.pathname);
|
|
150
|
+
if (logsPid !== null) {
|
|
151
|
+
return handleLogsWebSocketRequest(request, logsPid, {
|
|
152
|
+
processLogs: self.processLogs,
|
|
153
|
+
processTable: self.processTable,
|
|
154
|
+
// W9: pass ctx so the upgrade uses ctx.acceptWebSocket (hibernatable).
|
|
155
|
+
ctx: self.ctx,
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
if (url.pathname === '/api/processes') {
|
|
159
|
+
return handleProcessesListRequest(self.processTable, self.processLogs);
|
|
160
|
+
}
|
|
161
|
+
// Primitives wave (P11) — kill / restart by PID for the Process tab UI.
|
|
162
|
+
//
|
|
163
|
+
// Both endpoints accept POST only. The body is empty; the PID is in the
|
|
164
|
+
// URL. Same authorization model as the rest of /api/* (session-scoped
|
|
165
|
+
// via the /s/<id>/ prefix).
|
|
166
|
+
//
|
|
167
|
+
// Behaviour:
|
|
168
|
+
// POST /api/kill/<pid> — equivalent to typing `kill <pid>` in
|
|
169
|
+
// the shell. Tears down a vite-shim PID
|
|
170
|
+
// cleanly (stops viteDevServer, deletes
|
|
171
|
+
// vite-config) OR delegates to
|
|
172
|
+
// facetManager.kill for real facets.
|
|
173
|
+
// POST /api/restart/<pid> — kill + (if it was the vite shim)
|
|
174
|
+
// re-run `vite` with the same argv to
|
|
175
|
+
// boot a fresh server. For real facets,
|
|
176
|
+
// currently only kills (caller must
|
|
177
|
+
// re-issue spawn). Same response shape.
|
|
178
|
+
//
|
|
179
|
+
// Response: 200 {ok, pid, action} on success; 404 {error, pid} when
|
|
180
|
+
// PID isn't tracked; 502 on internal failure.
|
|
181
|
+
{
|
|
182
|
+
const killMatch = url.pathname.match(/^\/api\/(kill|restart)\/(\d+)$/);
|
|
183
|
+
if (killMatch && request.method === 'POST') {
|
|
184
|
+
const action = killMatch[1];
|
|
185
|
+
const pid = parseInt(killMatch[2], 10);
|
|
186
|
+
const json = (status, body) => new Response(JSON.stringify(body), {
|
|
187
|
+
status, headers: { 'Content-Type': 'application/json' },
|
|
188
|
+
});
|
|
189
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
190
|
+
return json(400, { error: 'invalid pid', pid });
|
|
191
|
+
}
|
|
192
|
+
const entry = self.processTable.get(pid);
|
|
193
|
+
if (!entry) {
|
|
194
|
+
return json(404, { error: 'no such process', pid });
|
|
195
|
+
}
|
|
196
|
+
const isViteShim = self._viteShimPid === pid;
|
|
197
|
+
try {
|
|
198
|
+
if (isViteShim) {
|
|
199
|
+
// Same teardown the `kill` shell handler does. Centralised
|
|
200
|
+
// here so the UI doesn't need to reimplement it.
|
|
201
|
+
try {
|
|
202
|
+
if (self.cirrusReal?.isRunning) {
|
|
203
|
+
self.cirrusReal.stop(self.ctx);
|
|
204
|
+
self.cirrusReal = null;
|
|
205
|
+
}
|
|
206
|
+
if (self.viteDevServer?.isRunning) {
|
|
207
|
+
self.viteDevServer.stop();
|
|
208
|
+
self.viteDevServer = null;
|
|
209
|
+
try {
|
|
210
|
+
await self.ctx.storage.delete('vite-config');
|
|
211
|
+
}
|
|
212
|
+
catch { }
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
catch { /* keep going to teardown processTable / portRegistry */ }
|
|
216
|
+
try {
|
|
217
|
+
self.portRegistry.unregisterByPid(pid);
|
|
218
|
+
}
|
|
219
|
+
catch { }
|
|
220
|
+
try {
|
|
221
|
+
self.processTable.kill(pid);
|
|
222
|
+
}
|
|
223
|
+
catch { }
|
|
224
|
+
self._viteShimPid = null;
|
|
225
|
+
self._viteShimPort = null;
|
|
226
|
+
}
|
|
227
|
+
else if (self.facetManager) {
|
|
228
|
+
const ok = self.facetManager.kill(pid);
|
|
229
|
+
if (!ok)
|
|
230
|
+
return json(404, { error: 'facetManager.kill returned false', pid });
|
|
231
|
+
}
|
|
232
|
+
else {
|
|
233
|
+
// No facetManager and not a vite shim — best-effort
|
|
234
|
+
// process-table tombstone so the UI can re-render the badge.
|
|
235
|
+
try {
|
|
236
|
+
self.portRegistry.unregisterByPid(pid);
|
|
237
|
+
}
|
|
238
|
+
catch { }
|
|
239
|
+
try {
|
|
240
|
+
self.processTable.kill(pid);
|
|
241
|
+
}
|
|
242
|
+
catch { }
|
|
243
|
+
}
|
|
244
|
+
}
|
|
245
|
+
catch (e) {
|
|
246
|
+
return json(502, { error: String(e?.message || e), pid });
|
|
247
|
+
}
|
|
248
|
+
// For 'restart', re-issue the equivalent of `vite` in the shell
|
|
249
|
+
// when the killed PID was the vite shim. We do NOT generically
|
|
250
|
+
// restart arbitrary processes — the supervisor doesn't keep
|
|
251
|
+
// enough argv/env state to do that safely for real facets.
|
|
252
|
+
if (action === 'restart' && isViteShim) {
|
|
253
|
+
// Send a synthetic 'vite' command line to the terminal so the
|
|
254
|
+
// existing registry handler runs. This is the simplest way to
|
|
255
|
+
// re-trigger the SAME code path the user originally invoked,
|
|
256
|
+
// without duplicating its 100-line setup here.
|
|
257
|
+
if (self.terminal?.ws) {
|
|
258
|
+
try {
|
|
259
|
+
self.terminal.ws.send(JSON.stringify({ type: 'output', data: '\r\n' }));
|
|
260
|
+
}
|
|
261
|
+
catch { }
|
|
262
|
+
}
|
|
263
|
+
// Drop a marker so a future probe can assert on the action.
|
|
264
|
+
notifyTerminalEvent(self.terminal, {
|
|
265
|
+
type: 'restart-requested', pid, command: entry.command || 'vite',
|
|
266
|
+
});
|
|
267
|
+
}
|
|
268
|
+
return json(200, { ok: true, pid, action });
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
if (url.pathname === '/api/memory') {
|
|
272
|
+
// Minimal memory probe for stability investigations (WORKERD-CRASH
|
|
273
|
+
// hypotheses). Reports whatever we can measure inside workerd:
|
|
274
|
+
// - vfs.{totalFiles, totalBytes} from the SQLite VFS
|
|
275
|
+
// - process.memoryUsage() if nodejs_compat exposes it (else zeros)
|
|
276
|
+
// - performance.memory when present (Chromium-style heap info)
|
|
277
|
+
self.ensureSqliteFs();
|
|
278
|
+
let nodeMem = null;
|
|
279
|
+
try {
|
|
280
|
+
const g = globalThis;
|
|
281
|
+
if (g.process && typeof g.process.memoryUsage === 'function') {
|
|
282
|
+
const mu = g.process.memoryUsage();
|
|
283
|
+
nodeMem = {
|
|
284
|
+
rss: mu.rss | 0,
|
|
285
|
+
heapTotal: mu.heapTotal | 0,
|
|
286
|
+
heapUsed: mu.heapUsed | 0,
|
|
287
|
+
external: mu.external | 0,
|
|
288
|
+
arrayBuffers: mu.arrayBuffers | 0,
|
|
289
|
+
};
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
catch { /* ignore */ }
|
|
293
|
+
let perfMem = null;
|
|
294
|
+
try {
|
|
295
|
+
const g = globalThis;
|
|
296
|
+
if (g.performance && g.performance.memory) {
|
|
297
|
+
perfMem = {
|
|
298
|
+
jsHeapSizeLimit: g.performance.memory.jsHeapSizeLimit | 0,
|
|
299
|
+
totalJSHeapSize: g.performance.memory.totalJSHeapSize | 0,
|
|
300
|
+
usedJSHeapSize: g.performance.memory.usedJSHeapSize | 0,
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
}
|
|
304
|
+
catch { /* ignore */ }
|
|
305
|
+
const vfs = self.sqliteFs.getStats();
|
|
306
|
+
return Response.json({
|
|
307
|
+
vfs: { files: vfs.files, usedBytes: vfs.usedBytes },
|
|
308
|
+
nodeMem,
|
|
309
|
+
perfMem,
|
|
310
|
+
ts: Date.now(),
|
|
311
|
+
});
|
|
312
|
+
}
|
|
313
|
+
// ── Diagnostic memory probe ──────────────────────────────────────────
|
|
314
|
+
// /api/_diag/memory — supervisor heap estimate, eviction taxonomy,
|
|
315
|
+
// and recovery-event ring.
|
|
316
|
+
//
|
|
317
|
+
// Why this endpoint exists
|
|
318
|
+
// ────────────────────────
|
|
319
|
+
// workerd's process.memoryUsage() returns zero for every field inside
|
|
320
|
+
// a DO class context (verified at audit/sections/PROD-RESET-RESEARCH-
|
|
321
|
+
// R1.md §R1.4). The previous endpoint reported nodeMem/perfMem from
|
|
322
|
+
// process.memoryUsage() and they were always zero — useless.
|
|
323
|
+
//
|
|
324
|
+
// C'.1 replaces the zero-everywhere readout with a deterministic
|
|
325
|
+
// estimator (src/observability/heap-estimate.ts) that sums known
|
|
326
|
+
// supervisor allocations from runtime counters. Every byte has a
|
|
327
|
+
// named contributor.
|
|
328
|
+
//
|
|
329
|
+
// C'.2 adds a recovery_event ring (src/oom-discriminator.ts) so
|
|
330
|
+
// probes can assert that lifecycle transitions preserve session
|
|
331
|
+
// state without data loss.
|
|
332
|
+
//
|
|
333
|
+
// Schema (v3, additive over v2):
|
|
334
|
+
// - heap: deterministic estimate + per-source breakdown + ceiling.
|
|
335
|
+
// - evictionLabels: workerd taxonomy (5 reasons).
|
|
336
|
+
// - recoveryEvents: ring of session lifecycle transitions.
|
|
337
|
+
// - All v1/v2 fields preserved for back-compat with existing tools.
|
|
338
|
+
if (url.pathname === '/api/_diag/memory') {
|
|
339
|
+
self.ensureSqliteFs();
|
|
340
|
+
self._diagSampleMemory();
|
|
341
|
+
const nodeMem = self._diagReadNodeMem();
|
|
342
|
+
const perfMem = self._diagReadPerfMem();
|
|
343
|
+
const vfs = self.sqliteFs.getStats();
|
|
344
|
+
const DO_HEAP_LIMIT_BYTES = 128 * 1024 * 1024;
|
|
345
|
+
const heapUsed = nodeMem?.heapUsed ?? 0;
|
|
346
|
+
const counters = readDiagCounters();
|
|
347
|
+
const cacheStats = vfs.cache ?? {};
|
|
348
|
+
const lastFailures = getFailures();
|
|
349
|
+
// ── C'.1 deterministic heap estimate ─────────────────────────────
|
|
350
|
+
// Sources every contributing byte from a runtime counter — never
|
|
351
|
+
// calls process.memoryUsage(). Ceiling is the architectural soft
|
|
352
|
+
// budget (SUPERVISOR_HEAP_CEILING_BYTES = 64 MiB), half the
|
|
353
|
+
// workerd hard cap of 128 MiB.
|
|
354
|
+
// N3 (heap-correctness wave). Pre-fix, this was hardcoded 0 with
|
|
355
|
+
// the comment "matches reality (writes are flushed in
|
|
356
|
+
// microseconds)" — which was wrong: pendingWrites can hold up to
|
|
357
|
+
// 500 chunks × 64 KiB = 32 MiB at peak, AND writeStream's spool
|
|
358
|
+
// can buffer the full incoming batch. Both were invisible to the
|
|
359
|
+
// estimator because there was no counter to read.
|
|
360
|
+
//
|
|
361
|
+
// Post-fix, SqliteVFS maintains TWO running byte sums:
|
|
362
|
+
// - _pendingWriteBytes : the post-deferWrite queue
|
|
363
|
+
// - _writeStreamSpoolBytes : N2 spool inside writeStream
|
|
364
|
+
// Both contribute to "in-flight write bytes the supervisor is
|
|
365
|
+
// currently holding"; the estimator sees their sum.
|
|
366
|
+
// (vfs.sql is the sub-object in getStats() that surfaces these.)
|
|
367
|
+
const sqlStats = vfs.sql ?? {};
|
|
368
|
+
const inFlightWriteBytes = (sqlStats.pendingWriteBytes ?? 0) +
|
|
369
|
+
(sqlStats.writeStreamSpoolBytes ?? 0);
|
|
370
|
+
const heap = estimateSupervisorHeap(counters, {
|
|
371
|
+
cacheHotBytes: cacheStats.hotBytes ?? 0,
|
|
372
|
+
inFlightWriteBytes,
|
|
373
|
+
});
|
|
374
|
+
return Response.json({
|
|
375
|
+
// ── v1 fields (preserved) ─────────────────────────────────
|
|
376
|
+
vfs: { files: vfs.files, usedBytes: vfs.usedBytes },
|
|
377
|
+
nodeMem,
|
|
378
|
+
perfMem,
|
|
379
|
+
peak: {
|
|
380
|
+
rssBytes: self._diagPeakRss,
|
|
381
|
+
heapUsedBytes: self._diagPeakHeapUsed,
|
|
382
|
+
atMs: self._diagPeakAt,
|
|
383
|
+
samples: self._diagSampleCount,
|
|
384
|
+
},
|
|
385
|
+
counters,
|
|
386
|
+
limitBytes: DO_HEAP_LIMIT_BYTES,
|
|
387
|
+
usagePctOfLimit: heapUsed > 0
|
|
388
|
+
? Math.round((heapUsed / DO_HEAP_LIMIT_BYTES) * 1000) / 10
|
|
389
|
+
: 0,
|
|
390
|
+
ts: Date.now(),
|
|
391
|
+
// ── v2 / W5 additions (preserved) ─────────────────────────
|
|
392
|
+
lastFailures,
|
|
393
|
+
vfsDetail: {
|
|
394
|
+
lruBytes: cacheStats.hotBytes ?? 0,
|
|
395
|
+
lruMaxEntries: cacheStats.maxEntries ?? LRU_MAX_ENTRIES,
|
|
396
|
+
lruMaxBytes: cacheStats.maxBytes ?? (LRU_MAX_ENTRIES * 65536),
|
|
397
|
+
lruShrunk: cacheStats.lruShrunk ?? false,
|
|
398
|
+
evictions: cacheStats.evictions ?? 0,
|
|
399
|
+
hitRate: cacheStats.hitRate ?? 0,
|
|
400
|
+
// N3 (heap-correctness wave): pending-writes observability.
|
|
401
|
+
// `pendingWrites` is the entry count (unchanged); the new
|
|
402
|
+
// `pendingWriteBytes` is the live byte total maintained by
|
|
403
|
+
// SqliteVFS._pendingWriteBytes. Probes use the pair to
|
|
404
|
+
// distinguish "many small chunks" from "few large chunks".
|
|
405
|
+
pendingWrites: sqlStats.pendingWrites ?? 0,
|
|
406
|
+
pendingWriteBytes: sqlStats.pendingWriteBytes ?? 0,
|
|
407
|
+
// N2: live byte count inside the writeStream() drain spool.
|
|
408
|
+
// Visible during a real npm install; ~0 at rest.
|
|
409
|
+
writeStreamSpoolBytes: sqlStats.writeStreamSpoolBytes ?? 0,
|
|
410
|
+
},
|
|
411
|
+
// H7 (heap-correctness wave): _NIMBUS_LOADED_CODES Map state.
|
|
412
|
+
// Pre-fix this Map grew unbounded — wrangler dev's rebuild
|
|
413
|
+
// loop accumulated one entry per save until the supervisor
|
|
414
|
+
// hit the workerd 128 MiB hard cap. Post-fix: hard-cap LRU
|
|
415
|
+
// (32 entries) with FIFO eviction. The counters here let
|
|
416
|
+
// ops dashboards visualise the bound + the eviction rate.
|
|
417
|
+
loadedCodes: getLoadedCodesStats(),
|
|
418
|
+
rpc: {
|
|
419
|
+
lastFrame: getLastRpcFrame(),
|
|
420
|
+
},
|
|
421
|
+
facet: {
|
|
422
|
+
lastDispatch: getLastFacetId(),
|
|
423
|
+
},
|
|
424
|
+
// ── v3 / C' observability foundation ──────────────────────
|
|
425
|
+
heap,
|
|
426
|
+
evictionLabels: WORKERD_EVICTION_LABELS,
|
|
427
|
+
recoveryEvents: getRecoveryEvents(),
|
|
428
|
+
// ── W9: hibernation observability ───────────────────────────
|
|
429
|
+
// `hib.isolateGen` increments per fresh isolate (cold start or
|
|
430
|
+
// post-hibernation wake). Two probe calls a minute apart with
|
|
431
|
+
// different gens means a hibernation/wake cycle ran in between.
|
|
432
|
+
// `rehydrated*` counters are >0 only on the first hydrate after
|
|
433
|
+
// a wake. `flushed*` counters track the alarm-driven SQL writes.
|
|
434
|
+
// `autoResponseConfigured` reports the runtime's actual
|
|
435
|
+
// capability (older workerd builds report false).
|
|
436
|
+
hib: {
|
|
437
|
+
isolateGen: self._w9IsolateGen,
|
|
438
|
+
autoResponseConfigured: self._w9WsConfig?.autoResponseConfigured ?? false,
|
|
439
|
+
autoResponseError: self._w9WsConfig?.autoResponseError ?? null,
|
|
440
|
+
hibernationEventTimeoutMs: self._w9WsConfig?.timeoutSetMs ?? null,
|
|
441
|
+
timeoutError: self._w9WsConfig?.timeoutError ?? null,
|
|
442
|
+
...self.processLogs.hibStats(),
|
|
443
|
+
},
|
|
444
|
+
// ── W12: replica observability ──────────────────────────────
|
|
445
|
+
// `replica.state` is one of 'enabled' / 'enabled-via-configure' /
|
|
446
|
+
// 'unsupported' / 'error' / 'unknown' (per tryEnableReplicas
|
|
447
|
+
// result; pre-GA runtimes get 'unsupported' graceful-degrade).
|
|
448
|
+
// `isReplica` is true when this isolate is a regional read
|
|
449
|
+
// replica (its `ctx.storage.primary` is an RpcStub). `bookmark`
|
|
450
|
+
// reflects ctx.storage.getCurrentBookmark() if the API surfaces
|
|
451
|
+
// it — used by future read-your-writes wait-for-bookmark wiring.
|
|
452
|
+
// `suspended` reflects the global write-burst guard (npm install
|
|
453
|
+
// / git clone in flight) per CF research §G.4 + ~lambros feedback.
|
|
454
|
+
replica: self.getReplicaState(),
|
|
455
|
+
});
|
|
456
|
+
}
|
|
457
|
+
// ── /api/_diag/session — Track B' state-store debug surface [B'.1] ──
|
|
458
|
+
//
|
|
459
|
+
// Exposes the persisted shell state so the probe at
|
|
460
|
+
// audit/probes/b-prime/b1-shell-state/ can verify the SQL row
|
|
461
|
+
// shape directly. Read-only; no side effects. Always returns
|
|
462
|
+
// 200 even when no row exists (the snapshot just shows
|
|
463
|
+
// hasPersistedState=false).
|
|
464
|
+
if (url.pathname === '/api/_diag/session') {
|
|
465
|
+
const snap = loadShellState(self.ctx);
|
|
466
|
+
const mounts = loadKernelMounts(self.ctx);
|
|
467
|
+
const sbStats = getScrollbackStats(self.ctx);
|
|
468
|
+
return Response.json({
|
|
469
|
+
cwd: snap.cwd,
|
|
470
|
+
env: snap.env,
|
|
471
|
+
hydratedAt: snap.hydratedAt,
|
|
472
|
+
hasPersistedState: snap.hasPersistedState,
|
|
473
|
+
// [B'.2] persisted kernel mount list — empty before first
|
|
474
|
+
// initSession, populated after.
|
|
475
|
+
mounts,
|
|
476
|
+
// [B'.3] scrollback stats — rows, total bytes, byte cap.
|
|
477
|
+
scrollbackRows: sbStats.rows,
|
|
478
|
+
scrollbackBytes: sbStats.bytes,
|
|
479
|
+
scrollbackMaxBytes: sbStats.maxBytes,
|
|
480
|
+
// [B'.4] live initSession phase. null pre-first-init;
|
|
481
|
+
// 'rehydrate'/'build'/'wire'/'online' during init progress;
|
|
482
|
+
// 'hydrated' after init completes; 'drained' after wsClose.
|
|
483
|
+
phase: self._b4Phase ?? null,
|
|
484
|
+
// [B'.5] count of /ws upgrades that took the warm-rejoin
|
|
485
|
+
// path (Phase B skipped). Probes assert ≥1 after a forced
|
|
486
|
+
// close + reconnect on the same isolate.
|
|
487
|
+
warmJoinCount: self._b4WarmJoinCount ?? 0,
|
|
488
|
+
// Live shell state — useful for confirming the in-memory
|
|
489
|
+
// shell agrees with SQL. Null when no shell is currently
|
|
490
|
+
// attached (between wsClose and next /ws upgrade).
|
|
491
|
+
liveCwd: (() => { try {
|
|
492
|
+
return self.shell?.getCwd() ?? null;
|
|
493
|
+
}
|
|
494
|
+
catch {
|
|
495
|
+
return null;
|
|
496
|
+
} })(),
|
|
497
|
+
liveEnvKeys: (() => {
|
|
498
|
+
try {
|
|
499
|
+
const e = self.shell?.getEnv();
|
|
500
|
+
return e ? Object.keys(e).sort() : null;
|
|
501
|
+
}
|
|
502
|
+
catch {
|
|
503
|
+
return null;
|
|
504
|
+
}
|
|
505
|
+
})(),
|
|
506
|
+
ts: Date.now(),
|
|
507
|
+
});
|
|
508
|
+
}
|
|
509
|
+
// ── [D'.1] /api/_diag/cirrus — cirrus-real DO Facet diagnostics ─────
|
|
510
|
+
//
|
|
511
|
+
// Returns null when cirrus-real is not running (no NIMBUS_REAL_VITE
|
|
512
|
+
// session yet). When running, returns the supervisor-side dispatch
|
|
513
|
+
// shape (kind = 'do-facet') + the in-facet identity cookie (proves
|
|
514
|
+
// own-SQLite is working and survives ctx.facets warm reuse).
|
|
515
|
+
//
|
|
516
|
+
// Probe at audit/probes/d-prime/d1-cirrus-real-facet/ asserts kind
|
|
517
|
+
// and cookie persistence across forced supervisor reconnect.
|
|
518
|
+
if (url.pathname === '/api/_diag/cirrus') {
|
|
519
|
+
if (!self.cirrusReal) {
|
|
520
|
+
return Response.json({ running: false, kind: null });
|
|
521
|
+
}
|
|
522
|
+
try {
|
|
523
|
+
const diag = await self.cirrusReal.getDiag();
|
|
524
|
+
return Response.json({ running: true, ...diag });
|
|
525
|
+
}
|
|
526
|
+
catch (e) {
|
|
527
|
+
return Response.json({ running: true, error: e?.message || String(e) }, { status: 500 });
|
|
528
|
+
}
|
|
529
|
+
}
|
|
530
|
+
// ── W9: hibernation simulation + diagnostic spawn (NIMBUS_DEBUG=1) ──
|
|
531
|
+
//
|
|
532
|
+
// These endpoints exist to let local probes exercise the cross-
|
|
533
|
+
// hibernation code path. Real DO hibernation only happens in prod;
|
|
534
|
+
// wrangler dev keeps state across requests. So we simulate the
|
|
535
|
+
// "fresh isolate per dispatch" rule by clearing the in-memory
|
|
536
|
+
// ProcessLogStore — the next read MUST hydrate from SQL.
|
|
537
|
+
//
|
|
538
|
+
// 404 when NIMBUS_DEBUG isn't set, so prod isn't a free vector.
|
|
539
|
+
if (url.pathname.startsWith('/api/_test/')) {
|
|
540
|
+
if (!self.nimbusDebug) {
|
|
541
|
+
return new Response('not found', { status: 404 });
|
|
542
|
+
}
|
|
543
|
+
if (url.pathname === '/api/_test/hib/simulate' && request.method === 'POST') {
|
|
544
|
+
// Drain any pending writes first so SQL is the source of truth,
|
|
545
|
+
// then nuke the in-memory ring. The next read on any pid will
|
|
546
|
+
// re-hydrate via the adapter.
|
|
547
|
+
try {
|
|
548
|
+
self.processLogs.flush();
|
|
549
|
+
}
|
|
550
|
+
catch { }
|
|
551
|
+
const fresh = new ProcessLogStore();
|
|
552
|
+
// Re-wire persist on the new store (mirrors constructor path).
|
|
553
|
+
self._w9PersistWired = false;
|
|
554
|
+
self.processLogs = fresh;
|
|
555
|
+
self._w9WireProcessLogPersist();
|
|
556
|
+
return Response.json({ cleared: true, ts: Date.now() });
|
|
557
|
+
}
|
|
558
|
+
if (url.pathname === '/api/_test/spawn-emitter' && request.method === 'POST') {
|
|
559
|
+
// Spawns a synthetic emitter directly into ProcessLogStore +
|
|
560
|
+
// ProcessTable without going through FacetManager. Lets the e2e
|
|
561
|
+
// probe drive the W9 code path without a real long-running
|
|
562
|
+
// facet (which the test environment may not support).
|
|
563
|
+
try {
|
|
564
|
+
const body = await request.json();
|
|
565
|
+
const lines = Math.max(1, Math.min(1000, Number(body.lines) || 50));
|
|
566
|
+
const text = String(body.lineText || 'line');
|
|
567
|
+
const entry = self.processTable.spawn(`_test:${text}`, ['_test'], '/');
|
|
568
|
+
const pid = entry.pid;
|
|
569
|
+
for (let i = 0; i < lines; i++) {
|
|
570
|
+
self.processLogs.append(pid, 'stdout', `${text} ${i}\n`);
|
|
571
|
+
}
|
|
572
|
+
// Force-flush so SQL reflects state before the next request.
|
|
573
|
+
try {
|
|
574
|
+
self.processLogs.flush();
|
|
575
|
+
}
|
|
576
|
+
catch { }
|
|
577
|
+
return Response.json({ pid, lines });
|
|
578
|
+
}
|
|
579
|
+
catch (e) {
|
|
580
|
+
return Response.json({ error: e?.message }, { status: 400 });
|
|
581
|
+
}
|
|
582
|
+
}
|
|
583
|
+
if (url.pathname === '/api/_test/log-tail' && request.method === 'GET') {
|
|
584
|
+
const pid = parseInt(url.searchParams.get('pid') || '', 10);
|
|
585
|
+
const linesQ = parseInt(url.searchParams.get('lines') || '0', 10) || undefined;
|
|
586
|
+
if (!Number.isFinite(pid) || pid <= 0) {
|
|
587
|
+
return Response.json({ error: 'bad pid' }, { status: 400 });
|
|
588
|
+
}
|
|
589
|
+
const chunks = self.processLogs.tail(pid, linesQ ? { lines: linesQ } : {});
|
|
590
|
+
const allText = chunks.map((c) => c.data).join('');
|
|
591
|
+
const lines = allText.split('\n').filter((l) => l !== '');
|
|
592
|
+
return Response.json({ pid, lines, chunkCount: chunks.length });
|
|
593
|
+
}
|
|
594
|
+
// ── C'.2 recovery-event test endpoints ────────────────────────────
|
|
595
|
+
// Used by audit/probes/c-prime/recovery-events/ to verify the ring
|
|
596
|
+
// schema works pre-Track-B'. Once Track B' transitions land, real
|
|
597
|
+
// events flow through the ring naturally — these endpoints stay
|
|
598
|
+
// for synthetic-trigger probes (audit/probes/interactive-liveness/
|
|
599
|
+
// error-recovery/).
|
|
600
|
+
if (url.pathname === '/api/_test/recovery-event/record' && request.method === 'POST') {
|
|
601
|
+
const body = await request.json();
|
|
602
|
+
recordRecoveryEvent({
|
|
603
|
+
at: Number(body.at) || Date.now(),
|
|
604
|
+
fromState: String(body.fromState ?? 'cold'),
|
|
605
|
+
toState: String(body.toState ?? 'hydrated'),
|
|
606
|
+
trigger: String(body.trigger ?? 'manual-test'),
|
|
607
|
+
isolateGen: Number(body.isolateGen) || self._w9IsolateGen,
|
|
608
|
+
dataLoss: !!body.dataLoss,
|
|
609
|
+
snapshotKeysRehydrated: Number(body.snapshotKeysRehydrated) || 0,
|
|
610
|
+
notes: body.notes ? String(body.notes) : undefined,
|
|
611
|
+
});
|
|
612
|
+
return Response.json({ recorded: true, ringSize: getRecoveryEvents().length });
|
|
613
|
+
}
|
|
614
|
+
if (url.pathname === '/api/_test/recovery-event/reset' && request.method === 'POST') {
|
|
615
|
+
resetRecoveryEvents();
|
|
616
|
+
return Response.json({ reset: true });
|
|
617
|
+
}
|
|
618
|
+
// ── B'.1 session-state reset ─────────────────────────────────────
|
|
619
|
+
// Drops every nimbus_session_kv / nimbus_kernel_mounts /
|
|
620
|
+
// nimbus_terminal_scrollback row so the next /ws upgrade
|
|
621
|
+
// takes the cold-start path (Phase O fires; banner reprints).
|
|
622
|
+
// Used by probes to start each scenario from a known-empty state.
|
|
623
|
+
if (url.pathname === '/api/_test/session/reset' && request.method === 'POST') {
|
|
624
|
+
clearSessionState(self.ctx);
|
|
625
|
+
return Response.json({ reset: true });
|
|
626
|
+
}
|
|
627
|
+
// ── cache-and-scrub L2 cache benchmark ───────────────────────────
|
|
628
|
+
// Used by audit/probes/cache-and-scrub/* to measure the latency
|
|
629
|
+
// contrast between a cold L3-only path and a warm L2-served
|
|
630
|
+
// path. The probe asserts the warm path is ≥5× faster than the
|
|
631
|
+
// cold path (the wave's hard ship-gate).
|
|
632
|
+
//
|
|
633
|
+
// Pattern (per cache-layer probe):
|
|
634
|
+
// 1. POST /api/_test/cache/<layer>/reset — purge L2 entry
|
|
635
|
+
// 2. POST /api/_test/cache/<layer>/seed — write L3 entry
|
|
636
|
+
// 3. GET /api/_test/cache/<layer>/bench?n=N — N timed reads
|
|
637
|
+
//
|
|
638
|
+
// The bench endpoint returns latencies[] in ms (high-res via
|
|
639
|
+
// `performance.now()`) so the probe can compute median, ratios,
|
|
640
|
+
// hit-flag from response headers, etc.
|
|
641
|
+
if (url.pathname.startsWith('/api/_test/cache/')) {
|
|
642
|
+
return await handleCacheTestEndpoint(self, url, request);
|
|
643
|
+
}
|
|
644
|
+
// ── two-tier-fanout primitive probe ──────────────────────────────
|
|
645
|
+
// Used by audit/probes/two-tier-fanout/* to measure the
|
|
646
|
+
// POC C in-DO and POC B peer-DO speedups via the
|
|
647
|
+
// NimbusFanoutPool primitive. Independent of any specific
|
|
648
|
+
// production site (install-batch, pre-bundle, etc.) so the
|
|
649
|
+
// primitive's behavior can be measured cleanly without
|
|
650
|
+
// confounders.
|
|
651
|
+
if (url.pathname.startsWith('/api/_test/fanout/')) {
|
|
652
|
+
return await handleFanoutTestEndpoint(self, url, request);
|
|
653
|
+
}
|
|
654
|
+
return new Response('unknown _test endpoint', { status: 404 });
|
|
655
|
+
}
|
|
656
|
+
if (url.pathname === '/api/stats') {
|
|
657
|
+
self.ensureSqliteFs();
|
|
658
|
+
const vfsStats = self.sqliteFs.getStats();
|
|
659
|
+
const processStats = self.processTable.stats;
|
|
660
|
+
const logStoreStats = self.processLogs.stats;
|
|
661
|
+
// Preview UI polls vite.running to decide between /preview/ and
|
|
662
|
+
// the "no dev server" placeholder. We report running:true if
|
|
663
|
+
// EITHER the Cirrus in-process ViteDevServer OR the opt-in
|
|
664
|
+
// real-vite facet (cirrusReal) is live. Without this merge, a
|
|
665
|
+
// session on NIMBUS_REAL_VITE=1 saw vite.running=false even
|
|
666
|
+
// while real-vite was happily serving on /preview/.
|
|
667
|
+
const legacyViteStats = self.viteDevServer?.stats || null;
|
|
668
|
+
const cirrusRealRunning = !!self.cirrusReal?.isRunning;
|
|
669
|
+
const viteStats = cirrusRealRunning
|
|
670
|
+
? {
|
|
671
|
+
running: true,
|
|
672
|
+
root: legacyViteStats?.root ?? 'home/user/app',
|
|
673
|
+
backend: 'real',
|
|
674
|
+
}
|
|
675
|
+
: legacyViteStats;
|
|
676
|
+
const wranglerStats = self.nimbusWrangler?.stats || null;
|
|
677
|
+
const portStats = self.portRegistry.stats;
|
|
678
|
+
// Audit C3: same-origin only. The UI shell at /s/<id>/ polls this
|
|
679
|
+
// from its own origin; no cross-origin reader is intended. If
|
|
680
|
+
// future embeds need cross-origin reads, add an explicit origin
|
|
681
|
+
// allowlist — not a wildcard.
|
|
682
|
+
return Response.json({ ...vfsStats, processes: processStats, logStore: logStoreStats, ports: portStats, vite: viteStats, wrangler: wranglerStats });
|
|
683
|
+
}
|
|
684
|
+
// ── File write API: bypasses shell for fast bulk seeding ──
|
|
685
|
+
// Audit C3: mutation endpoints do NOT advertise any CORS policy.
|
|
686
|
+
// A cross-origin page that learns a session ID would otherwise be
|
|
687
|
+
// able to write arbitrary files through the user's logged-in tab.
|
|
688
|
+
// Same-origin POSTs from the session shell still work — SOP (not
|
|
689
|
+
// CORS) governs them, so no preflight is emitted; cross-origin
|
|
690
|
+
// requests are rejected by the browser before reaching the Worker.
|
|
691
|
+
if (url.pathname === '/api/write-file' && request.method === 'POST') {
|
|
692
|
+
self.ensureSqliteFs();
|
|
693
|
+
try {
|
|
694
|
+
const body = await request.json();
|
|
695
|
+
const path = String(body.path).replace(/^\/+/, '');
|
|
696
|
+
// Ensure parent dirs
|
|
697
|
+
const parts = path.split('/');
|
|
698
|
+
for (let i = 1; i < parts.length; i++) {
|
|
699
|
+
const dir = parts.slice(0, i).join('/');
|
|
700
|
+
if (dir && !self.sqliteFs.exists(dir))
|
|
701
|
+
self.sqliteFs.mkdir(dir, { recursive: true });
|
|
702
|
+
}
|
|
703
|
+
self.sqliteFs.writeFile(path, String(body.content));
|
|
704
|
+
return Response.json({ ok: true, path });
|
|
705
|
+
}
|
|
706
|
+
catch (e) {
|
|
707
|
+
return Response.json({ error: e?.message }, { status: 400 });
|
|
708
|
+
}
|
|
709
|
+
}
|
|
710
|
+
if (url.pathname === '/api/mkdir' && request.method === 'POST') {
|
|
711
|
+
self.ensureSqliteFs();
|
|
712
|
+
try {
|
|
713
|
+
const body = await request.json();
|
|
714
|
+
const path = String(body.path).replace(/^\/+/, '');
|
|
715
|
+
self.sqliteFs.mkdir(path, { recursive: true });
|
|
716
|
+
return Response.json({ ok: true, path });
|
|
717
|
+
}
|
|
718
|
+
catch (e) {
|
|
719
|
+
return Response.json({ error: e?.message }, { status: 400 });
|
|
720
|
+
}
|
|
721
|
+
}
|
|
722
|
+
// ── Start vite via HTTP API (survives WS disconnects) ──
|
|
723
|
+
if (url.pathname === '/api/start-vite' && request.method === 'POST') {
|
|
724
|
+
self.ensureSqliteFs();
|
|
725
|
+
try {
|
|
726
|
+
const body = await request.json();
|
|
727
|
+
const root = String(body.root || 'home/user').replace(/^\/+/, '');
|
|
728
|
+
// Stop existing server
|
|
729
|
+
if (self.viteDevServer?.isRunning)
|
|
730
|
+
self.viteDevServer.stop();
|
|
731
|
+
// Start in-process ViteDevServer
|
|
732
|
+
if (!self.esbuildService)
|
|
733
|
+
self.esbuildService = new EsbuildService(self.sqliteFs);
|
|
734
|
+
const basePath = self.viteBasePath;
|
|
735
|
+
// Primitives wave (P5/P8): allocate a PID + port even on the
|
|
736
|
+
// /api/start-vite path so probes that drive vite via the test
|
|
737
|
+
// surface still see a real process in `ps` and stream
|
|
738
|
+
// diagnostics into the Process tab.
|
|
739
|
+
const apiVitePort = (typeof body.port === 'number' && body.port > 0) ? body.port : 5173;
|
|
740
|
+
const apiViteEntry = self.processTable.spawn('vite (api/start-vite, ' + root + ')', [], root);
|
|
741
|
+
self.processTable.setLongRunning(apiViteEntry.pid);
|
|
742
|
+
self.viteDevServer = new ViteDevServer({
|
|
743
|
+
vfs: self.sqliteFs, esbuild: self.esbuildService, root,
|
|
744
|
+
aliases: body.aliases, define: body.define,
|
|
745
|
+
onHmrMessage: () => { },
|
|
746
|
+
sql: self.ctx.storage.sql,
|
|
747
|
+
injectBasename: body.injectBasename,
|
|
748
|
+
basePath,
|
|
749
|
+
// env+ctx enable the on-demand facet bundle path. Without
|
|
750
|
+
// these, ViteDevServer falls back to in-supervisor esbuild
|
|
751
|
+
// for /preview/@modules/<spec> cold-path bundles — which OOMs
|
|
752
|
+
// on large packages (lucide-react). See vite-dev-server.ts:
|
|
753
|
+
// ensureOnDemandPool / serveModule.
|
|
754
|
+
env: self.env,
|
|
755
|
+
ctx: self.ctx,
|
|
756
|
+
port: apiVitePort,
|
|
757
|
+
pid: apiViteEntry.pid,
|
|
758
|
+
processLogs: self.processLogs,
|
|
759
|
+
});
|
|
760
|
+
self.viteDevServer.start();
|
|
761
|
+
try {
|
|
762
|
+
const apiViteStub = makeLongRunningPortStub(self.viteDevServer);
|
|
763
|
+
self.portRegistry.register(apiVitePort, apiViteEntry.pid, apiViteStub);
|
|
764
|
+
self._viteShimPid = apiViteEntry.pid;
|
|
765
|
+
self._viteShimPort = apiVitePort;
|
|
766
|
+
}
|
|
767
|
+
catch { }
|
|
768
|
+
// Persist so vite survives DO hibernation. basePath included so the
|
|
769
|
+
// rehydrated server after DO sleep emits URLs under the same prefix
|
|
770
|
+
// even before the next forwarded request updates sessionBasePath.
|
|
771
|
+
await self.ctx.storage.put('vite-config', { root, aliases: body.aliases, define: body.define, injectBasename: body.injectBasename, basePath });
|
|
772
|
+
return Response.json({ ok: true, root, running: true });
|
|
773
|
+
}
|
|
774
|
+
catch (e) {
|
|
775
|
+
return Response.json({ error: e?.message }, { status: 400 });
|
|
776
|
+
}
|
|
777
|
+
}
|
|
778
|
+
// [cleanup] /api/supervisor-rpc removed. The fetch-based fallback
|
|
779
|
+
// pre-dated the ctx.exports-driven SupervisorRPC class; once D'.1
|
|
780
|
+
// landed (cirrus-real on DO Facet) every facet ↔ supervisor RPC
|
|
781
|
+
// flows through service bindings. The HTTP fallback was quarantined
|
|
782
|
+
// under ARC-A-P3 with on-entry warnings; no caller ever hit it
|
|
783
|
+
// post-rebuild. Removed in this cleanup along with the
|
|
784
|
+
// handleSupervisorRpc body in src/session/supervisor-rpc.ts.
|
|
785
|
+
// CORS preflight for API endpoints (audit C3).
|
|
786
|
+
// Respond 204 with NO Access-Control-Allow-Origin. The browser
|
|
787
|
+
// treats a missing ACAO as "cross-origin denied," which is what
|
|
788
|
+
// we want for every endpoint in this DO: same-origin requests
|
|
789
|
+
// from the session shell skip preflight entirely (SOP governs
|
|
790
|
+
// them, not CORS); cross-origin callers are rejected.
|
|
791
|
+
//
|
|
792
|
+
// This handler matches only /api/* — the /preview, /worker, and
|
|
793
|
+
// /port proxies retain their own header handling (some of those
|
|
794
|
+
// still set wildcard ACAO in sibling modules like vite-dev-server
|
|
795
|
+
// and nimbus-wrangler; tightening those is tracked as follow-up
|
|
796
|
+
// since they serve user-controlled content and require separate
|
|
797
|
+
// review of each consumer).
|
|
798
|
+
if (request.method === 'OPTIONS' && url.pathname.startsWith('/api/')) {
|
|
799
|
+
return new Response(null, { status: 204 });
|
|
800
|
+
}
|
|
801
|
+
// ── Preview route: serves the Vite dev server output ──
|
|
802
|
+
// Uses in-process ViteDevServer (synchronous VFS access + esbuild transforms).
|
|
803
|
+
// This is reliable and avoids facet lifecycle issues.
|
|
804
|
+
if (url.pathname.startsWith('/preview/') || url.pathname === '/preview') {
|
|
805
|
+
// Ensure the starter project exists even if the user hits /preview/
|
|
806
|
+
// before opening a terminal session. Idempotent — no-op if already seeded.
|
|
807
|
+
try {
|
|
808
|
+
self.ensureSqliteFs();
|
|
809
|
+
self.seedFilesystem();
|
|
810
|
+
}
|
|
811
|
+
catch { /* non-fatal */ }
|
|
812
|
+
// Primitive #3 multi-target routing — `/preview/?port=N` lets a
|
|
813
|
+
// session with multiple long-running things (vite + Express, two
|
|
814
|
+
// vites on different ports, …) reach each one without changing
|
|
815
|
+
// the user-facing URL shape.
|
|
816
|
+
//
|
|
817
|
+
// Behaviour:
|
|
818
|
+
// /preview/ → first port in PortRegistry by registration
|
|
819
|
+
// time, OR if a non-port-registered cirrus
|
|
820
|
+
// shim / cirrusReal is live (legacy path),
|
|
821
|
+
// use it directly. The legacy path is the
|
|
822
|
+
// fast path because it avoids stub-rebuild.
|
|
823
|
+
// /preview/?port=N → routeRequest(N, …) regardless of legacy.
|
|
824
|
+
const queryPort = (() => {
|
|
825
|
+
const raw = url.searchParams.get('port');
|
|
826
|
+
if (!raw)
|
|
827
|
+
return null;
|
|
828
|
+
const n = parseInt(raw, 10);
|
|
829
|
+
return Number.isFinite(n) && n > 0 && n < 65536 ? n : null;
|
|
830
|
+
})();
|
|
831
|
+
if (queryPort != null) {
|
|
832
|
+
const previewInner = (url.pathname.replace(/^\/preview/, '') || '/') + (() => {
|
|
833
|
+
// Strip our `?port=N` so the inner handler doesn't re-see it.
|
|
834
|
+
const sp = new URLSearchParams(url.search);
|
|
835
|
+
sp.delete('port');
|
|
836
|
+
const q = sp.toString();
|
|
837
|
+
return q ? '?' + q : '';
|
|
838
|
+
})();
|
|
839
|
+
const proxied = await self.portRegistry.routeRequest(queryPort, request, previewInner);
|
|
840
|
+
if (proxied)
|
|
841
|
+
return proxied;
|
|
842
|
+
return new Response(`No process listening on port ${queryPort}`, { status: 502 });
|
|
843
|
+
}
|
|
844
|
+
// ── Real-vite takes precedence if running ───────────────────────
|
|
845
|
+
// Cirrus shim and real-vite are mutually exclusive per session.
|
|
846
|
+
// cirrusReal is checked first since users explicitly opted in via
|
|
847
|
+
// NIMBUS_REAL_VITE=1 or `nimbusDevServer: 'real'`.
|
|
848
|
+
if (self.cirrusReal?.isRunning) {
|
|
849
|
+
const previewPath = (url.pathname.replace(/^\/preview/, '') || '/') + url.search;
|
|
850
|
+
// Phase 2: HMR WebSocket upgrade. Vite's @vite/client opens a
|
|
851
|
+
// WS against `<base>/__nimbus_hmr` (our custom HMR path). We
|
|
852
|
+
// accept it here and plug it into cirrusReal.hmr so the facet's
|
|
853
|
+
// ws-shim sees a 'connection' event on its next long-poll.
|
|
854
|
+
// Non-hibernatable (server.accept, not ctx.acceptWebSocket) —
|
|
855
|
+
// clients auto-reconnect on DO wake.
|
|
856
|
+
if (previewPath.startsWith('/__nimbus_hmr') || previewPath === '/__nimbus_hmr') {
|
|
857
|
+
if (request.headers.get('Upgrade') !== 'websocket') {
|
|
858
|
+
return new Response('Expected WebSocket', { status: 426 });
|
|
859
|
+
}
|
|
860
|
+
const pair = new WebSocketPair();
|
|
861
|
+
const [client, server] = Object.values(pair);
|
|
862
|
+
// Use ctx.acceptWebSocket (hibernatable) — required because
|
|
863
|
+
// HMR sends messages from a DIFFERENT request context (the
|
|
864
|
+
// facet's long-poll RPC). workerd forbids cross-request I/O
|
|
865
|
+
// on server.accept()'d sockets.
|
|
866
|
+
self.ctx.acceptWebSocket(server, ['cirrus-hmr']);
|
|
867
|
+
const clientId = self.cirrusReal.attachHmrClient(server);
|
|
868
|
+
server.serializeAttachment?.({ kind: 'cirrus-hmr', clientId });
|
|
869
|
+
const hmrClients = (self._cirrusHmrWsClients ||= new Map());
|
|
870
|
+
hmrClients.set(server, clientId);
|
|
871
|
+
// Echo the vite-hmr subprotocol.
|
|
872
|
+
const wantedProto = request.headers.get('Sec-WebSocket-Protocol') || '';
|
|
873
|
+
const useProto = wantedProto.split(',').map(s => s.trim()).find(p => p === 'vite-hmr' || p === 'vite-ping');
|
|
874
|
+
const respHeaders = {};
|
|
875
|
+
if (useProto)
|
|
876
|
+
respHeaders['Sec-WebSocket-Protocol'] = useProto;
|
|
877
|
+
return new Response(null, { status: 101, webSocket: client, headers: respHeaders });
|
|
878
|
+
}
|
|
879
|
+
return self.cirrusReal.handleRequest(request, previewPath);
|
|
880
|
+
}
|
|
881
|
+
// Lazy-init: if DO hibernated and ViteDevServer was GC'd, reconstruct from saved config
|
|
882
|
+
if (!self.viteDevServer || !self.viteDevServer.isRunning) {
|
|
883
|
+
try {
|
|
884
|
+
const config = await self.ctx.storage.get('vite-config');
|
|
885
|
+
if (config?.root) {
|
|
886
|
+
self.ensureSqliteFs();
|
|
887
|
+
if (!self.esbuildService)
|
|
888
|
+
self.esbuildService = new EsbuildService(self.sqliteFs);
|
|
889
|
+
// Prefer the current request's basePath (just captured from the
|
|
890
|
+
// X-Nimbus-Base header) over the stored one — the latter is only
|
|
891
|
+
// a fallback for cold rehydrates that precede any header hit.
|
|
892
|
+
const basePath = self.viteBasePath || config.basePath;
|
|
893
|
+
// Primitives wave (P8): on hibernation rehydrate, re-allocate
|
|
894
|
+
// a PID so log streaming has somewhere to land. Without
|
|
895
|
+
// this, the rehydrated server would be silent again. The
|
|
896
|
+
// PID is registered against the previously-known port (or
|
|
897
|
+
// the default 5173 when the saved config predates P5).
|
|
898
|
+
const rehydratedPort = (config.port && Number.isFinite(config.port)) ? config.port : 5173;
|
|
899
|
+
const rehydratedEntry = self.processTable.spawn('vite (rehydrated, ' + config.root + ')', [], config.root);
|
|
900
|
+
self.processTable.setLongRunning(rehydratedEntry.pid);
|
|
901
|
+
self.viteDevServer = new ViteDevServer({
|
|
902
|
+
vfs: self.sqliteFs, esbuild: self.esbuildService, root: config.root,
|
|
903
|
+
aliases: config.aliases, define: config.define,
|
|
904
|
+
onHmrMessage: () => { },
|
|
905
|
+
sql: self.ctx.storage.sql,
|
|
906
|
+
injectBasename: config.injectBasename,
|
|
907
|
+
basePath,
|
|
908
|
+
env: self.env,
|
|
909
|
+
ctx: self.ctx,
|
|
910
|
+
port: rehydratedPort,
|
|
911
|
+
pid: rehydratedEntry.pid,
|
|
912
|
+
processLogs: self.processLogs,
|
|
913
|
+
});
|
|
914
|
+
self.viteDevServer.start();
|
|
915
|
+
// Re-register the port so /preview/?port=N keeps working
|
|
916
|
+
// across hibernation cycles.
|
|
917
|
+
try {
|
|
918
|
+
const rehydratedStub = makeLongRunningPortStub(self.viteDevServer);
|
|
919
|
+
self.portRegistry.register(rehydratedPort, rehydratedEntry.pid, rehydratedStub);
|
|
920
|
+
self._viteShimPid = rehydratedEntry.pid;
|
|
921
|
+
self._viteShimPort = rehydratedPort;
|
|
922
|
+
}
|
|
923
|
+
catch { /* registry full / unavailable — fall through */ }
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
catch { /* lazy-init failed, fall through to "no server" response */ }
|
|
927
|
+
}
|
|
928
|
+
if (self.viteDevServer?.isRunning) {
|
|
929
|
+
const previewPath = (url.pathname.replace(/^\/preview/, '') || '/') + url.search;
|
|
930
|
+
return self.viteDevServer.handleRequest(request, previewPath);
|
|
931
|
+
}
|
|
932
|
+
// Polished placeholder — auto-reloads when vite starts.
|
|
933
|
+
// Checks the VFS for the starter app so we can offer a context-aware hint.
|
|
934
|
+
const hasSeed = (() => {
|
|
935
|
+
try {
|
|
936
|
+
return self.sqliteFs.exists('home/user/app') &&
|
|
937
|
+
self.sqliteFs.exists('home/user/app/package.json');
|
|
938
|
+
}
|
|
939
|
+
catch {
|
|
940
|
+
return false;
|
|
941
|
+
}
|
|
942
|
+
})();
|
|
943
|
+
const hint = hasSeed
|
|
944
|
+
? 'cd app && npm install && npm run dev'
|
|
945
|
+
: 'vite';
|
|
946
|
+
// The placeholder JS polls the session's /api/stats. If this DO was
|
|
947
|
+
// reached directly (no session prefix), fall back to a relative path.
|
|
948
|
+
const statsUrl = (self.sessionBasePath || '') + '/api/stats';
|
|
949
|
+
return new Response(renderNoDevServerHtml({ hint, polled: statsUrl, liveKey: 'vite' }),
|
|
950
|
+
// Audit C3: HTML served same-origin to the session shell.
|
|
951
|
+
// No wildcard ACAO — the page's own fetch to /api/stats is
|
|
952
|
+
// same-origin and needs no CORS header.
|
|
953
|
+
{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
954
|
+
}
|
|
955
|
+
// ── Worker route: serves the nimbus-wrangler dev worker output ──
|
|
956
|
+
//
|
|
957
|
+
// Primitives wave (P5): the canonical path is `/__nimbus/worker/*`
|
|
958
|
+
// so projects with their own `worker/` directory at root (Markflow,
|
|
959
|
+
// CF Pages projects, …) can serve it via `/preview/worker/*` without
|
|
960
|
+
// collision. The bare `/worker/*` path remains accepted for
|
|
961
|
+
// back-compat — same handler — but the response carries a
|
|
962
|
+
// `Deprecation` and `Sunset` header so callers can migrate. New
|
|
963
|
+
// sessions are encouraged to use the namespaced form.
|
|
964
|
+
const workerPathMatch = url.pathname.startsWith('/__nimbus/worker/') || url.pathname === '/__nimbus/worker' ||
|
|
965
|
+
url.pathname.startsWith('/worker/') || url.pathname === '/worker';
|
|
966
|
+
const isLegacyWorkerPath = url.pathname.startsWith('/worker/') || url.pathname === '/worker';
|
|
967
|
+
if (workerPathMatch) {
|
|
968
|
+
if (!self.nimbusWrangler?.isRunning) {
|
|
969
|
+
// Mirror the polished /preview/ placeholder — auto-reloads when
|
|
970
|
+
// nimbus-wrangler starts. The placeholder references BOTH command
|
|
971
|
+
// names so users coming from either `wrangler dev` or
|
|
972
|
+
// `nimbus-wrangler dev` see a familiar hint.
|
|
973
|
+
const hasWranglerConfig = (() => {
|
|
974
|
+
try {
|
|
975
|
+
self.ensureSqliteFs();
|
|
976
|
+
return self.sqliteFs.exists('home/user/wrangler.jsonc') ||
|
|
977
|
+
self.sqliteFs.exists('home/user/wrangler.json') ||
|
|
978
|
+
self.sqliteFs.exists('home/user/wrangler.toml');
|
|
979
|
+
}
|
|
980
|
+
catch {
|
|
981
|
+
return false;
|
|
982
|
+
}
|
|
983
|
+
})();
|
|
984
|
+
const hint = hasWranglerConfig
|
|
985
|
+
? 'npm run dev'
|
|
986
|
+
: 'wrangler dev';
|
|
987
|
+
return new Response(renderNoDevServerHtml({ hint, polled: (self.sessionBasePath || '') + '/api/stats', liveKey: 'wrangler' }),
|
|
988
|
+
// Audit C3: same-origin HTML, no ACAO needed (see /preview/).
|
|
989
|
+
{ status: 200, headers: { 'Content-Type': 'text/html; charset=utf-8', 'Cache-Control': 'no-store' } });
|
|
990
|
+
}
|
|
991
|
+
// Strip the matched prefix to compute the inner worker path.
|
|
992
|
+
// `/__nimbus/worker/*` and `/worker/*` collapse to the same
|
|
993
|
+
// inner path so the dispatcher logic doesn't need to branch.
|
|
994
|
+
const innerPrefix = isLegacyWorkerPath ? '/worker' : '/__nimbus/worker';
|
|
995
|
+
const workerPath = url.pathname.replace(new RegExp('^' + innerPrefix), '') || '/';
|
|
996
|
+
// Full outer-facing prefix for the proxy (e.g.
|
|
997
|
+
// "/s/nimble-otter-4271/__nimbus/worker"). Used to rewrite
|
|
998
|
+
// Location headers emitted by the inner Worker so cross-redirects
|
|
999
|
+
// (POST /new → /s/<inner>/) land back on the correctly-prefixed
|
|
1000
|
+
// outer URL rather than a bare /s/<inner>/ path that would spawn
|
|
1001
|
+
// a different outer session.
|
|
1002
|
+
const outerWorkerBase = (self.sessionBasePath || '') + innerPrefix;
|
|
1003
|
+
const resp = await self.nimbusWrangler.handleRequest(request, workerPath, outerWorkerBase);
|
|
1004
|
+
if (isLegacyWorkerPath) {
|
|
1005
|
+
// Surface the deprecation in headers without rewriting body —
|
|
1006
|
+
// unobtrusive for browsers, visible to tooling.
|
|
1007
|
+
const newHeaders = new Headers(resp.headers);
|
|
1008
|
+
newHeaders.set('Deprecation', 'true');
|
|
1009
|
+
newHeaders.set('Sunset', 'Wed, 01 Jan 2027 00:00:00 GMT');
|
|
1010
|
+
newHeaders.set('Link', '<' + (self.sessionBasePath || '') + '/__nimbus/worker' + workerPath +
|
|
1011
|
+
'>; rel="successor-version"');
|
|
1012
|
+
return new Response(resp.body, {
|
|
1013
|
+
status: resp.status, statusText: resp.statusText, headers: newHeaders,
|
|
1014
|
+
});
|
|
1015
|
+
}
|
|
1016
|
+
return resp;
|
|
1017
|
+
}
|
|
1018
|
+
// ── Port route: routes to facet HTTP servers ──
|
|
1019
|
+
// Audit F3 (STABILITY-AUDIT.md C-S3): routeRequest now always
|
|
1020
|
+
// returns a Response — 501 when no facet has a stub registered
|
|
1021
|
+
// (the normal case today, since the facet-side producer was
|
|
1022
|
+
// never wired), or a real proxied response once wiring lands.
|
|
1023
|
+
// The post-routeRequest null-branch + 502 fallback below is kept
|
|
1024
|
+
// defensively for any future refactor that returns null for a
|
|
1025
|
+
// different error condition.
|
|
1026
|
+
const portMatch = url.pathname.match(/^\/port\/(\d+)(\/.*)?$/);
|
|
1027
|
+
if (portMatch) {
|
|
1028
|
+
const port = parseInt(portMatch[1]);
|
|
1029
|
+
const path = portMatch[2] || '/';
|
|
1030
|
+
const result = await self.portRegistry.routeRequest(port, request, path);
|
|
1031
|
+
if (result)
|
|
1032
|
+
return result;
|
|
1033
|
+
return new Response(`No process listening on port ${port}`, {
|
|
1034
|
+
status: 502,
|
|
1035
|
+
});
|
|
1036
|
+
}
|
|
1037
|
+
// ── /api/_diag/cache — per-tier cache observability ───────────────
|
|
1038
|
+
//
|
|
1039
|
+
// Cache-observability wave: surfaces the L1/L2/L3/L4 × tarball/
|
|
1040
|
+
// packument/asset hit/miss/bytes grid maintained by
|
|
1041
|
+
// src/_shared/cache-stats.ts. Pre-wave the only observability was
|
|
1042
|
+
// /api/_diag/memory.r2.* (RPC-call level), which conflated L2 and
|
|
1043
|
+
// L3 and missed L1 entirely.
|
|
1044
|
+
//
|
|
1045
|
+
// Added at the END of handleFetch so future wave additions to the
|
|
1046
|
+
// diag cluster (typically inserted near other /api/_diag/* routes)
|
|
1047
|
+
// don't cause merge conflicts in the cache-observability commit.
|
|
1048
|
+
// POST /reset zeros the counters in-place for fresh measurement
|
|
1049
|
+
// windows; the dual-endpoint pattern mirrors /api/_test/* reset
|
|
1050
|
+
// helpers used by the recovery-event probe.
|
|
1051
|
+
if (url.pathname === '/api/_diag/cache' && request.method === 'GET') {
|
|
1052
|
+
const { snapshot } = await import('../_shared/cache-stats.js');
|
|
1053
|
+
const snap = snapshot();
|
|
1054
|
+
return Response.json(snap);
|
|
1055
|
+
}
|
|
1056
|
+
if (url.pathname === '/api/_diag/cache/reset' && request.method === 'POST') {
|
|
1057
|
+
const { reset } = await import('../_shared/cache-stats.js');
|
|
1058
|
+
reset();
|
|
1059
|
+
return new Response(null, { status: 204 });
|
|
1060
|
+
}
|
|
1061
|
+
return new Response('Not found', { status: 404 });
|
|
1062
|
+
}
|
|
1063
|
+
// ── cache-and-scrub L2 benchmark endpoint ───────────────────────────────
|
|
1064
|
+
//
|
|
1065
|
+
// Routes under /api/_test/cache/* exercise the L2 (`caches.default`)
|
|
1066
|
+
// layer that wraps R2 packument/tarball reads + the env.ASSETS
|
|
1067
|
+
// esbuild-wasm fetch. Probes assert the L2 hit path is ≥5× faster
|
|
1068
|
+
// than the cold path (the wave's hard ship-gate).
|
|
1069
|
+
//
|
|
1070
|
+
// Endpoint surface (all NIMBUS_DEBUG-gated by the parent router):
|
|
1071
|
+
//
|
|
1072
|
+
// POST /api/_test/cache/packument/seed {name, payload}
|
|
1073
|
+
// → write the packument to R2 (so cold reads have something to
|
|
1074
|
+
// serve) AND purge any stale L2 entry (so the first bench
|
|
1075
|
+
// read is guaranteed L3-only).
|
|
1076
|
+
// GET /api/_test/cache/packument/bench?name=X&n=N
|
|
1077
|
+
// → run N sequential getPackument(X) calls, return latencies[].
|
|
1078
|
+
// POST /api/_test/cache/tarball/seed {name, version, sizeKb}
|
|
1079
|
+
// → similar; payload is a synthetic Uint8Array of sizeKb*1024.
|
|
1080
|
+
// GET /api/_test/cache/tarball/bench?name=X&version=Y&n=N
|
|
1081
|
+
// → similar.
|
|
1082
|
+
// GET /api/_test/cache/wasm/bench?n=N
|
|
1083
|
+
// → run N sequential fetchEsbuildWasmBytes() calls. The first
|
|
1084
|
+
// is asset-fetch + L2 write; subsequent should hit L2.
|
|
1085
|
+
async function handleCacheTestEndpoint(self, url, request) {
|
|
1086
|
+
const env = self.env;
|
|
1087
|
+
const path = url.pathname;
|
|
1088
|
+
// Build a fresh R2CacheClient bound to the request's env (mirrors
|
|
1089
|
+
// SupervisorRPC._r2 in semantics — graceful-degrade on missing
|
|
1090
|
+
// bindings).
|
|
1091
|
+
const r2 = new R2CacheClient(env?.NPM_TARBALL_CACHE ?? null, env?.NPM_PACKUMENT_CACHE ?? null);
|
|
1092
|
+
const caches = globalThis.caches;
|
|
1093
|
+
const purgeL2 = async (synthUrl) => {
|
|
1094
|
+
try {
|
|
1095
|
+
await caches?.default?.delete(new Request(synthUrl));
|
|
1096
|
+
}
|
|
1097
|
+
catch { }
|
|
1098
|
+
};
|
|
1099
|
+
if (path === '/api/_test/cache/packument/seed' && request.method === 'POST') {
|
|
1100
|
+
const body = await request.json();
|
|
1101
|
+
const name = String(body.name || '');
|
|
1102
|
+
const payload = String(body.payload || JSON.stringify({ name, versions: {} }));
|
|
1103
|
+
if (!name)
|
|
1104
|
+
return Response.json({ error: 'missing name' }, { status: 400 });
|
|
1105
|
+
// Purge L2 first so the next bench read starts from L3 cold.
|
|
1106
|
+
await purgeL2(`${L2_KEY_HOST}/${R2_CACHE_PREFIX}/p/${encodeURIComponent(name)}.json`);
|
|
1107
|
+
const ok = await r2.putPackument(name, payload);
|
|
1108
|
+
return Response.json({ seeded: ok, name, payloadBytes: payload.length });
|
|
1109
|
+
}
|
|
1110
|
+
if (path === '/api/_test/cache/packument/bench' && request.method === 'GET') {
|
|
1111
|
+
const name = url.searchParams.get('name') ?? '';
|
|
1112
|
+
const n = Math.max(1, Math.min(20, parseInt(url.searchParams.get('n') || '5', 10)));
|
|
1113
|
+
if (!name)
|
|
1114
|
+
return Response.json({ error: 'missing name' }, { status: 400 });
|
|
1115
|
+
const latencies = [];
|
|
1116
|
+
let lastBytes = 0;
|
|
1117
|
+
let nullCount = 0;
|
|
1118
|
+
for (let i = 0; i < n; i++) {
|
|
1119
|
+
const t0 = performance.now();
|
|
1120
|
+
const got = await r2.getPackument(name);
|
|
1121
|
+
const t1 = performance.now();
|
|
1122
|
+
latencies.push(t1 - t0);
|
|
1123
|
+
if (!got)
|
|
1124
|
+
nullCount++;
|
|
1125
|
+
else
|
|
1126
|
+
lastBytes = got.json.length;
|
|
1127
|
+
}
|
|
1128
|
+
// R2CacheClient instance was constructed at the top of this
|
|
1129
|
+
// handler; counters reflect the N calls just made.
|
|
1130
|
+
const stats = r2.stats();
|
|
1131
|
+
return Response.json({ name, n, latencies, lastBytes, nullCount, stats });
|
|
1132
|
+
}
|
|
1133
|
+
if (path === '/api/_test/cache/tarball/seed' && request.method === 'POST') {
|
|
1134
|
+
const body = await request.json();
|
|
1135
|
+
const name = String(body.name || '');
|
|
1136
|
+
const version = String(body.version || '');
|
|
1137
|
+
const sizeKb = Math.max(1, Math.min(15360, Number(body.sizeKb) || 16)); // up to 15 MiB (under MAX_R2_TARBALL_BYTES = 30 MiB)
|
|
1138
|
+
if (!name || !version)
|
|
1139
|
+
return Response.json({ error: 'missing name/version' }, { status: 400 });
|
|
1140
|
+
await purgeL2(`${L2_KEY_HOST}/${R2_CACHE_PREFIX}/t/${encodeURIComponent(name)}/${encodeURIComponent(version)}.tgz`);
|
|
1141
|
+
// Synthetic payload — bytes are arbitrary; the cache layer doesn't
|
|
1142
|
+
// care about content. Probe just measures fetch latency.
|
|
1143
|
+
const bytes = new Uint8Array(sizeKb * 1024);
|
|
1144
|
+
for (let i = 0; i < bytes.length; i++)
|
|
1145
|
+
bytes[i] = i & 0xff;
|
|
1146
|
+
const ok = await r2.putTarball(name, version, bytes);
|
|
1147
|
+
return Response.json({ seeded: ok, name, version, sizeBytes: bytes.length });
|
|
1148
|
+
}
|
|
1149
|
+
if (path === '/api/_test/cache/tarball/bench' && request.method === 'GET') {
|
|
1150
|
+
const name = url.searchParams.get('name') ?? '';
|
|
1151
|
+
const version = url.searchParams.get('version') ?? '';
|
|
1152
|
+
const n = Math.max(1, Math.min(20, parseInt(url.searchParams.get('n') || '5', 10)));
|
|
1153
|
+
if (!name || !version)
|
|
1154
|
+
return Response.json({ error: 'missing name/version' }, { status: 400 });
|
|
1155
|
+
const latencies = [];
|
|
1156
|
+
let lastBytes = 0;
|
|
1157
|
+
let nullCount = 0;
|
|
1158
|
+
for (let i = 0; i < n; i++) {
|
|
1159
|
+
const t0 = performance.now();
|
|
1160
|
+
const got = await r2.getTarball(name, version);
|
|
1161
|
+
const t1 = performance.now();
|
|
1162
|
+
latencies.push(t1 - t0);
|
|
1163
|
+
if (!got)
|
|
1164
|
+
nullCount++;
|
|
1165
|
+
else
|
|
1166
|
+
lastBytes = got.length;
|
|
1167
|
+
}
|
|
1168
|
+
const stats = r2.stats();
|
|
1169
|
+
return Response.json({ name, version, n, latencies, lastBytes, nullCount, stats });
|
|
1170
|
+
}
|
|
1171
|
+
if (path === '/api/_test/cache/wasm/reset' && request.method === 'POST') {
|
|
1172
|
+
// Purge the L2 entry so the next bench call goes cold (re-runs
|
|
1173
|
+
// env.ASSETS.fetch + L2 write-back). The L2 key is exported
|
|
1174
|
+
// from esbuild-wasm-bytes.ts so the test endpoint stays in
|
|
1175
|
+
// lockstep with the runtime module's key shape across any
|
|
1176
|
+
// future ESBUILD_VERSION bump.
|
|
1177
|
+
await purgeL2(ESBUILD_WASM_L2_KEY);
|
|
1178
|
+
return Response.json({ purged: true });
|
|
1179
|
+
}
|
|
1180
|
+
if (path === '/api/_test/cache/wasm/bench' && request.method === 'GET') {
|
|
1181
|
+
const n = Math.max(1, Math.min(10, parseInt(url.searchParams.get('n') || '3', 10)));
|
|
1182
|
+
const latencies = [];
|
|
1183
|
+
let lastBytes = 0;
|
|
1184
|
+
for (let i = 0; i < n; i++) {
|
|
1185
|
+
const t0 = performance.now();
|
|
1186
|
+
const ab = await fetchEsbuildWasmBytes(env);
|
|
1187
|
+
const t1 = performance.now();
|
|
1188
|
+
latencies.push(t1 - t0);
|
|
1189
|
+
lastBytes = ab.byteLength;
|
|
1190
|
+
}
|
|
1191
|
+
return Response.json({ n, latencies, lastBytes });
|
|
1192
|
+
}
|
|
1193
|
+
return new Response('unknown cache _test endpoint', { status: 404 });
|
|
1194
|
+
}
|
|
1195
|
+
// ── two-tier-fanout primitive benchmark endpoint ────────────────────────
|
|
1196
|
+
//
|
|
1197
|
+
// Routes under /api/_test/fanout/* exercise NimbusFanoutPool's two
|
|
1198
|
+
// topologies (POC C in-DO + POC B peer-DO) via a synthetic workload
|
|
1199
|
+
// that's independent of the production install-batch / pre-bundle
|
|
1200
|
+
// sites. The probe measures speedup, peer-DO routing determinism,
|
|
1201
|
+
// and backpressure behavior in isolation.
|
|
1202
|
+
//
|
|
1203
|
+
// Endpoint surface (all NIMBUS_DEBUG-gated by the parent router):
|
|
1204
|
+
//
|
|
1205
|
+
// GET /api/_test/fanout/topology?n=N
|
|
1206
|
+
// → returns which topology N would route to (no dispatch).
|
|
1207
|
+
// GET /api/_test/fanout/route?n=N&keys=k1,k2,...
|
|
1208
|
+
// → returns the deterministic peer-DO sibling-id per key.
|
|
1209
|
+
// POST /api/_test/fanout/bench {n, sleepMs}
|
|
1210
|
+
// → runs N synthetic tasks, each sleeping `sleepMs` inside
|
|
1211
|
+
// its loader isolate. Returns total wall time + per-peer
|
|
1212
|
+
// ledger of how many tasks each peer DO handled.
|
|
1213
|
+
// POST /api/_test/fanout/serial-bench {n, sleepMs}
|
|
1214
|
+
// → runs N synthetic tasks SERIALLY (concurrency=1) inside
|
|
1215
|
+
// ONE loader isolate. Used to compute T_serial for the
|
|
1216
|
+
// 5× speedup assertion.
|
|
1217
|
+
//
|
|
1218
|
+
// The synthetic worker function intentionally doesn't import npm
|
|
1219
|
+
// packages or do real I/O — it just sleeps, so the parallelism
|
|
1220
|
+
// floor is the loader/RPC overhead, not network jitter.
|
|
1221
|
+
async function handleFanoutTestEndpoint(self, url, request) {
|
|
1222
|
+
const env = self.env;
|
|
1223
|
+
const path = url.pathname;
|
|
1224
|
+
if (path === '/api/_test/fanout/topology' && request.method === 'GET') {
|
|
1225
|
+
const n = Math.max(0, parseInt(url.searchParams.get('n') || '0', 10));
|
|
1226
|
+
return Response.json({
|
|
1227
|
+
n,
|
|
1228
|
+
topology: n === 0 ? 'empty' : (n < IN_DO_THRESHOLD ? 'in-do' : 'peer-do'),
|
|
1229
|
+
inDoThreshold: IN_DO_THRESHOLD,
|
|
1230
|
+
maxPeerFanout: MAX_PEER_FANOUT,
|
|
1231
|
+
});
|
|
1232
|
+
}
|
|
1233
|
+
if (path === '/api/_test/fanout/route' && request.method === 'GET') {
|
|
1234
|
+
const keysRaw = url.searchParams.get('keys') || '';
|
|
1235
|
+
const keys = keysRaw.split(',').map((k) => k.trim()).filter(Boolean);
|
|
1236
|
+
const peerCount = Math.max(1, Math.min(parseInt(url.searchParams.get('n') || String(keys.length), 10), MAX_PEER_FANOUT));
|
|
1237
|
+
const placement = keys.map((k) => ({
|
|
1238
|
+
key: k,
|
|
1239
|
+
shard: hashKeyToShard(k, peerCount),
|
|
1240
|
+
}));
|
|
1241
|
+
return Response.json({ peerCount, placement });
|
|
1242
|
+
}
|
|
1243
|
+
if (path === '/api/_test/fanout/bench' && request.method === 'POST') {
|
|
1244
|
+
const body = await request.json();
|
|
1245
|
+
const n = Math.max(1, Math.min(64, Number(body.n) || 8));
|
|
1246
|
+
const sleepMs = Math.max(0, Math.min(2000, Number(body.sleepMs) || 100));
|
|
1247
|
+
const pool = new NimbusFanoutPool(env, self.ctx, {
|
|
1248
|
+
tag: 'fanout-bench',
|
|
1249
|
+
timeoutMs: 60_000,
|
|
1250
|
+
});
|
|
1251
|
+
const tasks = Array.from({ length: n }, (_, i) => ({
|
|
1252
|
+
key: `task-${i}`,
|
|
1253
|
+
args: { id: i, sleepMs },
|
|
1254
|
+
}));
|
|
1255
|
+
const t0 = performance.now();
|
|
1256
|
+
// The function runs INSIDE each loader isolate; we use Date.now()
|
|
1257
|
+
// (millisecond resolution is fine; we're sleeping for ms-scale)
|
|
1258
|
+
// to record per-task start/end so the supervisor can compute the
|
|
1259
|
+
// distribution after the fact.
|
|
1260
|
+
const results = await pool.submitMany(tasks, async (item, env) => {
|
|
1261
|
+
const startMs = Date.now();
|
|
1262
|
+
// Identify which env we're running in. SUPERVISOR is the
|
|
1263
|
+
// RPC stub auto-injected by NimbusLoaderPool; its presence
|
|
1264
|
+
// tells us we're inside a loader isolate (not the supervisor).
|
|
1265
|
+
const loaderEnvKeys = Object.keys(env || {}).sort();
|
|
1266
|
+
// Sleep entirely inside the isolate — no external network.
|
|
1267
|
+
await new Promise((r) => setTimeout(r, item.sleepMs));
|
|
1268
|
+
const endMs = Date.now();
|
|
1269
|
+
return { id: item.id, startMs, endMs, loaderEnvKeys };
|
|
1270
|
+
});
|
|
1271
|
+
const t1 = performance.now();
|
|
1272
|
+
// Aggregate per-peer ledger from the response shape. Each task's
|
|
1273
|
+
// result includes its loaderEnvKeys; the SUPERVISOR binding's
|
|
1274
|
+
// doId is observable to confirm peer routing (if peer-DO topology
|
|
1275
|
+
// is in use, each task's SUPERVISOR.doId differs from the
|
|
1276
|
+
// coordinator's). We don't expose doId here directly — we rely
|
|
1277
|
+
// on hashKeyToShard predicting placement and inspecting the
|
|
1278
|
+
// overlap in start/end timestamps to infer parallelism.
|
|
1279
|
+
const startTimes = results.map((r) => r.startMs);
|
|
1280
|
+
const endTimes = results.map((r) => r.endMs);
|
|
1281
|
+
const minStart = Math.min(...startTimes);
|
|
1282
|
+
const maxEnd = Math.max(...endTimes);
|
|
1283
|
+
const totalDurations = results.map((r) => r.endMs - r.startMs);
|
|
1284
|
+
return Response.json({
|
|
1285
|
+
n,
|
|
1286
|
+
sleepMs,
|
|
1287
|
+
wallTimeMs: t1 - t0,
|
|
1288
|
+
results,
|
|
1289
|
+
analysis: {
|
|
1290
|
+
minStart,
|
|
1291
|
+
maxEnd,
|
|
1292
|
+
spanMs: maxEnd - minStart,
|
|
1293
|
+
sumDurations: totalDurations.reduce((a, b) => a + b, 0),
|
|
1294
|
+
topology: n < IN_DO_THRESHOLD ? 'in-do' : 'peer-do',
|
|
1295
|
+
},
|
|
1296
|
+
});
|
|
1297
|
+
}
|
|
1298
|
+
if (path === '/api/_test/fanout/serial-bench' && request.method === 'POST') {
|
|
1299
|
+
const body = await request.json();
|
|
1300
|
+
const n = Math.max(1, Math.min(64, Number(body.n) || 8));
|
|
1301
|
+
const sleepMs = Math.max(0, Math.min(2000, Number(body.sleepMs) || 100));
|
|
1302
|
+
// Same workload, but FORCE serial dispatch by using a single
|
|
1303
|
+
// NimbusLoaderPool with concurrency=1 and submitting one task
|
|
1304
|
+
// at a time. This is the T_serial reference for the 5× speedup
|
|
1305
|
+
// assertion.
|
|
1306
|
+
const { NimbusLoaderPool } = await import('../loaders/loader-pool.js');
|
|
1307
|
+
const pool = new NimbusLoaderPool(env, self.ctx, {
|
|
1308
|
+
concurrency: 1,
|
|
1309
|
+
timeoutMs: 60_000,
|
|
1310
|
+
tag: 'fanout-serial',
|
|
1311
|
+
});
|
|
1312
|
+
const t0 = performance.now();
|
|
1313
|
+
try {
|
|
1314
|
+
for (let i = 0; i < n; i++) {
|
|
1315
|
+
await pool.submit(async (item) => {
|
|
1316
|
+
await new Promise((r) => setTimeout(r, item.sleepMs));
|
|
1317
|
+
return item.id;
|
|
1318
|
+
}, { id: i, sleepMs });
|
|
1319
|
+
}
|
|
1320
|
+
}
|
|
1321
|
+
finally {
|
|
1322
|
+
try {
|
|
1323
|
+
pool.dispose();
|
|
1324
|
+
}
|
|
1325
|
+
catch { }
|
|
1326
|
+
}
|
|
1327
|
+
const t1 = performance.now();
|
|
1328
|
+
return Response.json({ n, sleepMs, wallTimeMs: t1 - t0 });
|
|
1329
|
+
}
|
|
1330
|
+
return new Response('unknown fanout _test endpoint', { status: 404 });
|
|
1331
|
+
}
|