@nimbus-sh/core 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/dist/_shared/ai-egress.d.ts +73 -0
- package/dist/_shared/ai-egress.d.ts.map +1 -0
- package/dist/_shared/ai-egress.js +88 -0
- package/dist/_shared/byte-stream.d.ts +59 -0
- package/dist/_shared/byte-stream.d.ts.map +1 -0
- package/dist/_shared/byte-stream.js +77 -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 +145 -0
- package/dist/_shared/cache-stats.d.ts.map +1 -0
- package/dist/_shared/cache-stats.js +161 -0
- package/dist/_shared/crypto.d.ts +17 -0
- package/dist/_shared/crypto.d.ts.map +1 -0
- package/dist/_shared/crypto.js +131 -0
- package/dist/_shared/esbuild-facet-shim.d.ts +21 -0
- package/dist/_shared/esbuild-facet-shim.d.ts.map +1 -0
- package/dist/_shared/esbuild-facet-shim.js +22 -0
- package/dist/_shared/exports-resolver.d.ts +74 -0
- package/dist/_shared/exports-resolver.d.ts.map +1 -0
- package/dist/_shared/exports-resolver.js +284 -0
- package/dist/_shared/facet-vfs-cursor.d.ts +48 -0
- package/dist/_shared/facet-vfs-cursor.d.ts.map +1 -0
- package/dist/_shared/facet-vfs-cursor.js +51 -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/lru-map.d.ts +25 -0
- package/dist/_shared/lru-map.d.ts.map +1 -0
- package/dist/_shared/lru-map.js +57 -0
- package/dist/_shared/real-node-imports.d.ts +43 -0
- package/dist/_shared/real-node-imports.d.ts.map +1 -0
- package/dist/_shared/real-node-imports.js +52 -0
- package/dist/_shared/retry.d.ts +92 -0
- package/dist/_shared/retry.d.ts.map +1 -0
- package/dist/_shared/retry.js +155 -0
- package/dist/_shared/rpc-dispose.d.ts +4 -0
- package/dist/_shared/rpc-dispose.d.ts.map +1 -0
- package/dist/_shared/rpc-dispose.js +30 -0
- package/dist/_shared/terminal.d.ts +2 -0
- package/dist/_shared/terminal.d.ts.map +1 -0
- package/dist/_shared/terminal.js +3 -0
- package/dist/_shared/typescript-specifiers.d.ts +58 -0
- package/dist/_shared/typescript-specifiers.d.ts.map +1 -0
- package/dist/_shared/typescript-specifiers.js +84 -0
- package/dist/_shared/untrusted-request.d.ts +34 -0
- package/dist/_shared/untrusted-request.d.ts.map +1 -0
- package/dist/_shared/untrusted-request.js +74 -0
- package/dist/_shared/vfs-write-ledger.d.ts +3 -0
- package/dist/_shared/vfs-write-ledger.d.ts.map +1 -0
- package/dist/_shared/vfs-write-ledger.js +439 -0
- package/dist/_shared/w7-frame.d.ts +79 -0
- package/dist/_shared/w7-frame.d.ts.map +1 -0
- package/dist/_shared/w7-frame.js +839 -0
- package/dist/_shared/weighted-credit-pool.d.ts +61 -0
- package/dist/_shared/weighted-credit-pool.d.ts.map +1 -0
- package/dist/_shared/weighted-credit-pool.js +196 -0
- package/dist/constants.d.ts +64 -0
- package/dist/constants.d.ts.map +1 -0
- package/dist/constants.js +264 -0
- package/dist/index.d.ts +16 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +13 -0
- package/dist/observability/diag-counters.d.ts +236 -0
- package/dist/observability/diag-counters.d.ts.map +1 -0
- package/dist/observability/diag-counters.js +195 -0
- package/dist/observability/heap-estimate.d.ts +225 -0
- package/dist/observability/heap-estimate.d.ts.map +1 -0
- package/dist/observability/heap-estimate.js +185 -0
- package/dist/observability/heavy-alloc-coord.d.ts +93 -0
- package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
- package/dist/observability/heavy-alloc-coord.js +181 -0
- package/dist/observability/oom-classify.d.ts +138 -0
- package/dist/observability/oom-classify.d.ts.map +1 -0
- package/dist/observability/oom-classify.js +261 -0
- package/dist/observability/oom-discriminator.d.ts +176 -0
- package/dist/observability/oom-discriminator.d.ts.map +1 -0
- package/dist/observability/oom-discriminator.js +272 -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 +139 -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/bash/preamble.d.ts +2 -0
- package/dist/runtime/bash/preamble.d.ts.map +1 -0
- package/dist/runtime/bash/preamble.js +1849 -0
- package/dist/runtime/bash/types.d.ts +433 -0
- package/dist/runtime/bash/types.d.ts.map +1 -0
- package/dist/runtime/bash/types.js +1 -0
- package/dist/runtime/bundle-profile.d.ts +14 -0
- package/dist/runtime/bundle-profile.d.ts.map +1 -0
- package/dist/runtime/bundle-profile.js +20 -0
- package/dist/runtime/cirrus-vite-config-rewriter.d.ts +2 -0
- package/dist/runtime/cirrus-vite-config-rewriter.d.ts.map +1 -0
- package/dist/runtime/cirrus-vite-config-rewriter.js +28 -0
- package/dist/runtime/cli-flags.d.ts +2 -0
- package/dist/runtime/cli-flags.d.ts.map +1 -0
- package/dist/runtime/cli-flags.js +11 -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/cpython-preamble.d.ts +32 -0
- package/dist/runtime/cpython-preamble.d.ts.map +1 -0
- package/dist/runtime/cpython-preamble.js +296 -0
- package/dist/runtime/esbuild-service.d.ts +242 -0
- package/dist/runtime/esbuild-service.d.ts.map +1 -0
- package/dist/runtime/esbuild-service.js +1452 -0
- package/dist/runtime/framework-detect.d.ts +70 -0
- package/dist/runtime/framework-detect.d.ts.map +1 -0
- package/dist/runtime/framework-detect.js +177 -0
- package/dist/runtime/import-meta-transform.d.ts +3 -0
- package/dist/runtime/import-meta-transform.d.ts.map +1 -0
- package/dist/runtime/import-meta-transform.js +15 -0
- package/dist/runtime/javascript-ast.d.ts +13 -0
- package/dist/runtime/javascript-ast.d.ts.map +1 -0
- package/dist/runtime/javascript-ast.js +57 -0
- package/dist/runtime/long-running-handle.d.ts +119 -0
- package/dist/runtime/long-running-handle.d.ts.map +1 -0
- package/dist/runtime/long-running-handle.js +169 -0
- package/dist/runtime/module-source-rewriter.d.ts +11 -0
- package/dist/runtime/module-source-rewriter.d.ts.map +1 -0
- package/dist/runtime/module-source-rewriter.js +61 -0
- package/dist/runtime/opentui-wasm-backend.d.ts +160 -0
- package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -0
- package/dist/runtime/opentui-wasm-backend.js +416 -0
- package/dist/runtime/os-contracts.d.ts +436 -0
- package/dist/runtime/os-contracts.d.ts.map +1 -0
- package/dist/runtime/os-contracts.js +101 -0
- package/dist/runtime/port-registry.d.ts +100 -0
- package/dist/runtime/port-registry.d.ts.map +1 -0
- package/dist/runtime/port-registry.js +265 -0
- package/dist/runtime/process-input-routing.d.ts +33 -0
- package/dist/runtime/process-input-routing.d.ts.map +1 -0
- package/dist/runtime/process-input-routing.js +59 -0
- package/dist/runtime/process-input.d.ts +39 -0
- package/dist/runtime/process-input.d.ts.map +1 -0
- package/dist/runtime/process-input.js +117 -0
- package/dist/runtime/process-io-protocol.d.ts +90 -0
- package/dist/runtime/process-io-protocol.d.ts.map +1 -0
- package/dist/runtime/process-io-protocol.js +81 -0
- package/dist/runtime/process-logs.d.ts +352 -0
- package/dist/runtime/process-logs.d.ts.map +1 -0
- package/dist/runtime/process-logs.js +750 -0
- package/dist/runtime/process-table.d.ts +101 -0
- package/dist/runtime/process-table.d.ts.map +1 -0
- package/dist/runtime/process-table.js +175 -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-server-adapter.d.ts +33 -0
- package/dist/runtime/python-server-adapter.d.ts.map +1 -0
- package/dist/runtime/python-server-adapter.js +132 -0
- package/dist/runtime/require-resolver.d.ts +44 -0
- package/dist/runtime/require-resolver.d.ts.map +1 -0
- package/dist/runtime/require-resolver.js +698 -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-green-threads.d.ts +136 -0
- package/dist/runtime/ruby-green-threads.d.ts.map +1 -0
- package/dist/runtime/ruby-green-threads.js +904 -0
- package/dist/runtime/ruby-socket-shim.d.ts +2 -0
- package/dist/runtime/ruby-socket-shim.d.ts.map +1 -0
- package/dist/runtime/ruby-socket-shim.js +548 -0
- package/dist/runtime/session-process-supervisor.d.ts +156 -0
- package/dist/runtime/session-process-supervisor.d.ts.map +1 -0
- package/dist/runtime/session-process-supervisor.js +238 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +70 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
- package/dist/runtime/sqlite-runtime-fs-bridge.js +425 -0
- package/dist/runtime/streams.d.ts +13 -0
- package/dist/runtime/streams.d.ts.map +1 -0
- package/dist/runtime/streams.js +546 -0
- package/dist/runtime/undici-shim.d.ts +62 -0
- package/dist/runtime/undici-shim.d.ts.map +1 -0
- package/dist/runtime/undici-shim.js +516 -0
- package/dist/runtime/vfs-manifest.d.ts +63 -0
- package/dist/runtime/vfs-manifest.d.ts.map +1 -0
- package/dist/runtime/vfs-manifest.js +141 -0
- package/dist/runtime/vfs-snapshot.d.ts +75 -0
- package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
- package/dist/runtime/vfs-snapshot.js +220 -0
- package/dist/runtime/virtual-socket-kernel.d.ts +252 -0
- package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
- package/dist/runtime/virtual-socket-kernel.js +1143 -0
- package/dist/runtime/vite-config-parser.d.ts +13 -0
- package/dist/runtime/vite-config-parser.d.ts.map +1 -0
- package/dist/runtime/vite-config-parser.js +258 -0
- package/dist/runtime/wasi/types.d.ts +347 -0
- package/dist/runtime/wasi/types.d.ts.map +1 -0
- package/dist/runtime/wasi/types.js +1 -0
- package/dist/runtime/wasi-instance.d.ts +215 -0
- package/dist/runtime/wasi-instance.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.generated.d.ts +14 -0
- package/dist/runtime/wasi-instance.generated.d.ts.map +1 -0
- package/dist/runtime/wasi-instance.generated.js +13 -0
- package/dist/runtime/wasi-instance.js +179 -0
- package/dist/runtime/wasi-threads.d.ts +169 -0
- package/dist/runtime/wasi-threads.d.ts.map +1 -0
- package/dist/runtime/wasi-threads.js +669 -0
- package/dist/runtime/wasm-memory.d.ts +84 -0
- package/dist/runtime/wasm-memory.d.ts.map +1 -0
- package/dist/runtime/wasm-memory.js +235 -0
- package/dist/shell/elevation-commands.d.ts +5 -0
- package/dist/shell/elevation-commands.d.ts.map +1 -0
- package/dist/shell/elevation-commands.js +109 -0
- package/dist/shell/exec-dispatch.d.ts +57 -0
- package/dist/shell/exec-dispatch.d.ts.map +1 -0
- package/dist/shell/exec-dispatch.js +181 -0
- package/dist/shell/features.d.ts +154 -0
- package/dist/shell/features.d.ts.map +1 -0
- package/dist/shell/features.js +792 -0
- package/dist/shell/shell-entrypoints.d.ts +55 -0
- package/dist/shell/shell-entrypoints.d.ts.map +1 -0
- package/dist/shell/shell-entrypoints.js +230 -0
- package/dist/shell/shell-invocation.d.ts +36 -0
- package/dist/shell/shell-invocation.d.ts.map +1 -0
- package/dist/shell/shell-invocation.js +148 -0
- package/dist/shell/unix-accounts.d.ts +27 -0
- package/dist/shell/unix-accounts.d.ts.map +1 -0
- package/dist/shell/unix-accounts.js +105 -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 +4702 -0
- package/dist/substrate/lifo/commands/archive/gunzip.d.ts +4 -0
- package/dist/substrate/lifo/commands/archive/gunzip.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/archive/gunzip.js +58 -0
- package/dist/substrate/lifo/commands/archive/gzip.d.ts +4 -0
- package/dist/substrate/lifo/commands/archive/gzip.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/archive/gzip.js +73 -0
- package/dist/substrate/lifo/commands/archive/tar.d.ts +4 -0
- package/dist/substrate/lifo/commands/archive/tar.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/archive/tar.js +158 -0
- package/dist/substrate/lifo/commands/archive/unzip.d.ts +4 -0
- package/dist/substrate/lifo/commands/archive/unzip.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/archive/unzip.js +114 -0
- package/dist/substrate/lifo/commands/archive/zip.d.ts +4 -0
- package/dist/substrate/lifo/commands/archive/zip.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/archive/zip.js +58 -0
- package/dist/substrate/lifo/commands/fs/basename.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/basename.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/basename.js +12 -0
- package/dist/substrate/lifo/commands/fs/cat.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/cat.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/cat.js +52 -0
- package/dist/substrate/lifo/commands/fs/chmod.d.ts +25 -0
- package/dist/substrate/lifo/commands/fs/chmod.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/chmod.js +97 -0
- package/dist/substrate/lifo/commands/fs/chown.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/chown.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/chown.js +41 -0
- package/dist/substrate/lifo/commands/fs/cp.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/cp.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/cp.js +29 -0
- package/dist/substrate/lifo/commands/fs/df.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/df.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/df.js +52 -0
- package/dist/substrate/lifo/commands/fs/dirname.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/dirname.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/dirname.js +12 -0
- package/dist/substrate/lifo/commands/fs/du.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/du.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/du.js +104 -0
- package/dist/substrate/lifo/commands/fs/file.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/file.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/file.js +84 -0
- package/dist/substrate/lifo/commands/fs/find.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/find.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/find.js +72 -0
- package/dist/substrate/lifo/commands/fs/ln.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/ln.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/ln.js +15 -0
- package/dist/substrate/lifo/commands/fs/ls.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/ls.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/ls.js +146 -0
- package/dist/substrate/lifo/commands/fs/mkdir.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/mkdir.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/mkdir.js +31 -0
- package/dist/substrate/lifo/commands/fs/mktemp.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/mktemp.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/mktemp.js +48 -0
- package/dist/substrate/lifo/commands/fs/mv.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/mv.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/mv.js +113 -0
- package/dist/substrate/lifo/commands/fs/realpath.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/realpath.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/realpath.js +27 -0
- package/dist/substrate/lifo/commands/fs/rm.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/rm.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/rm.js +48 -0
- package/dist/substrate/lifo/commands/fs/rmdir.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/rmdir.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/rmdir.js +44 -0
- package/dist/substrate/lifo/commands/fs/stat.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/stat.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/stat.js +35 -0
- package/dist/substrate/lifo/commands/fs/touch.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/touch.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/touch.js +26 -0
- package/dist/substrate/lifo/commands/fs/tree.d.ts +4 -0
- package/dist/substrate/lifo/commands/fs/tree.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/fs/tree.js +69 -0
- package/dist/substrate/lifo/commands/index.d.ts +3 -0
- package/dist/substrate/lifo/commands/index.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/index.js +1 -0
- package/dist/substrate/lifo/commands/io/dd.d.ts +4 -0
- package/dist/substrate/lifo/commands/io/dd.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/io/dd.js +256 -0
- package/dist/substrate/lifo/commands/io/printf.d.ts +4 -0
- package/dist/substrate/lifo/commands/io/printf.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/io/printf.js +82 -0
- package/dist/substrate/lifo/commands/io/tee.d.ts +4 -0
- package/dist/substrate/lifo/commands/io/tee.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/io/tee.js +31 -0
- package/dist/substrate/lifo/commands/io/xargs.d.ts +4 -0
- package/dist/substrate/lifo/commands/io/xargs.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/io/xargs.js +44 -0
- package/dist/substrate/lifo/commands/io/yes.d.ts +4 -0
- package/dist/substrate/lifo/commands/io/yes.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/io/yes.js +10 -0
- package/dist/substrate/lifo/commands/net/curl.d.ts +6 -0
- package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/curl.js +617 -0
- package/dist/substrate/lifo/commands/net/dig.d.ts +4 -0
- package/dist/substrate/lifo/commands/net/dig.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/dig.js +77 -0
- package/dist/substrate/lifo/commands/net/forward.d.ts +14 -0
- package/dist/substrate/lifo/commands/net/forward.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/forward.js +80 -0
- package/dist/substrate/lifo/commands/net/host.d.ts +14 -0
- package/dist/substrate/lifo/commands/net/host.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/host.js +160 -0
- package/dist/substrate/lifo/commands/net/ifconfig.d.ts +8 -0
- package/dist/substrate/lifo/commands/net/ifconfig.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/ifconfig.js +120 -0
- package/dist/substrate/lifo/commands/net/ip.d.ts +14 -0
- package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/ip.js +508 -0
- package/dist/substrate/lifo/commands/net/netstat.d.ts +8 -0
- package/dist/substrate/lifo/commands/net/netstat.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/netstat.js +94 -0
- package/dist/substrate/lifo/commands/net/ping.d.ts +4 -0
- package/dist/substrate/lifo/commands/net/ping.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/ping.js +64 -0
- package/dist/substrate/lifo/commands/net/ports.d.ts +8 -0
- package/dist/substrate/lifo/commands/net/ports.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/ports.js +36 -0
- package/dist/substrate/lifo/commands/net/route.d.ts +8 -0
- package/dist/substrate/lifo/commands/net/route.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/route.js +128 -0
- package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts +4 -0
- package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/tunnel-v2.js +141 -0
- package/dist/substrate/lifo/commands/net/tunnel.d.ts +6 -0
- package/dist/substrate/lifo/commands/net/tunnel.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/tunnel.js +232 -0
- package/dist/substrate/lifo/commands/net/wget.d.ts +4 -0
- package/dist/substrate/lifo/commands/net/wget.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/net/wget.js +78 -0
- package/dist/substrate/lifo/commands/registry.d.ts +15 -0
- package/dist/substrate/lifo/commands/registry.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/registry.js +120 -0
- package/dist/substrate/lifo/commands/signal.d.ts +12 -0
- package/dist/substrate/lifo/commands/signal.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/signal.js +47 -0
- package/dist/substrate/lifo/commands/system/bc.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/bc.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/bc.js +243 -0
- package/dist/substrate/lifo/commands/system/cal.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/cal.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/cal.js +81 -0
- package/dist/substrate/lifo/commands/system/date.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/date.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/date.js +79 -0
- package/dist/substrate/lifo/commands/system/env.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/env.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/env.js +7 -0
- package/dist/substrate/lifo/commands/system/fastfetch.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/fastfetch.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/fastfetch.js +385 -0
- package/dist/substrate/lifo/commands/system/free.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/free.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/free.js +36 -0
- package/dist/substrate/lifo/commands/system/help.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/help.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/help.js +46 -0
- package/dist/substrate/lifo/commands/system/hostname.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/hostname.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/hostname.js +6 -0
- package/dist/substrate/lifo/commands/system/kill.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/kill.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/kill.js +76 -0
- package/dist/substrate/lifo/commands/system/lifo.d.ts +41 -0
- package/dist/substrate/lifo/commands/system/lifo.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/lifo.js +446 -0
- package/dist/substrate/lifo/commands/system/logout.d.ts +5 -0
- package/dist/substrate/lifo/commands/system/logout.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/logout.js +15 -0
- package/dist/substrate/lifo/commands/system/man.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/man.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/man.js +144 -0
- package/dist/substrate/lifo/commands/system/node.d.ts +6 -0
- package/dist/substrate/lifo/commands/system/node.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/node.js +1241 -0
- package/dist/substrate/lifo/commands/system/npm.d.ts +26 -0
- package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/npm.js +934 -0
- package/dist/substrate/lifo/commands/system/pkg.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/pkg.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/pkg.js +97 -0
- package/dist/substrate/lifo/commands/system/ps.d.ts +5 -0
- package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/ps.js +35 -0
- package/dist/substrate/lifo/commands/system/registry-schemas.d.ts +44 -0
- package/dist/substrate/lifo/commands/system/registry-schemas.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/registry-schemas.js +28 -0
- package/dist/substrate/lifo/commands/system/sha256sum.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/sha256sum.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/sha256sum.js +86 -0
- package/dist/substrate/lifo/commands/system/sl.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/sl.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/sl.js +204 -0
- package/dist/substrate/lifo/commands/system/sleep.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/sleep.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/sleep.js +27 -0
- package/dist/substrate/lifo/commands/system/stty.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/stty.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/stty.js +61 -0
- package/dist/substrate/lifo/commands/system/systemctl.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/systemctl.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/systemctl.js +164 -0
- package/dist/substrate/lifo/commands/system/top.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/top.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/top.js +38 -0
- package/dist/substrate/lifo/commands/system/uname.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/uname.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/uname.js +55 -0
- package/dist/substrate/lifo/commands/system/uptime.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/uptime.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/uptime.js +16 -0
- package/dist/substrate/lifo/commands/system/watch.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/watch.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/watch.js +53 -0
- package/dist/substrate/lifo/commands/system/which.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/which.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/which.js +45 -0
- package/dist/substrate/lifo/commands/system/whoami.d.ts +4 -0
- package/dist/substrate/lifo/commands/system/whoami.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/system/whoami.js +6 -0
- package/dist/substrate/lifo/commands/text/awk.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/awk.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/awk.js +200 -0
- package/dist/substrate/lifo/commands/text/base64.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/base64.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/base64.js +102 -0
- package/dist/substrate/lifo/commands/text/cut.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/cut.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/cut.js +91 -0
- package/dist/substrate/lifo/commands/text/diff.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/diff.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/diff.js +292 -0
- package/dist/substrate/lifo/commands/text/grep.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/grep.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/grep.js +189 -0
- package/dist/substrate/lifo/commands/text/head.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/head.js +111 -0
- package/dist/substrate/lifo/commands/text/less.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/less.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/less.js +335 -0
- package/dist/substrate/lifo/commands/text/nano.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/nano.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/nano.js +510 -0
- package/dist/substrate/lifo/commands/text/nl.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/nl.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/nl.js +64 -0
- package/dist/substrate/lifo/commands/text/rev.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/rev.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/rev.js +41 -0
- package/dist/substrate/lifo/commands/text/sed.d.ts +21 -0
- package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/sed.js +248 -0
- package/dist/substrate/lifo/commands/text/seq.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/seq.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/seq.js +69 -0
- package/dist/substrate/lifo/commands/text/sort.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/sort.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/sort.js +90 -0
- package/dist/substrate/lifo/commands/text/strings.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/strings.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/strings.js +69 -0
- package/dist/substrate/lifo/commands/text/tac.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/tac.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/tac.js +32 -0
- package/dist/substrate/lifo/commands/text/tail.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/tail.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/tail.js +64 -0
- package/dist/substrate/lifo/commands/text/tr.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/tr.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/tr.js +180 -0
- package/dist/substrate/lifo/commands/text/uniq.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/uniq.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/uniq.js +82 -0
- package/dist/substrate/lifo/commands/text/wc.d.ts +4 -0
- package/dist/substrate/lifo/commands/text/wc.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/text/wc.js +107 -0
- package/dist/substrate/lifo/commands/types.d.ts +49 -0
- package/dist/substrate/lifo/commands/types.d.ts.map +1 -0
- package/dist/substrate/lifo/commands/types.js +1 -0
- package/dist/substrate/lifo/index.d.ts +67 -0
- package/dist/substrate/lifo/index.d.ts.map +1 -0
- package/dist/substrate/lifo/index.js +66 -0
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts +51 -0
- package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/ServiceManager.js +302 -0
- package/dist/substrate/lifo/kernel/index.d.ts +40 -0
- package/dist/substrate/lifo/kernel/index.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/index.js +170 -0
- package/dist/substrate/lifo/kernel/network/Bridge.d.ts +110 -0
- package/dist/substrate/lifo/kernel/network/Bridge.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/Bridge.js +278 -0
- package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts +50 -0
- package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/DNSResolver.js +135 -0
- package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts +83 -0
- package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/NetworkInterface.js +183 -0
- package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts +98 -0
- package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/NetworkNamespace.js +149 -0
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +178 -0
- package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/NetworkStack.js +460 -0
- package/dist/substrate/lifo/kernel/network/PortBridge.d.ts +44 -0
- package/dist/substrate/lifo/kernel/network/PortBridge.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/PortBridge.js +158 -0
- package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts +55 -0
- package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/RoutingTable.js +167 -0
- package/dist/substrate/lifo/kernel/network/Socket.d.ts +65 -0
- package/dist/substrate/lifo/kernel/network/Socket.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/Socket.js +201 -0
- package/dist/substrate/lifo/kernel/network/index.d.ts +11 -0
- package/dist/substrate/lifo/kernel/network/index.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/index.js +9 -0
- package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts +55 -0
- package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.js +88 -0
- package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts +71 -0
- package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.js +191 -0
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +89 -0
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +324 -0
- package/dist/substrate/lifo/kernel/network/types.d.ts +159 -0
- package/dist/substrate/lifo/kernel/network/types.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/network/types.js +5 -0
- package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts +12 -0
- package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/persistence/PersistenceManager.js +47 -0
- package/dist/substrate/lifo/kernel/persistence/backends.d.ts +15 -0
- package/dist/substrate/lifo/kernel/persistence/backends.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/persistence/backends.js +18 -0
- package/dist/substrate/lifo/kernel/persistence/index.d.ts +6 -0
- package/dist/substrate/lifo/kernel/persistence/index.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/persistence/index.js +3 -0
- package/dist/substrate/lifo/kernel/persistence/serializer.d.ts +21 -0
- package/dist/substrate/lifo/kernel/persistence/serializer.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/persistence/serializer.js +99 -0
- package/dist/substrate/lifo/kernel/samples.d.ts +3 -0
- package/dist/substrate/lifo/kernel/samples.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/samples.js +930 -0
- package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts +22 -0
- package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/storage/BlobStore.js +84 -0
- package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts +54 -0
- package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/storage/ContentStore.js +120 -0
- package/dist/substrate/lifo/kernel/storage/index.d.ts +6 -0
- package/dist/substrate/lifo/kernel/storage/index.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/storage/index.js +3 -0
- package/dist/substrate/lifo/kernel/unit-parser.d.ts +22 -0
- package/dist/substrate/lifo/kernel/unit-parser.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/unit-parser.js +80 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +99 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/VFS.js +679 -0
- package/dist/substrate/lifo/kernel/vfs/index.d.ts +8 -0
- package/dist/substrate/lifo/kernel/vfs/index.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/index.js +5 -0
- package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts +21 -0
- package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.js +137 -0
- package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts +63 -0
- package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.js +175 -0
- package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts +14 -0
- package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.js +126 -0
- package/dist/substrate/lifo/kernel/vfs/types.d.ts +117 -0
- package/dist/substrate/lifo/kernel/vfs/types.d.ts.map +1 -0
- package/dist/substrate/lifo/kernel/vfs/types.js +42 -0
- package/dist/substrate/lifo/node-compat/buffer.d.ts +44 -0
- package/dist/substrate/lifo/node-compat/buffer.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/buffer.js +331 -0
- package/dist/substrate/lifo/node-compat/child_process.d.ts +10 -0
- package/dist/substrate/lifo/node-compat/child_process.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/child_process.js +42 -0
- package/dist/substrate/lifo/node-compat/console.d.ts +21 -0
- package/dist/substrate/lifo/node-compat/console.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/console.js +68 -0
- package/dist/substrate/lifo/node-compat/crypto.d.ts +24 -0
- package/dist/substrate/lifo/node-compat/crypto.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/crypto.js +216 -0
- package/dist/substrate/lifo/node-compat/dns.d.ts +117 -0
- package/dist/substrate/lifo/node-compat/dns.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/dns.js +144 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts +10 -0
- package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/esbuild.js +70 -0
- package/dist/substrate/lifo/node-compat/events.d.ts +19 -0
- package/dist/substrate/lifo/node-compat/events.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/events.js +83 -0
- package/dist/substrate/lifo/node-compat/fs.d.ts +195 -0
- package/dist/substrate/lifo/node-compat/fs.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/fs.js +624 -0
- package/dist/substrate/lifo/node-compat/http.d.ts +133 -0
- package/dist/substrate/lifo/node-compat/http.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/http.js +452 -0
- package/dist/substrate/lifo/node-compat/index.d.ts +20 -0
- package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/index.js +291 -0
- package/dist/substrate/lifo/node-compat/module.d.ts +55 -0
- package/dist/substrate/lifo/node-compat/module.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/module.js +124 -0
- package/dist/substrate/lifo/node-compat/os.d.ts +39 -0
- package/dist/substrate/lifo/node-compat/os.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/os.js +45 -0
- package/dist/substrate/lifo/node-compat/path.d.ts +86 -0
- package/dist/substrate/lifo/node-compat/path.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/path.js +49 -0
- package/dist/substrate/lifo/node-compat/process.d.ts +127 -0
- package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/process.js +173 -0
- package/dist/substrate/lifo/node-compat/readline.d.ts +85 -0
- package/dist/substrate/lifo/node-compat/readline.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/readline.js +205 -0
- package/dist/substrate/lifo/node-compat/rimraf.d.ts +30 -0
- package/dist/substrate/lifo/node-compat/rimraf.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/rimraf.js +63 -0
- package/dist/substrate/lifo/node-compat/stream.d.ts +36 -0
- package/dist/substrate/lifo/node-compat/stream.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/stream.js +90 -0
- package/dist/substrate/lifo/node-compat/string_decoder.d.ts +11 -0
- package/dist/substrate/lifo/node-compat/string_decoder.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/string_decoder.js +23 -0
- package/dist/substrate/lifo/node-compat/timers.d.ts +16 -0
- package/dist/substrate/lifo/node-compat/timers.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/timers.js +18 -0
- package/dist/substrate/lifo/node-compat/tty.d.ts +32 -0
- package/dist/substrate/lifo/node-compat/tty.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/tty.js +76 -0
- package/dist/substrate/lifo/node-compat/url.d.ts +37 -0
- package/dist/substrate/lifo/node-compat/url.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/url.js +65 -0
- package/dist/substrate/lifo/node-compat/util.d.ts +42 -0
- package/dist/substrate/lifo/node-compat/util.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/util.js +132 -0
- package/dist/substrate/lifo/node-compat/zlib.d.ts +66 -0
- package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -0
- package/dist/substrate/lifo/node-compat/zlib.js +80 -0
- package/dist/substrate/lifo/pkg/PackageManager.d.ts +19 -0
- package/dist/substrate/lifo/pkg/PackageManager.d.ts.map +1 -0
- package/dist/substrate/lifo/pkg/PackageManager.js +97 -0
- package/dist/substrate/lifo/pkg/lifo-dev.d.ts +38 -0
- package/dist/substrate/lifo/pkg/lifo-dev.d.ts.map +1 -0
- package/dist/substrate/lifo/pkg/lifo-dev.js +98 -0
- package/dist/substrate/lifo/pkg/lifo-runtime.d.ts +44 -0
- package/dist/substrate/lifo/pkg/lifo-runtime.d.ts.map +1 -0
- package/dist/substrate/lifo/pkg/lifo-runtime.js +372 -0
- package/dist/substrate/lifo/pkg/loader.d.ts +4 -0
- package/dist/substrate/lifo/pkg/loader.d.ts.map +1 -0
- package/dist/substrate/lifo/pkg/loader.js +24 -0
- package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts +18 -0
- package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/HeadlessTerminal.js +28 -0
- package/dist/substrate/lifo/sandbox/Sandbox.d.ts +59 -0
- package/dist/substrate/lifo/sandbox/Sandbox.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/Sandbox.js +234 -0
- package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts +18 -0
- package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/SandboxCommands.js +64 -0
- package/dist/substrate/lifo/sandbox/SandboxFs.d.ts +43 -0
- package/dist/substrate/lifo/sandbox/SandboxFs.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/SandboxFs.js +136 -0
- package/dist/substrate/lifo/sandbox/index.d.ts +3 -0
- package/dist/substrate/lifo/sandbox/index.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/index.js +1 -0
- package/dist/substrate/lifo/sandbox/types.d.ts +116 -0
- package/dist/substrate/lifo/sandbox/types.d.ts.map +1 -0
- package/dist/substrate/lifo/sandbox/types.js +1 -0
- package/dist/substrate/lifo/shell/ProcessRegistry.d.ts +142 -0
- package/dist/substrate/lifo/shell/ProcessRegistry.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/ProcessRegistry.js +256 -0
- package/dist/substrate/lifo/shell/Shell.d.ts +170 -0
- package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/Shell.js +1863 -0
- package/dist/substrate/lifo/shell/completer.d.ts +19 -0
- package/dist/substrate/lifo/shell/completer.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/completer.js +164 -0
- package/dist/substrate/lifo/shell/default-shell.d.ts +9 -0
- package/dist/substrate/lifo/shell/default-shell.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/default-shell.js +53 -0
- package/dist/substrate/lifo/shell/expander.d.ts +48 -0
- package/dist/substrate/lifo/shell/expander.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/expander.js +1204 -0
- package/dist/substrate/lifo/shell/history.d.ts +22 -0
- package/dist/substrate/lifo/shell/history.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/history.js +121 -0
- package/dist/substrate/lifo/shell/interpreter.d.ts +257 -0
- package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/interpreter.js +1490 -0
- package/dist/substrate/lifo/shell/jobs.d.ts +21 -0
- package/dist/substrate/lifo/shell/jobs.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/jobs.js +48 -0
- package/dist/substrate/lifo/shell/lexer.d.ts +13 -0
- package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/lexer.js +614 -0
- package/dist/substrate/lifo/shell/parser.d.ts +7 -0
- package/dist/substrate/lifo/shell/parser.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/parser.js +573 -0
- package/dist/substrate/lifo/shell/pipe.d.ts +15 -0
- package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/pipe.js +86 -0
- package/dist/substrate/lifo/shell/signals.d.ts +11 -0
- package/dist/substrate/lifo/shell/signals.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/signals.js +57 -0
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts +28 -0
- package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/terminal-stdin.js +100 -0
- package/dist/substrate/lifo/shell/test-builtin.d.ts +16 -0
- package/dist/substrate/lifo/shell/test-builtin.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/test-builtin.js +237 -0
- package/dist/substrate/lifo/shell/types.d.ts +140 -0
- package/dist/substrate/lifo/shell/types.d.ts.map +1 -0
- package/dist/substrate/lifo/shell/types.js +25 -0
- package/dist/substrate/lifo/terminal/ITerminal.d.ts +10 -0
- package/dist/substrate/lifo/terminal/ITerminal.d.ts.map +1 -0
- package/dist/substrate/lifo/terminal/ITerminal.js +1 -0
- package/dist/substrate/lifo/utils/archive.d.ts +28 -0
- package/dist/substrate/lifo/utils/archive.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/archive.js +323 -0
- package/dist/substrate/lifo/utils/args.d.ts +18 -0
- package/dist/substrate/lifo/utils/args.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/args.js +73 -0
- package/dist/substrate/lifo/utils/colors.d.ts +27 -0
- package/dist/substrate/lifo/utils/colors.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/colors.js +26 -0
- package/dist/substrate/lifo/utils/encoding.d.ts +4 -0
- package/dist/substrate/lifo/utils/encoding.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/encoding.js +18 -0
- package/dist/substrate/lifo/utils/glob.d.ts +12 -0
- package/dist/substrate/lifo/utils/glob.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/glob.js +150 -0
- package/dist/substrate/lifo/utils/mime.d.ts +4 -0
- package/dist/substrate/lifo/utils/mime.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/mime.js +107 -0
- package/dist/substrate/lifo/utils/path.d.ts +12 -0
- package/dist/substrate/lifo/utils/path.d.ts.map +1 -0
- package/dist/substrate/lifo/utils/path.js +82 -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 +46 -0
- package/dist/vfs/path.d.ts.map +1 -0
- package/dist/vfs/path.js +69 -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 +1054 -0
- package/dist/vfs/sqlite-vfs.d.ts +821 -0
- package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
- package/dist/vfs/sqlite-vfs.js +4135 -0
- package/dist/vfs/symlink-registry.d.ts +60 -0
- package/dist/vfs/symlink-registry.d.ts.map +1 -0
- package/dist/vfs/symlink-registry.js +162 -0
- package/dist/workspace/nimbus-workspace.d.ts +94 -0
- package/dist/workspace/nimbus-workspace.d.ts.map +1 -0
- package/dist/workspace/nimbus-workspace.js +167 -0
- package/package.json +69 -0
- package/src/_shared/ai-egress.ts +91 -0
- package/src/_shared/byte-stream.ts +97 -0
- package/src/_shared/bytes.ts +19 -0
- package/src/_shared/cache-stats.ts +231 -0
- package/src/_shared/crypto.ts +184 -0
- package/src/_shared/esbuild-facet-shim.ts +22 -0
- package/src/_shared/exports-resolver.ts +308 -0
- package/src/_shared/facet-vfs-cursor.ts +58 -0
- package/src/_shared/install-phase.ts +56 -0
- package/src/_shared/lru-map.ts +59 -0
- package/src/_shared/real-node-imports.ts +53 -0
- package/src/_shared/retry.ts +202 -0
- package/src/_shared/rpc-dispose.ts +31 -0
- package/src/_shared/terminal.ts +3 -0
- package/src/_shared/typescript-specifiers.ts +86 -0
- package/src/_shared/untrusted-request.ts +75 -0
- package/src/_shared/vfs-write-ledger.ts +440 -0
- package/src/_shared/w7-frame.ts +1012 -0
- package/src/_shared/weighted-credit-pool.ts +244 -0
- package/src/constants.ts +285 -0
- package/src/index.ts +22 -0
- package/src/observability/diag-counters.ts +343 -0
- package/src/observability/heap-estimate.ts +331 -0
- package/src/observability/heavy-alloc-coord.ts +241 -0
- package/src/observability/oom-classify.ts +253 -0
- package/src/observability/oom-discriminator.ts +405 -0
- package/src/runtime/assets-loader.ts +189 -0
- package/src/runtime/barrel-detect.ts +69 -0
- package/src/runtime/bash/preamble.ts +1519 -0
- package/src/runtime/bash/types.ts +497 -0
- package/src/runtime/bundle-profile.ts +32 -0
- package/src/runtime/cirrus-vite-config-rewriter.ts +28 -0
- package/src/runtime/cli-flags.ts +11 -0
- package/src/runtime/comment-strip.ts +73 -0
- package/src/runtime/cpython-preamble.ts +298 -0
- package/src/runtime/esbuild-service.ts +1470 -0
- package/src/runtime/framework-detect.ts +245 -0
- package/src/runtime/import-meta-transform.ts +16 -0
- package/src/runtime/javascript-ast.ts +66 -0
- package/src/runtime/long-running-handle.ts +185 -0
- package/src/runtime/module-source-rewriter.ts +95 -0
- package/src/runtime/opentui-wasm-backend.ts +608 -0
- package/src/runtime/os-contracts.ts +529 -0
- package/src/runtime/port-registry.ts +298 -0
- package/src/runtime/process-input-routing.ts +92 -0
- package/src/runtime/process-input.ts +148 -0
- package/src/runtime/process-io-protocol.ts +97 -0
- package/src/runtime/process-logs.ts +882 -0
- package/src/runtime/process-table.ts +225 -0
- package/src/runtime/project-detect.ts +42 -0
- package/src/runtime/python-server-adapter.ts +133 -0
- package/src/runtime/require-resolver.ts +760 -0
- package/src/runtime/router-basename.ts +279 -0
- package/src/runtime/ruby-green-threads.ts +904 -0
- package/src/runtime/ruby-socket-shim.ts +548 -0
- package/src/runtime/session-process-supervisor.ts +323 -0
- package/src/runtime/sqlite-runtime-fs-bridge.ts +468 -0
- package/src/runtime/streams.ts +547 -0
- package/src/runtime/undici-shim.ts +517 -0
- package/src/runtime/vfs-manifest.ts +170 -0
- package/src/runtime/vfs-snapshot.ts +273 -0
- package/src/runtime/virtual-socket-kernel.ts +1370 -0
- package/src/runtime/vite-config-parser.ts +273 -0
- package/src/runtime/wasi/types.ts +448 -0
- package/src/runtime/wasi-instance.generated.ts +14 -0
- package/src/runtime/wasi-instance.ts +259 -0
- package/src/runtime/wasi-threads.ts +682 -0
- package/src/runtime/wasm-memory.ts +261 -0
- package/src/shell/elevation-commands.ts +117 -0
- package/src/shell/exec-dispatch.ts +201 -0
- package/src/shell/features.ts +870 -0
- package/src/shell/shell-entrypoints.ts +323 -0
- package/src/shell/shell-invocation.ts +181 -0
- package/src/shell/unix-accounts.ts +128 -0
- package/src/shell/unix-commands.ts +4335 -0
- package/src/substrate/lifo/UPSTREAM.md +14 -0
- package/src/substrate/lifo/commands/archive/gunzip.ts +63 -0
- package/src/substrate/lifo/commands/archive/gzip.ts +76 -0
- package/src/substrate/lifo/commands/archive/tar.ts +157 -0
- package/src/substrate/lifo/commands/archive/unzip.ts +112 -0
- package/src/substrate/lifo/commands/archive/zip.ts +70 -0
- package/src/substrate/lifo/commands/fs/basename.ts +16 -0
- package/src/substrate/lifo/commands/fs/cat.ts +54 -0
- package/src/substrate/lifo/commands/fs/chmod.ts +110 -0
- package/src/substrate/lifo/commands/fs/chown.ts +45 -0
- package/src/substrate/lifo/commands/fs/cp.ts +33 -0
- package/src/substrate/lifo/commands/fs/df.ts +56 -0
- package/src/substrate/lifo/commands/fs/dirname.ts +16 -0
- package/src/substrate/lifo/commands/fs/du.ts +100 -0
- package/src/substrate/lifo/commands/fs/file.ts +90 -0
- package/src/substrate/lifo/commands/fs/find.ts +76 -0
- package/src/substrate/lifo/commands/fs/ln.ts +19 -0
- package/src/substrate/lifo/commands/fs/ls.ts +164 -0
- package/src/substrate/lifo/commands/fs/mkdir.ts +37 -0
- package/src/substrate/lifo/commands/fs/mktemp.ts +56 -0
- package/src/substrate/lifo/commands/fs/mv.ts +111 -0
- package/src/substrate/lifo/commands/fs/realpath.ts +31 -0
- package/src/substrate/lifo/commands/fs/rm.ts +53 -0
- package/src/substrate/lifo/commands/fs/rmdir.ts +50 -0
- package/src/substrate/lifo/commands/fs/stat.ts +39 -0
- package/src/substrate/lifo/commands/fs/touch.ts +30 -0
- package/src/substrate/lifo/commands/fs/tree.ts +76 -0
- package/src/substrate/lifo/commands/index.ts +2 -0
- package/src/substrate/lifo/commands/io/dd.ts +289 -0
- package/src/substrate/lifo/commands/io/printf.ts +78 -0
- package/src/substrate/lifo/commands/io/tee.ts +37 -0
- package/src/substrate/lifo/commands/io/xargs.ts +49 -0
- package/src/substrate/lifo/commands/io/yes.ts +15 -0
- package/src/substrate/lifo/commands/net/curl.ts +739 -0
- package/src/substrate/lifo/commands/net/dig.ts +99 -0
- package/src/substrate/lifo/commands/net/forward.ts +90 -0
- package/src/substrate/lifo/commands/net/host.ts +182 -0
- package/src/substrate/lifo/commands/net/ifconfig.ts +139 -0
- package/src/substrate/lifo/commands/net/ip.ts +633 -0
- package/src/substrate/lifo/commands/net/netstat.ts +106 -0
- package/src/substrate/lifo/commands/net/ping.ts +74 -0
- package/src/substrate/lifo/commands/net/ports.ts +45 -0
- package/src/substrate/lifo/commands/net/route.ts +153 -0
- package/src/substrate/lifo/commands/net/tunnel-v2.ts +183 -0
- package/src/substrate/lifo/commands/net/tunnel.ts +272 -0
- package/src/substrate/lifo/commands/net/wget.ts +88 -0
- package/src/substrate/lifo/commands/registry.ts +141 -0
- package/src/substrate/lifo/commands/signal.ts +65 -0
- package/src/substrate/lifo/commands/system/bc.ts +265 -0
- package/src/substrate/lifo/commands/system/cal.ts +90 -0
- package/src/substrate/lifo/commands/system/date.ts +59 -0
- package/src/substrate/lifo/commands/system/env.ts +10 -0
- package/src/substrate/lifo/commands/system/fastfetch.ts +428 -0
- package/src/substrate/lifo/commands/system/free.ts +38 -0
- package/src/substrate/lifo/commands/system/help.ts +53 -0
- package/src/substrate/lifo/commands/system/hostname.ts +9 -0
- package/src/substrate/lifo/commands/system/kill.ts +89 -0
- package/src/substrate/lifo/commands/system/lifo.ts +525 -0
- package/src/substrate/lifo/commands/system/logout.ts +17 -0
- package/src/substrate/lifo/commands/system/man.ts +167 -0
- package/src/substrate/lifo/commands/system/node.ts +1305 -0
- package/src/substrate/lifo/commands/system/npm.ts +1078 -0
- package/src/substrate/lifo/commands/system/pkg.ts +111 -0
- package/src/substrate/lifo/commands/system/ps.ts +46 -0
- package/src/substrate/lifo/commands/system/registry-schemas.ts +33 -0
- package/src/substrate/lifo/commands/system/sha256sum.ts +89 -0
- package/src/substrate/lifo/commands/system/sl.ts +249 -0
- package/src/substrate/lifo/commands/system/sleep.ts +36 -0
- package/src/substrate/lifo/commands/system/stty.ts +69 -0
- package/src/substrate/lifo/commands/system/systemctl.ts +184 -0
- package/src/substrate/lifo/commands/system/top.ts +53 -0
- package/src/substrate/lifo/commands/system/uname.ts +59 -0
- package/src/substrate/lifo/commands/system/uptime.ts +19 -0
- package/src/substrate/lifo/commands/system/watch.ts +67 -0
- package/src/substrate/lifo/commands/system/which.ts +51 -0
- package/src/substrate/lifo/commands/system/whoami.ts +9 -0
- package/src/substrate/lifo/commands/text/awk.ts +210 -0
- package/src/substrate/lifo/commands/text/base64.ts +102 -0
- package/src/substrate/lifo/commands/text/cut.ts +92 -0
- package/src/substrate/lifo/commands/text/diff.ts +314 -0
- package/src/substrate/lifo/commands/text/grep.ts +170 -0
- package/src/substrate/lifo/commands/text/head.ts +95 -0
- package/src/substrate/lifo/commands/text/less.ts +335 -0
- package/src/substrate/lifo/commands/text/nano.ts +513 -0
- package/src/substrate/lifo/commands/text/nl.ts +74 -0
- package/src/substrate/lifo/commands/text/rev.ts +46 -0
- package/src/substrate/lifo/commands/text/sed.ts +297 -0
- package/src/substrate/lifo/commands/text/seq.ts +76 -0
- package/src/substrate/lifo/commands/text/sort.ts +88 -0
- package/src/substrate/lifo/commands/text/strings.ts +73 -0
- package/src/substrate/lifo/commands/text/tac.ts +37 -0
- package/src/substrate/lifo/commands/text/tail.ts +63 -0
- package/src/substrate/lifo/commands/text/tr.ts +192 -0
- package/src/substrate/lifo/commands/text/uniq.ts +77 -0
- package/src/substrate/lifo/commands/text/wc.ts +104 -0
- package/src/substrate/lifo/commands/types.ts +58 -0
- package/src/substrate/lifo/index.ts +173 -0
- package/src/substrate/lifo/kernel/ServiceManager.ts +371 -0
- package/src/substrate/lifo/kernel/index.ts +209 -0
- package/src/substrate/lifo/kernel/network/Bridge.ts +336 -0
- package/src/substrate/lifo/kernel/network/DNSResolver.ts +158 -0
- package/src/substrate/lifo/kernel/network/NetworkInterface.ts +235 -0
- package/src/substrate/lifo/kernel/network/NetworkNamespace.ts +182 -0
- package/src/substrate/lifo/kernel/network/NetworkStack.ts +590 -0
- package/src/substrate/lifo/kernel/network/PortBridge.ts +182 -0
- package/src/substrate/lifo/kernel/network/RoutingTable.ts +209 -0
- package/src/substrate/lifo/kernel/network/Socket.ts +245 -0
- package/src/substrate/lifo/kernel/network/index.ts +29 -0
- package/src/substrate/lifo/kernel/network/tunnel/BaseTunnel.ts +124 -0
- package/src/substrate/lifo/kernel/network/tunnel/VETHPair.ts +227 -0
- package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +387 -0
- package/src/substrate/lifo/kernel/network/types.ts +178 -0
- package/src/substrate/lifo/kernel/persistence/PersistenceManager.ts +50 -0
- package/src/substrate/lifo/kernel/persistence/backends.ts +36 -0
- package/src/substrate/lifo/kernel/persistence/index.ts +5 -0
- package/src/substrate/lifo/kernel/persistence/serializer.ts +127 -0
- package/src/substrate/lifo/kernel/samples.ts +966 -0
- package/src/substrate/lifo/kernel/storage/BlobStore.ts +114 -0
- package/src/substrate/lifo/kernel/storage/ContentStore.ts +150 -0
- package/src/substrate/lifo/kernel/storage/index.ts +5 -0
- package/src/substrate/lifo/kernel/unit-parser.ts +95 -0
- package/src/substrate/lifo/kernel/vfs/VFS.ts +768 -0
- package/src/substrate/lifo/kernel/vfs/index.ts +9 -0
- package/src/substrate/lifo/kernel/vfs/providers/DevProvider.ts +172 -0
- package/src/substrate/lifo/kernel/vfs/providers/NativeFsProvider.ts +198 -0
- package/src/substrate/lifo/kernel/vfs/providers/ProcProvider.ts +144 -0
- package/src/substrate/lifo/kernel/vfs/types.ts +145 -0
- package/src/substrate/lifo/node-compat/buffer.ts +357 -0
- package/src/substrate/lifo/node-compat/child_process.ts +52 -0
- package/src/substrate/lifo/node-compat/console.ts +70 -0
- package/src/substrate/lifo/node-compat/crypto.ts +186 -0
- package/src/substrate/lifo/node-compat/dns.ts +214 -0
- package/src/substrate/lifo/node-compat/esbuild.ts +85 -0
- package/src/substrate/lifo/node-compat/events.ts +94 -0
- package/src/substrate/lifo/node-compat/fs.ts +762 -0
- package/src/substrate/lifo/node-compat/http.ts +561 -0
- package/src/substrate/lifo/node-compat/index.ts +300 -0
- package/src/substrate/lifo/node-compat/module.ts +148 -0
- package/src/substrate/lifo/node-compat/os.ts +44 -0
- package/src/substrate/lifo/node-compat/path.ts +74 -0
- package/src/substrate/lifo/node-compat/process.ts +178 -0
- package/src/substrate/lifo/node-compat/readline.ts +244 -0
- package/src/substrate/lifo/node-compat/rimraf.ts +80 -0
- package/src/substrate/lifo/node-compat/stream.ts +98 -0
- package/src/substrate/lifo/node-compat/string_decoder.ts +26 -0
- package/src/substrate/lifo/node-compat/timers.ts +21 -0
- package/src/substrate/lifo/node-compat/tty.ts +83 -0
- package/src/substrate/lifo/node-compat/url.ts +76 -0
- package/src/substrate/lifo/node-compat/util.ts +133 -0
- package/src/substrate/lifo/node-compat/zlib.ts +94 -0
- package/src/substrate/lifo/pkg/PackageManager.ts +105 -0
- package/src/substrate/lifo/pkg/lifo-dev.ts +130 -0
- package/src/substrate/lifo/pkg/lifo-runtime.ts +473 -0
- package/src/substrate/lifo/pkg/loader.ts +33 -0
- package/src/substrate/lifo/platform-globals.d.ts +41 -0
- package/src/substrate/lifo/sandbox/HeadlessTerminal.ts +38 -0
- package/src/substrate/lifo/sandbox/Sandbox.ts +279 -0
- package/src/substrate/lifo/sandbox/SandboxCommands.ts +74 -0
- package/src/substrate/lifo/sandbox/SandboxFs.ts +160 -0
- package/src/substrate/lifo/sandbox/index.ts +8 -0
- package/src/substrate/lifo/sandbox/types.ts +117 -0
- package/src/substrate/lifo/shell/ProcessRegistry.ts +379 -0
- package/src/substrate/lifo/shell/Shell.ts +2048 -0
- package/src/substrate/lifo/shell/completer.ts +212 -0
- package/src/substrate/lifo/shell/default-shell.ts +63 -0
- package/src/substrate/lifo/shell/expander.ts +1349 -0
- package/src/substrate/lifo/shell/history.ts +138 -0
- package/src/substrate/lifo/shell/interpreter.ts +1754 -0
- package/src/substrate/lifo/shell/jobs.ts +64 -0
- package/src/substrate/lifo/shell/lexer.ts +698 -0
- package/src/substrate/lifo/shell/parser.ts +711 -0
- package/src/substrate/lifo/shell/pipe.ts +92 -0
- package/src/substrate/lifo/shell/signals.ts +70 -0
- package/src/substrate/lifo/shell/terminal-stdin.ts +109 -0
- package/src/substrate/lifo/shell/test-builtin.ts +316 -0
- package/src/substrate/lifo/shell/types.ts +166 -0
- package/src/substrate/lifo/terminal/ITerminal.ts +9 -0
- package/src/substrate/lifo/utils/archive.ts +389 -0
- package/src/substrate/lifo/utils/args.ts +89 -0
- package/src/substrate/lifo/utils/colors.ts +29 -0
- package/src/substrate/lifo/utils/encoding.ts +21 -0
- package/src/substrate/lifo/utils/glob.ts +172 -0
- package/src/substrate/lifo/utils/mime.ts +114 -0
- package/src/substrate/lifo/utils/path.ts +83 -0
- package/src/types/assets.d.ts +4 -0
- package/src/vfs/events.ts +134 -0
- package/src/vfs/path.ts +71 -0
- package/src/vfs/seed-project.ts +1094 -0
- package/src/vfs/sqlite-vfs.ts +5035 -0
- package/src/vfs/symlink-registry.ts +172 -0
- package/src/workspace/nimbus-workspace.ts +213 -0
|
@@ -0,0 +1,4135 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* SqliteVFS — Demand-paged virtual filesystem on DO SQLite.
|
|
3
|
+
*
|
|
4
|
+
* Architecture (from webcontainer-v2-feasibility.md §4):
|
|
5
|
+
*
|
|
6
|
+
* ┌─────────────────────────────────────────┐
|
|
7
|
+
* │ Nimbus VFS (in-memory) │
|
|
8
|
+
* │ INode tree: always-resident metadata │ ~10-20 MB for 50K files
|
|
9
|
+
* │ ContentCache: LRU file content cache │ ~32 MB (512 × 64KB)
|
|
10
|
+
* │ ─────────────────────────────────────── │
|
|
11
|
+
* │ On cache miss → SQLite read │
|
|
12
|
+
* │ Writes commit synchronously to SQLite │
|
|
13
|
+
* │ On npm install → batch SQLite writes │
|
|
14
|
+
* └─────────────────────────────────────────┘
|
|
15
|
+
* │ │
|
|
16
|
+
* ▼ ▼
|
|
17
|
+
* ┌─────────────────┐ ┌─────────────────────┐
|
|
18
|
+
* │ file_chunks │ │ inodes │
|
|
19
|
+
* │ (content_id, │ │ (path, type, mode, │
|
|
20
|
+
* │ chunk_id, data)│ │ size, content_id) │
|
|
21
|
+
* │ 64KB chunks │ │ │
|
|
22
|
+
* └─────────────────┘ └─────────────────────┘
|
|
23
|
+
* DO SQLite (10 GB)
|
|
24
|
+
*
|
|
25
|
+
* Key design from do86's SqlPageStore:
|
|
26
|
+
* - Disposable read cache; SQLite owns every accepted write durably
|
|
27
|
+
* - Bounded batch writes (33 chunk rows per INSERT)
|
|
28
|
+
* - All operations SYNCHRONOUS (DO sql.exec() is sync)
|
|
29
|
+
*
|
|
30
|
+
* Durability:
|
|
31
|
+
* - writeFile() returns void (sync) — preserved to match LIFO's
|
|
32
|
+
* MountProvider.writeFile(subpath, content): void contract.
|
|
33
|
+
* - Every write returns only after its SQLite transaction commits. Large
|
|
34
|
+
* replacements stage bounded chunk groups and atomically publish the new
|
|
35
|
+
* content generation before returning.
|
|
36
|
+
*
|
|
37
|
+
* Key design decisions:
|
|
38
|
+
* - 64KB chunks (not 4KB): file access is sequential, fewer rows
|
|
39
|
+
* - INode metadata always in memory (small: ~200B per file)
|
|
40
|
+
* - File content demand-paged through LRU cache
|
|
41
|
+
*/
|
|
42
|
+
import { VfsEventEmitter } from './events.js';
|
|
43
|
+
import { normalizeVfsPath } from './path.js';
|
|
44
|
+
import { CHUNK_SIZE, LRU_MAX_ENTRIES, MAX_TX_BLOB_BYTES, MAX_TX_LOGICAL_ROWS, MAX_TX_SQL_EXECS, MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES, FS_LIST_PAGE_LIMIT, } from '../constants.js';
|
|
45
|
+
import { recordFailure } from '../observability/oom-discriminator.js';
|
|
46
|
+
import { classifyError } from '../observability/oom-classify.js';
|
|
47
|
+
import { acquireSupervisorAllocation } from '../observability/heavy-alloc-coord.js';
|
|
48
|
+
import { enc, dec } from '../_shared/bytes.js';
|
|
49
|
+
import { decodeWriteBatchStream, } from '../_shared/w7-frame.js';
|
|
50
|
+
import { WeightedCreditPool, } from '../_shared/weighted-credit-pool.js';
|
|
51
|
+
import { LEGACY_SYMLINK_REGISTRY_PATH } from './symlink-registry.js';
|
|
52
|
+
import { CRED_KERNEL, } from '../runtime/os-contracts.js';
|
|
53
|
+
const CONTENT_ID_ALLOCATION_ATTEMPTS = 8;
|
|
54
|
+
const INODE_ROWS_PER_SQL_EXEC = 9;
|
|
55
|
+
const CHUNK_ROWS_PER_SQL_EXEC = 33;
|
|
56
|
+
const CONTENT_IDS_PER_SQL_EXEC = 50;
|
|
57
|
+
const TRANSACTION_DURATION_SAMPLE_COUNT = 128;
|
|
58
|
+
const CONTENT_SCHEMA_MIGRATION = 'content_generations_v1';
|
|
59
|
+
export const VFS_APPEND_RECEIPT_LIMIT = 2048;
|
|
60
|
+
const INODE_KIND_FILE = 0;
|
|
61
|
+
const INODE_KIND_DIRECTORY = 1;
|
|
62
|
+
const INODE_KIND_SYMLINK = 2;
|
|
63
|
+
function withCommitRowMetrics(metrics) {
|
|
64
|
+
return {
|
|
65
|
+
...metrics,
|
|
66
|
+
logicalRows: metrics.logicalRows + 1,
|
|
67
|
+
sqlExecs: metrics.sqlExecs + 1,
|
|
68
|
+
};
|
|
69
|
+
}
|
|
70
|
+
const VFS_APPEND_INCARNATION_PATTERN = /^[0-9a-f]{8}-[0-9a-f]{4}-[1-5][0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
|
|
71
|
+
function assertAppendIncarnation(value, kind) {
|
|
72
|
+
if (!VFS_APPEND_INCARNATION_PATTERN.test(value)) {
|
|
73
|
+
throw vfsError('EINVAL', `invalid append ${kind} incarnation`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
export class SqliteVfsTransactionTooLargeError extends Error {
|
|
77
|
+
limit;
|
|
78
|
+
actual;
|
|
79
|
+
maximum;
|
|
80
|
+
metrics;
|
|
81
|
+
code = 'E2BIG';
|
|
82
|
+
constructor(limit, actual, maximum, metrics) {
|
|
83
|
+
super(`[sqlite-vfs] transaction exceeds ${limit} limit: ${actual} > ${maximum}`);
|
|
84
|
+
this.limit = limit;
|
|
85
|
+
this.actual = actual;
|
|
86
|
+
this.maximum = maximum;
|
|
87
|
+
this.metrics = metrics;
|
|
88
|
+
this.name = 'SqliteVfsTransactionTooLargeError';
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
class TransactionPlanBuilder {
|
|
92
|
+
inodes = [];
|
|
93
|
+
chunks = [];
|
|
94
|
+
deletedChunks = [];
|
|
95
|
+
deletedPaths = new Set();
|
|
96
|
+
stagingContentIds = new Set();
|
|
97
|
+
publishedContentIds = new Set();
|
|
98
|
+
gcContentIds = new Set();
|
|
99
|
+
affectedPaths = new Set();
|
|
100
|
+
deletedInodePaths = new Set();
|
|
101
|
+
blobBytes = 0;
|
|
102
|
+
deletedInodeRows = 0;
|
|
103
|
+
addInode(entry) {
|
|
104
|
+
this.inodes.push(entry);
|
|
105
|
+
this.affectedPaths.add(entry.path);
|
|
106
|
+
}
|
|
107
|
+
addChunk(entry) {
|
|
108
|
+
this.chunks.push(entry);
|
|
109
|
+
this.blobBytes += entry.data.byteLength;
|
|
110
|
+
this.affectedPaths.add(entry.path);
|
|
111
|
+
}
|
|
112
|
+
addChunkGroup(entries) {
|
|
113
|
+
for (const entry of entries)
|
|
114
|
+
this.addChunk(entry);
|
|
115
|
+
}
|
|
116
|
+
addDeletedChunk(entry) {
|
|
117
|
+
this.deletedChunks.push(entry);
|
|
118
|
+
this.blobBytes += entry.byteLength;
|
|
119
|
+
this.affectedPaths.add(entry.path);
|
|
120
|
+
}
|
|
121
|
+
addDeletedPath(path, inode) {
|
|
122
|
+
this.deletedPaths.add(path);
|
|
123
|
+
this.affectedPaths.add(path);
|
|
124
|
+
if (inode && !this.deletedInodePaths.has(inode.path)) {
|
|
125
|
+
this.deletedInodePaths.add(inode.path);
|
|
126
|
+
this.deletedInodeRows++;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
addStagingContent(contentId) {
|
|
130
|
+
this.stagingContentIds.add(contentId);
|
|
131
|
+
}
|
|
132
|
+
addPublishedContent(contentId) {
|
|
133
|
+
// Staged and published inside one transaction is a no-op pair: the
|
|
134
|
+
// content is published the instant the transaction commits and the
|
|
135
|
+
// intermediate row is never observable, so neither is written.
|
|
136
|
+
if (this.stagingContentIds.delete(contentId))
|
|
137
|
+
return;
|
|
138
|
+
this.publishedContentIds.add(contentId);
|
|
139
|
+
}
|
|
140
|
+
addGcContent(contentId) {
|
|
141
|
+
this.gcContentIds.add(contentId);
|
|
142
|
+
}
|
|
143
|
+
wouldExceedChunkGroup(entries) {
|
|
144
|
+
let additionalBlobBytes = 0;
|
|
145
|
+
for (const entry of entries)
|
|
146
|
+
additionalBlobBytes += entry.data.byteLength;
|
|
147
|
+
return this.wouldExceedChunks(additionalBlobBytes, entries.length);
|
|
148
|
+
}
|
|
149
|
+
wouldExceedChunks(additionalBlobBytes, additionalRows = 1) {
|
|
150
|
+
return exceededTransactionLimit(this.metricsWith({ blobBytes: additionalBlobBytes, chunkRows: additionalRows }));
|
|
151
|
+
}
|
|
152
|
+
/**
|
|
153
|
+
* Would admitting one whole file — every chunk, its inode, and the
|
|
154
|
+
* lifecycle row its publication writes — exceed the bound? Answering
|
|
155
|
+
* before the first chunk is staged is what keeps a file that fits inside
|
|
156
|
+
* one transaction from ever reaching the staging table.
|
|
157
|
+
*/
|
|
158
|
+
wouldExceedFile(byteLength, chunkCount, replacedContent) {
|
|
159
|
+
return exceededTransactionLimit(this.metricsWith({
|
|
160
|
+
blobBytes: byteLength,
|
|
161
|
+
chunkRows: chunkCount,
|
|
162
|
+
inodeRows: 1,
|
|
163
|
+
gcContentIds: replacedContent ? 1 : 0,
|
|
164
|
+
paths: 1,
|
|
165
|
+
}));
|
|
166
|
+
}
|
|
167
|
+
/**
|
|
168
|
+
* Would admitting one inode row — and the path it touches — exceed the
|
|
169
|
+
* bound? The admission test for a move, which republishes an existing
|
|
170
|
+
* inode at a new path and writes no content of its own.
|
|
171
|
+
*/
|
|
172
|
+
wouldExceedInode() {
|
|
173
|
+
return exceededTransactionLimit(this.metricsWith({ inodeRows: 1, paths: 1 }));
|
|
174
|
+
}
|
|
175
|
+
/**
|
|
176
|
+
* Would admitting one removal — the inode row it deletes, the statement
|
|
177
|
+
* that deletes it, and the content it orphans — exceed the bound?
|
|
178
|
+
*/
|
|
179
|
+
wouldExceedDeletion(orphansContent) {
|
|
180
|
+
return exceededTransactionLimit(this.metricsWith({
|
|
181
|
+
deletedInodes: 1,
|
|
182
|
+
gcContentIds: orphansContent ? 1 : 0,
|
|
183
|
+
paths: 1,
|
|
184
|
+
}));
|
|
185
|
+
}
|
|
186
|
+
get empty() {
|
|
187
|
+
return this.inodes.length === 0
|
|
188
|
+
&& this.chunks.length === 0
|
|
189
|
+
&& this.deletedChunks.length === 0
|
|
190
|
+
&& this.deletedPaths.size === 0
|
|
191
|
+
&& this.stagingContentIds.size === 0
|
|
192
|
+
&& this.publishedContentIds.size === 0
|
|
193
|
+
&& this.gcContentIds.size === 0;
|
|
194
|
+
}
|
|
195
|
+
build() {
|
|
196
|
+
return {
|
|
197
|
+
inodes: this.inodes,
|
|
198
|
+
chunks: this.chunks,
|
|
199
|
+
deletedChunks: this.deletedChunks,
|
|
200
|
+
deletedPaths: [...this.deletedPaths],
|
|
201
|
+
stagingContentIds: [...this.stagingContentIds],
|
|
202
|
+
publishedContentIds: [...this.publishedContentIds],
|
|
203
|
+
gcContentIds: [...this.gcContentIds],
|
|
204
|
+
affectedPaths: this.affectedPaths,
|
|
205
|
+
metrics: this.metrics,
|
|
206
|
+
};
|
|
207
|
+
}
|
|
208
|
+
get deletedChunkSqlExecs() {
|
|
209
|
+
const byContent = new Map();
|
|
210
|
+
for (const chunk of this.deletedChunks) {
|
|
211
|
+
byContent.set(chunk.contentId, (byContent.get(chunk.contentId) ?? 0) + 1);
|
|
212
|
+
}
|
|
213
|
+
let sqlExecs = 0;
|
|
214
|
+
for (const count of byContent.values()) {
|
|
215
|
+
sqlExecs += groupedSqlExecs(count, CHUNK_ROWS_PER_SQL_EXEC);
|
|
216
|
+
}
|
|
217
|
+
return sqlExecs;
|
|
218
|
+
}
|
|
219
|
+
get metrics() {
|
|
220
|
+
return this.metricsWith({});
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* What this plan would cost with `addition` admitted. One formula serves
|
|
224
|
+
* both the built plan and every admission test, so a bound can never be
|
|
225
|
+
* checked against a different accounting than the one it commits under.
|
|
226
|
+
*/
|
|
227
|
+
metricsWith(addition) {
|
|
228
|
+
const inodeRows = this.inodes.length + (addition.inodeRows ?? 0);
|
|
229
|
+
const chunkRows = this.chunks.length + (addition.chunkRows ?? 0);
|
|
230
|
+
const gcIds = this.gcContentIds.size + (addition.gcContentIds ?? 0);
|
|
231
|
+
const deletedInodeRows = this.deletedInodeRows + (addition.deletedInodes ?? 0);
|
|
232
|
+
return {
|
|
233
|
+
blobBytes: this.blobBytes + (addition.blobBytes ?? 0),
|
|
234
|
+
logicalRows: inodeRows
|
|
235
|
+
+ chunkRows
|
|
236
|
+
+ this.deletedChunks.length
|
|
237
|
+
+ deletedInodeRows
|
|
238
|
+
+ this.stagingContentIds.size
|
|
239
|
+
+ this.publishedContentIds.size
|
|
240
|
+
+ gcIds,
|
|
241
|
+
sqlExecs: this.deletedPaths.size + (addition.deletedInodes ?? 0)
|
|
242
|
+
+ groupedSqlExecs(this.stagingContentIds.size, CONTENT_IDS_PER_SQL_EXEC)
|
|
243
|
+
+ groupedSqlExecs(this.publishedContentIds.size, CONTENT_IDS_PER_SQL_EXEC)
|
|
244
|
+
+ groupedSqlExecs(gcIds, CONTENT_IDS_PER_SQL_EXEC)
|
|
245
|
+
+ groupedSqlExecs(inodeRows, INODE_ROWS_PER_SQL_EXEC)
|
|
246
|
+
+ groupedSqlExecs(chunkRows, CHUNK_ROWS_PER_SQL_EXEC)
|
|
247
|
+
+ this.deletedChunkSqlExecs,
|
|
248
|
+
affectedPaths: this.affectedPaths.size + (addition.paths ?? 0),
|
|
249
|
+
};
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
function groupedSqlExecs(rows, rowsPerExec) {
|
|
253
|
+
return rows === 0 ? 0 : Math.ceil(rows / rowsPerExec);
|
|
254
|
+
}
|
|
255
|
+
function exceededTransactionLimit(metrics) {
|
|
256
|
+
if (metrics.blobBytes > MAX_TX_BLOB_BYTES)
|
|
257
|
+
return 'blobBytes';
|
|
258
|
+
if (metrics.logicalRows > MAX_TX_LOGICAL_ROWS)
|
|
259
|
+
return 'logicalRows';
|
|
260
|
+
if (metrics.sqlExecs > MAX_TX_SQL_EXECS)
|
|
261
|
+
return 'sqlExecs';
|
|
262
|
+
return null;
|
|
263
|
+
}
|
|
264
|
+
// ── SqliteVFS ───────────────────────────────────────────────────────────────
|
|
265
|
+
export class SqliteVFS {
|
|
266
|
+
sql;
|
|
267
|
+
ctx;
|
|
268
|
+
events;
|
|
269
|
+
// ── INode tree (always resident) ──────────────────────────────────────
|
|
270
|
+
inodes = new Map();
|
|
271
|
+
/** Children index: parentPath → Set of child paths. O(1) readdir. */
|
|
272
|
+
children = new Map();
|
|
273
|
+
// ── Content cache (LRU, 512 × 64KB = 32MB) ───────────────────────────
|
|
274
|
+
// Map iteration order = insertion order. Delete+re-insert to move to MRU.
|
|
275
|
+
cache = new Map();
|
|
276
|
+
/** Actual bytes in cache (not all chunks are full 64KB) */
|
|
277
|
+
_cacheBytes = 0;
|
|
278
|
+
// ── W5 Lever 8: runtime-mutable LRU cap + shrink refcount ─────────
|
|
279
|
+
// Default seeded from LRU_MAX_ENTRIES (32 MiB). Heavy-alloc owners
|
|
280
|
+
// (npm install, git clone, pre-bundle) call shrinkForInstall() to
|
|
281
|
+
// drop the cap to ~8 MiB and free heap headroom for in-flight RPC and
|
|
282
|
+
// streamed-write payloads. Refcount-based: nested
|
|
283
|
+
// acquires stack; only the OUTERMOST restoreAfterInstall() actually
|
|
284
|
+
// raises the cap back to the default.
|
|
285
|
+
//
|
|
286
|
+
// Why instance-level (not module-level):
|
|
287
|
+
// - Tests need an in-memory VFS without polluting the constant.
|
|
288
|
+
// - Future per-DO tuning (e.g. set higher cap on a session running
|
|
289
|
+
// `vite build` vs `npm install`) becomes a one-call change.
|
|
290
|
+
//
|
|
291
|
+
// The eviction trigger at cacheSet() reads this field. Counter
|
|
292
|
+
// accounting is unchanged.
|
|
293
|
+
_lruMaxEntries = LRU_MAX_ENTRIES;
|
|
294
|
+
_lruShrinkRefcount = 0;
|
|
295
|
+
// ── Running counters for O(1) getStats() (B3 / AUDIT M10 / M-S8) ──
|
|
296
|
+
// Replaces the triple scan of this.inodes on every /api/stats poll.
|
|
297
|
+
// Bootstrapped in loadInodes(); maintained at every mutator entry:
|
|
298
|
+
// mkdir/rmdir/writeFile/unlink/writeBatch (rename is a no-op —
|
|
299
|
+
// same inode, new path). Invariant: these match a fresh O(N)
|
|
300
|
+
// walk of this.inodes. Unit-tested in the A5/B3 runtime tests.
|
|
301
|
+
_totalFiles = 0;
|
|
302
|
+
_totalDirs = 0;
|
|
303
|
+
_usedBytes = 0;
|
|
304
|
+
_revision = 0;
|
|
305
|
+
// ── Per-path revisions ────────────────────────────────────────────────
|
|
306
|
+
// _revision is the monotonic mutation clock. Every mutation stamps the
|
|
307
|
+
// mutated path AND each of its ancestors with the clock value, so
|
|
308
|
+
// revision(dir) is a subtree watermark: it changes iff something under
|
|
309
|
+
// dir changed. Consumers (runtime snapshot caches, page caches, handle
|
|
310
|
+
// staleness checks) key on revision(path) instead of the global clock,
|
|
311
|
+
// so unrelated writes no longer invalidate them. In-memory only — the
|
|
312
|
+
// clock resets with the DO lifetime, exactly like the caches keyed on it.
|
|
313
|
+
_pathRevisions = new Map();
|
|
314
|
+
// ── Invalidation log (facet cache coherence) ──────────────────────────
|
|
315
|
+
// A facet's resident set is a cache of this VFS, and it learns what to
|
|
316
|
+
// drop by asking `invalidatedSince(cursor)` for the delta. _pathRevisions
|
|
317
|
+
// cannot serve that: it answers "what is the watermark under here", not
|
|
318
|
+
// "what changed since when".
|
|
319
|
+
//
|
|
320
|
+
// _epoch exists because _revision is in-memory and restarts at 0 with the
|
|
321
|
+
// DO, while a facet outlives supervisor restarts. A bare revision compare
|
|
322
|
+
// fails OPEN across one: a facet holding cursor N sees the clock reset,
|
|
323
|
+
// N further writes land, and `rev === cursor` reads as "nothing changed"
|
|
324
|
+
// while every byte it holds is stale. Classic ABA, in the one direction
|
|
325
|
+
// this protocol may never fail in. An epoch never recurs, so the pair is
|
|
326
|
+
// globally monotonic. Supervisor DO resets are an observed event here,
|
|
327
|
+
// not a hypothetical.
|
|
328
|
+
_epoch = crypto.randomUUID();
|
|
329
|
+
_invalidations = [];
|
|
330
|
+
_invalidationBytes = 0;
|
|
331
|
+
// Bounded by BYTES, not by entry count. An entry-count bound is not a
|
|
332
|
+
// bound at all here: paths are unbounded in length, so N entries permit
|
|
333
|
+
// unbounded memory — and this lives in the supervisor DO, the side that
|
|
334
|
+
// is measurably memory-constrained and has been observed resetting under
|
|
335
|
+
// allocation pressure. Overflow poisons the cursors that fall behind the
|
|
336
|
+
// log, and a poisoned reader repairs itself against `list()`'s absolute
|
|
337
|
+
// per-path revisions rather than by rebuying its whole cache — see
|
|
338
|
+
// vfs/facet-resident-store.ts. So the budget can stay small even under the
|
|
339
|
+
// continuous churn of an npm install, which must not be able to grow it.
|
|
340
|
+
//
|
|
341
|
+
// Note what this bound does NOT license: it is small because a poison is
|
|
342
|
+
// CHEAP to recover from, and "cheap" is a property of the recovery path, not
|
|
343
|
+
// of this file. It was not, once — a poison used to re-materialise the
|
|
344
|
+
// reader's entire filesystem — and the two statements lived far enough apart
|
|
345
|
+
// that the cost went unnoticed until it took an agent turn past the DO CPU
|
|
346
|
+
// limit.
|
|
347
|
+
static INVALIDATION_LOG_MAX_BYTES = 256 * 1024;
|
|
348
|
+
/** Identifies this supervisor incarnation. Never reused across restarts. */
|
|
349
|
+
get epoch() { return this._epoch; }
|
|
350
|
+
exclusiveMutationLeases = new Map();
|
|
351
|
+
activeMutationOwner = null;
|
|
352
|
+
/** Shared by every concurrent stream targeting this session's VFS. */
|
|
353
|
+
writeStreamCredits = new WeightedCreditPool(MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES);
|
|
354
|
+
_stagedStreamBytes = 0;
|
|
355
|
+
_peakStagedStreamBytes = 0;
|
|
356
|
+
/** In-memory liveness only; content_lifecycle remains durable ownership. */
|
|
357
|
+
activeStagingContentIds = new Set();
|
|
358
|
+
/** True only while durable GC work or a known abandoned staging row exists. */
|
|
359
|
+
maintenancePending = false;
|
|
360
|
+
// Stage 2 transaction/phase telemetry. Scalar writes stay cheap; the
|
|
361
|
+
// percentile is computed from the fixed ring only when diagnostics read it.
|
|
362
|
+
_activeTransaction = null;
|
|
363
|
+
_transactionDuration = emptyDurationSummary();
|
|
364
|
+
_postCommitDuration = emptyDurationSummary();
|
|
365
|
+
_decodeDrainDuration = emptyDurationSummary();
|
|
366
|
+
_creditWaitDuration = emptyDurationSummary();
|
|
367
|
+
/** Whole content-maintenance runs, including the raw scans that execute
|
|
368
|
+
* outside executeMeasuredTransaction; count doubles as the run counter. */
|
|
369
|
+
_maintenanceDuration = emptyDurationSummary();
|
|
370
|
+
_transactionDurationSamples = new Float64Array(TRANSACTION_DURATION_SAMPLE_COUNT);
|
|
371
|
+
_transactionDurationSampleCount = 0;
|
|
372
|
+
_transactionDurationSampleIndex = 0;
|
|
373
|
+
_decodeDrainStarts = new Map();
|
|
374
|
+
_creditWaitStarts = new Map();
|
|
375
|
+
_transactionPeakBlobBytes = 0;
|
|
376
|
+
_transactionPeakLogicalRows = 0;
|
|
377
|
+
_transactionPeakSqlExecs = 0;
|
|
378
|
+
_transactionPeakAffectedPaths = 0;
|
|
379
|
+
_boundedTransactionPeakBlobBytes = 0;
|
|
380
|
+
_boundedTransactionPeakLogicalRows = 0;
|
|
381
|
+
_boundedTransactionPeakSqlExecs = 0;
|
|
382
|
+
_lastTransaction = null;
|
|
383
|
+
_overLimitFileCount = 0;
|
|
384
|
+
_lastOverLimitFile = null;
|
|
385
|
+
// ── Stats ─────────────────────────────────────────────────────────────
|
|
386
|
+
_cacheHits = 0;
|
|
387
|
+
_cacheMisses = 0;
|
|
388
|
+
_evictions = 0;
|
|
389
|
+
_sqlReads = 0;
|
|
390
|
+
_sqlWrites = 0;
|
|
391
|
+
_batchWrites = 0;
|
|
392
|
+
_batchWriteRows = 0;
|
|
393
|
+
constructor(sql, ctx) {
|
|
394
|
+
this.sql = sql;
|
|
395
|
+
this.ctx = ctx;
|
|
396
|
+
this.events = new VfsEventEmitter();
|
|
397
|
+
this.initSchema();
|
|
398
|
+
this.resumeAppendMaintenance();
|
|
399
|
+
this.loadInodes();
|
|
400
|
+
this.runContentMaintenanceSafely(2, true);
|
|
401
|
+
}
|
|
402
|
+
// ── Schema ────────────────────────────────────────────────────────────
|
|
403
|
+
initSchema() {
|
|
404
|
+
this.transactionSync(() => {
|
|
405
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_schema_migrations (
|
|
406
|
+
id TEXT PRIMARY KEY,
|
|
407
|
+
applied_at INTEGER NOT NULL
|
|
408
|
+
)`);
|
|
409
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_append_receipts (
|
|
410
|
+
pid INTEGER NOT NULL,
|
|
411
|
+
writer_id TEXT NOT NULL,
|
|
412
|
+
module_id TEXT NOT NULL,
|
|
413
|
+
operation_id INTEGER NOT NULL,
|
|
414
|
+
path TEXT NOT NULL,
|
|
415
|
+
byte_length INTEGER NOT NULL,
|
|
416
|
+
digest TEXT NOT NULL,
|
|
417
|
+
created_at INTEGER NOT NULL,
|
|
418
|
+
PRIMARY KEY (pid, writer_id, module_id, operation_id)
|
|
419
|
+
)`);
|
|
420
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_append_writer_state (
|
|
421
|
+
pid INTEGER NOT NULL,
|
|
422
|
+
writer_id TEXT NOT NULL,
|
|
423
|
+
revoked INTEGER NOT NULL DEFAULT 0,
|
|
424
|
+
retired_at INTEGER,
|
|
425
|
+
PRIMARY KEY (pid, writer_id)
|
|
426
|
+
)`);
|
|
427
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_append_module_state (
|
|
428
|
+
pid INTEGER NOT NULL,
|
|
429
|
+
writer_id TEXT NOT NULL,
|
|
430
|
+
module_id TEXT NOT NULL,
|
|
431
|
+
acked_through INTEGER NOT NULL DEFAULT 0,
|
|
432
|
+
PRIMARY KEY (pid, writer_id, module_id)
|
|
433
|
+
)`);
|
|
434
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_append_pid_revocations (
|
|
435
|
+
pid INTEGER PRIMARY KEY,
|
|
436
|
+
retired_at INTEGER NOT NULL
|
|
437
|
+
)`);
|
|
438
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS vfs_append_acked_gaps (
|
|
439
|
+
pid INTEGER NOT NULL,
|
|
440
|
+
writer_id TEXT NOT NULL,
|
|
441
|
+
module_id TEXT NOT NULL,
|
|
442
|
+
operation_id INTEGER NOT NULL,
|
|
443
|
+
path TEXT NOT NULL,
|
|
444
|
+
byte_length INTEGER NOT NULL,
|
|
445
|
+
digest TEXT NOT NULL,
|
|
446
|
+
PRIMARY KEY (pid, writer_id, module_id, operation_id)
|
|
447
|
+
)`);
|
|
448
|
+
const contentMigrationApplied = [...this.sql.exec('SELECT id FROM vfs_schema_migrations WHERE id = ?', CONTENT_SCHEMA_MIGRATION)].length > 0;
|
|
449
|
+
let existingInodeColumns = this.tableColumns('inodes');
|
|
450
|
+
if (existingInodeColumns.has('is_dir') && !existingInodeColumns.has('kind')) {
|
|
451
|
+
const invalidLegacyKinds = [...this.sql.exec('SELECT path, is_dir FROM inodes WHERE is_dir NOT IN (0, 1) LIMIT 1')];
|
|
452
|
+
if (invalidLegacyKinds.length > 0) {
|
|
453
|
+
throw new Error(`[sqlite-vfs] invalid legacy inode kind ${String(invalidLegacyKinds[0].is_dir)} ` +
|
|
454
|
+
`at ${String(invalidLegacyKinds[0].path)}`);
|
|
455
|
+
}
|
|
456
|
+
this.sql.exec('ALTER TABLE inodes RENAME COLUMN is_dir TO kind');
|
|
457
|
+
existingInodeColumns = this.tableColumns('inodes');
|
|
458
|
+
}
|
|
459
|
+
if (existingInodeColumns.has('is_dir') || (existingInodeColumns.size > 0 && !existingInodeColumns.has('kind'))) {
|
|
460
|
+
throw new Error('[sqlite-vfs] unsupported inode kind schema');
|
|
461
|
+
}
|
|
462
|
+
const existingChunkColumns = this.tableColumns('file_chunks');
|
|
463
|
+
const existingLifecycleColumns = this.tableColumns('content_lifecycle');
|
|
464
|
+
const chunksAreLegacy = existingChunkColumns.has('path')
|
|
465
|
+
&& existingChunkColumns.has('chunk_id')
|
|
466
|
+
&& existingChunkColumns.has('data')
|
|
467
|
+
&& !existingChunkColumns.has('content_id');
|
|
468
|
+
const chunksAreCurrent = existingChunkColumns.has('content_id')
|
|
469
|
+
&& existingChunkColumns.has('chunk_id')
|
|
470
|
+
&& existingChunkColumns.has('data')
|
|
471
|
+
&& !existingChunkColumns.has('path');
|
|
472
|
+
const lifecycleIsCurrent = existingLifecycleColumns.has('content_id')
|
|
473
|
+
&& existingLifecycleColumns.has('state')
|
|
474
|
+
&& existingLifecycleColumns.has('created_at');
|
|
475
|
+
if (contentMigrationApplied && (!existingInodeColumns.has('content_id')
|
|
476
|
+
|| !chunksAreCurrent
|
|
477
|
+
|| !lifecycleIsCurrent)) {
|
|
478
|
+
throw new Error('[sqlite-vfs] content generation marker does not match the durable schema');
|
|
479
|
+
}
|
|
480
|
+
if (existingChunkColumns.size > 0 && !chunksAreLegacy && !chunksAreCurrent) {
|
|
481
|
+
throw new Error('[sqlite-vfs] unsupported file_chunks schema');
|
|
482
|
+
}
|
|
483
|
+
if (existingLifecycleColumns.size > 0 && !lifecycleIsCurrent) {
|
|
484
|
+
throw new Error('[sqlite-vfs] unsupported content_lifecycle schema');
|
|
485
|
+
}
|
|
486
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS inodes (
|
|
487
|
+
path TEXT PRIMARY KEY,
|
|
488
|
+
parent_path TEXT NOT NULL DEFAULT '',
|
|
489
|
+
kind INTEGER NOT NULL DEFAULT 0 CHECK (kind IN (0, 1, 2)),
|
|
490
|
+
size INTEGER NOT NULL DEFAULT 0,
|
|
491
|
+
atime INTEGER NOT NULL DEFAULT 0,
|
|
492
|
+
mtime INTEGER NOT NULL DEFAULT 0,
|
|
493
|
+
mode INTEGER NOT NULL DEFAULT 0,
|
|
494
|
+
uid INTEGER NOT NULL DEFAULT 1000,
|
|
495
|
+
gid INTEGER NOT NULL DEFAULT 1000,
|
|
496
|
+
chunk_count INTEGER NOT NULL DEFAULT 0,
|
|
497
|
+
content_id TEXT NULL
|
|
498
|
+
)`);
|
|
499
|
+
const inodeColumns = this.tableColumns('inodes');
|
|
500
|
+
if (!inodeColumns.has('chunk_count')) {
|
|
501
|
+
this.sql.exec("ALTER TABLE inodes ADD COLUMN chunk_count INTEGER NOT NULL DEFAULT 0");
|
|
502
|
+
}
|
|
503
|
+
if (!inodeColumns.has('atime')) {
|
|
504
|
+
this.sql.exec("ALTER TABLE inodes ADD COLUMN atime INTEGER NOT NULL DEFAULT 0");
|
|
505
|
+
}
|
|
506
|
+
if (!inodeColumns.has('content_id')) {
|
|
507
|
+
this.sql.exec("ALTER TABLE inodes ADD COLUMN content_id TEXT NULL");
|
|
508
|
+
}
|
|
509
|
+
if (!inodeColumns.has('uid')) {
|
|
510
|
+
this.sql.exec("ALTER TABLE inodes ADD COLUMN uid INTEGER NOT NULL DEFAULT 1000");
|
|
511
|
+
}
|
|
512
|
+
if (!inodeColumns.has('gid')) {
|
|
513
|
+
this.sql.exec("ALTER TABLE inodes ADD COLUMN gid INTEGER NOT NULL DEFAULT 1000");
|
|
514
|
+
}
|
|
515
|
+
const invalidKinds = [...this.sql.exec('SELECT path, kind FROM inodes WHERE kind NOT IN (0, 1, 2) LIMIT 1')];
|
|
516
|
+
if (invalidKinds.length > 0) {
|
|
517
|
+
throw new Error(`[sqlite-vfs] invalid durable inode kind ${String(invalidKinds[0].kind)} ` +
|
|
518
|
+
`at ${String(invalidKinds[0].path)}`);
|
|
519
|
+
}
|
|
520
|
+
this.sql.exec(`CREATE TRIGGER IF NOT EXISTS trg_inodes_kind_insert
|
|
521
|
+
BEFORE INSERT ON inodes WHEN NEW.kind NOT IN (0, 1, 2)
|
|
522
|
+
BEGIN SELECT RAISE(ABORT, 'invalid inode kind'); END`);
|
|
523
|
+
this.sql.exec(`CREATE TRIGGER IF NOT EXISTS trg_inodes_kind_update
|
|
524
|
+
BEFORE UPDATE OF kind ON inodes WHEN NEW.kind NOT IN (0, 1, 2)
|
|
525
|
+
BEGIN SELECT RAISE(ABORT, 'invalid inode kind'); END`);
|
|
526
|
+
const chunkColumns = this.tableColumns('file_chunks');
|
|
527
|
+
if (chunkColumns.size === 0) {
|
|
528
|
+
this.sql.exec(`CREATE TABLE file_chunks (
|
|
529
|
+
content_id TEXT NOT NULL,
|
|
530
|
+
chunk_id INTEGER NOT NULL,
|
|
531
|
+
data BLOB NOT NULL,
|
|
532
|
+
PRIMARY KEY (content_id, chunk_id)
|
|
533
|
+
)`);
|
|
534
|
+
}
|
|
535
|
+
else if (chunksAreLegacy) {
|
|
536
|
+
// legacyContentId(path) is the path itself. Renaming the column is an
|
|
537
|
+
// O(1), rollback-atomic schema migration: no unbounded row-copy txn.
|
|
538
|
+
this.sql.exec("ALTER TABLE file_chunks RENAME COLUMN path TO content_id");
|
|
539
|
+
}
|
|
540
|
+
else if (!chunksAreCurrent) {
|
|
541
|
+
throw new Error('[sqlite-vfs] unsupported file_chunks schema');
|
|
542
|
+
}
|
|
543
|
+
this.sql.exec(`CREATE TABLE IF NOT EXISTS content_lifecycle (
|
|
544
|
+
content_id TEXT PRIMARY KEY,
|
|
545
|
+
state TEXT NOT NULL CHECK (state IN ('staging', 'gc')),
|
|
546
|
+
created_at INTEGER NOT NULL
|
|
547
|
+
)`);
|
|
548
|
+
this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_inodes_parent ON inodes(parent_path)`);
|
|
549
|
+
this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_inodes_content ON inodes(content_id)`);
|
|
550
|
+
if (!contentMigrationApplied) {
|
|
551
|
+
// The COALESCE expression indexes are unusable by the reference probes:
|
|
552
|
+
// SQLite cannot seek an expression index from a correlated subquery, so
|
|
553
|
+
// every probe now seeks idx_inodes_content or the path primary key
|
|
554
|
+
// (see sqlNoInodeContentReference). Keeping them would only tax writes.
|
|
555
|
+
//
|
|
556
|
+
// Gated on the marker because this is a one-time cleanup of indexes an
|
|
557
|
+
// older Nimbus created, and the database may not be ours alone: an
|
|
558
|
+
// embedder shares its Durable Object's SQLite with us, and dropping by
|
|
559
|
+
// name on every construction would silently delete a host's index that
|
|
560
|
+
// happened to collide.
|
|
561
|
+
this.sql.exec('DROP INDEX IF EXISTS idx_inodes_resolved_file_content');
|
|
562
|
+
this.sql.exec('DROP INDEX IF EXISTS idx_inodes_resolved_kind_content');
|
|
563
|
+
}
|
|
564
|
+
this.sql.exec("INSERT OR IGNORE INTO vfs_schema_migrations (id, applied_at) VALUES (?, ?)", CONTENT_SCHEMA_MIGRATION, Date.now());
|
|
565
|
+
});
|
|
566
|
+
this.migrateFromLegacy();
|
|
567
|
+
}
|
|
568
|
+
tableColumns(table) {
|
|
569
|
+
const rows = this.sql.exec(`PRAGMA table_info(${table})`);
|
|
570
|
+
return new Set([...rows].map((row) => String(row.name)));
|
|
571
|
+
}
|
|
572
|
+
migrateFromLegacy() {
|
|
573
|
+
const rows = [...this.sql.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='fs_objects'")];
|
|
574
|
+
if (rows.length === 0)
|
|
575
|
+
return;
|
|
576
|
+
const marker = [...this.sql.exec("SELECT id FROM vfs_schema_migrations WHERE id = 'legacy_fs_objects_v1'")];
|
|
577
|
+
if (marker.length > 0)
|
|
578
|
+
return;
|
|
579
|
+
console.log('[sqlite-vfs] Migrating from legacy fs_objects table...');
|
|
580
|
+
const LEGACY_CHUNK_SIZE = 1_800_000;
|
|
581
|
+
let migratedEntries = 0;
|
|
582
|
+
const paths = [...this.sql.exec("SELECT DISTINCT path FROM fs_objects ORDER BY path")];
|
|
583
|
+
for (const pathRow of paths) {
|
|
584
|
+
const path = String(pathRow.path);
|
|
585
|
+
const sourceRows = [...this.sql.exec(`SELECT chunk_index, parent_path, data, is_dir, size, mtime, mode
|
|
586
|
+
FROM fs_objects WHERE path = ? ORDER BY chunk_index`, path)];
|
|
587
|
+
if (sourceRows.length === 0)
|
|
588
|
+
continue;
|
|
589
|
+
const first = sourceRows[0];
|
|
590
|
+
const parentPath = String(first.parent_path);
|
|
591
|
+
const isDir = Number(first.is_dir) === 1;
|
|
592
|
+
const size = Number(first.size);
|
|
593
|
+
const mtime = Number(first.mtime);
|
|
594
|
+
const mode = Number(first.mode);
|
|
595
|
+
if (!Number.isSafeInteger(size) || size < 0) {
|
|
596
|
+
throw new Error(`EIO: invalid legacy size for ${path}: ${size}`);
|
|
597
|
+
}
|
|
598
|
+
for (const row of sourceRows) {
|
|
599
|
+
if (String(row.parent_path) !== parentPath
|
|
600
|
+
|| (Number(row.is_dir) === 1) !== isDir
|
|
601
|
+
|| Number(row.size) !== size
|
|
602
|
+
|| Number(row.mtime) !== mtime
|
|
603
|
+
|| Number(row.mode) !== mode) {
|
|
604
|
+
throw new Error(`EIO: inconsistent legacy metadata for ${path}`);
|
|
605
|
+
}
|
|
606
|
+
}
|
|
607
|
+
const chunkCount = isDir || size === 0 ? 0 : Math.ceil(size / CHUNK_SIZE);
|
|
608
|
+
if (!isDir) {
|
|
609
|
+
const markerPlan = this.metricsOnlyPlan({
|
|
610
|
+
blobBytes: 0, logicalRows: 1, sqlExecs: 1, affectedPaths: 1,
|
|
611
|
+
});
|
|
612
|
+
this.executeMeasuredTransaction(markerPlan, { source: 'content-stage', limitMode: 'bounded' }, () => {
|
|
613
|
+
this.sql.exec("INSERT OR IGNORE INTO content_lifecycle (content_id, state, created_at) VALUES (?, 'staging', ?)", path, Date.now());
|
|
614
|
+
});
|
|
615
|
+
let builder = new TransactionPlanBuilder();
|
|
616
|
+
const flush = () => {
|
|
617
|
+
if (builder.empty)
|
|
618
|
+
return;
|
|
619
|
+
const plan = builder.build();
|
|
620
|
+
builder = new TransactionPlanBuilder();
|
|
621
|
+
this.assertTransactionFits(plan.metrics);
|
|
622
|
+
this.executeTransactionPlan(plan, { source: 'content-stage', limitMode: 'bounded' });
|
|
623
|
+
};
|
|
624
|
+
let tail = new Uint8Array(0);
|
|
625
|
+
let nextChunkId = 0;
|
|
626
|
+
let bytesRead = 0;
|
|
627
|
+
for (let legacyIndex = 0; legacyIndex < sourceRows.length; legacyIndex++) {
|
|
628
|
+
const row = sourceRows[legacyIndex];
|
|
629
|
+
if (Number(row.chunk_index) !== legacyIndex) {
|
|
630
|
+
throw new Error(`EIO: invalid legacy chunk index ${row.chunk_index} for ${path}`);
|
|
631
|
+
}
|
|
632
|
+
const offset = legacyIndex * LEGACY_CHUNK_SIZE;
|
|
633
|
+
const expectedLength = Math.max(0, Math.min(LEGACY_CHUNK_SIZE, size - offset));
|
|
634
|
+
const data = row.data == null ? new Uint8Array(0) : this.blobToUint8Array(row.data);
|
|
635
|
+
if (offset > size || data.length !== expectedLength) {
|
|
636
|
+
throw new Error(`EIO: invalid legacy chunk ${legacyIndex} for ${path}: expected ${expectedLength} bytes, got ${data.length}`);
|
|
637
|
+
}
|
|
638
|
+
const combined = new Uint8Array(tail.length + data.length);
|
|
639
|
+
combined.set(tail);
|
|
640
|
+
combined.set(data, tail.length);
|
|
641
|
+
let offsetInCombined = 0;
|
|
642
|
+
while (combined.length - offsetInCombined >= CHUNK_SIZE) {
|
|
643
|
+
const chunk = {
|
|
644
|
+
path,
|
|
645
|
+
contentId: path,
|
|
646
|
+
chunkId: nextChunkId++,
|
|
647
|
+
data: combined.slice(offsetInCombined, offsetInCombined + CHUNK_SIZE),
|
|
648
|
+
};
|
|
649
|
+
if (builder.wouldExceedChunkGroup([chunk]) !== null)
|
|
650
|
+
flush();
|
|
651
|
+
builder.addChunk(chunk);
|
|
652
|
+
offsetInCombined += CHUNK_SIZE;
|
|
653
|
+
}
|
|
654
|
+
tail = combined.slice(offsetInCombined);
|
|
655
|
+
bytesRead += data.length;
|
|
656
|
+
}
|
|
657
|
+
if (tail.length > 0) {
|
|
658
|
+
const chunk = {
|
|
659
|
+
path,
|
|
660
|
+
contentId: path,
|
|
661
|
+
chunkId: nextChunkId++,
|
|
662
|
+
data: tail,
|
|
663
|
+
};
|
|
664
|
+
if (builder.wouldExceedChunkGroup([chunk]) !== null)
|
|
665
|
+
flush();
|
|
666
|
+
builder.addChunk(chunk);
|
|
667
|
+
}
|
|
668
|
+
flush();
|
|
669
|
+
if (bytesRead !== size || nextChunkId !== chunkCount) {
|
|
670
|
+
throw new Error(`EIO: migrated content mismatch for ${path}`);
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
const publishBuilder = new TransactionPlanBuilder();
|
|
674
|
+
publishBuilder.addInode({
|
|
675
|
+
path,
|
|
676
|
+
parentPath,
|
|
677
|
+
kind: isDir ? 'directory' : 'file',
|
|
678
|
+
isDir,
|
|
679
|
+
size,
|
|
680
|
+
atime: mtime,
|
|
681
|
+
mtime,
|
|
682
|
+
mode,
|
|
683
|
+
uid: 1000,
|
|
684
|
+
gid: 1000,
|
|
685
|
+
chunkCount,
|
|
686
|
+
contentId: null,
|
|
687
|
+
});
|
|
688
|
+
if (!isDir)
|
|
689
|
+
publishBuilder.addPublishedContent(path);
|
|
690
|
+
const publishPlan = publishBuilder.build();
|
|
691
|
+
this.assertTransactionFits(publishPlan.metrics);
|
|
692
|
+
this.executeTransactionPlan(publishPlan, { source: 'content-publish', limitMode: 'bounded' });
|
|
693
|
+
migratedEntries++;
|
|
694
|
+
}
|
|
695
|
+
const finishPlan = this.metricsOnlyPlan({
|
|
696
|
+
blobBytes: 0, logicalRows: 1, sqlExecs: 2, affectedPaths: 0,
|
|
697
|
+
});
|
|
698
|
+
this.executeMeasuredTransaction(finishPlan, { source: 'content-publish', limitMode: 'bounded' }, () => {
|
|
699
|
+
this.sql.exec("INSERT INTO vfs_schema_migrations (id, applied_at) VALUES ('legacy_fs_objects_v1', ?)", Date.now());
|
|
700
|
+
this.sql.exec("DROP TABLE fs_objects");
|
|
701
|
+
});
|
|
702
|
+
console.log(`[sqlite-vfs] Migration complete: ${migratedEntries} entries migrated.`);
|
|
703
|
+
}
|
|
704
|
+
// ── INode loading ─────────────────────────────────────────────────────
|
|
705
|
+
loadInodes() {
|
|
706
|
+
this.inodes.clear();
|
|
707
|
+
this.children.clear();
|
|
708
|
+
// Reset counters before rescanning (B3).
|
|
709
|
+
this._totalFiles = 0;
|
|
710
|
+
this._totalDirs = 0;
|
|
711
|
+
this._usedBytes = 0;
|
|
712
|
+
const rows = [...this.sql.exec("SELECT path, parent_path, kind, size, atime, mtime, mode, uid, gid, chunk_count, content_id FROM inodes")];
|
|
713
|
+
for (const row of rows) {
|
|
714
|
+
const mtime = Number(row.mtime);
|
|
715
|
+
const atime = Number(row.atime) || mtime;
|
|
716
|
+
const kind = inodeKindFromCode(Number(row.kind));
|
|
717
|
+
const inode = {
|
|
718
|
+
path: String(row.path),
|
|
719
|
+
parentPath: String(row.parent_path),
|
|
720
|
+
kind,
|
|
721
|
+
isDir: kind === 'directory',
|
|
722
|
+
size: Number(row.size),
|
|
723
|
+
atime,
|
|
724
|
+
mtime,
|
|
725
|
+
mode: Number(row.mode),
|
|
726
|
+
uid: Number(row.uid),
|
|
727
|
+
gid: Number(row.gid),
|
|
728
|
+
chunkCount: Number(row.chunk_count),
|
|
729
|
+
contentId: row.content_id === null ? null : String(row.content_id),
|
|
730
|
+
};
|
|
731
|
+
this.inodes.set(inode.path, inode);
|
|
732
|
+
this._addToChildrenIndex(inode.parentPath, inode.path);
|
|
733
|
+
// Bootstrap the counters (B3).
|
|
734
|
+
if (inode.isDir)
|
|
735
|
+
this._totalDirs++;
|
|
736
|
+
else {
|
|
737
|
+
this._totalFiles++;
|
|
738
|
+
this._usedBytes += inode.size;
|
|
739
|
+
}
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
_addToChildrenIndex(parentPath, childPath) {
|
|
743
|
+
let set = this.children.get(parentPath);
|
|
744
|
+
if (!set) {
|
|
745
|
+
set = new Set();
|
|
746
|
+
this.children.set(parentPath, set);
|
|
747
|
+
}
|
|
748
|
+
set.add(childPath);
|
|
749
|
+
}
|
|
750
|
+
_removeFromChildrenIndex(parentPath, childPath) {
|
|
751
|
+
const set = this.children.get(parentPath);
|
|
752
|
+
if (set) {
|
|
753
|
+
set.delete(childPath);
|
|
754
|
+
if (set.size === 0)
|
|
755
|
+
this.children.delete(parentPath);
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
// ── Cache key ─────────────────────────────────────────────────────────
|
|
759
|
+
cacheKey(path, chunkId) {
|
|
760
|
+
return `${path}\0${chunkId}`;
|
|
761
|
+
}
|
|
762
|
+
// ── LRU Content Cache ─────────────────────────────────────────────────
|
|
763
|
+
cacheGet(path, chunkId) {
|
|
764
|
+
const key = this.cacheKey(path, chunkId);
|
|
765
|
+
const entry = this.cache.get(key);
|
|
766
|
+
if (entry) {
|
|
767
|
+
this._cacheHits++;
|
|
768
|
+
// Move to MRU position
|
|
769
|
+
this.cache.delete(key);
|
|
770
|
+
this.cache.set(key, entry);
|
|
771
|
+
return entry.data;
|
|
772
|
+
}
|
|
773
|
+
this._cacheMisses++;
|
|
774
|
+
return null;
|
|
775
|
+
}
|
|
776
|
+
cacheSet(path, chunkId, data) {
|
|
777
|
+
const key = this.cacheKey(path, chunkId);
|
|
778
|
+
const owned = this.copyBytes(data);
|
|
779
|
+
// If already cached, update
|
|
780
|
+
const existing = this.cache.get(key);
|
|
781
|
+
if (existing) {
|
|
782
|
+
this._cacheBytes -= existing.data.length;
|
|
783
|
+
existing.data = owned;
|
|
784
|
+
this._cacheBytes += owned.length;
|
|
785
|
+
// Move to MRU
|
|
786
|
+
this.cache.delete(key);
|
|
787
|
+
this.cache.set(key, existing);
|
|
788
|
+
this.enforceCacheLimit();
|
|
789
|
+
return;
|
|
790
|
+
}
|
|
791
|
+
this._cacheBytes += owned.length;
|
|
792
|
+
this.cache.set(key, { path, chunkId, data: owned });
|
|
793
|
+
this.enforceCacheLimit();
|
|
794
|
+
}
|
|
795
|
+
enforceCacheLimit() {
|
|
796
|
+
while (this.cache.size > this._lruMaxEntries)
|
|
797
|
+
this.evictOne();
|
|
798
|
+
}
|
|
799
|
+
evictOne() {
|
|
800
|
+
// Evict the LRU entry (first in Map iteration order)
|
|
801
|
+
const firstKey = this.cache.keys().next().value;
|
|
802
|
+
if (firstKey === undefined)
|
|
803
|
+
return;
|
|
804
|
+
const entry = this.cache.get(firstKey);
|
|
805
|
+
this.cache.delete(firstKey);
|
|
806
|
+
this._cacheBytes -= entry.data.length;
|
|
807
|
+
this._evictions++;
|
|
808
|
+
}
|
|
809
|
+
// ── W5 Lever 8: public LRU shrink / restore + evictAll ───────────────
|
|
810
|
+
//
|
|
811
|
+
// shrinkForInstall(targetEntries): tighten the cap so heavy-alloc
|
|
812
|
+
// owners (npm install / git clone / pre-bundle) free heap headroom
|
|
813
|
+
// for in-flight RPC and streamed-write payloads. Refcount-based
|
|
814
|
+
// so nested heavy-alloc owners (e.g. concurrent install + clone)
|
|
815
|
+
// don't race; only the OUTERMOST restoreAfterInstall() raises the
|
|
816
|
+
// cap back to LRU_MAX_ENTRIES.
|
|
817
|
+
//
|
|
818
|
+
// Default target 128 entries × 64 KB = 8 MiB. Matches
|
|
819
|
+
// Reduce hot cache pressure while a memory-heavy install is active.
|
|
820
|
+
//
|
|
821
|
+
// The cache is disposable. Cold-cache bounce is acceptable for install
|
|
822
|
+
// workloads because accepted writes are already durable in SQLite.
|
|
823
|
+
shrinkForInstall(targetEntries = 128) {
|
|
824
|
+
const target = Math.max(1, Math.min(LRU_MAX_ENTRIES, targetEntries | 0));
|
|
825
|
+
// Refcount: nested acquires stack. Take the smallest target across
|
|
826
|
+
// owners — most aggressive shrinker wins.
|
|
827
|
+
if (this._lruShrinkRefcount > 0) {
|
|
828
|
+
if (target < this._lruMaxEntries)
|
|
829
|
+
this._lruMaxEntries = target;
|
|
830
|
+
this._lruShrinkRefcount++;
|
|
831
|
+
this.enforceCacheLimit();
|
|
832
|
+
return;
|
|
833
|
+
}
|
|
834
|
+
this._lruShrinkRefcount = 1;
|
|
835
|
+
this._lruMaxEntries = target;
|
|
836
|
+
// Evict down to the new cap; cache eviction is always disposable.
|
|
837
|
+
this.enforceCacheLimit();
|
|
838
|
+
}
|
|
839
|
+
/** Decrement the heavy-alloc refcount. When the count returns to
|
|
840
|
+
* zero, restore the cap to LRU_MAX_ENTRIES. No re-population —
|
|
841
|
+
* the cache warms naturally on next reads. */
|
|
842
|
+
restoreAfterInstall() {
|
|
843
|
+
if (this._lruShrinkRefcount <= 0)
|
|
844
|
+
return;
|
|
845
|
+
this._lruShrinkRefcount--;
|
|
846
|
+
if (this._lruShrinkRefcount === 0) {
|
|
847
|
+
this._lruMaxEntries = LRU_MAX_ENTRIES;
|
|
848
|
+
}
|
|
849
|
+
}
|
|
850
|
+
/** Drop every disposable cache entry before retrying a strict batch. */
|
|
851
|
+
evictAll() {
|
|
852
|
+
// Iterate a snapshot while deleting from the cache.
|
|
853
|
+
const keys = Array.from(this.cache.keys());
|
|
854
|
+
for (const key of keys) {
|
|
855
|
+
const entry = this.cache.get(key);
|
|
856
|
+
if (!entry)
|
|
857
|
+
continue;
|
|
858
|
+
this.cache.delete(key);
|
|
859
|
+
this._cacheBytes -= entry.data.length;
|
|
860
|
+
this._evictions++;
|
|
861
|
+
}
|
|
862
|
+
}
|
|
863
|
+
/**
|
|
864
|
+
* Batch version of cacheInvalidate — invalidate every cache entry
|
|
865
|
+
* whose path is in `paths`. One pass over the cache instead of one
|
|
866
|
+
* pass per path (audit R2: writeBatch was O(P × C) before this).
|
|
867
|
+
*
|
|
868
|
+
*/
|
|
869
|
+
cacheInvalidateBatch(paths) {
|
|
870
|
+
if (paths.size === 0)
|
|
871
|
+
return;
|
|
872
|
+
const toDelete = [];
|
|
873
|
+
for (const [key, entry] of this.cache) {
|
|
874
|
+
if (!paths.has(entry.path))
|
|
875
|
+
continue;
|
|
876
|
+
this._cacheBytes -= entry.data.length;
|
|
877
|
+
toDelete.push(key);
|
|
878
|
+
}
|
|
879
|
+
for (const key of toDelete) {
|
|
880
|
+
this.cache.delete(key);
|
|
881
|
+
}
|
|
882
|
+
}
|
|
883
|
+
// ── Helpers ───────────────────────────────────────────────────────────
|
|
884
|
+
now() { return Date.now(); }
|
|
885
|
+
parentPath(path) {
|
|
886
|
+
return path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : '';
|
|
887
|
+
}
|
|
888
|
+
/** The single content resolver for both legacy-null and generated inodes. */
|
|
889
|
+
contentIdForInode(inode) {
|
|
890
|
+
return inode.contentId ?? this.legacyContentId(inode.path);
|
|
891
|
+
}
|
|
892
|
+
legacyContentId(path) {
|
|
893
|
+
return path;
|
|
894
|
+
}
|
|
895
|
+
createContentId(reservedContentIds) {
|
|
896
|
+
// Canonical VFS keys never have a leading slash, so this namespace cannot
|
|
897
|
+
// overlap legacy IDs, which are raw canonical paths. One combined indexed
|
|
898
|
+
// guard also protects malformed legacy data, durable generations, and a
|
|
899
|
+
// repeated random value; the local reservation protects IDs in one plan.
|
|
900
|
+
// The inode arms mirror sqlNoInodeContentReference: generated and legacy
|
|
901
|
+
// path-keyed references are probed separately so both seek plain indexes.
|
|
902
|
+
for (let attempt = 0; attempt < CONTENT_ID_ALLOCATION_ATTEMPTS; attempt++) {
|
|
903
|
+
const contentId = `/content:${crypto.randomUUID()}`;
|
|
904
|
+
if (reservedContentIds?.has(contentId))
|
|
905
|
+
continue;
|
|
906
|
+
const collision = [...this.sql.exec(`SELECT 1 AS collision FROM file_chunks WHERE content_id = ?
|
|
907
|
+
UNION ALL
|
|
908
|
+
SELECT 1 FROM content_lifecycle WHERE content_id = ?
|
|
909
|
+
UNION ALL
|
|
910
|
+
SELECT 1 FROM inodes WHERE kind != 1 AND content_id = ?
|
|
911
|
+
UNION ALL
|
|
912
|
+
SELECT 1 FROM inodes WHERE kind != 1 AND content_id IS NULL AND path = ?
|
|
913
|
+
LIMIT 1`, contentId, contentId, contentId, contentId)];
|
|
914
|
+
if (collision.length === 0) {
|
|
915
|
+
reservedContentIds?.add(contentId);
|
|
916
|
+
return contentId;
|
|
917
|
+
}
|
|
918
|
+
}
|
|
919
|
+
throw new Error('EIO: failed to allocate a unique VFS content generation');
|
|
920
|
+
}
|
|
921
|
+
blobToUint8Array(blob) {
|
|
922
|
+
if (blob instanceof Uint8Array)
|
|
923
|
+
return blob;
|
|
924
|
+
if (blob instanceof ArrayBuffer)
|
|
925
|
+
return new Uint8Array(blob);
|
|
926
|
+
if (ArrayBuffer.isView(blob))
|
|
927
|
+
return new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength);
|
|
928
|
+
return new Uint8Array(0);
|
|
929
|
+
}
|
|
930
|
+
copyBytes(data) {
|
|
931
|
+
const copy = new Uint8Array(data.length);
|
|
932
|
+
copy.set(data);
|
|
933
|
+
return copy;
|
|
934
|
+
}
|
|
935
|
+
readChunkFromSql(inode, chunkId) {
|
|
936
|
+
this._sqlReads++;
|
|
937
|
+
const rows = [...this.sql.exec("SELECT data FROM file_chunks WHERE content_id = ? AND chunk_id = ?", this.contentIdForInode(inode), chunkId)];
|
|
938
|
+
if (rows.length === 0)
|
|
939
|
+
return null;
|
|
940
|
+
return this.blobToUint8Array(rows[0].data);
|
|
941
|
+
}
|
|
942
|
+
// ── Filesystem operations ─────────────────────────────────────────────
|
|
943
|
+
as(cred) {
|
|
944
|
+
const bound = Object.freeze({
|
|
945
|
+
uid: cred.uid,
|
|
946
|
+
gid: cred.gid,
|
|
947
|
+
groups: Object.freeze([...cred.groups]),
|
|
948
|
+
umask: cred.umask & 0o777,
|
|
949
|
+
});
|
|
950
|
+
return {
|
|
951
|
+
cred: bound,
|
|
952
|
+
exists: (path) => this.exists(path, bound),
|
|
953
|
+
isDirectory: (path) => this.isDirectory(path, bound),
|
|
954
|
+
isFile: (path) => this.isFile(path, bound),
|
|
955
|
+
isSymlink: (path) => this.isSymlink(path, bound),
|
|
956
|
+
access: (path, mode) => { this.checkAccess(path, mode, bound); },
|
|
957
|
+
mkdir: (path, options) => this.mkdir(path, options, bound),
|
|
958
|
+
writeFile: (path, content, options) => this.writeFile(path, content, options, bound),
|
|
959
|
+
symlink: (target, path) => this.symlink(target, path, bound),
|
|
960
|
+
readlink: (path) => this.readlink(path, bound),
|
|
961
|
+
resolveSymlink: (path) => this.resolveSymlink(path, bound),
|
|
962
|
+
readFile: (path) => this.readFile(path, bound),
|
|
963
|
+
readFileUncached: (path) => this.readFileUncached(path, bound),
|
|
964
|
+
readRange: (path, offset, length) => this.readRange(path, offset, length, bound),
|
|
965
|
+
readRangeUncached: (path, offset, length) => (this.readRange(path, offset, length, bound, { cached: false })),
|
|
966
|
+
writeRange: (path, offset, bytes) => this.writeRange(path, offset, bytes, bound),
|
|
967
|
+
appendOnce: (path, pid, writerId, moduleId, operationId, digest, bytes) => (this.appendOnce(path, pid, writerId, moduleId, operationId, digest, bytes, bound)),
|
|
968
|
+
acknowledgeAppend: (pid, writerId, moduleId, operationId) => (this.acknowledgeAppend(pid, writerId, moduleId, operationId)),
|
|
969
|
+
truncate: (path, size) => this.truncate(path, size, bound),
|
|
970
|
+
readFileString: (path) => this.readFileString(path, bound),
|
|
971
|
+
stat: (path) => this.stat(path, bound, true),
|
|
972
|
+
lstat: (path) => this.stat(path, bound, false),
|
|
973
|
+
utimes: (path, atimeMs, mtimeMs) => this.utimes(path, atimeMs, mtimeMs, bound),
|
|
974
|
+
chmod: (path, mode) => this.chmod(path, mode, bound),
|
|
975
|
+
chown: (path, uid, gid, options) => this.chown(path, uid, gid, bound, options?.followSymlinks !== false),
|
|
976
|
+
readdir: (path) => this.readdir(path, bound),
|
|
977
|
+
list: (after, limit) => this.list(after ?? null, Math.min(Math.max(1, Math.trunc(limit ?? FS_LIST_PAGE_LIMIT)), FS_LIST_PAGE_LIMIT), bound),
|
|
978
|
+
unlink: (path) => this.unlink(path, bound),
|
|
979
|
+
rmdir: (path) => this.rmdir(path, bound),
|
|
980
|
+
removeRecursive: (path) => this.removeRecursive(path, bound),
|
|
981
|
+
rename: (oldPath, newPath) => this.rename(oldPath, newPath, bound),
|
|
982
|
+
copyFile: (src, dest) => this.copyFile(src, dest, bound),
|
|
983
|
+
writeBatch: (payload) => this.writeBatch(payload, bound),
|
|
984
|
+
writeStream: (stream, options) => this.writeStream(stream, options, bound),
|
|
985
|
+
mkdirBatch: (paths) => this.mkdirBatch(paths, bound),
|
|
986
|
+
revision: (path) => this.revision(path),
|
|
987
|
+
epoch: this._epoch,
|
|
988
|
+
};
|
|
989
|
+
}
|
|
990
|
+
accessInode(inode, want, cred) {
|
|
991
|
+
return this.accessMode(inode.mode, inode.uid, inode.gid, want, cred);
|
|
992
|
+
}
|
|
993
|
+
accessMode(mode, uid, gid, want, cred) {
|
|
994
|
+
const requested = want & 0o7;
|
|
995
|
+
if (requested === 0)
|
|
996
|
+
return true;
|
|
997
|
+
const permissions = mode & 0o777;
|
|
998
|
+
if (cred.uid === 0) {
|
|
999
|
+
return (requested & 0o1) === 0 || (permissions & 0o111) !== 0;
|
|
1000
|
+
}
|
|
1001
|
+
const shift = cred.uid === uid
|
|
1002
|
+
? 6
|
|
1003
|
+
: cred.gid === gid || cred.groups.includes(gid)
|
|
1004
|
+
? 3
|
|
1005
|
+
: 0;
|
|
1006
|
+
const granted = (permissions >> shift) & 0o7;
|
|
1007
|
+
return (granted & requested) === requested;
|
|
1008
|
+
}
|
|
1009
|
+
resolvePath(path, cred, followLeaf, allowMissing) {
|
|
1010
|
+
let current = normalizeVfsPath(path);
|
|
1011
|
+
const seen = new Set();
|
|
1012
|
+
for (let hops = 0; hops <= 40; hops++) {
|
|
1013
|
+
const parts = current.split('/').filter(Boolean);
|
|
1014
|
+
let prefix = '';
|
|
1015
|
+
let restarted = false;
|
|
1016
|
+
for (let index = 0; index < parts.length; index++) {
|
|
1017
|
+
prefix = prefix ? `${prefix}/${parts[index]}` : parts[index];
|
|
1018
|
+
const inode = this.inodes.get(prefix);
|
|
1019
|
+
const leaf = index === parts.length - 1;
|
|
1020
|
+
if (!inode) {
|
|
1021
|
+
if (leaf || allowMissing)
|
|
1022
|
+
return { path: current, inode: undefined };
|
|
1023
|
+
throw vfsError('ENOENT', prefix);
|
|
1024
|
+
}
|
|
1025
|
+
if (inode.kind === 'symlink' && (!leaf || followLeaf)) {
|
|
1026
|
+
if (seen.has(prefix) || hops === 40)
|
|
1027
|
+
throw vfsError('ELOOP', path);
|
|
1028
|
+
seen.add(prefix);
|
|
1029
|
+
const target = dec.decode(this.readInodeBytes(prefix, inode));
|
|
1030
|
+
const suffix = parts.slice(index + 1).join('/');
|
|
1031
|
+
const resolvedTarget = target.startsWith('/')
|
|
1032
|
+
? normalizeVfsPath(target)
|
|
1033
|
+
: normalizeVfsPath(`${this.parentPath(prefix)}/${target}`);
|
|
1034
|
+
current = suffix ? normalizeVfsPath(`${resolvedTarget}/${suffix}`) : resolvedTarget;
|
|
1035
|
+
restarted = true;
|
|
1036
|
+
break;
|
|
1037
|
+
}
|
|
1038
|
+
if (!leaf) {
|
|
1039
|
+
if (inode.kind !== 'directory')
|
|
1040
|
+
throw vfsError('ENOTDIR', prefix);
|
|
1041
|
+
if (!this.accessInode(inode, 0o1, cred))
|
|
1042
|
+
throw vfsError('EACCES', prefix);
|
|
1043
|
+
}
|
|
1044
|
+
}
|
|
1045
|
+
if (restarted)
|
|
1046
|
+
continue;
|
|
1047
|
+
return { path: current, inode: this.inodes.get(current) };
|
|
1048
|
+
}
|
|
1049
|
+
throw vfsError('ELOOP', path);
|
|
1050
|
+
}
|
|
1051
|
+
checkAccess(path, want, cred, options = {}) {
|
|
1052
|
+
const resolved = this.resolvePath(path, cred, options.followLeaf ?? true, options.allowMissingLeaf ?? false);
|
|
1053
|
+
if (!resolved.inode) {
|
|
1054
|
+
if (options.allowMissingLeaf)
|
|
1055
|
+
return resolved;
|
|
1056
|
+
throw vfsError('ENOENT', normalizeVfsPath(path));
|
|
1057
|
+
}
|
|
1058
|
+
if (!this.accessInode(resolved.inode, want, cred)) {
|
|
1059
|
+
throw vfsError('EACCES', resolved.path);
|
|
1060
|
+
}
|
|
1061
|
+
return resolved;
|
|
1062
|
+
}
|
|
1063
|
+
checkParentAccess(path, cred) {
|
|
1064
|
+
const parent = this.parentPath(normalizeVfsPath(path));
|
|
1065
|
+
if (parent === '')
|
|
1066
|
+
return;
|
|
1067
|
+
const resolved = this.checkAccess(parent, 0o3, cred);
|
|
1068
|
+
if (resolved.inode?.kind !== 'directory')
|
|
1069
|
+
throw vfsError('ENOTDIR', parent);
|
|
1070
|
+
}
|
|
1071
|
+
/**
|
|
1072
|
+
* Shared resolver for the boolean probes (exists/isDirectory/isFile/
|
|
1073
|
+
* isSymlink). Resolution-structure failures — a missing or non-directory
|
|
1074
|
+
* path component — answer `undefined` (fs.existsSync semantics: module
|
|
1075
|
+
* resolvers probe paths through files, e.g. `entry.js/index.js`, and
|
|
1076
|
+
* expect false, not a throw). Permission denials still propagate so
|
|
1077
|
+
* traverse-x enforcement cannot be masked into a quiet false.
|
|
1078
|
+
*/
|
|
1079
|
+
probeInode(path, cred) {
|
|
1080
|
+
try {
|
|
1081
|
+
return this.checkAccess(path, 0, cred, { followLeaf: false, allowMissingLeaf: true }).inode;
|
|
1082
|
+
}
|
|
1083
|
+
catch (error) {
|
|
1084
|
+
const code = error.code;
|
|
1085
|
+
if (code === 'ENOENT' || code === 'ENOTDIR')
|
|
1086
|
+
return undefined;
|
|
1087
|
+
throw error;
|
|
1088
|
+
}
|
|
1089
|
+
}
|
|
1090
|
+
exists(path, cred) {
|
|
1091
|
+
return this.probeInode(path, cred) !== undefined;
|
|
1092
|
+
}
|
|
1093
|
+
isDirectory(path, cred) {
|
|
1094
|
+
return this.probeInode(path, cred)?.kind === 'directory';
|
|
1095
|
+
}
|
|
1096
|
+
isFile(path, cred) {
|
|
1097
|
+
return this.probeInode(path, cred)?.kind === 'file';
|
|
1098
|
+
}
|
|
1099
|
+
isSymlink(path, cred) {
|
|
1100
|
+
return this.probeInode(path, cred)?.kind === 'symlink';
|
|
1101
|
+
}
|
|
1102
|
+
/**
|
|
1103
|
+
* Without a path: the global mutation clock. With a path: the clock
|
|
1104
|
+
* value at the last mutation inside that path's subtree (0 if nothing
|
|
1105
|
+
* under it changed in this DO lifetime). `revision('')` equals the
|
|
1106
|
+
* global clock by construction (every mutation stamps all ancestors).
|
|
1107
|
+
*/
|
|
1108
|
+
revision(path) {
|
|
1109
|
+
if (path === undefined)
|
|
1110
|
+
return this._revision;
|
|
1111
|
+
const p = normalizeVfsPath(path);
|
|
1112
|
+
if (p === '')
|
|
1113
|
+
return this._revision;
|
|
1114
|
+
return this._pathRevisions.get(p) ?? 0;
|
|
1115
|
+
}
|
|
1116
|
+
/**
|
|
1117
|
+
* Advance the clock once, stamp every path + its ancestors, and record
|
|
1118
|
+
* the mutation in the invalidation log.
|
|
1119
|
+
*
|
|
1120
|
+
* This is the single mutation chokepoint for coherence purposes. Five
|
|
1121
|
+
* mutation paths bypass the `_writeBatchOnce` funnel — `_mkdirSingle`,
|
|
1122
|
+
* `utimes`, `chmod`, `chown`, `rename` — but all of them reach here, so
|
|
1123
|
+
* a hook sited anywhere else silently misses renames, which is the
|
|
1124
|
+
* mutation most likely to break a build tool.
|
|
1125
|
+
*
|
|
1126
|
+
* The log records the mutated path AND its parent. A facet's content
|
|
1127
|
+
* cells key on the exact path; its directory-shape view keys on the
|
|
1128
|
+
* parent. Recording only the path would let a facet observe a file's
|
|
1129
|
+
* bytes coherently while still believing the file does not exist.
|
|
1130
|
+
* Recording every ancestor would cost O(depth) entries per write for no
|
|
1131
|
+
* additional coverage, since no facet view keys on a grandparent.
|
|
1132
|
+
*/
|
|
1133
|
+
bumpRevision(paths) {
|
|
1134
|
+
const rev = ++this._revision;
|
|
1135
|
+
for (const path of paths) {
|
|
1136
|
+
let p = normalizeVfsPath(path);
|
|
1137
|
+
const mutated = p;
|
|
1138
|
+
while (p !== '') {
|
|
1139
|
+
this._pathRevisions.set(p, rev);
|
|
1140
|
+
p = this.parentPath(p);
|
|
1141
|
+
}
|
|
1142
|
+
if (mutated === '')
|
|
1143
|
+
continue;
|
|
1144
|
+
this._record(rev, mutated);
|
|
1145
|
+
const parent = this.parentPath(mutated);
|
|
1146
|
+
if (parent !== '')
|
|
1147
|
+
this._record(rev, parent);
|
|
1148
|
+
}
|
|
1149
|
+
if (this._invalidationBytes <= SqliteVFS.INVALIDATION_LOG_MAX_BYTES)
|
|
1150
|
+
return;
|
|
1151
|
+
let dropped = 0;
|
|
1152
|
+
while (dropped < this._invalidations.length
|
|
1153
|
+
&& this._invalidationBytes > SqliteVFS.INVALIDATION_LOG_MAX_BYTES) {
|
|
1154
|
+
this._invalidationBytes -= SqliteVFS.entryBytes(this._invalidations[dropped].path);
|
|
1155
|
+
dropped++;
|
|
1156
|
+
}
|
|
1157
|
+
if (dropped > 0)
|
|
1158
|
+
this._invalidations = this._invalidations.slice(dropped);
|
|
1159
|
+
}
|
|
1160
|
+
/** UTF-16 payload plus a flat allowance for the entry object itself. */
|
|
1161
|
+
static entryBytes(path) {
|
|
1162
|
+
return path.length * 2 + 48;
|
|
1163
|
+
}
|
|
1164
|
+
_record(rev, path) {
|
|
1165
|
+
this._invalidations.push({ rev, path });
|
|
1166
|
+
this._invalidationBytes += SqliteVFS.entryBytes(path);
|
|
1167
|
+
}
|
|
1168
|
+
/**
|
|
1169
|
+
* The paths mutated since `cursor`, for a facet holding a cache stamped
|
|
1170
|
+
* at `(epoch, cursor)`.
|
|
1171
|
+
*
|
|
1172
|
+
* Returns `poison` — meaning "drop the whole resident set" — when the
|
|
1173
|
+
* caller's view cannot be repaired incrementally: a different supervisor
|
|
1174
|
+
* incarnation (its revisions are unrelated to ours), or a cursor older
|
|
1175
|
+
* than the retained log (entries it needed have been dropped). Both
|
|
1176
|
+
* degrade to a cold cache, never to a stale byte.
|
|
1177
|
+
*
|
|
1178
|
+
* Each path carries the revision it was last mutated at inside the window,
|
|
1179
|
+
* not just its name. That is what lets a caller keep a cell it wrote
|
|
1180
|
+
* itself: it holds the revision its own write produced, so a report at
|
|
1181
|
+
* that same revision is its own mutation coming back, while a peer's
|
|
1182
|
+
* later write to the same path reports a HIGHER revision and still
|
|
1183
|
+
* invalidates. A name alone cannot separate those two, and the difference
|
|
1184
|
+
* between them is a whole resident set thrown away on every flush.
|
|
1185
|
+
*/
|
|
1186
|
+
invalidatedSince(epoch, cursor) {
|
|
1187
|
+
const rev = this._revision;
|
|
1188
|
+
if (epoch !== this._epoch || cursor > rev) {
|
|
1189
|
+
return { epoch: this._epoch, rev, paths: [], poison: true };
|
|
1190
|
+
}
|
|
1191
|
+
if (cursor === rev)
|
|
1192
|
+
return { epoch: this._epoch, rev, paths: [], poison: false };
|
|
1193
|
+
const oldest = this._invalidations.length > 0 ? this._invalidations[0].rev : rev + 1;
|
|
1194
|
+
// The log starts at rev 1; a cursor at or above the oldest retained
|
|
1195
|
+
// entry minus one can still be served completely.
|
|
1196
|
+
if (cursor < oldest - 1) {
|
|
1197
|
+
return { epoch: this._epoch, rev, paths: [], poison: true };
|
|
1198
|
+
}
|
|
1199
|
+
// The log is append-ordered by revision, so the last entry for a path
|
|
1200
|
+
// is its newest — the one the caller has to be at or past to keep it.
|
|
1201
|
+
const latest = new Map();
|
|
1202
|
+
for (const entry of this._invalidations) {
|
|
1203
|
+
if (entry.rev > cursor)
|
|
1204
|
+
latest.set(entry.path, entry.rev);
|
|
1205
|
+
}
|
|
1206
|
+
const paths = [...latest].map(([path, pathRev]) => ({ path, rev: pathRev }));
|
|
1207
|
+
return { epoch: this._epoch, rev, paths, poison: false };
|
|
1208
|
+
}
|
|
1209
|
+
acquireExclusiveMutation(path, options = {}) {
|
|
1210
|
+
let root = normalizeVfsPath(path);
|
|
1211
|
+
if (!root)
|
|
1212
|
+
throw vfsError('EINVAL', 'exclusive mutation root cannot be empty');
|
|
1213
|
+
if (options.includeMissingAncestors) {
|
|
1214
|
+
const parts = root.split('/');
|
|
1215
|
+
for (let index = 0; index < parts.length; index++) {
|
|
1216
|
+
const candidate = parts.slice(0, index + 1).join('/');
|
|
1217
|
+
const inode = this.inodes.get(candidate);
|
|
1218
|
+
if (!inode) {
|
|
1219
|
+
root = candidate;
|
|
1220
|
+
break;
|
|
1221
|
+
}
|
|
1222
|
+
if (inode.kind !== 'directory')
|
|
1223
|
+
break;
|
|
1224
|
+
}
|
|
1225
|
+
}
|
|
1226
|
+
for (const lockedRoot of this.exclusiveMutationLeases.values()) {
|
|
1227
|
+
if (pathsOverlap(root, lockedRoot)) {
|
|
1228
|
+
throw vfsError('EBUSY', `${root} overlaps exclusive mutation at ${lockedRoot || '/'}`);
|
|
1229
|
+
}
|
|
1230
|
+
}
|
|
1231
|
+
const owner = crypto.randomUUID();
|
|
1232
|
+
this.exclusiveMutationLeases.set(owner, root);
|
|
1233
|
+
return { root, owner };
|
|
1234
|
+
}
|
|
1235
|
+
acquireGlobalExclusiveMutation() {
|
|
1236
|
+
if (this.exclusiveMutationLeases.size > 0) {
|
|
1237
|
+
throw vfsError('EBUSY', 'session has an active exclusive filesystem mutation');
|
|
1238
|
+
}
|
|
1239
|
+
const owner = crypto.randomUUID();
|
|
1240
|
+
this.exclusiveMutationLeases.set(owner, '');
|
|
1241
|
+
return { root: '', owner };
|
|
1242
|
+
}
|
|
1243
|
+
releaseExclusiveMutation(owner) {
|
|
1244
|
+
this.exclusiveMutationLeases.delete(owner);
|
|
1245
|
+
}
|
|
1246
|
+
hasExclusiveMutation() {
|
|
1247
|
+
return this.exclusiveMutationLeases.size > 0;
|
|
1248
|
+
}
|
|
1249
|
+
withMutationOwner(owner, callback) {
|
|
1250
|
+
if (!owner || !this.exclusiveMutationLeases.has(owner)) {
|
|
1251
|
+
if (owner)
|
|
1252
|
+
throw vfsError('ESTALE', 'exclusive mutation lease is no longer active');
|
|
1253
|
+
return callback();
|
|
1254
|
+
}
|
|
1255
|
+
if (this.activeMutationOwner !== null) {
|
|
1256
|
+
throw new Error('[sqlite-vfs] nested mutation owner scope is not supported');
|
|
1257
|
+
}
|
|
1258
|
+
this.activeMutationOwner = owner;
|
|
1259
|
+
try {
|
|
1260
|
+
return callback();
|
|
1261
|
+
}
|
|
1262
|
+
finally {
|
|
1263
|
+
this.activeMutationOwner = null;
|
|
1264
|
+
}
|
|
1265
|
+
}
|
|
1266
|
+
assertMutationAllowed(path) {
|
|
1267
|
+
this.assertMutationsAllowed([path]);
|
|
1268
|
+
}
|
|
1269
|
+
assertMutationsAllowed(paths) {
|
|
1270
|
+
for (const path of paths) {
|
|
1271
|
+
const normalized = normalizeVfsPath(path);
|
|
1272
|
+
if (this.activeMutationOwner !== null) {
|
|
1273
|
+
const ownedRoot = this.exclusiveMutationLeases.get(this.activeMutationOwner);
|
|
1274
|
+
if (!ownedRoot || (normalized !== ownedRoot && !normalized.startsWith(`${ownedRoot}/`))) {
|
|
1275
|
+
throw vfsError('EPERM', `${normalized} is outside exclusive mutation root ${ownedRoot ?? ''}`);
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
if (this.activeMutationOwner === null &&
|
|
1279
|
+
normalized === LEGACY_SYMLINK_REGISTRY_PATH &&
|
|
1280
|
+
this.exclusiveMutationLeases.size > 0) {
|
|
1281
|
+
throw vfsError('EBUSY', `${normalized} is locked while an exclusive mutation is active`);
|
|
1282
|
+
}
|
|
1283
|
+
for (const [owner, root] of this.exclusiveMutationLeases) {
|
|
1284
|
+
if (!pathsOverlap(normalized, root) || owner === this.activeMutationOwner)
|
|
1285
|
+
continue;
|
|
1286
|
+
throw vfsError('EBUSY', `${normalized} is locked by exclusive mutation at ${root || '/'}`);
|
|
1287
|
+
}
|
|
1288
|
+
}
|
|
1289
|
+
}
|
|
1290
|
+
mkdir(path, options, cred) {
|
|
1291
|
+
const normalized = normalizeVfsPath(path);
|
|
1292
|
+
this.assertMutationsAllowed([normalized]);
|
|
1293
|
+
if (this.exists(normalized, cred))
|
|
1294
|
+
return;
|
|
1295
|
+
if (options?.recursive) {
|
|
1296
|
+
const parts = normalized.split('/').filter(Boolean);
|
|
1297
|
+
let current = '';
|
|
1298
|
+
for (const part of parts) {
|
|
1299
|
+
current = current ? current + '/' + part : part;
|
|
1300
|
+
if (!this.exists(current, cred)) {
|
|
1301
|
+
this.checkParentAccess(current, cred);
|
|
1302
|
+
this._mkdirSingle(current, options.mode, cred);
|
|
1303
|
+
}
|
|
1304
|
+
}
|
|
1305
|
+
}
|
|
1306
|
+
else {
|
|
1307
|
+
this.checkParentAccess(normalized, cred);
|
|
1308
|
+
this._mkdirSingle(normalized, options?.mode, cred);
|
|
1309
|
+
}
|
|
1310
|
+
}
|
|
1311
|
+
_mkdirSingle(path, requestedMode, cred) {
|
|
1312
|
+
const pp = this.parentPath(path);
|
|
1313
|
+
const now = this.now();
|
|
1314
|
+
const mode = (requestedMode ?? 0o777) & ~cred.umask & 0o7777;
|
|
1315
|
+
this.sql.exec("INSERT OR REPLACE INTO inodes (path, parent_path, kind, size, atime, mtime, mode, uid, gid, chunk_count, content_id) VALUES (?, ?, 1, 0, ?, ?, ?, ?, ?, 0, NULL)", path, pp, now, now, mode, cred.uid, cred.gid);
|
|
1316
|
+
const inode = {
|
|
1317
|
+
path,
|
|
1318
|
+
parentPath: pp,
|
|
1319
|
+
kind: 'directory',
|
|
1320
|
+
isDir: true,
|
|
1321
|
+
size: 0,
|
|
1322
|
+
atime: now,
|
|
1323
|
+
mtime: now,
|
|
1324
|
+
mode,
|
|
1325
|
+
uid: cred.uid,
|
|
1326
|
+
gid: cred.gid,
|
|
1327
|
+
chunkCount: 0,
|
|
1328
|
+
contentId: null,
|
|
1329
|
+
};
|
|
1330
|
+
this.inodes.set(path, inode);
|
|
1331
|
+
this._addToChildrenIndex(pp, path);
|
|
1332
|
+
this._totalDirs++; // B3
|
|
1333
|
+
this.bumpRevision([path]);
|
|
1334
|
+
this.events.emit('addDir', path);
|
|
1335
|
+
}
|
|
1336
|
+
writeFile(path, content, options, cred, onCommit) {
|
|
1337
|
+
this.assertMutationsAllowed([path]);
|
|
1338
|
+
const resolved = this.checkAccess(path, 0, cred, { allowMissingLeaf: true });
|
|
1339
|
+
const effectivePath = resolved.path;
|
|
1340
|
+
if (resolved.inode) {
|
|
1341
|
+
if (resolved.inode.kind === 'directory')
|
|
1342
|
+
throw vfsError('EISDIR', effectivePath);
|
|
1343
|
+
if (resolved.inode.kind !== 'file')
|
|
1344
|
+
throw vfsError('EINVAL', `${effectivePath} is not a regular file`);
|
|
1345
|
+
if (!this.accessInode(resolved.inode, 0o2, cred))
|
|
1346
|
+
throw vfsError('EACCES', effectivePath);
|
|
1347
|
+
}
|
|
1348
|
+
else {
|
|
1349
|
+
this.checkParentAccess(effectivePath, cred);
|
|
1350
|
+
}
|
|
1351
|
+
const data = typeof content === 'string' ? enc.encode(content) : content;
|
|
1352
|
+
const pp = this.parentPath(effectivePath);
|
|
1353
|
+
const now = this.now();
|
|
1354
|
+
const chunkCount = data.length === 0 ? 0 : Math.ceil(data.length / CHUNK_SIZE);
|
|
1355
|
+
const chunks = [];
|
|
1356
|
+
for (let chunkId = 0; chunkId < chunkCount; chunkId++) {
|
|
1357
|
+
chunks.push({
|
|
1358
|
+
path: effectivePath,
|
|
1359
|
+
chunkId,
|
|
1360
|
+
data: data.subarray(chunkId * CHUNK_SIZE, (chunkId + 1) * CHUNK_SIZE),
|
|
1361
|
+
});
|
|
1362
|
+
}
|
|
1363
|
+
// POSIX: rewriting an existing file never changes its mode; the mode
|
|
1364
|
+
// is chosen only at creation (open(2) O_CREAT).
|
|
1365
|
+
const prior = this.inodes.get(effectivePath);
|
|
1366
|
+
const inode = {
|
|
1367
|
+
path: effectivePath,
|
|
1368
|
+
parentPath: pp,
|
|
1369
|
+
kind: 'file',
|
|
1370
|
+
isDir: false,
|
|
1371
|
+
size: data.length,
|
|
1372
|
+
atime: now,
|
|
1373
|
+
mtime: now,
|
|
1374
|
+
mode: prior?.kind === 'file'
|
|
1375
|
+
? prior.mode
|
|
1376
|
+
: (options?.mode ?? 0o666) & ~cred.umask & 0o7777,
|
|
1377
|
+
uid: prior?.uid ?? cred.uid,
|
|
1378
|
+
gid: prior?.gid ?? cred.gid,
|
|
1379
|
+
chunkCount,
|
|
1380
|
+
};
|
|
1381
|
+
try {
|
|
1382
|
+
this.writeBatch({ inodes: [inode], chunks }, cred, onCommit);
|
|
1383
|
+
}
|
|
1384
|
+
catch (error) {
|
|
1385
|
+
if (!(error instanceof SqliteVfsTransactionTooLargeError))
|
|
1386
|
+
throw error;
|
|
1387
|
+
this.replaceFileWithStagedContent(inode, chunks, undefined, onCommit);
|
|
1388
|
+
}
|
|
1389
|
+
}
|
|
1390
|
+
symlink(target, path, cred) {
|
|
1391
|
+
this.assertMutationsAllowed([path]);
|
|
1392
|
+
const normalized = normalizeVfsPath(path);
|
|
1393
|
+
const prior = this.checkAccess(normalized, 0, cred, { followLeaf: false, allowMissingLeaf: true });
|
|
1394
|
+
if (prior.inode)
|
|
1395
|
+
throw vfsError('EEXIST', normalized);
|
|
1396
|
+
this.checkParentAccess(normalized, cred);
|
|
1397
|
+
const data = enc.encode(target);
|
|
1398
|
+
const now = this.now();
|
|
1399
|
+
const chunkCount = data.length === 0 ? 0 : Math.ceil(data.length / CHUNK_SIZE);
|
|
1400
|
+
const inode = {
|
|
1401
|
+
path: normalized,
|
|
1402
|
+
parentPath: this.parentPath(normalized),
|
|
1403
|
+
kind: 'symlink',
|
|
1404
|
+
isDir: false,
|
|
1405
|
+
size: data.length,
|
|
1406
|
+
atime: now,
|
|
1407
|
+
mtime: now,
|
|
1408
|
+
mode: inodeTypeBits('symlink') | 0o777,
|
|
1409
|
+
uid: cred.uid,
|
|
1410
|
+
gid: cred.gid,
|
|
1411
|
+
chunkCount,
|
|
1412
|
+
};
|
|
1413
|
+
const chunks = Array.from({ length: chunkCount }, (_, chunkId) => ({
|
|
1414
|
+
path: normalized,
|
|
1415
|
+
chunkId,
|
|
1416
|
+
data: data.subarray(chunkId * CHUNK_SIZE, (chunkId + 1) * CHUNK_SIZE),
|
|
1417
|
+
}));
|
|
1418
|
+
this.writeBatch({ inodes: [inode], chunks }, cred);
|
|
1419
|
+
}
|
|
1420
|
+
readlink(path, cred) {
|
|
1421
|
+
const resolved = this.checkAccess(path, 0, cred, { followLeaf: false });
|
|
1422
|
+
if (resolved.inode?.kind !== 'symlink')
|
|
1423
|
+
throw vfsError('EINVAL', `${path} is not a symlink`);
|
|
1424
|
+
return dec.decode(this.readInodeBytes(resolved.path, resolved.inode));
|
|
1425
|
+
}
|
|
1426
|
+
resolveSymlink(path, cred) {
|
|
1427
|
+
try {
|
|
1428
|
+
return this.resolvePath(path, cred, true, false).path;
|
|
1429
|
+
}
|
|
1430
|
+
catch (error) {
|
|
1431
|
+
if (typeof error === 'object' && error !== null && 'code' in error && error.code === 'ELOOP') {
|
|
1432
|
+
return null;
|
|
1433
|
+
}
|
|
1434
|
+
throw error;
|
|
1435
|
+
}
|
|
1436
|
+
}
|
|
1437
|
+
/** Read one chunk via cache → SQL, caching on miss. */
|
|
1438
|
+
readChunk(inode, chunkId) {
|
|
1439
|
+
const path = inode.path;
|
|
1440
|
+
const cached = this.cacheGet(path, chunkId);
|
|
1441
|
+
if (cached)
|
|
1442
|
+
return cached;
|
|
1443
|
+
const data = this.readChunkFromSql(inode, chunkId);
|
|
1444
|
+
if (data)
|
|
1445
|
+
this.cacheSet(path, chunkId, data);
|
|
1446
|
+
return data;
|
|
1447
|
+
}
|
|
1448
|
+
readFile(path, cred) {
|
|
1449
|
+
const resolved = this.checkAccess(path, 0o4, cred);
|
|
1450
|
+
const inode = resolved.inode;
|
|
1451
|
+
if (!inode)
|
|
1452
|
+
throw vfsError('ENOENT', path);
|
|
1453
|
+
if (inode.kind === 'directory')
|
|
1454
|
+
throw vfsError('EISDIR', resolved.path);
|
|
1455
|
+
if (inode.kind !== 'file')
|
|
1456
|
+
throw vfsError('EINVAL', `${resolved.path} is not a regular file`);
|
|
1457
|
+
return this.readInodeBytes(resolved.path, inode);
|
|
1458
|
+
}
|
|
1459
|
+
readInodeBytes(path, inode) {
|
|
1460
|
+
if (inode.size === 0 || inode.chunkCount === 0)
|
|
1461
|
+
return new Uint8Array(0);
|
|
1462
|
+
if (inode.chunkCount === 1) {
|
|
1463
|
+
return this.requireChunk(path, inode, 0).slice(0, inode.size);
|
|
1464
|
+
}
|
|
1465
|
+
// Multi-chunk: reassemble
|
|
1466
|
+
const chunks = [];
|
|
1467
|
+
let totalRead = 0;
|
|
1468
|
+
for (let i = 0; i < inode.chunkCount; i++) {
|
|
1469
|
+
const chunk = this.requireChunk(path, inode, i);
|
|
1470
|
+
chunks.push(chunk);
|
|
1471
|
+
totalRead += chunk.length;
|
|
1472
|
+
}
|
|
1473
|
+
if (totalRead < inode.size) {
|
|
1474
|
+
throw new Error(`EIO: ${path}: declared size ${inode.size} exceeds chunk bytes ${totalRead}`);
|
|
1475
|
+
}
|
|
1476
|
+
const result = new Uint8Array(inode.size);
|
|
1477
|
+
let offset = 0;
|
|
1478
|
+
for (const c of chunks) {
|
|
1479
|
+
const length = Math.min(c.length, result.length - offset);
|
|
1480
|
+
if (length <= 0)
|
|
1481
|
+
break;
|
|
1482
|
+
result.set(c.subarray(0, length), offset);
|
|
1483
|
+
offset += length;
|
|
1484
|
+
}
|
|
1485
|
+
return result;
|
|
1486
|
+
}
|
|
1487
|
+
requireChunk(path, inode, chunkId) {
|
|
1488
|
+
const chunk = this.readChunk(inode, chunkId);
|
|
1489
|
+
if (chunk)
|
|
1490
|
+
return chunk;
|
|
1491
|
+
throw new Error(`EIO: ${path}: missing declared chunk ${chunkId} of ${inode.chunkCount} (size ${inode.size})`);
|
|
1492
|
+
}
|
|
1493
|
+
/**
|
|
1494
|
+
* Read a whole file straight from SQL, bypassing the LRU content cache
|
|
1495
|
+
* entirely (neither consulted nor populated). For one-shot bulk reads
|
|
1496
|
+
* of large runtime binaries (e.g. the 31 MiB clang.wasm at facet
|
|
1497
|
+
* warm-up) that would otherwise evict the user's hot working set and
|
|
1498
|
+
* pin the file's chunks — the full 32 MiB LRU — resident in the DO heap
|
|
1499
|
+
* for the whole session. Demand-paging cache semantics are wrong for a
|
|
1500
|
+
* blob read once and handed to a Worker Loader module map.
|
|
1501
|
+
*/
|
|
1502
|
+
readFileUncached(path, cred) {
|
|
1503
|
+
const resolved = this.checkAccess(path, 0o4, cred);
|
|
1504
|
+
const inode = resolved.inode;
|
|
1505
|
+
if (!inode)
|
|
1506
|
+
throw vfsError('ENOENT', path);
|
|
1507
|
+
if (inode.kind === 'directory')
|
|
1508
|
+
throw vfsError('EISDIR', resolved.path);
|
|
1509
|
+
if (inode.kind !== 'file')
|
|
1510
|
+
throw vfsError('EINVAL', `${resolved.path} is not a regular file`);
|
|
1511
|
+
if (inode.size === 0 || inode.chunkCount === 0)
|
|
1512
|
+
return new Uint8Array(0);
|
|
1513
|
+
const result = new Uint8Array(inode.size);
|
|
1514
|
+
let offset = 0;
|
|
1515
|
+
for (let i = 0; i < inode.chunkCount; i++) {
|
|
1516
|
+
const chunk = this.readChunkFromSql(inode, i);
|
|
1517
|
+
if (!chunk) {
|
|
1518
|
+
throw new Error(`EIO: ${resolved.path}: missing declared chunk ${i} of ${inode.chunkCount} (size ${inode.size})`);
|
|
1519
|
+
}
|
|
1520
|
+
const length = Math.min(chunk.length, result.length - offset);
|
|
1521
|
+
if (length <= 0)
|
|
1522
|
+
break;
|
|
1523
|
+
result.set(chunk.subarray(0, length), offset);
|
|
1524
|
+
offset += length;
|
|
1525
|
+
}
|
|
1526
|
+
if (offset < inode.size) {
|
|
1527
|
+
throw new Error(`EIO: ${resolved.path}: declared size ${inode.size} exceeds chunk bytes ${offset}`);
|
|
1528
|
+
}
|
|
1529
|
+
return result;
|
|
1530
|
+
}
|
|
1531
|
+
/**
|
|
1532
|
+
* Read `length` bytes at `offset` without assembling the whole file —
|
|
1533
|
+
* only the chunks overlapping the range are touched. Reads past EOF
|
|
1534
|
+
* are clamped; missing spans retain the existing zero-fill range semantics.
|
|
1535
|
+
*/
|
|
1536
|
+
/**
|
|
1537
|
+
* `cached: false` reads the range straight from SQL, neither consulting nor
|
|
1538
|
+
* populating the LRU — the ranged counterpart of `readFileUncached`, and it
|
|
1539
|
+
* exists for the same reason. A boot spec's by-path members are the largest
|
|
1540
|
+
* files a session holds (a ruby interpreter image is 34.3 MiB against a
|
|
1541
|
+
* 32 MiB LRU) and each is read once and handed to a Worker Loader module
|
|
1542
|
+
* map, so demand-paging semantics are simply wrong for them: caching one
|
|
1543
|
+
* evicts the user's whole hot working set and pins the blob in this DO's
|
|
1544
|
+
* heap for the rest of the session. Ranged rather than whole-file because a
|
|
1545
|
+
* host that is not this DO reads them in slices that fit an RPC value, and
|
|
1546
|
+
* re-reading the whole file per slice would multiply the SQL work by the
|
|
1547
|
+
* slice count.
|
|
1548
|
+
*/
|
|
1549
|
+
readRange(path, offset, length, cred, options = {}) {
|
|
1550
|
+
const resolved = this.checkAccess(path, 0o4, cred);
|
|
1551
|
+
const inode = resolved.inode;
|
|
1552
|
+
if (!inode)
|
|
1553
|
+
throw vfsError('ENOENT', path);
|
|
1554
|
+
if (inode.kind === 'directory')
|
|
1555
|
+
throw vfsError('EISDIR', resolved.path);
|
|
1556
|
+
if (inode.kind !== 'file')
|
|
1557
|
+
throw vfsError('EINVAL', `${resolved.path} is not a regular file`);
|
|
1558
|
+
const start = clampNonNegativeInt(offset);
|
|
1559
|
+
const end = Math.min(inode.size, start + clampNonNegativeInt(length));
|
|
1560
|
+
if (start >= end)
|
|
1561
|
+
return new Uint8Array(0);
|
|
1562
|
+
const cached = options.cached !== false;
|
|
1563
|
+
const out = new Uint8Array(end - start);
|
|
1564
|
+
const firstChunk = Math.floor(start / CHUNK_SIZE);
|
|
1565
|
+
const lastChunk = Math.floor((end - 1) / CHUNK_SIZE);
|
|
1566
|
+
for (let i = firstChunk; i <= lastChunk; i++) {
|
|
1567
|
+
const chunk = cached ? this.readChunk(inode, i) : this.readChunkFromSql(inode, i);
|
|
1568
|
+
if (!chunk)
|
|
1569
|
+
continue;
|
|
1570
|
+
const chunkStart = i * CHUNK_SIZE;
|
|
1571
|
+
const copyFrom = Math.max(start, chunkStart);
|
|
1572
|
+
const copyTo = Math.min(end, chunkStart + chunk.length);
|
|
1573
|
+
if (copyFrom >= copyTo)
|
|
1574
|
+
continue;
|
|
1575
|
+
out.set(chunk.subarray(copyFrom - chunkStart, copyTo - chunkStart), copyFrom - start);
|
|
1576
|
+
}
|
|
1577
|
+
return out;
|
|
1578
|
+
}
|
|
1579
|
+
/**
|
|
1580
|
+
* Overwrite `bytes` at `offset`, rewriting only the chunks the range
|
|
1581
|
+
* (plus any EOF extension) touches — file-handle and page writers must
|
|
1582
|
+
* not pay a whole-file rewrite. Writing past EOF zero-fills the gap so
|
|
1583
|
+
* every chunk row up to the new EOF stays materialized at its
|
|
1584
|
+
* positional length (readFile reassembles by plain concatenation).
|
|
1585
|
+
* Creates the file when missing; callers own parent-dir creation
|
|
1586
|
+
* (same contract as writeFile).
|
|
1587
|
+
*/
|
|
1588
|
+
writeRange(path, offset, bytes, cred, onCommit) {
|
|
1589
|
+
this.assertMutationsAllowed([path]);
|
|
1590
|
+
const resolved = this.checkAccess(path, 0, cred, { allowMissingLeaf: true });
|
|
1591
|
+
const effectivePath = resolved.path;
|
|
1592
|
+
const prior = resolved.inode;
|
|
1593
|
+
if (prior?.kind === 'directory')
|
|
1594
|
+
throw vfsError('EISDIR', effectivePath);
|
|
1595
|
+
if (prior && prior.kind !== 'file')
|
|
1596
|
+
throw vfsError('EINVAL', `${effectivePath} is not a regular file`);
|
|
1597
|
+
if (prior && !this.accessInode(prior, 0o2, cred))
|
|
1598
|
+
throw vfsError('EACCES', effectivePath);
|
|
1599
|
+
if (!prior)
|
|
1600
|
+
this.checkParentAccess(effectivePath, cred);
|
|
1601
|
+
const isNew = prior === undefined;
|
|
1602
|
+
const start = clampNonNegativeInt(offset);
|
|
1603
|
+
const end = start + bytes.length;
|
|
1604
|
+
if (isNew) {
|
|
1605
|
+
const initial = new Uint8Array(end);
|
|
1606
|
+
initial.set(bytes, start);
|
|
1607
|
+
this.writeFile(effectivePath, initial, undefined, cred, onCommit);
|
|
1608
|
+
return;
|
|
1609
|
+
}
|
|
1610
|
+
// POSIX pwrite of zero bytes never extends or dirties an existing file.
|
|
1611
|
+
if (bytes.length === 0) {
|
|
1612
|
+
if (onCommit)
|
|
1613
|
+
this.transactionSync(onCommit);
|
|
1614
|
+
return;
|
|
1615
|
+
}
|
|
1616
|
+
const oldSize = prior.size;
|
|
1617
|
+
const oldChunkCount = prior.chunkCount;
|
|
1618
|
+
const newSize = Math.max(oldSize, end);
|
|
1619
|
+
const now = this.now();
|
|
1620
|
+
const changedChunks = new Map();
|
|
1621
|
+
if (newSize > 0) {
|
|
1622
|
+
// Contiguous chunk span: the written range, plus the stretch from
|
|
1623
|
+
// the old EOF when extending (gap chunks zero-fill; a partial old
|
|
1624
|
+
// last chunk is re-materialized at its grown length).
|
|
1625
|
+
const extending = end > oldSize;
|
|
1626
|
+
const fromChunk = Math.floor((extending ? Math.min(start, oldSize) : start) / CHUNK_SIZE);
|
|
1627
|
+
const toChunk = Math.floor(((extending ? newSize : end) - 1) / CHUNK_SIZE);
|
|
1628
|
+
for (let i = fromChunk; i <= toChunk; i++) {
|
|
1629
|
+
const chunkStart = i * CHUNK_SIZE;
|
|
1630
|
+
const chunkLen = Math.min(CHUNK_SIZE, newSize - chunkStart);
|
|
1631
|
+
const overlayFrom = Math.max(start, chunkStart);
|
|
1632
|
+
const overlayTo = Math.min(end, chunkStart + chunkLen);
|
|
1633
|
+
const chunk = new Uint8Array(chunkLen);
|
|
1634
|
+
const fullyCovered = overlayFrom <= chunkStart && overlayTo >= chunkStart + chunkLen;
|
|
1635
|
+
if (!fullyCovered && i < oldChunkCount) {
|
|
1636
|
+
const existing = this.requireChunk(effectivePath, prior, i);
|
|
1637
|
+
chunk.set(existing.subarray(0, Math.min(existing.length, chunkLen)), 0);
|
|
1638
|
+
}
|
|
1639
|
+
if (overlayFrom < overlayTo) {
|
|
1640
|
+
chunk.set(bytes.subarray(overlayFrom - start, overlayTo - start), overlayFrom - chunkStart);
|
|
1641
|
+
}
|
|
1642
|
+
changedChunks.set(i, chunk);
|
|
1643
|
+
}
|
|
1644
|
+
}
|
|
1645
|
+
const newChunkCount = newSize === 0 ? 0 : Math.ceil(newSize / CHUNK_SIZE);
|
|
1646
|
+
const next = this.updatedFileInode(prior, newSize, newChunkCount, now);
|
|
1647
|
+
if (this.commitCurrentContentMutation(prior, next, changedChunks, [], onCommit))
|
|
1648
|
+
return;
|
|
1649
|
+
this.replaceFileWithGeneratedContent(next, (chunkId) => this.generatedMutationChunk(prior, next, changedChunks, chunkId), onCommit);
|
|
1650
|
+
}
|
|
1651
|
+
/**
|
|
1652
|
+
* Publish an append and its dedupe receipt in the same SQLite transaction.
|
|
1653
|
+
* Large content may stage privately first, but its inode publication and
|
|
1654
|
+
* receipt still share the final transaction. Receipts are removed only by
|
|
1655
|
+
* explicit client acknowledgement after that client relinquishes retries.
|
|
1656
|
+
*/
|
|
1657
|
+
appendOnce(path, pid, writerId, moduleId, operationId, digest, bytes, cred) {
|
|
1658
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1659
|
+
throw vfsError('EINVAL', `invalid append pid ${pid}`);
|
|
1660
|
+
assertAppendIncarnation(writerId, 'writer');
|
|
1661
|
+
assertAppendIncarnation(moduleId, 'module');
|
|
1662
|
+
if (!Number.isSafeInteger(operationId) || operationId <= 0) {
|
|
1663
|
+
throw vfsError('EINVAL', `invalid append operation ${operationId}`);
|
|
1664
|
+
}
|
|
1665
|
+
const normalized = normalizeVfsPath(path);
|
|
1666
|
+
const pidRevoked = [...this.sql.exec('SELECT 1 AS revoked FROM vfs_append_pid_revocations WHERE pid = ?', pid)].length > 0;
|
|
1667
|
+
if (pidRevoked)
|
|
1668
|
+
throw vfsError('ESTALE', `append process ${pid} is being retired`);
|
|
1669
|
+
const writer = [...this.sql.exec(`SELECT revoked FROM vfs_append_writer_state
|
|
1670
|
+
WHERE pid = ? AND writer_id = ?`, pid, writerId)][0];
|
|
1671
|
+
if (!writer || Number(writer.revoked) !== 0) {
|
|
1672
|
+
throw vfsError('ESTALE', `append writer ${pid}/${writerId} is unavailable`);
|
|
1673
|
+
}
|
|
1674
|
+
let moduleState = [...this.sql.exec(`SELECT acked_through FROM vfs_append_module_state
|
|
1675
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ?`, pid, writerId, moduleId)][0];
|
|
1676
|
+
const ackedThrough = Number(moduleState?.acked_through ?? 0);
|
|
1677
|
+
if (operationId <= ackedThrough)
|
|
1678
|
+
return bytes.byteLength;
|
|
1679
|
+
const acknowledgedGap = [...this.sql.exec(`SELECT path, byte_length, digest
|
|
1680
|
+
FROM vfs_append_acked_gaps
|
|
1681
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id = ?`, pid, writerId, moduleId, operationId)][0];
|
|
1682
|
+
if (acknowledgedGap) {
|
|
1683
|
+
if (acknowledgedGap.path !== normalized
|
|
1684
|
+
|| Number(acknowledgedGap.byte_length) !== bytes.byteLength
|
|
1685
|
+
|| acknowledgedGap.digest !== digest) {
|
|
1686
|
+
throw vfsError('EINVAL', `append acknowledgement collision for ${pid}/${operationId}`);
|
|
1687
|
+
}
|
|
1688
|
+
return bytes.byteLength;
|
|
1689
|
+
}
|
|
1690
|
+
const existing = [...this.sql.exec(`SELECT path, byte_length, digest
|
|
1691
|
+
FROM vfs_append_receipts
|
|
1692
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id = ?`, pid, writerId, moduleId, operationId)][0];
|
|
1693
|
+
if (existing) {
|
|
1694
|
+
if (existing.path !== normalized
|
|
1695
|
+
|| Number(existing.byte_length) !== bytes.byteLength
|
|
1696
|
+
|| existing.digest !== digest) {
|
|
1697
|
+
throw vfsError('EINVAL', `append receipt collision for ${pid}/${writerId}/${operationId}`);
|
|
1698
|
+
}
|
|
1699
|
+
return bytes.byteLength;
|
|
1700
|
+
}
|
|
1701
|
+
const highestKnown = Number([...this.sql.exec(`SELECT MAX(operation_id) AS operation_id
|
|
1702
|
+
FROM (
|
|
1703
|
+
SELECT operation_id FROM vfs_append_receipts
|
|
1704
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ?
|
|
1705
|
+
UNION ALL
|
|
1706
|
+
SELECT operation_id FROM vfs_append_acked_gaps
|
|
1707
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ?
|
|
1708
|
+
)`, pid, writerId, moduleId, pid, writerId, moduleId)][0]?.operation_id
|
|
1709
|
+
?? ackedThrough);
|
|
1710
|
+
if (operationId > Math.max(ackedThrough, highestKnown) + VFS_APPEND_RECEIPT_LIMIT) {
|
|
1711
|
+
throw vfsError('EINVAL', `append operation gap exceeds ${VFS_APPEND_RECEIPT_LIMIT}`);
|
|
1712
|
+
}
|
|
1713
|
+
const retainedCount = Number([...this.sql.exec(`SELECT
|
|
1714
|
+
(SELECT COUNT(*) FROM vfs_append_writer_state WHERE revoked = 0)
|
|
1715
|
+
+ (SELECT COUNT(*) FROM vfs_append_module_state)
|
|
1716
|
+
+ (SELECT COUNT(*) FROM vfs_append_receipts)
|
|
1717
|
+
+ (SELECT COUNT(*) FROM vfs_append_acked_gaps) AS count`)][0]?.count ?? 0);
|
|
1718
|
+
const requiredRows = 1 + (moduleState ? 0 : 1);
|
|
1719
|
+
if (retainedCount > VFS_APPEND_RECEIPT_LIMIT - requiredRows) {
|
|
1720
|
+
throw vfsError('ENOSPC', 'append receipt journal is full');
|
|
1721
|
+
}
|
|
1722
|
+
if (!moduleState) {
|
|
1723
|
+
this.sql.exec(`INSERT INTO vfs_append_module_state
|
|
1724
|
+
(pid, writer_id, module_id, acked_through) VALUES (?, ?, ?, 0)`, pid, writerId, moduleId);
|
|
1725
|
+
moduleState = { acked_through: 0 };
|
|
1726
|
+
}
|
|
1727
|
+
const resolved = this.checkAccess(normalized, 0, cred, { allowMissingLeaf: true });
|
|
1728
|
+
const effectivePath = resolved.path;
|
|
1729
|
+
const inode = resolved.inode;
|
|
1730
|
+
if (inode?.kind === 'directory')
|
|
1731
|
+
throw vfsError('EISDIR', effectivePath);
|
|
1732
|
+
if (inode && inode.kind !== 'file') {
|
|
1733
|
+
throw vfsError('EINVAL', `${effectivePath} is not a regular file`);
|
|
1734
|
+
}
|
|
1735
|
+
const offset = inode?.size ?? 0;
|
|
1736
|
+
const recordReceipt = () => {
|
|
1737
|
+
this.sql.exec(`INSERT INTO vfs_append_receipts
|
|
1738
|
+
(pid, writer_id, module_id, operation_id, path, byte_length, digest, created_at)
|
|
1739
|
+
VALUES (?, ?, ?, ?, ?, ?, ?, ?)`, pid, writerId, moduleId, operationId, normalized, bytes.byteLength, digest, Date.now());
|
|
1740
|
+
};
|
|
1741
|
+
this.writeRange(effectivePath, offset, bytes, cred, recordReceipt);
|
|
1742
|
+
return bytes.byteLength;
|
|
1743
|
+
}
|
|
1744
|
+
activateAppendWriter(pid, writerId) {
|
|
1745
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1746
|
+
throw vfsError('EINVAL', `invalid append pid ${pid}`);
|
|
1747
|
+
assertAppendIncarnation(writerId, 'writer');
|
|
1748
|
+
if ([...this.sql.exec('SELECT 1 AS revoked FROM vfs_append_pid_revocations WHERE pid = ?', pid)].length > 0) {
|
|
1749
|
+
throw vfsError('ESTALE', `append process ${pid} is being retired`);
|
|
1750
|
+
}
|
|
1751
|
+
const writers = [...this.sql.exec(`SELECT writer_id, revoked FROM vfs_append_writer_state
|
|
1752
|
+
WHERE pid = ?`, pid)];
|
|
1753
|
+
const writer = writers.find((candidate) => candidate.writer_id === writerId);
|
|
1754
|
+
if (writer && Number(writer.revoked) === 0) {
|
|
1755
|
+
return;
|
|
1756
|
+
}
|
|
1757
|
+
if (writers.length > 0) {
|
|
1758
|
+
throw vfsError('ESTALE', `append process ${pid} already has a different writer`);
|
|
1759
|
+
}
|
|
1760
|
+
const retainedCount = Number([...this.sql.exec(`SELECT
|
|
1761
|
+
(SELECT COUNT(*) FROM vfs_append_writer_state WHERE revoked = 0)
|
|
1762
|
+
+ (SELECT COUNT(*) FROM vfs_append_module_state)
|
|
1763
|
+
+ (SELECT COUNT(*) FROM vfs_append_receipts)
|
|
1764
|
+
+ (SELECT COUNT(*) FROM vfs_append_acked_gaps) AS count`)][0]?.count ?? 0);
|
|
1765
|
+
if (retainedCount >= VFS_APPEND_RECEIPT_LIMIT) {
|
|
1766
|
+
throw vfsError('ENOSPC', 'append receipt journal is full');
|
|
1767
|
+
}
|
|
1768
|
+
this.transactionSync(() => {
|
|
1769
|
+
this.sql.exec(`INSERT INTO vfs_append_writer_state
|
|
1770
|
+
(pid, writer_id, revoked, retired_at) VALUES (?, ?, 0, NULL)`, pid, writerId);
|
|
1771
|
+
});
|
|
1772
|
+
}
|
|
1773
|
+
acknowledgeAppend(pid, writerId, moduleId, operationId) {
|
|
1774
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1775
|
+
throw vfsError('EINVAL', `invalid append pid ${pid}`);
|
|
1776
|
+
assertAppendIncarnation(writerId, 'writer');
|
|
1777
|
+
assertAppendIncarnation(moduleId, 'module');
|
|
1778
|
+
if (!Number.isSafeInteger(operationId) || operationId <= 0) {
|
|
1779
|
+
throw vfsError('EINVAL', `invalid append operation ${operationId}`);
|
|
1780
|
+
}
|
|
1781
|
+
const writer = [...this.sql.exec(`SELECT revoked FROM vfs_append_writer_state
|
|
1782
|
+
WHERE pid = ? AND writer_id = ?`, pid, writerId)][0];
|
|
1783
|
+
if (!writer || Number(writer.revoked) !== 0) {
|
|
1784
|
+
throw vfsError('ESTALE', `append writer ${pid} is unavailable`);
|
|
1785
|
+
}
|
|
1786
|
+
const moduleState = [...this.sql.exec(`SELECT acked_through FROM vfs_append_module_state
|
|
1787
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ?`, pid, writerId, moduleId)][0];
|
|
1788
|
+
if (!moduleState) {
|
|
1789
|
+
throw vfsError('ESTALE', `append module ${pid}/${writerId}/${moduleId} is unavailable`);
|
|
1790
|
+
}
|
|
1791
|
+
let ackedThrough = Number(moduleState.acked_through);
|
|
1792
|
+
if (operationId <= ackedThrough)
|
|
1793
|
+
return;
|
|
1794
|
+
const existingGap = [...this.sql.exec(`SELECT 1 AS present FROM vfs_append_acked_gaps
|
|
1795
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id = ?`, pid, writerId, moduleId, operationId)].length > 0;
|
|
1796
|
+
let receipt;
|
|
1797
|
+
if (!existingGap) {
|
|
1798
|
+
receipt = [...this.sql.exec(`SELECT path, byte_length, digest
|
|
1799
|
+
FROM vfs_append_receipts
|
|
1800
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id = ?`, pid, writerId, moduleId, operationId)][0];
|
|
1801
|
+
if (!receipt) {
|
|
1802
|
+
throw vfsError('EINVAL', `append operation ${pid}/${operationId} has not completed`);
|
|
1803
|
+
}
|
|
1804
|
+
}
|
|
1805
|
+
const acknowledged = [...this.sql.exec(`SELECT operation_id FROM vfs_append_acked_gaps
|
|
1806
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id > ?
|
|
1807
|
+
ORDER BY operation_id`, pid, writerId, moduleId, ackedThrough)];
|
|
1808
|
+
if (!existingGap)
|
|
1809
|
+
acknowledged.push({ operation_id: operationId });
|
|
1810
|
+
acknowledged.sort((left, right) => Number(left.operation_id) - Number(right.operation_id));
|
|
1811
|
+
for (const row of acknowledged) {
|
|
1812
|
+
const sequence = Number(row.operation_id);
|
|
1813
|
+
if (sequence <= ackedThrough)
|
|
1814
|
+
continue;
|
|
1815
|
+
if (sequence !== ackedThrough + 1)
|
|
1816
|
+
break;
|
|
1817
|
+
ackedThrough = sequence;
|
|
1818
|
+
}
|
|
1819
|
+
const priorAckedThrough = Number(moduleState.acked_through);
|
|
1820
|
+
if (!existingGap || ackedThrough > priorAckedThrough) {
|
|
1821
|
+
this.transactionSync(() => {
|
|
1822
|
+
if (!existingGap) {
|
|
1823
|
+
const completed = receipt;
|
|
1824
|
+
this.sql.exec(`INSERT INTO vfs_append_acked_gaps
|
|
1825
|
+
(pid, writer_id, module_id, operation_id, path, byte_length, digest)
|
|
1826
|
+
VALUES (?, ?, ?, ?, ?, ?, ?)`, pid, writerId, moduleId, operationId, completed.path, completed.byte_length, completed.digest);
|
|
1827
|
+
this.sql.exec(`DELETE FROM vfs_append_receipts
|
|
1828
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ? AND operation_id = ?`, pid, writerId, moduleId, operationId);
|
|
1829
|
+
}
|
|
1830
|
+
if (ackedThrough > priorAckedThrough) {
|
|
1831
|
+
this.sql.exec(`UPDATE vfs_append_module_state SET acked_through = ?
|
|
1832
|
+
WHERE pid = ? AND writer_id = ? AND module_id = ?`, ackedThrough, pid, writerId, moduleId);
|
|
1833
|
+
}
|
|
1834
|
+
});
|
|
1835
|
+
}
|
|
1836
|
+
if (ackedThrough > priorAckedThrough) {
|
|
1837
|
+
this.deleteAppendRowsBounded('vfs_append_acked_gaps', 'pid = ? AND writer_id = ? AND module_id = ? AND operation_id <= ?', [pid, writerId, moduleId, ackedThrough]);
|
|
1838
|
+
}
|
|
1839
|
+
}
|
|
1840
|
+
revokeAppendWriter(pid, writerId) {
|
|
1841
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1842
|
+
throw vfsError('EINVAL', `invalid append pid ${pid}`);
|
|
1843
|
+
assertAppendIncarnation(writerId, 'writer');
|
|
1844
|
+
this.transactionSync(() => {
|
|
1845
|
+
this.sql.exec(`UPDATE vfs_append_writer_state
|
|
1846
|
+
SET revoked = 1, retired_at = ?
|
|
1847
|
+
WHERE pid = ? AND writer_id = ?`, Date.now(), pid, writerId);
|
|
1848
|
+
});
|
|
1849
|
+
this.deleteAppendRowsBounded('vfs_append_receipts', 'pid = ? AND writer_id = ?', [pid, writerId]);
|
|
1850
|
+
this.deleteAppendRowsBounded('vfs_append_acked_gaps', 'pid = ? AND writer_id = ?', [pid, writerId]);
|
|
1851
|
+
this.deleteAppendRowsBounded('vfs_append_module_state', 'pid = ? AND writer_id = ?', [pid, writerId]);
|
|
1852
|
+
this.deleteAppendRowsBounded('vfs_append_writer_state', 'pid = ? AND writer_id = ? AND revoked = 1', [pid, writerId]);
|
|
1853
|
+
}
|
|
1854
|
+
revokeAppendWriters(pid) {
|
|
1855
|
+
if (!Number.isSafeInteger(pid) || pid <= 0)
|
|
1856
|
+
throw vfsError('EINVAL', `invalid append pid ${pid}`);
|
|
1857
|
+
this.transactionSync(() => {
|
|
1858
|
+
this.sql.exec(`INSERT INTO vfs_append_pid_revocations (pid, retired_at) VALUES (?, ?)
|
|
1859
|
+
ON CONFLICT(pid) DO UPDATE SET retired_at = excluded.retired_at`, pid, Date.now());
|
|
1860
|
+
});
|
|
1861
|
+
this.finishAppendPidRevocation(pid);
|
|
1862
|
+
}
|
|
1863
|
+
revokeAppendWritersThrough(maxPid) {
|
|
1864
|
+
if (!Number.isSafeInteger(maxPid) || maxPid < 0) {
|
|
1865
|
+
throw vfsError('EINVAL', `invalid append pid ceiling ${maxPid}`);
|
|
1866
|
+
}
|
|
1867
|
+
for (;;) {
|
|
1868
|
+
const pids = [...this.sql.exec(`SELECT DISTINCT pid FROM vfs_append_writer_state
|
|
1869
|
+
WHERE pid <= ?
|
|
1870
|
+
ORDER BY pid
|
|
1871
|
+
LIMIT ?`, maxPid, MAX_TX_LOGICAL_ROWS)];
|
|
1872
|
+
if (pids.length === 0)
|
|
1873
|
+
return;
|
|
1874
|
+
for (const row of pids)
|
|
1875
|
+
this.revokeAppendWriters(Number(row.pid));
|
|
1876
|
+
}
|
|
1877
|
+
}
|
|
1878
|
+
finishAppendPidRevocation(pid) {
|
|
1879
|
+
for (;;) {
|
|
1880
|
+
const writers = [...this.sql.exec(`SELECT writer_id FROM vfs_append_writer_state
|
|
1881
|
+
WHERE pid = ? AND revoked = 0
|
|
1882
|
+
ORDER BY rowid
|
|
1883
|
+
LIMIT ?`, pid, MAX_TX_LOGICAL_ROWS)];
|
|
1884
|
+
if (writers.length === 0)
|
|
1885
|
+
break;
|
|
1886
|
+
const placeholders = writers.map(() => '?').join(',');
|
|
1887
|
+
this.transactionSync(() => {
|
|
1888
|
+
this.sql.exec(`UPDATE vfs_append_writer_state
|
|
1889
|
+
SET revoked = 1, retired_at = ?
|
|
1890
|
+
WHERE pid = ? AND writer_id IN (${placeholders})`, Date.now(), pid, ...writers.map((writer) => writer.writer_id));
|
|
1891
|
+
});
|
|
1892
|
+
}
|
|
1893
|
+
this.deleteAppendRowsBounded('vfs_append_receipts', 'pid = ?', [pid]);
|
|
1894
|
+
this.deleteAppendRowsBounded('vfs_append_acked_gaps', 'pid = ?', [pid]);
|
|
1895
|
+
this.deleteAppendRowsBounded('vfs_append_module_state', 'pid = ?', [pid]);
|
|
1896
|
+
this.deleteAppendRowsBounded('vfs_append_writer_state', 'pid = ? AND revoked = 1', [pid]);
|
|
1897
|
+
this.transactionSync(() => {
|
|
1898
|
+
this.sql.exec('DELETE FROM vfs_append_pid_revocations WHERE pid = ?', pid);
|
|
1899
|
+
});
|
|
1900
|
+
}
|
|
1901
|
+
deleteAppendRowsBounded(table, predicate, params) {
|
|
1902
|
+
for (;;) {
|
|
1903
|
+
const rows = [...this.sql.exec(`SELECT rowid FROM ${table} WHERE ${predicate} ORDER BY rowid LIMIT ?`, ...params, MAX_TX_LOGICAL_ROWS)];
|
|
1904
|
+
if (rows.length === 0)
|
|
1905
|
+
return;
|
|
1906
|
+
const placeholders = rows.map(() => '?').join(',');
|
|
1907
|
+
this.transactionSync(() => {
|
|
1908
|
+
this.sql.exec(`DELETE FROM ${table} WHERE rowid IN (${placeholders})`, ...rows.map((row) => row.rowid));
|
|
1909
|
+
});
|
|
1910
|
+
}
|
|
1911
|
+
}
|
|
1912
|
+
resumeAppendMaintenance() {
|
|
1913
|
+
for (;;) {
|
|
1914
|
+
const revocations = [...this.sql.exec(`SELECT pid FROM vfs_append_pid_revocations
|
|
1915
|
+
ORDER BY pid
|
|
1916
|
+
LIMIT ?`, MAX_TX_LOGICAL_ROWS)];
|
|
1917
|
+
if (revocations.length === 0)
|
|
1918
|
+
break;
|
|
1919
|
+
for (const row of revocations) {
|
|
1920
|
+
this.finishAppendPidRevocation(Number(row.pid));
|
|
1921
|
+
}
|
|
1922
|
+
}
|
|
1923
|
+
for (const table of [
|
|
1924
|
+
'vfs_append_receipts',
|
|
1925
|
+
'vfs_append_acked_gaps',
|
|
1926
|
+
'vfs_append_module_state',
|
|
1927
|
+
]) {
|
|
1928
|
+
this.deleteAppendRowsBounded(table, `EXISTS (
|
|
1929
|
+
SELECT 1 FROM vfs_append_writer_state AS writer
|
|
1930
|
+
WHERE writer.pid = ${table}.pid
|
|
1931
|
+
AND writer.writer_id = ${table}.writer_id
|
|
1932
|
+
AND writer.revoked = 1
|
|
1933
|
+
)`, []);
|
|
1934
|
+
}
|
|
1935
|
+
this.deleteAppendRowsBounded('vfs_append_writer_state', 'revoked = 1', []);
|
|
1936
|
+
this.deleteAppendRowsBounded('vfs_append_acked_gaps', `EXISTS (
|
|
1937
|
+
SELECT 1 FROM vfs_append_module_state AS module
|
|
1938
|
+
WHERE module.pid = vfs_append_acked_gaps.pid
|
|
1939
|
+
AND module.writer_id = vfs_append_acked_gaps.writer_id
|
|
1940
|
+
AND module.module_id = vfs_append_acked_gaps.module_id
|
|
1941
|
+
AND module.acked_through >= vfs_append_acked_gaps.operation_id
|
|
1942
|
+
)`, []);
|
|
1943
|
+
}
|
|
1944
|
+
/**
|
|
1945
|
+
* Truncate or zero-extend to `size`, touching only the boundary chunk.
|
|
1946
|
+
* Shrinking drops trailing chunk rows and trims the new last chunk;
|
|
1947
|
+
* growing zero-fills like writeRange. Every mutation commits before return.
|
|
1948
|
+
*/
|
|
1949
|
+
truncate(path, size, cred) {
|
|
1950
|
+
this.assertMutationsAllowed([path]);
|
|
1951
|
+
const resolved = this.checkAccess(path, 0o2, cred);
|
|
1952
|
+
const inode = resolved.inode;
|
|
1953
|
+
if (!inode)
|
|
1954
|
+
throw vfsError('ENOENT', path);
|
|
1955
|
+
if (inode.kind === 'directory')
|
|
1956
|
+
throw vfsError('EISDIR', resolved.path);
|
|
1957
|
+
if (inode.kind !== 'file')
|
|
1958
|
+
throw vfsError('EINVAL', `${resolved.path} is not a regular file`);
|
|
1959
|
+
const newSize = clampNonNegativeInt(size);
|
|
1960
|
+
const oldSize = inode.size;
|
|
1961
|
+
if (newSize === oldSize)
|
|
1962
|
+
return;
|
|
1963
|
+
const newChunkCount = newSize === 0 ? 0 : Math.ceil(newSize / CHUNK_SIZE);
|
|
1964
|
+
const now = this.now();
|
|
1965
|
+
const changedChunks = new Map();
|
|
1966
|
+
if (newSize > oldSize) {
|
|
1967
|
+
// Zero-extend from the old EOF. A full old last chunk is skipped;
|
|
1968
|
+
// a partial one is re-materialized at its grown length.
|
|
1969
|
+
const fromChunk = oldSize % CHUNK_SIZE === 0 ? oldSize / CHUNK_SIZE : Math.floor(oldSize / CHUNK_SIZE);
|
|
1970
|
+
for (let i = fromChunk; i < newChunkCount; i++) {
|
|
1971
|
+
const chunkStart = i * CHUNK_SIZE;
|
|
1972
|
+
const chunkLen = Math.min(CHUNK_SIZE, newSize - chunkStart);
|
|
1973
|
+
const chunk = new Uint8Array(chunkLen);
|
|
1974
|
+
if (i < inode.chunkCount) {
|
|
1975
|
+
const existing = this.requireChunk(path, inode, i);
|
|
1976
|
+
chunk.set(existing.subarray(0, Math.min(existing.length, chunkLen)), 0);
|
|
1977
|
+
}
|
|
1978
|
+
changedChunks.set(i, chunk);
|
|
1979
|
+
}
|
|
1980
|
+
}
|
|
1981
|
+
else {
|
|
1982
|
+
if (newChunkCount > 0) {
|
|
1983
|
+
const lastId = newChunkCount - 1;
|
|
1984
|
+
const lastLen = newSize - lastId * CHUNK_SIZE;
|
|
1985
|
+
if (lastLen < CHUNK_SIZE) {
|
|
1986
|
+
const existing = this.requireChunk(path, inode, lastId);
|
|
1987
|
+
if (existing.length > lastLen) {
|
|
1988
|
+
const trimmed = existing.slice(0, lastLen);
|
|
1989
|
+
changedChunks.set(lastId, trimmed);
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
const deletedChunks = [];
|
|
1995
|
+
if (newSize < oldSize) {
|
|
1996
|
+
const contentId = this.contentIdForInode(inode);
|
|
1997
|
+
const rows = [...this.sql.exec(`SELECT chunk_id, length(data) AS byte_length
|
|
1998
|
+
FROM file_chunks
|
|
1999
|
+
WHERE content_id = ? AND chunk_id >= ?
|
|
2000
|
+
ORDER BY chunk_id
|
|
2001
|
+
LIMIT ?`, contentId, newChunkCount, MAX_TX_LOGICAL_ROWS + 1)];
|
|
2002
|
+
for (const row of rows) {
|
|
2003
|
+
deletedChunks.push({
|
|
2004
|
+
path,
|
|
2005
|
+
contentId,
|
|
2006
|
+
chunkId: clampNonNegativeInt(Number(row.chunk_id)),
|
|
2007
|
+
byteLength: clampNonNegativeInt(Number(row.byte_length)),
|
|
2008
|
+
});
|
|
2009
|
+
}
|
|
2010
|
+
}
|
|
2011
|
+
const next = this.updatedFileInode(inode, newSize, newChunkCount, now);
|
|
2012
|
+
if (this.commitCurrentContentMutation(inode, next, changedChunks, deletedChunks))
|
|
2013
|
+
return;
|
|
2014
|
+
this.replaceFileWithGeneratedContent(next, (chunkId) => this.generatedMutationChunk(inode, next, changedChunks, chunkId));
|
|
2015
|
+
}
|
|
2016
|
+
updatedFileInode(inode, size, chunkCount, mtime) {
|
|
2017
|
+
return {
|
|
2018
|
+
path: inode.path,
|
|
2019
|
+
parentPath: inode.parentPath,
|
|
2020
|
+
kind: 'file',
|
|
2021
|
+
isDir: false,
|
|
2022
|
+
size,
|
|
2023
|
+
atime: inode.atime,
|
|
2024
|
+
mtime,
|
|
2025
|
+
mode: inode.mode,
|
|
2026
|
+
uid: inode.uid,
|
|
2027
|
+
gid: inode.gid,
|
|
2028
|
+
chunkCount,
|
|
2029
|
+
};
|
|
2030
|
+
}
|
|
2031
|
+
commitCurrentContentMutation(prior, inode, changedChunks, deletedChunks, onCommit) {
|
|
2032
|
+
const contentId = this.contentIdForInode(prior);
|
|
2033
|
+
const builder = new TransactionPlanBuilder();
|
|
2034
|
+
builder.addInode({
|
|
2035
|
+
...inode,
|
|
2036
|
+
kind: inodeKind(inode),
|
|
2037
|
+
uid: inode.uid ?? prior.uid,
|
|
2038
|
+
gid: inode.gid ?? prior.gid,
|
|
2039
|
+
contentId,
|
|
2040
|
+
});
|
|
2041
|
+
for (const [chunkId, data] of changedChunks) {
|
|
2042
|
+
builder.addChunk({ path: inode.path, contentId, chunkId, data });
|
|
2043
|
+
}
|
|
2044
|
+
for (const chunk of deletedChunks)
|
|
2045
|
+
builder.addDeletedChunk(chunk);
|
|
2046
|
+
const plan = builder.build();
|
|
2047
|
+
const metrics = onCommit ? withCommitRowMetrics(plan.metrics) : plan.metrics;
|
|
2048
|
+
if (exceededTransactionLimit(metrics) !== null)
|
|
2049
|
+
return false;
|
|
2050
|
+
this._writeBatchOnce({
|
|
2051
|
+
payload: { inodes: [inode], chunks: [] },
|
|
2052
|
+
plan,
|
|
2053
|
+
deletedInodes: [],
|
|
2054
|
+
}, { source: 'range-mutation', limitMode: 'bounded' }, changedChunks.size, onCommit);
|
|
2055
|
+
return true;
|
|
2056
|
+
}
|
|
2057
|
+
generatedMutationChunk(prior, inode, changedChunks, chunkId) {
|
|
2058
|
+
const changed = changedChunks.get(chunkId);
|
|
2059
|
+
if (changed)
|
|
2060
|
+
return changed;
|
|
2061
|
+
const existing = this.requireChunk(prior.path, prior, chunkId);
|
|
2062
|
+
const expected = Math.min(CHUNK_SIZE, inode.size - (chunkId * CHUNK_SIZE));
|
|
2063
|
+
if (existing.byteLength !== expected) {
|
|
2064
|
+
throw new Error(`EIO: ${prior.path}: chunk ${chunkId} has ${existing.byteLength} bytes; expected ${expected}`);
|
|
2065
|
+
}
|
|
2066
|
+
return existing;
|
|
2067
|
+
}
|
|
2068
|
+
readFileString(path, cred) {
|
|
2069
|
+
return dec.decode(this.readFile(path, cred));
|
|
2070
|
+
}
|
|
2071
|
+
stat(path, cred, followLeaf) {
|
|
2072
|
+
const resolved = this.checkAccess(path, 0, cred, { followLeaf });
|
|
2073
|
+
const inode = resolved.inode;
|
|
2074
|
+
if (!inode)
|
|
2075
|
+
throw vfsError('ENOENT', path);
|
|
2076
|
+
return {
|
|
2077
|
+
type: inode.kind,
|
|
2078
|
+
size: inode.size,
|
|
2079
|
+
atime: inode.atime || inode.mtime,
|
|
2080
|
+
ctime: inode.mtime,
|
|
2081
|
+
mtime: inode.mtime,
|
|
2082
|
+
mode: inode.mode,
|
|
2083
|
+
uid: inode.uid,
|
|
2084
|
+
gid: inode.gid,
|
|
2085
|
+
};
|
|
2086
|
+
}
|
|
2087
|
+
utimes(path, atimeMs, mtimeMs, cred) {
|
|
2088
|
+
const resolved = this.checkAccess(path, 0, cred);
|
|
2089
|
+
const inode = resolved.inode;
|
|
2090
|
+
if (!inode)
|
|
2091
|
+
throw vfsError('ENOENT', path);
|
|
2092
|
+
this.assertMutationsAllowed([inode.path]);
|
|
2093
|
+
const useNow = atimeMs === null && mtimeMs === null;
|
|
2094
|
+
if (useNow) {
|
|
2095
|
+
if (!this.accessInode(inode, 0o2, cred))
|
|
2096
|
+
throw vfsError('EACCES', resolved.path);
|
|
2097
|
+
}
|
|
2098
|
+
else if (cred.uid !== 0 && cred.uid !== inode.uid) {
|
|
2099
|
+
throw vfsError('EPERM', resolved.path);
|
|
2100
|
+
}
|
|
2101
|
+
const atime = atimeMs !== null && Number.isFinite(atimeMs) ? Math.trunc(atimeMs) : this.now();
|
|
2102
|
+
const mtime = mtimeMs !== null && Number.isFinite(mtimeMs) ? Math.trunc(mtimeMs) : this.now();
|
|
2103
|
+
inode.atime = atime;
|
|
2104
|
+
inode.mtime = mtime;
|
|
2105
|
+
this.sql.exec("UPDATE inodes SET atime = ?, mtime = ? WHERE path = ?", atime, mtime, inode.path);
|
|
2106
|
+
this.bumpRevision([inode.path]);
|
|
2107
|
+
this.events.emit('change', inode.path);
|
|
2108
|
+
}
|
|
2109
|
+
/**
|
|
2110
|
+
* Set the permission bits durably. Follows symlinks (POSIX chmod).
|
|
2111
|
+
*
|
|
2112
|
+
* The stored value is a full POSIX st_mode: S_IF* filetype bits ORed
|
|
2113
|
+
* with the permission bits. Filetype bits double as the "mode was
|
|
2114
|
+
* explicitly set" marker: rows written before chmod existed carry
|
|
2115
|
+
* bare permission values (0o644/0o755), and the exec-dispatch
|
|
2116
|
+
* grandfather rule (see shell/exec-dispatch.ts) keeps wasm-magic
|
|
2117
|
+
* files with such untouched modes executable. No migration — legacy
|
|
2118
|
+
* rows upgrade the first time they are chmod'ed.
|
|
2119
|
+
*/
|
|
2120
|
+
chmod(path, mode, cred) {
|
|
2121
|
+
const resolved = this.checkAccess(path, 0, cred);
|
|
2122
|
+
const inode = resolved.inode;
|
|
2123
|
+
if (!inode)
|
|
2124
|
+
throw vfsError('ENOENT', path);
|
|
2125
|
+
if (cred.uid !== 0 && cred.uid !== inode.uid)
|
|
2126
|
+
throw vfsError('EPERM', resolved.path);
|
|
2127
|
+
this.assertMutationsAllowed([inode.path]);
|
|
2128
|
+
const full = inodeTypeBits(inode.kind) | (mode & 0o7777);
|
|
2129
|
+
inode.mode = full;
|
|
2130
|
+
this.sql.exec("UPDATE inodes SET mode = ? WHERE path = ?", full, inode.path);
|
|
2131
|
+
this.bumpRevision([inode.path]);
|
|
2132
|
+
this.events.emit('change', inode.path);
|
|
2133
|
+
}
|
|
2134
|
+
chown(path, uid, gid, cred, followLeaf) {
|
|
2135
|
+
const resolved = this.checkAccess(path, 0, cred, { followLeaf });
|
|
2136
|
+
const inode = resolved.inode;
|
|
2137
|
+
if (!inode)
|
|
2138
|
+
throw vfsError('ENOENT', path);
|
|
2139
|
+
if (uid !== null && (!Number.isSafeInteger(uid) || uid < 0))
|
|
2140
|
+
throw vfsError('EINVAL', `invalid uid ${uid}`);
|
|
2141
|
+
if (gid !== null && (!Number.isSafeInteger(gid) || gid < 0))
|
|
2142
|
+
throw vfsError('EINVAL', `invalid gid ${gid}`);
|
|
2143
|
+
const owner = cred.uid === inode.uid;
|
|
2144
|
+
if (uid !== null && uid !== inode.uid && cred.uid !== 0)
|
|
2145
|
+
throw vfsError('EPERM', resolved.path);
|
|
2146
|
+
if (uid !== null && uid === inode.uid && cred.uid !== 0 && !owner)
|
|
2147
|
+
throw vfsError('EPERM', resolved.path);
|
|
2148
|
+
if (gid !== null && gid !== inode.gid && cred.uid !== 0 && (!owner || !cred.groups.includes(gid))) {
|
|
2149
|
+
throw vfsError('EPERM', resolved.path);
|
|
2150
|
+
}
|
|
2151
|
+
if (gid !== null && gid === inode.gid && cred.uid !== 0 && !owner)
|
|
2152
|
+
throw vfsError('EPERM', resolved.path);
|
|
2153
|
+
this.assertMutationsAllowed([inode.path]);
|
|
2154
|
+
inode.uid = uid ?? inode.uid;
|
|
2155
|
+
inode.gid = gid ?? inode.gid;
|
|
2156
|
+
if (cred.uid !== 0)
|
|
2157
|
+
inode.mode &= ~0o6000;
|
|
2158
|
+
this.sql.exec('UPDATE inodes SET uid = ?, gid = ?, mode = ? WHERE path = ?', inode.uid, inode.gid, inode.mode, inode.path);
|
|
2159
|
+
this.bumpRevision([inode.path]);
|
|
2160
|
+
this.events.emit('change', inode.path);
|
|
2161
|
+
}
|
|
2162
|
+
/**
|
|
2163
|
+
* Enumerate the filesystem, one bounded page at a time.
|
|
2164
|
+
*
|
|
2165
|
+
* This is the answer to a question no facet could previously ask. A process
|
|
2166
|
+
* is shipped a prefetch bundle plus the ancestors of what is in it, so every
|
|
2167
|
+
* map it holds describes what it was GIVEN, never what EXISTS — a resident
|
|
2168
|
+
* store that enumerated from those maps could only ever re-cache the bundle,
|
|
2169
|
+
* which is the admission problem it exists to delete.
|
|
2170
|
+
*
|
|
2171
|
+
* Ordered by path so `after` is a stable resume key across pages. Ordering
|
|
2172
|
+
* by anything else would let an insert during pagination shift entries
|
|
2173
|
+
* across the page boundary and drop them.
|
|
2174
|
+
*
|
|
2175
|
+
* Access is checked per path against the caller's credential, and a path it
|
|
2176
|
+
* cannot reach is OMITTED rather than reported. Omission is the safe
|
|
2177
|
+
* direction: a filler that never learns of a path simply misses it, and the
|
|
2178
|
+
* miss falls through to the supervisor, which denies it in its own right.
|
|
2179
|
+
* Reporting the path instead would leak the existence of files the process
|
|
2180
|
+
* has no permission to see.
|
|
2181
|
+
*/
|
|
2182
|
+
list(after, limit, cred) {
|
|
2183
|
+
// Before the walk, deliberately — see VfsListPage.
|
|
2184
|
+
const epoch = this._epoch;
|
|
2185
|
+
const rev = this._revision;
|
|
2186
|
+
const from = after === null || after === undefined ? '' : normalizeVfsPath(after);
|
|
2187
|
+
// this.inodes is insertion-ordered, not path-ordered, so the sort is what
|
|
2188
|
+
// makes `after` a resume key at all. Paid once per page against a map the
|
|
2189
|
+
// DO already holds whole.
|
|
2190
|
+
const paths = [];
|
|
2191
|
+
for (const path of this.inodes.keys()) {
|
|
2192
|
+
if (path === '' || path <= from)
|
|
2193
|
+
continue;
|
|
2194
|
+
paths.push(path);
|
|
2195
|
+
}
|
|
2196
|
+
paths.sort();
|
|
2197
|
+
const entries = [];
|
|
2198
|
+
let next = null;
|
|
2199
|
+
for (const path of paths) {
|
|
2200
|
+
if (entries.length >= limit) {
|
|
2201
|
+
next = entries[entries.length - 1].path;
|
|
2202
|
+
break;
|
|
2203
|
+
}
|
|
2204
|
+
const inode = this.inodes.get(path);
|
|
2205
|
+
if (!inode)
|
|
2206
|
+
continue;
|
|
2207
|
+
try {
|
|
2208
|
+
this.checkAccess(path, 0, cred, { followLeaf: false });
|
|
2209
|
+
}
|
|
2210
|
+
catch {
|
|
2211
|
+
continue;
|
|
2212
|
+
}
|
|
2213
|
+
entries.push({
|
|
2214
|
+
path,
|
|
2215
|
+
kind: inode.kind,
|
|
2216
|
+
size: inode.size,
|
|
2217
|
+
rev: this._pathRevisions.get(path) ?? 0,
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
return { epoch, rev, entries, next };
|
|
2221
|
+
}
|
|
2222
|
+
readdir(path, cred) {
|
|
2223
|
+
const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
2224
|
+
const resolved = np ? this.checkAccess(np, 0o4, cred) : { path: '', inode: undefined };
|
|
2225
|
+
const inode = resolved.inode;
|
|
2226
|
+
if (inode && inode.kind !== 'directory')
|
|
2227
|
+
throw vfsError('ENOTDIR', path);
|
|
2228
|
+
const kids = this.children.get(np);
|
|
2229
|
+
if (!kids) {
|
|
2230
|
+
// W2.5b diagnostic: empty children-set for a directory we expected
|
|
2231
|
+
// to be populated.
|
|
2232
|
+
if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1') {
|
|
2233
|
+
// eslint-disable-next-line no-console
|
|
2234
|
+
console.warn('[sqlite-vfs/W2.5b] readdir miss path=' + np +
|
|
2235
|
+
' kidsUndefined=true inodeExists=' + this.inodes.has(np));
|
|
2236
|
+
}
|
|
2237
|
+
return [];
|
|
2238
|
+
}
|
|
2239
|
+
const results = [];
|
|
2240
|
+
for (const childPath of kids) {
|
|
2241
|
+
const inode = this.inodes.get(childPath);
|
|
2242
|
+
if (inode) {
|
|
2243
|
+
const name = inode.path.split('/').pop();
|
|
2244
|
+
results.push({ name, type: inode.kind });
|
|
2245
|
+
}
|
|
2246
|
+
}
|
|
2247
|
+
// W2.5b diagnostic: if children-set has entries but readdir returns
|
|
2248
|
+
// fewer (some entries' inodes are missing from this.inodes), log it.
|
|
2249
|
+
// This distinguishes (a) "children index broken" from (b) "inodes
|
|
2250
|
+
// map lost entries".
|
|
2251
|
+
if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1' &&
|
|
2252
|
+
kids.size !== results.length) {
|
|
2253
|
+
// eslint-disable-next-line no-console
|
|
2254
|
+
console.warn('[sqlite-vfs/W2.5b] readdir size mismatch path=' + np +
|
|
2255
|
+
' kidsSize=' + kids.size +
|
|
2256
|
+
' resultsLength=' + results.length +
|
|
2257
|
+
' missingInodes=' + (kids.size - results.length));
|
|
2258
|
+
}
|
|
2259
|
+
// W2.6a: sort lexicographically. Set-insertion order tracks
|
|
2260
|
+
// writeBatch arrival order, which under concurrent npm install
|
|
2261
|
+
// (pLimit=3) is non-deterministic. Sorting here removes a class
|
|
2262
|
+
// of "works on Tuesday" bugs in any consumer that walks readdir
|
|
2263
|
+
// results — buildPrefetchBundle, buildManifest, the kernel-VFS
|
|
2264
|
+
// mount layer, etc. Cost is O(n log n) on dirs that already cost
|
|
2265
|
+
// O(n) to assemble; negligible for typical npm package depths.
|
|
2266
|
+
results.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
|
|
2267
|
+
return results;
|
|
2268
|
+
}
|
|
2269
|
+
unlink(path, cred) {
|
|
2270
|
+
this.assertMutationsAllowed([path]);
|
|
2271
|
+
const resolved = this.checkAccess(path, 0, cred, { followLeaf: false });
|
|
2272
|
+
const inode = resolved.inode;
|
|
2273
|
+
if (!inode)
|
|
2274
|
+
throw vfsError('ENOENT', path);
|
|
2275
|
+
this.checkParentAccess(resolved.path, cred);
|
|
2276
|
+
if (inode.isDir)
|
|
2277
|
+
throw vfsError('EISDIR', resolved.path);
|
|
2278
|
+
this.writeBatch({ inodes: [], chunks: [], deletePaths: [resolved.path] }, cred);
|
|
2279
|
+
}
|
|
2280
|
+
rmdir(path, cred) {
|
|
2281
|
+
this.assertMutationsAllowed([path]);
|
|
2282
|
+
const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
2283
|
+
const resolved = this.checkAccess(np, 0, cred, { followLeaf: false });
|
|
2284
|
+
this.checkParentAccess(resolved.path, cred);
|
|
2285
|
+
// Check if empty using children index (O(1) instead of O(N))
|
|
2286
|
+
const kids = this.children.get(np);
|
|
2287
|
+
if (kids && kids.size > 0) {
|
|
2288
|
+
throw vfsError('ENOTEMPTY', path);
|
|
2289
|
+
}
|
|
2290
|
+
const inode = this.inodes.get(np);
|
|
2291
|
+
if (!inode)
|
|
2292
|
+
throw vfsError('ENOENT', path);
|
|
2293
|
+
if (!inode.isDir)
|
|
2294
|
+
throw vfsError('ENOTDIR', path);
|
|
2295
|
+
this.writeBatch({ inodes: [], chunks: [], deletePaths: [np] }, cred);
|
|
2296
|
+
}
|
|
2297
|
+
/**
|
|
2298
|
+
* Remove a path and everything beneath it, in bounded transactions.
|
|
2299
|
+
*
|
|
2300
|
+
* Walking the tree and issuing one transaction per entry cost a commit and
|
|
2301
|
+
* a content-maintenance pass apiece — 19,429 of each for a single npm
|
|
2302
|
+
* install's tree, on top of the whole-filesystem scan every one of them
|
|
2303
|
+
* paid. That took long enough on the object's only thread to exceed its
|
|
2304
|
+
* per-request CPU budget: the removal committed, the object was reset, and
|
|
2305
|
+
* every WebSocket it held closed 1006. A bounded group of entries commits
|
|
2306
|
+
* together instead, closed on the entry before the one that would overflow
|
|
2307
|
+
* it, and the removal owes one maintenance pass rather than one per group.
|
|
2308
|
+
*
|
|
2309
|
+
* Removal is group-atomic rather than path-atomic. Because entries go
|
|
2310
|
+
* deepest first, every committed prefix is a consistent smaller tree —
|
|
2311
|
+
* exactly the state an interrupted per-entry walk left behind.
|
|
2312
|
+
*/
|
|
2313
|
+
removeRecursive(path, cred) {
|
|
2314
|
+
this.assertMutationsAllowed([path]);
|
|
2315
|
+
const resolved = this.checkAccess(path, 0, cred, { followLeaf: false });
|
|
2316
|
+
if (!resolved.inode)
|
|
2317
|
+
throw vfsError('ENOENT', normalizeVfsPath(path));
|
|
2318
|
+
this.checkParentAccess(resolved.path, cred);
|
|
2319
|
+
const removable = this.collectSubtreeInodes([resolved.path]);
|
|
2320
|
+
// The directories the recursive walk used to enumerate are exactly the
|
|
2321
|
+
// directory inodes of the subtree, and enumerating one needed read
|
|
2322
|
+
// permission. Answering from the index would otherwise skip that check.
|
|
2323
|
+
for (const inode of removable) {
|
|
2324
|
+
if (inode.isDir && !this.accessInode(inode, 0o4, cred)) {
|
|
2325
|
+
throw vfsError('EACCES', inode.path);
|
|
2326
|
+
}
|
|
2327
|
+
}
|
|
2328
|
+
let removed = 0;
|
|
2329
|
+
let group = [];
|
|
2330
|
+
let budget = new TransactionPlanBuilder();
|
|
2331
|
+
const flush = () => {
|
|
2332
|
+
if (group.length === 0)
|
|
2333
|
+
return;
|
|
2334
|
+
const paths = group;
|
|
2335
|
+
group = [];
|
|
2336
|
+
budget = new TransactionPlanBuilder();
|
|
2337
|
+
// Every group re-authorises its own paths and re-checks the mutation
|
|
2338
|
+
// guard through commitBatch, so each check is contemporaneous with the
|
|
2339
|
+
// transaction that acts on it.
|
|
2340
|
+
this.commitBatch({ inodes: [], chunks: [], deletePaths: paths }, cred);
|
|
2341
|
+
removed += paths.length;
|
|
2342
|
+
};
|
|
2343
|
+
for (const inode of removable) {
|
|
2344
|
+
// Close the group before the entry that would overflow it. The estimate
|
|
2345
|
+
// only picks the boundary — the commit asserts the bound it writes.
|
|
2346
|
+
if (budget.wouldExceedDeletion(!inode.isDir) !== null)
|
|
2347
|
+
flush();
|
|
2348
|
+
budget.addDeletedPath(inode.path, inode);
|
|
2349
|
+
if (!inode.isDir)
|
|
2350
|
+
budget.addGcContent(this.contentIdForInode(inode));
|
|
2351
|
+
group.push(inode.path);
|
|
2352
|
+
}
|
|
2353
|
+
flush();
|
|
2354
|
+
this.runContentMaintenanceSafely(1);
|
|
2355
|
+
return removed;
|
|
2356
|
+
}
|
|
2357
|
+
rename(oldPath, newPath, cred) {
|
|
2358
|
+
this.assertMutationsAllowed([oldPath, newPath]);
|
|
2359
|
+
this.checkAccess(oldPath, 0, cred, { followLeaf: false });
|
|
2360
|
+
this.checkParentAccess(oldPath, cred);
|
|
2361
|
+
this.checkParentAccess(newPath, cred);
|
|
2362
|
+
if (oldPath === newPath)
|
|
2363
|
+
return;
|
|
2364
|
+
if (newPath.startsWith(`${oldPath}/`)) {
|
|
2365
|
+
throw new Error(`EINVAL: cannot move ${oldPath} inside itself`);
|
|
2366
|
+
}
|
|
2367
|
+
const inode = this.inodes.get(oldPath);
|
|
2368
|
+
if (!inode)
|
|
2369
|
+
throw new Error("ENOENT: " + oldPath);
|
|
2370
|
+
// W-3 (WASI filesystem WASI): if newPath already exists, unlink it first so the
|
|
2371
|
+
// SQL UPDATE doesn't conflict on inodes.path uniqueness. POSIX rename(2)
|
|
2372
|
+
// overwrites the destination atomically; clang's atomic-write pattern
|
|
2373
|
+
// (write tmp + rename → final) depends on this. Without this branch,
|
|
2374
|
+
// a 2nd `make` after a prior successful build throws on UPDATE
|
|
2375
|
+
// failure. Pre-unlink covers both file-over-file and file-over-dir
|
|
2376
|
+
// (the latter is rare but POSIX permits it for empty dirs).
|
|
2377
|
+
const destInode = this.inodes.get(newPath);
|
|
2378
|
+
if (destInode) {
|
|
2379
|
+
if (destInode.isDir) {
|
|
2380
|
+
// POSIX semantics: rename onto a non-empty dir is an error
|
|
2381
|
+
// (ENOTEMPTY); rename onto an empty dir is allowed. We surface
|
|
2382
|
+
// both as a thrown error since the WASI shim treats every
|
|
2383
|
+
// unexpected exception as ENOSYS → caller diagnostics.
|
|
2384
|
+
// Conservative: refuse dir overwrite entirely; the WASI fixture
|
|
2385
|
+
// we care about (file → file) is the high-value case.
|
|
2386
|
+
if (inode.isDir) {
|
|
2387
|
+
throw new Error("ENOTDIR-or-EISDIR: rename onto existing dir not supported");
|
|
2388
|
+
}
|
|
2389
|
+
throw new Error("EISDIR: cannot rename file onto existing directory");
|
|
2390
|
+
}
|
|
2391
|
+
// POSIX: a directory never replaces a non-directory. Refusing it also
|
|
2392
|
+
// keeps an occupied destination to the single-entry file-onto-file
|
|
2393
|
+
// move, whose publication is one transaction — so the destination this
|
|
2394
|
+
// move supersedes is never dropped by a group that later fails.
|
|
2395
|
+
if (inode.isDir) {
|
|
2396
|
+
throw new Error(`ENOTDIR: cannot overwrite non-directory ${newPath} with directory ${oldPath}`);
|
|
2397
|
+
}
|
|
2398
|
+
}
|
|
2399
|
+
// Both questions a rename asks — what moves, and what is already at the
|
|
2400
|
+
// destination — are subtree queries, and the index answers them in the
|
|
2401
|
+
// size of those subtrees. Scanning every inode twice made an atomic write
|
|
2402
|
+
// (write temp, rename over) cost the whole filesystem per call.
|
|
2403
|
+
const moving = this.collectSubtreeInodes([oldPath]).reverse();
|
|
2404
|
+
const movingPaths = new Set(moving.map((entry) => entry.path));
|
|
2405
|
+
const targetPaths = new Set(moving.map((entry) => newPath + entry.path.substring(oldPath.length)));
|
|
2406
|
+
for (const existing of this.collectSubtreeInodes([newPath])) {
|
|
2407
|
+
if (movingPaths.has(existing.path) || existing === destInode)
|
|
2408
|
+
continue;
|
|
2409
|
+
if (targetPaths.has(existing.path) || existing.path.startsWith(`${newPath}/`)) {
|
|
2410
|
+
throw new Error(`ENOTEMPTY: rename target subtree conflicts at ${existing.path}`);
|
|
2411
|
+
}
|
|
2412
|
+
}
|
|
2413
|
+
// `moving` is shallowest-first, which is the order publication needs: a
|
|
2414
|
+
// parent directory exists before the children that name it. Retiring the
|
|
2415
|
+
// source needs the opposite.
|
|
2416
|
+
const retiring = [...moving].reverse();
|
|
2417
|
+
const touchedPaths = new Set();
|
|
2418
|
+
const commit = (builder) => {
|
|
2419
|
+
if (builder.empty)
|
|
2420
|
+
return;
|
|
2421
|
+
const plan = builder.build();
|
|
2422
|
+
this.assertTransactionFits(plan.metrics);
|
|
2423
|
+
this.executeTransactionPlan(plan, { source: 'content-publish', limitMode: 'bounded' });
|
|
2424
|
+
this.cacheInvalidateBatch(plan.affectedPaths);
|
|
2425
|
+
for (const path of plan.affectedPaths)
|
|
2426
|
+
touchedPaths.add(path);
|
|
2427
|
+
const rows = plan.inodes.length + plan.deletedPaths.length;
|
|
2428
|
+
this._sqlWrites += rows;
|
|
2429
|
+
this._batchWrites++;
|
|
2430
|
+
this._batchWriteRows += rows;
|
|
2431
|
+
};
|
|
2432
|
+
// ── Phase 1: publish the whole tree at the destination ────────────────
|
|
2433
|
+
//
|
|
2434
|
+
// Content moves by id, so a file is reachable from both paths for the
|
|
2435
|
+
// width of this phase and its bytes are written once. Nothing leaves the
|
|
2436
|
+
// source until this phase has committed in full, so an entry is never at
|
|
2437
|
+
// neither path — the guarantee a caller that has already cleared the
|
|
2438
|
+
// destination is relying on.
|
|
2439
|
+
//
|
|
2440
|
+
// A failure here unwinds what it published, leaving exactly the pre-move
|
|
2441
|
+
// state: a partially populated destination would otherwise refuse the
|
|
2442
|
+
// retry as a subtree conflict.
|
|
2443
|
+
let builder = new TransactionPlanBuilder();
|
|
2444
|
+
if (destInode) {
|
|
2445
|
+
// Path uniqueness: the occupant goes in the same transaction as the
|
|
2446
|
+
// inode replacing it. That is only ever the file-onto-file case, whose
|
|
2447
|
+
// subtree is a single entry, so this group is the whole move.
|
|
2448
|
+
builder.addDeletedPath(newPath, destInode);
|
|
2449
|
+
builder.addGcContent(this.contentIdForInode(destInode));
|
|
2450
|
+
}
|
|
2451
|
+
const renamed = moving.map((entry) => {
|
|
2452
|
+
const path = newPath + entry.path.substring(oldPath.length);
|
|
2453
|
+
const stored = {
|
|
2454
|
+
path,
|
|
2455
|
+
parentPath: this.parentPath(path),
|
|
2456
|
+
kind: entry.kind,
|
|
2457
|
+
isDir: entry.isDir,
|
|
2458
|
+
size: entry.size,
|
|
2459
|
+
atime: entry.atime,
|
|
2460
|
+
mtime: entry.mtime,
|
|
2461
|
+
mode: entry.mode,
|
|
2462
|
+
uid: entry.uid,
|
|
2463
|
+
gid: entry.gid,
|
|
2464
|
+
chunkCount: entry.chunkCount,
|
|
2465
|
+
// Materialize the old legacy key before the logical path changes, so
|
|
2466
|
+
// the published inode names the content explicitly rather than
|
|
2467
|
+
// inheriting a path-derived key that is about to stop existing.
|
|
2468
|
+
contentId: entry.isDir ? null : this.contentIdForInode(entry),
|
|
2469
|
+
};
|
|
2470
|
+
return { entry, stored };
|
|
2471
|
+
});
|
|
2472
|
+
const committed = [];
|
|
2473
|
+
let group = [];
|
|
2474
|
+
const flushPublished = () => {
|
|
2475
|
+
if (builder.empty)
|
|
2476
|
+
return;
|
|
2477
|
+
commit(builder);
|
|
2478
|
+
committed.push(...group);
|
|
2479
|
+
group = [];
|
|
2480
|
+
builder = new TransactionPlanBuilder();
|
|
2481
|
+
};
|
|
2482
|
+
try {
|
|
2483
|
+
for (const { stored } of renamed) {
|
|
2484
|
+
// Close the group before the entry that would overflow it. The
|
|
2485
|
+
// estimate only picks the boundary — the commit asserts the bound it
|
|
2486
|
+
// writes.
|
|
2487
|
+
if (builder.wouldExceedInode() !== null)
|
|
2488
|
+
flushPublished();
|
|
2489
|
+
builder.addInode(stored);
|
|
2490
|
+
group.push(stored);
|
|
2491
|
+
}
|
|
2492
|
+
flushPublished();
|
|
2493
|
+
}
|
|
2494
|
+
catch (error) {
|
|
2495
|
+
this.unpublishRenameDestination(committed);
|
|
2496
|
+
throw error;
|
|
2497
|
+
}
|
|
2498
|
+
// The superseded occupant goes first: it shares its path with the entry
|
|
2499
|
+
// published over it, exactly as the transaction deleted before inserting.
|
|
2500
|
+
if (destInode) {
|
|
2501
|
+
this._removeFromChildrenIndex(destInode.parentPath, destInode.path);
|
|
2502
|
+
this.inodes.delete(destInode.path);
|
|
2503
|
+
this._totalFiles--;
|
|
2504
|
+
this._usedBytes -= destInode.size;
|
|
2505
|
+
}
|
|
2506
|
+
for (const { entry, stored } of renamed) {
|
|
2507
|
+
const moved = {
|
|
2508
|
+
...entry,
|
|
2509
|
+
path: stored.path,
|
|
2510
|
+
parentPath: stored.parentPath,
|
|
2511
|
+
contentId: stored.contentId,
|
|
2512
|
+
};
|
|
2513
|
+
this.inodes.set(moved.path, moved);
|
|
2514
|
+
this._addToChildrenIndex(moved.parentPath, moved.path);
|
|
2515
|
+
if (moved.isDir)
|
|
2516
|
+
this._totalDirs++;
|
|
2517
|
+
else {
|
|
2518
|
+
this._totalFiles++;
|
|
2519
|
+
this._usedBytes += moved.size;
|
|
2520
|
+
}
|
|
2521
|
+
}
|
|
2522
|
+
// ── Phase 2: retire the source ────────────────────────────────────────
|
|
2523
|
+
//
|
|
2524
|
+
// Deepest-first, so every committed prefix leaves a smaller consistent
|
|
2525
|
+
// tree behind — the same property the batched removal relies on. No
|
|
2526
|
+
// content is collected here: the destination inodes published above
|
|
2527
|
+
// reference it, so these paths orphan nothing.
|
|
2528
|
+
builder = new TransactionPlanBuilder();
|
|
2529
|
+
for (const entry of retiring) {
|
|
2530
|
+
if (builder.wouldExceedDeletion(false) !== null) {
|
|
2531
|
+
commit(builder);
|
|
2532
|
+
builder = new TransactionPlanBuilder();
|
|
2533
|
+
}
|
|
2534
|
+
builder.addDeletedPath(entry.path, entry);
|
|
2535
|
+
}
|
|
2536
|
+
commit(builder);
|
|
2537
|
+
for (const entry of retiring) {
|
|
2538
|
+
this._removeFromChildrenIndex(entry.parentPath, entry.path);
|
|
2539
|
+
this.inodes.delete(entry.path);
|
|
2540
|
+
if (entry.isDir)
|
|
2541
|
+
this._totalDirs--;
|
|
2542
|
+
else {
|
|
2543
|
+
this._totalFiles--;
|
|
2544
|
+
this._usedBytes -= entry.size;
|
|
2545
|
+
}
|
|
2546
|
+
}
|
|
2547
|
+
this.bumpRevision([...touchedPaths]);
|
|
2548
|
+
this.events.emit('rename', newPath, oldPath);
|
|
2549
|
+
this.runContentMaintenanceSafely(1);
|
|
2550
|
+
}
|
|
2551
|
+
/**
|
|
2552
|
+
* Unwind the destination inodes a failed move had already published.
|
|
2553
|
+
*
|
|
2554
|
+
* These rows name content the source still owns, so removing them collects
|
|
2555
|
+
* nothing — the point is only that a retry sees an empty destination rather
|
|
2556
|
+
* than a subtree conflict. Deepest-first in bounded groups, like any other
|
|
2557
|
+
* removal. A failure here is swallowed: the caller is already unwinding, and
|
|
2558
|
+
* the source tree — which is what the data lives in — is untouched either
|
|
2559
|
+
* way.
|
|
2560
|
+
*/
|
|
2561
|
+
unpublishRenameDestination(published) {
|
|
2562
|
+
if (published.length === 0)
|
|
2563
|
+
return;
|
|
2564
|
+
const deepestFirst = [...published].reverse();
|
|
2565
|
+
let builder = new TransactionPlanBuilder();
|
|
2566
|
+
const flush = () => {
|
|
2567
|
+
if (builder.empty)
|
|
2568
|
+
return;
|
|
2569
|
+
const plan = builder.build();
|
|
2570
|
+
builder = new TransactionPlanBuilder();
|
|
2571
|
+
this.assertTransactionFits(plan.metrics);
|
|
2572
|
+
this.executeTransactionPlan(plan, { source: 'content-publish', limitMode: 'bounded' });
|
|
2573
|
+
this.cacheInvalidateBatch(plan.affectedPaths);
|
|
2574
|
+
};
|
|
2575
|
+
try {
|
|
2576
|
+
for (const stored of deepestFirst) {
|
|
2577
|
+
if (builder.wouldExceedDeletion(false) !== null)
|
|
2578
|
+
flush();
|
|
2579
|
+
// The published row is the inode being removed; the in-memory index
|
|
2580
|
+
// has not adopted it yet, so the entry that was written is the one
|
|
2581
|
+
// the plan must account for.
|
|
2582
|
+
builder.addDeletedPath(stored.path, { ...stored, atime: stored.atime ?? stored.mtime });
|
|
2583
|
+
}
|
|
2584
|
+
flush();
|
|
2585
|
+
}
|
|
2586
|
+
catch { /* the source is intact; report the original failure */ }
|
|
2587
|
+
}
|
|
2588
|
+
copyFile(src, dest, cred) {
|
|
2589
|
+
this.writeFile(dest, this.readFile(src, cred), undefined, cred);
|
|
2590
|
+
}
|
|
2591
|
+
// ── Batch write (npm install fast path) ───────────────────────────────
|
|
2592
|
+
normalizeBatchInode(entry, cred) {
|
|
2593
|
+
const path = normalizeVfsPath(entry.path);
|
|
2594
|
+
const prior = this.inodes.get(path);
|
|
2595
|
+
const newUid = cred.uid === 0 ? (entry.uid ?? 1000) : cred.uid;
|
|
2596
|
+
const newGid = cred.uid === 0 ? (entry.gid ?? 1000) : cred.gid;
|
|
2597
|
+
return {
|
|
2598
|
+
...entry,
|
|
2599
|
+
path,
|
|
2600
|
+
parentPath: normalizeVfsPath(entry.parentPath),
|
|
2601
|
+
mode: prior
|
|
2602
|
+
? prior.mode
|
|
2603
|
+
: inodeKind(entry) === 'symlink'
|
|
2604
|
+
? inodeTypeBits('symlink') | 0o777
|
|
2605
|
+
: entry.mode & ~cred.umask & 0o7777,
|
|
2606
|
+
uid: prior?.uid ?? newUid,
|
|
2607
|
+
gid: prior?.gid ?? newGid,
|
|
2608
|
+
};
|
|
2609
|
+
}
|
|
2610
|
+
authorizeBatch(payload, cred) {
|
|
2611
|
+
const inodes = payload.inodes.map((entry) => this.normalizeBatchInode(entry, cred));
|
|
2612
|
+
const pending = new Map(inodes.map((entry) => [entry.path, entry]));
|
|
2613
|
+
const checkedParents = new Set();
|
|
2614
|
+
const checkParent = (path) => {
|
|
2615
|
+
const parent = this.parentPath(path);
|
|
2616
|
+
if (parent === '')
|
|
2617
|
+
return;
|
|
2618
|
+
if (checkedParents.has(parent))
|
|
2619
|
+
return;
|
|
2620
|
+
checkedParents.add(parent);
|
|
2621
|
+
const existing = this.inodes.get(parent);
|
|
2622
|
+
if (existing) {
|
|
2623
|
+
this.checkAccess(parent, 0o3, cred);
|
|
2624
|
+
return;
|
|
2625
|
+
}
|
|
2626
|
+
const staged = pending.get(parent);
|
|
2627
|
+
if (!staged || !staged.isDir)
|
|
2628
|
+
throw vfsError('ENOENT', parent);
|
|
2629
|
+
checkParent(parent);
|
|
2630
|
+
if (!this.accessMode(staged.mode, staged.uid ?? 1000, staged.gid ?? 1000, 0o3, cred)) {
|
|
2631
|
+
throw vfsError('EACCES', parent);
|
|
2632
|
+
}
|
|
2633
|
+
};
|
|
2634
|
+
for (const path of payload.deletePaths ?? []) {
|
|
2635
|
+
const normalized = normalizeVfsPath(path);
|
|
2636
|
+
const existing = this.checkAccess(normalized, 0, cred, {
|
|
2637
|
+
followLeaf: false,
|
|
2638
|
+
allowMissingLeaf: true,
|
|
2639
|
+
}).inode;
|
|
2640
|
+
if (existing)
|
|
2641
|
+
checkParent(normalized);
|
|
2642
|
+
}
|
|
2643
|
+
for (const entry of inodes) {
|
|
2644
|
+
const prior = this.inodes.get(entry.path);
|
|
2645
|
+
if (prior)
|
|
2646
|
+
this.checkAccess(entry.path, 0o2, cred, { followLeaf: false });
|
|
2647
|
+
else
|
|
2648
|
+
checkParent(entry.path);
|
|
2649
|
+
}
|
|
2650
|
+
for (const chunk of payload.chunks) {
|
|
2651
|
+
const path = normalizeVfsPath(chunk.path);
|
|
2652
|
+
if (!pending.has(path))
|
|
2653
|
+
this.checkAccess(path, 0o2, cred, { followLeaf: false });
|
|
2654
|
+
}
|
|
2655
|
+
return {
|
|
2656
|
+
...payload,
|
|
2657
|
+
inodes,
|
|
2658
|
+
deletePaths: payload.deletePaths?.map(normalizeVfsPath),
|
|
2659
|
+
};
|
|
2660
|
+
}
|
|
2661
|
+
/**
|
|
2662
|
+
* Atomic bulk write: ALL inodes + chunks in ONE transactionSync().
|
|
2663
|
+
*
|
|
2664
|
+
* The complete mutation is preflighted against the Stage 2 transaction
|
|
2665
|
+
* limits, then executed in one transaction with 9-inode / 33-chunk SQL
|
|
2666
|
+
* grouping. Oversized strict calls fail with E2BIG before mutation.
|
|
2667
|
+
*/
|
|
2668
|
+
writeBatch(payload, cred, onCommit) {
|
|
2669
|
+
const result = this.commitBatch(payload, cred, onCommit);
|
|
2670
|
+
this.runContentMaintenanceSafely(1);
|
|
2671
|
+
return result;
|
|
2672
|
+
}
|
|
2673
|
+
/**
|
|
2674
|
+
* Authorise and commit one batch, without the maintenance pass. A standalone
|
|
2675
|
+
* mutation owes that pass; an operation built from several transactions owes
|
|
2676
|
+
* exactly one when it is finished. Charging it per transaction made removing
|
|
2677
|
+
* a tree run the orphan scan — which reads the chunk table — once for every
|
|
2678
|
+
* bounded group of the removal.
|
|
2679
|
+
*/
|
|
2680
|
+
commitBatch(payload, cred, onCommit) {
|
|
2681
|
+
const normalized = this.authorizeBatch(payload, cred);
|
|
2682
|
+
this.assertMutationsAllowed(batchMutationPaths(normalized));
|
|
2683
|
+
return this._writeBatchWithRetry(normalized, { source: 'strict-batch', limitMode: 'bounded' }, true, onCommit);
|
|
2684
|
+
}
|
|
2685
|
+
replaceFileWithStagedContent(inode, chunks, onPhase, onCommit) {
|
|
2686
|
+
this.validateFileChunks(inode, chunks);
|
|
2687
|
+
onPhase?.('stage');
|
|
2688
|
+
const contentId = this.beginStagedContent();
|
|
2689
|
+
let builder = new TransactionPlanBuilder();
|
|
2690
|
+
const flushChunks = () => {
|
|
2691
|
+
if (builder.empty)
|
|
2692
|
+
return;
|
|
2693
|
+
const plan = builder.build();
|
|
2694
|
+
builder = new TransactionPlanBuilder();
|
|
2695
|
+
this.executeStagedChunkPlan(plan);
|
|
2696
|
+
};
|
|
2697
|
+
try {
|
|
2698
|
+
for (const chunk of chunks) {
|
|
2699
|
+
const stored = { ...chunk, contentId };
|
|
2700
|
+
if (builder.wouldExceedChunkGroup([stored]) !== null)
|
|
2701
|
+
flushChunks();
|
|
2702
|
+
builder.addChunk(stored);
|
|
2703
|
+
}
|
|
2704
|
+
flushChunks();
|
|
2705
|
+
onPhase?.('publish');
|
|
2706
|
+
const result = this.publishStagedFile(inode, contentId, chunks.length, onCommit);
|
|
2707
|
+
this.runContentMaintenanceSafely(1);
|
|
2708
|
+
return result;
|
|
2709
|
+
}
|
|
2710
|
+
catch (error) {
|
|
2711
|
+
this.activeStagingContentIds.delete(contentId);
|
|
2712
|
+
this.maintenancePending = true;
|
|
2713
|
+
this.runContentMaintenanceSafely(1);
|
|
2714
|
+
throw error;
|
|
2715
|
+
}
|
|
2716
|
+
}
|
|
2717
|
+
/**
|
|
2718
|
+
* Copy-on-write replacement for an over-limit range/truncate mutation.
|
|
2719
|
+
* Chunks are produced and staged one at a time, so the operation never
|
|
2720
|
+
* assembles the file as one BLOB or exceeds a Stage 2 transaction bound.
|
|
2721
|
+
*/
|
|
2722
|
+
replaceFileWithGeneratedContent(inode, chunkAt, onCommit) {
|
|
2723
|
+
this.validateInodeContentShape(inode);
|
|
2724
|
+
const contentId = this.beginStagedContent();
|
|
2725
|
+
let builder = new TransactionPlanBuilder();
|
|
2726
|
+
const flushChunks = () => {
|
|
2727
|
+
if (builder.empty)
|
|
2728
|
+
return;
|
|
2729
|
+
const plan = builder.build();
|
|
2730
|
+
builder = new TransactionPlanBuilder();
|
|
2731
|
+
this.executeStagedChunkPlan(plan);
|
|
2732
|
+
};
|
|
2733
|
+
try {
|
|
2734
|
+
for (let chunkId = 0; chunkId < inode.chunkCount; chunkId++) {
|
|
2735
|
+
const data = chunkAt(chunkId);
|
|
2736
|
+
const expected = Math.min(CHUNK_SIZE, inode.size - (chunkId * CHUNK_SIZE));
|
|
2737
|
+
if (data.byteLength !== expected) {
|
|
2738
|
+
throw new Error(`EIO: ${inode.path}: generated chunk ${chunkId} has ${data.byteLength} bytes; expected ${expected}`);
|
|
2739
|
+
}
|
|
2740
|
+
const chunk = { path: inode.path, contentId, chunkId, data };
|
|
2741
|
+
if (builder.wouldExceedChunkGroup([chunk]) !== null)
|
|
2742
|
+
flushChunks();
|
|
2743
|
+
builder.addChunk(chunk);
|
|
2744
|
+
}
|
|
2745
|
+
flushChunks();
|
|
2746
|
+
const result = this.publishStagedFile(inode, contentId, inode.chunkCount, onCommit);
|
|
2747
|
+
this.runContentMaintenanceSafely(1);
|
|
2748
|
+
return result;
|
|
2749
|
+
}
|
|
2750
|
+
catch (error) {
|
|
2751
|
+
this.activeStagingContentIds.delete(contentId);
|
|
2752
|
+
this.maintenancePending = true;
|
|
2753
|
+
this.runContentMaintenanceSafely(1);
|
|
2754
|
+
throw error;
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
beginStagedContent() {
|
|
2758
|
+
const contentId = this.createContentId();
|
|
2759
|
+
const builder = new TransactionPlanBuilder();
|
|
2760
|
+
builder.addStagingContent(contentId);
|
|
2761
|
+
const plan = builder.build();
|
|
2762
|
+
this.assertTransactionFits(plan.metrics);
|
|
2763
|
+
this.executeTransactionPlan(plan, { source: 'content-stage', limitMode: 'bounded' });
|
|
2764
|
+
this.activeStagingContentIds.add(contentId);
|
|
2765
|
+
return contentId;
|
|
2766
|
+
}
|
|
2767
|
+
executeStagedChunkPlan(plan) {
|
|
2768
|
+
this.assertTransactionFits(plan.metrics);
|
|
2769
|
+
this.executeTransactionPlan(plan, { source: 'content-stage', limitMode: 'bounded' });
|
|
2770
|
+
}
|
|
2771
|
+
/**
|
|
2772
|
+
* The rows that publishing `inode` from `contentId` writes: the inode
|
|
2773
|
+
* itself, the lifecycle transition, and the content it supersedes. The
|
|
2774
|
+
* single definition of a file's publication — ownership inheritance and
|
|
2775
|
+
* the GC of the replaced content are the same whether the file publishes
|
|
2776
|
+
* alone or as one member of a batched group.
|
|
2777
|
+
*/
|
|
2778
|
+
addFilePublication(builder, inode, contentId) {
|
|
2779
|
+
this.assertMutationsAllowed([inode.path]);
|
|
2780
|
+
const prior = this.inodes.get(inode.path);
|
|
2781
|
+
builder.addInode({
|
|
2782
|
+
...inode,
|
|
2783
|
+
kind: inodeKind(inode),
|
|
2784
|
+
uid: inode.uid ?? prior?.uid ?? 1000,
|
|
2785
|
+
gid: inode.gid ?? prior?.gid ?? 1000,
|
|
2786
|
+
contentId,
|
|
2787
|
+
});
|
|
2788
|
+
builder.addPublishedContent(contentId);
|
|
2789
|
+
if (prior && !prior.isDir)
|
|
2790
|
+
builder.addGcContent(this.contentIdForInode(prior));
|
|
2791
|
+
}
|
|
2792
|
+
publishStagedFile(inode, contentId, publishedChunkCount, onCommit) {
|
|
2793
|
+
const builder = new TransactionPlanBuilder();
|
|
2794
|
+
this.addFilePublication(builder, inode, contentId);
|
|
2795
|
+
const plan = builder.build();
|
|
2796
|
+
this.assertTransactionFits(onCommit ? withCommitRowMetrics(plan.metrics) : plan.metrics);
|
|
2797
|
+
const result = this._writeBatchOnce({ payload: { inodes: [inode], chunks: [] }, plan, deletedInodes: [] }, { source: 'content-publish', limitMode: 'bounded' }, publishedChunkCount, onCommit);
|
|
2798
|
+
this.activeStagingContentIds.delete(contentId);
|
|
2799
|
+
return result;
|
|
2800
|
+
}
|
|
2801
|
+
/**
|
|
2802
|
+
* Incremental W7 v3 consumer. Chunk payload is admitted through one
|
|
2803
|
+
* per-VFS weighted credit pool and committed in bounded synchronous
|
|
2804
|
+
* transactions, which release their credit before the decoder pulls
|
|
2805
|
+
* another record.
|
|
2806
|
+
*
|
|
2807
|
+
* Publication is group-atomic with a committed prefix: a bounded group of
|
|
2808
|
+
* whole files commits in one transaction, and either every file in it is
|
|
2809
|
+
* durable or none is. A file never publishes partially — a group is closed
|
|
2810
|
+
* on the record boundary before the file that would overflow it, so a file
|
|
2811
|
+
* too large for one transaction stages across several and publishes on the
|
|
2812
|
+
* last, exactly as the per-file path did. Chunks staged for a file still
|
|
2813
|
+
* in flight may ride along in a group that publishes other files; they
|
|
2814
|
+
* carry a content id no inode references yet, so nothing observes them.
|
|
2815
|
+
*
|
|
2816
|
+
* Publishing per file cost three transactions each (stage the content
|
|
2817
|
+
* row, flush the chunks, publish), which at ~0.9 ms of commit apiece made
|
|
2818
|
+
* writing 19,429 files the dominant term of an npm install and stalled
|
|
2819
|
+
* every download shard behind the one Durable Object's storage queue.
|
|
2820
|
+
*/
|
|
2821
|
+
async writeStream(stream, options = {}, cred) {
|
|
2822
|
+
const decodeDrainStartedAt = options.decodeDrainStartedAt ?? performance.now();
|
|
2823
|
+
const decodeDrainToken = {};
|
|
2824
|
+
this._decodeDrainStarts.set(decodeDrainToken, decodeDrainStartedAt);
|
|
2825
|
+
let decodeDrainFinished = false;
|
|
2826
|
+
let decodeDrainWaitMs = Math.max(0, performance.now() - decodeDrainStartedAt);
|
|
2827
|
+
let recordIterator = null;
|
|
2828
|
+
let recordIteratorFinished = false;
|
|
2829
|
+
let decodedRecordLease = null;
|
|
2830
|
+
const ownedStagingContentIds = new Set();
|
|
2831
|
+
let activeFile = null;
|
|
2832
|
+
const progress = {
|
|
2833
|
+
committedGroupSequence: 0,
|
|
2834
|
+
committedPathCount: 0,
|
|
2835
|
+
inodes: 0,
|
|
2836
|
+
chunks: 0,
|
|
2837
|
+
};
|
|
2838
|
+
let phase = 'decode';
|
|
2839
|
+
// The pending publish group: chunks, inodes and lifecycle rows that
|
|
2840
|
+
// will commit together. Everything reset by a flush lives here.
|
|
2841
|
+
let group = new TransactionPlanBuilder();
|
|
2842
|
+
let groupLeases = [];
|
|
2843
|
+
let groupInodes = [];
|
|
2844
|
+
let groupContentIds = [];
|
|
2845
|
+
let groupPublishedChunks = 0;
|
|
2846
|
+
let groupPaths = 0;
|
|
2847
|
+
let groupStagedBytes = 0;
|
|
2848
|
+
// Directory upserts batch on their own: authorising a file consults the
|
|
2849
|
+
// committed inode tree for its parent, so directories flush before the
|
|
2850
|
+
// first file record rather than sharing the file group.
|
|
2851
|
+
let pendingDirectories = [];
|
|
2852
|
+
const flushGroup = () => {
|
|
2853
|
+
if (group.empty)
|
|
2854
|
+
return;
|
|
2855
|
+
const plan = group.build();
|
|
2856
|
+
const leases = groupLeases;
|
|
2857
|
+
const inodes = groupInodes;
|
|
2858
|
+
const contentIds = groupContentIds;
|
|
2859
|
+
const publishedChunks = groupPublishedChunks;
|
|
2860
|
+
const paths = groupPaths;
|
|
2861
|
+
const stagedBytes = groupStagedBytes;
|
|
2862
|
+
group = new TransactionPlanBuilder();
|
|
2863
|
+
groupLeases = [];
|
|
2864
|
+
groupInodes = [];
|
|
2865
|
+
groupContentIds = [];
|
|
2866
|
+
groupPublishedChunks = 0;
|
|
2867
|
+
groupPaths = 0;
|
|
2868
|
+
groupStagedBytes = 0;
|
|
2869
|
+
try {
|
|
2870
|
+
if (inodes.length === 0) {
|
|
2871
|
+
// No file has completed, so the plan publishes nothing: these are
|
|
2872
|
+
// the staged chunks of a file still in flight, and the bytes stay
|
|
2873
|
+
// charged to it until its publication commits.
|
|
2874
|
+
this.executeStagedChunkPlan(plan);
|
|
2875
|
+
this._stagedStreamBytes += plan.metrics.blobBytes;
|
|
2876
|
+
this._peakStagedStreamBytes = Math.max(this._peakStagedStreamBytes, this._stagedStreamBytes);
|
|
2877
|
+
if (activeFile)
|
|
2878
|
+
activeFile.stagedBytes += plan.metrics.blobBytes;
|
|
2879
|
+
return;
|
|
2880
|
+
}
|
|
2881
|
+
this.assertTransactionFits(plan.metrics);
|
|
2882
|
+
// Re-check the mutation guard here rather than only where each file
|
|
2883
|
+
// was accepted: a group commits after the records that follow it, so
|
|
2884
|
+
// this is the check that is contemporaneous with the write.
|
|
2885
|
+
const result = this.withMutationOwner(options.mutationOwner, () => {
|
|
2886
|
+
this.assertMutationsAllowed(inodes.map((inode) => inode.path));
|
|
2887
|
+
return this._writeBatchOnce({ payload: { inodes, chunks: [] }, plan, deletedInodes: [] }, { source: 'content-publish', limitMode: 'bounded' }, publishedChunks);
|
|
2888
|
+
});
|
|
2889
|
+
this._stagedStreamBytes = Math.max(0, this._stagedStreamBytes - stagedBytes);
|
|
2890
|
+
for (const contentId of contentIds) {
|
|
2891
|
+
this.activeStagingContentIds.delete(contentId);
|
|
2892
|
+
ownedStagingContentIds.delete(contentId);
|
|
2893
|
+
}
|
|
2894
|
+
progress.committedGroupSequence++;
|
|
2895
|
+
progress.committedPathCount += paths;
|
|
2896
|
+
progress.inodes += result.inodes;
|
|
2897
|
+
progress.chunks += result.chunks;
|
|
2898
|
+
}
|
|
2899
|
+
finally {
|
|
2900
|
+
for (const lease of leases)
|
|
2901
|
+
lease.release();
|
|
2902
|
+
}
|
|
2903
|
+
};
|
|
2904
|
+
const flushDirectories = () => {
|
|
2905
|
+
if (pendingDirectories.length === 0)
|
|
2906
|
+
return;
|
|
2907
|
+
const inodes = pendingDirectories;
|
|
2908
|
+
pendingDirectories = [];
|
|
2909
|
+
const result = this.withMutationOwner(options.mutationOwner, () => (this.writeBatch({ inodes, chunks: [] }, cred)));
|
|
2910
|
+
progress.committedGroupSequence++;
|
|
2911
|
+
progress.committedPathCount += inodes.length;
|
|
2912
|
+
progress.inodes += result.inodes;
|
|
2913
|
+
};
|
|
2914
|
+
const retainChunk = async (byteLength, signal) => {
|
|
2915
|
+
if (group.wouldExceedChunks(byteLength) !== null)
|
|
2916
|
+
flushGroup();
|
|
2917
|
+
let writeLease = this.writeStreamCredits.tryAcquire(byteLength);
|
|
2918
|
+
if (!writeLease && !group.empty) {
|
|
2919
|
+
flushGroup();
|
|
2920
|
+
writeLease = this.writeStreamCredits.tryAcquire(byteLength);
|
|
2921
|
+
}
|
|
2922
|
+
if (!writeLease) {
|
|
2923
|
+
const waitToken = {};
|
|
2924
|
+
const waitStartedAt = performance.now();
|
|
2925
|
+
this._creditWaitStarts.set(waitToken, waitStartedAt);
|
|
2926
|
+
try {
|
|
2927
|
+
writeLease = await this.writeStreamCredits.acquire(byteLength, signal);
|
|
2928
|
+
}
|
|
2929
|
+
finally {
|
|
2930
|
+
this._creditWaitStarts.delete(waitToken);
|
|
2931
|
+
this.recordDuration(this._creditWaitDuration, performance.now() - waitStartedAt);
|
|
2932
|
+
}
|
|
2933
|
+
}
|
|
2934
|
+
let supervisorLease;
|
|
2935
|
+
const waitToken = {};
|
|
2936
|
+
const waitStartedAt = performance.now();
|
|
2937
|
+
this._creditWaitStarts.set(waitToken, waitStartedAt);
|
|
2938
|
+
try {
|
|
2939
|
+
supervisorLease = await acquireSupervisorAllocation(byteLength, signal);
|
|
2940
|
+
}
|
|
2941
|
+
catch (error) {
|
|
2942
|
+
writeLease.release();
|
|
2943
|
+
throw error;
|
|
2944
|
+
}
|
|
2945
|
+
finally {
|
|
2946
|
+
this._creditWaitStarts.delete(waitToken);
|
|
2947
|
+
this.recordDuration(this._creditWaitDuration, performance.now() - waitStartedAt);
|
|
2948
|
+
}
|
|
2949
|
+
let released = false;
|
|
2950
|
+
return {
|
|
2951
|
+
bytes: byteLength,
|
|
2952
|
+
release: () => {
|
|
2953
|
+
if (released)
|
|
2954
|
+
return;
|
|
2955
|
+
released = true;
|
|
2956
|
+
writeLease.release();
|
|
2957
|
+
supervisorLease.release();
|
|
2958
|
+
},
|
|
2959
|
+
};
|
|
2960
|
+
};
|
|
2961
|
+
try {
|
|
2962
|
+
const decoded = await decodeWriteBatchStream(stream, {
|
|
2963
|
+
signal: options.signal,
|
|
2964
|
+
retainChunk,
|
|
2965
|
+
});
|
|
2966
|
+
recordIterator = decoded.records[Symbol.asyncIterator]();
|
|
2967
|
+
while (true) {
|
|
2968
|
+
phase = 'decode';
|
|
2969
|
+
const waitStartedAt = performance.now();
|
|
2970
|
+
let next;
|
|
2971
|
+
try {
|
|
2972
|
+
next = await recordIterator.next();
|
|
2973
|
+
}
|
|
2974
|
+
finally {
|
|
2975
|
+
decodeDrainWaitMs += performance.now() - waitStartedAt;
|
|
2976
|
+
}
|
|
2977
|
+
if (next.done) {
|
|
2978
|
+
recordIteratorFinished = true;
|
|
2979
|
+
throw new Error('w7-frame: stream ended without batch-end');
|
|
2980
|
+
}
|
|
2981
|
+
const record = next.value;
|
|
2982
|
+
switch (record.type) {
|
|
2983
|
+
case 'delete': {
|
|
2984
|
+
// A delete observes everything the stream wrote before it.
|
|
2985
|
+
phase = 'publish';
|
|
2986
|
+
flushGroup();
|
|
2987
|
+
flushDirectories();
|
|
2988
|
+
const affected = Math.max(1, this.collectSubtreeInodes([record.path]).length);
|
|
2989
|
+
this.withMutationOwner(options.mutationOwner, () => {
|
|
2990
|
+
this.writeBatch({ inodes: [], chunks: [], deletePaths: [record.path] }, cred);
|
|
2991
|
+
});
|
|
2992
|
+
progress.committedGroupSequence++;
|
|
2993
|
+
progress.committedPathCount += affected;
|
|
2994
|
+
break;
|
|
2995
|
+
}
|
|
2996
|
+
case 'directory': {
|
|
2997
|
+
phase = 'validation';
|
|
2998
|
+
this.validateFileChunks(record.inode, []);
|
|
2999
|
+
phase = 'publish';
|
|
3000
|
+
pendingDirectories.push(record.inode);
|
|
3001
|
+
// Directory inodes carry no payload, so the row count is the
|
|
3002
|
+
// only bound in reach; the flush re-asserts it regardless.
|
|
3003
|
+
if (pendingDirectories.length >= MAX_TX_LOGICAL_ROWS)
|
|
3004
|
+
flushDirectories();
|
|
3005
|
+
break;
|
|
3006
|
+
}
|
|
3007
|
+
case 'file-begin': {
|
|
3008
|
+
if (activeFile)
|
|
3009
|
+
throw new Error(`EINVAL: nested streamed file ${record.inode.path}`);
|
|
3010
|
+
phase = 'validation';
|
|
3011
|
+
this.validateInodeContentShape(record.inode);
|
|
3012
|
+
phase = 'publish';
|
|
3013
|
+
// Authorising a file reads its parent from the committed inode
|
|
3014
|
+
// tree, so pending directories become visible first.
|
|
3015
|
+
flushDirectories();
|
|
3016
|
+
phase = 'validation';
|
|
3017
|
+
this.withMutationOwner(options.mutationOwner, () => {
|
|
3018
|
+
this.authorizeBatch({ inodes: [record.inode], chunks: [] }, cred);
|
|
3019
|
+
this.assertMutationsAllowed([record.inode.path]);
|
|
3020
|
+
});
|
|
3021
|
+
phase = 'stage';
|
|
3022
|
+
// Close the group before the file that would overflow it, so a
|
|
3023
|
+
// file either fits whole or begins a group of its own. The
|
|
3024
|
+
// estimate only picks the boundary — the flush asserts the bound.
|
|
3025
|
+
const prior = this.inodes.get(normalizeVfsPath(record.inode.path));
|
|
3026
|
+
const overflows = group.wouldExceedFile(record.inode.size, record.inode.chunkCount, prior !== undefined && !prior.isDir);
|
|
3027
|
+
if (overflows !== null)
|
|
3028
|
+
flushGroup();
|
|
3029
|
+
const durableContentId = this.createContentId();
|
|
3030
|
+
this.activeStagingContentIds.add(durableContentId);
|
|
3031
|
+
ownedStagingContentIds.add(durableContentId);
|
|
3032
|
+
group.addStagingContent(durableContentId);
|
|
3033
|
+
activeFile = {
|
|
3034
|
+
streamContentId: record.streamContentId,
|
|
3035
|
+
durableContentId,
|
|
3036
|
+
inode: record.inode,
|
|
3037
|
+
stagedBytes: 0,
|
|
3038
|
+
};
|
|
3039
|
+
break;
|
|
3040
|
+
}
|
|
3041
|
+
case 'file-chunk': {
|
|
3042
|
+
decodedRecordLease = record.retention;
|
|
3043
|
+
phase = 'validation';
|
|
3044
|
+
if (!activeFile
|
|
3045
|
+
|| record.streamContentId !== activeFile.streamContentId
|
|
3046
|
+
|| record.path !== activeFile.inode.path) {
|
|
3047
|
+
throw new Error(`EINVAL: streamed chunk ownership mismatch: ${record.path}`);
|
|
3048
|
+
}
|
|
3049
|
+
phase = 'stage';
|
|
3050
|
+
group.addChunk({
|
|
3051
|
+
path: record.path,
|
|
3052
|
+
contentId: activeFile.durableContentId,
|
|
3053
|
+
chunkId: record.chunkId,
|
|
3054
|
+
data: record.data,
|
|
3055
|
+
});
|
|
3056
|
+
groupLeases.push(record.retention);
|
|
3057
|
+
decodedRecordLease = null;
|
|
3058
|
+
break;
|
|
3059
|
+
}
|
|
3060
|
+
case 'file-end': {
|
|
3061
|
+
phase = 'validation';
|
|
3062
|
+
if (!activeFile || record.streamContentId !== activeFile.streamContentId) {
|
|
3063
|
+
throw new Error(`EINVAL: streamed file-end ownership mismatch: ${record.path}`);
|
|
3064
|
+
}
|
|
3065
|
+
phase = 'publish';
|
|
3066
|
+
const completed = activeFile;
|
|
3067
|
+
const inode = this.normalizeBatchInode(completed.inode, cred);
|
|
3068
|
+
this.withMutationOwner(options.mutationOwner, () => {
|
|
3069
|
+
this.addFilePublication(group, inode, completed.durableContentId);
|
|
3070
|
+
});
|
|
3071
|
+
groupInodes.push(inode);
|
|
3072
|
+
groupContentIds.push(completed.durableContentId);
|
|
3073
|
+
groupPublishedChunks += record.chunkCount;
|
|
3074
|
+
groupStagedBytes += completed.stagedBytes;
|
|
3075
|
+
groupPaths++;
|
|
3076
|
+
activeFile = null;
|
|
3077
|
+
break;
|
|
3078
|
+
}
|
|
3079
|
+
case 'batch-end':
|
|
3080
|
+
phase = 'publish';
|
|
3081
|
+
flushDirectories();
|
|
3082
|
+
flushGroup();
|
|
3083
|
+
if (recordIterator.return)
|
|
3084
|
+
await recordIterator.return();
|
|
3085
|
+
recordIteratorFinished = true;
|
|
3086
|
+
this._decodeDrainStarts.delete(decodeDrainToken);
|
|
3087
|
+
this.recordDuration(this._decodeDrainDuration, decodeDrainWaitMs);
|
|
3088
|
+
decodeDrainFinished = true;
|
|
3089
|
+
return { ok: true, ...progress };
|
|
3090
|
+
}
|
|
3091
|
+
}
|
|
3092
|
+
}
|
|
3093
|
+
catch (error) {
|
|
3094
|
+
return {
|
|
3095
|
+
ok: false,
|
|
3096
|
+
...progress,
|
|
3097
|
+
error: {
|
|
3098
|
+
code: 'ERR_WRITE_BATCH_STREAM',
|
|
3099
|
+
phase,
|
|
3100
|
+
message: this.errorMessage(error),
|
|
3101
|
+
},
|
|
3102
|
+
};
|
|
3103
|
+
}
|
|
3104
|
+
finally {
|
|
3105
|
+
decodedRecordLease?.release();
|
|
3106
|
+
for (const lease of groupLeases)
|
|
3107
|
+
lease.release();
|
|
3108
|
+
groupLeases = [];
|
|
3109
|
+
if (!recordIteratorFinished && recordIterator?.return) {
|
|
3110
|
+
try {
|
|
3111
|
+
await recordIterator.return();
|
|
3112
|
+
}
|
|
3113
|
+
catch { /* preserve the primary stream result */ }
|
|
3114
|
+
}
|
|
3115
|
+
if (!decodeDrainFinished) {
|
|
3116
|
+
this._decodeDrainStarts.delete(decodeDrainToken);
|
|
3117
|
+
this.recordDuration(this._decodeDrainDuration, decodeDrainWaitMs);
|
|
3118
|
+
}
|
|
3119
|
+
// Staged bytes belonging to work that never reached a commit: the
|
|
3120
|
+
// file in flight, plus any completed file still pending in the group.
|
|
3121
|
+
const abandonedStagedBytes = (activeFile?.stagedBytes ?? 0) + groupStagedBytes;
|
|
3122
|
+
this._stagedStreamBytes = Math.max(0, this._stagedStreamBytes - abandonedStagedBytes);
|
|
3123
|
+
for (const contentId of ownedStagingContentIds) {
|
|
3124
|
+
if (this.activeStagingContentIds.has(contentId))
|
|
3125
|
+
this.maintenancePending = true;
|
|
3126
|
+
this.activeStagingContentIds.delete(contentId);
|
|
3127
|
+
}
|
|
3128
|
+
this.runContentMaintenanceSafely(2);
|
|
3129
|
+
}
|
|
3130
|
+
}
|
|
3131
|
+
_writeBatchWithRetry(payload, execution, enforceLimits, onCommit) {
|
|
3132
|
+
if (enforceLimits) {
|
|
3133
|
+
const preflight = this.prepareBatchTransaction(payload, false);
|
|
3134
|
+
if (preflight.plan.metrics.sqlExecs === 0)
|
|
3135
|
+
return { inodes: 0, chunks: 0 };
|
|
3136
|
+
this.assertTransactionFits(onCommit ? withCommitRowMetrics(preflight.plan.metrics) : preflight.plan.metrics);
|
|
3137
|
+
}
|
|
3138
|
+
const prepared = this.prepareBatchTransaction(payload, true);
|
|
3139
|
+
if (prepared.plan.metrics.sqlExecs === 0)
|
|
3140
|
+
return { inodes: 0, chunks: 0 };
|
|
3141
|
+
try {
|
|
3142
|
+
return this._writeBatchOnce(prepared, execution, undefined, onCommit);
|
|
3143
|
+
}
|
|
3144
|
+
catch (error) {
|
|
3145
|
+
const cause = classifyError(error);
|
|
3146
|
+
// Classify before deciding to retry. Only the SQLITE_NOMEM family
|
|
3147
|
+
// is retryable; constraint conflicts / disk-full / clone-refused
|
|
3148
|
+
// / unknown all surface to the caller (fail loud).
|
|
3149
|
+
const lru = this._cacheBytes;
|
|
3150
|
+
const inFlight = this._estimateBatchBytes(payload);
|
|
3151
|
+
recordFailure({
|
|
3152
|
+
at: Date.now(),
|
|
3153
|
+
phase: 'install',
|
|
3154
|
+
cause,
|
|
3155
|
+
rssEstimateBytes: 0,
|
|
3156
|
+
heapUsedBytes: this._safeHeapUsed(),
|
|
3157
|
+
lruBytes: lru,
|
|
3158
|
+
inFlightBytes: inFlight,
|
|
3159
|
+
lastRpcFrame: null,
|
|
3160
|
+
lastFacetId: null,
|
|
3161
|
+
message: this.errorMessage(error),
|
|
3162
|
+
});
|
|
3163
|
+
if (!this.isSqliteNoMem(error))
|
|
3164
|
+
throw error;
|
|
3165
|
+
// Free clean cache pages, then retry the exact same indivisible
|
|
3166
|
+
// transaction once. Splitting a strict batch would publish a prefix
|
|
3167
|
+
// if a later half failed and would advance its revision more than once.
|
|
3168
|
+
this.evictAll();
|
|
3169
|
+
return this._writeBatchOnce(prepared, execution, undefined, onCommit);
|
|
3170
|
+
}
|
|
3171
|
+
}
|
|
3172
|
+
/**
|
|
3173
|
+
* Estimate the byte cost of a writeBatch payload. Used by the W5
|
|
3174
|
+
* recordFailure call so /api/_diag/memory can report inFlightBytes
|
|
3175
|
+
* at the moment of the SQLITE_NOMEM. Fast (no copy).
|
|
3176
|
+
*/
|
|
3177
|
+
_estimateBatchBytes(payload) {
|
|
3178
|
+
let n = 0;
|
|
3179
|
+
for (const c of payload.chunks)
|
|
3180
|
+
n += c.data.length;
|
|
3181
|
+
// Path strings + inode header overhead — rough estimate.
|
|
3182
|
+
for (const i of payload.inodes)
|
|
3183
|
+
n += 80 + i.path.length;
|
|
3184
|
+
return n;
|
|
3185
|
+
}
|
|
3186
|
+
errorMessage(error) {
|
|
3187
|
+
return error instanceof Error ? error.message : String(error);
|
|
3188
|
+
}
|
|
3189
|
+
isSqliteNoMem(error) {
|
|
3190
|
+
if (typeof error === 'object' && error !== null) {
|
|
3191
|
+
const code = error.code;
|
|
3192
|
+
if (code === 'SQLITE_NOMEM' || code === 7)
|
|
3193
|
+
return true;
|
|
3194
|
+
}
|
|
3195
|
+
return this.errorMessage(error).toUpperCase().includes('SQLITE_NOMEM');
|
|
3196
|
+
}
|
|
3197
|
+
transactionSync(callback) {
|
|
3198
|
+
if (!this.ctx?.storage?.transactionSync) {
|
|
3199
|
+
throw new Error('[sqlite-vfs] atomic storage operation requires transactionSync');
|
|
3200
|
+
}
|
|
3201
|
+
this.ctx.storage.transactionSync(callback);
|
|
3202
|
+
}
|
|
3203
|
+
executeTransactionPlan(plan, execution, onCommit) {
|
|
3204
|
+
this.executeMeasuredTransaction(plan, execution, () => {
|
|
3205
|
+
for (const path of plan.deletedPaths) {
|
|
3206
|
+
this.sql.exec("DELETE FROM inodes WHERE path = ?", path);
|
|
3207
|
+
}
|
|
3208
|
+
for (let i = 0; i < plan.stagingContentIds.length; i += CONTENT_IDS_PER_SQL_EXEC) {
|
|
3209
|
+
const batch = plan.stagingContentIds.slice(i, i + CONTENT_IDS_PER_SQL_EXEC);
|
|
3210
|
+
const placeholders = batch.map(() => "(?, 'staging', ?)").join(',');
|
|
3211
|
+
const values = [];
|
|
3212
|
+
const createdAt = Date.now();
|
|
3213
|
+
for (const contentId of batch)
|
|
3214
|
+
values.push(contentId, createdAt);
|
|
3215
|
+
this.sql.exec(`INSERT INTO content_lifecycle (content_id, state, created_at) VALUES ${placeholders}`, ...values);
|
|
3216
|
+
}
|
|
3217
|
+
for (let i = 0; i < plan.inodes.length; i += INODE_ROWS_PER_SQL_EXEC) {
|
|
3218
|
+
const batch = plan.inodes.slice(i, i + INODE_ROWS_PER_SQL_EXEC);
|
|
3219
|
+
const placeholders = batch.map(() => '(?,?,?,?,?,?,?,?,?,?,?)').join(',');
|
|
3220
|
+
const values = [];
|
|
3221
|
+
for (const inode of batch) {
|
|
3222
|
+
const atime = inode.atime !== undefined && Number.isFinite(inode.atime)
|
|
3223
|
+
? inode.atime
|
|
3224
|
+
: inode.mtime;
|
|
3225
|
+
values.push(inode.path, inode.parentPath, inodeKindCode(inode.kind), inode.size, atime, inode.mtime, inode.mode, inode.uid, inode.gid, inode.chunkCount, inode.contentId);
|
|
3226
|
+
}
|
|
3227
|
+
this.sql.exec(`INSERT OR REPLACE INTO inodes (path, parent_path, kind, size, atime, mtime, mode, uid, gid, chunk_count, content_id) VALUES ${placeholders}`, ...values);
|
|
3228
|
+
}
|
|
3229
|
+
for (let i = 0; i < plan.chunks.length; i += CHUNK_ROWS_PER_SQL_EXEC) {
|
|
3230
|
+
const batch = plan.chunks.slice(i, i + CHUNK_ROWS_PER_SQL_EXEC);
|
|
3231
|
+
const placeholders = batch.map(() => '(?,?,?)').join(',');
|
|
3232
|
+
const values = [];
|
|
3233
|
+
for (const chunk of batch)
|
|
3234
|
+
values.push(chunk.contentId, chunk.chunkId, chunk.data);
|
|
3235
|
+
this.sql.exec(`INSERT OR REPLACE INTO file_chunks (content_id, chunk_id, data) VALUES ${placeholders}`, ...values);
|
|
3236
|
+
}
|
|
3237
|
+
const deletedChunksByContent = new Map();
|
|
3238
|
+
for (const chunk of plan.deletedChunks) {
|
|
3239
|
+
const entries = deletedChunksByContent.get(chunk.contentId);
|
|
3240
|
+
if (entries)
|
|
3241
|
+
entries.push(chunk);
|
|
3242
|
+
else
|
|
3243
|
+
deletedChunksByContent.set(chunk.contentId, [chunk]);
|
|
3244
|
+
}
|
|
3245
|
+
for (const [contentId, chunks] of deletedChunksByContent) {
|
|
3246
|
+
for (let i = 0; i < chunks.length; i += CHUNK_ROWS_PER_SQL_EXEC) {
|
|
3247
|
+
const batch = chunks.slice(i, i + CHUNK_ROWS_PER_SQL_EXEC);
|
|
3248
|
+
const placeholders = batch.map(() => '?').join(',');
|
|
3249
|
+
this.sql.exec(`DELETE FROM file_chunks WHERE content_id = ? AND chunk_id IN (${placeholders})`, contentId, ...batch.map((chunk) => chunk.chunkId));
|
|
3250
|
+
}
|
|
3251
|
+
}
|
|
3252
|
+
for (let i = 0; i < plan.publishedContentIds.length; i += CONTENT_IDS_PER_SQL_EXEC) {
|
|
3253
|
+
const batch = plan.publishedContentIds.slice(i, i + CONTENT_IDS_PER_SQL_EXEC);
|
|
3254
|
+
const placeholders = batch.map(() => '?').join(',');
|
|
3255
|
+
this.sql.exec(`DELETE FROM content_lifecycle WHERE content_id IN (${placeholders}) AND state = 'staging'`, ...batch);
|
|
3256
|
+
}
|
|
3257
|
+
for (let i = 0; i < plan.gcContentIds.length; i += CONTENT_IDS_PER_SQL_EXEC) {
|
|
3258
|
+
const batch = plan.gcContentIds.slice(i, i + CONTENT_IDS_PER_SQL_EXEC);
|
|
3259
|
+
const placeholders = batch.map(() => "(?, 'gc', ?)").join(',');
|
|
3260
|
+
const values = [];
|
|
3261
|
+
const createdAt = Date.now();
|
|
3262
|
+
for (const contentId of batch)
|
|
3263
|
+
values.push(contentId, createdAt);
|
|
3264
|
+
this.sql.exec(`INSERT INTO content_lifecycle (content_id, state, created_at) VALUES ${placeholders}
|
|
3265
|
+
ON CONFLICT(content_id) DO UPDATE SET state = 'gc'`, ...values);
|
|
3266
|
+
}
|
|
3267
|
+
onCommit?.();
|
|
3268
|
+
});
|
|
3269
|
+
if (plan.gcContentIds.length > 0)
|
|
3270
|
+
this.maintenancePending = true;
|
|
3271
|
+
}
|
|
3272
|
+
executeMeasuredTransaction(plan, execution, callback) {
|
|
3273
|
+
if (this._activeTransaction !== null) {
|
|
3274
|
+
throw new Error('[sqlite-vfs] nested transaction plan execution is not supported');
|
|
3275
|
+
}
|
|
3276
|
+
const startedAt = performance.now();
|
|
3277
|
+
this._activeTransaction = { startedAt, plan, execution };
|
|
3278
|
+
try {
|
|
3279
|
+
this.transactionSync(callback);
|
|
3280
|
+
}
|
|
3281
|
+
finally {
|
|
3282
|
+
const durationMs = performance.now() - startedAt;
|
|
3283
|
+
this.recordDuration(this._transactionDuration, durationMs);
|
|
3284
|
+
this._transactionDurationSamples[this._transactionDurationSampleIndex] = durationMs;
|
|
3285
|
+
this._transactionDurationSampleIndex = (this._transactionDurationSampleIndex + 1) % TRANSACTION_DURATION_SAMPLE_COUNT;
|
|
3286
|
+
this._transactionDurationSampleCount = Math.min(this._transactionDurationSampleCount + 1, TRANSACTION_DURATION_SAMPLE_COUNT);
|
|
3287
|
+
this._transactionPeakBlobBytes = Math.max(this._transactionPeakBlobBytes, plan.metrics.blobBytes);
|
|
3288
|
+
this._transactionPeakLogicalRows = Math.max(this._transactionPeakLogicalRows, plan.metrics.logicalRows);
|
|
3289
|
+
this._transactionPeakSqlExecs = Math.max(this._transactionPeakSqlExecs, plan.metrics.sqlExecs);
|
|
3290
|
+
this._transactionPeakAffectedPaths = Math.max(this._transactionPeakAffectedPaths, plan.metrics.affectedPaths);
|
|
3291
|
+
if (execution.limitMode === 'bounded') {
|
|
3292
|
+
this._boundedTransactionPeakBlobBytes = Math.max(this._boundedTransactionPeakBlobBytes, plan.metrics.blobBytes);
|
|
3293
|
+
this._boundedTransactionPeakLogicalRows = Math.max(this._boundedTransactionPeakLogicalRows, plan.metrics.logicalRows);
|
|
3294
|
+
this._boundedTransactionPeakSqlExecs = Math.max(this._boundedTransactionPeakSqlExecs, plan.metrics.sqlExecs);
|
|
3295
|
+
}
|
|
3296
|
+
this._lastTransaction = { metrics: plan.metrics, execution };
|
|
3297
|
+
this._activeTransaction = null;
|
|
3298
|
+
}
|
|
3299
|
+
}
|
|
3300
|
+
/**
|
|
3301
|
+
* Bounded, idempotent content maintenance. Age only orders work; durable
|
|
3302
|
+
* reference checks in each mutation transaction are the deletion authority.
|
|
3303
|
+
*/
|
|
3304
|
+
runContentMaintenance(maxTransactions = 4) {
|
|
3305
|
+
let transactions = 0;
|
|
3306
|
+
const maximum = clampNonNegativeInt(maxTransactions);
|
|
3307
|
+
let orphanScanComplete = true;
|
|
3308
|
+
let stagingScanComplete = true;
|
|
3309
|
+
if (transactions < maximum) {
|
|
3310
|
+
const orphanRows = [...this.sql.exec(`SELECT chunks.content_id
|
|
3311
|
+
FROM file_chunks AS chunks
|
|
3312
|
+
WHERE NOT EXISTS (
|
|
3313
|
+
SELECT 1 FROM content_lifecycle AS lifecycle
|
|
3314
|
+
WHERE lifecycle.content_id = chunks.content_id
|
|
3315
|
+
)
|
|
3316
|
+
AND ${sqlNoInodeContentReference('chunks.content_id')}
|
|
3317
|
+
GROUP BY chunks.content_id
|
|
3318
|
+
ORDER BY chunks.content_id
|
|
3319
|
+
LIMIT ?`, CONTENT_IDS_PER_SQL_EXEC)];
|
|
3320
|
+
orphanScanComplete = orphanRows.length < CONTENT_IDS_PER_SQL_EXEC;
|
|
3321
|
+
const contentIds = orphanRows.map((row) => String(row.content_id));
|
|
3322
|
+
if (contentIds.length > 0) {
|
|
3323
|
+
const candidates = contentIds
|
|
3324
|
+
.map((_, index) => index === 0 ? 'SELECT ? AS content_id' : 'SELECT ?')
|
|
3325
|
+
.join(' UNION ALL ');
|
|
3326
|
+
const plan = this.metricsOnlyPlan({
|
|
3327
|
+
blobBytes: 0,
|
|
3328
|
+
logicalRows: contentIds.length,
|
|
3329
|
+
sqlExecs: 1,
|
|
3330
|
+
affectedPaths: 0,
|
|
3331
|
+
});
|
|
3332
|
+
this.executeMeasuredTransaction(plan, { source: 'content-gc', limitMode: 'bounded' }, () => {
|
|
3333
|
+
this.sql.exec(`INSERT OR IGNORE INTO content_lifecycle (content_id, state, created_at)
|
|
3334
|
+
SELECT candidate.content_id, 'gc', ?
|
|
3335
|
+
FROM (${candidates}) AS candidate
|
|
3336
|
+
WHERE EXISTS (
|
|
3337
|
+
SELECT 1 FROM file_chunks
|
|
3338
|
+
WHERE file_chunks.content_id = candidate.content_id
|
|
3339
|
+
)
|
|
3340
|
+
AND ${sqlNoInodeContentReference('candidate.content_id')}`, Date.now(), ...contentIds);
|
|
3341
|
+
});
|
|
3342
|
+
transactions++;
|
|
3343
|
+
}
|
|
3344
|
+
}
|
|
3345
|
+
if (transactions < maximum) {
|
|
3346
|
+
const stagingRows = [...this.sql.exec(`SELECT lifecycle.content_id
|
|
3347
|
+
FROM content_lifecycle AS lifecycle
|
|
3348
|
+
WHERE lifecycle.state = 'staging'
|
|
3349
|
+
AND ${sqlNoInodeContentReference('lifecycle.content_id')}
|
|
3350
|
+
ORDER BY lifecycle.created_at, lifecycle.content_id
|
|
3351
|
+
LIMIT ?`, CONTENT_IDS_PER_SQL_EXEC)];
|
|
3352
|
+
stagingScanComplete = stagingRows.length < CONTENT_IDS_PER_SQL_EXEC;
|
|
3353
|
+
const contentIds = stagingRows
|
|
3354
|
+
.map((row) => String(row.content_id))
|
|
3355
|
+
.filter((contentId) => !this.activeStagingContentIds.has(contentId));
|
|
3356
|
+
if (contentIds.length > 0) {
|
|
3357
|
+
const placeholders = contentIds.map(() => '?').join(',');
|
|
3358
|
+
const plan = this.metricsOnlyPlan({
|
|
3359
|
+
blobBytes: 0,
|
|
3360
|
+
logicalRows: contentIds.length,
|
|
3361
|
+
sqlExecs: 1,
|
|
3362
|
+
affectedPaths: 0,
|
|
3363
|
+
});
|
|
3364
|
+
this.executeMeasuredTransaction(plan, { source: 'content-gc', limitMode: 'bounded' }, () => {
|
|
3365
|
+
this.sql.exec(`UPDATE content_lifecycle AS lifecycle
|
|
3366
|
+
SET state = 'gc'
|
|
3367
|
+
WHERE lifecycle.state = 'staging'
|
|
3368
|
+
AND lifecycle.content_id IN (${placeholders})
|
|
3369
|
+
AND ${sqlNoInodeContentReference('lifecycle.content_id')}`, ...contentIds);
|
|
3370
|
+
});
|
|
3371
|
+
transactions++;
|
|
3372
|
+
}
|
|
3373
|
+
}
|
|
3374
|
+
while (transactions < maximum) {
|
|
3375
|
+
const lifecycle = [...this.sql.exec(`SELECT lifecycle.content_id
|
|
3376
|
+
FROM content_lifecycle AS lifecycle
|
|
3377
|
+
WHERE lifecycle.state = 'gc'
|
|
3378
|
+
AND ${sqlNoInodeContentReference('lifecycle.content_id')}
|
|
3379
|
+
ORDER BY lifecycle.created_at, lifecycle.content_id
|
|
3380
|
+
LIMIT 1`)];
|
|
3381
|
+
if (lifecycle.length === 0)
|
|
3382
|
+
break;
|
|
3383
|
+
const contentId = String(lifecycle[0].content_id);
|
|
3384
|
+
const candidates = [...this.sql.exec(`SELECT chunk_id, length(data) AS byte_length
|
|
3385
|
+
FROM file_chunks
|
|
3386
|
+
WHERE content_id = ?
|
|
3387
|
+
ORDER BY chunk_id
|
|
3388
|
+
LIMIT ?`, contentId, Math.min(MAX_TX_LOGICAL_ROWS - 1, CHUNK_ROWS_PER_SQL_EXEC))];
|
|
3389
|
+
const chunkIds = [];
|
|
3390
|
+
let blobBytes = 0;
|
|
3391
|
+
for (const row of candidates) {
|
|
3392
|
+
const byteLength = clampNonNegativeInt(Number(row.byte_length));
|
|
3393
|
+
if (chunkIds.length > 0 && blobBytes + byteLength > MAX_TX_BLOB_BYTES)
|
|
3394
|
+
break;
|
|
3395
|
+
chunkIds.push(clampNonNegativeInt(Number(row.chunk_id)));
|
|
3396
|
+
blobBytes += byteLength;
|
|
3397
|
+
}
|
|
3398
|
+
const plan = this.metricsOnlyPlan({
|
|
3399
|
+
blobBytes,
|
|
3400
|
+
logicalRows: chunkIds.length + 1,
|
|
3401
|
+
sqlExecs: chunkIds.length === 0 ? 1 : 2,
|
|
3402
|
+
affectedPaths: 0,
|
|
3403
|
+
});
|
|
3404
|
+
this.assertTransactionFits(plan.metrics);
|
|
3405
|
+
this.executeMeasuredTransaction(plan, { source: 'content-gc', limitMode: 'bounded' }, () => {
|
|
3406
|
+
if (chunkIds.length > 0) {
|
|
3407
|
+
const placeholders = chunkIds.map(() => '?').join(',');
|
|
3408
|
+
this.sql.exec(`DELETE FROM file_chunks
|
|
3409
|
+
WHERE content_id = ?
|
|
3410
|
+
AND chunk_id IN (${placeholders})
|
|
3411
|
+
AND EXISTS (
|
|
3412
|
+
SELECT 1 FROM content_lifecycle
|
|
3413
|
+
WHERE content_lifecycle.content_id = ?
|
|
3414
|
+
AND content_lifecycle.state = 'gc'
|
|
3415
|
+
)
|
|
3416
|
+
AND ${sqlNoInodeContentReference('?')}`, contentId, ...chunkIds, contentId, contentId, contentId);
|
|
3417
|
+
}
|
|
3418
|
+
this.sql.exec(`DELETE FROM content_lifecycle AS lifecycle
|
|
3419
|
+
WHERE lifecycle.content_id = ?
|
|
3420
|
+
AND lifecycle.state = 'gc'
|
|
3421
|
+
AND ${sqlNoInodeContentReference('lifecycle.content_id')}
|
|
3422
|
+
AND NOT EXISTS (
|
|
3423
|
+
SELECT 1 FROM file_chunks
|
|
3424
|
+
WHERE file_chunks.content_id = lifecycle.content_id
|
|
3425
|
+
)`, contentId);
|
|
3426
|
+
});
|
|
3427
|
+
transactions++;
|
|
3428
|
+
}
|
|
3429
|
+
if (maximum > 0) {
|
|
3430
|
+
const hasGcBacklog = transactions >= maximum && [...this.sql.exec(`SELECT 1
|
|
3431
|
+
FROM content_lifecycle AS lifecycle
|
|
3432
|
+
WHERE lifecycle.state = 'gc'
|
|
3433
|
+
AND ${sqlNoInodeContentReference('lifecycle.content_id')}
|
|
3434
|
+
LIMIT 1`)].length > 0;
|
|
3435
|
+
this.maintenancePending = !orphanScanComplete || !stagingScanComplete || hasGcBacklog;
|
|
3436
|
+
}
|
|
3437
|
+
return { transactions };
|
|
3438
|
+
}
|
|
3439
|
+
runContentMaintenanceSafely(maxTransactions, force = false) {
|
|
3440
|
+
if (!force && !this.maintenancePending)
|
|
3441
|
+
return;
|
|
3442
|
+
const startedAt = performance.now();
|
|
3443
|
+
try {
|
|
3444
|
+
this.runContentMaintenance(maxTransactions);
|
|
3445
|
+
}
|
|
3446
|
+
catch (error) {
|
|
3447
|
+
this.maintenancePending = true;
|
|
3448
|
+
console.error('[sqlite-vfs] content maintenance failed:', this.errorMessage(error));
|
|
3449
|
+
}
|
|
3450
|
+
finally {
|
|
3451
|
+
this.recordDuration(this._maintenanceDuration, performance.now() - startedAt);
|
|
3452
|
+
}
|
|
3453
|
+
}
|
|
3454
|
+
metricsOnlyPlan(metrics) {
|
|
3455
|
+
return {
|
|
3456
|
+
inodes: [],
|
|
3457
|
+
chunks: [],
|
|
3458
|
+
deletedChunks: [],
|
|
3459
|
+
deletedPaths: [],
|
|
3460
|
+
stagingContentIds: [],
|
|
3461
|
+
publishedContentIds: [],
|
|
3462
|
+
gcContentIds: [],
|
|
3463
|
+
affectedPaths: new Set(),
|
|
3464
|
+
metrics,
|
|
3465
|
+
};
|
|
3466
|
+
}
|
|
3467
|
+
recordOverLimitFile(path, limit, metrics) {
|
|
3468
|
+
this._overLimitFileCount++;
|
|
3469
|
+
this._lastOverLimitFile = { path, limit, ...metrics };
|
|
3470
|
+
}
|
|
3471
|
+
recordDuration(summary, durationMs) {
|
|
3472
|
+
summary.count++;
|
|
3473
|
+
summary.totalMs += durationMs;
|
|
3474
|
+
summary.lastMs = durationMs;
|
|
3475
|
+
summary.maxMs = Math.max(summary.maxMs, durationMs);
|
|
3476
|
+
}
|
|
3477
|
+
currentRetainedWriteBytes() {
|
|
3478
|
+
return this.writeStreamCredits.stats.current;
|
|
3479
|
+
}
|
|
3480
|
+
/** Best-effort process.memoryUsage().heapUsed; 0 in DO contexts. */
|
|
3481
|
+
_safeHeapUsed() {
|
|
3482
|
+
try {
|
|
3483
|
+
const mu = globalThis.process?.memoryUsage?.();
|
|
3484
|
+
return Number(mu?.heapUsed) || 0;
|
|
3485
|
+
}
|
|
3486
|
+
catch {
|
|
3487
|
+
return 0;
|
|
3488
|
+
}
|
|
3489
|
+
}
|
|
3490
|
+
prepareBatchTransaction(payload, allocateContentIds) {
|
|
3491
|
+
const deletedInodes = this.collectSubtreeInodes(payload.deletePaths ?? []);
|
|
3492
|
+
const deletedInodesByPath = new Map(deletedInodes.map((inode) => [inode.path, inode]));
|
|
3493
|
+
const builder = new TransactionPlanBuilder();
|
|
3494
|
+
const deletedPaths = new Set(payload.deletePaths ?? []);
|
|
3495
|
+
for (const inode of deletedInodes)
|
|
3496
|
+
deletedPaths.add(inode.path);
|
|
3497
|
+
for (const path of deletedPaths) {
|
|
3498
|
+
const inode = deletedInodesByPath.get(path);
|
|
3499
|
+
builder.addDeletedPath(path, inode);
|
|
3500
|
+
if (inode && !inode.isDir)
|
|
3501
|
+
builder.addGcContent(this.contentIdForInode(inode));
|
|
3502
|
+
}
|
|
3503
|
+
const normalizedInodes = new Map();
|
|
3504
|
+
for (const entry of payload.inodes) {
|
|
3505
|
+
this.validateInodeContentShape(entry);
|
|
3506
|
+
const kind = inodeKind(entry);
|
|
3507
|
+
normalizedInodes.set(entry.path, {
|
|
3508
|
+
...entry,
|
|
3509
|
+
kind,
|
|
3510
|
+
isDir: kind === 'directory',
|
|
3511
|
+
uid: entry.uid ?? 1000,
|
|
3512
|
+
gid: entry.gid ?? 1000,
|
|
3513
|
+
});
|
|
3514
|
+
}
|
|
3515
|
+
const chunksByPath = new Map();
|
|
3516
|
+
for (const chunk of payload.chunks) {
|
|
3517
|
+
const entries = chunksByPath.get(chunk.path);
|
|
3518
|
+
if (entries)
|
|
3519
|
+
entries.push(chunk);
|
|
3520
|
+
else
|
|
3521
|
+
chunksByPath.set(chunk.path, [chunk]);
|
|
3522
|
+
}
|
|
3523
|
+
const contentIds = new Map();
|
|
3524
|
+
const reservedContentIds = allocateContentIds ? new Set() : undefined;
|
|
3525
|
+
let preflightContentIndex = 0;
|
|
3526
|
+
for (const entry of normalizedInodes.values()) {
|
|
3527
|
+
const prior = this.inodes.get(entry.path);
|
|
3528
|
+
if (entry.isDir) {
|
|
3529
|
+
if ((chunksByPath.get(entry.path)?.length ?? 0) > 0) {
|
|
3530
|
+
throw new Error(`EINVAL: directory batch entry has chunks: ${entry.path}`);
|
|
3531
|
+
}
|
|
3532
|
+
builder.addInode({ ...entry, kind: inodeKind(entry), contentId: null });
|
|
3533
|
+
}
|
|
3534
|
+
else {
|
|
3535
|
+
const fileChunks = chunksByPath.get(entry.path) ?? [];
|
|
3536
|
+
this.validateFileChunks(entry, fileChunks);
|
|
3537
|
+
const contentId = allocateContentIds
|
|
3538
|
+
? this.createContentId(reservedContentIds)
|
|
3539
|
+
: `preflight:${preflightContentIndex++}`;
|
|
3540
|
+
contentIds.set(entry.path, contentId);
|
|
3541
|
+
builder.addStagingContent(contentId);
|
|
3542
|
+
builder.addInode({ ...entry, kind: inodeKind(entry), contentId });
|
|
3543
|
+
builder.addPublishedContent(contentId);
|
|
3544
|
+
}
|
|
3545
|
+
if (prior && !prior.isDir)
|
|
3546
|
+
builder.addGcContent(this.contentIdForInode(prior));
|
|
3547
|
+
}
|
|
3548
|
+
for (const entry of payload.chunks) {
|
|
3549
|
+
const contentId = contentIds.get(entry.path)
|
|
3550
|
+
?? (() => {
|
|
3551
|
+
const inode = this.inodes.get(entry.path);
|
|
3552
|
+
if (!inode || inode.kind !== 'file') {
|
|
3553
|
+
throw new Error(`EINVAL: chunk has no regular file inode: ${entry.path}`);
|
|
3554
|
+
}
|
|
3555
|
+
return this.contentIdForInode(inode);
|
|
3556
|
+
})();
|
|
3557
|
+
builder.addChunk({ ...entry, contentId });
|
|
3558
|
+
}
|
|
3559
|
+
return {
|
|
3560
|
+
payload: { ...payload, inodes: [...normalizedInodes.values()] },
|
|
3561
|
+
plan: builder.build(),
|
|
3562
|
+
deletedInodes,
|
|
3563
|
+
};
|
|
3564
|
+
}
|
|
3565
|
+
validateFileChunks(inode, chunks) {
|
|
3566
|
+
this.validateInodeContentShape(inode);
|
|
3567
|
+
if (inode.chunkCount !== chunks.length) {
|
|
3568
|
+
throw new Error(`EINVAL: ${inode.path}: expected ${inode.chunkCount} chunks, got ${chunks.length}`);
|
|
3569
|
+
}
|
|
3570
|
+
let total = 0;
|
|
3571
|
+
const ordered = [...chunks].sort((a, b) => a.chunkId - b.chunkId);
|
|
3572
|
+
for (let index = 0; index < ordered.length; index++) {
|
|
3573
|
+
const chunk = ordered[index];
|
|
3574
|
+
if (chunk.chunkId !== index) {
|
|
3575
|
+
throw new Error(`EINVAL: ${inode.path}: expected chunk ${index}, got ${chunk.chunkId}`);
|
|
3576
|
+
}
|
|
3577
|
+
const expected = Math.min(CHUNK_SIZE, inode.size - (index * CHUNK_SIZE));
|
|
3578
|
+
if (chunk.data.byteLength !== expected) {
|
|
3579
|
+
throw new Error(`EINVAL: ${inode.path}: chunk ${index} has ${chunk.data.byteLength} bytes; expected ${expected}`);
|
|
3580
|
+
}
|
|
3581
|
+
total += chunk.data.byteLength;
|
|
3582
|
+
}
|
|
3583
|
+
if (total !== inode.size) {
|
|
3584
|
+
throw new Error(`EINVAL: ${inode.path}: chunk bytes ${total} do not match size ${inode.size}`);
|
|
3585
|
+
}
|
|
3586
|
+
}
|
|
3587
|
+
validateInodeContentShape(inode) {
|
|
3588
|
+
const kind = inodeKind(inode);
|
|
3589
|
+
const expectedParent = this.parentPath(inode.path);
|
|
3590
|
+
if (inode.parentPath !== expectedParent) {
|
|
3591
|
+
throw new Error(`EINVAL: ${inode.path}: parentPath ${inode.parentPath} does not match ${expectedParent}`);
|
|
3592
|
+
}
|
|
3593
|
+
if (inode.isDir !== (kind === 'directory')) {
|
|
3594
|
+
throw new Error(`EINVAL: ${inode.path}: inode kind ${kind} conflicts with isDir=${inode.isDir}`);
|
|
3595
|
+
}
|
|
3596
|
+
if (!Number.isSafeInteger(inode.size) || inode.size < 0) {
|
|
3597
|
+
throw new Error(`EINVAL: ${inode.path}: invalid size ${inode.size}`);
|
|
3598
|
+
}
|
|
3599
|
+
if (!Number.isSafeInteger(inode.chunkCount) || inode.chunkCount < 0) {
|
|
3600
|
+
throw new Error(`EINVAL: ${inode.path}: invalid chunk count ${inode.chunkCount}`);
|
|
3601
|
+
}
|
|
3602
|
+
if (kind === 'directory' && inode.size !== 0) {
|
|
3603
|
+
throw new Error(`EINVAL: ${inode.path}: directory size must be zero`);
|
|
3604
|
+
}
|
|
3605
|
+
const expectedChunkCount = kind === 'directory' || inode.size === 0
|
|
3606
|
+
? 0
|
|
3607
|
+
: Math.ceil(inode.size / CHUNK_SIZE);
|
|
3608
|
+
if (inode.chunkCount !== expectedChunkCount) {
|
|
3609
|
+
throw new Error(`EINVAL: ${inode.path}: expected ${expectedChunkCount} chunks for ${inode.size} bytes, got ${inode.chunkCount}`);
|
|
3610
|
+
}
|
|
3611
|
+
}
|
|
3612
|
+
assertTransactionFits(metrics) {
|
|
3613
|
+
const limit = exceededTransactionLimit(metrics);
|
|
3614
|
+
if (limit === null)
|
|
3615
|
+
return;
|
|
3616
|
+
const maximum = limit === 'blobBytes'
|
|
3617
|
+
? MAX_TX_BLOB_BYTES
|
|
3618
|
+
: limit === 'logicalRows'
|
|
3619
|
+
? MAX_TX_LOGICAL_ROWS
|
|
3620
|
+
: MAX_TX_SQL_EXECS;
|
|
3621
|
+
throw new SqliteVfsTransactionTooLargeError(limit, metrics[limit], maximum, metrics);
|
|
3622
|
+
}
|
|
3623
|
+
_writeBatchOnce(prepared, execution, publishedChunkCount, onCommit) {
|
|
3624
|
+
const { payload, plan, deletedInodes } = prepared;
|
|
3625
|
+
try {
|
|
3626
|
+
this.executeTransactionPlan(plan, execution, onCommit);
|
|
3627
|
+
}
|
|
3628
|
+
catch (error) {
|
|
3629
|
+
console.error('[sqlite-vfs] writeBatch failed:', this.errorMessage(error));
|
|
3630
|
+
throw error;
|
|
3631
|
+
}
|
|
3632
|
+
const postCommitStartedAt = performance.now();
|
|
3633
|
+
// Durable commit succeeded. Only now may this operation supersede cached
|
|
3634
|
+
// data on the same paths.
|
|
3635
|
+
this.cacheInvalidateBatch(plan.affectedPaths);
|
|
3636
|
+
// 4. Publish the recursive deletions, then inode replacements.
|
|
3637
|
+
for (const inode of deletedInodes) {
|
|
3638
|
+
this._removeFromChildrenIndex(inode.parentPath, inode.path);
|
|
3639
|
+
this.children.delete(inode.path);
|
|
3640
|
+
this.inodes.delete(inode.path);
|
|
3641
|
+
if (inode.isDir) {
|
|
3642
|
+
this._totalDirs--;
|
|
3643
|
+
}
|
|
3644
|
+
else {
|
|
3645
|
+
this._totalFiles--;
|
|
3646
|
+
this._usedBytes -= inode.size;
|
|
3647
|
+
}
|
|
3648
|
+
}
|
|
3649
|
+
// Update in-memory inode tree + children index (outside transaction — fast).
|
|
3650
|
+
// B3: also maintain running counters in sync. For each payload entry,
|
|
3651
|
+
// compute the delta against any pre-existing inode at that path.
|
|
3652
|
+
//
|
|
3653
|
+
// OUTSIDE the `prior === undefined` guard. Pre-W2.5a, ~37 of 46 packages
|
|
3654
|
+
// per `npm install fastify` accumulated inodes in SQL but never reached
|
|
3655
|
+
// the in-memory `this.children` index because some path's `prior` was
|
|
3656
|
+
// unexpectedly defined when the package's writeBatch arrived (root
|
|
3657
|
+
// cause unidentified — see §4.2 diagnostic plan in W2.5-plan.md).
|
|
3658
|
+
// `_addToChildrenIndex` uses Set.add so repeated calls are idempotent;
|
|
3659
|
+
// gating it on `prior === undefined` was the bug. Counters remain
|
|
3660
|
+
// gated correctly so they don't double-count.
|
|
3661
|
+
const __diag = (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1');
|
|
3662
|
+
const replacedPaths = new Set();
|
|
3663
|
+
for (const entry of plan.inodes) {
|
|
3664
|
+
const prior = this.inodes.get(entry.path);
|
|
3665
|
+
if (prior !== undefined)
|
|
3666
|
+
replacedPaths.add(entry.path);
|
|
3667
|
+
const atime = entry.atime !== undefined && Number.isFinite(entry.atime) ? entry.atime : entry.mtime;
|
|
3668
|
+
const node = {
|
|
3669
|
+
path: entry.path,
|
|
3670
|
+
parentPath: entry.parentPath,
|
|
3671
|
+
kind: entry.kind,
|
|
3672
|
+
isDir: entry.isDir,
|
|
3673
|
+
size: entry.size,
|
|
3674
|
+
atime,
|
|
3675
|
+
mtime: entry.mtime,
|
|
3676
|
+
mode: entry.mode,
|
|
3677
|
+
uid: entry.uid,
|
|
3678
|
+
gid: entry.gid,
|
|
3679
|
+
chunkCount: entry.chunkCount,
|
|
3680
|
+
contentId: entry.contentId,
|
|
3681
|
+
};
|
|
3682
|
+
if (prior && prior.parentPath !== entry.parentPath) {
|
|
3683
|
+
this._removeFromChildrenIndex(prior.parentPath, entry.path);
|
|
3684
|
+
}
|
|
3685
|
+
this.inodes.set(entry.path, node);
|
|
3686
|
+
// ALWAYS re-affirm the children-index entry. Idempotent.
|
|
3687
|
+
this._addToChildrenIndex(entry.parentPath, entry.path);
|
|
3688
|
+
// W2.5b diagnostic: log every "stale prior" case where we'd have
|
|
3689
|
+
// skipped the index call pre-W2.5a. Reveals which paths were
|
|
3690
|
+
// pre-populated in this.inodes by a code path other than the
|
|
3691
|
+
// current writeBatch — H5a / H8 candidate.
|
|
3692
|
+
if (__diag && prior !== undefined) {
|
|
3693
|
+
const indexed = this.children.get(entry.parentPath)?.has(entry.path) ?? false;
|
|
3694
|
+
// eslint-disable-next-line no-console
|
|
3695
|
+
console.warn('[sqlite-vfs/W2.5b] stale-prior path=' + entry.path +
|
|
3696
|
+
' parent=' + entry.parentPath +
|
|
3697
|
+
' priorParent=' + prior.parentPath +
|
|
3698
|
+
' priorIsDir=' + prior.isDir +
|
|
3699
|
+
' entryIsDir=' + entry.isDir +
|
|
3700
|
+
' indexedBefore=' + indexed);
|
|
3701
|
+
}
|
|
3702
|
+
// Counter delta — gated on prior so we don't double-count.
|
|
3703
|
+
if (prior === undefined) {
|
|
3704
|
+
if (entry.isDir)
|
|
3705
|
+
this._totalDirs++;
|
|
3706
|
+
else {
|
|
3707
|
+
this._totalFiles++;
|
|
3708
|
+
this._usedBytes += entry.size;
|
|
3709
|
+
}
|
|
3710
|
+
}
|
|
3711
|
+
else {
|
|
3712
|
+
// Replace: handle dir↔file flip + size delta. (Identical to pre-W2.5a.)
|
|
3713
|
+
if (prior.isDir && !entry.isDir) {
|
|
3714
|
+
this._totalDirs--;
|
|
3715
|
+
this._totalFiles++;
|
|
3716
|
+
this._usedBytes += entry.size;
|
|
3717
|
+
}
|
|
3718
|
+
else if (!prior.isDir && entry.isDir) {
|
|
3719
|
+
this._totalFiles--;
|
|
3720
|
+
this._usedBytes -= prior.size;
|
|
3721
|
+
this._totalDirs++;
|
|
3722
|
+
}
|
|
3723
|
+
else if (!entry.isDir) {
|
|
3724
|
+
// File-replace: size delta only.
|
|
3725
|
+
this._usedBytes += entry.size - prior.size;
|
|
3726
|
+
}
|
|
3727
|
+
// Dir-replace (both dir): no counter change.
|
|
3728
|
+
}
|
|
3729
|
+
}
|
|
3730
|
+
const inodeCount = plan.inodes.length;
|
|
3731
|
+
const chunkCount = publishedChunkCount ?? plan.chunks.length;
|
|
3732
|
+
this._sqlWrites += inodeCount + chunkCount;
|
|
3733
|
+
this._batchWrites++;
|
|
3734
|
+
this._batchWriteRows += inodeCount + chunkCount;
|
|
3735
|
+
if (inodeCount > 0 || chunkCount > 0 || plan.deletedPaths.length > 0) {
|
|
3736
|
+
// One clock tick for the whole batch; stamp every touched path
|
|
3737
|
+
// (affectedPaths covers files/chunks/deletes; add dir inodes too).
|
|
3738
|
+
const touched = new Set(plan.affectedPaths);
|
|
3739
|
+
for (const entry of plan.inodes)
|
|
3740
|
+
touched.add(entry.path);
|
|
3741
|
+
this.bumpRevision(Array.from(touched));
|
|
3742
|
+
}
|
|
3743
|
+
// 5. Events observe the already-published metadata and revision.
|
|
3744
|
+
for (const inode of deletedInodes) {
|
|
3745
|
+
this.events.emit(inode.isDir ? 'unlinkDir' : 'unlink', inode.path);
|
|
3746
|
+
}
|
|
3747
|
+
for (const entry of plan.inodes) {
|
|
3748
|
+
this.events.emit(entry.isDir ? 'addDir' : replacedPaths.has(entry.path) ? 'change' : 'add', entry.path);
|
|
3749
|
+
}
|
|
3750
|
+
this.recordDuration(this._postCommitDuration, performance.now() - postCommitStartedAt);
|
|
3751
|
+
return { inodes: inodeCount, chunks: chunkCount };
|
|
3752
|
+
}
|
|
3753
|
+
/**
|
|
3754
|
+
* Every inode at or under each root, deepest first.
|
|
3755
|
+
*
|
|
3756
|
+
* The children index answers "what is under this prefix?" in the size of
|
|
3757
|
+
* the subtree. The scan it replaces answered it in the size of the whole
|
|
3758
|
+
* filesystem, and every mutation resolved its deletions twice — once to
|
|
3759
|
+
* preflight the plan, once to commit it — so removing a tree of N entries
|
|
3760
|
+
* one path at a time cost N(N+1) comparisons: ~4.8 × 10^8 for a
|
|
3761
|
+
* 19,429-file tree, on the object's only thread.
|
|
3762
|
+
*/
|
|
3763
|
+
collectSubtreeInodes(roots) {
|
|
3764
|
+
if (roots.length === 0)
|
|
3765
|
+
return [];
|
|
3766
|
+
const collected = [];
|
|
3767
|
+
const visited = new Set();
|
|
3768
|
+
const frontier = [];
|
|
3769
|
+
for (const root of roots) {
|
|
3770
|
+
frontier.push(root);
|
|
3771
|
+
while (frontier.length > 0) {
|
|
3772
|
+
const path = frontier.pop();
|
|
3773
|
+
if (visited.has(path))
|
|
3774
|
+
continue;
|
|
3775
|
+
visited.add(path);
|
|
3776
|
+
const inode = this.inodes.get(path);
|
|
3777
|
+
if (inode)
|
|
3778
|
+
collected.push(inode);
|
|
3779
|
+
const children = this.children.get(path);
|
|
3780
|
+
if (children)
|
|
3781
|
+
for (const child of children)
|
|
3782
|
+
frontier.push(child);
|
|
3783
|
+
}
|
|
3784
|
+
}
|
|
3785
|
+
// A child's path is always longer than its parent's, so length order
|
|
3786
|
+
// removes every descendant before the directory holding it. Ties break on
|
|
3787
|
+
// the path itself: nothing a delete publishes should depend on traversal
|
|
3788
|
+
// or insertion order.
|
|
3789
|
+
return collected.sort((a, b) => (b.path.length - a.path.length || (a.path < b.path ? -1 : a.path > b.path ? 1 : 0)));
|
|
3790
|
+
}
|
|
3791
|
+
/**
|
|
3792
|
+
* Bulk mkdir: create all directories in a single transactionSync.
|
|
3793
|
+
* Pre-creates the full directory tree before file writes to avoid
|
|
3794
|
+
* per-file mkdir overhead.
|
|
3795
|
+
*/
|
|
3796
|
+
mkdirBatch(paths, cred) {
|
|
3797
|
+
this.assertMutationsAllowed(paths);
|
|
3798
|
+
const mtime = Date.now();
|
|
3799
|
+
const toCreate = [];
|
|
3800
|
+
const seen = new Set();
|
|
3801
|
+
for (const path of paths) {
|
|
3802
|
+
const parts = path.split('/').filter(Boolean);
|
|
3803
|
+
let current = '';
|
|
3804
|
+
for (const part of parts) {
|
|
3805
|
+
current = current ? current + '/' + part : part;
|
|
3806
|
+
if (!seen.has(current) && !this.exists(current, cred)) {
|
|
3807
|
+
seen.add(current);
|
|
3808
|
+
toCreate.push({
|
|
3809
|
+
path: current,
|
|
3810
|
+
parentPath: this.parentPath(current),
|
|
3811
|
+
isDir: true,
|
|
3812
|
+
size: 0,
|
|
3813
|
+
mtime,
|
|
3814
|
+
mode: 0o777,
|
|
3815
|
+
uid: cred.uid,
|
|
3816
|
+
gid: cred.gid,
|
|
3817
|
+
chunkCount: 0,
|
|
3818
|
+
});
|
|
3819
|
+
}
|
|
3820
|
+
}
|
|
3821
|
+
}
|
|
3822
|
+
if (toCreate.length === 0)
|
|
3823
|
+
return 0;
|
|
3824
|
+
this.writeBatch({ inodes: toCreate, chunks: [] }, cred);
|
|
3825
|
+
return toCreate.length;
|
|
3826
|
+
}
|
|
3827
|
+
// ── Stats ─────────────────────────────────────────────────────────────
|
|
3828
|
+
/**
|
|
3829
|
+
* Debug-only: recompute counters from scratch and return any drift
|
|
3830
|
+
* against the running counters. Returns null if consistent. Used by
|
|
3831
|
+
* the B3 runtime test; production paths should never call this
|
|
3832
|
+
* (the whole point of B3 is avoiding the O(N) walk).
|
|
3833
|
+
*/
|
|
3834
|
+
_verifyCounters() {
|
|
3835
|
+
let f = 0, d = 0, b = 0;
|
|
3836
|
+
for (const inode of this.inodes.values()) {
|
|
3837
|
+
if (inode.isDir)
|
|
3838
|
+
d++;
|
|
3839
|
+
else {
|
|
3840
|
+
f++;
|
|
3841
|
+
b += inode.size;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
if (f === this._totalFiles && d === this._totalDirs && b === this._usedBytes)
|
|
3845
|
+
return null;
|
|
3846
|
+
return {
|
|
3847
|
+
expected: { files: f, dirs: d, bytes: b },
|
|
3848
|
+
actual: { files: this._totalFiles, dirs: this._totalDirs, bytes: this._usedBytes },
|
|
3849
|
+
};
|
|
3850
|
+
}
|
|
3851
|
+
getStats() {
|
|
3852
|
+
// B3: O(1) — read the running counters. Previously three passes
|
|
3853
|
+
// over this.inodes (two filter + one for-of); at 50K inodes that
|
|
3854
|
+
// was 150K iterations per poll, every 5 s, serialising on the
|
|
3855
|
+
// input gate alongside shell keystrokes (AUDIT M10 / M-S8).
|
|
3856
|
+
const totalFiles = this._totalFiles;
|
|
3857
|
+
const totalDirs = this._totalDirs;
|
|
3858
|
+
const usedBytes = this._usedBytes;
|
|
3859
|
+
const totalAccesses = this._cacheHits + this._cacheMisses;
|
|
3860
|
+
const hitRate = totalAccesses > 0 ? (this._cacheHits / totalAccesses * 100) : 0;
|
|
3861
|
+
const now = performance.now();
|
|
3862
|
+
const activeTransactionDuration = this._activeTransaction === null
|
|
3863
|
+
? 0
|
|
3864
|
+
: now - this._activeTransaction.startedAt;
|
|
3865
|
+
let activeDecodeDrainDuration = 0;
|
|
3866
|
+
for (const startedAt of this._decodeDrainStarts.values()) {
|
|
3867
|
+
activeDecodeDrainDuration += now - startedAt;
|
|
3868
|
+
}
|
|
3869
|
+
let activeCreditWaitDuration = 0;
|
|
3870
|
+
for (const startedAt of this._creditWaitStarts.values()) {
|
|
3871
|
+
activeCreditWaitDuration += now - startedAt;
|
|
3872
|
+
}
|
|
3873
|
+
const activeMetrics = this._activeTransaction?.plan.metrics ?? null;
|
|
3874
|
+
const creditStats = this.writeStreamCredits.stats;
|
|
3875
|
+
return {
|
|
3876
|
+
// Legacy compat
|
|
3877
|
+
files: totalFiles,
|
|
3878
|
+
directories: totalDirs,
|
|
3879
|
+
usedBytes,
|
|
3880
|
+
capacityBytes: 10 * 1024 * 1024 * 1024, // 10 GB
|
|
3881
|
+
backend: 'DO SQLite (demand-paged VFS)',
|
|
3882
|
+
// Cache stats. maxEntries / maxBytes are now W5-runtime-mutable —
|
|
3883
|
+
// shrinkForInstall() drops them, restoreAfterInstall() restores.
|
|
3884
|
+
// lruShrunk is the at-a-glance signal for /api/_diag/memory.
|
|
3885
|
+
cache: {
|
|
3886
|
+
entries: this.cache.size,
|
|
3887
|
+
maxEntries: this._lruMaxEntries,
|
|
3888
|
+
chunkSize: CHUNK_SIZE,
|
|
3889
|
+
hotBytes: this._cacheBytes,
|
|
3890
|
+
maxBytes: this._lruMaxEntries * CHUNK_SIZE,
|
|
3891
|
+
hits: this._cacheHits,
|
|
3892
|
+
misses: this._cacheMisses,
|
|
3893
|
+
hitRate: Math.round(hitRate * 100) / 100,
|
|
3894
|
+
evictions: this._evictions,
|
|
3895
|
+
lruShrunk: this._lruMaxEntries < LRU_MAX_ENTRIES,
|
|
3896
|
+
},
|
|
3897
|
+
// SQL I/O stats
|
|
3898
|
+
sql: {
|
|
3899
|
+
reads: this._sqlReads,
|
|
3900
|
+
writes: this._sqlWrites,
|
|
3901
|
+
batchWrites: this._batchWrites,
|
|
3902
|
+
batchWriteRows: this._batchWriteRows,
|
|
3903
|
+
// Legacy names alias the same credited logical payload counter. The
|
|
3904
|
+
// 8 MiB pool includes both a decoded chunk record and staged buckets.
|
|
3905
|
+
writeStreamSpoolBytes: creditStats.current,
|
|
3906
|
+
retainedWriteBytes: {
|
|
3907
|
+
current: this.currentRetainedWriteBytes(),
|
|
3908
|
+
peak: creditStats.peak,
|
|
3909
|
+
},
|
|
3910
|
+
decoderRetainedBytes: {
|
|
3911
|
+
current: creditStats.current,
|
|
3912
|
+
peak: creditStats.peak,
|
|
3913
|
+
},
|
|
3914
|
+
creditRetainedBytes: {
|
|
3915
|
+
current: creditStats.current,
|
|
3916
|
+
peak: creditStats.peak,
|
|
3917
|
+
limit: MAX_GLOBAL_WRITE_STREAM_CREDIT_BYTES,
|
|
3918
|
+
queued: creditStats.queued,
|
|
3919
|
+
},
|
|
3920
|
+
stagedBytes: {
|
|
3921
|
+
current: this._stagedStreamBytes,
|
|
3922
|
+
peak: this._peakStagedStreamBytes,
|
|
3923
|
+
},
|
|
3924
|
+
gcBytes: { current: 0, peak: 0 },
|
|
3925
|
+
phases: {
|
|
3926
|
+
decodeDrainWaitMs: durationSnapshot(this._decodeDrainDuration, activeDecodeDrainDuration),
|
|
3927
|
+
creditWaitMs: durationSnapshot(this._creditWaitDuration, activeCreditWaitDuration),
|
|
3928
|
+
// count is the number of content-maintenance runs.
|
|
3929
|
+
maintenanceMs: durationSnapshot(this._maintenanceDuration, 0),
|
|
3930
|
+
},
|
|
3931
|
+
transactions: {
|
|
3932
|
+
limits: {
|
|
3933
|
+
blobBytes: MAX_TX_BLOB_BYTES,
|
|
3934
|
+
logicalRows: MAX_TX_LOGICAL_ROWS,
|
|
3935
|
+
sqlExecs: MAX_TX_SQL_EXECS,
|
|
3936
|
+
},
|
|
3937
|
+
active: this._activeTransaction !== null,
|
|
3938
|
+
durationMs: {
|
|
3939
|
+
...durationSnapshot(this._transactionDuration, activeTransactionDuration),
|
|
3940
|
+
p95: recentPercentile(this._transactionDurationSamples, this._transactionDurationSampleCount, 0.95),
|
|
3941
|
+
},
|
|
3942
|
+
postCommitDurationMs: durationSnapshot(this._postCommitDuration, 0),
|
|
3943
|
+
blobBytes: {
|
|
3944
|
+
current: activeMetrics?.blobBytes ?? 0,
|
|
3945
|
+
last: this._lastTransaction?.metrics.blobBytes ?? 0,
|
|
3946
|
+
peak: this._transactionPeakBlobBytes,
|
|
3947
|
+
},
|
|
3948
|
+
logicalRows: {
|
|
3949
|
+
current: activeMetrics?.logicalRows ?? 0,
|
|
3950
|
+
last: this._lastTransaction?.metrics.logicalRows ?? 0,
|
|
3951
|
+
peak: this._transactionPeakLogicalRows,
|
|
3952
|
+
},
|
|
3953
|
+
sqlExecs: {
|
|
3954
|
+
current: activeMetrics?.sqlExecs ?? 0,
|
|
3955
|
+
last: this._lastTransaction?.metrics.sqlExecs ?? 0,
|
|
3956
|
+
peak: this._transactionPeakSqlExecs,
|
|
3957
|
+
},
|
|
3958
|
+
affectedPaths: {
|
|
3959
|
+
current: activeMetrics?.affectedPaths ?? 0,
|
|
3960
|
+
last: this._lastTransaction?.metrics.affectedPaths ?? 0,
|
|
3961
|
+
peak: this._transactionPeakAffectedPaths,
|
|
3962
|
+
},
|
|
3963
|
+
boundedPeak: {
|
|
3964
|
+
blobBytes: this._boundedTransactionPeakBlobBytes,
|
|
3965
|
+
logicalRows: this._boundedTransactionPeakLogicalRows,
|
|
3966
|
+
sqlExecs: this._boundedTransactionPeakSqlExecs,
|
|
3967
|
+
},
|
|
3968
|
+
last: this._lastTransaction === null
|
|
3969
|
+
? null
|
|
3970
|
+
: {
|
|
3971
|
+
...this._lastTransaction.metrics,
|
|
3972
|
+
...this._lastTransaction.execution,
|
|
3973
|
+
},
|
|
3974
|
+
overLimitFiles: {
|
|
3975
|
+
count: this._overLimitFileCount,
|
|
3976
|
+
last: this._lastOverLimitFile,
|
|
3977
|
+
},
|
|
3978
|
+
},
|
|
3979
|
+
},
|
|
3980
|
+
// Event stats
|
|
3981
|
+
events: this.events.stats,
|
|
3982
|
+
// INode stats
|
|
3983
|
+
inodes: {
|
|
3984
|
+
total: this.inodes.size,
|
|
3985
|
+
files: totalFiles,
|
|
3986
|
+
directories: totalDirs,
|
|
3987
|
+
memoryEstimate: this.inodes.size * 200, // ~200 bytes per entry
|
|
3988
|
+
},
|
|
3989
|
+
};
|
|
3990
|
+
}
|
|
3991
|
+
}
|
|
3992
|
+
function inodeKind(inode) {
|
|
3993
|
+
const kind = inode.kind ?? (inode.isDir ? 'directory' : 'file');
|
|
3994
|
+
if (kind !== 'file' && kind !== 'directory' && kind !== 'symlink') {
|
|
3995
|
+
throw new Error(`EINVAL: invalid inode kind ${String(kind)}`);
|
|
3996
|
+
}
|
|
3997
|
+
return kind;
|
|
3998
|
+
}
|
|
3999
|
+
function inodeKindCode(kind) {
|
|
4000
|
+
if (kind === 'file')
|
|
4001
|
+
return INODE_KIND_FILE;
|
|
4002
|
+
if (kind === 'directory')
|
|
4003
|
+
return INODE_KIND_DIRECTORY;
|
|
4004
|
+
if (kind === 'symlink')
|
|
4005
|
+
return INODE_KIND_SYMLINK;
|
|
4006
|
+
throw new Error(`EINVAL: invalid inode kind ${String(kind)}`);
|
|
4007
|
+
}
|
|
4008
|
+
function inodeKindFromCode(code) {
|
|
4009
|
+
if (code === INODE_KIND_FILE)
|
|
4010
|
+
return 'file';
|
|
4011
|
+
if (code === INODE_KIND_DIRECTORY)
|
|
4012
|
+
return 'directory';
|
|
4013
|
+
if (code === INODE_KIND_SYMLINK)
|
|
4014
|
+
return 'symlink';
|
|
4015
|
+
throw new Error(`EIO: invalid durable inode kind ${code}`);
|
|
4016
|
+
}
|
|
4017
|
+
/** POSIX S_IFMT filetype bits for a stored st_mode (S_IFREG/S_IFDIR/S_IFLNK). */
|
|
4018
|
+
function inodeTypeBits(kind) {
|
|
4019
|
+
if (kind === 'file')
|
|
4020
|
+
return 0o100000;
|
|
4021
|
+
if (kind === 'directory')
|
|
4022
|
+
return 0o040000;
|
|
4023
|
+
return 0o120000;
|
|
4024
|
+
}
|
|
4025
|
+
function batchMutationPaths(payload) {
|
|
4026
|
+
const paths = new Set(payload.deletePaths ?? []);
|
|
4027
|
+
for (const inode of payload.inodes)
|
|
4028
|
+
paths.add(inode.path);
|
|
4029
|
+
for (const chunk of payload.chunks)
|
|
4030
|
+
paths.add(chunk.path);
|
|
4031
|
+
return paths;
|
|
4032
|
+
}
|
|
4033
|
+
function pathsOverlap(left, right) {
|
|
4034
|
+
return left === ''
|
|
4035
|
+
|| right === ''
|
|
4036
|
+
|| left === right
|
|
4037
|
+
|| left.startsWith(`${right}/`)
|
|
4038
|
+
|| right.startsWith(`${left}/`);
|
|
4039
|
+
}
|
|
4040
|
+
function vfsError(code, message) {
|
|
4041
|
+
return Object.assign(new Error(`${code}: ${message}`), { code });
|
|
4042
|
+
}
|
|
4043
|
+
/**
|
|
4044
|
+
* Seekable form of the durable reference probe
|
|
4045
|
+
* `NOT EXISTS (... WHERE inodes.kind != 1 AND COALESCE(inodes.content_id,
|
|
4046
|
+
* inodes.path) = <ref>)`. SQLite cannot seek an expression index from a
|
|
4047
|
+
* correlated subquery (the probe degrades to a per-outer-row SCAN of inodes,
|
|
4048
|
+
* making the orphan scan O(chunks × inodes)), so the generated-content and
|
|
4049
|
+
* legacy path-keyed cases are split into two probes that seek plain-column
|
|
4050
|
+
* indexes: idx_inodes_content and the path primary key. `ref` is a column
|
|
4051
|
+
* reference or a `?` placeholder; with `?` the value must be bound twice.
|
|
4052
|
+
*/
|
|
4053
|
+
function sqlNoInodeContentReference(ref) {
|
|
4054
|
+
return `NOT EXISTS (
|
|
4055
|
+
SELECT 1 FROM inodes
|
|
4056
|
+
WHERE inodes.kind != 1 AND inodes.content_id = ${ref}
|
|
4057
|
+
)
|
|
4058
|
+
AND NOT EXISTS (
|
|
4059
|
+
SELECT 1 FROM inodes
|
|
4060
|
+
WHERE inodes.kind != 1 AND inodes.content_id IS NULL AND inodes.path = ${ref}
|
|
4061
|
+
)`;
|
|
4062
|
+
}
|
|
4063
|
+
function emptyDurationSummary() {
|
|
4064
|
+
return { count: 0, totalMs: 0, lastMs: 0, maxMs: 0 };
|
|
4065
|
+
}
|
|
4066
|
+
function durationSnapshot(summary, current) {
|
|
4067
|
+
return {
|
|
4068
|
+
current,
|
|
4069
|
+
count: summary.count,
|
|
4070
|
+
total: summary.totalMs,
|
|
4071
|
+
last: summary.lastMs,
|
|
4072
|
+
max: summary.maxMs,
|
|
4073
|
+
};
|
|
4074
|
+
}
|
|
4075
|
+
function recentPercentile(samples, count, percentile) {
|
|
4076
|
+
if (count === 0)
|
|
4077
|
+
return 0;
|
|
4078
|
+
const sorted = Array.from(samples.subarray(0, count)).sort((a, b) => a - b);
|
|
4079
|
+
const index = Math.min(sorted.length - 1, Math.ceil(sorted.length * percentile) - 1);
|
|
4080
|
+
return sorted[index] ?? 0;
|
|
4081
|
+
}
|
|
4082
|
+
function clampNonNegativeInt(value) {
|
|
4083
|
+
return Number.isFinite(value) ? Math.max(0, Math.trunc(value)) : 0;
|
|
4084
|
+
}
|
|
4085
|
+
// ── SqliteVFSProvider (MountProvider for Nimbus Kernel VFS) ────────────────────
|
|
4086
|
+
export class SqliteVFSProvider {
|
|
4087
|
+
raw;
|
|
4088
|
+
vfs;
|
|
4089
|
+
prefix;
|
|
4090
|
+
constructor(vfs, prefix, cred = CRED_KERNEL) {
|
|
4091
|
+
this.raw = vfs;
|
|
4092
|
+
this.vfs = vfs.as(cred);
|
|
4093
|
+
this.prefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
4094
|
+
}
|
|
4095
|
+
as(cred) { return new SqliteVFSProvider(this.raw, this.prefix, cred); }
|
|
4096
|
+
resolve(sub) {
|
|
4097
|
+
const c = sub.replace(/^\/+/, '').replace(/\/+$/, '');
|
|
4098
|
+
return c ? this.prefix + '/' + c : this.prefix;
|
|
4099
|
+
}
|
|
4100
|
+
readFile(sub) { return this.vfs.readFile(this.resolve(sub)); }
|
|
4101
|
+
readFileString(sub) { return this.vfs.readFileString(this.resolve(sub)); }
|
|
4102
|
+
readRange(sub, offset, length) {
|
|
4103
|
+
return this.vfs.readRange(this.resolve(sub), offset, length);
|
|
4104
|
+
}
|
|
4105
|
+
writeFile(sub, content) {
|
|
4106
|
+
const fp = this.resolve(sub);
|
|
4107
|
+
const pp = fp.includes('/') ? fp.substring(0, fp.lastIndexOf('/')) : '';
|
|
4108
|
+
if (pp && !this.vfs.exists(pp))
|
|
4109
|
+
this.vfs.mkdir(pp, { recursive: true });
|
|
4110
|
+
this.vfs.writeFile(fp, content);
|
|
4111
|
+
}
|
|
4112
|
+
writeRange(sub, offset, bytes) {
|
|
4113
|
+
const fp = this.resolve(sub);
|
|
4114
|
+
const pp = fp.includes('/') ? fp.substring(0, fp.lastIndexOf('/')) : '';
|
|
4115
|
+
if (pp && !this.vfs.exists(pp))
|
|
4116
|
+
this.vfs.mkdir(pp, { recursive: true });
|
|
4117
|
+
this.vfs.writeRange(fp, offset, bytes);
|
|
4118
|
+
}
|
|
4119
|
+
truncate(sub, size) { this.vfs.truncate(this.resolve(sub), size); }
|
|
4120
|
+
exists(sub) { return this.vfs.exists(this.resolve(sub)); }
|
|
4121
|
+
access(sub, mode) { this.vfs.access(this.resolve(sub), mode); }
|
|
4122
|
+
stat(sub) { return this.vfs.stat(this.resolve(sub)); }
|
|
4123
|
+
readdir(sub) { return this.vfs.readdir(this.resolve(sub)); }
|
|
4124
|
+
unlink(sub) { this.vfs.unlink(this.resolve(sub)); }
|
|
4125
|
+
mkdir(sub, opts) {
|
|
4126
|
+
this.vfs.mkdir(this.resolve(sub), opts);
|
|
4127
|
+
}
|
|
4128
|
+
rmdir(sub) { this.vfs.rmdir(this.resolve(sub)); }
|
|
4129
|
+
rename(o, n) { this.vfs.rename(this.resolve(o), this.resolve(n)); }
|
|
4130
|
+
copyFile(s, d) { this.vfs.copyFile(this.resolve(s), this.resolve(d)); }
|
|
4131
|
+
chmod(sub, mode) { this.vfs.chmod(this.resolve(sub), mode); }
|
|
4132
|
+
chown(sub, uid, gid) {
|
|
4133
|
+
this.vfs.chown(this.resolve(sub), uid, gid);
|
|
4134
|
+
}
|
|
4135
|
+
}
|