@statewalker/cellb 0.0.0-stage → 0.1.1

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 (365) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +323 -2
  3. package/dist/adapters/files-backup.d.ts +16 -0
  4. package/dist/adapters/files-backup.js +32 -0
  5. package/dist/adapters/files-backup.js.map +1 -0
  6. package/dist/adapters/files-changelog.d.ts +33 -0
  7. package/dist/adapters/files-changelog.js +96 -0
  8. package/dist/adapters/files-changelog.js.map +1 -0
  9. package/dist/adapters/files-ltx.d.ts +35 -0
  10. package/dist/adapters/files-ltx.js +9 -0
  11. package/dist/adapters/files-ltx.js.map +1 -0
  12. package/dist/adapters/ltx-vfs.d.ts +21 -0
  13. package/dist/adapters/ltx-vfs.js +286 -0
  14. package/dist/adapters/ltx-vfs.js.map +1 -0
  15. package/dist/adapters/node-sqlite.d.ts +45 -0
  16. package/dist/adapters/node-sqlite.js +254 -0
  17. package/dist/adapters/node-sqlite.js.map +1 -0
  18. package/dist/adapters/wasm-page-hook.d.ts +26 -0
  19. package/dist/adapters/wasm-page-hook.js +55 -0
  20. package/dist/adapters/wasm-page-hook.js.map +1 -0
  21. package/dist/adapters/wasm-session.d.ts +11 -0
  22. package/dist/adapters/wasm-session.js +76 -0
  23. package/dist/adapters/wasm-session.js.map +1 -0
  24. package/dist/adapters/wasm-sqlite.d.ts +89 -0
  25. package/dist/adapters/wasm-sqlite.js +258 -0
  26. package/dist/adapters/wasm-sqlite.js.map +1 -0
  27. package/dist/bindings/assets.d.ts +26 -0
  28. package/dist/bindings/assets.js +260 -0
  29. package/dist/bindings/assets.js.map +1 -0
  30. package/dist/bindings/cache.d.ts +16 -0
  31. package/dist/bindings/cache.js +34 -0
  32. package/dist/bindings/cache.js.map +1 -0
  33. package/dist/bindings/celld-kv.d.ts +26 -0
  34. package/dist/bindings/celld-kv.js +81 -0
  35. package/dist/bindings/celld-kv.js.map +1 -0
  36. package/dist/bindings/celld-queues.d.ts +56 -0
  37. package/dist/bindings/celld-queues.js +223 -0
  38. package/dist/bindings/celld-queues.js.map +1 -0
  39. package/dist/bindings/celld-workflows.d.ts +22 -0
  40. package/dist/bindings/celld-workflows.js +54 -0
  41. package/dist/bindings/celld-workflows.js.map +1 -0
  42. package/dist/bindings/cron.d.ts +27 -0
  43. package/dist/bindings/cron.js +232 -0
  44. package/dist/bindings/cron.js.map +1 -0
  45. package/dist/bindings/d1-migrations.d.ts +22 -0
  46. package/dist/bindings/d1-migrations.js +32 -0
  47. package/dist/bindings/d1-migrations.js.map +1 -0
  48. package/dist/bindings/html-rewriter.d.ts +8 -0
  49. package/dist/bindings/html-rewriter.js +75 -0
  50. package/dist/bindings/html-rewriter.js.map +1 -0
  51. package/dist/bindings/r2-celld.d.ts +3 -0
  52. package/dist/bindings/r2-celld.js +113 -0
  53. package/dist/bindings/r2-celld.js.map +1 -0
  54. package/dist/bindings/r2.d.ts +236 -0
  55. package/dist/bindings/r2.js +419 -0
  56. package/dist/bindings/r2.js.map +1 -0
  57. package/dist/bindings/worker-loader.d.ts +83 -0
  58. package/dist/bindings/worker-loader.js +183 -0
  59. package/dist/bindings/worker-loader.js.map +1 -0
  60. package/dist/bindings/workers-ai.d.ts +39 -0
  61. package/dist/bindings/workers-ai.js +175 -0
  62. package/dist/bindings/workers-ai.js.map +1 -0
  63. package/dist/browser.d.ts +7 -0
  64. package/dist/browser.js +8 -0
  65. package/dist/browser.js.map +1 -0
  66. package/dist/bucket/celld-cells.d.ts +86 -0
  67. package/dist/bucket/celld-cells.js +273 -0
  68. package/dist/bucket/celld-cells.js.map +1 -0
  69. package/dist/bucket/celld-format.d.ts +10 -0
  70. package/dist/bucket/celld-format.js +27 -0
  71. package/dist/bucket/celld-format.js.map +1 -0
  72. package/dist/bucket/idb-object-store.d.ts +9 -0
  73. package/dist/bucket/idb-object-store.js +73 -0
  74. package/dist/bucket/idb-object-store.js.map +1 -0
  75. package/dist/bucket/ltx-log.d.ts +34 -0
  76. package/dist/bucket/ltx-log.js +289 -0
  77. package/dist/bucket/ltx-log.js.map +1 -0
  78. package/dist/bucket/object-store.d.ts +51 -0
  79. package/dist/bucket/object-store.js +114 -0
  80. package/dist/bucket/object-store.js.map +1 -0
  81. package/dist/bucket/s3-object-store.d.ts +25 -0
  82. package/dist/bucket/s3-object-store.js +151 -0
  83. package/dist/bucket/s3-object-store.js.map +1 -0
  84. package/dist/bucket/sqlite-object-store.d.ts +10 -0
  85. package/dist/bucket/sqlite-object-store.js +107 -0
  86. package/dist/bucket/sqlite-object-store.js.map +1 -0
  87. package/dist/core/alarms.d.ts +27 -0
  88. package/dist/core/alarms.js +88 -0
  89. package/dist/core/alarms.js.map +1 -0
  90. package/dist/core/async-context.d.ts +34 -0
  91. package/dist/core/async-context.js +50 -0
  92. package/dist/core/async-context.js.map +1 -0
  93. package/dist/core/celld-schema.d.ts +14 -0
  94. package/dist/core/celld-schema.js +34 -0
  95. package/dist/core/celld-schema.js.map +1 -0
  96. package/dist/core/durable-object.d.ts +28 -0
  97. package/dist/core/durable-object.js +31 -0
  98. package/dist/core/durable-object.js.map +1 -0
  99. package/dist/core/gates.d.ts +33 -0
  100. package/dist/core/gates.js +89 -0
  101. package/dist/core/gates.js.map +1 -0
  102. package/dist/core/handles.d.ts +90 -0
  103. package/dist/core/handles.js +224 -0
  104. package/dist/core/handles.js.map +1 -0
  105. package/dist/core/id.d.ts +19 -0
  106. package/dist/core/id.js +64 -0
  107. package/dist/core/id.js.map +1 -0
  108. package/dist/core/namespace.d.ts +38 -0
  109. package/dist/core/namespace.js +141 -0
  110. package/dist/core/namespace.js.map +1 -0
  111. package/dist/core/object-host.d.ts +91 -0
  112. package/dist/core/object-host.js +616 -0
  113. package/dist/core/object-host.js.map +1 -0
  114. package/dist/core/request-body.d.ts +9 -0
  115. package/dist/core/request-body.js +55 -0
  116. package/dist/core/request-body.js.map +1 -0
  117. package/dist/core/runtime.d.ts +121 -0
  118. package/dist/core/runtime.js +366 -0
  119. package/dist/core/runtime.js.map +1 -0
  120. package/dist/core/shard.d.ts +7 -0
  121. package/dist/core/shard.js +10 -0
  122. package/dist/core/shard.js.map +1 -0
  123. package/dist/core/sql-split.d.ts +15 -0
  124. package/dist/core/sql-split.js +101 -0
  125. package/dist/core/sql-split.js.map +1 -0
  126. package/dist/core/sql-storage.d.ts +53 -0
  127. package/dist/core/sql-storage.js +407 -0
  128. package/dist/core/sql-storage.js.map +1 -0
  129. package/dist/core/state.d.ts +44 -0
  130. package/dist/core/state.js +93 -0
  131. package/dist/core/state.js.map +1 -0
  132. package/dist/core/storage.d.ts +103 -0
  133. package/dist/core/storage.js +351 -0
  134. package/dist/core/storage.js.map +1 -0
  135. package/dist/core/worker-rejections.d.ts +21 -0
  136. package/dist/core/worker-rejections.js +55 -0
  137. package/dist/core/worker-rejections.js.map +1 -0
  138. package/dist/index.d.ts +22 -0
  139. package/dist/index.js +23 -0
  140. package/dist/index.js.map +1 -0
  141. package/dist/isolate/browser.d.ts +16 -0
  142. package/dist/isolate/browser.js +37 -0
  143. package/dist/isolate/browser.js.map +1 -0
  144. package/dist/isolate/host.d.ts +24 -0
  145. package/dist/isolate/host.js +74 -0
  146. package/dist/isolate/host.js.map +1 -0
  147. package/dist/isolate/isolate-worker.d.ts +1 -0
  148. package/dist/isolate/isolate-worker.js +53 -0
  149. package/dist/isolate/isolate-worker.js.map +1 -0
  150. package/dist/isolate/isolate.d.ts +10 -0
  151. package/dist/isolate/isolate.js +169 -0
  152. package/dist/isolate/isolate.js.map +1 -0
  153. package/dist/isolate/node-entry.d.ts +1 -0
  154. package/dist/isolate/node-entry.js +12 -0
  155. package/dist/isolate/node-entry.js.map +1 -0
  156. package/dist/isolate/protocol.d.ts +66 -0
  157. package/dist/isolate/protocol.js +205 -0
  158. package/dist/isolate/protocol.js.map +1 -0
  159. package/dist/ltx/crc64.d.ts +6 -0
  160. package/dist/ltx/crc64.js +46 -0
  161. package/dist/ltx/crc64.js.map +1 -0
  162. package/dist/ltx/ltx-page-server.d.ts +65 -0
  163. package/dist/ltx/ltx-page-server.js +309 -0
  164. package/dist/ltx/ltx-page-server.js.map +1 -0
  165. package/dist/ltx/ltx-pages.d.ts +81 -0
  166. package/dist/ltx/ltx-pages.js +315 -0
  167. package/dist/ltx/ltx-pages.js.map +1 -0
  168. package/dist/ltx/ltx-store-worker.d.ts +1 -0
  169. package/dist/ltx/ltx-store-worker.js +38 -0
  170. package/dist/ltx/ltx-store-worker.js.map +1 -0
  171. package/dist/ltx/ltx.d.ts +45 -0
  172. package/dist/ltx/ltx.js +203 -0
  173. package/dist/ltx/ltx.js.map +1 -0
  174. package/dist/ltx/lz4.d.ts +9 -0
  175. package/dist/ltx/lz4.js +156 -0
  176. package/dist/ltx/lz4.js.map +1 -0
  177. package/dist/ltx/opfs-sync-files.d.ts +23 -0
  178. package/dist/ltx/opfs-sync-files.js +191 -0
  179. package/dist/ltx/opfs-sync-files.js.map +1 -0
  180. package/dist/ltx/xxh32.d.ts +1 -0
  181. package/dist/ltx/xxh32.js +43 -0
  182. package/dist/ltx/xxh32.js.map +1 -0
  183. package/dist/miniflare-compat/blob-service.d.ts +13 -0
  184. package/dist/miniflare-compat/blob-service.js +66 -0
  185. package/dist/miniflare-compat/blob-service.js.map +1 -0
  186. package/dist/miniflare-compat/kv-binding.d.ts +44 -0
  187. package/dist/miniflare-compat/kv-binding.js +99 -0
  188. package/dist/miniflare-compat/kv-binding.js.map +1 -0
  189. package/dist/miniflare-compat/shims/assert.d.ts +15 -0
  190. package/dist/miniflare-compat/shims/assert.js +24 -0
  191. package/dist/miniflare-compat/shims/assert.js.map +1 -0
  192. package/dist/miniflare-compat/shims/workerd-cloudflare.d.ts +7 -0
  193. package/dist/miniflare-compat/shims/workerd-cloudflare.js +6 -0
  194. package/dist/miniflare-compat/shims/workerd-cloudflare.js.map +1 -0
  195. package/dist/miniflare-compat/shims/workerd-globals.d.ts +1 -0
  196. package/dist/miniflare-compat/shims/workerd-globals.js +30 -0
  197. package/dist/miniflare-compat/shims/workerd-globals.js.map +1 -0
  198. package/dist/miniflare-compat/shims/workerd-internal.d.ts +20 -0
  199. package/dist/miniflare-compat/shims/workerd-internal.js +23 -0
  200. package/dist/miniflare-compat/shims/workerd-internal.js.map +1 -0
  201. package/dist/miniflare-compat/shims/workerd-performance.d.ts +4 -0
  202. package/dist/miniflare-compat/shims/workerd-performance.js +17 -0
  203. package/dist/miniflare-compat/shims/workerd-performance.js.map +1 -0
  204. package/dist/miniflare-compat/simulator-fetcher.d.ts +9 -0
  205. package/dist/miniflare-compat/simulator-fetcher.js +14 -0
  206. package/dist/miniflare-compat/simulator-fetcher.js.map +1 -0
  207. package/dist/miniflare-compat/vendored.d.ts +15 -0
  208. package/dist/miniflare-compat/vendored.js +27 -0
  209. package/dist/miniflare-compat/vendored.js.map +1 -0
  210. package/dist/node-compat/async_hooks.d.ts +22 -0
  211. package/dist/node-compat/async_hooks.js +176 -0
  212. package/dist/node-compat/async_hooks.js.map +1 -0
  213. package/dist/node-compat/browser-registry.d.ts +3 -0
  214. package/dist/node-compat/browser-registry.js +39 -0
  215. package/dist/node-compat/browser-registry.js.map +1 -0
  216. package/dist/node-compat/crypto.d.ts +60 -0
  217. package/dist/node-compat/crypto.js +143 -0
  218. package/dist/node-compat/crypto.js.map +1 -0
  219. package/dist/node-compat/diagnostics_channel.d.ts +27 -0
  220. package/dist/node-compat/diagnostics_channel.js +46 -0
  221. package/dist/node-compat/diagnostics_channel.js.map +1 -0
  222. package/dist/node-compat/fs.d.ts +35 -0
  223. package/dist/node-compat/fs.js +40 -0
  224. package/dist/node-compat/fs.js.map +1 -0
  225. package/dist/node-compat/globals.d.ts +3 -0
  226. package/dist/node-compat/globals.js +4 -0
  227. package/dist/node-compat/globals.js.map +1 -0
  228. package/dist/node-compat/install.d.ts +5 -0
  229. package/dist/node-compat/install.js +12 -0
  230. package/dist/node-compat/install.js.map +1 -0
  231. package/dist/node-compat/module.d.ts +12 -0
  232. package/dist/node-compat/module.js +36 -0
  233. package/dist/node-compat/module.js.map +1 -0
  234. package/dist/node-compat/modules.d.ts +8 -0
  235. package/dist/node-compat/modules.js +23 -0
  236. package/dist/node-compat/modules.js.map +1 -0
  237. package/dist/node-compat/os.d.ts +36 -0
  238. package/dist/node-compat/os.js +36 -0
  239. package/dist/node-compat/os.js.map +1 -0
  240. package/dist/node-compat/process.d.ts +14 -0
  241. package/dist/node-compat/process.js +16 -0
  242. package/dist/node-compat/process.js.map +1 -0
  243. package/dist/node-compat/shims.d.ts +4 -0
  244. package/dist/node.d.ts +5 -0
  245. package/dist/node.js +6 -0
  246. package/dist/node.js.map +1 -0
  247. package/dist/peer/celld-peer.d.ts +53 -0
  248. package/dist/peer/celld-peer.js +555 -0
  249. package/dist/peer/celld-peer.js.map +1 -0
  250. package/dist/ports.d.ts +183 -0
  251. package/dist/ports.js +10 -0
  252. package/dist/ports.js.map +1 -0
  253. package/dist/project/browser-bundle.d.ts +17 -0
  254. package/dist/project/browser-bundle.js +125 -0
  255. package/dist/project/browser-bundle.js.map +1 -0
  256. package/dist/project/browser-cookies.d.ts +12 -0
  257. package/dist/project/browser-cookies.js +90 -0
  258. package/dist/project/browser-cookies.js.map +1 -0
  259. package/dist/project/browser-host.d.ts +173 -0
  260. package/dist/project/browser-host.js +200 -0
  261. package/dist/project/browser-host.js.map +1 -0
  262. package/dist/project/browser-modules.d.ts +17 -0
  263. package/dist/project/browser-modules.js +89 -0
  264. package/dist/project/browser-modules.js.map +1 -0
  265. package/dist/project/browser-shards.d.ts +62 -0
  266. package/dist/project/browser-shards.js +210 -0
  267. package/dist/project/browser-shards.js.map +1 -0
  268. package/dist/project/browser-tabs.d.ts +103 -0
  269. package/dist/project/browser-tabs.js +418 -0
  270. package/dist/project/browser-tabs.js.map +1 -0
  271. package/dist/project/cloudflare-email.d.ts +8 -0
  272. package/dist/project/cloudflare-email.js +11 -0
  273. package/dist/project/cloudflare-email.js.map +1 -0
  274. package/dist/project/cloudflare-workers.d.ts +12 -0
  275. package/dist/project/cloudflare-workers.js +16 -0
  276. package/dist/project/cloudflare-workers.js.map +1 -0
  277. package/dist/project/host-modules.d.ts +4 -0
  278. package/dist/project/host-modules.js +40 -0
  279. package/dist/project/host-modules.js.map +1 -0
  280. package/dist/project/jsonc.d.ts +2 -0
  281. package/dist/project/jsonc.js +27 -0
  282. package/dist/project/jsonc.js.map +1 -0
  283. package/dist/project/lower-await.d.ts +2 -0
  284. package/dist/project/lower-await.js +151 -0
  285. package/dist/project/lower-await.js.map +1 -0
  286. package/dist/project/node-project.d.ts +38 -0
  287. package/dist/project/node-project.js +373 -0
  288. package/dist/project/node-project.js.map +1 -0
  289. package/dist/project/own-module.d.ts +6 -0
  290. package/dist/project/own-module.js +13 -0
  291. package/dist/project/own-module.js.map +1 -0
  292. package/dist/project/scheduler.d.ts +2 -0
  293. package/dist/project/scheduler.js +3 -0
  294. package/dist/project/scheduler.js.map +1 -0
  295. package/dist/project/site.d.ts +74 -0
  296. package/dist/project/site.js +206 -0
  297. package/dist/project/site.js.map +1 -0
  298. package/dist/project/virtual-modules.d.ts +3 -0
  299. package/dist/project/worker-scope.d.ts +27 -0
  300. package/dist/project/worker-scope.js +46 -0
  301. package/dist/project/worker-scope.js.map +1 -0
  302. package/dist/project/workerd-message-channel.d.ts +13 -0
  303. package/dist/project/workerd-message-channel.js +137 -0
  304. package/dist/project/workerd-message-channel.js.map +1 -0
  305. package/dist/project/workers-global-names.d.ts +6 -0
  306. package/dist/project/workers-global-names.js +15 -0
  307. package/dist/project/workers-global-names.js.map +1 -0
  308. package/dist/project/workers-globals-module.d.ts +8 -0
  309. package/dist/project/workers-globals-module.js +9 -0
  310. package/dist/project/workers-globals-module.js.map +1 -0
  311. package/dist/project/workers-globals.d.ts +9 -0
  312. package/dist/project/workers-globals.js +19 -0
  313. package/dist/project/workers-globals.js.map +1 -0
  314. package/dist/project/wrangler.d.ts +116 -0
  315. package/dist/project/wrangler.js +342 -0
  316. package/dist/project/wrangler.js.map +1 -0
  317. package/dist/router/cell-router.d.ts +72 -0
  318. package/dist/router/cell-router.js +247 -0
  319. package/dist/router/cell-router.js.map +1 -0
  320. package/dist/router/shard-worker.d.ts +44 -0
  321. package/dist/router/shard-worker.js +163 -0
  322. package/dist/router/shard-worker.js.map +1 -0
  323. package/dist/transport/cells-http.d.ts +25 -0
  324. package/dist/transport/cells-http.js +141 -0
  325. package/dist/transport/cells-http.js.map +1 -0
  326. package/dist/transport/cells-link.d.ts +27 -0
  327. package/dist/transport/cells-link.js +71 -0
  328. package/dist/transport/cells-link.js.map +1 -0
  329. package/dist/transport/port.d.ts +26 -0
  330. package/dist/transport/port.js +100 -0
  331. package/dist/transport/port.js.map +1 -0
  332. package/dist/v8/codec.d.ts +6 -0
  333. package/dist/v8/codec.js +9 -0
  334. package/dist/v8/codec.js.map +1 -0
  335. package/dist/v8/serialize.d.ts +10 -0
  336. package/dist/v8/serialize.js +601 -0
  337. package/dist/v8/serialize.js.map +1 -0
  338. package/dist/ws/client.d.ts +25 -0
  339. package/dist/ws/client.js +203 -0
  340. package/dist/ws/client.js.map +1 -0
  341. package/dist/ws/control.d.ts +48 -0
  342. package/dist/ws/control.js +44 -0
  343. package/dist/ws/control.js.map +1 -0
  344. package/dist/ws/frames.d.ts +38 -0
  345. package/dist/ws/frames.js +39 -0
  346. package/dist/ws/frames.js.map +1 -0
  347. package/dist/ws/server.d.ts +99 -0
  348. package/dist/ws/server.js +295 -0
  349. package/dist/ws/server.js.map +1 -0
  350. package/dist/ws/sw-connector.d.ts +10 -0
  351. package/dist/ws/sw-connector.js +25 -0
  352. package/dist/ws/sw-connector.js.map +1 -0
  353. package/dist/ws/upgrade.d.ts +28 -0
  354. package/dist/ws/upgrade.js +75 -0
  355. package/dist/ws/upgrade.js.map +1 -0
  356. package/package.json +111 -3
  357. package/vendor/celld/LICENSE +202 -0
  358. package/vendor/celld/kv.js +1131 -0
  359. package/vendor/celld/queues.js +1656 -0
  360. package/vendor/celld/workflows.js +1610 -0
  361. package/vendor/html-rewriter-wasm/html_rewriter.js +925 -0
  362. package/vendor/html-rewriter-wasm/html_rewriter_bg.wasm +0 -0
  363. package/vendor/miniflare/d1-api.js +738 -0
  364. package/vendor/miniflare/d1-database.js +6978 -0
  365. package/vendor/miniflare/kv-namespace.js +3061 -0
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Mikhail Kotelnikov
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md CHANGED
@@ -1,3 +1,324 @@
1
- # Temporary Holding Version
1
+ # @statewalker/cellb
2
2
 
3
- This version is a temporary placeholder for this package. An operational version to replace this has been submitted for review and is awaiting a staged release.
3
+ cellb is a Durable Objects runtime written in TypeScript that runs the same Workers code in a
4
+ browser and in Node. A Worker module and its Durable Object classes, unmodified, get synchronous
5
+ SQLite storage, input and output gates, stubs with `fetch` and RPC, alarms, WebSockets, Worker
6
+ Loaders and facets. In a browser the cells live in SQLite on OPFS inside Dedicated Workers and are
7
+ reached through a ServiceWorker; in Node they live in `node:sqlite`. Cells are stored exactly as
8
+ celld stores them, so one cell can move between cellb in a browser, cellb in Node and celld nodes
9
+ through one bucket.
10
+
11
+ ## Why it exists
12
+
13
+ Workers and Durable Objects normally need a server: Cloudflare, workerd (which Miniflare wraps) or
14
+ celld, all native binaries. That leaves out a browser tab, which can hold durable state in OPFS
15
+ but cannot run a binary, accept a socket, or start a server. cellb is the runtime written for that
16
+ constraint: pure TypeScript, synchronous SQLite through WebAssembly, HTTP through a ServiceWorker,
17
+ WebSockets over MessagePorts. The same core runs in Node, so a project behaves the same on both.
18
+
19
+ celld is the reference, not an afterthought. cellb answers every recorded celld request the same
20
+ way, stores cells in celld's format, and speaks celld's ownership and peer protocols, so a fleet can
21
+ mix celld nodes, cellb nodes and browsers.
22
+
23
+ ## How to use it
24
+
25
+ ```sh
26
+ npm install @statewalker/cellb
27
+ npm install esbuild # for loadNodeProject and bundleWorkerForBrowser (an optional peer dependency)
28
+ ```
29
+
30
+ The package has three entry points, one per platform, and two more for bundling. Published, it is
31
+ JavaScript with type declarations (`dist/`, built by `scripts/build.mjs`); in this repository the
32
+ entry points are the TypeScript sources, which the workspace's examples and tests import directly.
33
+ In a browser app, a bundler must handle `new Worker(new URL("…", import.meta.url))`: the LTX store
34
+ worker is started that way (checked with a Vite build, see `scripts/pack-check.mjs`).
35
+
36
+ | Import | What it gives |
37
+ |---|---|
38
+ | `@statewalker/cellb` | the runtime (`createCellRuntime`, `createProjectRuntime`), `DurableObject` and `WorkerEntrypoint`, the ports and `CellStorage`, `celldBucket`, the object stores (`s3ObjectStore`, `memoryObjectStore`, `filesApiObjectStore`), `filesApiLtxLog`, `filesApiChangeLog`, `filesApiBackup`, `wasmMemoryOpener` (in-memory SQLite WASM), links over ports, `CellRouter`, `createShardWorker`, the WebSocket classes, and the errors embedders catch |
39
+ | `@statewalker/cellb/node` | `nodeSqliteOpener`, `sqliteObjectStore` (celld dev's local bucket), `loadNodeProject`, `applyNodeD1Migrations`, and celld's peer protocol (`startCelldPeerServer`, `celldPeers`) |
40
+ | `@statewalker/cellb/browser` | `browserProjectRuntime`, `startProjectShards`, `joinProjectShards` and `serveProjectShard` (shards in workers and tabs), `installProjectRouter` (the ServiceWorker side), `sahpoolOpener`, `idbObjectStore`, `createProjectSite`, `serviceWorkerConnector` |
41
+ | `@statewalker/cellb/host-modules` | for bundlers other than cellb's own (webrun-bundler in a browser, say): the sources of `cloudflare:workers`, `node:*` and the Workers globals as cellb's realms provide them (`cellbHostModule`, `CELLS_GLOBALS_MODULE`) |
42
+ | `@statewalker/cellb/isolate-worker` | the Dedicated Worker that runs a Worker-Loader Worker in a realm of its own (`isolateScript`) |
43
+
44
+ Node 22.13 or later is needed for `node:sqlite`. `loadNodeProject` bundles a project with
45
+ `esbuild`, which the embedder must install.
46
+
47
+ A runtime is a `(Request) ⇒ Response` handler: mount `runtime.fetch` on `node:http`, a
48
+ ServiceWorker or a test, and call `runtime.close()` when done. `close()` takes final backups,
49
+ closes every database, then closes the storage.
50
+
51
+ ## Examples
52
+
53
+ ### A Worker and a Durable Object in Node
54
+
55
+ ```ts
56
+ import { createCellRuntime, DurableObject } from "@statewalker/cellb";
57
+ import { nodeSqliteOpener } from "@statewalker/cellb/node";
58
+
59
+ class Counter extends DurableObject {
60
+ increment(): number {
61
+ const sql = this.ctx.storage.sql;
62
+ sql.exec("CREATE TABLE IF NOT EXISTS c (id INTEGER PRIMARY KEY, n INTEGER)");
63
+ sql.exec("INSERT INTO c (id, n) VALUES (1, 1) ON CONFLICT(id) DO UPDATE SET n = n + 1");
64
+ return sql.exec<{ n: number }>("SELECT n FROM c WHERE id = 1").one().n;
65
+ }
66
+ }
67
+
68
+ const runtime = createCellRuntime({
69
+ worker: {
70
+ Counter,
71
+ default: {
72
+ fetch: async (req: Request, env: any) =>
73
+ Response.json({ n: await env.COUNTER.getByName(new URL(req.url).pathname).increment() }),
74
+ },
75
+ },
76
+ durableObjects: { COUNTER: { className: "Counter" } },
77
+ openSql: nodeSqliteOpener("./data"), // a file per cell in an existing directory; () = in memory
78
+ });
79
+ const res = await runtime.fetch(new Request("http://local/counter/a")); // {"n":1}
80
+ await runtime.close();
81
+ ```
82
+
83
+ ### A wrangler project on a bucket shared with celld
84
+
85
+ ```ts
86
+ import { celldBucket } from "@statewalker/cellb";
87
+ import { loadNodeProject, nodeSqliteOpener, sqliteObjectStore } from "@statewalker/cellb/node";
88
+ import { MemFilesApi } from "@statewalker/webrun-files-mem";
89
+
90
+ const dir = "./counter"; // wrangler.jsonc and its `main`
91
+ const { runtime, unsupported } = await loadNodeProject(dir, {
92
+ openSql: nodeSqliteOpener(), // in memory: every cell is restored from the bucket on first use
93
+ files: new MemFilesApi(), // blob store for large KV values
94
+ storage: celldBucket(sqliteObjectStore(`${dir}/.celld/dev/objects.sqlite3`), { node: "cellb-1" }),
95
+ });
96
+ console.log(unsupported); // configuration entries cellb does not support, e.g. []
97
+ await runtime.fetch(new Request("http://localhost/increment?name=a"));
98
+ await runtime.close(); // releases the cells, so celld can take them at once
99
+ ```
100
+
101
+ `sqliteObjectStore` is the bucket `celld dev` keeps in that file. A fleet on S3 uses
102
+ `s3ObjectStore({ endpoint, bucket, prefix, region, accessKeyId, secretAccessKey })` instead.
103
+
104
+ A project with a Workers AI binding (`"ai": { "binding": "AI" }`) needs an OpenAI-compatible
105
+ endpoint behind it (llama.cpp, Ollama, a hosted API); without one, `ai` is in `unsupported`:
106
+
107
+ ```ts
108
+ const { runtime } = await loadNodeProject(dir, {
109
+ openSql: nodeSqliteOpener(),
110
+ files: new MemFilesApi(),
111
+ ai: {
112
+ baseUrl: "http://127.0.0.1:8080/v1",
113
+ apiKey: process.env.LLM_KEY, // optional
114
+ models: { "@cf/meta/llama-3.3-70b-instruct-fp8-fast": "llama3", "*": "llama3" },
115
+ },
116
+ });
117
+ ```
118
+
119
+ `env.AI.run(model, inputs, options)` then answers text generation (`{ prompt }` or
120
+ `{ messages, tools?, stream? }`, in Workers AI's `{ response, tool_calls, usage }` shape or its
121
+ `data: {"response": …}` stream), the endpoint's own response with `returnRawResponse` (Flue's
122
+ `cloudflare` provider reads it), and embeddings (`{ text }`); other model kinds throw an `AiError`.
123
+ `browserProjectRuntime`, `startProjectShards` and `joinProjectShards` take the same `ai`.
124
+
125
+ ### A wrangler project in a browser worker
126
+
127
+ ```ts
128
+ // in a Dedicated Worker
129
+ import sqlite3InitModule from "@sqlite.org/sqlite-wasm";
130
+ import { browserProjectRuntime } from "@statewalker/cellb/browser";
131
+
132
+ const host = await browserProjectRuntime({
133
+ config, // the parsed wrangler.jsonc
134
+ code: { bundle }, // `main` bundled for browsers (or { site } for the in-browser HTTP server)
135
+ sqlite3: await sqlite3InitModule(),
136
+ pool: "my-app", // the OPFS SQLite pool; one per worker
137
+ filesDir: "my-app-files",
138
+ });
139
+ const res = await host.runtime.fetch(new Request("http://browser/"));
140
+ await host.close();
141
+ ```
142
+
143
+ Where the cells live is the `pool` (an opfs-sahpool SQLite pool, one per worker) unless `bucket`
144
+ says otherwise: `bucket: { idb, node }` keeps them in memory with page logs in IndexedDB, and
145
+ `bucket: { ltx, node }` keeps each one as LTX files in an OPFS directory, read page by page through
146
+ a SQLite VFS (cross-origin isolation needed). [docs/browser-host.md](docs/browser-host.md)
147
+ compares them.
148
+
149
+ To run each Worker-Loader Worker in a Dedicated Worker of its own, pass the shipped isolate worker:
150
+ with Vite, `import isolateWorker from "@statewalker/cellb/isolate-worker?worker&url"` and
151
+ `isolateScript: new URL(isolateWorker, import.meta.url)`.
152
+
153
+ [docs/browser-host.md](docs/browser-host.md) shows how requests reach this worker through a
154
+ ServiceWorker, how to shard an origin, and how to put the cells on an S3 bucket shared with celld.
155
+
156
+ ### A runtime behind a MessagePort
157
+
158
+ ```ts
159
+ import { connectRuntime, serveRuntime } from "@statewalker/cellb";
160
+
161
+ const { port1, port2 } = new MessageChannel();
162
+ await serveRuntime(port1, runtime); // in the worker that owns the cells
163
+ const remote = await connectRuntime(port2); // in the page, or another thread
164
+ const res = await remote.fetch(new Request("http://local/counter/a")); // bodies stream
165
+ ```
166
+
167
+ ## Internals
168
+
169
+ ### One core, and ports for what differs per platform
170
+
171
+ `src/core` knows nothing about the platform. It depends on the ports in `src/ports.ts`: a
172
+ synchronous SQLite driver per cell, a `CellStorage` bundle (durability, backup, ownership, R2
173
+ objects), a `Lease`, a `Clock`. The SQL port is synchronous because `ctx.storage.sql.exec()` is
174
+ synchronous in Workers, and both drivers are synchronous underneath (`node:sqlite`, and SQLite WASM
175
+ in a Dedicated Worker). [docs/architecture.md](docs/architecture.md) has the request path and the
176
+ layers.
177
+
178
+ ### Where everything is
179
+
180
+ ```
181
+ src/core runtime, stubs, ids, gates, SQL and KV storage, alarms, celld's internal schema
182
+ src/ports.ts the port interfaces and CellStorage
183
+ src/adapters node:sqlite and SQLite WASM drivers, the WASM page hook, the LTX VFS, FilesApi logs and backups
184
+ src/bucket object stores (S3, SQLite, memory, FilesApi), the LTX writer, celldBucket
185
+ src/ltx the LTX v3 codec (LZ4, CRC-64, xxHash32), page reads and compaction over a FilesApi,
186
+ the LTX store worker and its OPFS files (bucket: { ltx })
187
+ src/v8 V8 serialization in TypeScript
188
+ src/transport the HTTP surface of a runtime, and links over ports
189
+ src/router CellRouter and the shard worker (browser origins)
190
+ src/ws WebSocket client, WebSocketPair and 101 Response over MessagePorts
191
+ src/project wrangler projects for Node and browsers, the in-browser site, Workers globals
192
+ src/bindings assets, cache, cron, D1 migrations, HTMLRewriter, KV, Queues, R2, Workflows, Worker Loader
193
+ src/isolate loaded Workers in a realm of their own (worker threads, Dedicated Workers)
194
+ src/peer celld's peer protocol, server and client (Node)
195
+ src/node-compat node:* modules for nodejs_compat in browsers
196
+ src/miniflare-compat shims for Miniflare's vendored D1 simulator
197
+ vendor/ celld's KV, Queues and Workflows code; Miniflare's D1; html-rewriter-wasm
198
+ conformance/ celld examples, our projects, workerd's tests, recorded goldens
199
+ test/ unit tests (vitest, Node)
200
+ e2e/ tests against a real celld and RustFS (vitest; skipped without CELLD_BIN)
201
+ e2e-browser/ Playwright specs; pages/ holds the demo pages, the host app, ServiceWorker and
202
+ shard workers they drive; probes/ the exploratory browser probes (ServiceWorker idling)
203
+ scripts/ vendoring, the interop environment, mutation harnesses
204
+ probes/ Node hypothesis probes (SQLite locks, webrun-modules globals)
205
+ docs/ topic pages
206
+ ```
207
+
208
+ ### Decisions a reader would otherwise undo
209
+
210
+ - **celld's storage format is the only one** — ids, internal tables, V8-serialized values, LTX
211
+ logs — so cells move between cellb and celld without conversion
212
+ ([docs/celld-compatibility.md](docs/celld-compatibility.md)).
213
+ - **KV, Queues and Workflows run celld's own JavaScript**, vendored unchanged, so a queue, workflow
214
+ instance or KV namespace written by celld continues on cellb, and back.
215
+ - **celld judges conformance**, because cellb's storage must be celld's; workerd is recorded to
216
+ pin where celld differs, and judges only where cellb follows it on purpose: stubs, functions and
217
+ `RpcTarget`s pass by reference, and calls on a pending result are pipelined, which celld
218
+ refuses ([docs/conformance.md](docs/conformance.md)).
219
+ - **The gates use no `AsyncLocalStorage`**, which browsers lack.
220
+ - **A browser origin has a fixed number of shards**, because an OPFS pool is exclusive per worker
221
+ ([docs/browser-host.md](docs/browser-host.md)).
222
+ - **WebSockets travel over MessagePorts**, because a ServiceWorker cannot accept them. A port
223
+ has no send buffer to measure, so the server end acknowledges each message it dispatched and
224
+ a client's `bufferedAmount` counts the bytes not acknowledged yet: the WHATWG signal a sender
225
+ throttles on. workerd's server-side WebSocket has no `bufferedAmount`, and cellb's has none.
226
+ - **Each loaded Worker gets a realm of its own**, a thread or a Dedicated Worker: as in celld's
227
+ isolates, loaded code must not see the host's globals or env.
228
+ - **`idFromName` is synchronous**, with `@noble/hashes` instead of `crypto.subtle`, which is
229
+ asynchronous only; Worker code relies on getting the id at once.
230
+ - **Ownership depends on the deployment**: a Web Lock per shard in an origin, celld's ownership
231
+ records per cell on a fleet bucket
232
+ ([docs/ownership-and-durability.md](docs/ownership-and-durability.md)).
233
+ - **An idle database closes without letting go of its cell.** `hibernateAfterMs` drops an idle
234
+ object's instance; `closeDatabaseAfterMs` (longer, opt-in) also closes its SQLite handle, and the
235
+ next event reopens it. The cell lease is kept: taking the cell again would start a new generation,
236
+ whose first commit uploads the whole database. It is off by default because an in-memory database
237
+ without a log or backup would come back empty; a cell whose last changes are not backed up yet
238
+ stays open until they are.
239
+
240
+ What cellb does not do yet is in [docs/gaps.md](docs/gaps.md); how it matches celld is in
241
+ [docs/celld-compatibility.md](docs/celld-compatibility.md). The vocabulary is in
242
+ [CONTEXT.md](CONTEXT.md).
243
+
244
+ ### Dependencies, and why
245
+
246
+ - `@sqlite.org/sqlite-wasm` — SQLite in browsers (opfs-sahpool needs no COOP/COEP headers);
247
+ `sqlite-vec` for celld's `sqlite_vec` flag.
248
+ - `@noble/hashes` — synchronous SHA-256 and HMAC for object ids.
249
+ - `@statewalker/webrun-rpc`, `webrun-http-streams`, `webrun-streams` — HTTP with streaming bodies
250
+ over MessagePorts.
251
+ - `@statewalker/webrun-files` — the FilesApi for logs, backups and KV blobs;
252
+ `webrun-http-browser`, `webrun-site-builder`, `webrun-site-host` — ServiceWorker ingress and the
253
+ in-browser HTTP server.
254
+ - `worker-timers` — unthrottled heartbeat timers on a page's main thread.
255
+ - `buffer`, `events`, `util`, `assert`, `path-browserify` — `nodejs_compat` in browsers.
256
+
257
+ ### What will surprise you
258
+
259
+ - **`ERR_MODULE_NOT_FOUND` for `…/src/…/*.js` when importing this repository's sources with plain
260
+ `node`.** In the workspace the entry points are TypeScript; use vitest or tsx there, or the
261
+ published package (`pnpm pack`).
262
+ - **`Durable Object <Class>/<id> is owned by another node`** (`CellOwnedElsewhereError`). Another live
263
+ node owns the cell in the fleet bucket. Pass `peers` to route to it, or wait for it to release the
264
+ cell or for its node lease to expire.
265
+ - **`Durable Object reset because …`** (`BrokenObjectError`). A commit did not become durable, the
266
+ constructor's `blockConcurrencyWhile()` failed, or the cell lease was lost. The next call builds a
267
+ fresh instance.
268
+ - **`unable to open database file`** from `nodeSqliteOpener(dir)` or `sqliteObjectStore(file)`. The
269
+ directory does not exist; neither creates it. (`celld dev` creates `.celld/dev/` on its first run.)
270
+ - **`NoModificationAllowedError` in a browser.** Two workers opened the same OPFS pool. Each worker
271
+ needs its own `pool`.
272
+ - **`storage quota exceeded writing …`** or **`ltx: cell … is open in another tab or worker`** with
273
+ `bucket: { ltx }`: the origin's storage is full, or another tab's store holds the cell. See
274
+ [docs/browser-host.md](docs/browser-host.md#what-breaks).
275
+ - **`A stub was used after it was disposed.`** Loaded code, or its host, called a stub of the other
276
+ side after `stub[Symbol.dispose]()`. A stub into a loaded Worker holds its value until disposed or
277
+ garbage-collected ([docs/gaps.md](docs/gaps.md)).
278
+ - **`DataCloneError: Functions, RpcTargets and stubs cannot be sent to another node …`** A call to
279
+ an object owned by another tab, worker or peer carried one of them, as an argument or a result.
280
+ They pass by reference only within one runtime ([docs/gaps.md](docs/gaps.md)).
281
+ - **`DataCloneError: Only ctx.exports.<name>({ props }) stubs and class handles can be stored.`** A
282
+ Durable Object stub or an `RpcTarget` was put into storage; only `ctx.exports` handles with props
283
+ are storable.
284
+ - **Writes after the last backup are gone after a crash** when the storage is a periodic backup
285
+ alone (`filesApiBackup`). That is its RPO; a log (`celldBucket`, `filesApiLtxLog`) has none.
286
+
287
+ ### Commands
288
+
289
+ Run from `packages/cellb` after `pnpm install` at the repository root.
290
+
291
+ | Command | What it does |
292
+ |---|---|
293
+ | `pnpm test` | `test/` and `e2e/` (vitest). The `e2e/` tests skip without `CELLD_BIN`; with it, each run makes its own S3 bucket (`TEST_BUCKET`) on `RUSTFS_ENDPOINT`, or starts a RustFS of its own on a free port when that is unset. Every server a test starts takes a free port, so several checkouts can run at once. `pnpm test:unit` and `pnpm test:e2e` run one folder. |
294
+ | `pnpm typecheck` | TypeScript for `src/`, `test/`, `e2e/` and the browser pages |
295
+ | `pnpm build` | `dist/` as published: `tsc -p tsconfig.build.json`, the hand-written declarations, worker URLs pointed at the compiled `.js` (`scripts/build.mjs`); `pnpm pack` runs it |
296
+ | `node scripts/pack-check.mjs` | the package as published: packs it, checks the packed manifest (no `workspace:`/`catalog:` specifier, export targets present, every import of `dist/` and `vendor/` declared), installs the tarball with npm outside the workspace and runs it from plain Node, TypeScript and a Vite app in Chromium (`PW_CHROMIUM` for the binary) |
297
+ | `pnpm e2e` | Chromium tests (Playwright, `e2e-browser/`), on a dev server at a free port, with the same bucket setup as `pnpm test`. Chromium is found from `PW_CHROMIUM`, then Playwright's own, then the one `@sparticuz/chromium` packages in `node_modules`. |
298
+ | `pnpm e2e:firefox`, `pnpm e2e:webkit` | the same suite in Playwright's Firefox or WebKit (`npx playwright install firefox webkit`; `PW_BROWSERS` takes a comma-separated list). What differs in Firefox: [docs/browser-host.md](docs/browser-host.md#firefox-and-webkit). |
299
+ | `pnpm interop` | `e2e/` and the celld conformance in Node: `scripts/interop-env.sh` downloads the pinned celld and RustFS into `~/.cache/cellb-interop`, starts a shared RustFS on `127.0.0.1:9200` (`RUSTFS_PORT`), and sets `CELLD_BIN`, `ESBUILD_DIR`, `RUSTFS_ENDPOINT` |
300
+ | `pnpm interop:browser` | the same for Chromium |
301
+ | `pnpm mutate`, `pnpm mutate:browser` | mutation harnesses in a throw-away copy, after a baseline run; `MUTANTS=0,3` runs a subset |
302
+ | `pnpm browser:dev` | the demo pages in `e2e-browser/pages`: `/` (one cell worker), `/sharded/`, `/backup/`, and `/host/`, the host app that runs a project folder |
303
+
304
+ ### Releasing
305
+
306
+ Releases are made by hand from a local checkout, with the maintainer's npm login and 2FA; CI
307
+ never publishes (the statewalker convention). A change to the package carries a changeset
308
+ (`pnpm changeset` at the repository root). To release:
309
+
310
+ ```sh
311
+ pnpm changeset version # versions and changelog from the pending changesets
312
+ git commit -am "chore: version packages"
313
+ pnpm changeset publish # publishes what npm lacks (prepack builds dist/), tags it
314
+ git push --follow-tags
315
+ ```
316
+
317
+ Run `node scripts/pack-check.mjs` first: it checks exactly what `publish` uploads.
318
+
319
+ ## License
320
+
321
+ MIT. Vendored code keeps its own licence, named in each file's first line or a `LICENSE` beside it:
322
+ `vendor/celld/` and `conformance/celld-examples/` are Apache-2.0 (celld), `vendor/miniflare/` is
323
+ MIT OR Apache-2.0 (Miniflare) and Apache-2.0 (workerd's D1 client), `vendor/html-rewriter-wasm/` is
324
+ BSD-3-Clause, and `conformance/workerd-tests/` carry workerd's Apache-2.0.
@@ -0,0 +1,16 @@
1
+ import type { FilesApi } from "@statewalker/webrun-files";
2
+ import type { CellBackup } from "../ports.js";
3
+ export interface FilesApiBackupOptions {
4
+ /** Folder inside the FilesApi (default "/cells"). */
5
+ dir?: string;
6
+ intervalMs?: number;
7
+ onError?: (cellKey: string, error: unknown) => void;
8
+ }
9
+ /**
10
+ * CellBackup on a @statewalker/webrun-files FilesApi (OPFS, S3, memory, node fs…).
11
+ * Layout per cell: {dir}/{Class}/{hexId}.sqlite (+ .json with the BackupInfo).
12
+ * The image is written to a temporary file and `move`d over the previous one, so a
13
+ * crash during upload never leaves a truncated image as "the" backup (on backends
14
+ * whose move is atomic; S3 has no rename — there the last complete PUT wins).
15
+ */
16
+ export declare function filesApiBackup(files: FilesApi, opts?: FilesApiBackupOptions): CellBackup;
@@ -0,0 +1,32 @@
1
+ import { tryReadFile } from "@statewalker/webrun-files";
2
+ /**
3
+ * CellBackup on a @statewalker/webrun-files FilesApi (OPFS, S3, memory, node fs…).
4
+ * Layout per cell: {dir}/{Class}/{hexId}.sqlite (+ .json with the BackupInfo).
5
+ * The image is written to a temporary file and `move`d over the previous one, so a
6
+ * crash during upload never leaves a truncated image as "the" backup (on backends
7
+ * whose move is atomic; S3 has no rename — there the last complete PUT wins).
8
+ */
9
+ export function filesApiBackup(files, opts = {}) {
10
+ const dir = (opts.dir ?? "/cells").replace(/\/$/, "");
11
+ const path = (cellKey) => `${dir}/${cellKey
12
+ .split("/")
13
+ .map((p) => p.replace(/[^\w.-]/g, "_"))
14
+ .join("/")}.sqlite`;
15
+ return {
16
+ intervalMs: opts.intervalMs,
17
+ onError: opts.onError,
18
+ async save(cellKey, image, info) {
19
+ const target = path(cellKey);
20
+ const tmp = `${target}.${info.seq}.${info.takenAt}.tmp`;
21
+ await files.write(tmp, [image]);
22
+ await files.move(tmp, target);
23
+ await files.write(`${target}.json`, [
24
+ new TextEncoder().encode(JSON.stringify({ cellKey, ...info, bytes: image.byteLength })),
25
+ ]);
26
+ },
27
+ async load(cellKey) {
28
+ return tryReadFile(files, path(cellKey));
29
+ },
30
+ };
31
+ }
32
+ //# sourceMappingURL=files-backup.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files-backup.js","sourceRoot":"","sources":["../../src/adapters/files-backup.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AAUxD;;;;;;GAMG;AACH,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,IAAI,GAA0B,EAAE;IAC9E,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,CAAC,OAAe,EAAE,EAAE,CAC/B,GAAG,GAAG,IAAI,OAAO;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;SACtC,IAAI,CAAC,GAAG,CAAC,SAAS,CAAC;IACxB,OAAO;QACL,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QACrB,KAAK,CAAC,IAAI,CAAC,OAAe,EAAE,KAAiB,EAAE,IAAgB;YAC7D,MAAM,MAAM,GAAG,IAAI,CAAC,OAAO,CAAC,CAAC;YAC7B,MAAM,GAAG,GAAG,GAAG,MAAM,IAAI,IAAI,CAAC,GAAG,IAAI,IAAI,CAAC,OAAO,MAAM,CAAC;YACxD,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;YAChC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,MAAM,CAAC,CAAC;YAC9B,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,MAAM,OAAO,EAAE;gBAClC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,KAAK,EAAE,KAAK,CAAC,UAAU,EAAE,CAAC,CAAC;aACxF,CAAC,CAAC;QACL,CAAC;QACD,KAAK,CAAC,IAAI,CAAC,OAAe;YACxB,OAAO,WAAW,CAAC,KAAK,EAAE,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC;QAC3C,CAAC;KACF,CAAC;AACJ,CAAC","sourcesContent":["import type { FilesApi } from \"@statewalker/webrun-files\";\nimport { tryReadFile } from \"@statewalker/webrun-files\";\nimport type { BackupInfo, CellBackup } from \"../ports.js\";\n\nexport interface FilesApiBackupOptions {\n /** Folder inside the FilesApi (default \"/cells\"). */\n dir?: string;\n intervalMs?: number;\n onError?: (cellKey: string, error: unknown) => void;\n}\n\n/**\n * CellBackup on a @statewalker/webrun-files FilesApi (OPFS, S3, memory, node fs…).\n * Layout per cell: {dir}/{Class}/{hexId}.sqlite (+ .json with the BackupInfo).\n * The image is written to a temporary file and `move`d over the previous one, so a\n * crash during upload never leaves a truncated image as \"the\" backup (on backends\n * whose move is atomic; S3 has no rename — there the last complete PUT wins).\n */\nexport function filesApiBackup(files: FilesApi, opts: FilesApiBackupOptions = {}): CellBackup {\n const dir = (opts.dir ?? \"/cells\").replace(/\\/$/, \"\");\n const path = (cellKey: string) =>\n `${dir}/${cellKey\n .split(\"/\")\n .map((p) => p.replace(/[^\\w.-]/g, \"_\"))\n .join(\"/\")}.sqlite`;\n return {\n intervalMs: opts.intervalMs,\n onError: opts.onError,\n async save(cellKey: string, image: Uint8Array, info: BackupInfo) {\n const target = path(cellKey);\n const tmp = `${target}.${info.seq}.${info.takenAt}.tmp`;\n await files.write(tmp, [image]);\n await files.move(tmp, target);\n await files.write(`${target}.json`, [\n new TextEncoder().encode(JSON.stringify({ cellKey, ...info, bytes: image.byteLength })),\n ]);\n },\n async load(cellKey: string) {\n return tryReadFile(files, path(cellKey));\n },\n };\n}\n"]}
@@ -0,0 +1,33 @@
1
+ import type { FilesApi } from "@statewalker/webrun-files";
2
+ import type { CellBackup, CellDurability, CommitChanges } from "../ports.js";
3
+ /**
4
+ * Transaction-log durability on a webrun-files FilesApi (OPFS, S3, node fs, memory…) —
5
+ * celld's model (every acknowledged write is in the bucket) with SQLite *changesets*
6
+ * instead of LTX page frames:
7
+ *
8
+ * {dir}/{cell}/log/{epoch}-{logSeq}.seg one segment per committed transaction
9
+ * {dir}/{cell}/snapshot.sqlite (+ .json) periodic image, carrying its logSeq
10
+ *
11
+ * `committed()` resolves when the segment is written, and the output gate holds every
12
+ * reply until then: RPO = 0 against the FilesApi. Restore = snapshot + the segments after
13
+ * its logSeq. After a successful snapshot the segments it contains are deleted.
14
+ * `epoch` (e.g. a lease fencing token) prefixes segment names: segments of different owners
15
+ * never overwrite each other, and a stale owner's segments are void from the first sequence
16
+ * number a newer owner wrote (celld fences the same way with the epoch in its log keys).
17
+ */
18
+ export interface ChangeLogOptions {
19
+ dir?: string;
20
+ /** Fencing epoch of the current owner (default 0). */
21
+ epoch?: number;
22
+ /** Snapshot interval (compaction); default: no periodic snapshot. */
23
+ intervalMs?: number;
24
+ onError?: (cellKey: string, error: unknown) => void;
25
+ }
26
+ export declare function encodeSegment(c: CommitChanges, epoch: number): Uint8Array;
27
+ export declare function decodeSegment(bytes: Uint8Array): CommitChanges & {
28
+ epoch: number;
29
+ };
30
+ export declare function filesApiChangeLog(files: FilesApi, opts?: ChangeLogOptions): {
31
+ durability: CellDurability;
32
+ backup: CellBackup;
33
+ };
@@ -0,0 +1,96 @@
1
+ import { tryReadFile } from "@statewalker/webrun-files";
2
+ const MAGIC = new TextEncoder().encode("CLOG1");
3
+ const pad = (n, w) => String(n).padStart(w, "0");
4
+ export function encodeSegment(c, epoch) {
5
+ const header = new TextEncoder().encode(JSON.stringify({
6
+ logSeq: c.logSeq,
7
+ epoch,
8
+ ddl: c.ddl,
9
+ statements: c.statements,
10
+ at: Date.now(),
11
+ }));
12
+ const out = new Uint8Array(MAGIC.length + 4 + header.length + c.changeset.byteLength);
13
+ out.set(MAGIC, 0);
14
+ new DataView(out.buffer).setUint32(MAGIC.length, header.length);
15
+ out.set(header, MAGIC.length + 4);
16
+ out.set(c.changeset, MAGIC.length + 4 + header.length);
17
+ return out;
18
+ }
19
+ export function decodeSegment(bytes) {
20
+ for (let i = 0; i < MAGIC.length; i++)
21
+ if (bytes[i] !== MAGIC[i])
22
+ throw new Error("not a change-log segment");
23
+ const len = new DataView(bytes.buffer, bytes.byteOffset).getUint32(MAGIC.length);
24
+ const header = JSON.parse(new TextDecoder().decode(bytes.subarray(MAGIC.length + 4, MAGIC.length + 4 + len)));
25
+ return { ...header, changeset: bytes.slice(MAGIC.length + 4 + len) };
26
+ }
27
+ export function filesApiChangeLog(files, opts = {}) {
28
+ const dir = (opts.dir ?? "/cells").replace(/\/$/, "");
29
+ const epoch = opts.epoch ?? 0;
30
+ const cellDir = (cellKey) => `${dir}/${cellKey
31
+ .split("/")
32
+ .map((p) => p.replace(/[^\w.-]/g, "_"))
33
+ .join("/")}`;
34
+ const segments = async (cellKey) => {
35
+ const logDir = `${cellDir(cellKey)}/log`;
36
+ const out = [];
37
+ if (!(await files.exists(logDir)))
38
+ return out;
39
+ for await (const f of files.list(logDir)) {
40
+ const m = /(\d+)-(\d+)\.seg$/.exec(f.path);
41
+ if (f.kind === "file" && m)
42
+ out.push({ path: f.path, epoch: Number(m[1]), logSeq: Number(m[2]) });
43
+ }
44
+ // Fencing: a segment written by an older owner (epoch e) is void from the first logSeq
45
+ // that a newer owner (epoch > e) wrote — a stale owner's late writes never replay.
46
+ const firstOfEpoch = new Map();
47
+ for (const s of out)
48
+ firstOfEpoch.set(s.epoch, Math.min(firstOfEpoch.get(s.epoch) ?? s.logSeq, s.logSeq));
49
+ const valid = out.filter((s) => ![...firstOfEpoch].some(([e, first]) => e > s.epoch && first <= s.logSeq));
50
+ return valid.sort((a, b) => a.logSeq - b.logSeq || b.epoch - a.epoch);
51
+ };
52
+ const log = {
53
+ captureChanges: true,
54
+ intervalMs: opts.intervalMs,
55
+ onError: opts.onError,
56
+ async committed(cellKey, { changes }) {
57
+ if (!changes)
58
+ return;
59
+ const e = changes.epoch ?? epoch; // the cell's lease epoch wins over the static one
60
+ const name = `${pad(e, 10)}-${pad(changes.logSeq, 12)}.seg`;
61
+ await files.write(`${cellDir(cellKey)}/log/${name}`, [encodeSegment(changes, e)]);
62
+ },
63
+ async save(cellKey, image, info) {
64
+ const base = cellDir(cellKey);
65
+ const tmp = `${base}/snapshot.${info.takenAt}.tmp`;
66
+ await files.write(tmp, [image]);
67
+ await files.move(tmp, `${base}/snapshot.sqlite`);
68
+ await files.write(`${base}/snapshot.json`, [
69
+ new TextEncoder().encode(JSON.stringify({ cellKey, ...info })),
70
+ ]);
71
+ // Compaction: the image contains every segment up to its logSeq.
72
+ for (const s of await segments(cellKey))
73
+ if (s.logSeq <= (info.logSeq ?? 0))
74
+ await files.remove(s.path);
75
+ },
76
+ async load(cellKey) {
77
+ return tryReadFile(files, `${cellDir(cellKey)}/snapshot.sqlite`);
78
+ },
79
+ async *replay(cellKey, afterLogSeq) {
80
+ let last = afterLogSeq;
81
+ for (const s of await segments(cellKey)) {
82
+ if (s.logSeq <= last)
83
+ continue;
84
+ if (s.logSeq !== last + 1)
85
+ throw new Error(`change log of ${cellKey}: segment ${last + 1} is missing`);
86
+ const bytes = await tryReadFile(files, s.path);
87
+ if (!bytes)
88
+ throw new Error(`change log of ${cellKey}: cannot read ${s.path}`);
89
+ last = s.logSeq;
90
+ yield decodeSegment(bytes);
91
+ }
92
+ },
93
+ };
94
+ return { durability: log, backup: log };
95
+ }
96
+ //# sourceMappingURL=files-changelog.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files-changelog.js","sourceRoot":"","sources":["../../src/adapters/files-changelog.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,WAAW,EAAE,MAAM,2BAA2B,CAAC;AA2BxD,MAAM,KAAK,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,OAAO,CAAC,CAAC;AAChD,MAAM,GAAG,GAAG,CAAC,CAAS,EAAE,CAAS,EAAE,EAAE,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,QAAQ,CAAC,CAAC,EAAE,GAAG,CAAC,CAAC;AAEjE,MAAM,UAAU,aAAa,CAAC,CAAgB,EAAE,KAAa;IAC3D,MAAM,MAAM,GAAG,IAAI,WAAW,EAAE,CAAC,MAAM,CACrC,IAAI,CAAC,SAAS,CAAC;QACb,MAAM,EAAE,CAAC,CAAC,MAAM;QAChB,KAAK;QACL,GAAG,EAAE,CAAC,CAAC,GAAG;QACV,UAAU,EAAE,CAAC,CAAC,UAAU;QACxB,EAAE,EAAE,IAAI,CAAC,GAAG,EAAE;KACf,CAAC,CACH,CAAC;IACF,MAAM,GAAG,GAAG,IAAI,UAAU,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,GAAG,CAAC,CAAC,SAAS,CAAC,UAAU,CAAC,CAAC;IACtF,GAAG,CAAC,GAAG,CAAC,KAAK,EAAE,CAAC,CAAC,CAAC;IAClB,IAAI,QAAQ,CAAC,GAAG,CAAC,MAAM,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,EAAE,MAAM,CAAC,MAAM,CAAC,CAAC;IAChE,GAAG,CAAC,GAAG,CAAC,MAAM,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,CAAC,CAAC;IAClC,GAAG,CAAC,GAAG,CAAC,CAAC,CAAC,SAAS,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IACvD,OAAO,GAAG,CAAC;AACb,CAAC;AAED,MAAM,UAAU,aAAa,CAAC,KAAiB;IAC7C,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,KAAK,CAAC,MAAM,EAAE,CAAC,EAAE;QACnC,IAAI,KAAK,CAAC,CAAC,CAAC,KAAK,KAAK,CAAC,CAAC,CAAC;YAAE,MAAM,IAAI,KAAK,CAAC,0BAA0B,CAAC,CAAC;IACzE,MAAM,GAAG,GAAG,IAAI,QAAQ,CAAC,KAAK,CAAC,MAAM,EAAE,KAAK,CAAC,UAAU,CAAC,CAAC,SAAS,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;IACjF,MAAM,MAAM,GAAG,IAAI,CAAC,KAAK,CACvB,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,KAAK,CAAC,QAAQ,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,EAAE,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,CAAC,CACnF,CAAC;IACF,OAAO,EAAE,GAAG,MAAM,EAAE,SAAS,EAAE,KAAK,CAAC,KAAK,CAAC,KAAK,CAAC,MAAM,GAAG,CAAC,GAAG,GAAG,CAAC,EAAE,CAAC;AACvE,CAAC;AAED,MAAM,UAAU,iBAAiB,CAC/B,KAAe,EACf,IAAI,GAAqB,EAAE;IAE3B,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,KAAK,GAAG,IAAI,CAAC,KAAK,IAAI,CAAC,CAAC;IAC9B,MAAM,OAAO,GAAG,CAAC,OAAe,EAAE,EAAE,CAClC,GAAG,GAAG,IAAI,OAAO;SACd,KAAK,CAAC,GAAG,CAAC;SACV,GAAG,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC,UAAU,EAAE,GAAG,CAAC,CAAC;SACtC,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC;IACjB,MAAM,QAAQ,GAAG,KAAK,EAAE,OAAe,EAAE,EAAE;QACzC,MAAM,MAAM,GAAG,GAAG,OAAO,CAAC,OAAO,CAAC,MAAM,CAAC;QACzC,MAAM,GAAG,GAAsD,EAAE,CAAC;QAClE,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;YAAE,OAAO,GAAG,CAAC;QAC9C,IAAI,KAAK,EAAE,MAAM,CAAC,IAAI,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,EAAE,CAAC;YACzC,MAAM,CAAC,GAAG,mBAAmB,CAAC,IAAI,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;YAC3C,IAAI,CAAC,CAAC,IAAI,KAAK,MAAM,IAAI,CAAC;gBACxB,GAAG,CAAC,IAAI,CAAC,EAAE,IAAI,EAAE,CAAC,CAAC,IAAI,EAAE,KAAK,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,MAAM,EAAE,MAAM,CAAC,CAAC,CAAC,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC;QAC1E,CAAC;QACD,uFAAuF;QACvF,mFAAmF;QACnF,MAAM,YAAY,GAAG,IAAI,GAAG,EAAkB,CAAC;QAC/C,KAAK,MAAM,CAAC,IAAI,GAAG;YACjB,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,EAAE,IAAI,CAAC,GAAG,CAAC,YAAY,CAAC,GAAG,CAAC,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,CAAC,MAAM,EAAE,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC;QACvF,MAAM,KAAK,GAAG,GAAG,CAAC,MAAM,CACtB,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,GAAG,YAAY,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,EAAE,KAAK,CAAC,EAAE,EAAE,CAAC,CAAC,GAAG,CAAC,CAAC,KAAK,IAAI,KAAK,IAAI,CAAC,CAAC,MAAM,CAAC,CACjF,CAAC;QACF,OAAO,KAAK,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,MAAM,GAAG,CAAC,CAAC,MAAM,IAAI,CAAC,CAAC,KAAK,GAAG,CAAC,CAAC,KAAK,CAAC,CAAC;IACxE,CAAC,CAAC;IAEF,MAAM,GAAG,GAAgC;QACvC,cAAc,EAAE,IAAI;QACpB,UAAU,EAAE,IAAI,CAAC,UAAU;QAC3B,OAAO,EAAE,IAAI,CAAC,OAAO;QAErB,KAAK,CAAC,SAAS,CAAC,OAAO,EAAE,EAAE,OAAO,EAAE;YAClC,IAAI,CAAC,OAAO;gBAAE,OAAO;YACrB,MAAM,CAAC,GAAG,OAAO,CAAC,KAAK,IAAI,KAAK,CAAC,CAAC,kDAAkD;YACpF,MAAM,IAAI,GAAG,GAAG,GAAG,CAAC,CAAC,EAAE,EAAE,CAAC,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,MAAM,CAAC;YAC5D,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,OAAO,CAAC,OAAO,CAAC,QAAQ,IAAI,EAAE,EAAE,CAAC,aAAa,CAAC,OAAO,EAAE,CAAC,CAAC,CAAC,CAAC,CAAC;QACpF,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,OAAe,EAAE,KAAiB,EAAE,IAAgB;YAC7D,MAAM,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;YAC9B,MAAM,GAAG,GAAG,GAAG,IAAI,aAAa,IAAI,CAAC,OAAO,MAAM,CAAC;YACnD,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,EAAE,CAAC,KAAK,CAAC,CAAC,CAAC;YAChC,MAAM,KAAK,CAAC,IAAI,CAAC,GAAG,EAAE,GAAG,IAAI,kBAAkB,CAAC,CAAC;YACjD,MAAM,KAAK,CAAC,KAAK,CAAC,GAAG,IAAI,gBAAgB,EAAE;gBACzC,IAAI,WAAW,EAAE,CAAC,MAAM,CAAC,IAAI,CAAC,SAAS,CAAC,EAAE,OAAO,EAAE,GAAG,IAAI,EAAE,CAAC,CAAC;aAC/D,CAAC,CAAC;YACH,iEAAiE;YACjE,KAAK,MAAM,CAAC,IAAI,MAAM,QAAQ,CAAC,OAAO,CAAC;gBACrC,IAAI,CAAC,CAAC,MAAM,IAAI,CAAC,IAAI,CAAC,MAAM,IAAI,CAAC,CAAC;oBAAE,MAAM,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC;QACnE,CAAC;QAED,KAAK,CAAC,IAAI,CAAC,OAAe;YACxB,OAAO,WAAW,CAAC,KAAK,EAAE,GAAG,OAAO,CAAC,OAAO,CAAC,kBAAkB,CAAC,CAAC;QACnE,CAAC;QAED,KAAK,CAAC,CAAC,MAAM,CAAC,OAAe,EAAE,WAAmB;YAChD,IAAI,IAAI,GAAG,WAAW,CAAC;YACvB,KAAK,MAAM,CAAC,IAAI,MAAM,QAAQ,CAAC,OAAO,CAAC,EAAE,CAAC;gBACxC,IAAI,CAAC,CAAC,MAAM,IAAI,IAAI;oBAAE,SAAS;gBAC/B,IAAI,CAAC,CAAC,MAAM,KAAK,IAAI,GAAG,CAAC;oBACvB,MAAM,IAAI,KAAK,CAAC,iBAAiB,OAAO,aAAa,IAAI,GAAG,CAAC,aAAa,CAAC,CAAC;gBAC9E,MAAM,KAAK,GAAG,MAAM,WAAW,CAAC,KAAK,EAAE,CAAC,CAAC,IAAI,CAAC,CAAC;gBAC/C,IAAI,CAAC,KAAK;oBAAE,MAAM,IAAI,KAAK,CAAC,iBAAiB,OAAO,iBAAiB,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;gBAC/E,IAAI,GAAG,CAAC,CAAC,MAAM,CAAC;gBAChB,MAAM,aAAa,CAAC,KAAK,CAAC,CAAC;YAC7B,CAAC;QACH,CAAC;KACF,CAAC;IACF,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAC1C,CAAC","sourcesContent":["import type { FilesApi } from \"@statewalker/webrun-files\";\nimport { tryReadFile } from \"@statewalker/webrun-files\";\nimport type { BackupInfo, CellBackup, CellDurability, CommitChanges } from \"../ports.js\";\n\n/**\n * Transaction-log durability on a webrun-files FilesApi (OPFS, S3, node fs, memory…) —\n * celld's model (every acknowledged write is in the bucket) with SQLite *changesets*\n * instead of LTX page frames:\n *\n * {dir}/{cell}/log/{epoch}-{logSeq}.seg one segment per committed transaction\n * {dir}/{cell}/snapshot.sqlite (+ .json) periodic image, carrying its logSeq\n *\n * `committed()` resolves when the segment is written, and the output gate holds every\n * reply until then: RPO = 0 against the FilesApi. Restore = snapshot + the segments after\n * its logSeq. After a successful snapshot the segments it contains are deleted.\n * `epoch` (e.g. a lease fencing token) prefixes segment names: segments of different owners\n * never overwrite each other, and a stale owner's segments are void from the first sequence\n * number a newer owner wrote (celld fences the same way with the epoch in its log keys).\n */\nexport interface ChangeLogOptions {\n dir?: string;\n /** Fencing epoch of the current owner (default 0). */\n epoch?: number;\n /** Snapshot interval (compaction); default: no periodic snapshot. */\n intervalMs?: number;\n onError?: (cellKey: string, error: unknown) => void;\n}\n\nconst MAGIC = new TextEncoder().encode(\"CLOG1\");\nconst pad = (n: number, w: number) => String(n).padStart(w, \"0\");\n\nexport function encodeSegment(c: CommitChanges, epoch: number): Uint8Array {\n const header = new TextEncoder().encode(\n JSON.stringify({\n logSeq: c.logSeq,\n epoch,\n ddl: c.ddl,\n statements: c.statements,\n at: Date.now(),\n }),\n );\n const out = new Uint8Array(MAGIC.length + 4 + header.length + c.changeset.byteLength);\n out.set(MAGIC, 0);\n new DataView(out.buffer).setUint32(MAGIC.length, header.length);\n out.set(header, MAGIC.length + 4);\n out.set(c.changeset, MAGIC.length + 4 + header.length);\n return out;\n}\n\nexport function decodeSegment(bytes: Uint8Array): CommitChanges & { epoch: number } {\n for (let i = 0; i < MAGIC.length; i++)\n if (bytes[i] !== MAGIC[i]) throw new Error(\"not a change-log segment\");\n const len = new DataView(bytes.buffer, bytes.byteOffset).getUint32(MAGIC.length);\n const header = JSON.parse(\n new TextDecoder().decode(bytes.subarray(MAGIC.length + 4, MAGIC.length + 4 + len)),\n );\n return { ...header, changeset: bytes.slice(MAGIC.length + 4 + len) };\n}\n\nexport function filesApiChangeLog(\n files: FilesApi,\n opts: ChangeLogOptions = {},\n): { durability: CellDurability; backup: CellBackup } {\n const dir = (opts.dir ?? \"/cells\").replace(/\\/$/, \"\");\n const epoch = opts.epoch ?? 0;\n const cellDir = (cellKey: string) =>\n `${dir}/${cellKey\n .split(\"/\")\n .map((p) => p.replace(/[^\\w.-]/g, \"_\"))\n .join(\"/\")}`;\n const segments = async (cellKey: string) => {\n const logDir = `${cellDir(cellKey)}/log`;\n const out: { path: string; epoch: number; logSeq: number }[] = [];\n if (!(await files.exists(logDir))) return out;\n for await (const f of files.list(logDir)) {\n const m = /(\\d+)-(\\d+)\\.seg$/.exec(f.path);\n if (f.kind === \"file\" && m)\n out.push({ path: f.path, epoch: Number(m[1]), logSeq: Number(m[2]) });\n }\n // Fencing: a segment written by an older owner (epoch e) is void from the first logSeq\n // that a newer owner (epoch > e) wrote — a stale owner's late writes never replay.\n const firstOfEpoch = new Map<number, number>();\n for (const s of out)\n firstOfEpoch.set(s.epoch, Math.min(firstOfEpoch.get(s.epoch) ?? s.logSeq, s.logSeq));\n const valid = out.filter(\n (s) => ![...firstOfEpoch].some(([e, first]) => e > s.epoch && first <= s.logSeq),\n );\n return valid.sort((a, b) => a.logSeq - b.logSeq || b.epoch - a.epoch);\n };\n\n const log: CellDurability & CellBackup = {\n captureChanges: true,\n intervalMs: opts.intervalMs,\n onError: opts.onError,\n\n async committed(cellKey, { changes }) {\n if (!changes) return;\n const e = changes.epoch ?? epoch; // the cell's lease epoch wins over the static one\n const name = `${pad(e, 10)}-${pad(changes.logSeq, 12)}.seg`;\n await files.write(`${cellDir(cellKey)}/log/${name}`, [encodeSegment(changes, e)]);\n },\n\n async save(cellKey: string, image: Uint8Array, info: BackupInfo) {\n const base = cellDir(cellKey);\n const tmp = `${base}/snapshot.${info.takenAt}.tmp`;\n await files.write(tmp, [image]);\n await files.move(tmp, `${base}/snapshot.sqlite`);\n await files.write(`${base}/snapshot.json`, [\n new TextEncoder().encode(JSON.stringify({ cellKey, ...info })),\n ]);\n // Compaction: the image contains every segment up to its logSeq.\n for (const s of await segments(cellKey))\n if (s.logSeq <= (info.logSeq ?? 0)) await files.remove(s.path);\n },\n\n async load(cellKey: string) {\n return tryReadFile(files, `${cellDir(cellKey)}/snapshot.sqlite`);\n },\n\n async *replay(cellKey: string, afterLogSeq: number) {\n let last = afterLogSeq;\n for (const s of await segments(cellKey)) {\n if (s.logSeq <= last) continue;\n if (s.logSeq !== last + 1)\n throw new Error(`change log of ${cellKey}: segment ${last + 1} is missing`);\n const bytes = await tryReadFile(files, s.path);\n if (!bytes) throw new Error(`change log of ${cellKey}: cannot read ${s.path}`);\n last = s.logSeq;\n yield decodeSegment(bytes);\n }\n },\n };\n return { durability: log, backup: log };\n}\n"]}
@@ -0,0 +1,35 @@
1
+ import type { FilesApi } from "@statewalker/webrun-files";
2
+ /**
3
+ * The LTX log on a webrun-files FilesApi (OPFS in browsers, a directory on Node), without
4
+ * ownership: the same writer as celldBucket (src/bucket/ltx-log.ts), on the FilesApi as an
5
+ * object store. Keys are celld's: `<dir>/<Class>:<id>/ltx/e<epoch>/0000/<min>-<max>.ltx`
6
+ * (`dir` defaults to "/cells"), so the files read the same in celld.
7
+ */
8
+ export interface FilesApiLtxLogOptions {
9
+ /** Where the `cells/` keys live; defaults to "/cells". */
10
+ dir?: string;
11
+ /** The epoch the files are written in (no ownership); 0 by default. */
12
+ epoch?: number;
13
+ intervalMs?: number;
14
+ onError?: (cellKey: string, error: unknown) => void;
15
+ }
16
+ export declare function filesApiLtxLog(files: FilesApi, opts?: FilesApiLtxLogOptions): {
17
+ durability: import("../ports.js").CellDurability & import("../ports.js").CellBackup & {
18
+ logCommit: import("../ports.js").CellDurability["committed"];
19
+ } & {
20
+ begin(cellKey: string, epoch: number): void;
21
+ forget(cellKey: string): void;
22
+ } & {
23
+ intervalMs: number | undefined;
24
+ onError: ((cellKey: string, error: unknown) => void) | undefined;
25
+ };
26
+ backup: import("../ports.js").CellDurability & import("../ports.js").CellBackup & {
27
+ logCommit: import("../ports.js").CellDurability["committed"];
28
+ } & {
29
+ begin(cellKey: string, epoch: number): void;
30
+ forget(cellKey: string): void;
31
+ } & {
32
+ intervalMs: number | undefined;
33
+ onError: ((cellKey: string, error: unknown) => void) | undefined;
34
+ };
35
+ };
@@ -0,0 +1,9 @@
1
+ import { ltxLog } from "../bucket/ltx-log.js";
2
+ import { filesApiObjectStore } from "../bucket/object-store.js";
3
+ export function filesApiLtxLog(files, opts = {}) {
4
+ const dir = (opts.dir ?? "/cells").replace(/\/$/, "");
5
+ const root = dir.endsWith("/cells") ? dir.slice(0, -"/cells".length) : dir;
6
+ const log = Object.assign(ltxLog(filesApiObjectStore(files, root), { defaultEpoch: opts.epoch ?? 0, compact: true }), { intervalMs: opts.intervalMs, onError: opts.onError });
7
+ return { durability: log, backup: log };
8
+ }
9
+ //# sourceMappingURL=files-ltx.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"files-ltx.js","sourceRoot":"","sources":["../../src/adapters/files-ltx.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,MAAM,EAAE,MAAM,sBAAsB,CAAC;AAC9C,OAAO,EAAE,mBAAmB,EAAE,MAAM,2BAA2B,CAAC;AAiBhE,MAAM,UAAU,cAAc,CAAC,KAAe,EAAE,IAAI,GAA0B,EAAE;IAC9E,MAAM,GAAG,GAAG,CAAC,IAAI,CAAC,GAAG,IAAI,QAAQ,CAAC,CAAC,OAAO,CAAC,KAAK,EAAE,EAAE,CAAC,CAAC;IACtD,MAAM,IAAI,GAAG,GAAG,CAAC,QAAQ,CAAC,QAAQ,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC,KAAK,CAAC,CAAC,EAAE,CAAC,QAAQ,CAAC,MAAM,CAAC,CAAC,CAAC,CAAC,GAAG,CAAC;IAC3E,MAAM,GAAG,GAAG,MAAM,CAAC,MAAM,CACvB,MAAM,CAAC,mBAAmB,CAAC,KAAK,EAAE,IAAI,CAAC,EAAE,EAAE,YAAY,EAAE,IAAI,CAAC,KAAK,IAAI,CAAC,EAAE,OAAO,EAAE,IAAI,EAAE,CAAC,EAC1F,EAAE,UAAU,EAAE,IAAI,CAAC,UAAU,EAAE,OAAO,EAAE,IAAI,CAAC,OAAO,EAAE,CACvD,CAAC;IACF,OAAO,EAAE,UAAU,EAAE,GAAG,EAAE,MAAM,EAAE,GAAG,EAAE,CAAC;AAC1C,CAAC","sourcesContent":["import type { FilesApi } from \"@statewalker/webrun-files\";\nimport { ltxLog } from \"../bucket/ltx-log.js\";\nimport { filesApiObjectStore } from \"../bucket/object-store.js\";\n\n/**\n * The LTX log on a webrun-files FilesApi (OPFS in browsers, a directory on Node), without\n * ownership: the same writer as celldBucket (src/bucket/ltx-log.ts), on the FilesApi as an\n * object store. Keys are celld's: `<dir>/<Class>:<id>/ltx/e<epoch>/0000/<min>-<max>.ltx`\n * (`dir` defaults to \"/cells\"), so the files read the same in celld.\n */\nexport interface FilesApiLtxLogOptions {\n /** Where the `cells/` keys live; defaults to \"/cells\". */\n dir?: string;\n /** The epoch the files are written in (no ownership); 0 by default. */\n epoch?: number;\n intervalMs?: number;\n onError?: (cellKey: string, error: unknown) => void;\n}\n\nexport function filesApiLtxLog(files: FilesApi, opts: FilesApiLtxLogOptions = {}) {\n const dir = (opts.dir ?? \"/cells\").replace(/\\/$/, \"\");\n const root = dir.endsWith(\"/cells\") ? dir.slice(0, -\"/cells\".length) : dir;\n const log = Object.assign(\n ltxLog(filesApiObjectStore(files, root), { defaultEpoch: opts.epoch ?? 0, compact: true }),\n { intervalMs: opts.intervalMs, onError: opts.onError },\n );\n return { durability: log, backup: log };\n}\n"]}
@@ -0,0 +1,21 @@
1
+ export interface LtxVfsChannel {
2
+ /** Shared control words [state, length] + payload (≥ one page). */
3
+ sab: SharedArrayBuffer;
4
+ /** Posts a request to the store (it answers into `sab`, then notifies). */
5
+ post(msg: unknown): void;
6
+ }
7
+ export interface LtxVfsStats {
8
+ reads: number;
9
+ cacheHits: number;
10
+ storeReads: number;
11
+ commits: number;
12
+ blockedMs: number;
13
+ }
14
+ export declare function installLtxVfs(sqlite3: any, channel: LtxVfsChannel, opts?: {
15
+ name?: string;
16
+ cachePages?: number;
17
+ readAhead?: number;
18
+ }): {
19
+ name: string;
20
+ stats: LtxVfsStats;
21
+ };