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