@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
@@ -0,0 +1,1131 @@
1
+ // GENERATED by scripts/vendor-celld.mjs from denoland/celld@8702f6d186c77698dbfb30cf29e3b1922a88f008 crates/celld/js/services/kv.js
2
+ // (Apache-2.0, unchanged apart from the export below). Do not edit.
3
+ // Copyright 2026 Deno Land Inc. Apache-2.0 license.
4
+
5
+ // The KV service, shared by both engines: the namespace cell (the server) and
6
+ // the binding's client. A KV namespace is a cell of a runtime-supplied Durable
7
+ // Object class, so it inherits ownership, fencing, LTX replication and durable
8
+ // acknowledgement from the cell it already is.
9
+ //
10
+ // Reads here are strongly consistent and the published contract is upstream's:
11
+ // a value can be up to 60 seconds old. That gap is deliberate. A node-local
12
+ // read cache is the obvious next optimisation and it would make reads
13
+ // genuinely stale, and a freshness promise made now could not be withdrawn
14
+ // then. Implement strong, promise weak.
15
+ //
16
+ // Storage is ordinary cell SQL rather than the KV surface, because a namespace
17
+ // is read by ordered prefix scan and `list` is the operation that decides the
18
+ // schema. `celld_logic::kv` owns every decision about a key, a deadline or a
19
+ // limit; nothing here re-derives one.
20
+ //
21
+ // A function declaration, so the V8 harness (one function body) and the
22
+ // workerd module can each append this text and call it from above. `host` is
23
+ // what differs between the engines:
24
+ //
25
+ // - `DurableObject`: the cell's base class. workerd's gives a cell RPC; the
26
+ // V8 harness grants that to its runtime classes instead.
27
+ // - `limits()`: every bound `celld_logic::kv` declares, plus the test knobs.
28
+ // - `blob(cell, request, value)`: the large-value path (prepare, put, get,
29
+ // sweep) to the fleet bucket, with the cell's activation epoch as the
30
+ // authority. Bytes cross as a typed view in both directions: encoding a
31
+ // 25 MiB value as a JSON number array creates millions of heap objects and
32
+ // can crash an otherwise valid put. The answer is `{ found: true, value }`
33
+ // for bytes and the parsed JSON reply otherwise.
34
+ // - `btoa(bytes)`, `atob(text)`: base64 of bytes and back, for the operator
35
+ // route.
36
+ function kvService(host) {
37
+ // Run one bounded reclamation transaction. A cell supplies only its ordered
38
+ // candidate query and its per-row transition, so KV expiry and Queue retention
39
+ // share the turn bound and the transaction shape without pretending their SQL
40
+ // state machines are the same.
41
+ const __cellSweepBatch = (storage, select, reclaim, limit) => {
42
+ let processed = 0;
43
+ storage.transactionSync(() => {
44
+ const rows = select(limit);
45
+ if (rows.length > limit) throw new Error("a cell sweep exceeded its row bound");
46
+ for (const row of rows) {
47
+ reclaim(row);
48
+ processed += 1;
49
+ }
50
+ });
51
+ return processed;
52
+ };
53
+
54
+ const __KV_TABLE = "__kv";
55
+ const __KV_META_TABLE = "__kv_meta";
56
+
57
+ // Upstream's four content types. The wire form is what `get` returns for
58
+ // `type: "text"`; the binding converts from there, so the cell stores bytes
59
+ // and a tag and never a parsed value.
60
+ const __kvError = (message) => new Error("KV_ERROR: " + String(message));
61
+
62
+ // The authenticated operator route still uses JSON for its control envelope.
63
+ // A byte array is acceptable for an inline value, but a 25 MiB array creates
64
+ // 25 million JavaScript numbers and exhausts the isolate. The raw base64 ops
65
+ // accept typed views for this internal wire form; the public atob() and btoa()
66
+ // wrappers keep their standard string-only behaviour.
67
+ const __kvOperatorValue = (value) => {
68
+ const bytes = value instanceof Uint8Array ? value : new Uint8Array(value);
69
+ return bytes.byteLength > __kvLimits().maxInlineValueBytes
70
+ ? { value: host.btoa(bytes), valueEncoding: "base64" }
71
+ : { value: [...bytes] };
72
+ };
73
+
74
+ // Content addressing, so the same value written twice in one ownership epoch
75
+ // stores once and a retry after an uncertain failure costs nothing. A later
76
+ // epoch deliberately uses a different object. SHA-256 makes an accidental
77
+ // collision infeasible. The host adds the active cell scope and epoch to the
78
+ // object key, because one activation cannot prove another activation's live
79
+ // set and therefore cannot safely collect from a shared digest prefix.
80
+ const __kvDigest = async (bytes) => {
81
+ const hash = new Uint8Array(await crypto.subtle.digest("SHA-256", bytes));
82
+ let out = "";
83
+ for (const byte of hash) out += byte.toString(16).padStart(2, "0");
84
+ return out;
85
+ };
86
+
87
+ // The least name that sorts after every name with this prefix, or null when no
88
+ // such name exists. SQLite compares TEXT byte by byte and UTF-8 byte order is
89
+ // code point order, so the successor is the prefix with its final code point
90
+ // incremented; a final code point that is already the highest carries into the
91
+ // code point before it, and an empty prefix has no upper bound at all.
92
+ //
93
+ // A lone surrogate has no UTF-8 form, so a prefix that holds one gets no bound
94
+ // rather than a bound derived from whatever the host substitutes. Such a
95
+ // prefix matches no stored name, and answering it with a wrong range would
96
+ // answer it with the wrong keys instead of slowly.
97
+ const __kvPrefixUpperBound = (prefix) => {
98
+ if (/[\uD800-\uDBFF](?![\uDC00-\uDFFF])|(?<![\uD800-\uDBFF])[\uDC00-\uDFFF]/.test(prefix)) {
99
+ return null;
100
+ }
101
+ // Array.from splits on code points, so a surrogate pair stays one element
102
+ // and cannot be incremented into a different character.
103
+ const points = Array.from(prefix);
104
+ while (points.length > 0) {
105
+ const code = points.pop().codePointAt(0);
106
+ if (code >= 0x10FFFF) continue;
107
+ // The surrogate block is unencodable, so step over it in one move.
108
+ const next = code + 1 === 0xD800 ? 0xE000 : code + 1;
109
+ return points.join("") + String.fromCodePoint(next);
110
+ }
111
+ return null;
112
+ };
113
+
114
+ // Which rows a list visits is not visible in its result, so a lost bound reads
115
+ // as a correct answer and is measurable only as latency. The plan is the
116
+ // evidence, and a gated test seam reads it from the shipped query rather than
117
+ // from a copy that can drift.
118
+ const __kvReportListPlan = (sql, query, params) => {
119
+ if (typeof __test_kv_list_plan !== "function") return;
120
+ const plan = sql.exec(`EXPLAIN QUERY PLAN ${query}`, ...params)
121
+ .toArray()
122
+ .map((row) => row.detail)
123
+ .join("; ");
124
+ __test_kv_list_plan(plan);
125
+ };
126
+
127
+ class __KvNamespaceCell extends host.DurableObject {
128
+ constructor(ctx, env) {
129
+ super(ctx, env);
130
+ this._ready = false;
131
+ // Blob references written to the bucket whose row has not committed yet. A sweep
132
+ // must spare these, or it collects bytes a put is about to reference.
133
+ this._pending = new Set();
134
+ // Blob writes and the mark-and-sweep protocol share one per-cell queue.
135
+ // The pending set is live state, not a snapshot that stays valid across an
136
+ // await, so collection cannot overlap a put from its announcement through
137
+ // its row commit. Other cells and ordinary reads stay independent.
138
+ this._blobProtocolTail = Promise.resolve();
139
+ }
140
+
141
+ async _withBlobProtocol(operation) {
142
+ const previous = this._blobProtocolTail;
143
+ let release;
144
+ this._blobProtocolTail = new Promise((resolve) => {
145
+ release = resolve;
146
+ });
147
+ await previous;
148
+ try {
149
+ return await operation();
150
+ } finally {
151
+ release();
152
+ }
153
+ }
154
+
155
+ // Schedule collection before a blob can leave the process or a row can drop
156
+ // its reference. `setAlarm()` writes cell state, and the following bucket
157
+ // egress waits on that write's output gate. A crash can therefore leave an
158
+ // orphan only together with the durable wake that will collect it.
159
+ async _armBlobSweep() {
160
+ this._open().exec(
161
+ `INSERT INTO ${__KV_META_TABLE} (name, value) VALUES ('blob-sweep-due', 1)
162
+ ON CONFLICT(name) DO UPDATE SET value = excluded.value`,
163
+ );
164
+ const now = Date.now();
165
+ const deadline = now + __kvLimits().blobSweepMs;
166
+ const armed = await this.ctx.storage.getAlarm();
167
+ // During an alarm event, `getAlarm()` can still report the timestamp that
168
+ // caused the current delivery. It is not a future wake and cannot cover a
169
+ // reference removed by an event that interleaves with this one.
170
+ if (armed === null || armed <= now || armed > deadline) {
171
+ await this.ctx.storage.setAlarm(deadline);
172
+ }
173
+ }
174
+
175
+ _blobSweepDue() {
176
+ return this._open().exec(
177
+ `SELECT 1 AS due FROM ${__KV_META_TABLE}
178
+ WHERE name = 'blob-sweep-due' AND value = 1`,
179
+ ).toArray().length > 0;
180
+ }
181
+
182
+ _clearBlobSweepDue() {
183
+ this._open().exec(
184
+ `DELETE FROM ${__KV_META_TABLE} WHERE name = 'blob-sweep-due'`,
185
+ );
186
+ }
187
+
188
+ // Created on first touch rather than at construction, so a namespace that is
189
+ // only ever read costs no write, and an eviction that drops the isolate does
190
+ // not need the table rebuilt before the cell can answer.
191
+ _open() {
192
+ if (this._ready) return this.ctx.storage.sql;
193
+ const sql = this.ctx.storage.sql;
194
+ // Recreate the first release's table before the normal open path. The
195
+ // migration test then reaches the same `CREATE IF NOT EXISTS` boundary as
196
+ // an upgraded cell and starts with a row it must preserve.
197
+ if (__kvLimits().legacySchema) {
198
+ sql.exec(
199
+ `CREATE TABLE IF NOT EXISTS ${__KV_TABLE} (
200
+ name TEXT PRIMARY KEY,
201
+ value BLOB,
202
+ tag TEXT NOT NULL,
203
+ metadata TEXT,
204
+ expires_at INTEGER
205
+ ) WITHOUT ROWID`,
206
+ );
207
+ sql.exec(
208
+ `INSERT OR IGNORE INTO ${__KV_TABLE}
209
+ (name, value, tag, metadata, expires_at)
210
+ VALUES ('before-upgrade', CAST('legacy' AS BLOB), 'text', NULL, NULL)`,
211
+ );
212
+ }
213
+ sql.exec(
214
+ `CREATE TABLE IF NOT EXISTS ${__KV_TABLE} (
215
+ name TEXT PRIMARY KEY,
216
+ value BLOB,
217
+ blob_id TEXT,
218
+ size INTEGER NOT NULL,
219
+ tag TEXT NOT NULL,
220
+ metadata TEXT,
221
+ expires_at INTEGER
222
+ ) WITHOUT ROWID`,
223
+ );
224
+ // Inline KV shipped before `blob_id` and `size`. `CREATE IF NOT EXISTS`
225
+ // does not change that table, so migrate each column independently. A
226
+ // crash between the ALTER statements is safe because the next open reads
227
+ // the surviving shape and resumes at the missing column.
228
+ const columns = new Set(
229
+ sql.exec(`PRAGMA table_info(${__KV_TABLE})`).toArray().map((row) => row.name),
230
+ );
231
+ if (!columns.has("blob_id")) {
232
+ sql.exec(`ALTER TABLE ${__KV_TABLE} ADD COLUMN blob_id TEXT`);
233
+ }
234
+ if (!columns.has("size")) {
235
+ sql.exec(
236
+ `ALTER TABLE ${__KV_TABLE}
237
+ ADD COLUMN size INTEGER NOT NULL DEFAULT 0`,
238
+ );
239
+ sql.exec(`UPDATE ${__KV_TABLE} SET size = LENGTH(value)`);
240
+ }
241
+ // `list` walks the primary key in order, so the only index worth carrying
242
+ // is the sweeper's. Without it the sweep is a full scan of a namespace
243
+ // whose whole point is being large.
244
+ sql.exec(
245
+ `CREATE INDEX IF NOT EXISTS ${__KV_TABLE}_expires
246
+ ON ${__KV_TABLE} (expires_at) WHERE expires_at IS NOT NULL`,
247
+ );
248
+ // The due bit is durable because the isolate that wrote a blob can die
249
+ // before its row commit. In-memory state cannot schedule that orphan's
250
+ // collector on the next owner.
251
+ sql.exec(
252
+ `CREATE TABLE IF NOT EXISTS ${__KV_META_TABLE} (
253
+ name TEXT PRIMARY KEY,
254
+ value INTEGER NOT NULL
255
+ ) WITHOUT ROWID`,
256
+ );
257
+ this._ready = true;
258
+ return sql;
259
+ }
260
+
261
+ // A key past its deadline is invisible from the instant it expires, not from
262
+ // whenever the sweeper next runs. The read path filters and the sweep only
263
+ // reclaims space, so the two can never disagree about which side of the
264
+ // boundary a key is on.
265
+ async __kvGet({ keys, withMetadata, withExpiration = false, now }) {
266
+ const sql = this._open();
267
+ const out = [];
268
+ for (const key of keys) {
269
+ const row = sql.exec(
270
+ `SELECT value, blob_id, tag, metadata, expires_at FROM ${__KV_TABLE}
271
+ WHERE name = ? AND (expires_at IS NULL OR expires_at > ?)`,
272
+ key,
273
+ now,
274
+ ).toArray()[0];
275
+ if (row === undefined) {
276
+ out.push({ key, found: false });
277
+ continue;
278
+ }
279
+ let value = row.value;
280
+ if (row.blob_id !== null && row.blob_id !== undefined) {
281
+ const blob = await host.blob(this, { mode: "get", reference: row.blob_id });
282
+ // A row naming a blob the bucket does not hold is the failure the
283
+ // commit order exists to prevent, so it is reported as itself rather
284
+ // than as a missing key: an absent key and a broken one need different
285
+ // answers from an operator.
286
+ if (!blob.found) {
287
+ throw __kvError(
288
+ `the value for ${JSON.stringify(key)} is missing from the fleet ` +
289
+ `bucket (blob ${row.blob_id})`,
290
+ );
291
+ }
292
+ value = new Uint8Array(blob.value);
293
+ }
294
+ const entry = { key, found: true, value, tag: row.tag };
295
+ if (withMetadata) entry.metadata = row.metadata ?? null;
296
+ // These fields describe one row snapshot. The operator bulk export must
297
+ // not combine a value from this read with an expiration from an earlier
298
+ // listing when a concurrent put replaces the key between both calls.
299
+ if (withExpiration) entry.expiration = row.expires_at ?? null;
300
+ out.push(entry);
301
+ }
302
+ return out;
303
+ }
304
+
305
+ // One transaction for the whole batch, so a bulk put is all-or-nothing
306
+ // rather than a prefix of itself. Upstream gives no such guarantee, and
307
+ // giving a stronger one here costs nothing and cannot surprise a caller.
308
+ // Blob first, row second. An orphan blob is bytes the collector reclaims; a
309
+ // row naming a blob that was never written is a read that fails forever, and
310
+ // no sweep repairs it. The opposite commit order can leave that dangling
311
+ // row when a process stops between the two writes.
312
+ //
313
+ // The blob reference travels in `_pending` until the row commits, so a sweep that
314
+ // runs mid-put does not collect bytes that are about to be referenced. That
315
+ // costs the put and not the read -- the commit requires the blob, so a
316
+ // collected blob means the commit cannot fire -- which is why the model's
317
+ // tooth for it is an action property and not an invariant.
318
+ async _store(value) {
319
+ if (value.byteLength <= __kvLimits().maxInlineValueBytes) {
320
+ return { value, blobId: null };
321
+ }
322
+ const digest = await __kvDigest(value);
323
+ await this._armBlobSweep();
324
+ // The host mints the reference from the epoch installed with this cell.
325
+ // JavaScript never guesses that authority or recovers it after an await.
326
+ const { reference } = await host.blob(this, { mode: "prepare", digest });
327
+ this._pending.add(reference);
328
+ try {
329
+ await host.blob(this, { mode: "put", reference }, value);
330
+ // Force the collector race at its real seam. The alarm took its mark
331
+ // snapshot before this put started and waits briefly after the put
332
+ // announces itself. Without blob-protocol serialization, this write
333
+ // lands during that wait and must stay here until the stale sweep runs.
334
+ if (
335
+ __kvLimits().raceSweepPut && this._testSweepMarked &&
336
+ !this._testSweepFinished
337
+ ) {
338
+ await new Promise((resolve) => {
339
+ this._testResumePut = resolve;
340
+ });
341
+ }
342
+ // The crash window the commit order exists for: the blob is in the
343
+ // bucket and the row is not written yet. The failed operation no longer
344
+ // protects the digest in memory, so the durable wake can reclaim it even
345
+ // when this isolate remains resident.
346
+ if (__kvLimits().failAfterBlobWrite) {
347
+ throw __kvError("CELLD_TEST_KV_FAIL_AFTER_BLOB_WRITE");
348
+ }
349
+ return { value: null, blobId: reference };
350
+ } catch (error) {
351
+ this._pending.delete(reference);
352
+ throw error;
353
+ }
354
+ }
355
+
356
+ async __kvPut({ entries }) {
357
+ if (
358
+ __kvLimits().raceSweepPut &&
359
+ entries.some((entry) =>
360
+ entry.value.byteLength > __kvLimits().maxInlineValueBytes
361
+ )
362
+ ) {
363
+ if (!this._testSweepMarked || this._testPutAttempted === undefined) {
364
+ throw __kvError("the test blob sweep did not reach its mark snapshot");
365
+ }
366
+ this._testPutAttempted();
367
+ this._testPutAttempted = undefined;
368
+ }
369
+ return this._withBlobProtocol(() => this._putLocked(entries));
370
+ }
371
+
372
+ async _putLocked(entries) {
373
+ const sql = this._open();
374
+ // An inline replacement can remove the final row reference to an old
375
+ // blob, so arm before changing the row. A new large value also arms in
376
+ // `_store`, before its own bucket write.
377
+ const replacesBlob = entries.some((entry) =>
378
+ sql.exec(
379
+ `SELECT 1 AS found FROM ${__KV_TABLE}
380
+ WHERE name = ? AND blob_id IS NOT NULL`,
381
+ entry.key,
382
+ ).toArray().length > 0
383
+ );
384
+ if (replacesBlob) await this._armBlobSweep();
385
+ const stored = [];
386
+ try {
387
+ for (const entry of entries) {
388
+ stored.push({ entry, ...(await this._store(entry.value)) });
389
+ }
390
+ this.ctx.storage.transactionSync(() => {
391
+ for (const { entry, value, blobId } of stored) {
392
+ sql.exec(
393
+ `INSERT INTO ${__KV_TABLE}
394
+ (name, value, blob_id, size, tag, metadata, expires_at)
395
+ VALUES (?, ?, ?, ?, ?, ?, ?)
396
+ ON CONFLICT(name) DO UPDATE SET
397
+ value = excluded.value,
398
+ blob_id = excluded.blob_id,
399
+ size = excluded.size,
400
+ tag = excluded.tag,
401
+ metadata = excluded.metadata,
402
+ expires_at = excluded.expires_at`,
403
+ entry.key,
404
+ value,
405
+ blobId,
406
+ entry.value.byteLength,
407
+ entry.tag,
408
+ entry.metadata ?? null,
409
+ entry.expiresAt ?? null,
410
+ );
411
+ }
412
+ });
413
+ } finally {
414
+ // A committed row protects the blob durably. A failed put protects
415
+ // nothing, so it must not pin an orphan until this isolate is evicted.
416
+ for (const { blobId } of stored) {
417
+ if (blobId !== null) this._pending.delete(blobId);
418
+ }
419
+ }
420
+ // Only when something in this batch can expire. A namespace of permanent
421
+ // keys arms no alarm and wakes for nothing.
422
+ if (entries.some((entry) => entry.expiresAt !== null && entry.expiresAt !== undefined)) {
423
+ await this._rearm();
424
+ }
425
+ }
426
+
427
+ async __kvDelete({ keys }) {
428
+ return this._withBlobProtocol(() => this._deleteLocked(keys));
429
+ }
430
+
431
+ async _deleteLocked(keys) {
432
+ const sql = this._open();
433
+ const removesBlob = keys.some((key) =>
434
+ sql.exec(
435
+ `SELECT 1 AS found FROM ${__KV_TABLE}
436
+ WHERE name = ? AND blob_id IS NOT NULL`,
437
+ key,
438
+ ).toArray().length > 0
439
+ );
440
+ if (removesBlob) await this._armBlobSweep();
441
+ this.ctx.storage.transactionSync(() => {
442
+ for (const key of keys) {
443
+ sql.exec(`DELETE FROM ${__KV_TABLE} WHERE name = ?`, key);
444
+ }
445
+ });
446
+ }
447
+
448
+ // Arm for the earliest deadline the table holds, or disarm when it holds
449
+ // none. Asked from the row rather than held in memory, so the answer does
450
+ // not depend on what this activation happens to remember -- an evicted cell
451
+ // that wakes for an alarm re-derives the same deadline from the same rows.
452
+ //
453
+ // What carries the deadline across an eviction is the alarm itself, which is
454
+ // durable cell state. This is *not* recomputed when a cell is merely opened,
455
+ // so the one gap is a `_rearm` that throws after its put committed: the rows
456
+ // are written, the caller sees an error, and nothing is armed until the next
457
+ // put with a deadline. That costs space and never correctness, because the
458
+ // read path filters an expired key whether or not it was reclaimed.
459
+ async _rearm() {
460
+ // A test that means to pin the read filter must be able to stop the sweep,
461
+ // or it cannot tell which mechanism hid the key -- and a test that cannot
462
+ // tell will pass when the filter is broken. This seam exists because that
463
+ // is exactly what happened: the first version of the expiry test stayed
464
+ // green with the filter removed.
465
+ if (__kvLimits().sweepDisabled) return;
466
+ const sql = this._open();
467
+ const row = sql.exec(
468
+ `SELECT MIN(expires_at) AS next FROM ${__KV_TABLE} WHERE expires_at IS NOT NULL`,
469
+ ).toArray()[0];
470
+ const next = row === undefined ? null : row.next;
471
+ const blobDue = this._blobSweepDue();
472
+ if ((next === null || next === undefined) && !blobDue) {
473
+ await this.ctx.storage.deleteAlarm();
474
+ return;
475
+ }
476
+ const now = Date.now();
477
+ const deadlines = [];
478
+ // Never in the past: a deadline already due is swept on this wake, and
479
+ // arming behind `now` would spin.
480
+ if (next !== null && next !== undefined) {
481
+ deadlines.push(Math.max(next, now + 1));
482
+ }
483
+ if (blobDue) deadlines.push(now + __kvLimits().blobSweepMs);
484
+ const deadline = Math.min(...deadlines);
485
+ const armed = await this.ctx.storage.getAlarm();
486
+ if (armed === null || armed <= now || armed > deadline) {
487
+ await this.ctx.storage.setAlarm(deadline);
488
+ }
489
+ }
490
+
491
+ // Reclaiming space, never deciding visibility. A key is invisible from the
492
+ // instant it expires because the read path filters, so a late sweep costs
493
+ // storage and never correctness -- which is what lets this be bounded and
494
+ // re-armed rather than obliged to finish.
495
+ // Every reference a row names, plus every reference a put has written and
496
+ // not yet committed. The two go in one list because the bucket end cannot
497
+ // tell them apart and does not need to: it only needs "do not delete these".
498
+ _liveBlobs() {
499
+ const rows = this._open().exec(
500
+ `SELECT blob_id FROM ${__KV_TABLE} WHERE blob_id IS NOT NULL`,
501
+ ).toArray();
502
+ return [...new Set([...rows.map((row) => row.blob_id), ...this._pending])];
503
+ }
504
+
505
+ async alarm() {
506
+ const removed = await this._withBlobProtocol(async () => {
507
+ const now = Date.now();
508
+ // If expiry will remove a blob reference, persist the due bit and a
509
+ // future wake before deleting the row. A crash later in this alarm then
510
+ // leaves the next owner enough durable state to finish collection.
511
+ const expiresBlob = this._open().exec(
512
+ `SELECT 1 AS found FROM ${__KV_TABLE}
513
+ WHERE expires_at IS NOT NULL AND expires_at <= ?
514
+ AND blob_id IS NOT NULL
515
+ LIMIT ?`,
516
+ now,
517
+ __kvLimits().sweepBatchRows,
518
+ ).toArray().length > 0;
519
+ if (expiresBlob) await this._armBlobSweep();
520
+
521
+ const { removed } = this.__kvSweep({
522
+ now,
523
+ limit: __kvLimits().sweepBatchRows,
524
+ });
525
+ // The blob sweep runs after the row sweep, on the same wake, so a row
526
+ // reclaimed above has already dropped its reference by the time the live
527
+ // set is read. A durable due bit distinguishes a GC wake from an inline
528
+ // expiry wake and survives isolate eviction or a failed bucket request.
529
+ if (this._blobSweepDue() || __kvLimits().raceSweepPut) {
530
+ const live = this._liveBlobs();
531
+ if (__kvLimits().raceSweepPut && !this._testSweepFinished) {
532
+ const putAttempted = new Promise((resolve) => {
533
+ this._testPutAttempted = resolve;
534
+ });
535
+ this._testSweepMarked = true;
536
+ await putAttempted;
537
+ // The fixture bucket is local. This bounded pause lets an
538
+ // unprotected put finish its blob write; a protected put is waiting
539
+ // for this protocol lock and therefore cannot enter the window.
540
+ await new Promise((resolve) => setTimeout(resolve, 100));
541
+ }
542
+ let swept = false;
543
+ try {
544
+ await host.blob(this, { mode: "sweep", live });
545
+ swept = true;
546
+ } catch (error) {
547
+ // Reclaiming space is not deciding visibility, so a bucket that
548
+ // refuses a delete costs storage and never correctness. The next
549
+ // wake tries again.
550
+ console.error("kv blob sweep failed:", error);
551
+ } finally {
552
+ if (__kvLimits().raceSweepPut) {
553
+ this._testSweepFinished = true;
554
+ this._testResumePut?.();
555
+ this._testResumePut = undefined;
556
+ }
557
+ }
558
+ if (swept) this._clearBlobSweepDue();
559
+ }
560
+ return removed;
561
+ });
562
+ // A full batch means there is more to reclaim, so come back promptly
563
+ // rather than waiting for the next natural deadline.
564
+ if (removed >= __kvLimits().sweepBatchRows) {
565
+ await this.ctx.storage.setAlarm(Date.now() + 1);
566
+ return;
567
+ }
568
+ await this._rearm();
569
+ }
570
+
571
+ // Pagination resumes at `name > after`, never at an offset. A caller holds
572
+ // no transaction across pages, so an offset would skip or repeat keys the
573
+ // moment a concurrent writer inserted or deleted inside the prefix.
574
+ //
575
+ // One row beyond the limit is read to decide `list_complete`, and discarded.
576
+ // Asking the database is the only honest answer: a page that happens to fill
577
+ // exactly is indistinguishable from a page that ended.
578
+ __kvList({ prefix, limit, after, now }) {
579
+ const sql = this._open();
580
+ // Every name a prefix can match is one contiguous run of the primary key,
581
+ // so bound the key on both sides. The query used to carry the cursor as
582
+ // its only range, and SQLite then walked every earlier row in the
583
+ // namespace before it could report that a late prefix matches nothing: an
584
+ // empty-result list measured 684/s over 4,849 rows and 43/s over 114,881.
585
+ //
586
+ // The bounds decide which rows a list visits and the prefix comparison
587
+ // still decides which rows match. A prefix whose bounds cannot be derived
588
+ // -- one holding a lone surrogate, which no stored name can hold -- is
589
+ // therefore answered at the old cost rather than short of keys.
590
+ const upper = __kvPrefixUpperBound(prefix);
591
+ // A cursor names a key this namespace already returned for this prefix, so
592
+ // it is inside the range and replaces the prefix as the start. Carrying
593
+ // both would leave SQLite to pick one as the seek and demote the other to
594
+ // a filter, and picking the prefix rescans every earlier page. A cursor
595
+ // arrives from the caller, so a hand-made one can start below the prefix;
596
+ // the comparison below, and not the range, keeps that page correct.
597
+ const start = after ? "name > ?" : "name >= ?";
598
+ const params = [after ? after : prefix];
599
+ if (upper !== null) params.push(upper);
600
+ // `substr` compares under BINARY, so a prefix matches by bytes and not by
601
+ // ASCII case. `LIKE` stood here and folded case, which made `list({prefix:
602
+ // "A"})` answer a key named `a` -- upstream compares a byte prefix, and no
603
+ // range bound can reproduce a case-folded match anyway. The length counts
604
+ // code points, which is what SQLite counts, not UTF-16 units.
605
+ if (prefix !== "") params.push(Array.from(prefix).length, prefix);
606
+ const query = `SELECT name, metadata, expires_at FROM ${__KV_TABLE}
607
+ WHERE ${start}
608
+ ${upper === null ? "" : "AND name < ?"}
609
+ ${prefix === "" ? "" : "AND substr(name, 1, ?) = ?"}
610
+ AND (expires_at IS NULL OR expires_at > ?)
611
+ ORDER BY name
612
+ LIMIT ?`;
613
+ params.push(now, limit + 1);
614
+ __kvReportListPlan(sql, query, params);
615
+ const rows = sql.exec(query, ...params).toArray();
616
+ const complete = rows.length <= limit;
617
+ const page = complete ? rows : rows.slice(0, limit);
618
+ return {
619
+ keys: page.map((row) => ({
620
+ name: row.name,
621
+ metadata: row.metadata ?? null,
622
+ expiration: row.expires_at ?? null,
623
+ })),
624
+ complete,
625
+ };
626
+ }
627
+
628
+ // The operator's live key and byte counts. `size` stays in the row because
629
+ // a bucket-backed value has a NULL inline `value`; `LENGTH(value)` would
630
+ // report zero bytes for exactly the values whose size matters most.
631
+ __kvMetrics({ now }) {
632
+ const sql = this._open();
633
+ const row = sql.exec(
634
+ `SELECT COUNT(*) AS count,
635
+ COALESCE(SUM(size), 0) AS bytes
636
+ FROM ${__KV_TABLE}
637
+ WHERE expires_at IS NULL OR expires_at > ?`,
638
+ now,
639
+ ).toArray()[0];
640
+ return { count: row.count, bytes: row.bytes };
641
+ }
642
+
643
+ // Reclaim what the read path already treats as gone. Bounded per call so a
644
+ // namespace with a large expired population cannot hold the cell for an
645
+ // unbounded time; the caller re-arms while there is more.
646
+ __kvSweep({ now, limit }) {
647
+ const sql = this._open();
648
+ const removed = __cellSweepBatch(
649
+ this.ctx.storage,
650
+ (bound) => sql.exec(
651
+ `SELECT name FROM ${__KV_TABLE}
652
+ WHERE expires_at IS NOT NULL AND expires_at <= ?
653
+ ORDER BY expires_at
654
+ LIMIT ?`,
655
+ now,
656
+ bound,
657
+ ).toArray(),
658
+ (row) => {
659
+ sql.exec(`DELETE FROM ${__KV_TABLE} WHERE name = ?`, row.name);
660
+ },
661
+ limit,
662
+ );
663
+ return { removed };
664
+ }
665
+
666
+ // The operator surface, reached only over `/runtime/`, which authenticates
667
+ // with the fleet secret. `/do/` refuses every reserved class structurally,
668
+ // so adding this handler does not put a namespace on an unauthenticated
669
+ // route -- the trap d1.md decision 4 records paying for once, and the reason
670
+ // that refusal is one question rather than one per class.
671
+ //
672
+ // Values cross this boundary as arrays of bytes rather than as text, because
673
+ // a namespace holds bytes: a value written from a Worker as an ArrayBuffer
674
+ // has no faithful string form, and `celld kv` must be able to read back
675
+ // exactly what was written.
676
+ async fetch(request) {
677
+ let body;
678
+ try {
679
+ body = await request.json();
680
+ } catch {
681
+ return Response.json({ error: "KV_ERROR: invalid request body" }, { status: 400 });
682
+ }
683
+ const now = Date.now();
684
+ try {
685
+ let result;
686
+ switch (body.op) {
687
+ case "get": {
688
+ // Awaited: `__kvGet` resolves a bucket-backed value, so it is
689
+ // async. Destructuring the promise instead threw "object is not
690
+ // iterable" on every operator read, and only here -- the binding
691
+ // reaches the same method over RPC, which awaits for it.
692
+ const [row] = await this.__kvGet({
693
+ keys: [String(body.key)],
694
+ withMetadata: true,
695
+ withExpiration: true,
696
+ now,
697
+ });
698
+ result = row.found
699
+ ? {
700
+ found: true,
701
+ ...__kvOperatorValue(row.value),
702
+ tag: row.tag,
703
+ metadata: row.metadata ?? null,
704
+ expiration: row.expiration,
705
+ }
706
+ : { found: false };
707
+ break;
708
+ }
709
+ case "put":
710
+ case "put-base64": {
711
+ // Validated here, through the same helpers the binding calls, so a
712
+ // key written by `celld kv` is a key the binding could have written.
713
+ // The operator route used to arrive pre-validated by the CLI, which
714
+ // put the same bound in two processes -- and they disagreed the first
715
+ // time a test shortened one of them.
716
+ const key = String(body.key);
717
+ __kvCheckKey(key);
718
+ const value = body.op === "put-base64"
719
+ ? host.atob(String(body.value))
720
+ : new Uint8Array(body.value);
721
+ __kvCheckValue(value.byteLength);
722
+ const metadata = body.metadata === undefined ? null : body.metadata;
723
+ if (metadata !== null) __kvCheckMetadata(metadata);
724
+ // Resolved from the cell's clock, not the caller's, because that is
725
+ // the clock the read filter and the sweeper compare against.
726
+ const expiresAt = __kvExpiryAt(now, body.expiration, body.expirationTtl);
727
+ await this.__kvPut({
728
+ entries: [{
729
+ key,
730
+ value,
731
+ tag: String(body.tag ?? __KV_TAG_BYTES),
732
+ metadata,
733
+ expiresAt,
734
+ }],
735
+ });
736
+ result = { ok: true };
737
+ break;
738
+ }
739
+ case "delete":
740
+ for (const key of body.keys) __kvCheckKey(String(key));
741
+ await this.__kvDelete({ keys: body.keys.map(String) });
742
+ result = { ok: true };
743
+ break;
744
+ case "list":
745
+ result = this.__kvList({
746
+ prefix: String(body.prefix ?? ""),
747
+ limit: Number(body.limit),
748
+ after: String(body.after ?? ""),
749
+ now,
750
+ });
751
+ break;
752
+ // What `celld kv info` reports. `stored` counts every row and `live`
753
+ // counts only what a read would return, so the difference is exactly
754
+ // the population the sweeper still owes -- which is the one number
755
+ // that makes reclamation observable from outside the cell.
756
+ case "info": {
757
+ const live = this.__kvMetrics({ now });
758
+ const total = this._open().exec(
759
+ `SELECT COUNT(*) AS count FROM ${__KV_TABLE}`,
760
+ ).toArray()[0];
761
+ result = { live: live.count, bytes: live.bytes, stored: total.count };
762
+ break;
763
+ }
764
+ default:
765
+ return Response.json(
766
+ { error: `KV_ERROR: unknown operation ${JSON.stringify(body.op)}` },
767
+ { status: 400 },
768
+ );
769
+ }
770
+ return Response.json({ result });
771
+ } catch (error) {
772
+ return Response.json(
773
+ { error: String(error && error.message || error) },
774
+ { status: 400 },
775
+ );
776
+ }
777
+ }
778
+ }
779
+
780
+ // The client half.
781
+ //
782
+ // Every number below is injected from `celld_logic::kv` as `host.limits()`
783
+ // rather than written here, so the binding, `celld kv` and deploy-time
784
+ // validation cannot disagree about what a valid key is. No new host op: D1 set
785
+ // the bar at one and Workflows came in under it, and a limit is data rather
786
+ // than a decision, so shipping the values is enough. What stays in JS is the
787
+ // presentation -- the error text is the binding's published contract, in
788
+ // upstream's `KV <OP> failed:` shape, and belongs beside the calls that raise
789
+ // it.
790
+ //
791
+ // The cursor codec is the one thing that must byte-match Rust's, and it is
792
+ // plain hex: a total function with a single right answer, which two correct
793
+ // implementations cannot disagree about. A policy would be a different matter
794
+ // and none is duplicated here.
795
+ const __kvLimits = host.limits;
796
+
797
+ const __kvCheckKey = (key) => {
798
+ if (key.length === 0) throw __kvError("a key must not be empty");
799
+ const bytes = __kvEncoder.encode(key).byteLength;
800
+ if (bytes > __kvLimits().maxKeyBytes) {
801
+ throw __kvError(
802
+ `a key is at most ${__kvLimits().maxKeyBytes} bytes, got ${bytes}`,
803
+ );
804
+ }
805
+ };
806
+
807
+ // A TTL becomes an absolute deadline here, once, at the call. Resolving it
808
+ // again later against a newer clock would extend the life of the key every
809
+ // time anything re-read the row -- the reason a workflow persists a sleep
810
+ // deadline and not its duration.
811
+ const __kvCheckValue = (size) => {
812
+ // Upstream's bound, and now the only one. A value above the inline bound is
813
+ // no longer refused: it goes to the fleet bucket, which is the split
814
+ // Cloudflare's own KV rearchitecture made and for the same reason -- a cell
815
+ // replicates every write as LTX, so an inline value is paid for twice.
816
+ if (size > __kvLimits().maxValueBytes) {
817
+ throw __kvError(`a value is at most ${__kvLimits().maxValueBytes} bytes, got ${size}`);
818
+ }
819
+ };
820
+
821
+ const __kvCheckMetadata = (metadata) => {
822
+ const bytes = __kvEncoder.encode(metadata).byteLength;
823
+ if (bytes > __kvLimits().maxMetadataBytes) {
824
+ throw __kvError(
825
+ `metadata is at most ${__kvLimits().maxMetadataBytes} bytes, got ${bytes}`,
826
+ );
827
+ }
828
+ };
829
+
830
+ const __kvExpiryAt = (now, expiration, expirationTtl) => {
831
+ if (expirationTtl !== undefined && expirationTtl !== null) {
832
+ const ms = Math.floor(Number(expirationTtl) * 1000);
833
+ if (!Number.isFinite(ms) || ms < __kvLimits().minExpirationTtlMs) {
834
+ throw __kvError(
835
+ `expirationTtl is at least ${__kvLimits().minExpirationTtlMs / 1000} seconds`,
836
+ );
837
+ }
838
+ return now + ms;
839
+ }
840
+ if (expiration !== undefined && expiration !== null) {
841
+ const at = Math.floor(Number(expiration) * 1000);
842
+ if (!Number.isFinite(at) || at <= now) {
843
+ throw __kvError(`expiration ${expiration} is in the past`);
844
+ }
845
+ return at;
846
+ }
847
+ return null;
848
+ };
849
+
850
+ const __kvCursor = {
851
+ encode(key) {
852
+ let out = "";
853
+ for (const byte of __kvEncoder.encode(key)) {
854
+ out += byte.toString(16).padStart(2, "0");
855
+ }
856
+ return out;
857
+ },
858
+ decode(cursor) {
859
+ // A malformed cursor is refused, never read as "start from the
860
+ // beginning": that would hand a paginating caller its first page a second
861
+ // time and call it progress.
862
+ if (cursor.length % 2 !== 0 || /[^0-9a-f]/.test(cursor)) {
863
+ throw __kvError("the list cursor is malformed");
864
+ }
865
+ const bytes = new Uint8Array(cursor.length / 2);
866
+ for (let at = 0; at < bytes.length; at += 1) {
867
+ bytes[at] = Number.parseInt(cursor.slice(at * 2, at * 2 + 2), 16);
868
+ }
869
+ return __kvDecoder.decode(bytes);
870
+ },
871
+ };
872
+
873
+ const __kvListLimit = (requested) => {
874
+ const max = __kvLimits().maxListLimit;
875
+ if (requested === undefined || requested === null) return max;
876
+ const limit = Math.floor(Number(requested));
877
+ // Zero would be a page that never ends.
878
+ if (!Number.isFinite(limit) || limit <= 0) return max;
879
+ return Math.min(limit, max);
880
+ };
881
+
882
+ // Upstream's four content types, and what each one means on the way in and out.
883
+ const __KV_TAG_TEXT = "text";
884
+ const __KV_TAG_BYTES = "bytes";
885
+
886
+ const __kvEncoder = new TextEncoder();
887
+ const __kvDecoder = new TextDecoder();
888
+
889
+ // A value becomes bytes plus a tag, once, at the public boundary. Storing the
890
+ // tag is what lets `get(key, "text")` and `get(key)` answer differently from
891
+ // the same row without the cell parsing anything.
892
+ const __kvEncodeValue = (value) => {
893
+ if (typeof value === "string") {
894
+ return { value: __kvEncoder.encode(value), tag: __KV_TAG_TEXT };
895
+ }
896
+ if (value instanceof ArrayBuffer) {
897
+ return { value: new Uint8Array(value.slice(0)), tag: __KV_TAG_BYTES };
898
+ }
899
+ if (ArrayBuffer.isView(value)) {
900
+ // Copied, not referenced. `sendBatch`'s resizable-ArrayBuffer regression
901
+ // upstream is the same hazard: a caller may resize or reuse the buffer
902
+ // between this call and the write, and a shallow view would then read
903
+ // decommitted pages.
904
+ return {
905
+ value: new Uint8Array(
906
+ value.buffer.slice(value.byteOffset, value.byteOffset + value.byteLength),
907
+ ),
908
+ tag: __KV_TAG_BYTES,
909
+ };
910
+ }
911
+ throw __kvError(
912
+ "a KV value must be a string, an ArrayBuffer, a typed array, or a ReadableStream",
913
+ );
914
+ };
915
+
916
+ // Upstream accepts a ReadableStream, and `put(key, request.body)` is how a
917
+ // handler stores a request body, so refusing one broke the obvious spelling.
918
+ // The stream must become bytes before the cell write: a cell replicates a
919
+ // value as LTX, and a pending stream has no bytes to replicate.
920
+ //
921
+ // The bound is enforced while draining rather than on the finished buffer.
922
+ // Checking afterwards would let a body larger than the limit reach memory in
923
+ // full before the refusal, which hands an unbounded allocation to whoever
924
+ // sends the request.
925
+ const __kvDrainStream = async (stream) => {
926
+ const limit = __kvLimits().maxValueBytes;
927
+ const reader = stream.getReader();
928
+ const chunks = [];
929
+ let size = 0;
930
+ try {
931
+ for (;;) {
932
+ const { done, value } = await reader.read();
933
+ if (done) break;
934
+ const chunk = ArrayBuffer.isView(value)
935
+ ? new Uint8Array(value.buffer, value.byteOffset, value.byteLength)
936
+ : new Uint8Array(value);
937
+ size += chunk.byteLength;
938
+ if (size > limit) {
939
+ throw __kvError(`a value is at most ${limit} bytes, and the stream is larger`);
940
+ }
941
+ chunks.push(chunk);
942
+ }
943
+ } catch (error) {
944
+ await reader.cancel(error).catch(() => {});
945
+ throw error;
946
+ } finally {
947
+ reader.releaseLock();
948
+ }
949
+ // The chunk wrappers can alias the buffers yielded by the stream. `set()`
950
+ // copies their current bytes into one owned result, so later mutation of a
951
+ // yielded buffer cannot change the stored value.
952
+ const value = new Uint8Array(size);
953
+ let offset = 0;
954
+ for (const chunk of chunks) {
955
+ value.set(chunk, offset);
956
+ offset += chunk.byteLength;
957
+ }
958
+ return value;
959
+ };
960
+
961
+ const __kvDecodeValue = (bytes, tag, type) => {
962
+ const view = bytes instanceof Uint8Array ? bytes : new Uint8Array(bytes);
963
+ switch (type) {
964
+ case "arrayBuffer":
965
+ return view.buffer.slice(view.byteOffset, view.byteOffset + view.byteLength);
966
+ case "json":
967
+ return JSON.parse(__kvDecoder.decode(view));
968
+ case "stream":
969
+ return new Response(view).body;
970
+ case "text":
971
+ default:
972
+ // A value written as bytes still decodes as text when asked for text,
973
+ // which is upstream's behaviour and the reason the tag is advisory
974
+ // rather than a type check.
975
+ return __kvDecoder.decode(view);
976
+ }
977
+ };
978
+
979
+ const __kvReadOptions = (options) => {
980
+ if (typeof options === "string") return { type: options };
981
+ if (options === null || options === undefined) return { type: "text" };
982
+ return { type: options.type ?? "text", cacheTtl: options.cacheTtl };
983
+ };
984
+
985
+ class KvNamespace {
986
+ // `namespace()` answers the namespace of KV cells, and `cellName` names this
987
+ // namespace's one cell (`celld_logic::kv::cell_name`).
988
+ constructor(namespace, cellName) {
989
+ Object.defineProperty(this, "_namespace", { value: namespace });
990
+ Object.defineProperty(this, "_cellName", { value: cellName });
991
+ }
992
+
993
+ // Resolved per call rather than cached: a cell can move between calls, and
994
+ // `getByName` costs what the Durable Object path already pays.
995
+ get _stub() {
996
+ return this._namespace().getByName(this._cellName);
997
+ }
998
+
999
+ async _read(keys, options, withMetadata) {
1000
+ const { type } = __kvReadOptions(options);
1001
+ for (const key of keys) __kvCheckKey(key);
1002
+ const rows = await this._stub.__kvGet({
1003
+ keys,
1004
+ withMetadata,
1005
+ now: Date.now(),
1006
+ });
1007
+ return rows.map((row) => {
1008
+ if (!row.found) return { key: row.key, value: null, metadata: null };
1009
+ return {
1010
+ key: row.key,
1011
+ value: __kvDecodeValue(row.value, row.tag, type),
1012
+ metadata: row.metadata === null || row.metadata === undefined
1013
+ ? null
1014
+ : JSON.parse(row.metadata),
1015
+ };
1016
+ });
1017
+ }
1018
+
1019
+ // `get(key)` answers a value; `get([keys])` answers a Map with a null hole
1020
+ // for a key that is not there. Upstream's bulk form is a Map and not an
1021
+ // object, which matters: an object would collide a key named `__proto__`
1022
+ // with the prototype chain.
1023
+ async get(key, options) {
1024
+ if (Array.isArray(key)) {
1025
+ const rows = await this._read(__kvBulkKeys(key), options, false);
1026
+ return new Map(rows.map((row) => [row.key, row.value]));
1027
+ }
1028
+ const [row] = await this._read([String(key)], options, false);
1029
+ return row.value;
1030
+ }
1031
+
1032
+ async getWithMetadata(key, options) {
1033
+ if (Array.isArray(key)) {
1034
+ const rows = await this._read(__kvBulkKeys(key), options, true);
1035
+ return new Map(
1036
+ rows.map((row) => [row.key, { value: row.value, metadata: row.metadata }]),
1037
+ );
1038
+ }
1039
+ const [row] = await this._read([String(key)], options, true);
1040
+ return {
1041
+ value: row.value,
1042
+ metadata: row.metadata,
1043
+ // Null, and honestly so: celld has no read cache, and reporting a HIT
1044
+ // from a runtime that never cached anything would be a lie in a field
1045
+ // applications read.
1046
+ cacheStatus: null,
1047
+ };
1048
+ }
1049
+
1050
+ async put(key, value, options) {
1051
+ const name = String(key);
1052
+ __kvCheckKey(name);
1053
+ let encoded;
1054
+ if (typeof ReadableStream !== "undefined" && value instanceof ReadableStream) {
1055
+ encoded = { value: await __kvDrainStream(value), tag: __KV_TAG_BYTES };
1056
+ } else {
1057
+ encoded = __kvEncodeValue(value);
1058
+ __kvCheckValue(encoded.value.byteLength);
1059
+ }
1060
+ const metadata = options && options.metadata !== undefined
1061
+ ? JSON.stringify(options.metadata)
1062
+ : null;
1063
+ if (metadata !== null) __kvCheckMetadata(metadata);
1064
+ const expiresAt = __kvExpiryAt(
1065
+ Date.now(),
1066
+ options && options.expiration,
1067
+ options && options.expirationTtl,
1068
+ );
1069
+ await this._stub.__kvPut({
1070
+ entries: [{ key: name, value: encoded.value, tag: encoded.tag, metadata, expiresAt }],
1071
+ });
1072
+ }
1073
+
1074
+ async delete(key) {
1075
+ const name = String(key);
1076
+ __kvCheckKey(name);
1077
+ await this._stub.__kvDelete({ keys: [name] });
1078
+ }
1079
+
1080
+ // Upstream takes one key or an array, and caps the array at the same 100 a
1081
+ // bulk get takes.
1082
+ async deleteBulk(keys) {
1083
+ const names = Array.isArray(keys) ? __kvBulkKeys(keys) : [String(keys)];
1084
+ for (const name of names) __kvCheckKey(name);
1085
+ await this._stub.__kvDelete({ keys: names });
1086
+ }
1087
+
1088
+ async list(options) {
1089
+ const prefix = options && options.prefix ? String(options.prefix) : "";
1090
+ const limit = __kvListLimit(options && options.limit);
1091
+ const after = options && options.cursor ? __kvCursor.decode(String(options.cursor)) : "";
1092
+ const page = await this._stub.__kvList({ prefix, limit, after, now: Date.now() });
1093
+ const keys = page.keys.map((entry) => {
1094
+ const key = { name: entry.name };
1095
+ if (entry.metadata !== null) key.metadata = JSON.parse(entry.metadata);
1096
+ // Upstream reports an expiration in seconds.
1097
+ if (entry.expiration !== null) key.expiration = Math.floor(entry.expiration / 1000);
1098
+ return key;
1099
+ });
1100
+ if (page.complete) return { keys, list_complete: true, cacheStatus: null };
1101
+ return {
1102
+ keys,
1103
+ list_complete: false,
1104
+ cursor: __kvCursor.encode(keys[keys.length - 1].name),
1105
+ cacheStatus: null,
1106
+ };
1107
+ }
1108
+ }
1109
+
1110
+ // The bulk ceiling belongs to the binding, and a CLI chunks beneath it rather
1111
+ // than inheriting it. An empty array is refused because upstream refuses it:
1112
+ // answering an empty Map would look like "none of these keys exist".
1113
+ const __kvBulkKeys = (keys) => {
1114
+ if (keys.length === 0) throw __kvError("a bulk get needs at least one key");
1115
+ if (keys.length > __kvLimits().maxBulkKeys) {
1116
+ throw __kvError(
1117
+ `a bulk get takes at most ${__kvLimits().maxBulkKeys} keys, got ${keys.length}`,
1118
+ );
1119
+ }
1120
+ return keys.map(String);
1121
+ };
1122
+
1123
+ return {
1124
+ KvNamespaceCell: __KvNamespaceCell,
1125
+ KvNamespace,
1126
+ // Queue retention runs the same bounded transaction.
1127
+ cellSweepBatch: __cellSweepBatch,
1128
+ };
1129
+ }
1130
+
1131
+ export { kvService };