@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.
- package/LICENSE +21 -0
- package/README.md +323 -2
- package/dist/adapters/files-backup.d.ts +16 -0
- package/dist/adapters/files-backup.js +32 -0
- package/dist/adapters/files-backup.js.map +1 -0
- package/dist/adapters/files-changelog.d.ts +33 -0
- package/dist/adapters/files-changelog.js +96 -0
- package/dist/adapters/files-changelog.js.map +1 -0
- package/dist/adapters/files-ltx.d.ts +35 -0
- package/dist/adapters/files-ltx.js +9 -0
- package/dist/adapters/files-ltx.js.map +1 -0
- package/dist/adapters/ltx-vfs.d.ts +21 -0
- package/dist/adapters/ltx-vfs.js +286 -0
- package/dist/adapters/ltx-vfs.js.map +1 -0
- package/dist/adapters/node-sqlite.d.ts +45 -0
- package/dist/adapters/node-sqlite.js +254 -0
- package/dist/adapters/node-sqlite.js.map +1 -0
- package/dist/adapters/wasm-page-hook.d.ts +26 -0
- package/dist/adapters/wasm-page-hook.js +55 -0
- package/dist/adapters/wasm-page-hook.js.map +1 -0
- package/dist/adapters/wasm-session.d.ts +11 -0
- package/dist/adapters/wasm-session.js +76 -0
- package/dist/adapters/wasm-session.js.map +1 -0
- package/dist/adapters/wasm-sqlite.d.ts +89 -0
- package/dist/adapters/wasm-sqlite.js +258 -0
- package/dist/adapters/wasm-sqlite.js.map +1 -0
- package/dist/bindings/assets.d.ts +26 -0
- package/dist/bindings/assets.js +260 -0
- package/dist/bindings/assets.js.map +1 -0
- package/dist/bindings/cache.d.ts +16 -0
- package/dist/bindings/cache.js +34 -0
- package/dist/bindings/cache.js.map +1 -0
- package/dist/bindings/celld-kv.d.ts +26 -0
- package/dist/bindings/celld-kv.js +81 -0
- package/dist/bindings/celld-kv.js.map +1 -0
- package/dist/bindings/celld-queues.d.ts +56 -0
- package/dist/bindings/celld-queues.js +223 -0
- package/dist/bindings/celld-queues.js.map +1 -0
- package/dist/bindings/celld-workflows.d.ts +22 -0
- package/dist/bindings/celld-workflows.js +54 -0
- package/dist/bindings/celld-workflows.js.map +1 -0
- package/dist/bindings/cron.d.ts +27 -0
- package/dist/bindings/cron.js +232 -0
- package/dist/bindings/cron.js.map +1 -0
- package/dist/bindings/d1-migrations.d.ts +22 -0
- package/dist/bindings/d1-migrations.js +32 -0
- package/dist/bindings/d1-migrations.js.map +1 -0
- package/dist/bindings/html-rewriter.d.ts +8 -0
- package/dist/bindings/html-rewriter.js +75 -0
- package/dist/bindings/html-rewriter.js.map +1 -0
- package/dist/bindings/r2-celld.d.ts +3 -0
- package/dist/bindings/r2-celld.js +113 -0
- package/dist/bindings/r2-celld.js.map +1 -0
- package/dist/bindings/r2.d.ts +236 -0
- package/dist/bindings/r2.js +419 -0
- package/dist/bindings/r2.js.map +1 -0
- package/dist/bindings/worker-loader.d.ts +83 -0
- package/dist/bindings/worker-loader.js +183 -0
- package/dist/bindings/worker-loader.js.map +1 -0
- package/dist/bindings/workers-ai.d.ts +39 -0
- package/dist/bindings/workers-ai.js +175 -0
- package/dist/bindings/workers-ai.js.map +1 -0
- package/dist/browser.d.ts +7 -0
- package/dist/browser.js +8 -0
- package/dist/browser.js.map +1 -0
- package/dist/bucket/celld-cells.d.ts +86 -0
- package/dist/bucket/celld-cells.js +273 -0
- package/dist/bucket/celld-cells.js.map +1 -0
- package/dist/bucket/celld-format.d.ts +10 -0
- package/dist/bucket/celld-format.js +27 -0
- package/dist/bucket/celld-format.js.map +1 -0
- package/dist/bucket/idb-object-store.d.ts +9 -0
- package/dist/bucket/idb-object-store.js +73 -0
- package/dist/bucket/idb-object-store.js.map +1 -0
- package/dist/bucket/ltx-log.d.ts +34 -0
- package/dist/bucket/ltx-log.js +289 -0
- package/dist/bucket/ltx-log.js.map +1 -0
- package/dist/bucket/object-store.d.ts +51 -0
- package/dist/bucket/object-store.js +114 -0
- package/dist/bucket/object-store.js.map +1 -0
- package/dist/bucket/s3-object-store.d.ts +25 -0
- package/dist/bucket/s3-object-store.js +151 -0
- package/dist/bucket/s3-object-store.js.map +1 -0
- package/dist/bucket/sqlite-object-store.d.ts +10 -0
- package/dist/bucket/sqlite-object-store.js +107 -0
- package/dist/bucket/sqlite-object-store.js.map +1 -0
- package/dist/core/alarms.d.ts +27 -0
- package/dist/core/alarms.js +88 -0
- package/dist/core/alarms.js.map +1 -0
- package/dist/core/async-context.d.ts +34 -0
- package/dist/core/async-context.js +50 -0
- package/dist/core/async-context.js.map +1 -0
- package/dist/core/celld-schema.d.ts +14 -0
- package/dist/core/celld-schema.js +34 -0
- package/dist/core/celld-schema.js.map +1 -0
- package/dist/core/durable-object.d.ts +28 -0
- package/dist/core/durable-object.js +31 -0
- package/dist/core/durable-object.js.map +1 -0
- package/dist/core/gates.d.ts +33 -0
- package/dist/core/gates.js +89 -0
- package/dist/core/gates.js.map +1 -0
- package/dist/core/handles.d.ts +90 -0
- package/dist/core/handles.js +224 -0
- package/dist/core/handles.js.map +1 -0
- package/dist/core/id.d.ts +19 -0
- package/dist/core/id.js +64 -0
- package/dist/core/id.js.map +1 -0
- package/dist/core/namespace.d.ts +38 -0
- package/dist/core/namespace.js +141 -0
- package/dist/core/namespace.js.map +1 -0
- package/dist/core/object-host.d.ts +91 -0
- package/dist/core/object-host.js +616 -0
- package/dist/core/object-host.js.map +1 -0
- package/dist/core/request-body.d.ts +9 -0
- package/dist/core/request-body.js +55 -0
- package/dist/core/request-body.js.map +1 -0
- package/dist/core/runtime.d.ts +121 -0
- package/dist/core/runtime.js +366 -0
- package/dist/core/runtime.js.map +1 -0
- package/dist/core/shard.d.ts +7 -0
- package/dist/core/shard.js +10 -0
- package/dist/core/shard.js.map +1 -0
- package/dist/core/sql-split.d.ts +15 -0
- package/dist/core/sql-split.js +101 -0
- package/dist/core/sql-split.js.map +1 -0
- package/dist/core/sql-storage.d.ts +53 -0
- package/dist/core/sql-storage.js +407 -0
- package/dist/core/sql-storage.js.map +1 -0
- package/dist/core/state.d.ts +44 -0
- package/dist/core/state.js +93 -0
- package/dist/core/state.js.map +1 -0
- package/dist/core/storage.d.ts +103 -0
- package/dist/core/storage.js +351 -0
- package/dist/core/storage.js.map +1 -0
- package/dist/core/worker-rejections.d.ts +21 -0
- package/dist/core/worker-rejections.js +55 -0
- package/dist/core/worker-rejections.js.map +1 -0
- package/dist/index.d.ts +22 -0
- package/dist/index.js +23 -0
- package/dist/index.js.map +1 -0
- package/dist/isolate/browser.d.ts +16 -0
- package/dist/isolate/browser.js +37 -0
- package/dist/isolate/browser.js.map +1 -0
- package/dist/isolate/host.d.ts +24 -0
- package/dist/isolate/host.js +74 -0
- package/dist/isolate/host.js.map +1 -0
- package/dist/isolate/isolate-worker.d.ts +1 -0
- package/dist/isolate/isolate-worker.js +53 -0
- package/dist/isolate/isolate-worker.js.map +1 -0
- package/dist/isolate/isolate.d.ts +10 -0
- package/dist/isolate/isolate.js +169 -0
- package/dist/isolate/isolate.js.map +1 -0
- package/dist/isolate/node-entry.d.ts +1 -0
- package/dist/isolate/node-entry.js +12 -0
- package/dist/isolate/node-entry.js.map +1 -0
- package/dist/isolate/protocol.d.ts +66 -0
- package/dist/isolate/protocol.js +205 -0
- package/dist/isolate/protocol.js.map +1 -0
- package/dist/ltx/crc64.d.ts +6 -0
- package/dist/ltx/crc64.js +46 -0
- package/dist/ltx/crc64.js.map +1 -0
- package/dist/ltx/ltx-page-server.d.ts +65 -0
- package/dist/ltx/ltx-page-server.js +309 -0
- package/dist/ltx/ltx-page-server.js.map +1 -0
- package/dist/ltx/ltx-pages.d.ts +81 -0
- package/dist/ltx/ltx-pages.js +315 -0
- package/dist/ltx/ltx-pages.js.map +1 -0
- package/dist/ltx/ltx-store-worker.d.ts +1 -0
- package/dist/ltx/ltx-store-worker.js +38 -0
- package/dist/ltx/ltx-store-worker.js.map +1 -0
- package/dist/ltx/ltx.d.ts +45 -0
- package/dist/ltx/ltx.js +203 -0
- package/dist/ltx/ltx.js.map +1 -0
- package/dist/ltx/lz4.d.ts +9 -0
- package/dist/ltx/lz4.js +156 -0
- package/dist/ltx/lz4.js.map +1 -0
- package/dist/ltx/opfs-sync-files.d.ts +23 -0
- package/dist/ltx/opfs-sync-files.js +191 -0
- package/dist/ltx/opfs-sync-files.js.map +1 -0
- package/dist/ltx/xxh32.d.ts +1 -0
- package/dist/ltx/xxh32.js +43 -0
- package/dist/ltx/xxh32.js.map +1 -0
- package/dist/miniflare-compat/blob-service.d.ts +13 -0
- package/dist/miniflare-compat/blob-service.js +66 -0
- package/dist/miniflare-compat/blob-service.js.map +1 -0
- package/dist/miniflare-compat/kv-binding.d.ts +44 -0
- package/dist/miniflare-compat/kv-binding.js +99 -0
- package/dist/miniflare-compat/kv-binding.js.map +1 -0
- package/dist/miniflare-compat/shims/assert.d.ts +15 -0
- package/dist/miniflare-compat/shims/assert.js +24 -0
- package/dist/miniflare-compat/shims/assert.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.d.ts +7 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.js +6 -0
- package/dist/miniflare-compat/shims/workerd-cloudflare.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-globals.d.ts +1 -0
- package/dist/miniflare-compat/shims/workerd-globals.js +30 -0
- package/dist/miniflare-compat/shims/workerd-globals.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-internal.d.ts +20 -0
- package/dist/miniflare-compat/shims/workerd-internal.js +23 -0
- package/dist/miniflare-compat/shims/workerd-internal.js.map +1 -0
- package/dist/miniflare-compat/shims/workerd-performance.d.ts +4 -0
- package/dist/miniflare-compat/shims/workerd-performance.js +17 -0
- package/dist/miniflare-compat/shims/workerd-performance.js.map +1 -0
- package/dist/miniflare-compat/simulator-fetcher.d.ts +9 -0
- package/dist/miniflare-compat/simulator-fetcher.js +14 -0
- package/dist/miniflare-compat/simulator-fetcher.js.map +1 -0
- package/dist/miniflare-compat/vendored.d.ts +15 -0
- package/dist/miniflare-compat/vendored.js +27 -0
- package/dist/miniflare-compat/vendored.js.map +1 -0
- package/dist/node-compat/async_hooks.d.ts +22 -0
- package/dist/node-compat/async_hooks.js +176 -0
- package/dist/node-compat/async_hooks.js.map +1 -0
- package/dist/node-compat/browser-registry.d.ts +3 -0
- package/dist/node-compat/browser-registry.js +39 -0
- package/dist/node-compat/browser-registry.js.map +1 -0
- package/dist/node-compat/crypto.d.ts +60 -0
- package/dist/node-compat/crypto.js +143 -0
- package/dist/node-compat/crypto.js.map +1 -0
- package/dist/node-compat/diagnostics_channel.d.ts +27 -0
- package/dist/node-compat/diagnostics_channel.js +46 -0
- package/dist/node-compat/diagnostics_channel.js.map +1 -0
- package/dist/node-compat/fs.d.ts +35 -0
- package/dist/node-compat/fs.js +40 -0
- package/dist/node-compat/fs.js.map +1 -0
- package/dist/node-compat/globals.d.ts +3 -0
- package/dist/node-compat/globals.js +4 -0
- package/dist/node-compat/globals.js.map +1 -0
- package/dist/node-compat/install.d.ts +5 -0
- package/dist/node-compat/install.js +12 -0
- package/dist/node-compat/install.js.map +1 -0
- package/dist/node-compat/module.d.ts +12 -0
- package/dist/node-compat/module.js +36 -0
- package/dist/node-compat/module.js.map +1 -0
- package/dist/node-compat/modules.d.ts +8 -0
- package/dist/node-compat/modules.js +23 -0
- package/dist/node-compat/modules.js.map +1 -0
- package/dist/node-compat/os.d.ts +36 -0
- package/dist/node-compat/os.js +36 -0
- package/dist/node-compat/os.js.map +1 -0
- package/dist/node-compat/process.d.ts +14 -0
- package/dist/node-compat/process.js +16 -0
- package/dist/node-compat/process.js.map +1 -0
- package/dist/node-compat/shims.d.ts +4 -0
- package/dist/node.d.ts +5 -0
- package/dist/node.js +6 -0
- package/dist/node.js.map +1 -0
- package/dist/peer/celld-peer.d.ts +53 -0
- package/dist/peer/celld-peer.js +555 -0
- package/dist/peer/celld-peer.js.map +1 -0
- package/dist/ports.d.ts +183 -0
- package/dist/ports.js +10 -0
- package/dist/ports.js.map +1 -0
- package/dist/project/browser-bundle.d.ts +17 -0
- package/dist/project/browser-bundle.js +125 -0
- package/dist/project/browser-bundle.js.map +1 -0
- package/dist/project/browser-cookies.d.ts +12 -0
- package/dist/project/browser-cookies.js +90 -0
- package/dist/project/browser-cookies.js.map +1 -0
- package/dist/project/browser-host.d.ts +173 -0
- package/dist/project/browser-host.js +200 -0
- package/dist/project/browser-host.js.map +1 -0
- package/dist/project/browser-modules.d.ts +17 -0
- package/dist/project/browser-modules.js +89 -0
- package/dist/project/browser-modules.js.map +1 -0
- package/dist/project/browser-shards.d.ts +62 -0
- package/dist/project/browser-shards.js +210 -0
- package/dist/project/browser-shards.js.map +1 -0
- package/dist/project/browser-tabs.d.ts +103 -0
- package/dist/project/browser-tabs.js +418 -0
- package/dist/project/browser-tabs.js.map +1 -0
- package/dist/project/cloudflare-email.d.ts +8 -0
- package/dist/project/cloudflare-email.js +11 -0
- package/dist/project/cloudflare-email.js.map +1 -0
- package/dist/project/cloudflare-workers.d.ts +12 -0
- package/dist/project/cloudflare-workers.js +16 -0
- package/dist/project/cloudflare-workers.js.map +1 -0
- package/dist/project/host-modules.d.ts +4 -0
- package/dist/project/host-modules.js +40 -0
- package/dist/project/host-modules.js.map +1 -0
- package/dist/project/jsonc.d.ts +2 -0
- package/dist/project/jsonc.js +27 -0
- package/dist/project/jsonc.js.map +1 -0
- package/dist/project/lower-await.d.ts +2 -0
- package/dist/project/lower-await.js +151 -0
- package/dist/project/lower-await.js.map +1 -0
- package/dist/project/node-project.d.ts +38 -0
- package/dist/project/node-project.js +373 -0
- package/dist/project/node-project.js.map +1 -0
- package/dist/project/own-module.d.ts +6 -0
- package/dist/project/own-module.js +13 -0
- package/dist/project/own-module.js.map +1 -0
- package/dist/project/scheduler.d.ts +2 -0
- package/dist/project/scheduler.js +3 -0
- package/dist/project/scheduler.js.map +1 -0
- package/dist/project/site.d.ts +74 -0
- package/dist/project/site.js +206 -0
- package/dist/project/site.js.map +1 -0
- package/dist/project/virtual-modules.d.ts +3 -0
- package/dist/project/worker-scope.d.ts +27 -0
- package/dist/project/worker-scope.js +46 -0
- package/dist/project/worker-scope.js.map +1 -0
- package/dist/project/workerd-message-channel.d.ts +13 -0
- package/dist/project/workerd-message-channel.js +137 -0
- package/dist/project/workerd-message-channel.js.map +1 -0
- package/dist/project/workers-global-names.d.ts +6 -0
- package/dist/project/workers-global-names.js +15 -0
- package/dist/project/workers-global-names.js.map +1 -0
- package/dist/project/workers-globals-module.d.ts +8 -0
- package/dist/project/workers-globals-module.js +9 -0
- package/dist/project/workers-globals-module.js.map +1 -0
- package/dist/project/workers-globals.d.ts +9 -0
- package/dist/project/workers-globals.js +19 -0
- package/dist/project/workers-globals.js.map +1 -0
- package/dist/project/wrangler.d.ts +116 -0
- package/dist/project/wrangler.js +342 -0
- package/dist/project/wrangler.js.map +1 -0
- package/dist/router/cell-router.d.ts +72 -0
- package/dist/router/cell-router.js +247 -0
- package/dist/router/cell-router.js.map +1 -0
- package/dist/router/shard-worker.d.ts +44 -0
- package/dist/router/shard-worker.js +163 -0
- package/dist/router/shard-worker.js.map +1 -0
- package/dist/transport/cells-http.d.ts +25 -0
- package/dist/transport/cells-http.js +141 -0
- package/dist/transport/cells-http.js.map +1 -0
- package/dist/transport/cells-link.d.ts +27 -0
- package/dist/transport/cells-link.js +71 -0
- package/dist/transport/cells-link.js.map +1 -0
- package/dist/transport/port.d.ts +26 -0
- package/dist/transport/port.js +100 -0
- package/dist/transport/port.js.map +1 -0
- package/dist/v8/codec.d.ts +6 -0
- package/dist/v8/codec.js +9 -0
- package/dist/v8/codec.js.map +1 -0
- package/dist/v8/serialize.d.ts +10 -0
- package/dist/v8/serialize.js +601 -0
- package/dist/v8/serialize.js.map +1 -0
- package/dist/ws/client.d.ts +25 -0
- package/dist/ws/client.js +203 -0
- package/dist/ws/client.js.map +1 -0
- package/dist/ws/control.d.ts +48 -0
- package/dist/ws/control.js +44 -0
- package/dist/ws/control.js.map +1 -0
- package/dist/ws/frames.d.ts +38 -0
- package/dist/ws/frames.js +39 -0
- package/dist/ws/frames.js.map +1 -0
- package/dist/ws/server.d.ts +99 -0
- package/dist/ws/server.js +295 -0
- package/dist/ws/server.js.map +1 -0
- package/dist/ws/sw-connector.d.ts +10 -0
- package/dist/ws/sw-connector.js +25 -0
- package/dist/ws/sw-connector.js.map +1 -0
- package/dist/ws/upgrade.d.ts +28 -0
- package/dist/ws/upgrade.js +75 -0
- package/dist/ws/upgrade.js.map +1 -0
- package/package.json +111 -3
- package/vendor/celld/LICENSE +202 -0
- package/vendor/celld/kv.js +1131 -0
- package/vendor/celld/queues.js +1656 -0
- package/vendor/celld/workflows.js +1610 -0
- package/vendor/html-rewriter-wasm/html_rewriter.js +925 -0
- package/vendor/html-rewriter-wasm/html_rewriter_bg.wasm +0 -0
- package/vendor/miniflare/d1-api.js +738 -0
- package/vendor/miniflare/d1-database.js +6978 -0
- 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 };
|