@nimbus-sh/worker 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (408) hide show
  1. package/README.md +219 -0
  2. package/dist/_shared/bytes.d.ts +18 -0
  3. package/dist/_shared/bytes.d.ts.map +1 -0
  4. package/dist/_shared/bytes.js +17 -0
  5. package/dist/_shared/cache-stats.d.ts +146 -0
  6. package/dist/_shared/cache-stats.d.ts.map +1 -0
  7. package/dist/_shared/cache-stats.js +162 -0
  8. package/dist/_shared/exports-resolver.d.ts +76 -0
  9. package/dist/_shared/exports-resolver.d.ts.map +1 -0
  10. package/dist/_shared/exports-resolver.js +284 -0
  11. package/dist/_shared/install-phase.d.ts +47 -0
  12. package/dist/_shared/install-phase.d.ts.map +1 -0
  13. package/dist/_shared/install-phase.js +1 -0
  14. package/dist/_shared/real-node-imports.d.ts +38 -0
  15. package/dist/_shared/real-node-imports.d.ts.map +1 -0
  16. package/dist/_shared/real-node-imports.js +46 -0
  17. package/dist/_shared/retry.d.ts +93 -0
  18. package/dist/_shared/retry.d.ts.map +1 -0
  19. package/dist/_shared/retry.js +161 -0
  20. package/dist/_shared/session-id.d.ts +39 -0
  21. package/dist/_shared/session-id.d.ts.map +1 -0
  22. package/dist/_shared/session-id.js +127 -0
  23. package/dist/_shared/session-router.d.ts +104 -0
  24. package/dist/_shared/session-router.d.ts.map +1 -0
  25. package/dist/_shared/session-router.js +165 -0
  26. package/dist/_shared/w7-frame.d.ts +93 -0
  27. package/dist/_shared/w7-frame.d.ts.map +1 -0
  28. package/dist/_shared/w7-frame.js +402 -0
  29. package/dist/auth/index.d.ts +16 -0
  30. package/dist/auth/index.d.ts.map +1 -0
  31. package/dist/auth/index.js +15 -0
  32. package/dist/auth/middleware.d.ts +85 -0
  33. package/dist/auth/middleware.d.ts.map +1 -0
  34. package/dist/auth/middleware.js +150 -0
  35. package/dist/auth/token.d.ts +96 -0
  36. package/dist/auth/token.d.ts.map +1 -0
  37. package/dist/auth/token.js +226 -0
  38. package/dist/auth/types.d.ts +141 -0
  39. package/dist/auth/types.d.ts.map +1 -0
  40. package/dist/auth/types.js +114 -0
  41. package/dist/bindings/d1.d.ts +102 -0
  42. package/dist/bindings/d1.d.ts.map +1 -0
  43. package/dist/bindings/d1.js +549 -0
  44. package/dist/bindings/kv.d.ts +82 -0
  45. package/dist/bindings/kv.d.ts.map +1 -0
  46. package/dist/bindings/kv.js +290 -0
  47. package/dist/bindings/r2.d.ts +137 -0
  48. package/dist/bindings/r2.d.ts.map +1 -0
  49. package/dist/bindings/r2.js +379 -0
  50. package/dist/cirrus-npm-cjs.generated.d.ts +17 -0
  51. package/dist/cirrus-npm-cjs.generated.d.ts.map +1 -0
  52. package/dist/cirrus-npm-cjs.generated.js +41 -0
  53. package/dist/cirrus-plugin-react.generated.d.ts +34 -0
  54. package/dist/cirrus-plugin-react.generated.d.ts.map +1 -0
  55. package/dist/cirrus-plugin-react.generated.js +33 -0
  56. package/dist/constants.d.ts +34 -0
  57. package/dist/constants.d.ts.map +1 -0
  58. package/dist/constants.js +116 -0
  59. package/dist/esbuild-wasm-bundle.generated.d.ts +41 -0
  60. package/dist/esbuild-wasm-bundle.generated.d.ts.map +1 -0
  61. package/dist/esbuild-wasm-bundle.generated.js +40 -0
  62. package/dist/facets/cirrus-real.d.ts +147 -0
  63. package/dist/facets/cirrus-real.d.ts.map +1 -0
  64. package/dist/facets/cirrus-real.js +981 -0
  65. package/dist/facets/inner-do-registry.d.ts +41 -0
  66. package/dist/facets/inner-do-registry.d.ts.map +1 -0
  67. package/dist/facets/inner-do-registry.js +51 -0
  68. package/dist/facets/manager.d.ts +314 -0
  69. package/dist/facets/manager.d.ts.map +1 -0
  70. package/dist/facets/manager.js +2681 -0
  71. package/dist/facets/process.d.ts +318 -0
  72. package/dist/facets/process.d.ts.map +1 -0
  73. package/dist/facets/process.js +567 -0
  74. package/dist/facets/real-vite-fs-shim.d.ts +151 -0
  75. package/dist/facets/real-vite-fs-shim.d.ts.map +1 -0
  76. package/dist/facets/real-vite-fs-shim.js +1279 -0
  77. package/dist/facets/real-vite-hmr.d.ts +142 -0
  78. package/dist/facets/real-vite-hmr.d.ts.map +1 -0
  79. package/dist/facets/real-vite-hmr.js +476 -0
  80. package/dist/facets/vite-dev-server.d.ts +293 -0
  81. package/dist/facets/vite-dev-server.d.ts.map +1 -0
  82. package/dist/facets/vite-dev-server.js +2580 -0
  83. package/dist/facets/wasm-swap-registry.d.ts +261 -0
  84. package/dist/facets/wasm-swap-registry.d.ts.map +1 -0
  85. package/dist/facets/wasm-swap-registry.js +551 -0
  86. package/dist/facets/ws-terminal.d.ts +107 -0
  87. package/dist/facets/ws-terminal.d.ts.map +1 -0
  88. package/dist/facets/ws-terminal.js +207 -0
  89. package/dist/git/commands.d.ts +13 -0
  90. package/dist/git/commands.d.ts.map +1 -0
  91. package/dist/git/commands.js +633 -0
  92. package/dist/git/network-facet.d.ts +61 -0
  93. package/dist/git/network-facet.d.ts.map +1 -0
  94. package/dist/git/network-facet.js +700 -0
  95. package/dist/git-bundle.generated.d.ts +16 -0
  96. package/dist/git-bundle.generated.d.ts.map +1 -0
  97. package/dist/git-bundle.generated.js +15 -0
  98. package/dist/index.d.ts +41 -0
  99. package/dist/index.d.ts.map +1 -0
  100. package/dist/index.js +73 -0
  101. package/dist/loaders/child-process/spawn-facet.d.ts +90 -0
  102. package/dist/loaders/child-process/spawn-facet.d.ts.map +1 -0
  103. package/dist/loaders/child-process/spawn-facet.js +103 -0
  104. package/dist/loaders/child-process/spawn-pool.d.ts +110 -0
  105. package/dist/loaders/child-process/spawn-pool.d.ts.map +1 -0
  106. package/dist/loaders/child-process/spawn-pool.js +161 -0
  107. package/dist/loaders/fanout-pool.d.ts +195 -0
  108. package/dist/loaders/fanout-pool.d.ts.map +1 -0
  109. package/dist/loaders/fanout-pool.js +326 -0
  110. package/dist/loaders/generated-workers.d.ts +26 -0
  111. package/dist/loaders/generated-workers.d.ts.map +1 -0
  112. package/dist/loaders/generated-workers.js +25 -0
  113. package/dist/loaders/index.d.ts +15 -0
  114. package/dist/loaders/index.d.ts.map +1 -0
  115. package/dist/loaders/index.js +13 -0
  116. package/dist/loaders/loader-pool.d.ts +273 -0
  117. package/dist/loaders/loader-pool.d.ts.map +1 -0
  118. package/dist/loaders/loader-pool.js +624 -0
  119. package/dist/loaders/npm-resolve-preamble.d.ts +34 -0
  120. package/dist/loaders/npm-resolve-preamble.d.ts.map +1 -0
  121. package/dist/loaders/npm-resolve-preamble.js +237 -0
  122. package/dist/loaders/pre-bundle-preamble.d.ts +41 -0
  123. package/dist/loaders/pre-bundle-preamble.d.ts.map +1 -0
  124. package/dist/loaders/pre-bundle-preamble.js +114 -0
  125. package/dist/loaders/vendor/codegen.d.ts +24 -0
  126. package/dist/loaders/vendor/codegen.d.ts.map +1 -0
  127. package/dist/loaders/vendor/codegen.js +44 -0
  128. package/dist/loaders/vendor/errors.d.ts +24 -0
  129. package/dist/loaders/vendor/errors.d.ts.map +1 -0
  130. package/dist/loaders/vendor/errors.js +46 -0
  131. package/dist/loaders/vendor/index.d.ts +16 -0
  132. package/dist/loaders/vendor/index.d.ts.map +1 -0
  133. package/dist/loaders/vendor/index.js +11 -0
  134. package/dist/loaders/vendor/pool.d.ts +88 -0
  135. package/dist/loaders/vendor/pool.d.ts.map +1 -0
  136. package/dist/loaders/vendor/pool.js +353 -0
  137. package/dist/loaders/vendor/primitives.d.ts +11 -0
  138. package/dist/loaders/vendor/primitives.d.ts.map +1 -0
  139. package/dist/loaders/vendor/primitives.js +33 -0
  140. package/dist/loaders/vendor/serialize.d.ts +3 -0
  141. package/dist/loaders/vendor/serialize.d.ts.map +1 -0
  142. package/dist/loaders/vendor/serialize.js +25 -0
  143. package/dist/loaders/vendor/types.d.ts +60 -0
  144. package/dist/loaders/vendor/types.d.ts.map +1 -0
  145. package/dist/loaders/vendor/types.js +4 -0
  146. package/dist/npm/cache.d.ts +136 -0
  147. package/dist/npm/cache.d.ts.map +1 -0
  148. package/dist/npm/cache.js +429 -0
  149. package/dist/npm/install-batch-facet.d.ts +129 -0
  150. package/dist/npm/install-batch-facet.d.ts.map +1 -0
  151. package/dist/npm/install-batch-facet.js +598 -0
  152. package/dist/npm/install-facet.d.ts +29 -0
  153. package/dist/npm/install-facet.d.ts.map +1 -0
  154. package/dist/npm/install-facet.js +9 -0
  155. package/dist/npm/installer.d.ts +239 -0
  156. package/dist/npm/installer.d.ts.map +1 -0
  157. package/dist/npm/installer.js +2022 -0
  158. package/dist/npm/npx-install.d.ts +74 -0
  159. package/dist/npm/npx-install.d.ts.map +1 -0
  160. package/dist/npm/npx-install.js +302 -0
  161. package/dist/npm/pre-bundle-facet.d.ts +168 -0
  162. package/dist/npm/pre-bundle-facet.d.ts.map +1 -0
  163. package/dist/npm/pre-bundle-facet.js +653 -0
  164. package/dist/npm/r2-cache.d.ts +286 -0
  165. package/dist/npm/r2-cache.d.ts.map +1 -0
  166. package/dist/npm/r2-cache.js +506 -0
  167. package/dist/npm/resolve-facet.d.ts +202 -0
  168. package/dist/npm/resolve-facet.d.ts.map +1 -0
  169. package/dist/npm/resolve-facet.js +833 -0
  170. package/dist/npm/resolve-one-facet.d.ts +204 -0
  171. package/dist/npm/resolve-one-facet.d.ts.map +1 -0
  172. package/dist/npm/resolve-one-facet.js +437 -0
  173. package/dist/npm/resolver.d.ts +109 -0
  174. package/dist/npm/resolver.d.ts.map +1 -0
  175. package/dist/npm/resolver.js +899 -0
  176. package/dist/npm/tarball-stream.d.ts +85 -0
  177. package/dist/npm/tarball-stream.d.ts.map +1 -0
  178. package/dist/npm/tarball-stream.js +205 -0
  179. package/dist/npm/tarball.d.ts +51 -0
  180. package/dist/npm/tarball.d.ts.map +1 -0
  181. package/dist/npm/tarball.js +185 -0
  182. package/dist/observability/diag-counters.d.ts +292 -0
  183. package/dist/observability/diag-counters.d.ts.map +1 -0
  184. package/dist/observability/diag-counters.js +302 -0
  185. package/dist/observability/heap-estimate.d.ts +166 -0
  186. package/dist/observability/heap-estimate.d.ts.map +1 -0
  187. package/dist/observability/heap-estimate.js +192 -0
  188. package/dist/observability/heavy-alloc-coord.d.ts +95 -0
  189. package/dist/observability/heavy-alloc-coord.d.ts.map +1 -0
  190. package/dist/observability/heavy-alloc-coord.js +149 -0
  191. package/dist/observability/oom-classify.d.ts +56 -0
  192. package/dist/observability/oom-classify.d.ts.map +1 -0
  193. package/dist/observability/oom-classify.js +118 -0
  194. package/dist/observability/oom-discriminator.d.ts +192 -0
  195. package/dist/observability/oom-discriminator.d.ts.map +1 -0
  196. package/dist/observability/oom-discriminator.js +233 -0
  197. package/dist/real-vite-bundle.generated.d.ts +46 -0
  198. package/dist/real-vite-bundle.generated.d.ts.map +1 -0
  199. package/dist/real-vite-bundle.generated.js +53 -0
  200. package/dist/replica/routing.d.ts +171 -0
  201. package/dist/replica/routing.d.ts.map +1 -0
  202. package/dist/replica/routing.js +288 -0
  203. package/dist/replica/suspension.d.ts +36 -0
  204. package/dist/replica/suspension.d.ts.map +1 -0
  205. package/dist/replica/suspension.js +49 -0
  206. package/dist/router/index.d.ts +156 -0
  207. package/dist/router/index.d.ts.map +1 -0
  208. package/dist/router/index.js +220 -0
  209. package/dist/runtime/assets-loader.d.ts +71 -0
  210. package/dist/runtime/assets-loader.d.ts.map +1 -0
  211. package/dist/runtime/assets-loader.js +128 -0
  212. package/dist/runtime/barrel-detect.d.ts +46 -0
  213. package/dist/runtime/barrel-detect.d.ts.map +1 -0
  214. package/dist/runtime/barrel-detect.js +73 -0
  215. package/dist/runtime/barrel-synthesizer.d.ts +181 -0
  216. package/dist/runtime/barrel-synthesizer.d.ts.map +1 -0
  217. package/dist/runtime/barrel-synthesizer.js +541 -0
  218. package/dist/runtime/bun-repl.d.ts +46 -0
  219. package/dist/runtime/bun-repl.d.ts.map +1 -0
  220. package/dist/runtime/bun-repl.js +326 -0
  221. package/dist/runtime/bun-runner.d.ts +66 -0
  222. package/dist/runtime/bun-runner.d.ts.map +1 -0
  223. package/dist/runtime/bun-runner.js +219 -0
  224. package/dist/runtime/clang-runner.d.ts +38 -0
  225. package/dist/runtime/clang-runner.d.ts.map +1 -0
  226. package/dist/runtime/clang-runner.js +1033 -0
  227. package/dist/runtime/comment-strip.d.ts +37 -0
  228. package/dist/runtime/comment-strip.d.ts.map +1 -0
  229. package/dist/runtime/comment-strip.js +74 -0
  230. package/dist/runtime/esbuild-service.d.ts +201 -0
  231. package/dist/runtime/esbuild-service.d.ts.map +1 -0
  232. package/dist/runtime/esbuild-service.js +1444 -0
  233. package/dist/runtime/esbuild-wasm-bytes.d.ts +87 -0
  234. package/dist/runtime/esbuild-wasm-bytes.d.ts.map +1 -0
  235. package/dist/runtime/esbuild-wasm-bytes.js +131 -0
  236. package/dist/runtime/framework-detect.d.ts +71 -0
  237. package/dist/runtime/framework-detect.d.ts.map +1 -0
  238. package/dist/runtime/framework-detect.js +178 -0
  239. package/dist/runtime/long-running-handle.d.ts +133 -0
  240. package/dist/runtime/long-running-handle.d.ts.map +1 -0
  241. package/dist/runtime/long-running-handle.js +192 -0
  242. package/dist/runtime/node-repl.d.ts +36 -0
  243. package/dist/runtime/node-repl.d.ts.map +1 -0
  244. package/dist/runtime/node-repl.js +293 -0
  245. package/dist/runtime/node-runner.d.ts +111 -0
  246. package/dist/runtime/node-runner.d.ts.map +1 -0
  247. package/dist/runtime/node-runner.js +194 -0
  248. package/dist/runtime/node-shims.d.ts +24 -0
  249. package/dist/runtime/node-shims.d.ts.map +1 -0
  250. package/dist/runtime/node-shims.js +3019 -0
  251. package/dist/runtime/package-manager.d.ts +118 -0
  252. package/dist/runtime/package-manager.d.ts.map +1 -0
  253. package/dist/runtime/package-manager.js +420 -0
  254. package/dist/runtime/port-registry.d.ts +80 -0
  255. package/dist/runtime/port-registry.d.ts.map +1 -0
  256. package/dist/runtime/port-registry.js +166 -0
  257. package/dist/runtime/process-logs-api.d.ts +102 -0
  258. package/dist/runtime/process-logs-api.d.ts.map +1 -0
  259. package/dist/runtime/process-logs-api.js +253 -0
  260. package/dist/runtime/process-logs.d.ts +311 -0
  261. package/dist/runtime/process-logs.d.ts.map +1 -0
  262. package/dist/runtime/process-logs.js +656 -0
  263. package/dist/runtime/process-table.d.ts +65 -0
  264. package/dist/runtime/process-table.d.ts.map +1 -0
  265. package/dist/runtime/process-table.js +107 -0
  266. package/dist/runtime/project-detect.d.ts +21 -0
  267. package/dist/runtime/project-detect.d.ts.map +1 -0
  268. package/dist/runtime/project-detect.js +46 -0
  269. package/dist/runtime/python-repl.d.ts +66 -0
  270. package/dist/runtime/python-repl.d.ts.map +1 -0
  271. package/dist/runtime/python-repl.js +563 -0
  272. package/dist/runtime/python-runner.d.ts +50 -0
  273. package/dist/runtime/python-runner.d.ts.map +1 -0
  274. package/dist/runtime/python-runner.js +870 -0
  275. package/dist/runtime/repl-session.d.ts +160 -0
  276. package/dist/runtime/repl-session.d.ts.map +1 -0
  277. package/dist/runtime/repl-session.js +393 -0
  278. package/dist/runtime/require-resolver.d.ts +52 -0
  279. package/dist/runtime/require-resolver.d.ts.map +1 -0
  280. package/dist/runtime/require-resolver.js +690 -0
  281. package/dist/runtime/router-basename.d.ts +36 -0
  282. package/dist/runtime/router-basename.d.ts.map +1 -0
  283. package/dist/runtime/router-basename.js +310 -0
  284. package/dist/runtime/ruby-repl.d.ts +43 -0
  285. package/dist/runtime/ruby-repl.d.ts.map +1 -0
  286. package/dist/runtime/ruby-repl.js +229 -0
  287. package/dist/runtime/ruby-runner.d.ts +62 -0
  288. package/dist/runtime/ruby-runner.d.ts.map +1 -0
  289. package/dist/runtime/ruby-runner.js +737 -0
  290. package/dist/runtime/runtime-catalog.d.ts +99 -0
  291. package/dist/runtime/runtime-catalog.d.ts.map +1 -0
  292. package/dist/runtime/runtime-catalog.js +183 -0
  293. package/dist/runtime/runtime-registry.d.ts +136 -0
  294. package/dist/runtime/runtime-registry.d.ts.map +1 -0
  295. package/dist/runtime/runtime-registry.js +318 -0
  296. package/dist/runtime/streams.d.ts +13 -0
  297. package/dist/runtime/streams.d.ts.map +1 -0
  298. package/dist/runtime/streams.js +402 -0
  299. package/dist/runtime/wasi-instance.d.ts +190 -0
  300. package/dist/runtime/wasi-instance.d.ts.map +1 -0
  301. package/dist/runtime/wasi-instance.js +2044 -0
  302. package/dist/runtime/wasm-runner.d.ts +117 -0
  303. package/dist/runtime/wasm-runner.d.ts.map +1 -0
  304. package/dist/runtime/wasm-runner.js +705 -0
  305. package/dist/session/bindings.d.ts +168 -0
  306. package/dist/session/bindings.d.ts.map +1 -0
  307. package/dist/session/bindings.js +524 -0
  308. package/dist/session/ctx-exports.d.ts +16 -0
  309. package/dist/session/ctx-exports.d.ts.map +1 -0
  310. package/dist/session/ctx-exports.js +22 -0
  311. package/dist/session/diag.d.ts +62 -0
  312. package/dist/session/diag.d.ts.map +1 -0
  313. package/dist/session/diag.js +118 -0
  314. package/dist/session/fs-watch.d.ts +132 -0
  315. package/dist/session/fs-watch.d.ts.map +1 -0
  316. package/dist/session/fs-watch.js +255 -0
  317. package/dist/session/helpers.d.ts +150 -0
  318. package/dist/session/helpers.d.ts.map +1 -0
  319. package/dist/session/helpers.js +472 -0
  320. package/dist/session/hibernation.d.ts +145 -0
  321. package/dist/session/hibernation.d.ts.map +1 -0
  322. package/dist/session/hibernation.js +385 -0
  323. package/dist/session/init-phases.d.ts +87 -0
  324. package/dist/session/init-phases.d.ts.map +1 -0
  325. package/dist/session/init-phases.js +137 -0
  326. package/dist/session/init.d.ts +46 -0
  327. package/dist/session/init.d.ts.map +1 -0
  328. package/dist/session/init.js +3579 -0
  329. package/dist/session/keys.d.ts +56 -0
  330. package/dist/session/keys.d.ts.map +1 -0
  331. package/dist/session/keys.js +55 -0
  332. package/dist/session/nimbus-session.d.ts +434 -0
  333. package/dist/session/nimbus-session.d.ts.map +1 -0
  334. package/dist/session/nimbus-session.js +1084 -0
  335. package/dist/session/programmatic.d.ts +105 -0
  336. package/dist/session/programmatic.d.ts.map +1 -0
  337. package/dist/session/programmatic.js +326 -0
  338. package/dist/session/replica-routes.d.ts +68 -0
  339. package/dist/session/replica-routes.d.ts.map +1 -0
  340. package/dist/session/replica-routes.js +77 -0
  341. package/dist/session/routes.d.ts +29 -0
  342. package/dist/session/routes.d.ts.map +1 -0
  343. package/dist/session/routes.js +1331 -0
  344. package/dist/session/rpc.d.ts +323 -0
  345. package/dist/session/rpc.d.ts.map +1 -0
  346. package/dist/session/rpc.js +808 -0
  347. package/dist/session/state-store.d.ts +210 -0
  348. package/dist/session/state-store.d.ts.map +1 -0
  349. package/dist/session/state-store.js +459 -0
  350. package/dist/session/supervisor-rpc.d.ts +269 -0
  351. package/dist/session/supervisor-rpc.d.ts.map +1 -0
  352. package/dist/session/supervisor-rpc.js +437 -0
  353. package/dist/session/ws-hibernation-config.d.ts +64 -0
  354. package/dist/session/ws-hibernation-config.d.ts.map +1 -0
  355. package/dist/session/ws-hibernation-config.js +89 -0
  356. package/dist/session/ws.d.ts +95 -0
  357. package/dist/session/ws.d.ts.map +1 -0
  358. package/dist/session/ws.js +395 -0
  359. package/dist/shell/features.d.ts +410 -0
  360. package/dist/shell/features.d.ts.map +1 -0
  361. package/dist/shell/features.js +1885 -0
  362. package/dist/shell/unix-commands.d.ts +15 -0
  363. package/dist/shell/unix-commands.d.ts.map +1 -0
  364. package/dist/shell/unix-commands.js +3703 -0
  365. package/dist/stubs/lifo-ui.d.ts +10 -0
  366. package/dist/stubs/lifo-ui.d.ts.map +1 -0
  367. package/dist/stubs/lifo-ui.js +1 -0
  368. package/dist/tailwind-play.generated.d.ts +19 -0
  369. package/dist/tailwind-play.generated.d.ts.map +1 -0
  370. package/dist/tailwind-play.generated.js +18 -0
  371. package/dist/vfs/dev-provider.d.ts +64 -0
  372. package/dist/vfs/dev-provider.d.ts.map +1 -0
  373. package/dist/vfs/dev-provider.js +191 -0
  374. package/dist/vfs/events.d.ts +51 -0
  375. package/dist/vfs/events.d.ts.map +1 -0
  376. package/dist/vfs/events.js +127 -0
  377. package/dist/vfs/path.d.ts +30 -0
  378. package/dist/vfs/path.d.ts.map +1 -0
  379. package/dist/vfs/path.js +41 -0
  380. package/dist/vfs/seed-project.d.ts +62 -0
  381. package/dist/vfs/seed-project.d.ts.map +1 -0
  382. package/dist/vfs/seed-project.js +1045 -0
  383. package/dist/vfs/sqlite-vfs.d.ts +435 -0
  384. package/dist/vfs/sqlite-vfs.d.ts.map +1 -0
  385. package/dist/vfs/sqlite-vfs.js +1670 -0
  386. package/dist/vfs/symlink-registry.d.ts +64 -0
  387. package/dist/vfs/symlink-registry.d.ts.map +1 -0
  388. package/dist/vfs/symlink-registry.js +137 -0
  389. package/dist/wrangler/nimbus-wrangler.d.ts +157 -0
  390. package/dist/wrangler/nimbus-wrangler.d.ts.map +1 -0
  391. package/dist/wrangler/nimbus-wrangler.js +931 -0
  392. package/package.json +77 -0
  393. package/public/_assets/cirrus-plugin-react.bundle.js +70740 -0
  394. package/public/_assets/esbuild-0.24.2.wasm +0 -0
  395. package/public/_assets/real-vite-bundle.js +101478 -0
  396. package/public/_assets/rollup.wasm +0 -0
  397. package/public/favicon.svg +8 -0
  398. package/public/index.html +565 -0
  399. package/public/s/index.html +2379 -0
  400. package/scripts/bundle-esbuild-wasm.mjs +244 -0
  401. package/scripts/bundle-facet-workers.mjs +137 -0
  402. package/scripts/bundle-git.mjs +118 -0
  403. package/scripts/bundle-npm-cjs.mjs +363 -0
  404. package/scripts/bundle-plugin-react.mjs +480 -0
  405. package/scripts/bundle-real-vite.mjs +959 -0
  406. package/scripts/bundle-runtime.mjs +600 -0
  407. package/scripts/bundle-tailwind-play.mjs +130 -0
  408. package/scripts/patch-lifo-ui.mjs +131 -0
@@ -0,0 +1,1670 @@
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
+ * │ On eviction → SQLite write (if dirty) │
13
+ * │ On npm install → batch SQLite writes │
14
+ * └─────────────────────────────────────────┘
15
+ * │ │
16
+ * ▼ ▼
17
+ * ┌─────────────────┐ ┌─────────────────────┐
18
+ * │ file_chunks │ │ inodes │
19
+ * │ (path, chunk_id,│ │ (path, type, mode, │
20
+ * │ data BLOB) │ │ size, mtime, ...) │
21
+ * │ 64KB chunks │ │ │
22
+ * └─────────────────┘ └─────────────────────┘
23
+ * DO SQLite (10 GB)
24
+ *
25
+ * Key design from do86's SqlPageStore:
26
+ * - LRU eviction with dirty-write-back
27
+ * - Microtask-deferred batch writes (64 rows per INSERT)
28
+ * - All operations SYNCHRONOUS (DO sql.exec() is sync)
29
+ *
30
+ * Durability (audit C1):
31
+ * - writeFile() returns void (sync) — preserved to match LIFO's
32
+ * MountProvider.writeFile(subpath, content): void contract.
33
+ * - Deferred-flush failures (from transactionSync or individual row
34
+ * inserts) are retried ONCE without a transaction wrapper. Entries
35
+ * that fail both attempts land in failedWrites and are surfaced to
36
+ * subscribers via onWriteError(). flushAll() throws if any failed
37
+ * writes accumulated since last clearWriteFailures().
38
+ * - Callers that need a hard guarantee can use flushAndWait() (async)
39
+ * at explicit persistence boundaries.
40
+ *
41
+ * Key design decisions:
42
+ * - 64KB chunks (not 4KB): file access is sequential, fewer rows
43
+ * - INode metadata always in memory (small: ~200B per file)
44
+ * - File content demand-paged through LRU cache
45
+ */
46
+ import { VfsEventEmitter } from './events.js';
47
+ import { CHUNK_SIZE, LRU_MAX_ENTRIES } from '../constants.js';
48
+ import { recordFailure } from '../observability/oom-discriminator.js';
49
+ import { classifyError } from '../observability/oom-classify.js';
50
+ import { enc, dec } from '../_shared/bytes.js';
51
+ // ── SqliteVFS ───────────────────────────────────────────────────────────────
52
+ export class SqliteVFS {
53
+ sql;
54
+ ctx;
55
+ events;
56
+ // ── INode tree (always resident) ──────────────────────────────────────
57
+ inodes = new Map();
58
+ /** Children index: parentPath → Set of child paths. O(1) readdir. */
59
+ children = new Map();
60
+ // ── Content cache (LRU, 512 × 64KB = 32MB) ───────────────────────────
61
+ // Map iteration order = insertion order. Delete+re-insert to move to MRU.
62
+ cache = new Map();
63
+ /** Actual bytes in cache (not all chunks are full 64KB) */
64
+ _cacheBytes = 0;
65
+ // ── W5 Lever 8: runtime-mutable LRU cap + shrink refcount ─────────
66
+ // Default seeded from LRU_MAX_ENTRIES (32 MiB). Heavy-alloc owners
67
+ // (npm install, git clone, pre-bundle) call shrinkForInstall() to
68
+ // drop the cap to ~8 MiB and free heap headroom for the in-flight
69
+ // RPC payloads + pending-writes queue. Refcount-based: nested
70
+ // acquires stack; only the OUTERMOST restoreAfterInstall() actually
71
+ // raises the cap back to the default.
72
+ //
73
+ // Why instance-level (not module-level):
74
+ // - Tests need an in-memory VFS without polluting the constant.
75
+ // - Future per-DO tuning (e.g. set higher cap on a session running
76
+ // `vite build` vs `npm install`) becomes a one-call change.
77
+ //
78
+ // The eviction trigger at cacheSet() reads this field. Counter
79
+ // accounting is unchanged.
80
+ _lruMaxEntries = LRU_MAX_ENTRIES;
81
+ _lruShrinkRefcount = 0;
82
+ // ── Running counters for O(1) getStats() (B3 / AUDIT M10 / M-S8) ──
83
+ // Replaces the triple scan of this.inodes on every /api/stats poll.
84
+ // Bootstrapped in loadInodes(); maintained at every mutator entry:
85
+ // mkdir/rmdir/writeFile/unlink/writeBatch (rename is a no-op —
86
+ // same inode, new path). Invariant: these match a fresh O(N)
87
+ // walk of this.inodes. Unit-tested in the A5/B3 runtime tests.
88
+ _totalFiles = 0;
89
+ _totalDirs = 0;
90
+ _usedBytes = 0;
91
+ // ── Deferred write queue (do86 pattern) ───────────────────────────────
92
+ pendingWrites = new Map();
93
+ /**
94
+ * Sum of `data.length` across pendingWrites entries. Maintained
95
+ * in lockstep with the Map by every code path that mutates it
96
+ * (deferWrite/clearPendingWritesForPath/flushPendingWrites/
97
+ * cleanupAfterDelete). Read by getStats() so /api/_diag/memory's
98
+ * `heap.breakdown.vfsInFlightBytes` is no longer a hardcoded 0.
99
+ * (N3, heap-correctness wave.)
100
+ */
101
+ _pendingWriteBytes = 0;
102
+ /**
103
+ * N2 (heap-correctness wave). Sum of bytes currently held in the
104
+ * `chunks: []` spool inside writeStream(). Maintained per-chunk so
105
+ * a long-running drain shows live, not the steady-state 0.
106
+ * Reset (or decremented to the drained amount) inside writeStream's
107
+ * finally block, after transactionSync has either consumed the
108
+ * bytes or thrown.
109
+ *
110
+ * Sums into `heap.breakdown.vfsInFlightBytes` alongside
111
+ * _pendingWriteBytes so a single value reflects ALL transient
112
+ * write-bytes the supervisor is holding.
113
+ */
114
+ _writeStreamSpoolBytes = 0;
115
+ writeFlushScheduled = false;
116
+ // ── Write-failure tracking (audit C1) ─────────────────────────────────
117
+ // When a deferred flush fails, the entry lands here so (a) flushAll()
118
+ // can throw with accurate context on the next forced flush, (b) the
119
+ // supervisor can surface the error to the user's terminal via the
120
+ // onWriteError subscription, and (c) flushAndWait() can report.
121
+ //
122
+ // Keys use cacheKey(path, chunkId). Retry count bounds backoff to one
123
+ // additional attempt (per audit recommendation: "re-queue on transient
124
+ // SQL errors once"); entries that fail twice are considered lost.
125
+ //
126
+ // NOTE: we intentionally do NOT keep the chunk bytes on this record.
127
+ // The retry happens inline inside flushPendingWrites — nothing
128
+ // re-reads the bytes after that. Storing them would turn a bad
129
+ // session into a multi-MB leak (64 KB per chunk) inside a DO with
130
+ // a ~128 MB isolate cap.
131
+ failedWrites = new Map();
132
+ writeErrorHandlers = new Set();
133
+ _writeFailures = 0;
134
+ // ── Stats ─────────────────────────────────────────────────────────────
135
+ _cacheHits = 0;
136
+ _cacheMisses = 0;
137
+ _evictions = 0;
138
+ _sqlReads = 0;
139
+ _sqlWrites = 0;
140
+ _batchWrites = 0;
141
+ _batchWriteRows = 0;
142
+ constructor(sql, ctx) {
143
+ this.sql = sql;
144
+ this.ctx = ctx;
145
+ this.events = new VfsEventEmitter();
146
+ this.initSchema();
147
+ this.loadInodes();
148
+ }
149
+ // ── Schema ────────────────────────────────────────────────────────────
150
+ initSchema() {
151
+ // Migrate from legacy fs_objects table FIRST (before creating new tables)
152
+ this.migrateFromLegacy();
153
+ this.sql.exec(`CREATE TABLE IF NOT EXISTS inodes (
154
+ path TEXT PRIMARY KEY,
155
+ parent_path TEXT NOT NULL DEFAULT '',
156
+ is_dir INTEGER NOT NULL DEFAULT 0,
157
+ size INTEGER NOT NULL DEFAULT 0,
158
+ mtime INTEGER NOT NULL DEFAULT 0,
159
+ mode INTEGER NOT NULL DEFAULT 0,
160
+ chunk_count INTEGER NOT NULL DEFAULT 0
161
+ )`);
162
+ this.sql.exec(`CREATE INDEX IF NOT EXISTS idx_inodes_parent ON inodes(parent_path)`);
163
+ this.sql.exec(`CREATE TABLE IF NOT EXISTS file_chunks (
164
+ path TEXT NOT NULL,
165
+ chunk_id INTEGER NOT NULL,
166
+ data BLOB NOT NULL,
167
+ PRIMARY KEY (path, chunk_id)
168
+ )`);
169
+ // Ensure chunk_count column exists (handles upgrade from older schema)
170
+ try {
171
+ this.sql.exec("SELECT chunk_count FROM inodes LIMIT 0");
172
+ }
173
+ catch {
174
+ try {
175
+ this.sql.exec("ALTER TABLE inodes ADD COLUMN chunk_count INTEGER NOT NULL DEFAULT 0");
176
+ }
177
+ catch { }
178
+ }
179
+ }
180
+ migrateFromLegacy() {
181
+ // Check if old fs_objects table exists
182
+ const rows = [...this.sql.exec("SELECT name FROM sqlite_master WHERE type='table' AND name='fs_objects'")];
183
+ if (rows.length === 0)
184
+ return;
185
+ console.log('[sqlite-vfs] Migrating from legacy fs_objects table...');
186
+ const oldRows = [...this.sql.exec("SELECT path, chunk_index, parent_path, data, is_dir, size, mtime, mode FROM fs_objects ORDER BY path, chunk_index")];
187
+ // Group by path, extract inodes and chunks
188
+ const seenPaths = new Set();
189
+ for (const row of oldRows) {
190
+ const path = String(row.path);
191
+ const chunkIndex = Number(row.chunk_index);
192
+ const parentPath = String(row.parent_path);
193
+ const isDir = Number(row.is_dir) === 1;
194
+ const size = Number(row.size);
195
+ const mtime = Number(row.mtime);
196
+ const mode = Number(row.mode);
197
+ if (!seenPaths.has(path)) {
198
+ seenPaths.add(path);
199
+ const chunkCount = isDir ? 0 : Math.max(1, Math.ceil(size / CHUNK_SIZE));
200
+ this.sql.exec("INSERT OR REPLACE INTO inodes (path, parent_path, is_dir, size, mtime, mode, chunk_count) VALUES (?, ?, ?, ?, ?, ?, ?)", path, parentPath, isDir ? 1 : 0, size, mtime, mode, chunkCount);
201
+ }
202
+ if (!isDir && row.data != null) {
203
+ const data = this.blobToUint8Array(row.data);
204
+ if (data.length > 0) {
205
+ // Re-chunk: old CHUNK_SIZE was 1.8MB, new is 64KB
206
+ if (chunkIndex === 0 && data.length <= CHUNK_SIZE) {
207
+ // Small file, single chunk — direct insert
208
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", path, 0, data);
209
+ this.sql.exec("UPDATE inodes SET chunk_count = 1 WHERE path = ?", path);
210
+ }
211
+ else {
212
+ // Large file: re-chunk with 64KB chunks
213
+ // For multi-chunk old files, accumulate data then re-chunk
214
+ // This is simplified: we re-chunk the data we have
215
+ const numNewChunks = Math.ceil(data.length / CHUNK_SIZE);
216
+ for (let i = 0; i < numNewChunks; i++) {
217
+ // H10: subarray view; SQLite copies the bytes through
218
+ // its blob-bind boundary so retention ends at exec().
219
+ const chunk = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
220
+ // Offset chunk IDs by the old chunk's position in the new scheme
221
+ const oldStart = chunkIndex * 1_800_000; // old chunk size
222
+ const newBaseChunk = Math.floor(oldStart / CHUNK_SIZE);
223
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", path, newBaseChunk + i, chunk);
224
+ }
225
+ // Update chunk count based on total file size
226
+ const totalChunks = Math.ceil(size / CHUNK_SIZE);
227
+ this.sql.exec("UPDATE inodes SET chunk_count = ? WHERE path = ?", totalChunks, path);
228
+ }
229
+ }
230
+ }
231
+ }
232
+ // Drop old table
233
+ this.sql.exec("DROP TABLE IF EXISTS fs_objects");
234
+ console.log(`[sqlite-vfs] Migration complete: ${seenPaths.size} entries migrated.`);
235
+ }
236
+ // ── INode loading ─────────────────────────────────────────────────────
237
+ loadInodes() {
238
+ this.inodes.clear();
239
+ this.children.clear();
240
+ // Reset counters before rescanning (B3).
241
+ this._totalFiles = 0;
242
+ this._totalDirs = 0;
243
+ this._usedBytes = 0;
244
+ const rows = [...this.sql.exec("SELECT path, parent_path, is_dir, size, mtime, mode, chunk_count FROM inodes")];
245
+ for (const row of rows) {
246
+ const inode = {
247
+ path: String(row.path),
248
+ parentPath: String(row.parent_path),
249
+ isDir: Number(row.is_dir) === 1,
250
+ size: Number(row.size),
251
+ mtime: Number(row.mtime),
252
+ mode: Number(row.mode),
253
+ chunkCount: Number(row.chunk_count),
254
+ };
255
+ this.inodes.set(inode.path, inode);
256
+ this._addToChildrenIndex(inode.parentPath, inode.path);
257
+ // Bootstrap the counters (B3).
258
+ if (inode.isDir)
259
+ this._totalDirs++;
260
+ else {
261
+ this._totalFiles++;
262
+ this._usedBytes += inode.size;
263
+ }
264
+ }
265
+ }
266
+ _addToChildrenIndex(parentPath, childPath) {
267
+ let set = this.children.get(parentPath);
268
+ if (!set) {
269
+ set = new Set();
270
+ this.children.set(parentPath, set);
271
+ }
272
+ set.add(childPath);
273
+ }
274
+ _removeFromChildrenIndex(parentPath, childPath) {
275
+ const set = this.children.get(parentPath);
276
+ if (set) {
277
+ set.delete(childPath);
278
+ if (set.size === 0)
279
+ this.children.delete(parentPath);
280
+ }
281
+ }
282
+ // ── Cache key ─────────────────────────────────────────────────────────
283
+ cacheKey(path, chunkId) {
284
+ return `${path}\0${chunkId}`;
285
+ }
286
+ // ── LRU Content Cache ─────────────────────────────────────────────────
287
+ cacheGet(path, chunkId) {
288
+ const key = this.cacheKey(path, chunkId);
289
+ const entry = this.cache.get(key);
290
+ if (entry) {
291
+ this._cacheHits++;
292
+ // Move to MRU position
293
+ this.cache.delete(key);
294
+ this.cache.set(key, entry);
295
+ return entry.data;
296
+ }
297
+ this._cacheMisses++;
298
+ return null;
299
+ }
300
+ cacheSet(path, chunkId, data, dirty) {
301
+ const key = this.cacheKey(path, chunkId);
302
+ // If already cached, update
303
+ const existing = this.cache.get(key);
304
+ if (existing) {
305
+ this._cacheBytes -= existing.data.length;
306
+ existing.data = data;
307
+ existing.dirty = existing.dirty || dirty;
308
+ this._cacheBytes += data.length;
309
+ // Move to MRU
310
+ this.cache.delete(key);
311
+ this.cache.set(key, existing);
312
+ return;
313
+ }
314
+ // Evict if at capacity. W5 Lever 8: read the runtime-mutable
315
+ // _lruMaxEntries instead of the constant so shrinkForInstall()
316
+ // takes effect for in-flight writes too.
317
+ while (this.cache.size >= this._lruMaxEntries) {
318
+ this.evictOne();
319
+ }
320
+ this._cacheBytes += data.length;
321
+ this.cache.set(key, { path, chunkId, data, dirty });
322
+ }
323
+ evictOne() {
324
+ // Evict the LRU entry (first in Map iteration order)
325
+ const firstKey = this.cache.keys().next().value;
326
+ if (firstKey === undefined)
327
+ return;
328
+ const entry = this.cache.get(firstKey);
329
+ this.cache.delete(firstKey);
330
+ this._cacheBytes -= entry.data.length;
331
+ this._evictions++;
332
+ if (entry.dirty) {
333
+ this.deferWrite(entry.path, entry.chunkId, entry.data);
334
+ }
335
+ }
336
+ // ── W5 Lever 8: public LRU shrink / restore + evictAll ───────────────
337
+ //
338
+ // shrinkForInstall(targetEntries): tighten the cap so heavy-alloc
339
+ // owners (npm install / git clone / pre-bundle) free heap headroom
340
+ // for in-flight RPC payloads + pending-writes queue. Refcount-based
341
+ // so nested heavy-alloc owners (e.g. concurrent install + clone)
342
+ // don't race; only the OUTERMOST restoreAfterInstall() raises the
343
+ // cap back to LRU_MAX_ENTRIES.
344
+ //
345
+ // Default target 128 entries × 64 KB = 8 MiB. Matches
346
+ // CF-INTERNAL-OPTIMIZATION-RESEARCH.md J.1.2.
347
+ //
348
+ // Eviction during shrink flows through deferWrite → flushPendingWrites
349
+ // (existing path), so no data loss. Cold-cache bounce for the next
350
+ // reads of evicted pages is acceptable since install workloads
351
+ // write-once-and-rarely-reread.
352
+ shrinkForInstall(targetEntries = 128) {
353
+ const target = Math.max(1, Math.min(LRU_MAX_ENTRIES, targetEntries | 0));
354
+ // Refcount: nested acquires stack. Take the smallest target across
355
+ // owners — most aggressive shrinker wins.
356
+ if (this._lruShrinkRefcount > 0) {
357
+ if (target < this._lruMaxEntries)
358
+ this._lruMaxEntries = target;
359
+ this._lruShrinkRefcount++;
360
+ return;
361
+ }
362
+ this._lruShrinkRefcount = 1;
363
+ this._lruMaxEntries = target;
364
+ // Evict down to the new cap. Each evictOne() flushes the dirty
365
+ // entry (if any) via deferWrite; queueMicrotask schedules the
366
+ // flush. Stays sync — preserves the sqlite-vfs invariant.
367
+ while (this.cache.size > this._lruMaxEntries) {
368
+ this.evictOne();
369
+ }
370
+ }
371
+ /** Decrement the heavy-alloc refcount. When the count returns to
372
+ * zero, restore the cap to LRU_MAX_ENTRIES. No re-population —
373
+ * the cache warms naturally on next reads. */
374
+ restoreAfterInstall() {
375
+ if (this._lruShrinkRefcount <= 0)
376
+ return;
377
+ this._lruShrinkRefcount--;
378
+ if (this._lruShrinkRefcount === 0) {
379
+ this._lruMaxEntries = LRU_MAX_ENTRIES;
380
+ }
381
+ }
382
+ /**
383
+ * Drop EVERY cache entry, flushing dirty ones via deferWrite. Used
384
+ * by the W5 Lever 9 SQLITE_NOMEM retry path to free pages owned by
385
+ * us before retrying a smaller batch. Sync; safe inside the input
386
+ * gate.
387
+ */
388
+ evictAll() {
389
+ // Iterate a snapshot so concurrent mutation through deferWrite
390
+ // doesn't disturb iteration.
391
+ const keys = Array.from(this.cache.keys());
392
+ for (const key of keys) {
393
+ const entry = this.cache.get(key);
394
+ if (!entry)
395
+ continue;
396
+ this.cache.delete(key);
397
+ this._cacheBytes -= entry.data.length;
398
+ this._evictions++;
399
+ if (entry.dirty) {
400
+ this.deferWrite(entry.path, entry.chunkId, entry.data);
401
+ }
402
+ }
403
+ }
404
+ /**
405
+ * Invalidate all cache entries for a path.
406
+ * @param discard If true, dirty entries are discarded (not flushed).
407
+ * Use discard=true when the file is about to be overwritten or deleted.
408
+ */
409
+ cacheInvalidate(path, discard = false) {
410
+ const toDelete = [];
411
+ for (const [key, entry] of this.cache) {
412
+ if (entry.path === path) {
413
+ if (entry.dirty && !discard) {
414
+ this.deferWrite(entry.path, entry.chunkId, entry.data);
415
+ }
416
+ this._cacheBytes -= entry.data.length;
417
+ toDelete.push(key);
418
+ }
419
+ }
420
+ for (const key of toDelete) {
421
+ this.cache.delete(key);
422
+ }
423
+ }
424
+ /** Remove all pending writes for a path (prevents orphan chunks). */
425
+ clearPendingWritesForPath(path) {
426
+ const toRemove = [];
427
+ for (const [key, entry] of this.pendingWrites) {
428
+ if (entry.path === path)
429
+ toRemove.push(key);
430
+ }
431
+ for (const key of toRemove) {
432
+ const e = this.pendingWrites.get(key);
433
+ if (e)
434
+ this._pendingWriteBytes -= e.data.length;
435
+ this.pendingWrites.delete(key);
436
+ }
437
+ }
438
+ /**
439
+ * Batch version of cacheInvalidate — invalidate every cache entry
440
+ * whose path is in `paths`. One pass over the cache instead of one
441
+ * pass per path (audit R2: writeBatch was O(P × C) before this).
442
+ *
443
+ * `discard` semantics match cacheInvalidate(path, discard): when
444
+ * false, dirty entries are re-queued for persistence before being
445
+ * dropped from the cache; when true (the writeBatch case — the row
446
+ * is about to be overwritten), dirty data is abandoned.
447
+ */
448
+ cacheInvalidateBatch(paths, discard = false) {
449
+ if (paths.size === 0)
450
+ return;
451
+ const toDelete = [];
452
+ for (const [key, entry] of this.cache) {
453
+ if (!paths.has(entry.path))
454
+ continue;
455
+ if (entry.dirty && !discard) {
456
+ this.deferWrite(entry.path, entry.chunkId, entry.data);
457
+ }
458
+ this._cacheBytes -= entry.data.length;
459
+ toDelete.push(key);
460
+ }
461
+ for (const key of toDelete) {
462
+ this.cache.delete(key);
463
+ }
464
+ }
465
+ /** Batch version of clearPendingWritesForPath — one pass for N paths. */
466
+ clearPendingWritesForPaths(paths) {
467
+ if (paths.size === 0)
468
+ return;
469
+ const toRemove = [];
470
+ for (const [key, entry] of this.pendingWrites) {
471
+ if (paths.has(entry.path))
472
+ toRemove.push(key);
473
+ }
474
+ for (const key of toRemove) {
475
+ const e = this.pendingWrites.get(key);
476
+ if (e)
477
+ this._pendingWriteBytes -= e.data.length;
478
+ this.pendingWrites.delete(key);
479
+ }
480
+ }
481
+ // ── Deferred batch writes (do86 pattern) ──────────────────────────────
482
+ deferWrite(path, chunkId, data) {
483
+ const key = this.cacheKey(path, chunkId);
484
+ // SINGLE-OWNERSHIP BOUNDARY (H10 audit, heap-correctness wave).
485
+ //
486
+ // `data` is one of:
487
+ // - a `subarray` view into the caller's source ArrayBuffer
488
+ // (writeFile post-H10), or
489
+ // - a fresh Uint8Array (writeBatch chunks come pre-decoded), or
490
+ // - any caller-supplied buffer.
491
+ //
492
+ // The pendingWrites entry MUST own its bytes outright — until
493
+ // flush, the supervisor must be free to drop the caller's ref
494
+ // without losing the not-yet-persisted data. So we materialise a
495
+ // fresh ArrayBuffer here. This is the ONE copy on the writeFile
496
+ // path; before the H10 fix it was the second of THREE copies
497
+ // (slice + this) per chunk.
498
+ //
499
+ // _pendingWriteBytes counter is the source-of-truth for N3
500
+ // (heap-correctness wave): the `vfsInFlightBytes` heap-estimator
501
+ // contributor reads from it.
502
+ const copy = new Uint8Array(data.length);
503
+ copy.set(data);
504
+ // Overwrite-correctness for the byte counter: if a prior entry
505
+ // exists at this key (re-write of the same chunk before flush),
506
+ // subtract its bytes BEFORE replacing.
507
+ const prior = this.pendingWrites.get(key);
508
+ if (prior)
509
+ this._pendingWriteBytes -= prior.data.length;
510
+ this.pendingWrites.set(key, { path, chunkId, data: copy });
511
+ this._pendingWriteBytes += copy.length;
512
+ // Throttle: if too many writes pending, flush synchronously.
513
+ // 500 threshold balances memory (500 × ~64KB = ~32MB max pending) vs throughput
514
+ // (fewer flushes = faster git clone / npm install).
515
+ if (this.pendingWrites.size >= 500) {
516
+ this.flushPendingWrites();
517
+ return;
518
+ }
519
+ if (!this.writeFlushScheduled) {
520
+ this.writeFlushScheduled = true;
521
+ queueMicrotask(() => this.flushPendingWrites());
522
+ }
523
+ }
524
+ flushPendingWrites() {
525
+ this.writeFlushScheduled = false;
526
+ if (this.pendingWrites.size === 0)
527
+ return;
528
+ const entries = Array.from(this.pendingWrites.values());
529
+ // The bytes are about to leave pendingWrites (either land in SQL
530
+ // on the transactionSync below, or move to failedWrites without
531
+ // their data — see line 171 comment). Either way, _pendingWriteBytes
532
+ // drops to 0 here. We do NOT subtract per-entry; clearing in one
533
+ // shot is the cheapest correct accounting (and matches the .clear
534
+ // below).
535
+ this._pendingWriteBytes = 0;
536
+ this.pendingWrites.clear();
537
+ this._batchWrites++;
538
+ // First attempt: batch inside transactionSync. Inner per-row
539
+ // try/catch captures row-level failures without aborting the
540
+ // transaction — those rows go to retry.
541
+ //
542
+ // Counter accounting: we do NOT increment _sqlWrites / _batchWriteRows
543
+ // inside the transaction block. If transactionSync later throws
544
+ // (rollback), every row it "wrote" is reverted — but the counter
545
+ // would already have been bumped. Instead, we count confirmed
546
+ // writes after each path completes.
547
+ const rowFailures = [];
548
+ let transactionFailed = null;
549
+ let confirmedRows = 0;
550
+ try {
551
+ if (this.ctx?.storage?.transactionSync) {
552
+ this.ctx.storage.transactionSync(() => {
553
+ for (const entry of entries) {
554
+ try {
555
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", entry.path, entry.chunkId, entry.data);
556
+ }
557
+ catch (e) {
558
+ rowFailures.push({ entry, error: e?.message || String(e) });
559
+ }
560
+ }
561
+ });
562
+ // Transaction committed. Rows that didn't hit the inner catch
563
+ // landed on disk.
564
+ confirmedRows += entries.length - rowFailures.length;
565
+ }
566
+ else {
567
+ // Fallback: individual writes without transaction (slower but safe).
568
+ // Each successful exec() is its own commit, so counters are
569
+ // safe to bump inline here.
570
+ for (const entry of entries) {
571
+ try {
572
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", entry.path, entry.chunkId, entry.data);
573
+ confirmedRows++;
574
+ }
575
+ catch (e) {
576
+ rowFailures.push({ entry, error: e?.message || String(e) });
577
+ }
578
+ }
579
+ }
580
+ }
581
+ catch (e) {
582
+ // transactionSync itself threw (rollback). Every entry in the
583
+ // batch is now un-written — retry them individually so one bad
584
+ // row doesn't poison the whole batch. rowFailures entries from
585
+ // before the rollback are discarded; the retry covers them too.
586
+ transactionFailed = e?.message || String(e);
587
+ rowFailures.length = 0;
588
+ }
589
+ // Retry path (audit C1: "re-queue on transient SQL errors once").
590
+ // If the transaction aborted, retry every entry individually without
591
+ // a transaction wrapper. Row-level failures from the successful-
592
+ // transaction path also get one retry here in case the failure was
593
+ // transient (e.g. a transient constraint conflict resolved by a
594
+ // concurrent write that completed between our attempts).
595
+ if (transactionFailed !== null) {
596
+ for (const entry of entries) {
597
+ try {
598
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", entry.path, entry.chunkId, entry.data);
599
+ confirmedRows++;
600
+ }
601
+ catch (e) {
602
+ this._recordFailedWrite(entry, e?.message || String(e), 2);
603
+ }
604
+ }
605
+ }
606
+ else if (rowFailures.length > 0) {
607
+ for (const { entry, error: firstError } of rowFailures) {
608
+ try {
609
+ this.sql.exec("INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES (?, ?, ?)", entry.path, entry.chunkId, entry.data);
610
+ confirmedRows++;
611
+ }
612
+ catch (e) {
613
+ this._recordFailedWrite(entry, `${firstError}; retry: ${e?.message || String(e)}`, 2);
614
+ }
615
+ }
616
+ }
617
+ // Single accounting update for the whole flush. Reflects rows that
618
+ // actually committed (via either the first attempt or the retry).
619
+ this._sqlWrites += confirmedRows;
620
+ this._batchWriteRows += confirmedRows;
621
+ }
622
+ /**
623
+ * Move an un-writable chunk into failedWrites and notify subscribers.
624
+ * Called from the retry path of flushPendingWrites(). Entries recorded
625
+ * here are the ones that failed BOTH the original attempt and the
626
+ * one-shot retry; they are considered lost (we do not re-queue a
627
+ * third time — the audit recommendation was a single retry). The
628
+ * chunk bytes are NOT retained — see failedWrites comment above.
629
+ */
630
+ _recordFailedWrite(entry, error, attempts) {
631
+ const key = this.cacheKey(entry.path, entry.chunkId);
632
+ // Defensive cap: a session that writes to a broken SQLite could
633
+ // otherwise grow failedWrites without bound between flushAll calls.
634
+ // 1000 entries × ~120 B = ~120 KB ceiling. The cumulative
635
+ // _writeFailures counter still grows so observers can see drops.
636
+ if (!this.failedWrites.has(key) && this.failedWrites.size >= 1000) {
637
+ // Drop the oldest recorded failure to make room; a 1000-deep
638
+ // failure queue already signals a serious problem.
639
+ const oldest = this.failedWrites.keys().next().value;
640
+ if (oldest !== undefined)
641
+ this.failedWrites.delete(oldest);
642
+ }
643
+ this.failedWrites.set(key, {
644
+ path: entry.path, chunkId: entry.chunkId,
645
+ error, attempts,
646
+ });
647
+ this._writeFailures++;
648
+ // Surface to subscribers so the supervisor can, e.g., write to the
649
+ // user's terminal. Handler errors are swallowed — they mustn't
650
+ // affect the flush path (called from a microtask).
651
+ const payload = { path: entry.path, chunkId: entry.chunkId, error, attempts };
652
+ for (const handler of this.writeErrorHandlers) {
653
+ try {
654
+ handler(payload);
655
+ }
656
+ catch { /* handler is last line of defense */ }
657
+ }
658
+ console.error('[sqlite-vfs] write permanently failed for', entry.path, 'chunk', entry.chunkId, 'after', attempts, 'attempts:', error);
659
+ }
660
+ /**
661
+ * Subscribe to write failures. Fires once per chunk that failed both
662
+ * its first attempt AND the one-shot retry. Returns an unsubscribe
663
+ * function. Multiple subscribers are permitted.
664
+ *
665
+ * Handlers run synchronously inside the flush microtask. Keep them
666
+ * cheap and non-throwing; errors thrown by a handler are caught and
667
+ * discarded so one bad subscriber can't break the flush path.
668
+ */
669
+ onWriteError(handler) {
670
+ this.writeErrorHandlers.add(handler);
671
+ return () => { this.writeErrorHandlers.delete(handler); };
672
+ }
673
+ /**
674
+ * Snapshot of currently-recorded write failures. Intended for
675
+ * diagnostics (e.g. /api/stats). The underlying Map is not exposed
676
+ * so external code can't accidentally mutate it.
677
+ */
678
+ getWriteFailures() {
679
+ return Array.from(this.failedWrites.values()).map(f => ({
680
+ path: f.path, chunkId: f.chunkId, error: f.error, attempts: f.attempts,
681
+ }));
682
+ }
683
+ /**
684
+ * Clear recorded failures. Callers that have recovered (e.g. retried
685
+ * the user-facing operation, or logged the error and decided to move
686
+ * on) can call this to reset the counter so flushAll() stops
687
+ * throwing. Without this, a single poisoned chunk would make every
688
+ * subsequent flushAll() throw forever.
689
+ */
690
+ clearWriteFailures() {
691
+ const n = this.failedWrites.size;
692
+ this.failedWrites.clear();
693
+ return n;
694
+ }
695
+ /**
696
+ * Force flush all dirty cache entries and pending writes to SQLite.
697
+ *
698
+ * Throws if any chunk failed both its first attempt AND the one-shot
699
+ * retry during this or any previous flush in this DO's lifetime.
700
+ * Callers that invoke flushAll() on a critical boundary (e.g.
701
+ * webSocketClose) get a synchronous error signal; callers that don't
702
+ * want to assert cleanliness should use flushAndWait() instead.
703
+ *
704
+ * Staying synchronous preserves the sqlite-vfs invariant that all
705
+ * file ops are sync (documented at the top of this file) — required
706
+ * by the LIFO @lifo-sh/core MountProvider interface.
707
+ */
708
+ flushAll() {
709
+ // Flush cache dirty entries
710
+ for (const [, entry] of this.cache) {
711
+ if (entry.dirty) {
712
+ this.deferWrite(entry.path, entry.chunkId, entry.data);
713
+ entry.dirty = false;
714
+ }
715
+ }
716
+ // Flush pending writes synchronously
717
+ this.flushPendingWrites();
718
+ if (this.failedWrites.size > 0) {
719
+ // Build a concise error message that names the first few paths
720
+ // so the operator can triage. Full list is available via
721
+ // getWriteFailures().
722
+ const first = Array.from(this.failedWrites.values()).slice(0, 3);
723
+ const preview = first.map(f => `${f.path}#${f.chunkId} (${f.error})`).join('; ');
724
+ const remaining = this.failedWrites.size > first.length
725
+ ? ` +${this.failedWrites.size - first.length} more`
726
+ : '';
727
+ throw new Error(`[sqlite-vfs] flushAll: ${this.failedWrites.size} write(s) failed permanently: ${preview}${remaining}`);
728
+ }
729
+ }
730
+ /**
731
+ * Force-flush and resolve only after the flush completes with no
732
+ * recorded failures. Rejects with the same error shape as flushAll()
733
+ * when one or more chunks are un-writable after retry.
734
+ *
735
+ * Use this at explicit persistence boundaries (e.g. end of
736
+ * `npm install`, end of `git clone`, seed-filesystem completion) to
737
+ * guarantee data landed. Synchronous `writeFile()` callers that
738
+ * don't opt in continue to get best-effort semantics — the audit's
739
+ * alternative fix path (keep void API, surface errors through
740
+ * onWriteError + throwing flushAll).
741
+ */
742
+ async flushAndWait() {
743
+ // queueMicrotask-scheduled flush may be in flight. Await a microtask
744
+ // turn before checking + forcing a flush so we don't double-run.
745
+ await Promise.resolve();
746
+ this.flushAll(); // throws on failures
747
+ }
748
+ // ── Helpers ───────────────────────────────────────────────────────────
749
+ now() { return Date.now(); }
750
+ parentPath(path) {
751
+ return path.includes('/') ? path.substring(0, path.lastIndexOf('/')) : '';
752
+ }
753
+ blobToUint8Array(blob) {
754
+ if (blob instanceof Uint8Array)
755
+ return blob;
756
+ if (blob instanceof ArrayBuffer)
757
+ return new Uint8Array(blob);
758
+ if (ArrayBuffer.isView(blob))
759
+ return new Uint8Array(blob.buffer, blob.byteOffset, blob.byteLength);
760
+ return new Uint8Array(0);
761
+ }
762
+ readChunkFromSql(path, chunkId) {
763
+ // Check pending writes first (do86 pattern: avoid stale reads)
764
+ const key = this.cacheKey(path, chunkId);
765
+ const pending = this.pendingWrites.get(key);
766
+ if (pending)
767
+ return pending.data;
768
+ this._sqlReads++;
769
+ const rows = [...this.sql.exec("SELECT data FROM file_chunks WHERE path = ? AND chunk_id = ?", path, chunkId)];
770
+ if (rows.length === 0)
771
+ return null;
772
+ return this.blobToUint8Array(rows[0].data);
773
+ }
774
+ // ── Filesystem operations ─────────────────────────────────────────────
775
+ exists(path) {
776
+ return this.inodes.has(path);
777
+ }
778
+ isDirectory(path) {
779
+ const inode = this.inodes.get(path);
780
+ return inode !== undefined && inode.isDir;
781
+ }
782
+ isFile(path) {
783
+ const inode = this.inodes.get(path);
784
+ return inode !== undefined && !inode.isDir;
785
+ }
786
+ mkdir(path, options) {
787
+ if (this.exists(path))
788
+ return;
789
+ if (options?.recursive) {
790
+ const parts = path.split('/').filter(Boolean);
791
+ let current = '';
792
+ for (const part of parts) {
793
+ current = current ? current + '/' + part : part;
794
+ if (!this.exists(current)) {
795
+ this._mkdirSingle(current);
796
+ }
797
+ }
798
+ }
799
+ else {
800
+ this._mkdirSingle(path);
801
+ }
802
+ }
803
+ _mkdirSingle(path) {
804
+ const pp = this.parentPath(path);
805
+ const mtime = this.now();
806
+ this.sql.exec("INSERT OR REPLACE INTO inodes (path, parent_path, is_dir, size, mtime, mode, chunk_count) VALUES (?, ?, 1, 0, ?, ?, 0)", path, pp, mtime, 0o755);
807
+ const inode = { path, parentPath: pp, isDir: true, size: 0, mtime, mode: 0o755, chunkCount: 0 };
808
+ this.inodes.set(path, inode);
809
+ this._addToChildrenIndex(pp, path);
810
+ this._totalDirs++; // B3
811
+ this.events.emit('addDir', path);
812
+ }
813
+ writeFile(path, content) {
814
+ const data = typeof content === 'string' ? enc.encode(content) : content;
815
+ const pp = this.parentPath(path);
816
+ const mtime = this.now();
817
+ const chunkCount = data.length === 0 ? 0 : Math.ceil(data.length / CHUNK_SIZE);
818
+ // Capture prior state BEFORE mutating this.inodes (B3 delta tracking).
819
+ const prior = this.inodes.get(path);
820
+ const isNew = prior === undefined;
821
+ // Invalidate old cache entries (discard=true: old data is being replaced)
822
+ if (!isNew) {
823
+ this.cacheInvalidate(path, true);
824
+ this.clearPendingWritesForPath(path);
825
+ // Delete old chunks from SQL (chunk count may differ)
826
+ this.sql.exec("DELETE FROM file_chunks WHERE path = ?", path);
827
+ }
828
+ // Write inode
829
+ this.sql.exec("INSERT OR REPLACE INTO inodes (path, parent_path, is_dir, size, mtime, mode, chunk_count) VALUES (?, ?, 0, ?, ?, ?, ?)", path, pp, data.length, mtime, 0o644, chunkCount);
830
+ const newInode = { path, parentPath: pp, isDir: false, size: data.length, mtime, mode: 0o644, chunkCount };
831
+ this.inodes.set(path, newInode);
832
+ if (isNew)
833
+ this._addToChildrenIndex(pp, path);
834
+ // B3: update running counters. Handles new file, replace file,
835
+ // and the edge case of writing a file path that previously
836
+ // belonged to a directory (writeFile replacing an old dir — rare
837
+ // but `INSERT OR REPLACE` + `this.inodes.set` would silently
838
+ // convert it; handle the count flip defensively).
839
+ if (isNew) {
840
+ this._totalFiles++;
841
+ this._usedBytes += data.length;
842
+ }
843
+ else if (prior.isDir) {
844
+ // Dir → file conversion.
845
+ this._totalDirs--;
846
+ this._totalFiles++;
847
+ this._usedBytes += data.length; // prior size was 0
848
+ }
849
+ else {
850
+ // File → file replace: delta on size, no count change.
851
+ this._usedBytes += data.length - prior.size;
852
+ }
853
+ // Write chunks to cache (clean, not dirty) and defer SQL write.
854
+ // The deferred write handles persistence; cache entry stays clean
855
+ // to avoid double-writing when the entry is eventually evicted.
856
+ //
857
+ // H10 (heap-correctness wave) — `data.slice(...)` here used to
858
+ // allocate a brand-new chunk-sized ArrayBuffer per chunk. Combined
859
+ // with `deferWrite`'s defensive copy at the persistence boundary
860
+ // (sqlite-vfs.ts:_deferWriteCopyForOwnership), the loop transient
861
+ // peaked at 3× the source size:
862
+ //
863
+ // source AB (caller's content) 1 × N
864
+ // + per-chunk slice copies 1 × N
865
+ // + per-chunk deferWrite copies 1 × N
866
+ // = 3 × N
867
+ //
868
+ // `subarray` returns a VIEW into the source AB — zero allocation,
869
+ // zero copy. The subarray pins the source AB until the last view
870
+ // (in cache + in pendingWrites) is dropped, but that is exactly
871
+ // the same retention the caller already had via their `content`
872
+ // reference. We don't INCREASE pinning; we just stop creating
873
+ // redundant copies that the GC immediately has to reclaim.
874
+ //
875
+ // Single-ownership boundary moved to `deferWrite`: it still does
876
+ // ONE copy into a fresh ArrayBuffer at the SQLite persistence
877
+ // boundary so pendingWrites entries don't accidentally extend
878
+ // the lifetime of the caller's source.
879
+ if (data.length === 0) {
880
+ // Empty file: no chunks to write
881
+ }
882
+ else if (data.length <= CHUNK_SIZE) {
883
+ this.cacheSet(path, 0, data, false);
884
+ this.deferWrite(path, 0, data);
885
+ }
886
+ else {
887
+ for (let i = 0; i < chunkCount; i++) {
888
+ const chunk = data.subarray(i * CHUNK_SIZE, (i + 1) * CHUNK_SIZE);
889
+ this.cacheSet(path, i, chunk, false);
890
+ this.deferWrite(path, i, chunk);
891
+ }
892
+ }
893
+ this.events.emit(isNew ? 'add' : 'change', path);
894
+ }
895
+ readFile(path) {
896
+ const inode = this.inodes.get(path);
897
+ if (!inode)
898
+ throw new Error("ENOENT: " + path);
899
+ if (inode.isDir)
900
+ throw new Error("EISDIR: " + path);
901
+ if (inode.size === 0 || inode.chunkCount === 0)
902
+ return new Uint8Array(0);
903
+ if (inode.chunkCount === 1) {
904
+ // Single chunk
905
+ const cached = this.cacheGet(path, 0);
906
+ if (cached)
907
+ return cached;
908
+ const data = this.readChunkFromSql(path, 0);
909
+ if (!data)
910
+ return new Uint8Array(0);
911
+ this.cacheSet(path, 0, data, false);
912
+ return data;
913
+ }
914
+ // Multi-chunk: reassemble
915
+ const chunks = [];
916
+ let totalRead = 0;
917
+ for (let i = 0; i < inode.chunkCount; i++) {
918
+ let chunk = this.cacheGet(path, i);
919
+ if (!chunk) {
920
+ chunk = this.readChunkFromSql(path, i);
921
+ if (chunk) {
922
+ this.cacheSet(path, i, chunk, false);
923
+ }
924
+ }
925
+ if (chunk) {
926
+ chunks.push(chunk);
927
+ totalRead += chunk.length;
928
+ }
929
+ }
930
+ if (chunks.length === 1)
931
+ return chunks[0];
932
+ const result = new Uint8Array(totalRead);
933
+ let offset = 0;
934
+ for (const c of chunks) {
935
+ result.set(c, offset);
936
+ offset += c.length;
937
+ }
938
+ return result;
939
+ }
940
+ readFileString(path) {
941
+ return dec.decode(this.readFile(path));
942
+ }
943
+ stat(path) {
944
+ const inode = this.inodes.get(path);
945
+ if (!inode)
946
+ throw new Error("ENOENT: " + path);
947
+ return {
948
+ type: inode.isDir ? 'directory' : 'file',
949
+ size: inode.size,
950
+ ctime: inode.mtime,
951
+ mtime: inode.mtime,
952
+ mode: inode.mode,
953
+ };
954
+ }
955
+ readdir(path) {
956
+ const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
957
+ const kids = this.children.get(np);
958
+ if (!kids) {
959
+ // W2.5b diagnostic: empty children-set for a directory we expected
960
+ // to be populated.
961
+ if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1') {
962
+ // eslint-disable-next-line no-console
963
+ console.warn('[sqlite-vfs/W2.5b] readdir miss path=' + np +
964
+ ' kidsUndefined=true inodeExists=' + this.inodes.has(np));
965
+ }
966
+ return [];
967
+ }
968
+ const results = [];
969
+ for (const childPath of kids) {
970
+ const inode = this.inodes.get(childPath);
971
+ if (inode) {
972
+ const name = inode.path.split('/').pop();
973
+ results.push({ name, type: inode.isDir ? 'directory' : 'file' });
974
+ }
975
+ }
976
+ // W2.5b diagnostic: if children-set has entries but readdir returns
977
+ // fewer (some entries' inodes are missing from this.inodes), log it.
978
+ // This distinguishes (a) "children index broken" from (b) "inodes
979
+ // map lost entries".
980
+ if (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1' &&
981
+ kids.size !== results.length) {
982
+ // eslint-disable-next-line no-console
983
+ console.warn('[sqlite-vfs/W2.5b] readdir size mismatch path=' + np +
984
+ ' kidsSize=' + kids.size +
985
+ ' resultsLength=' + results.length +
986
+ ' missingInodes=' + (kids.size - results.length));
987
+ }
988
+ // W2.6a: sort lexicographically. Set-insertion order tracks
989
+ // writeBatch arrival order, which under concurrent npm install
990
+ // (pLimit=3) is non-deterministic. Sorting here removes a class
991
+ // of "works on Tuesday" bugs in any consumer that walks readdir
992
+ // results — buildPrefetchBundle, buildManifest, the kernel-VFS
993
+ // mount layer, etc. Cost is O(n log n) on dirs that already cost
994
+ // O(n) to assemble; negligible for typical npm package depths.
995
+ results.sort((a, b) => a.name < b.name ? -1 : a.name > b.name ? 1 : 0);
996
+ return results;
997
+ }
998
+ unlink(path) {
999
+ const inode = this.inodes.get(path);
1000
+ this.cacheInvalidate(path, true);
1001
+ this.clearPendingWritesForPath(path);
1002
+ this.sql.exec("DELETE FROM file_chunks WHERE path = ?", path);
1003
+ this.sql.exec("DELETE FROM inodes WHERE path = ?", path);
1004
+ if (inode) {
1005
+ this._removeFromChildrenIndex(inode.parentPath, path);
1006
+ // B3: running counters. unlink of a dir shouldn't happen (that's
1007
+ // rmdir's job) but guard defensively against the count flipping.
1008
+ if (inode.isDir)
1009
+ this._totalDirs--;
1010
+ else {
1011
+ this._totalFiles--;
1012
+ this._usedBytes -= inode.size;
1013
+ }
1014
+ }
1015
+ this.inodes.delete(path);
1016
+ this.events.emit('unlink', path);
1017
+ }
1018
+ rmdir(path) {
1019
+ const np = path.replace(/^\/+/, '').replace(/\/+$/, '');
1020
+ // Check if empty using children index (O(1) instead of O(N))
1021
+ const kids = this.children.get(np);
1022
+ if (kids && kids.size > 0) {
1023
+ throw new Error("ENOTEMPTY: " + path);
1024
+ }
1025
+ const inode = this.inodes.get(np);
1026
+ this.sql.exec("DELETE FROM inodes WHERE path = ?", np);
1027
+ if (inode) {
1028
+ this._removeFromChildrenIndex(inode.parentPath, np);
1029
+ // B3: running counters. Rmdir targets a dir; a non-dir entry here
1030
+ // would be a caller bug. Still, match the actual inode state.
1031
+ if (inode.isDir)
1032
+ this._totalDirs--;
1033
+ else {
1034
+ this._totalFiles--;
1035
+ this._usedBytes -= inode.size;
1036
+ }
1037
+ }
1038
+ this.inodes.delete(np);
1039
+ this.children.delete(np); // Remove empty children set
1040
+ this.events.emit('unlinkDir', np);
1041
+ }
1042
+ rename(oldPath, newPath) {
1043
+ // Flush pending writes for old path first to avoid orphans
1044
+ this.cacheInvalidate(oldPath, false); // flush dirty, not discard
1045
+ this.flushPendingWrites(); // synchronously flush so SQL paths are current
1046
+ this.clearPendingWritesForPath(oldPath);
1047
+ const newPp = this.parentPath(newPath);
1048
+ const oldPp = this.parentPath(oldPath);
1049
+ // Update inode
1050
+ const inode = this.inodes.get(oldPath);
1051
+ if (!inode)
1052
+ throw new Error("ENOENT: " + oldPath);
1053
+ // W-3 (WASI Wave-2): if newPath already exists, unlink it first so the
1054
+ // SQL UPDATE doesn't conflict on inodes.path uniqueness. POSIX rename(2)
1055
+ // overwrites the destination atomically; clang's atomic-write pattern
1056
+ // (write tmp + rename → final) depends on this. Without this branch,
1057
+ // a 2nd `make` after a prior successful build throws on UPDATE
1058
+ // failure. Pre-unlink covers both file-over-file and file-over-dir
1059
+ // (the latter is rare but POSIX permits it for empty dirs).
1060
+ const destInode = this.inodes.get(newPath);
1061
+ if (destInode) {
1062
+ if (destInode.isDir) {
1063
+ // POSIX semantics: rename onto a non-empty dir is an error
1064
+ // (ENOTEMPTY); rename onto an empty dir is allowed. We surface
1065
+ // both as a thrown error since the WASI shim treats every
1066
+ // unexpected exception as ENOSYS → caller diagnostics.
1067
+ // Conservative: refuse dir overwrite entirely; the WASI fixture
1068
+ // we care about (file → file) is the high-value case.
1069
+ if (inode.isDir) {
1070
+ throw new Error("ENOTDIR-or-EISDIR: rename onto existing dir not supported");
1071
+ }
1072
+ throw new Error("EISDIR: cannot rename file onto existing directory");
1073
+ }
1074
+ // Existing file at newPath — unlink it (drops chunks + inode row).
1075
+ this.cacheInvalidate(newPath, true /* discard */);
1076
+ this.clearPendingWritesForPath(newPath);
1077
+ this.sql.exec("DELETE FROM file_chunks WHERE path=?", newPath);
1078
+ this.sql.exec("DELETE FROM inodes WHERE path=?", newPath);
1079
+ this._removeFromChildrenIndex(newPp, newPath);
1080
+ this.inodes.delete(newPath);
1081
+ // Update running counters mirror unlink's accounting.
1082
+ try {
1083
+ this._totalFiles--;
1084
+ }
1085
+ catch { }
1086
+ }
1087
+ this.sql.exec("UPDATE inodes SET path=?, parent_path=? WHERE path=?", newPath, newPp, oldPath);
1088
+ this.sql.exec("UPDATE file_chunks SET path=? WHERE path=?", newPath, oldPath);
1089
+ // Update in-memory state
1090
+ this._removeFromChildrenIndex(oldPp, oldPath);
1091
+ this.inodes.delete(oldPath);
1092
+ inode.path = newPath;
1093
+ inode.parentPath = newPp;
1094
+ this.inodes.set(newPath, inode);
1095
+ this._addToChildrenIndex(newPp, newPath);
1096
+ // Rename children if directory
1097
+ if (inode.isDir) {
1098
+ const childPaths = [];
1099
+ for (const [p] of this.inodes) {
1100
+ if (p.startsWith(oldPath + '/'))
1101
+ childPaths.push(p);
1102
+ }
1103
+ for (const cp of childPaths) {
1104
+ const ncp = newPath + cp.substring(oldPath.length);
1105
+ const ncpp = this.parentPath(ncp);
1106
+ this.cacheInvalidate(cp, false);
1107
+ this.clearPendingWritesForPath(cp);
1108
+ this.sql.exec("UPDATE inodes SET path=?, parent_path=? WHERE path=?", ncp, ncpp, cp);
1109
+ this.sql.exec("UPDATE file_chunks SET path=? WHERE path=?", ncp, cp);
1110
+ const childInode = this.inodes.get(cp);
1111
+ const oldCpp = childInode.parentPath;
1112
+ this._removeFromChildrenIndex(oldCpp, cp);
1113
+ this.inodes.delete(cp);
1114
+ childInode.path = ncp;
1115
+ childInode.parentPath = ncpp;
1116
+ this.inodes.set(ncp, childInode);
1117
+ this._addToChildrenIndex(ncpp, ncp);
1118
+ }
1119
+ }
1120
+ this.events.emit('rename', newPath, oldPath);
1121
+ }
1122
+ copyFile(src, dest) {
1123
+ this.writeFile(dest, this.readFile(src));
1124
+ }
1125
+ // ── Batch write (npm install fast path) ───────────────────────────────
1126
+ /**
1127
+ * Atomic bulk write: ALL inodes + chunks in ONE transactionSync().
1128
+ *
1129
+ * Why this exists:
1130
+ * writeFile() does 1 DELETE + 1 INSERT per inode (each auto-committed)
1131
+ * plus deferWrite() which flushes at 500-threshold.
1132
+ * For 30K files: ~60K sync SQL ops → 30-60s, often crashes DO.
1133
+ *
1134
+ * writeBatch() does:
1135
+ * 1 transactionSync() containing:
1136
+ * - N DELETE for old paths (if any)
1137
+ * - Multi-row INSERT for inodes (up to 4000/statement)
1138
+ * - Multi-row INSERT for chunks (up to 200/statement, blob-heavy)
1139
+ * Total: 1 transactionSync() per wave of ~300-500 files.
1140
+ *
1141
+ * Speedup: 60K ops → ~60 ops (1000x fewer transaction commits).
1142
+ */
1143
+ writeBatch(payload) {
1144
+ // W5 Lever 9: top-level entry. Inner _writeBatchOnce throws on
1145
+ // SQLITE_NOMEM; this wrapper catches, classifies, drops the LRU,
1146
+ // and retries by halving — bounded depth 4. Other errors propagate
1147
+ // unchanged (loud failure preserves the W2.5 error contract for
1148
+ // constraint conflicts etc.).
1149
+ return this._writeBatchWithRetry(payload, 0);
1150
+ }
1151
+ /**
1152
+ * W7 — streaming bulk-write. Same semantics as writeBatch() but
1153
+ * accepts the chunks list as an `AsyncIterable<BatchChunkEntry>`
1154
+ * rather than a fully-realised array.
1155
+ *
1156
+ * v1 (this wave) is "spool-then-commit": we drain the iterator into
1157
+ * an in-memory Array<BatchChunkEntry>, then delegate to writeBatch.
1158
+ * The HEAP-savings claim of W7 lives on the FACET side — by the
1159
+ * time chunks reach this method (post-RPC), they've already
1160
+ * traversed the byte-stream boundary without hitting the 32 MiB
1161
+ * structured-clone cap.
1162
+ *
1163
+ * Heap-correctness wave (N2): the spool is bounded by the peer's
1164
+ * SHARED_RPC_FLUSH_THRESHOLD (4 MiB; see install-batch-facet.ts:196)
1165
+ * — a peer never sends more than ~4 MiB of chunk-bytes per
1166
+ * writeBatchStream RPC, AND workerd's input gate serialises
1167
+ * concurrent RPCs on the same DO. So the supervisor's `chunks: []`
1168
+ * spool peak is ≤ 4 MiB + path-overhead per call.
1169
+ *
1170
+ * What this method DID lack: the spool bytes were invisible to the
1171
+ * heap estimator. We now maintain `_writeStreamSpoolBytes` that
1172
+ * tracks the live spool contents during the drain; the diag's
1173
+ * vfsInFlightBytes contributor now sums pendingWriteBytes +
1174
+ * writeStreamSpoolBytes. The N2 RED probe asserts on this sum.
1175
+ *
1176
+ * Throws on SQLITE_NOMEM (with halve-retry per writeBatch); any
1177
+ * iterator-source error propagates unchanged. Atomicity guarantee
1178
+ * matches writeBatch: either ALL inodes + chunks land in SQLite or
1179
+ * NONE do.
1180
+ */
1181
+ async writeStream(payload) {
1182
+ // Drain the iterator. Iterator errors propagate unchanged.
1183
+ // We build the chunks array fully BEFORE entering transactionSync
1184
+ // because transactionSync is synchronous (cannot await mid-txn).
1185
+ //
1186
+ // N2 (heap-correctness wave): track spool bytes so the diag
1187
+ // estimator's vfsInFlightBytes is no longer a 0 lie. Increment
1188
+ // per chunk pushed; reset to 0 immediately before delegating to
1189
+ // writeBatch (the bytes are now `pendingWrites' problem if they
1190
+ // survive transactionSync).
1191
+ const chunks = [];
1192
+ try {
1193
+ for await (const c of payload.chunkIter) {
1194
+ chunks.push(c);
1195
+ this._writeStreamSpoolBytes += c.data.length;
1196
+ }
1197
+ const r = this.writeBatch({
1198
+ inodes: payload.inodes,
1199
+ chunks,
1200
+ deletePaths: payload.deletePaths,
1201
+ });
1202
+ return r;
1203
+ }
1204
+ finally {
1205
+ // The bytes are no longer in `chunks` — they've either been
1206
+ // copied into pendingWrites by writeBatch's inner deferWrite
1207
+ // path, or written directly inside transactionSync. Either
1208
+ // way, the spool counter resets here. Subtract by the actual
1209
+ // sum we tracked, NOT by chunks.length × CHUNK_SIZE — chunks
1210
+ // can be smaller than CHUNK_SIZE on the last entry of a file.
1211
+ let drained = 0;
1212
+ for (const c of chunks)
1213
+ drained += c.data.length;
1214
+ this._writeStreamSpoolBytes -= drained;
1215
+ if (this._writeStreamSpoolBytes < 0)
1216
+ this._writeStreamSpoolBytes = 0;
1217
+ }
1218
+ }
1219
+ _writeBatchWithRetry(payload, depth) {
1220
+ try {
1221
+ return this._writeBatchOnce(payload);
1222
+ }
1223
+ catch (e) {
1224
+ const cause = classifyError(e);
1225
+ // Classify before deciding to retry. Only the SQLITE_NOMEM family
1226
+ // is retryable; constraint conflicts / disk-full / clone-refused
1227
+ // / unknown all surface to the caller (fail loud).
1228
+ const lru = this._cacheBytes;
1229
+ const inFlight = this._estimateBatchBytes(payload);
1230
+ recordFailure({
1231
+ at: Date.now(),
1232
+ phase: 'install',
1233
+ cause,
1234
+ rssEstimateBytes: 0,
1235
+ heapUsedBytes: this._safeHeapUsed(),
1236
+ lruBytes: lru,
1237
+ inFlightBytes: inFlight,
1238
+ lastRpcFrame: null,
1239
+ lastFacetId: null,
1240
+ message: e?.message || String(e),
1241
+ });
1242
+ if (cause !== 'sqlite_nomem')
1243
+ throw e;
1244
+ // Bounded retry depth. 500-row batch → 250 → 125 → ~63 → ~32.
1245
+ // Beyond depth=4, give up: persistent OOM at 32 rows means we
1246
+ // are not the bottleneck.
1247
+ if (depth >= 4)
1248
+ throw e;
1249
+ // Free pages owned by us before re-attempting. evictAll() flushes
1250
+ // dirty entries via deferWrite (existing path) so no data loss.
1251
+ this.evictAll();
1252
+ // Halve the payload by partitioning ALL three lists (inodes,
1253
+ // chunks, deletePaths) by path-set so each half operates on
1254
+ // disjoint paths. Halving is on inodes (the primary index);
1255
+ // chunks and deletePaths are partitioned to match.
1256
+ const halves = this._halveBatchPayload(payload);
1257
+ const r1 = this._writeBatchWithRetry(halves[0], depth + 1);
1258
+ const r2 = this._writeBatchWithRetry(halves[1], depth + 1);
1259
+ return {
1260
+ inodes: r1.inodes + r2.inodes,
1261
+ chunks: r1.chunks + r2.chunks,
1262
+ };
1263
+ }
1264
+ }
1265
+ /**
1266
+ * Estimate the byte cost of a writeBatch payload. Used by the W5
1267
+ * recordFailure call so /api/_diag/memory can report inFlightBytes
1268
+ * at the moment of the SQLITE_NOMEM. Fast (no copy).
1269
+ */
1270
+ _estimateBatchBytes(payload) {
1271
+ let n = 0;
1272
+ for (const c of payload.chunks)
1273
+ n += c.data.length;
1274
+ // Path strings + inode header overhead — rough estimate.
1275
+ for (const i of payload.inodes)
1276
+ n += 80 + i.path.length;
1277
+ return n;
1278
+ }
1279
+ /** Best-effort process.memoryUsage().heapUsed; 0 in DO contexts. */
1280
+ _safeHeapUsed() {
1281
+ try {
1282
+ const mu = globalThis.process?.memoryUsage?.();
1283
+ return Number(mu?.heapUsed) || 0;
1284
+ }
1285
+ catch {
1286
+ return 0;
1287
+ }
1288
+ }
1289
+ /**
1290
+ * Partition a writeBatch payload into two halves with disjoint
1291
+ * path-sets. Preserves the W2.5 invariant: deletePaths and chunks
1292
+ * follow their owning inode into the same half. Used by the
1293
+ * SQLITE_NOMEM retry path.
1294
+ */
1295
+ _halveBatchPayload(p) {
1296
+ // If there are inodes, halve by inode list and partition chunks +
1297
+ // deletePaths by path-set. If there are NO inodes (chunks-only
1298
+ // batch), halve chunks directly.
1299
+ const inodes = p.inodes ?? [];
1300
+ const chunks = p.chunks ?? [];
1301
+ const dels = p.deletePaths ?? [];
1302
+ if (inodes.length >= 2) {
1303
+ const mid = Math.ceil(inodes.length / 2);
1304
+ const i1 = inodes.slice(0, mid);
1305
+ const i2 = inodes.slice(mid);
1306
+ const set1 = new Set(i1.map(n => n.path));
1307
+ const set2 = new Set(i2.map(n => n.path));
1308
+ const c1 = [];
1309
+ const c2 = [];
1310
+ for (const c of chunks) {
1311
+ if (set1.has(c.path))
1312
+ c1.push(c);
1313
+ else if (set2.has(c.path))
1314
+ c2.push(c);
1315
+ // Chunks orphaned from inodes (defensive) go to half 1.
1316
+ else
1317
+ c1.push(c);
1318
+ }
1319
+ const d1 = [];
1320
+ const d2 = [];
1321
+ for (const d of dels) {
1322
+ if (set1.has(d))
1323
+ d1.push(d);
1324
+ else if (set2.has(d))
1325
+ d2.push(d);
1326
+ else
1327
+ d1.push(d);
1328
+ }
1329
+ return [
1330
+ { inodes: i1, chunks: c1, deletePaths: d1 },
1331
+ { inodes: i2, chunks: c2, deletePaths: d2 },
1332
+ ];
1333
+ }
1334
+ // No inodes — chunks-only or delete-only payload. Halve directly.
1335
+ if (chunks.length >= 2) {
1336
+ const mid = Math.ceil(chunks.length / 2);
1337
+ return [
1338
+ { inodes: [], chunks: chunks.slice(0, mid), deletePaths: dels },
1339
+ { inodes: [], chunks: chunks.slice(mid), deletePaths: [] },
1340
+ ];
1341
+ }
1342
+ if (dels.length >= 2) {
1343
+ const mid = Math.ceil(dels.length / 2);
1344
+ return [
1345
+ { inodes: [], chunks: [], deletePaths: dels.slice(0, mid) },
1346
+ { inodes: [], chunks: [], deletePaths: dels.slice(mid) },
1347
+ ];
1348
+ }
1349
+ // Single-item payload — can't halve further; return original + empty.
1350
+ return [p, { inodes: [], chunks: [], deletePaths: [] }];
1351
+ }
1352
+ _writeBatchOnce(payload) {
1353
+ let inodeCount = 0;
1354
+ let chunkCount = 0;
1355
+ // Invalidate cache for all affected paths before writing.
1356
+ // Audit R2: the previous per-path loop was O(P × C) — each
1357
+ // cacheInvalidate/clearPendingWritesForPath did a full scan of
1358
+ // the 512-entry cache / pendingWrites map. For a 500-path wave
1359
+ // that was ~256K comparisons; a 30K-file git clone paid ~15M
1360
+ // total. The cacheInvalidateBatch / clearPendingWritesForPaths
1361
+ // helpers do a single pass each — O(P + C).
1362
+ const affectedPaths = new Set();
1363
+ for (const inode of payload.inodes) {
1364
+ if (!inode.isDir)
1365
+ affectedPaths.add(inode.path);
1366
+ }
1367
+ for (const chunk of payload.chunks) {
1368
+ affectedPaths.add(chunk.path);
1369
+ }
1370
+ if (payload.deletePaths) {
1371
+ for (const p of payload.deletePaths)
1372
+ affectedPaths.add(p);
1373
+ }
1374
+ this.cacheInvalidateBatch(affectedPaths, true);
1375
+ this.clearPendingWritesForPaths(affectedPaths);
1376
+ try {
1377
+ const doTransaction = (fn) => {
1378
+ if (this.ctx?.storage?.transactionSync) {
1379
+ this.ctx.storage.transactionSync(fn);
1380
+ }
1381
+ else {
1382
+ fn();
1383
+ }
1384
+ };
1385
+ doTransaction(() => {
1386
+ // 1. Delete old paths
1387
+ if (payload.deletePaths?.length) {
1388
+ for (const path of payload.deletePaths) {
1389
+ this.sql.exec("DELETE FROM file_chunks WHERE path = ?", path);
1390
+ this.sql.exec("DELETE FROM inodes WHERE path = ?", path);
1391
+ }
1392
+ }
1393
+ // 2. Batch insert inodes — multi-row VALUES.
1394
+ // DO SQLite has a low bind-parameter limit (~100 variables).
1395
+ // 7 columns per inode → max 14 rows per statement (14×7=98).
1396
+ const INODE_BATCH = 14;
1397
+ for (let i = 0; i < payload.inodes.length; i += INODE_BATCH) {
1398
+ const batch = payload.inodes.slice(i, i + INODE_BATCH);
1399
+ const placeholders = batch.map(() => '(?,?,?,?,?,?,?)').join(',');
1400
+ const values = [];
1401
+ for (const n of batch) {
1402
+ values.push(n.path, n.parentPath, n.isDir ? 1 : 0, n.size, n.mtime, n.mode, n.chunkCount);
1403
+ }
1404
+ this.sql.exec(`INSERT OR REPLACE INTO inodes (path, parent_path, is_dir, size, mtime, mode, chunk_count) VALUES ${placeholders}`, ...values);
1405
+ inodeCount += batch.length;
1406
+ }
1407
+ // 3. Batch insert chunks — multi-row VALUES.
1408
+ // 3 columns per chunk → max 33 rows per statement (33×3=99).
1409
+ const CHUNK_BATCH = 33;
1410
+ for (let i = 0; i < payload.chunks.length; i += CHUNK_BATCH) {
1411
+ const batch = payload.chunks.slice(i, i + CHUNK_BATCH);
1412
+ const placeholders = batch.map(() => '(?,?,?)').join(',');
1413
+ const values = [];
1414
+ for (const c of batch) {
1415
+ values.push(c.path, c.chunkId, c.data);
1416
+ }
1417
+ this.sql.exec(`INSERT OR REPLACE INTO file_chunks (path, chunk_id, data) VALUES ${placeholders}`, ...values);
1418
+ chunkCount += batch.length;
1419
+ }
1420
+ });
1421
+ }
1422
+ catch (e) {
1423
+ console.error('[sqlite-vfs] writeBatch failed:', e?.message);
1424
+ throw e;
1425
+ }
1426
+ // 4. Update in-memory inode tree + children index (outside transaction — fast).
1427
+ // B3: also maintain running counters in sync. For each payload entry,
1428
+ // compute the delta against any pre-existing inode at that path.
1429
+ //
1430
+ // W2.5a (audit/sections/W2.5-plan.md): the children-index call is now
1431
+ // OUTSIDE the `prior === undefined` guard. Pre-W2.5a, ~37 of 46 packages
1432
+ // per `npm install fastify` accumulated inodes in SQL but never reached
1433
+ // the in-memory `this.children` index because some path's `prior` was
1434
+ // unexpectedly defined when the package's writeBatch arrived (root
1435
+ // cause unidentified — see §4.2 diagnostic plan in W2.5-plan.md).
1436
+ // `_addToChildrenIndex` uses Set.add so repeated calls are idempotent;
1437
+ // gating it on `prior === undefined` was the bug. Counters remain
1438
+ // gated correctly so they don't double-count.
1439
+ const __diag = (globalThis.process?.env?.NIMBUS_DIAG_INSTALL_PIPELINE === '1');
1440
+ for (const entry of payload.inodes) {
1441
+ const prior = this.inodes.get(entry.path);
1442
+ const node = {
1443
+ path: entry.path,
1444
+ parentPath: entry.parentPath,
1445
+ isDir: entry.isDir,
1446
+ size: entry.size,
1447
+ mtime: entry.mtime,
1448
+ mode: entry.mode,
1449
+ chunkCount: entry.chunkCount,
1450
+ };
1451
+ this.inodes.set(entry.path, node);
1452
+ // ALWAYS re-affirm the children-index entry. Idempotent.
1453
+ this._addToChildrenIndex(entry.parentPath, entry.path);
1454
+ // W2.5b diagnostic: log every "stale prior" case where we'd have
1455
+ // skipped the index call pre-W2.5a. Reveals which paths were
1456
+ // pre-populated in this.inodes by a code path other than the
1457
+ // current writeBatch — H5a / H8 candidate.
1458
+ if (__diag && prior !== undefined) {
1459
+ const indexed = this.children.get(entry.parentPath)?.has(entry.path) ?? false;
1460
+ // eslint-disable-next-line no-console
1461
+ console.warn('[sqlite-vfs/W2.5b] stale-prior path=' + entry.path +
1462
+ ' parent=' + entry.parentPath +
1463
+ ' priorParent=' + prior.parentPath +
1464
+ ' priorIsDir=' + prior.isDir +
1465
+ ' entryIsDir=' + entry.isDir +
1466
+ ' indexedBefore=' + indexed);
1467
+ }
1468
+ // Counter delta — gated on prior so we don't double-count.
1469
+ if (prior === undefined) {
1470
+ if (entry.isDir)
1471
+ this._totalDirs++;
1472
+ else {
1473
+ this._totalFiles++;
1474
+ this._usedBytes += entry.size;
1475
+ }
1476
+ }
1477
+ else {
1478
+ // Replace: handle dir↔file flip + size delta. (Identical to pre-W2.5a.)
1479
+ if (prior.isDir && !entry.isDir) {
1480
+ this._totalDirs--;
1481
+ this._totalFiles++;
1482
+ this._usedBytes += entry.size;
1483
+ }
1484
+ else if (!prior.isDir && entry.isDir) {
1485
+ this._totalFiles--;
1486
+ this._usedBytes -= prior.size;
1487
+ this._totalDirs++;
1488
+ }
1489
+ else if (!entry.isDir) {
1490
+ // File-replace: size delta only.
1491
+ this._usedBytes += entry.size - prior.size;
1492
+ }
1493
+ // Dir-replace (both dir): no counter change.
1494
+ }
1495
+ }
1496
+ // Note: payload.deletePaths only removes from SQL, not from
1497
+ // this.inodes (pre-existing behaviour — see writeBatch SQL section
1498
+ // around line 1020). Counters therefore track in-memory state,
1499
+ // which matches what getStats() observes. If the in-memory cleanup
1500
+ // of deletePaths is ever added, update the counters there too.
1501
+ // 5. Fire events for VFS-aware subscribers (HMR etc.)
1502
+ for (const entry of payload.inodes) {
1503
+ if (entry.isDir) {
1504
+ this.events.emit('addDir', entry.path);
1505
+ }
1506
+ else {
1507
+ this.events.emit('add', entry.path);
1508
+ }
1509
+ }
1510
+ this._sqlWrites += inodeCount + chunkCount;
1511
+ this._batchWrites++;
1512
+ this._batchWriteRows += inodeCount + chunkCount;
1513
+ return { inodes: inodeCount, chunks: chunkCount };
1514
+ }
1515
+ /**
1516
+ * Bulk mkdir: create all directories in a single transactionSync.
1517
+ * Pre-creates the full directory tree before file writes to avoid
1518
+ * per-file mkdir overhead.
1519
+ */
1520
+ mkdirBatch(paths) {
1521
+ const mtime = Date.now();
1522
+ const toCreate = [];
1523
+ const seen = new Set();
1524
+ for (const path of paths) {
1525
+ const parts = path.split('/').filter(Boolean);
1526
+ let current = '';
1527
+ for (const part of parts) {
1528
+ current = current ? current + '/' + part : part;
1529
+ if (!seen.has(current) && !this.exists(current)) {
1530
+ seen.add(current);
1531
+ toCreate.push({
1532
+ path: current,
1533
+ parentPath: this.parentPath(current),
1534
+ isDir: true,
1535
+ size: 0,
1536
+ mtime,
1537
+ mode: 0o755,
1538
+ chunkCount: 0,
1539
+ });
1540
+ }
1541
+ }
1542
+ }
1543
+ if (toCreate.length === 0)
1544
+ return 0;
1545
+ this.writeBatch({ inodes: toCreate, chunks: [] });
1546
+ return toCreate.length;
1547
+ }
1548
+ // ── Stats ─────────────────────────────────────────────────────────────
1549
+ /**
1550
+ * Debug-only: recompute counters from scratch and return any drift
1551
+ * against the running counters. Returns null if consistent. Used by
1552
+ * the B3 runtime test; production paths should never call this
1553
+ * (the whole point of B3 is avoiding the O(N) walk).
1554
+ */
1555
+ _verifyCounters() {
1556
+ let f = 0, d = 0, b = 0;
1557
+ for (const inode of this.inodes.values()) {
1558
+ if (inode.isDir)
1559
+ d++;
1560
+ else {
1561
+ f++;
1562
+ b += inode.size;
1563
+ }
1564
+ }
1565
+ if (f === this._totalFiles && d === this._totalDirs && b === this._usedBytes)
1566
+ return null;
1567
+ return {
1568
+ expected: { files: f, dirs: d, bytes: b },
1569
+ actual: { files: this._totalFiles, dirs: this._totalDirs, bytes: this._usedBytes },
1570
+ };
1571
+ }
1572
+ getStats() {
1573
+ // B3: O(1) — read the running counters. Previously three passes
1574
+ // over this.inodes (two filter + one for-of); at 50K inodes that
1575
+ // was 150K iterations per poll, every 5 s, serialising on the
1576
+ // input gate alongside shell keystrokes (AUDIT M10 / M-S8).
1577
+ const totalFiles = this._totalFiles;
1578
+ const totalDirs = this._totalDirs;
1579
+ const usedBytes = this._usedBytes;
1580
+ const totalAccesses = this._cacheHits + this._cacheMisses;
1581
+ const hitRate = totalAccesses > 0 ? (this._cacheHits / totalAccesses * 100) : 0;
1582
+ return {
1583
+ // Legacy compat
1584
+ files: totalFiles,
1585
+ directories: totalDirs,
1586
+ usedBytes,
1587
+ capacityBytes: 10 * 1024 * 1024 * 1024, // 10 GB
1588
+ backend: 'DO SQLite (demand-paged VFS)',
1589
+ // Cache stats. maxEntries / maxBytes are now W5-runtime-mutable —
1590
+ // shrinkForInstall() drops them, restoreAfterInstall() restores.
1591
+ // lruShrunk is the at-a-glance signal for /api/_diag/memory.
1592
+ cache: {
1593
+ entries: this.cache.size,
1594
+ maxEntries: this._lruMaxEntries,
1595
+ chunkSize: CHUNK_SIZE,
1596
+ hotBytes: this._cacheBytes,
1597
+ maxBytes: this._lruMaxEntries * CHUNK_SIZE,
1598
+ hits: this._cacheHits,
1599
+ misses: this._cacheMisses,
1600
+ hitRate: Math.round(hitRate * 100) / 100,
1601
+ evictions: this._evictions,
1602
+ lruShrunk: this._lruMaxEntries < LRU_MAX_ENTRIES,
1603
+ },
1604
+ // SQL I/O stats
1605
+ sql: {
1606
+ reads: this._sqlReads,
1607
+ writes: this._sqlWrites,
1608
+ batchWrites: this._batchWrites,
1609
+ batchWriteRows: this._batchWriteRows,
1610
+ pendingWrites: this.pendingWrites.size,
1611
+ // N3 (heap-correctness wave): real byte sum of the
1612
+ // pendingWrites Map. Source-of-truth for the heap-estimator's
1613
+ // vfsInFlightBytes contributor (was hardcoded 0 at routes.ts:347
1614
+ // pre-fix). Adding this AS WELL AS the entry count above so
1615
+ // ops dashboards that already plot the count don't break, while
1616
+ // memory triage gets the actual byte number.
1617
+ pendingWriteBytes: this._pendingWriteBytes,
1618
+ // N2 (heap-correctness wave): live byte count inside the
1619
+ // writeStream() drain spool. Non-zero only during the brief
1620
+ // window between the first chunk arriving and writeBatch
1621
+ // returning — bounded by SHARED_RPC_FLUSH_THRESHOLD (4 MiB)
1622
+ // on the peer side.
1623
+ writeStreamSpoolBytes: this._writeStreamSpoolBytes,
1624
+ failedWrites: this.failedWrites.size,
1625
+ totalWriteFailures: this._writeFailures,
1626
+ },
1627
+ // Event stats
1628
+ events: this.events.stats,
1629
+ // INode stats
1630
+ inodes: {
1631
+ total: this.inodes.size,
1632
+ files: totalFiles,
1633
+ directories: totalDirs,
1634
+ memoryEstimate: this.inodes.size * 200, // ~200 bytes per entry
1635
+ },
1636
+ };
1637
+ }
1638
+ }
1639
+ // ── SqliteVFSProvider (MountProvider for Nimbus Kernel VFS) ────────────────────
1640
+ export class SqliteVFSProvider {
1641
+ vfs;
1642
+ prefix;
1643
+ constructor(vfs, prefix) {
1644
+ this.vfs = vfs;
1645
+ this.prefix = prefix.replace(/^\/+/, '').replace(/\/+$/, '');
1646
+ }
1647
+ resolve(sub) {
1648
+ const c = sub.replace(/^\/+/, '').replace(/\/+$/, '');
1649
+ return c ? this.prefix + '/' + c : this.prefix;
1650
+ }
1651
+ readFile(sub) { return this.vfs.readFile(this.resolve(sub)); }
1652
+ readFileString(sub) { return this.vfs.readFileString(this.resolve(sub)); }
1653
+ writeFile(sub, content) {
1654
+ const fp = this.resolve(sub);
1655
+ const pp = fp.includes('/') ? fp.substring(0, fp.lastIndexOf('/')) : '';
1656
+ if (pp && !this.vfs.exists(pp))
1657
+ this.vfs.mkdir(pp, { recursive: true });
1658
+ this.vfs.writeFile(fp, content);
1659
+ }
1660
+ exists(sub) { return this.vfs.exists(this.resolve(sub)); }
1661
+ stat(sub) { return this.vfs.stat(this.resolve(sub)); }
1662
+ readdir(sub) { return this.vfs.readdir(this.resolve(sub)); }
1663
+ unlink(sub) { this.vfs.unlink(this.resolve(sub)); }
1664
+ mkdir(sub, opts) {
1665
+ this.vfs.mkdir(this.resolve(sub), opts);
1666
+ }
1667
+ rmdir(sub) { this.vfs.rmdir(this.resolve(sub)); }
1668
+ rename(o, n) { this.vfs.rename(this.resolve(o), this.resolve(n)); }
1669
+ copyFile(s, d) { this.vfs.copyFile(this.resolve(s), this.resolve(d)); }
1670
+ }