@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.
Files changed (1056) hide show
  1. package/dist/_shared/ai-egress.d.ts +73 -0
  2. package/dist/_shared/ai-egress.d.ts.map +1 -0
  3. package/dist/_shared/ai-egress.js +88 -0
  4. package/dist/_shared/byte-stream.d.ts +59 -0
  5. package/dist/_shared/byte-stream.d.ts.map +1 -0
  6. package/dist/_shared/byte-stream.js +77 -0
  7. package/dist/_shared/bytes.d.ts +18 -0
  8. package/dist/_shared/bytes.d.ts.map +1 -0
  9. package/dist/_shared/bytes.js +17 -0
  10. package/dist/_shared/cache-stats.d.ts +145 -0
  11. package/dist/_shared/cache-stats.d.ts.map +1 -0
  12. package/dist/_shared/cache-stats.js +161 -0
  13. package/dist/_shared/crypto.d.ts +17 -0
  14. package/dist/_shared/crypto.d.ts.map +1 -0
  15. package/dist/_shared/crypto.js +131 -0
  16. package/dist/_shared/esbuild-facet-shim.d.ts +21 -0
  17. package/dist/_shared/esbuild-facet-shim.d.ts.map +1 -0
  18. package/dist/_shared/esbuild-facet-shim.js +22 -0
  19. package/dist/_shared/exports-resolver.d.ts +74 -0
  20. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  21. package/dist/_shared/exports-resolver.js +284 -0
  22. package/dist/_shared/facet-vfs-cursor.d.ts +48 -0
  23. package/dist/_shared/facet-vfs-cursor.d.ts.map +1 -0
  24. package/dist/_shared/facet-vfs-cursor.js +51 -0
  25. package/dist/_shared/install-phase.d.ts +47 -0
  26. package/dist/_shared/install-phase.d.ts.map +1 -0
  27. package/dist/_shared/install-phase.js +1 -0
  28. package/dist/_shared/lru-map.d.ts +25 -0
  29. package/dist/_shared/lru-map.d.ts.map +1 -0
  30. package/dist/_shared/lru-map.js +57 -0
  31. package/dist/_shared/real-node-imports.d.ts +43 -0
  32. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  33. package/dist/_shared/real-node-imports.js +52 -0
  34. package/dist/_shared/retry.d.ts +92 -0
  35. package/dist/_shared/retry.d.ts.map +1 -0
  36. package/dist/_shared/retry.js +155 -0
  37. package/dist/_shared/rpc-dispose.d.ts +4 -0
  38. package/dist/_shared/rpc-dispose.d.ts.map +1 -0
  39. package/dist/_shared/rpc-dispose.js +30 -0
  40. package/dist/_shared/terminal.d.ts +2 -0
  41. package/dist/_shared/terminal.d.ts.map +1 -0
  42. package/dist/_shared/terminal.js +3 -0
  43. package/dist/_shared/typescript-specifiers.d.ts +58 -0
  44. package/dist/_shared/typescript-specifiers.d.ts.map +1 -0
  45. package/dist/_shared/typescript-specifiers.js +84 -0
  46. package/dist/_shared/untrusted-request.d.ts +34 -0
  47. package/dist/_shared/untrusted-request.d.ts.map +1 -0
  48. package/dist/_shared/untrusted-request.js +74 -0
  49. package/dist/_shared/vfs-write-ledger.d.ts +3 -0
  50. package/dist/_shared/vfs-write-ledger.d.ts.map +1 -0
  51. package/dist/_shared/vfs-write-ledger.js +439 -0
  52. package/dist/_shared/w7-frame.d.ts +79 -0
  53. package/dist/_shared/w7-frame.d.ts.map +1 -0
  54. package/dist/_shared/w7-frame.js +839 -0
  55. package/dist/_shared/weighted-credit-pool.d.ts +61 -0
  56. package/dist/_shared/weighted-credit-pool.d.ts.map +1 -0
  57. package/dist/_shared/weighted-credit-pool.js +196 -0
  58. package/dist/constants.d.ts +64 -0
  59. package/dist/constants.d.ts.map +1 -0
  60. package/dist/constants.js +264 -0
  61. package/dist/index.d.ts +16 -0
  62. package/dist/index.d.ts.map +1 -0
  63. package/dist/index.js +13 -0
  64. package/dist/observability/diag-counters.d.ts +236 -0
  65. package/dist/observability/diag-counters.d.ts.map +1 -0
  66. package/dist/observability/diag-counters.js +195 -0
  67. package/dist/observability/heap-estimate.d.ts +225 -0
  68. package/dist/observability/heap-estimate.d.ts.map +1 -0
  69. package/dist/observability/heap-estimate.js +185 -0
  70. package/dist/observability/heavy-alloc-coord.d.ts +93 -0
  71. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  72. package/dist/observability/heavy-alloc-coord.js +181 -0
  73. package/dist/observability/oom-classify.d.ts +138 -0
  74. package/dist/observability/oom-classify.d.ts.map +1 -0
  75. package/dist/observability/oom-classify.js +261 -0
  76. package/dist/observability/oom-discriminator.d.ts +176 -0
  77. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  78. package/dist/observability/oom-discriminator.js +272 -0
  79. package/dist/runtime/assets-loader.d.ts +71 -0
  80. package/dist/runtime/assets-loader.d.ts.map +1 -0
  81. package/dist/runtime/assets-loader.js +139 -0
  82. package/dist/runtime/barrel-detect.d.ts +46 -0
  83. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  84. package/dist/runtime/barrel-detect.js +73 -0
  85. package/dist/runtime/bash/preamble.d.ts +2 -0
  86. package/dist/runtime/bash/preamble.d.ts.map +1 -0
  87. package/dist/runtime/bash/preamble.js +1849 -0
  88. package/dist/runtime/bash/types.d.ts +433 -0
  89. package/dist/runtime/bash/types.d.ts.map +1 -0
  90. package/dist/runtime/bash/types.js +1 -0
  91. package/dist/runtime/bundle-profile.d.ts +14 -0
  92. package/dist/runtime/bundle-profile.d.ts.map +1 -0
  93. package/dist/runtime/bundle-profile.js +20 -0
  94. package/dist/runtime/cirrus-vite-config-rewriter.d.ts +2 -0
  95. package/dist/runtime/cirrus-vite-config-rewriter.d.ts.map +1 -0
  96. package/dist/runtime/cirrus-vite-config-rewriter.js +28 -0
  97. package/dist/runtime/cli-flags.d.ts +2 -0
  98. package/dist/runtime/cli-flags.d.ts.map +1 -0
  99. package/dist/runtime/cli-flags.js +11 -0
  100. package/dist/runtime/comment-strip.d.ts +37 -0
  101. package/dist/runtime/comment-strip.d.ts.map +1 -0
  102. package/dist/runtime/comment-strip.js +74 -0
  103. package/dist/runtime/cpython-preamble.d.ts +32 -0
  104. package/dist/runtime/cpython-preamble.d.ts.map +1 -0
  105. package/dist/runtime/cpython-preamble.js +296 -0
  106. package/dist/runtime/esbuild-service.d.ts +242 -0
  107. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  108. package/dist/runtime/esbuild-service.js +1452 -0
  109. package/dist/runtime/framework-detect.d.ts +70 -0
  110. package/dist/runtime/framework-detect.d.ts.map +1 -0
  111. package/dist/runtime/framework-detect.js +177 -0
  112. package/dist/runtime/import-meta-transform.d.ts +3 -0
  113. package/dist/runtime/import-meta-transform.d.ts.map +1 -0
  114. package/dist/runtime/import-meta-transform.js +15 -0
  115. package/dist/runtime/javascript-ast.d.ts +13 -0
  116. package/dist/runtime/javascript-ast.d.ts.map +1 -0
  117. package/dist/runtime/javascript-ast.js +57 -0
  118. package/dist/runtime/long-running-handle.d.ts +119 -0
  119. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  120. package/dist/runtime/long-running-handle.js +169 -0
  121. package/dist/runtime/module-source-rewriter.d.ts +11 -0
  122. package/dist/runtime/module-source-rewriter.d.ts.map +1 -0
  123. package/dist/runtime/module-source-rewriter.js +61 -0
  124. package/dist/runtime/opentui-wasm-backend.d.ts +160 -0
  125. package/dist/runtime/opentui-wasm-backend.d.ts.map +1 -0
  126. package/dist/runtime/opentui-wasm-backend.js +416 -0
  127. package/dist/runtime/os-contracts.d.ts +436 -0
  128. package/dist/runtime/os-contracts.d.ts.map +1 -0
  129. package/dist/runtime/os-contracts.js +101 -0
  130. package/dist/runtime/port-registry.d.ts +100 -0
  131. package/dist/runtime/port-registry.d.ts.map +1 -0
  132. package/dist/runtime/port-registry.js +265 -0
  133. package/dist/runtime/process-input-routing.d.ts +33 -0
  134. package/dist/runtime/process-input-routing.d.ts.map +1 -0
  135. package/dist/runtime/process-input-routing.js +59 -0
  136. package/dist/runtime/process-input.d.ts +39 -0
  137. package/dist/runtime/process-input.d.ts.map +1 -0
  138. package/dist/runtime/process-input.js +117 -0
  139. package/dist/runtime/process-io-protocol.d.ts +90 -0
  140. package/dist/runtime/process-io-protocol.d.ts.map +1 -0
  141. package/dist/runtime/process-io-protocol.js +81 -0
  142. package/dist/runtime/process-logs.d.ts +352 -0
  143. package/dist/runtime/process-logs.d.ts.map +1 -0
  144. package/dist/runtime/process-logs.js +750 -0
  145. package/dist/runtime/process-table.d.ts +101 -0
  146. package/dist/runtime/process-table.d.ts.map +1 -0
  147. package/dist/runtime/process-table.js +175 -0
  148. package/dist/runtime/project-detect.d.ts +21 -0
  149. package/dist/runtime/project-detect.d.ts.map +1 -0
  150. package/dist/runtime/project-detect.js +46 -0
  151. package/dist/runtime/python-server-adapter.d.ts +33 -0
  152. package/dist/runtime/python-server-adapter.d.ts.map +1 -0
  153. package/dist/runtime/python-server-adapter.js +132 -0
  154. package/dist/runtime/require-resolver.d.ts +44 -0
  155. package/dist/runtime/require-resolver.d.ts.map +1 -0
  156. package/dist/runtime/require-resolver.js +698 -0
  157. package/dist/runtime/router-basename.d.ts +36 -0
  158. package/dist/runtime/router-basename.d.ts.map +1 -0
  159. package/dist/runtime/router-basename.js +310 -0
  160. package/dist/runtime/ruby-green-threads.d.ts +136 -0
  161. package/dist/runtime/ruby-green-threads.d.ts.map +1 -0
  162. package/dist/runtime/ruby-green-threads.js +904 -0
  163. package/dist/runtime/ruby-socket-shim.d.ts +2 -0
  164. package/dist/runtime/ruby-socket-shim.d.ts.map +1 -0
  165. package/dist/runtime/ruby-socket-shim.js +548 -0
  166. package/dist/runtime/session-process-supervisor.d.ts +156 -0
  167. package/dist/runtime/session-process-supervisor.d.ts.map +1 -0
  168. package/dist/runtime/session-process-supervisor.js +238 -0
  169. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts +70 -0
  170. package/dist/runtime/sqlite-runtime-fs-bridge.d.ts.map +1 -0
  171. package/dist/runtime/sqlite-runtime-fs-bridge.js +425 -0
  172. package/dist/runtime/streams.d.ts +13 -0
  173. package/dist/runtime/streams.d.ts.map +1 -0
  174. package/dist/runtime/streams.js +546 -0
  175. package/dist/runtime/undici-shim.d.ts +62 -0
  176. package/dist/runtime/undici-shim.d.ts.map +1 -0
  177. package/dist/runtime/undici-shim.js +516 -0
  178. package/dist/runtime/vfs-manifest.d.ts +63 -0
  179. package/dist/runtime/vfs-manifest.d.ts.map +1 -0
  180. package/dist/runtime/vfs-manifest.js +141 -0
  181. package/dist/runtime/vfs-snapshot.d.ts +75 -0
  182. package/dist/runtime/vfs-snapshot.d.ts.map +1 -0
  183. package/dist/runtime/vfs-snapshot.js +220 -0
  184. package/dist/runtime/virtual-socket-kernel.d.ts +252 -0
  185. package/dist/runtime/virtual-socket-kernel.d.ts.map +1 -0
  186. package/dist/runtime/virtual-socket-kernel.js +1143 -0
  187. package/dist/runtime/vite-config-parser.d.ts +13 -0
  188. package/dist/runtime/vite-config-parser.d.ts.map +1 -0
  189. package/dist/runtime/vite-config-parser.js +258 -0
  190. package/dist/runtime/wasi/types.d.ts +347 -0
  191. package/dist/runtime/wasi/types.d.ts.map +1 -0
  192. package/dist/runtime/wasi/types.js +1 -0
  193. package/dist/runtime/wasi-instance.d.ts +215 -0
  194. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  195. package/dist/runtime/wasi-instance.generated.d.ts +14 -0
  196. package/dist/runtime/wasi-instance.generated.d.ts.map +1 -0
  197. package/dist/runtime/wasi-instance.generated.js +13 -0
  198. package/dist/runtime/wasi-instance.js +179 -0
  199. package/dist/runtime/wasi-threads.d.ts +169 -0
  200. package/dist/runtime/wasi-threads.d.ts.map +1 -0
  201. package/dist/runtime/wasi-threads.js +669 -0
  202. package/dist/runtime/wasm-memory.d.ts +84 -0
  203. package/dist/runtime/wasm-memory.d.ts.map +1 -0
  204. package/dist/runtime/wasm-memory.js +235 -0
  205. package/dist/shell/elevation-commands.d.ts +5 -0
  206. package/dist/shell/elevation-commands.d.ts.map +1 -0
  207. package/dist/shell/elevation-commands.js +109 -0
  208. package/dist/shell/exec-dispatch.d.ts +57 -0
  209. package/dist/shell/exec-dispatch.d.ts.map +1 -0
  210. package/dist/shell/exec-dispatch.js +181 -0
  211. package/dist/shell/features.d.ts +154 -0
  212. package/dist/shell/features.d.ts.map +1 -0
  213. package/dist/shell/features.js +792 -0
  214. package/dist/shell/shell-entrypoints.d.ts +55 -0
  215. package/dist/shell/shell-entrypoints.d.ts.map +1 -0
  216. package/dist/shell/shell-entrypoints.js +230 -0
  217. package/dist/shell/shell-invocation.d.ts +36 -0
  218. package/dist/shell/shell-invocation.d.ts.map +1 -0
  219. package/dist/shell/shell-invocation.js +148 -0
  220. package/dist/shell/unix-accounts.d.ts +27 -0
  221. package/dist/shell/unix-accounts.d.ts.map +1 -0
  222. package/dist/shell/unix-accounts.js +105 -0
  223. package/dist/shell/unix-commands.d.ts +15 -0
  224. package/dist/shell/unix-commands.d.ts.map +1 -0
  225. package/dist/shell/unix-commands.js +4702 -0
  226. package/dist/substrate/lifo/commands/archive/gunzip.d.ts +4 -0
  227. package/dist/substrate/lifo/commands/archive/gunzip.d.ts.map +1 -0
  228. package/dist/substrate/lifo/commands/archive/gunzip.js +58 -0
  229. package/dist/substrate/lifo/commands/archive/gzip.d.ts +4 -0
  230. package/dist/substrate/lifo/commands/archive/gzip.d.ts.map +1 -0
  231. package/dist/substrate/lifo/commands/archive/gzip.js +73 -0
  232. package/dist/substrate/lifo/commands/archive/tar.d.ts +4 -0
  233. package/dist/substrate/lifo/commands/archive/tar.d.ts.map +1 -0
  234. package/dist/substrate/lifo/commands/archive/tar.js +158 -0
  235. package/dist/substrate/lifo/commands/archive/unzip.d.ts +4 -0
  236. package/dist/substrate/lifo/commands/archive/unzip.d.ts.map +1 -0
  237. package/dist/substrate/lifo/commands/archive/unzip.js +114 -0
  238. package/dist/substrate/lifo/commands/archive/zip.d.ts +4 -0
  239. package/dist/substrate/lifo/commands/archive/zip.d.ts.map +1 -0
  240. package/dist/substrate/lifo/commands/archive/zip.js +58 -0
  241. package/dist/substrate/lifo/commands/fs/basename.d.ts +4 -0
  242. package/dist/substrate/lifo/commands/fs/basename.d.ts.map +1 -0
  243. package/dist/substrate/lifo/commands/fs/basename.js +12 -0
  244. package/dist/substrate/lifo/commands/fs/cat.d.ts +4 -0
  245. package/dist/substrate/lifo/commands/fs/cat.d.ts.map +1 -0
  246. package/dist/substrate/lifo/commands/fs/cat.js +52 -0
  247. package/dist/substrate/lifo/commands/fs/chmod.d.ts +25 -0
  248. package/dist/substrate/lifo/commands/fs/chmod.d.ts.map +1 -0
  249. package/dist/substrate/lifo/commands/fs/chmod.js +97 -0
  250. package/dist/substrate/lifo/commands/fs/chown.d.ts +4 -0
  251. package/dist/substrate/lifo/commands/fs/chown.d.ts.map +1 -0
  252. package/dist/substrate/lifo/commands/fs/chown.js +41 -0
  253. package/dist/substrate/lifo/commands/fs/cp.d.ts +4 -0
  254. package/dist/substrate/lifo/commands/fs/cp.d.ts.map +1 -0
  255. package/dist/substrate/lifo/commands/fs/cp.js +29 -0
  256. package/dist/substrate/lifo/commands/fs/df.d.ts +4 -0
  257. package/dist/substrate/lifo/commands/fs/df.d.ts.map +1 -0
  258. package/dist/substrate/lifo/commands/fs/df.js +52 -0
  259. package/dist/substrate/lifo/commands/fs/dirname.d.ts +4 -0
  260. package/dist/substrate/lifo/commands/fs/dirname.d.ts.map +1 -0
  261. package/dist/substrate/lifo/commands/fs/dirname.js +12 -0
  262. package/dist/substrate/lifo/commands/fs/du.d.ts +4 -0
  263. package/dist/substrate/lifo/commands/fs/du.d.ts.map +1 -0
  264. package/dist/substrate/lifo/commands/fs/du.js +104 -0
  265. package/dist/substrate/lifo/commands/fs/file.d.ts +4 -0
  266. package/dist/substrate/lifo/commands/fs/file.d.ts.map +1 -0
  267. package/dist/substrate/lifo/commands/fs/file.js +84 -0
  268. package/dist/substrate/lifo/commands/fs/find.d.ts +4 -0
  269. package/dist/substrate/lifo/commands/fs/find.d.ts.map +1 -0
  270. package/dist/substrate/lifo/commands/fs/find.js +72 -0
  271. package/dist/substrate/lifo/commands/fs/ln.d.ts +4 -0
  272. package/dist/substrate/lifo/commands/fs/ln.d.ts.map +1 -0
  273. package/dist/substrate/lifo/commands/fs/ln.js +15 -0
  274. package/dist/substrate/lifo/commands/fs/ls.d.ts +4 -0
  275. package/dist/substrate/lifo/commands/fs/ls.d.ts.map +1 -0
  276. package/dist/substrate/lifo/commands/fs/ls.js +146 -0
  277. package/dist/substrate/lifo/commands/fs/mkdir.d.ts +4 -0
  278. package/dist/substrate/lifo/commands/fs/mkdir.d.ts.map +1 -0
  279. package/dist/substrate/lifo/commands/fs/mkdir.js +31 -0
  280. package/dist/substrate/lifo/commands/fs/mktemp.d.ts +4 -0
  281. package/dist/substrate/lifo/commands/fs/mktemp.d.ts.map +1 -0
  282. package/dist/substrate/lifo/commands/fs/mktemp.js +48 -0
  283. package/dist/substrate/lifo/commands/fs/mv.d.ts +4 -0
  284. package/dist/substrate/lifo/commands/fs/mv.d.ts.map +1 -0
  285. package/dist/substrate/lifo/commands/fs/mv.js +113 -0
  286. package/dist/substrate/lifo/commands/fs/realpath.d.ts +4 -0
  287. package/dist/substrate/lifo/commands/fs/realpath.d.ts.map +1 -0
  288. package/dist/substrate/lifo/commands/fs/realpath.js +27 -0
  289. package/dist/substrate/lifo/commands/fs/rm.d.ts +4 -0
  290. package/dist/substrate/lifo/commands/fs/rm.d.ts.map +1 -0
  291. package/dist/substrate/lifo/commands/fs/rm.js +48 -0
  292. package/dist/substrate/lifo/commands/fs/rmdir.d.ts +4 -0
  293. package/dist/substrate/lifo/commands/fs/rmdir.d.ts.map +1 -0
  294. package/dist/substrate/lifo/commands/fs/rmdir.js +44 -0
  295. package/dist/substrate/lifo/commands/fs/stat.d.ts +4 -0
  296. package/dist/substrate/lifo/commands/fs/stat.d.ts.map +1 -0
  297. package/dist/substrate/lifo/commands/fs/stat.js +35 -0
  298. package/dist/substrate/lifo/commands/fs/touch.d.ts +4 -0
  299. package/dist/substrate/lifo/commands/fs/touch.d.ts.map +1 -0
  300. package/dist/substrate/lifo/commands/fs/touch.js +26 -0
  301. package/dist/substrate/lifo/commands/fs/tree.d.ts +4 -0
  302. package/dist/substrate/lifo/commands/fs/tree.d.ts.map +1 -0
  303. package/dist/substrate/lifo/commands/fs/tree.js +69 -0
  304. package/dist/substrate/lifo/commands/index.d.ts +3 -0
  305. package/dist/substrate/lifo/commands/index.d.ts.map +1 -0
  306. package/dist/substrate/lifo/commands/index.js +1 -0
  307. package/dist/substrate/lifo/commands/io/dd.d.ts +4 -0
  308. package/dist/substrate/lifo/commands/io/dd.d.ts.map +1 -0
  309. package/dist/substrate/lifo/commands/io/dd.js +256 -0
  310. package/dist/substrate/lifo/commands/io/printf.d.ts +4 -0
  311. package/dist/substrate/lifo/commands/io/printf.d.ts.map +1 -0
  312. package/dist/substrate/lifo/commands/io/printf.js +82 -0
  313. package/dist/substrate/lifo/commands/io/tee.d.ts +4 -0
  314. package/dist/substrate/lifo/commands/io/tee.d.ts.map +1 -0
  315. package/dist/substrate/lifo/commands/io/tee.js +31 -0
  316. package/dist/substrate/lifo/commands/io/xargs.d.ts +4 -0
  317. package/dist/substrate/lifo/commands/io/xargs.d.ts.map +1 -0
  318. package/dist/substrate/lifo/commands/io/xargs.js +44 -0
  319. package/dist/substrate/lifo/commands/io/yes.d.ts +4 -0
  320. package/dist/substrate/lifo/commands/io/yes.d.ts.map +1 -0
  321. package/dist/substrate/lifo/commands/io/yes.js +10 -0
  322. package/dist/substrate/lifo/commands/net/curl.d.ts +6 -0
  323. package/dist/substrate/lifo/commands/net/curl.d.ts.map +1 -0
  324. package/dist/substrate/lifo/commands/net/curl.js +617 -0
  325. package/dist/substrate/lifo/commands/net/dig.d.ts +4 -0
  326. package/dist/substrate/lifo/commands/net/dig.d.ts.map +1 -0
  327. package/dist/substrate/lifo/commands/net/dig.js +77 -0
  328. package/dist/substrate/lifo/commands/net/forward.d.ts +14 -0
  329. package/dist/substrate/lifo/commands/net/forward.d.ts.map +1 -0
  330. package/dist/substrate/lifo/commands/net/forward.js +80 -0
  331. package/dist/substrate/lifo/commands/net/host.d.ts +14 -0
  332. package/dist/substrate/lifo/commands/net/host.d.ts.map +1 -0
  333. package/dist/substrate/lifo/commands/net/host.js +160 -0
  334. package/dist/substrate/lifo/commands/net/ifconfig.d.ts +8 -0
  335. package/dist/substrate/lifo/commands/net/ifconfig.d.ts.map +1 -0
  336. package/dist/substrate/lifo/commands/net/ifconfig.js +120 -0
  337. package/dist/substrate/lifo/commands/net/ip.d.ts +14 -0
  338. package/dist/substrate/lifo/commands/net/ip.d.ts.map +1 -0
  339. package/dist/substrate/lifo/commands/net/ip.js +508 -0
  340. package/dist/substrate/lifo/commands/net/netstat.d.ts +8 -0
  341. package/dist/substrate/lifo/commands/net/netstat.d.ts.map +1 -0
  342. package/dist/substrate/lifo/commands/net/netstat.js +94 -0
  343. package/dist/substrate/lifo/commands/net/ping.d.ts +4 -0
  344. package/dist/substrate/lifo/commands/net/ping.d.ts.map +1 -0
  345. package/dist/substrate/lifo/commands/net/ping.js +64 -0
  346. package/dist/substrate/lifo/commands/net/ports.d.ts +8 -0
  347. package/dist/substrate/lifo/commands/net/ports.d.ts.map +1 -0
  348. package/dist/substrate/lifo/commands/net/ports.js +36 -0
  349. package/dist/substrate/lifo/commands/net/route.d.ts +8 -0
  350. package/dist/substrate/lifo/commands/net/route.d.ts.map +1 -0
  351. package/dist/substrate/lifo/commands/net/route.js +128 -0
  352. package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts +4 -0
  353. package/dist/substrate/lifo/commands/net/tunnel-v2.d.ts.map +1 -0
  354. package/dist/substrate/lifo/commands/net/tunnel-v2.js +141 -0
  355. package/dist/substrate/lifo/commands/net/tunnel.d.ts +6 -0
  356. package/dist/substrate/lifo/commands/net/tunnel.d.ts.map +1 -0
  357. package/dist/substrate/lifo/commands/net/tunnel.js +232 -0
  358. package/dist/substrate/lifo/commands/net/wget.d.ts +4 -0
  359. package/dist/substrate/lifo/commands/net/wget.d.ts.map +1 -0
  360. package/dist/substrate/lifo/commands/net/wget.js +78 -0
  361. package/dist/substrate/lifo/commands/registry.d.ts +15 -0
  362. package/dist/substrate/lifo/commands/registry.d.ts.map +1 -0
  363. package/dist/substrate/lifo/commands/registry.js +120 -0
  364. package/dist/substrate/lifo/commands/signal.d.ts +12 -0
  365. package/dist/substrate/lifo/commands/signal.d.ts.map +1 -0
  366. package/dist/substrate/lifo/commands/signal.js +47 -0
  367. package/dist/substrate/lifo/commands/system/bc.d.ts +4 -0
  368. package/dist/substrate/lifo/commands/system/bc.d.ts.map +1 -0
  369. package/dist/substrate/lifo/commands/system/bc.js +243 -0
  370. package/dist/substrate/lifo/commands/system/cal.d.ts +4 -0
  371. package/dist/substrate/lifo/commands/system/cal.d.ts.map +1 -0
  372. package/dist/substrate/lifo/commands/system/cal.js +81 -0
  373. package/dist/substrate/lifo/commands/system/date.d.ts +4 -0
  374. package/dist/substrate/lifo/commands/system/date.d.ts.map +1 -0
  375. package/dist/substrate/lifo/commands/system/date.js +79 -0
  376. package/dist/substrate/lifo/commands/system/env.d.ts +4 -0
  377. package/dist/substrate/lifo/commands/system/env.d.ts.map +1 -0
  378. package/dist/substrate/lifo/commands/system/env.js +7 -0
  379. package/dist/substrate/lifo/commands/system/fastfetch.d.ts +4 -0
  380. package/dist/substrate/lifo/commands/system/fastfetch.d.ts.map +1 -0
  381. package/dist/substrate/lifo/commands/system/fastfetch.js +385 -0
  382. package/dist/substrate/lifo/commands/system/free.d.ts +4 -0
  383. package/dist/substrate/lifo/commands/system/free.d.ts.map +1 -0
  384. package/dist/substrate/lifo/commands/system/free.js +36 -0
  385. package/dist/substrate/lifo/commands/system/help.d.ts +4 -0
  386. package/dist/substrate/lifo/commands/system/help.d.ts.map +1 -0
  387. package/dist/substrate/lifo/commands/system/help.js +46 -0
  388. package/dist/substrate/lifo/commands/system/hostname.d.ts +4 -0
  389. package/dist/substrate/lifo/commands/system/hostname.d.ts.map +1 -0
  390. package/dist/substrate/lifo/commands/system/hostname.js +6 -0
  391. package/dist/substrate/lifo/commands/system/kill.d.ts +4 -0
  392. package/dist/substrate/lifo/commands/system/kill.d.ts.map +1 -0
  393. package/dist/substrate/lifo/commands/system/kill.js +76 -0
  394. package/dist/substrate/lifo/commands/system/lifo.d.ts +41 -0
  395. package/dist/substrate/lifo/commands/system/lifo.d.ts.map +1 -0
  396. package/dist/substrate/lifo/commands/system/lifo.js +446 -0
  397. package/dist/substrate/lifo/commands/system/logout.d.ts +5 -0
  398. package/dist/substrate/lifo/commands/system/logout.d.ts.map +1 -0
  399. package/dist/substrate/lifo/commands/system/logout.js +15 -0
  400. package/dist/substrate/lifo/commands/system/man.d.ts +4 -0
  401. package/dist/substrate/lifo/commands/system/man.d.ts.map +1 -0
  402. package/dist/substrate/lifo/commands/system/man.js +144 -0
  403. package/dist/substrate/lifo/commands/system/node.d.ts +6 -0
  404. package/dist/substrate/lifo/commands/system/node.d.ts.map +1 -0
  405. package/dist/substrate/lifo/commands/system/node.js +1241 -0
  406. package/dist/substrate/lifo/commands/system/npm.d.ts +26 -0
  407. package/dist/substrate/lifo/commands/system/npm.d.ts.map +1 -0
  408. package/dist/substrate/lifo/commands/system/npm.js +934 -0
  409. package/dist/substrate/lifo/commands/system/pkg.d.ts +4 -0
  410. package/dist/substrate/lifo/commands/system/pkg.d.ts.map +1 -0
  411. package/dist/substrate/lifo/commands/system/pkg.js +97 -0
  412. package/dist/substrate/lifo/commands/system/ps.d.ts +5 -0
  413. package/dist/substrate/lifo/commands/system/ps.d.ts.map +1 -0
  414. package/dist/substrate/lifo/commands/system/ps.js +35 -0
  415. package/dist/substrate/lifo/commands/system/registry-schemas.d.ts +44 -0
  416. package/dist/substrate/lifo/commands/system/registry-schemas.d.ts.map +1 -0
  417. package/dist/substrate/lifo/commands/system/registry-schemas.js +28 -0
  418. package/dist/substrate/lifo/commands/system/sha256sum.d.ts +4 -0
  419. package/dist/substrate/lifo/commands/system/sha256sum.d.ts.map +1 -0
  420. package/dist/substrate/lifo/commands/system/sha256sum.js +86 -0
  421. package/dist/substrate/lifo/commands/system/sl.d.ts +4 -0
  422. package/dist/substrate/lifo/commands/system/sl.d.ts.map +1 -0
  423. package/dist/substrate/lifo/commands/system/sl.js +204 -0
  424. package/dist/substrate/lifo/commands/system/sleep.d.ts +4 -0
  425. package/dist/substrate/lifo/commands/system/sleep.d.ts.map +1 -0
  426. package/dist/substrate/lifo/commands/system/sleep.js +27 -0
  427. package/dist/substrate/lifo/commands/system/stty.d.ts +4 -0
  428. package/dist/substrate/lifo/commands/system/stty.d.ts.map +1 -0
  429. package/dist/substrate/lifo/commands/system/stty.js +61 -0
  430. package/dist/substrate/lifo/commands/system/systemctl.d.ts +4 -0
  431. package/dist/substrate/lifo/commands/system/systemctl.d.ts.map +1 -0
  432. package/dist/substrate/lifo/commands/system/systemctl.js +164 -0
  433. package/dist/substrate/lifo/commands/system/top.d.ts +4 -0
  434. package/dist/substrate/lifo/commands/system/top.d.ts.map +1 -0
  435. package/dist/substrate/lifo/commands/system/top.js +38 -0
  436. package/dist/substrate/lifo/commands/system/uname.d.ts +4 -0
  437. package/dist/substrate/lifo/commands/system/uname.d.ts.map +1 -0
  438. package/dist/substrate/lifo/commands/system/uname.js +55 -0
  439. package/dist/substrate/lifo/commands/system/uptime.d.ts +4 -0
  440. package/dist/substrate/lifo/commands/system/uptime.d.ts.map +1 -0
  441. package/dist/substrate/lifo/commands/system/uptime.js +16 -0
  442. package/dist/substrate/lifo/commands/system/watch.d.ts +4 -0
  443. package/dist/substrate/lifo/commands/system/watch.d.ts.map +1 -0
  444. package/dist/substrate/lifo/commands/system/watch.js +53 -0
  445. package/dist/substrate/lifo/commands/system/which.d.ts +4 -0
  446. package/dist/substrate/lifo/commands/system/which.d.ts.map +1 -0
  447. package/dist/substrate/lifo/commands/system/which.js +45 -0
  448. package/dist/substrate/lifo/commands/system/whoami.d.ts +4 -0
  449. package/dist/substrate/lifo/commands/system/whoami.d.ts.map +1 -0
  450. package/dist/substrate/lifo/commands/system/whoami.js +6 -0
  451. package/dist/substrate/lifo/commands/text/awk.d.ts +4 -0
  452. package/dist/substrate/lifo/commands/text/awk.d.ts.map +1 -0
  453. package/dist/substrate/lifo/commands/text/awk.js +200 -0
  454. package/dist/substrate/lifo/commands/text/base64.d.ts +4 -0
  455. package/dist/substrate/lifo/commands/text/base64.d.ts.map +1 -0
  456. package/dist/substrate/lifo/commands/text/base64.js +102 -0
  457. package/dist/substrate/lifo/commands/text/cut.d.ts +4 -0
  458. package/dist/substrate/lifo/commands/text/cut.d.ts.map +1 -0
  459. package/dist/substrate/lifo/commands/text/cut.js +91 -0
  460. package/dist/substrate/lifo/commands/text/diff.d.ts +4 -0
  461. package/dist/substrate/lifo/commands/text/diff.d.ts.map +1 -0
  462. package/dist/substrate/lifo/commands/text/diff.js +292 -0
  463. package/dist/substrate/lifo/commands/text/grep.d.ts +4 -0
  464. package/dist/substrate/lifo/commands/text/grep.d.ts.map +1 -0
  465. package/dist/substrate/lifo/commands/text/grep.js +189 -0
  466. package/dist/substrate/lifo/commands/text/head.d.ts +4 -0
  467. package/dist/substrate/lifo/commands/text/head.d.ts.map +1 -0
  468. package/dist/substrate/lifo/commands/text/head.js +111 -0
  469. package/dist/substrate/lifo/commands/text/less.d.ts +4 -0
  470. package/dist/substrate/lifo/commands/text/less.d.ts.map +1 -0
  471. package/dist/substrate/lifo/commands/text/less.js +335 -0
  472. package/dist/substrate/lifo/commands/text/nano.d.ts +4 -0
  473. package/dist/substrate/lifo/commands/text/nano.d.ts.map +1 -0
  474. package/dist/substrate/lifo/commands/text/nano.js +510 -0
  475. package/dist/substrate/lifo/commands/text/nl.d.ts +4 -0
  476. package/dist/substrate/lifo/commands/text/nl.d.ts.map +1 -0
  477. package/dist/substrate/lifo/commands/text/nl.js +64 -0
  478. package/dist/substrate/lifo/commands/text/rev.d.ts +4 -0
  479. package/dist/substrate/lifo/commands/text/rev.d.ts.map +1 -0
  480. package/dist/substrate/lifo/commands/text/rev.js +41 -0
  481. package/dist/substrate/lifo/commands/text/sed.d.ts +21 -0
  482. package/dist/substrate/lifo/commands/text/sed.d.ts.map +1 -0
  483. package/dist/substrate/lifo/commands/text/sed.js +248 -0
  484. package/dist/substrate/lifo/commands/text/seq.d.ts +4 -0
  485. package/dist/substrate/lifo/commands/text/seq.d.ts.map +1 -0
  486. package/dist/substrate/lifo/commands/text/seq.js +69 -0
  487. package/dist/substrate/lifo/commands/text/sort.d.ts +4 -0
  488. package/dist/substrate/lifo/commands/text/sort.d.ts.map +1 -0
  489. package/dist/substrate/lifo/commands/text/sort.js +90 -0
  490. package/dist/substrate/lifo/commands/text/strings.d.ts +4 -0
  491. package/dist/substrate/lifo/commands/text/strings.d.ts.map +1 -0
  492. package/dist/substrate/lifo/commands/text/strings.js +69 -0
  493. package/dist/substrate/lifo/commands/text/tac.d.ts +4 -0
  494. package/dist/substrate/lifo/commands/text/tac.d.ts.map +1 -0
  495. package/dist/substrate/lifo/commands/text/tac.js +32 -0
  496. package/dist/substrate/lifo/commands/text/tail.d.ts +4 -0
  497. package/dist/substrate/lifo/commands/text/tail.d.ts.map +1 -0
  498. package/dist/substrate/lifo/commands/text/tail.js +64 -0
  499. package/dist/substrate/lifo/commands/text/tr.d.ts +4 -0
  500. package/dist/substrate/lifo/commands/text/tr.d.ts.map +1 -0
  501. package/dist/substrate/lifo/commands/text/tr.js +180 -0
  502. package/dist/substrate/lifo/commands/text/uniq.d.ts +4 -0
  503. package/dist/substrate/lifo/commands/text/uniq.d.ts.map +1 -0
  504. package/dist/substrate/lifo/commands/text/uniq.js +82 -0
  505. package/dist/substrate/lifo/commands/text/wc.d.ts +4 -0
  506. package/dist/substrate/lifo/commands/text/wc.d.ts.map +1 -0
  507. package/dist/substrate/lifo/commands/text/wc.js +107 -0
  508. package/dist/substrate/lifo/commands/types.d.ts +49 -0
  509. package/dist/substrate/lifo/commands/types.d.ts.map +1 -0
  510. package/dist/substrate/lifo/commands/types.js +1 -0
  511. package/dist/substrate/lifo/index.d.ts +67 -0
  512. package/dist/substrate/lifo/index.d.ts.map +1 -0
  513. package/dist/substrate/lifo/index.js +66 -0
  514. package/dist/substrate/lifo/kernel/ServiceManager.d.ts +51 -0
  515. package/dist/substrate/lifo/kernel/ServiceManager.d.ts.map +1 -0
  516. package/dist/substrate/lifo/kernel/ServiceManager.js +302 -0
  517. package/dist/substrate/lifo/kernel/index.d.ts +40 -0
  518. package/dist/substrate/lifo/kernel/index.d.ts.map +1 -0
  519. package/dist/substrate/lifo/kernel/index.js +170 -0
  520. package/dist/substrate/lifo/kernel/network/Bridge.d.ts +110 -0
  521. package/dist/substrate/lifo/kernel/network/Bridge.d.ts.map +1 -0
  522. package/dist/substrate/lifo/kernel/network/Bridge.js +278 -0
  523. package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts +50 -0
  524. package/dist/substrate/lifo/kernel/network/DNSResolver.d.ts.map +1 -0
  525. package/dist/substrate/lifo/kernel/network/DNSResolver.js +135 -0
  526. package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts +83 -0
  527. package/dist/substrate/lifo/kernel/network/NetworkInterface.d.ts.map +1 -0
  528. package/dist/substrate/lifo/kernel/network/NetworkInterface.js +183 -0
  529. package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts +98 -0
  530. package/dist/substrate/lifo/kernel/network/NetworkNamespace.d.ts.map +1 -0
  531. package/dist/substrate/lifo/kernel/network/NetworkNamespace.js +149 -0
  532. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts +178 -0
  533. package/dist/substrate/lifo/kernel/network/NetworkStack.d.ts.map +1 -0
  534. package/dist/substrate/lifo/kernel/network/NetworkStack.js +460 -0
  535. package/dist/substrate/lifo/kernel/network/PortBridge.d.ts +44 -0
  536. package/dist/substrate/lifo/kernel/network/PortBridge.d.ts.map +1 -0
  537. package/dist/substrate/lifo/kernel/network/PortBridge.js +158 -0
  538. package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts +55 -0
  539. package/dist/substrate/lifo/kernel/network/RoutingTable.d.ts.map +1 -0
  540. package/dist/substrate/lifo/kernel/network/RoutingTable.js +167 -0
  541. package/dist/substrate/lifo/kernel/network/Socket.d.ts +65 -0
  542. package/dist/substrate/lifo/kernel/network/Socket.d.ts.map +1 -0
  543. package/dist/substrate/lifo/kernel/network/Socket.js +201 -0
  544. package/dist/substrate/lifo/kernel/network/index.d.ts +11 -0
  545. package/dist/substrate/lifo/kernel/network/index.d.ts.map +1 -0
  546. package/dist/substrate/lifo/kernel/network/index.js +9 -0
  547. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts +55 -0
  548. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.d.ts.map +1 -0
  549. package/dist/substrate/lifo/kernel/network/tunnel/BaseTunnel.js +88 -0
  550. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts +71 -0
  551. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.d.ts.map +1 -0
  552. package/dist/substrate/lifo/kernel/network/tunnel/VETHPair.js +191 -0
  553. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts +89 -0
  554. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.d.ts.map +1 -0
  555. package/dist/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.js +324 -0
  556. package/dist/substrate/lifo/kernel/network/types.d.ts +159 -0
  557. package/dist/substrate/lifo/kernel/network/types.d.ts.map +1 -0
  558. package/dist/substrate/lifo/kernel/network/types.js +5 -0
  559. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts +12 -0
  560. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.d.ts.map +1 -0
  561. package/dist/substrate/lifo/kernel/persistence/PersistenceManager.js +47 -0
  562. package/dist/substrate/lifo/kernel/persistence/backends.d.ts +15 -0
  563. package/dist/substrate/lifo/kernel/persistence/backends.d.ts.map +1 -0
  564. package/dist/substrate/lifo/kernel/persistence/backends.js +18 -0
  565. package/dist/substrate/lifo/kernel/persistence/index.d.ts +6 -0
  566. package/dist/substrate/lifo/kernel/persistence/index.d.ts.map +1 -0
  567. package/dist/substrate/lifo/kernel/persistence/index.js +3 -0
  568. package/dist/substrate/lifo/kernel/persistence/serializer.d.ts +21 -0
  569. package/dist/substrate/lifo/kernel/persistence/serializer.d.ts.map +1 -0
  570. package/dist/substrate/lifo/kernel/persistence/serializer.js +99 -0
  571. package/dist/substrate/lifo/kernel/samples.d.ts +3 -0
  572. package/dist/substrate/lifo/kernel/samples.d.ts.map +1 -0
  573. package/dist/substrate/lifo/kernel/samples.js +930 -0
  574. package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts +22 -0
  575. package/dist/substrate/lifo/kernel/storage/BlobStore.d.ts.map +1 -0
  576. package/dist/substrate/lifo/kernel/storage/BlobStore.js +84 -0
  577. package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts +54 -0
  578. package/dist/substrate/lifo/kernel/storage/ContentStore.d.ts.map +1 -0
  579. package/dist/substrate/lifo/kernel/storage/ContentStore.js +120 -0
  580. package/dist/substrate/lifo/kernel/storage/index.d.ts +6 -0
  581. package/dist/substrate/lifo/kernel/storage/index.d.ts.map +1 -0
  582. package/dist/substrate/lifo/kernel/storage/index.js +3 -0
  583. package/dist/substrate/lifo/kernel/unit-parser.d.ts +22 -0
  584. package/dist/substrate/lifo/kernel/unit-parser.d.ts.map +1 -0
  585. package/dist/substrate/lifo/kernel/unit-parser.js +80 -0
  586. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts +99 -0
  587. package/dist/substrate/lifo/kernel/vfs/VFS.d.ts.map +1 -0
  588. package/dist/substrate/lifo/kernel/vfs/VFS.js +679 -0
  589. package/dist/substrate/lifo/kernel/vfs/index.d.ts +8 -0
  590. package/dist/substrate/lifo/kernel/vfs/index.d.ts.map +1 -0
  591. package/dist/substrate/lifo/kernel/vfs/index.js +5 -0
  592. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts +21 -0
  593. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.d.ts.map +1 -0
  594. package/dist/substrate/lifo/kernel/vfs/providers/DevProvider.js +137 -0
  595. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts +63 -0
  596. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.d.ts.map +1 -0
  597. package/dist/substrate/lifo/kernel/vfs/providers/NativeFsProvider.js +175 -0
  598. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts +14 -0
  599. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.d.ts.map +1 -0
  600. package/dist/substrate/lifo/kernel/vfs/providers/ProcProvider.js +126 -0
  601. package/dist/substrate/lifo/kernel/vfs/types.d.ts +117 -0
  602. package/dist/substrate/lifo/kernel/vfs/types.d.ts.map +1 -0
  603. package/dist/substrate/lifo/kernel/vfs/types.js +42 -0
  604. package/dist/substrate/lifo/node-compat/buffer.d.ts +44 -0
  605. package/dist/substrate/lifo/node-compat/buffer.d.ts.map +1 -0
  606. package/dist/substrate/lifo/node-compat/buffer.js +331 -0
  607. package/dist/substrate/lifo/node-compat/child_process.d.ts +10 -0
  608. package/dist/substrate/lifo/node-compat/child_process.d.ts.map +1 -0
  609. package/dist/substrate/lifo/node-compat/child_process.js +42 -0
  610. package/dist/substrate/lifo/node-compat/console.d.ts +21 -0
  611. package/dist/substrate/lifo/node-compat/console.d.ts.map +1 -0
  612. package/dist/substrate/lifo/node-compat/console.js +68 -0
  613. package/dist/substrate/lifo/node-compat/crypto.d.ts +24 -0
  614. package/dist/substrate/lifo/node-compat/crypto.d.ts.map +1 -0
  615. package/dist/substrate/lifo/node-compat/crypto.js +216 -0
  616. package/dist/substrate/lifo/node-compat/dns.d.ts +117 -0
  617. package/dist/substrate/lifo/node-compat/dns.d.ts.map +1 -0
  618. package/dist/substrate/lifo/node-compat/dns.js +144 -0
  619. package/dist/substrate/lifo/node-compat/esbuild.d.ts +10 -0
  620. package/dist/substrate/lifo/node-compat/esbuild.d.ts.map +1 -0
  621. package/dist/substrate/lifo/node-compat/esbuild.js +70 -0
  622. package/dist/substrate/lifo/node-compat/events.d.ts +19 -0
  623. package/dist/substrate/lifo/node-compat/events.d.ts.map +1 -0
  624. package/dist/substrate/lifo/node-compat/events.js +83 -0
  625. package/dist/substrate/lifo/node-compat/fs.d.ts +195 -0
  626. package/dist/substrate/lifo/node-compat/fs.d.ts.map +1 -0
  627. package/dist/substrate/lifo/node-compat/fs.js +624 -0
  628. package/dist/substrate/lifo/node-compat/http.d.ts +133 -0
  629. package/dist/substrate/lifo/node-compat/http.d.ts.map +1 -0
  630. package/dist/substrate/lifo/node-compat/http.js +452 -0
  631. package/dist/substrate/lifo/node-compat/index.d.ts +20 -0
  632. package/dist/substrate/lifo/node-compat/index.d.ts.map +1 -0
  633. package/dist/substrate/lifo/node-compat/index.js +291 -0
  634. package/dist/substrate/lifo/node-compat/module.d.ts +55 -0
  635. package/dist/substrate/lifo/node-compat/module.d.ts.map +1 -0
  636. package/dist/substrate/lifo/node-compat/module.js +124 -0
  637. package/dist/substrate/lifo/node-compat/os.d.ts +39 -0
  638. package/dist/substrate/lifo/node-compat/os.d.ts.map +1 -0
  639. package/dist/substrate/lifo/node-compat/os.js +45 -0
  640. package/dist/substrate/lifo/node-compat/path.d.ts +86 -0
  641. package/dist/substrate/lifo/node-compat/path.d.ts.map +1 -0
  642. package/dist/substrate/lifo/node-compat/path.js +49 -0
  643. package/dist/substrate/lifo/node-compat/process.d.ts +127 -0
  644. package/dist/substrate/lifo/node-compat/process.d.ts.map +1 -0
  645. package/dist/substrate/lifo/node-compat/process.js +173 -0
  646. package/dist/substrate/lifo/node-compat/readline.d.ts +85 -0
  647. package/dist/substrate/lifo/node-compat/readline.d.ts.map +1 -0
  648. package/dist/substrate/lifo/node-compat/readline.js +205 -0
  649. package/dist/substrate/lifo/node-compat/rimraf.d.ts +30 -0
  650. package/dist/substrate/lifo/node-compat/rimraf.d.ts.map +1 -0
  651. package/dist/substrate/lifo/node-compat/rimraf.js +63 -0
  652. package/dist/substrate/lifo/node-compat/stream.d.ts +36 -0
  653. package/dist/substrate/lifo/node-compat/stream.d.ts.map +1 -0
  654. package/dist/substrate/lifo/node-compat/stream.js +90 -0
  655. package/dist/substrate/lifo/node-compat/string_decoder.d.ts +11 -0
  656. package/dist/substrate/lifo/node-compat/string_decoder.d.ts.map +1 -0
  657. package/dist/substrate/lifo/node-compat/string_decoder.js +23 -0
  658. package/dist/substrate/lifo/node-compat/timers.d.ts +16 -0
  659. package/dist/substrate/lifo/node-compat/timers.d.ts.map +1 -0
  660. package/dist/substrate/lifo/node-compat/timers.js +18 -0
  661. package/dist/substrate/lifo/node-compat/tty.d.ts +32 -0
  662. package/dist/substrate/lifo/node-compat/tty.d.ts.map +1 -0
  663. package/dist/substrate/lifo/node-compat/tty.js +76 -0
  664. package/dist/substrate/lifo/node-compat/url.d.ts +37 -0
  665. package/dist/substrate/lifo/node-compat/url.d.ts.map +1 -0
  666. package/dist/substrate/lifo/node-compat/url.js +65 -0
  667. package/dist/substrate/lifo/node-compat/util.d.ts +42 -0
  668. package/dist/substrate/lifo/node-compat/util.d.ts.map +1 -0
  669. package/dist/substrate/lifo/node-compat/util.js +132 -0
  670. package/dist/substrate/lifo/node-compat/zlib.d.ts +66 -0
  671. package/dist/substrate/lifo/node-compat/zlib.d.ts.map +1 -0
  672. package/dist/substrate/lifo/node-compat/zlib.js +80 -0
  673. package/dist/substrate/lifo/pkg/PackageManager.d.ts +19 -0
  674. package/dist/substrate/lifo/pkg/PackageManager.d.ts.map +1 -0
  675. package/dist/substrate/lifo/pkg/PackageManager.js +97 -0
  676. package/dist/substrate/lifo/pkg/lifo-dev.d.ts +38 -0
  677. package/dist/substrate/lifo/pkg/lifo-dev.d.ts.map +1 -0
  678. package/dist/substrate/lifo/pkg/lifo-dev.js +98 -0
  679. package/dist/substrate/lifo/pkg/lifo-runtime.d.ts +44 -0
  680. package/dist/substrate/lifo/pkg/lifo-runtime.d.ts.map +1 -0
  681. package/dist/substrate/lifo/pkg/lifo-runtime.js +372 -0
  682. package/dist/substrate/lifo/pkg/loader.d.ts +4 -0
  683. package/dist/substrate/lifo/pkg/loader.d.ts.map +1 -0
  684. package/dist/substrate/lifo/pkg/loader.js +24 -0
  685. package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts +18 -0
  686. package/dist/substrate/lifo/sandbox/HeadlessTerminal.d.ts.map +1 -0
  687. package/dist/substrate/lifo/sandbox/HeadlessTerminal.js +28 -0
  688. package/dist/substrate/lifo/sandbox/Sandbox.d.ts +59 -0
  689. package/dist/substrate/lifo/sandbox/Sandbox.d.ts.map +1 -0
  690. package/dist/substrate/lifo/sandbox/Sandbox.js +234 -0
  691. package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts +18 -0
  692. package/dist/substrate/lifo/sandbox/SandboxCommands.d.ts.map +1 -0
  693. package/dist/substrate/lifo/sandbox/SandboxCommands.js +64 -0
  694. package/dist/substrate/lifo/sandbox/SandboxFs.d.ts +43 -0
  695. package/dist/substrate/lifo/sandbox/SandboxFs.d.ts.map +1 -0
  696. package/dist/substrate/lifo/sandbox/SandboxFs.js +136 -0
  697. package/dist/substrate/lifo/sandbox/index.d.ts +3 -0
  698. package/dist/substrate/lifo/sandbox/index.d.ts.map +1 -0
  699. package/dist/substrate/lifo/sandbox/index.js +1 -0
  700. package/dist/substrate/lifo/sandbox/types.d.ts +116 -0
  701. package/dist/substrate/lifo/sandbox/types.d.ts.map +1 -0
  702. package/dist/substrate/lifo/sandbox/types.js +1 -0
  703. package/dist/substrate/lifo/shell/ProcessRegistry.d.ts +142 -0
  704. package/dist/substrate/lifo/shell/ProcessRegistry.d.ts.map +1 -0
  705. package/dist/substrate/lifo/shell/ProcessRegistry.js +256 -0
  706. package/dist/substrate/lifo/shell/Shell.d.ts +170 -0
  707. package/dist/substrate/lifo/shell/Shell.d.ts.map +1 -0
  708. package/dist/substrate/lifo/shell/Shell.js +1863 -0
  709. package/dist/substrate/lifo/shell/completer.d.ts +19 -0
  710. package/dist/substrate/lifo/shell/completer.d.ts.map +1 -0
  711. package/dist/substrate/lifo/shell/completer.js +164 -0
  712. package/dist/substrate/lifo/shell/default-shell.d.ts +9 -0
  713. package/dist/substrate/lifo/shell/default-shell.d.ts.map +1 -0
  714. package/dist/substrate/lifo/shell/default-shell.js +53 -0
  715. package/dist/substrate/lifo/shell/expander.d.ts +48 -0
  716. package/dist/substrate/lifo/shell/expander.d.ts.map +1 -0
  717. package/dist/substrate/lifo/shell/expander.js +1204 -0
  718. package/dist/substrate/lifo/shell/history.d.ts +22 -0
  719. package/dist/substrate/lifo/shell/history.d.ts.map +1 -0
  720. package/dist/substrate/lifo/shell/history.js +121 -0
  721. package/dist/substrate/lifo/shell/interpreter.d.ts +257 -0
  722. package/dist/substrate/lifo/shell/interpreter.d.ts.map +1 -0
  723. package/dist/substrate/lifo/shell/interpreter.js +1490 -0
  724. package/dist/substrate/lifo/shell/jobs.d.ts +21 -0
  725. package/dist/substrate/lifo/shell/jobs.d.ts.map +1 -0
  726. package/dist/substrate/lifo/shell/jobs.js +48 -0
  727. package/dist/substrate/lifo/shell/lexer.d.ts +13 -0
  728. package/dist/substrate/lifo/shell/lexer.d.ts.map +1 -0
  729. package/dist/substrate/lifo/shell/lexer.js +614 -0
  730. package/dist/substrate/lifo/shell/parser.d.ts +7 -0
  731. package/dist/substrate/lifo/shell/parser.d.ts.map +1 -0
  732. package/dist/substrate/lifo/shell/parser.js +573 -0
  733. package/dist/substrate/lifo/shell/pipe.d.ts +15 -0
  734. package/dist/substrate/lifo/shell/pipe.d.ts.map +1 -0
  735. package/dist/substrate/lifo/shell/pipe.js +86 -0
  736. package/dist/substrate/lifo/shell/signals.d.ts +11 -0
  737. package/dist/substrate/lifo/shell/signals.d.ts.map +1 -0
  738. package/dist/substrate/lifo/shell/signals.js +57 -0
  739. package/dist/substrate/lifo/shell/terminal-stdin.d.ts +28 -0
  740. package/dist/substrate/lifo/shell/terminal-stdin.d.ts.map +1 -0
  741. package/dist/substrate/lifo/shell/terminal-stdin.js +100 -0
  742. package/dist/substrate/lifo/shell/test-builtin.d.ts +16 -0
  743. package/dist/substrate/lifo/shell/test-builtin.d.ts.map +1 -0
  744. package/dist/substrate/lifo/shell/test-builtin.js +237 -0
  745. package/dist/substrate/lifo/shell/types.d.ts +140 -0
  746. package/dist/substrate/lifo/shell/types.d.ts.map +1 -0
  747. package/dist/substrate/lifo/shell/types.js +25 -0
  748. package/dist/substrate/lifo/terminal/ITerminal.d.ts +10 -0
  749. package/dist/substrate/lifo/terminal/ITerminal.d.ts.map +1 -0
  750. package/dist/substrate/lifo/terminal/ITerminal.js +1 -0
  751. package/dist/substrate/lifo/utils/archive.d.ts +28 -0
  752. package/dist/substrate/lifo/utils/archive.d.ts.map +1 -0
  753. package/dist/substrate/lifo/utils/archive.js +323 -0
  754. package/dist/substrate/lifo/utils/args.d.ts +18 -0
  755. package/dist/substrate/lifo/utils/args.d.ts.map +1 -0
  756. package/dist/substrate/lifo/utils/args.js +73 -0
  757. package/dist/substrate/lifo/utils/colors.d.ts +27 -0
  758. package/dist/substrate/lifo/utils/colors.d.ts.map +1 -0
  759. package/dist/substrate/lifo/utils/colors.js +26 -0
  760. package/dist/substrate/lifo/utils/encoding.d.ts +4 -0
  761. package/dist/substrate/lifo/utils/encoding.d.ts.map +1 -0
  762. package/dist/substrate/lifo/utils/encoding.js +18 -0
  763. package/dist/substrate/lifo/utils/glob.d.ts +12 -0
  764. package/dist/substrate/lifo/utils/glob.d.ts.map +1 -0
  765. package/dist/substrate/lifo/utils/glob.js +150 -0
  766. package/dist/substrate/lifo/utils/mime.d.ts +4 -0
  767. package/dist/substrate/lifo/utils/mime.d.ts.map +1 -0
  768. package/dist/substrate/lifo/utils/mime.js +107 -0
  769. package/dist/substrate/lifo/utils/path.d.ts +12 -0
  770. package/dist/substrate/lifo/utils/path.d.ts.map +1 -0
  771. package/dist/substrate/lifo/utils/path.js +82 -0
  772. package/dist/vfs/events.d.ts +51 -0
  773. package/dist/vfs/events.d.ts.map +1 -0
  774. package/dist/vfs/events.js +127 -0
  775. package/dist/vfs/path.d.ts +46 -0
  776. package/dist/vfs/path.d.ts.map +1 -0
  777. package/dist/vfs/path.js +69 -0
  778. package/dist/vfs/seed-project.d.ts +62 -0
  779. package/dist/vfs/seed-project.d.ts.map +1 -0
  780. package/dist/vfs/seed-project.js +1054 -0
  781. package/dist/vfs/sqlite-vfs.d.ts +821 -0
  782. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  783. package/dist/vfs/sqlite-vfs.js +4135 -0
  784. package/dist/vfs/symlink-registry.d.ts +60 -0
  785. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  786. package/dist/vfs/symlink-registry.js +162 -0
  787. package/dist/workspace/nimbus-workspace.d.ts +94 -0
  788. package/dist/workspace/nimbus-workspace.d.ts.map +1 -0
  789. package/dist/workspace/nimbus-workspace.js +167 -0
  790. package/package.json +69 -0
  791. package/src/_shared/ai-egress.ts +91 -0
  792. package/src/_shared/byte-stream.ts +97 -0
  793. package/src/_shared/bytes.ts +19 -0
  794. package/src/_shared/cache-stats.ts +231 -0
  795. package/src/_shared/crypto.ts +184 -0
  796. package/src/_shared/esbuild-facet-shim.ts +22 -0
  797. package/src/_shared/exports-resolver.ts +308 -0
  798. package/src/_shared/facet-vfs-cursor.ts +58 -0
  799. package/src/_shared/install-phase.ts +56 -0
  800. package/src/_shared/lru-map.ts +59 -0
  801. package/src/_shared/real-node-imports.ts +53 -0
  802. package/src/_shared/retry.ts +202 -0
  803. package/src/_shared/rpc-dispose.ts +31 -0
  804. package/src/_shared/terminal.ts +3 -0
  805. package/src/_shared/typescript-specifiers.ts +86 -0
  806. package/src/_shared/untrusted-request.ts +75 -0
  807. package/src/_shared/vfs-write-ledger.ts +440 -0
  808. package/src/_shared/w7-frame.ts +1012 -0
  809. package/src/_shared/weighted-credit-pool.ts +244 -0
  810. package/src/constants.ts +285 -0
  811. package/src/index.ts +22 -0
  812. package/src/observability/diag-counters.ts +343 -0
  813. package/src/observability/heap-estimate.ts +331 -0
  814. package/src/observability/heavy-alloc-coord.ts +241 -0
  815. package/src/observability/oom-classify.ts +253 -0
  816. package/src/observability/oom-discriminator.ts +405 -0
  817. package/src/runtime/assets-loader.ts +189 -0
  818. package/src/runtime/barrel-detect.ts +69 -0
  819. package/src/runtime/bash/preamble.ts +1519 -0
  820. package/src/runtime/bash/types.ts +497 -0
  821. package/src/runtime/bundle-profile.ts +32 -0
  822. package/src/runtime/cirrus-vite-config-rewriter.ts +28 -0
  823. package/src/runtime/cli-flags.ts +11 -0
  824. package/src/runtime/comment-strip.ts +73 -0
  825. package/src/runtime/cpython-preamble.ts +298 -0
  826. package/src/runtime/esbuild-service.ts +1470 -0
  827. package/src/runtime/framework-detect.ts +245 -0
  828. package/src/runtime/import-meta-transform.ts +16 -0
  829. package/src/runtime/javascript-ast.ts +66 -0
  830. package/src/runtime/long-running-handle.ts +185 -0
  831. package/src/runtime/module-source-rewriter.ts +95 -0
  832. package/src/runtime/opentui-wasm-backend.ts +608 -0
  833. package/src/runtime/os-contracts.ts +529 -0
  834. package/src/runtime/port-registry.ts +298 -0
  835. package/src/runtime/process-input-routing.ts +92 -0
  836. package/src/runtime/process-input.ts +148 -0
  837. package/src/runtime/process-io-protocol.ts +97 -0
  838. package/src/runtime/process-logs.ts +882 -0
  839. package/src/runtime/process-table.ts +225 -0
  840. package/src/runtime/project-detect.ts +42 -0
  841. package/src/runtime/python-server-adapter.ts +133 -0
  842. package/src/runtime/require-resolver.ts +760 -0
  843. package/src/runtime/router-basename.ts +279 -0
  844. package/src/runtime/ruby-green-threads.ts +904 -0
  845. package/src/runtime/ruby-socket-shim.ts +548 -0
  846. package/src/runtime/session-process-supervisor.ts +323 -0
  847. package/src/runtime/sqlite-runtime-fs-bridge.ts +468 -0
  848. package/src/runtime/streams.ts +547 -0
  849. package/src/runtime/undici-shim.ts +517 -0
  850. package/src/runtime/vfs-manifest.ts +170 -0
  851. package/src/runtime/vfs-snapshot.ts +273 -0
  852. package/src/runtime/virtual-socket-kernel.ts +1370 -0
  853. package/src/runtime/vite-config-parser.ts +273 -0
  854. package/src/runtime/wasi/types.ts +448 -0
  855. package/src/runtime/wasi-instance.generated.ts +14 -0
  856. package/src/runtime/wasi-instance.ts +259 -0
  857. package/src/runtime/wasi-threads.ts +682 -0
  858. package/src/runtime/wasm-memory.ts +261 -0
  859. package/src/shell/elevation-commands.ts +117 -0
  860. package/src/shell/exec-dispatch.ts +201 -0
  861. package/src/shell/features.ts +870 -0
  862. package/src/shell/shell-entrypoints.ts +323 -0
  863. package/src/shell/shell-invocation.ts +181 -0
  864. package/src/shell/unix-accounts.ts +128 -0
  865. package/src/shell/unix-commands.ts +4335 -0
  866. package/src/substrate/lifo/UPSTREAM.md +14 -0
  867. package/src/substrate/lifo/commands/archive/gunzip.ts +63 -0
  868. package/src/substrate/lifo/commands/archive/gzip.ts +76 -0
  869. package/src/substrate/lifo/commands/archive/tar.ts +157 -0
  870. package/src/substrate/lifo/commands/archive/unzip.ts +112 -0
  871. package/src/substrate/lifo/commands/archive/zip.ts +70 -0
  872. package/src/substrate/lifo/commands/fs/basename.ts +16 -0
  873. package/src/substrate/lifo/commands/fs/cat.ts +54 -0
  874. package/src/substrate/lifo/commands/fs/chmod.ts +110 -0
  875. package/src/substrate/lifo/commands/fs/chown.ts +45 -0
  876. package/src/substrate/lifo/commands/fs/cp.ts +33 -0
  877. package/src/substrate/lifo/commands/fs/df.ts +56 -0
  878. package/src/substrate/lifo/commands/fs/dirname.ts +16 -0
  879. package/src/substrate/lifo/commands/fs/du.ts +100 -0
  880. package/src/substrate/lifo/commands/fs/file.ts +90 -0
  881. package/src/substrate/lifo/commands/fs/find.ts +76 -0
  882. package/src/substrate/lifo/commands/fs/ln.ts +19 -0
  883. package/src/substrate/lifo/commands/fs/ls.ts +164 -0
  884. package/src/substrate/lifo/commands/fs/mkdir.ts +37 -0
  885. package/src/substrate/lifo/commands/fs/mktemp.ts +56 -0
  886. package/src/substrate/lifo/commands/fs/mv.ts +111 -0
  887. package/src/substrate/lifo/commands/fs/realpath.ts +31 -0
  888. package/src/substrate/lifo/commands/fs/rm.ts +53 -0
  889. package/src/substrate/lifo/commands/fs/rmdir.ts +50 -0
  890. package/src/substrate/lifo/commands/fs/stat.ts +39 -0
  891. package/src/substrate/lifo/commands/fs/touch.ts +30 -0
  892. package/src/substrate/lifo/commands/fs/tree.ts +76 -0
  893. package/src/substrate/lifo/commands/index.ts +2 -0
  894. package/src/substrate/lifo/commands/io/dd.ts +289 -0
  895. package/src/substrate/lifo/commands/io/printf.ts +78 -0
  896. package/src/substrate/lifo/commands/io/tee.ts +37 -0
  897. package/src/substrate/lifo/commands/io/xargs.ts +49 -0
  898. package/src/substrate/lifo/commands/io/yes.ts +15 -0
  899. package/src/substrate/lifo/commands/net/curl.ts +739 -0
  900. package/src/substrate/lifo/commands/net/dig.ts +99 -0
  901. package/src/substrate/lifo/commands/net/forward.ts +90 -0
  902. package/src/substrate/lifo/commands/net/host.ts +182 -0
  903. package/src/substrate/lifo/commands/net/ifconfig.ts +139 -0
  904. package/src/substrate/lifo/commands/net/ip.ts +633 -0
  905. package/src/substrate/lifo/commands/net/netstat.ts +106 -0
  906. package/src/substrate/lifo/commands/net/ping.ts +74 -0
  907. package/src/substrate/lifo/commands/net/ports.ts +45 -0
  908. package/src/substrate/lifo/commands/net/route.ts +153 -0
  909. package/src/substrate/lifo/commands/net/tunnel-v2.ts +183 -0
  910. package/src/substrate/lifo/commands/net/tunnel.ts +272 -0
  911. package/src/substrate/lifo/commands/net/wget.ts +88 -0
  912. package/src/substrate/lifo/commands/registry.ts +141 -0
  913. package/src/substrate/lifo/commands/signal.ts +65 -0
  914. package/src/substrate/lifo/commands/system/bc.ts +265 -0
  915. package/src/substrate/lifo/commands/system/cal.ts +90 -0
  916. package/src/substrate/lifo/commands/system/date.ts +59 -0
  917. package/src/substrate/lifo/commands/system/env.ts +10 -0
  918. package/src/substrate/lifo/commands/system/fastfetch.ts +428 -0
  919. package/src/substrate/lifo/commands/system/free.ts +38 -0
  920. package/src/substrate/lifo/commands/system/help.ts +53 -0
  921. package/src/substrate/lifo/commands/system/hostname.ts +9 -0
  922. package/src/substrate/lifo/commands/system/kill.ts +89 -0
  923. package/src/substrate/lifo/commands/system/lifo.ts +525 -0
  924. package/src/substrate/lifo/commands/system/logout.ts +17 -0
  925. package/src/substrate/lifo/commands/system/man.ts +167 -0
  926. package/src/substrate/lifo/commands/system/node.ts +1305 -0
  927. package/src/substrate/lifo/commands/system/npm.ts +1078 -0
  928. package/src/substrate/lifo/commands/system/pkg.ts +111 -0
  929. package/src/substrate/lifo/commands/system/ps.ts +46 -0
  930. package/src/substrate/lifo/commands/system/registry-schemas.ts +33 -0
  931. package/src/substrate/lifo/commands/system/sha256sum.ts +89 -0
  932. package/src/substrate/lifo/commands/system/sl.ts +249 -0
  933. package/src/substrate/lifo/commands/system/sleep.ts +36 -0
  934. package/src/substrate/lifo/commands/system/stty.ts +69 -0
  935. package/src/substrate/lifo/commands/system/systemctl.ts +184 -0
  936. package/src/substrate/lifo/commands/system/top.ts +53 -0
  937. package/src/substrate/lifo/commands/system/uname.ts +59 -0
  938. package/src/substrate/lifo/commands/system/uptime.ts +19 -0
  939. package/src/substrate/lifo/commands/system/watch.ts +67 -0
  940. package/src/substrate/lifo/commands/system/which.ts +51 -0
  941. package/src/substrate/lifo/commands/system/whoami.ts +9 -0
  942. package/src/substrate/lifo/commands/text/awk.ts +210 -0
  943. package/src/substrate/lifo/commands/text/base64.ts +102 -0
  944. package/src/substrate/lifo/commands/text/cut.ts +92 -0
  945. package/src/substrate/lifo/commands/text/diff.ts +314 -0
  946. package/src/substrate/lifo/commands/text/grep.ts +170 -0
  947. package/src/substrate/lifo/commands/text/head.ts +95 -0
  948. package/src/substrate/lifo/commands/text/less.ts +335 -0
  949. package/src/substrate/lifo/commands/text/nano.ts +513 -0
  950. package/src/substrate/lifo/commands/text/nl.ts +74 -0
  951. package/src/substrate/lifo/commands/text/rev.ts +46 -0
  952. package/src/substrate/lifo/commands/text/sed.ts +297 -0
  953. package/src/substrate/lifo/commands/text/seq.ts +76 -0
  954. package/src/substrate/lifo/commands/text/sort.ts +88 -0
  955. package/src/substrate/lifo/commands/text/strings.ts +73 -0
  956. package/src/substrate/lifo/commands/text/tac.ts +37 -0
  957. package/src/substrate/lifo/commands/text/tail.ts +63 -0
  958. package/src/substrate/lifo/commands/text/tr.ts +192 -0
  959. package/src/substrate/lifo/commands/text/uniq.ts +77 -0
  960. package/src/substrate/lifo/commands/text/wc.ts +104 -0
  961. package/src/substrate/lifo/commands/types.ts +58 -0
  962. package/src/substrate/lifo/index.ts +173 -0
  963. package/src/substrate/lifo/kernel/ServiceManager.ts +371 -0
  964. package/src/substrate/lifo/kernel/index.ts +209 -0
  965. package/src/substrate/lifo/kernel/network/Bridge.ts +336 -0
  966. package/src/substrate/lifo/kernel/network/DNSResolver.ts +158 -0
  967. package/src/substrate/lifo/kernel/network/NetworkInterface.ts +235 -0
  968. package/src/substrate/lifo/kernel/network/NetworkNamespace.ts +182 -0
  969. package/src/substrate/lifo/kernel/network/NetworkStack.ts +590 -0
  970. package/src/substrate/lifo/kernel/network/PortBridge.ts +182 -0
  971. package/src/substrate/lifo/kernel/network/RoutingTable.ts +209 -0
  972. package/src/substrate/lifo/kernel/network/Socket.ts +245 -0
  973. package/src/substrate/lifo/kernel/network/index.ts +29 -0
  974. package/src/substrate/lifo/kernel/network/tunnel/BaseTunnel.ts +124 -0
  975. package/src/substrate/lifo/kernel/network/tunnel/VETHPair.ts +227 -0
  976. package/src/substrate/lifo/kernel/network/tunnel/WebSocketTunnel.ts +387 -0
  977. package/src/substrate/lifo/kernel/network/types.ts +178 -0
  978. package/src/substrate/lifo/kernel/persistence/PersistenceManager.ts +50 -0
  979. package/src/substrate/lifo/kernel/persistence/backends.ts +36 -0
  980. package/src/substrate/lifo/kernel/persistence/index.ts +5 -0
  981. package/src/substrate/lifo/kernel/persistence/serializer.ts +127 -0
  982. package/src/substrate/lifo/kernel/samples.ts +966 -0
  983. package/src/substrate/lifo/kernel/storage/BlobStore.ts +114 -0
  984. package/src/substrate/lifo/kernel/storage/ContentStore.ts +150 -0
  985. package/src/substrate/lifo/kernel/storage/index.ts +5 -0
  986. package/src/substrate/lifo/kernel/unit-parser.ts +95 -0
  987. package/src/substrate/lifo/kernel/vfs/VFS.ts +768 -0
  988. package/src/substrate/lifo/kernel/vfs/index.ts +9 -0
  989. package/src/substrate/lifo/kernel/vfs/providers/DevProvider.ts +172 -0
  990. package/src/substrate/lifo/kernel/vfs/providers/NativeFsProvider.ts +198 -0
  991. package/src/substrate/lifo/kernel/vfs/providers/ProcProvider.ts +144 -0
  992. package/src/substrate/lifo/kernel/vfs/types.ts +145 -0
  993. package/src/substrate/lifo/node-compat/buffer.ts +357 -0
  994. package/src/substrate/lifo/node-compat/child_process.ts +52 -0
  995. package/src/substrate/lifo/node-compat/console.ts +70 -0
  996. package/src/substrate/lifo/node-compat/crypto.ts +186 -0
  997. package/src/substrate/lifo/node-compat/dns.ts +214 -0
  998. package/src/substrate/lifo/node-compat/esbuild.ts +85 -0
  999. package/src/substrate/lifo/node-compat/events.ts +94 -0
  1000. package/src/substrate/lifo/node-compat/fs.ts +762 -0
  1001. package/src/substrate/lifo/node-compat/http.ts +561 -0
  1002. package/src/substrate/lifo/node-compat/index.ts +300 -0
  1003. package/src/substrate/lifo/node-compat/module.ts +148 -0
  1004. package/src/substrate/lifo/node-compat/os.ts +44 -0
  1005. package/src/substrate/lifo/node-compat/path.ts +74 -0
  1006. package/src/substrate/lifo/node-compat/process.ts +178 -0
  1007. package/src/substrate/lifo/node-compat/readline.ts +244 -0
  1008. package/src/substrate/lifo/node-compat/rimraf.ts +80 -0
  1009. package/src/substrate/lifo/node-compat/stream.ts +98 -0
  1010. package/src/substrate/lifo/node-compat/string_decoder.ts +26 -0
  1011. package/src/substrate/lifo/node-compat/timers.ts +21 -0
  1012. package/src/substrate/lifo/node-compat/tty.ts +83 -0
  1013. package/src/substrate/lifo/node-compat/url.ts +76 -0
  1014. package/src/substrate/lifo/node-compat/util.ts +133 -0
  1015. package/src/substrate/lifo/node-compat/zlib.ts +94 -0
  1016. package/src/substrate/lifo/pkg/PackageManager.ts +105 -0
  1017. package/src/substrate/lifo/pkg/lifo-dev.ts +130 -0
  1018. package/src/substrate/lifo/pkg/lifo-runtime.ts +473 -0
  1019. package/src/substrate/lifo/pkg/loader.ts +33 -0
  1020. package/src/substrate/lifo/platform-globals.d.ts +41 -0
  1021. package/src/substrate/lifo/sandbox/HeadlessTerminal.ts +38 -0
  1022. package/src/substrate/lifo/sandbox/Sandbox.ts +279 -0
  1023. package/src/substrate/lifo/sandbox/SandboxCommands.ts +74 -0
  1024. package/src/substrate/lifo/sandbox/SandboxFs.ts +160 -0
  1025. package/src/substrate/lifo/sandbox/index.ts +8 -0
  1026. package/src/substrate/lifo/sandbox/types.ts +117 -0
  1027. package/src/substrate/lifo/shell/ProcessRegistry.ts +379 -0
  1028. package/src/substrate/lifo/shell/Shell.ts +2048 -0
  1029. package/src/substrate/lifo/shell/completer.ts +212 -0
  1030. package/src/substrate/lifo/shell/default-shell.ts +63 -0
  1031. package/src/substrate/lifo/shell/expander.ts +1349 -0
  1032. package/src/substrate/lifo/shell/history.ts +138 -0
  1033. package/src/substrate/lifo/shell/interpreter.ts +1754 -0
  1034. package/src/substrate/lifo/shell/jobs.ts +64 -0
  1035. package/src/substrate/lifo/shell/lexer.ts +698 -0
  1036. package/src/substrate/lifo/shell/parser.ts +711 -0
  1037. package/src/substrate/lifo/shell/pipe.ts +92 -0
  1038. package/src/substrate/lifo/shell/signals.ts +70 -0
  1039. package/src/substrate/lifo/shell/terminal-stdin.ts +109 -0
  1040. package/src/substrate/lifo/shell/test-builtin.ts +316 -0
  1041. package/src/substrate/lifo/shell/types.ts +166 -0
  1042. package/src/substrate/lifo/terminal/ITerminal.ts +9 -0
  1043. package/src/substrate/lifo/utils/archive.ts +389 -0
  1044. package/src/substrate/lifo/utils/args.ts +89 -0
  1045. package/src/substrate/lifo/utils/colors.ts +29 -0
  1046. package/src/substrate/lifo/utils/encoding.ts +21 -0
  1047. package/src/substrate/lifo/utils/glob.ts +172 -0
  1048. package/src/substrate/lifo/utils/mime.ts +114 -0
  1049. package/src/substrate/lifo/utils/path.ts +83 -0
  1050. package/src/types/assets.d.ts +4 -0
  1051. package/src/vfs/events.ts +134 -0
  1052. package/src/vfs/path.ts +71 -0
  1053. package/src/vfs/seed-project.ts +1094 -0
  1054. package/src/vfs/sqlite-vfs.ts +5035 -0
  1055. package/src/vfs/symlink-registry.ts +172 -0
  1056. 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
+ }