@statewalker/cellb 0.0.0-stage → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- 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 +362 -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,273 @@
|
|
|
1
|
+
import { bytesToHex } from "@noble/hashes/utils";
|
|
2
|
+
import { scopeOf } from "../core/celld-schema.js";
|
|
3
|
+
import { ltxLog } from "./ltx-log.js";
|
|
4
|
+
import { PreconditionFailedError } from "./object-store.js";
|
|
5
|
+
const enc = new TextEncoder();
|
|
6
|
+
const dec = new TextDecoder();
|
|
7
|
+
/** celld 0.6.3: how long a node refuses a cell whose ownership record it found lost. */
|
|
8
|
+
export const LOST_RECORD_RECHECK_MS = 10_000;
|
|
9
|
+
const lostRecordError = (scope) => Object.assign(new Error(`ownership record lost: cells/${scope}/own.json is missing, but the bucket holds LTX under ` +
|
|
10
|
+
`cells/${scope}/ltx/; refusing to claim ${scope} as a new cell (celld docs/guarantees.md: ` +
|
|
11
|
+
`write a released record {"node": "", "epoch": <newest epoch>} to restore it)`), { name: "OwnershipRecordLostError" });
|
|
12
|
+
/** The bucket's `close()` also closes `store` (when it has a `close`): pass a store of its own. */
|
|
13
|
+
export function celldBucket(store, opts) {
|
|
14
|
+
const now = opts.now ?? (() => Date.now());
|
|
15
|
+
const ttl = opts.leaseTtlMs ?? 10_000;
|
|
16
|
+
const lost = new AbortController();
|
|
17
|
+
let renew;
|
|
18
|
+
let deadline;
|
|
19
|
+
let leaseWritten;
|
|
20
|
+
/** nodes/<node>.json, renewed every ttl/3; a failed renewal stops every cell (self-fencing). */
|
|
21
|
+
// celld decodes every node record (ownership_store.rs NodeLeaseWire / NodeLoadWire): a record
|
|
22
|
+
// missing a required load field breaks its fleet posture repair. This one is complete. It
|
|
23
|
+
// reports pressure and zero placement weight, so celld never offers this node a cell — it
|
|
24
|
+
// takes cells only when a request arrives.
|
|
25
|
+
//
|
|
26
|
+
// The address must not be empty: celld treats a live owner WITHOUT an address as dead and
|
|
27
|
+
// takes its cells over (logic/lib.rs apply_node_lease_result) — a split brain. With an
|
|
28
|
+
// address celld routes a request for our cell to it; a node that does not accept celld's
|
|
29
|
+
// peer protocol (a browser) gets an unroutable placeholder, so the request fails instead.
|
|
30
|
+
let addr = opts.addr || `${opts.node}.cellb.invalid:9`;
|
|
31
|
+
// celld's per-process generation is its signed-probe public key (Ed25519, peer_probe.rs):
|
|
32
|
+
// `probe_public_key` = `ownership_index_generation`. A real key lets `celld diagnose --peer`
|
|
33
|
+
// verify that a probe reached this node.
|
|
34
|
+
let generation = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));
|
|
35
|
+
let probeKey;
|
|
36
|
+
const probeKeyReady = (async () => {
|
|
37
|
+
try {
|
|
38
|
+
const pair = (await crypto.subtle.generateKey({ name: "Ed25519" }, true, [
|
|
39
|
+
"sign",
|
|
40
|
+
"verify",
|
|
41
|
+
]));
|
|
42
|
+
probeKey = pair.privateKey;
|
|
43
|
+
generation = bytesToHex(new Uint8Array(await crypto.subtle.exportKey("raw", pair.publicKey)));
|
|
44
|
+
}
|
|
45
|
+
catch {
|
|
46
|
+
// no Ed25519 in this WebCrypto: the record keeps a random generation (probes then fail)
|
|
47
|
+
}
|
|
48
|
+
})();
|
|
49
|
+
let validUntil = 0;
|
|
50
|
+
const writeLease = async () => {
|
|
51
|
+
const t = now();
|
|
52
|
+
const body = {
|
|
53
|
+
node: opts.node,
|
|
54
|
+
expires_ms: t + ttl,
|
|
55
|
+
addr,
|
|
56
|
+
probe_public_key: generation,
|
|
57
|
+
peer_protocol: 5,
|
|
58
|
+
paced_handoff: false,
|
|
59
|
+
ownership_index_generation: generation,
|
|
60
|
+
load: {
|
|
61
|
+
sampled_ms: t,
|
|
62
|
+
owned_cells: owned.size,
|
|
63
|
+
placement_weight: 0,
|
|
64
|
+
bucket_format: 2,
|
|
65
|
+
resident_cells: owned.size,
|
|
66
|
+
host_websockets: 0,
|
|
67
|
+
rss_bytes: 0,
|
|
68
|
+
in_use_bytes: 0,
|
|
69
|
+
cpu_percent_x100: 0,
|
|
70
|
+
open_fds: 0,
|
|
71
|
+
fd_limit: 0,
|
|
72
|
+
pressured: true,
|
|
73
|
+
memory_headroom: false,
|
|
74
|
+
shed_cells: 0,
|
|
75
|
+
restoring: 0,
|
|
76
|
+
rebalance_paused: false,
|
|
77
|
+
draining: false,
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
await store.put(`nodes/${opts.node}.json`, enc.encode(JSON.stringify(body)));
|
|
81
|
+
// Our authority lasts until the record we just wrote expires — minus a margin, so a node
|
|
82
|
+
// that stalls (a paused process, a frozen tab) stops acknowledging before celld may take
|
|
83
|
+
// over: celld waits for expires_ms; we stop at expires_ms - margin.
|
|
84
|
+
validUntil = t + ttl - Math.min(1_000, ttl / 4);
|
|
85
|
+
};
|
|
86
|
+
const owned = new Set();
|
|
87
|
+
/** Cells whose ownership record was found lost, with the time their refusal ends. */
|
|
88
|
+
const lostRecords = new Map();
|
|
89
|
+
const ensureLease = () => {
|
|
90
|
+
leaseWritten ??= probeKeyReady.then(writeLease).then(() => {
|
|
91
|
+
const tick = () => {
|
|
92
|
+
renew = setTimeout(() => {
|
|
93
|
+
writeLease().then(tick, (e) => lost.abort(e));
|
|
94
|
+
}, ttl / 3);
|
|
95
|
+
// If renewals stop arriving (a stalled process, an unreachable bucket), every cell
|
|
96
|
+
// stops at the local deadline — before celld may take it over.
|
|
97
|
+
if (deadline)
|
|
98
|
+
clearTimeout(deadline);
|
|
99
|
+
deadline = setTimeout(() => {
|
|
100
|
+
if (now() >= validUntil)
|
|
101
|
+
lost.abort(new Error(`node ${opts.node}: lease deadline passed`));
|
|
102
|
+
}, Math.max(0, validUntil - now()));
|
|
103
|
+
deadline.unref?.();
|
|
104
|
+
renew.unref?.();
|
|
105
|
+
};
|
|
106
|
+
tick();
|
|
107
|
+
});
|
|
108
|
+
return leaseWritten;
|
|
109
|
+
};
|
|
110
|
+
const readJson = async (key) => {
|
|
111
|
+
const o = await store.get(key);
|
|
112
|
+
return o ? { value: JSON.parse(dec.decode(o.body)), etag: o.etag } : undefined;
|
|
113
|
+
};
|
|
114
|
+
// One LTX writer (ltx-log.ts) holds each cell's epoch, TXID and page mirror.
|
|
115
|
+
// A write is acknowledged only while our node lease is still ours (self-fencing).
|
|
116
|
+
const log = ltxLog(store, { now, authorized: () => now() < validUntil });
|
|
117
|
+
const ownership = {
|
|
118
|
+
async tryAcquire(cellKey) {
|
|
119
|
+
await ensureLease();
|
|
120
|
+
const scope = scopeOf(cellKey);
|
|
121
|
+
const key = `cells/${scope}/own.json`;
|
|
122
|
+
const t0 = now();
|
|
123
|
+
if ((lostRecords.get(scope) ?? 0) > t0)
|
|
124
|
+
throw lostRecordError(scope); // still refused
|
|
125
|
+
let cur = await readJson(key);
|
|
126
|
+
// celld 0.6.3 (actor.rs read_owner_for_claim): a missing record next to LTX means the
|
|
127
|
+
// record was LOST (e.g. a lifecycle rule) — claiming epoch 1 would start the cell empty.
|
|
128
|
+
// Read once more, then refuse the cell for 10 s without asking the bucket again.
|
|
129
|
+
if (!cur && (await store.list(`cells/${scope}/ltx`)).length > 0) {
|
|
130
|
+
cur = await readJson(key);
|
|
131
|
+
if (!cur) {
|
|
132
|
+
lostRecords.set(scope, t0 + LOST_RECORD_RECHECK_MS);
|
|
133
|
+
throw lostRecordError(scope);
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
let epoch;
|
|
137
|
+
try {
|
|
138
|
+
if (!cur) {
|
|
139
|
+
epoch = 1;
|
|
140
|
+
await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {
|
|
141
|
+
ifNoneMatch: "*",
|
|
142
|
+
});
|
|
143
|
+
}
|
|
144
|
+
else {
|
|
145
|
+
const holder = cur.value.node;
|
|
146
|
+
if (holder && holder !== opts.node) {
|
|
147
|
+
const lease = await readJson(`nodes/${holder}.json`);
|
|
148
|
+
if (lease && lease.value.expires_ms > now())
|
|
149
|
+
return undefined; // a live owner
|
|
150
|
+
}
|
|
151
|
+
epoch = cur.value.epoch + 1; // every activation advances the epoch
|
|
152
|
+
await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {
|
|
153
|
+
ifMatch: cur.etag,
|
|
154
|
+
});
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
catch (e) {
|
|
158
|
+
if (e instanceof PreconditionFailedError)
|
|
159
|
+
return undefined; // another node won the race
|
|
160
|
+
throw e;
|
|
161
|
+
}
|
|
162
|
+
log.begin(cellKey, epoch); // a new owner epoch starts a new generation
|
|
163
|
+
owned.add(cellKey);
|
|
164
|
+
return {
|
|
165
|
+
name: cellKey,
|
|
166
|
+
epoch,
|
|
167
|
+
lost: lost.signal,
|
|
168
|
+
release: async () => {
|
|
169
|
+
const now2 = await readJson(key);
|
|
170
|
+
if (now2?.value.node === opts.node && now2.value.epoch === epoch)
|
|
171
|
+
await store
|
|
172
|
+
.put(key, enc.encode(JSON.stringify({ node: "", epoch })), { ifMatch: now2.etag })
|
|
173
|
+
.catch(() => { });
|
|
174
|
+
log.forget(cellKey);
|
|
175
|
+
owned.delete(cellKey);
|
|
176
|
+
},
|
|
177
|
+
};
|
|
178
|
+
},
|
|
179
|
+
};
|
|
180
|
+
// celld's waker role (wake.rs try_hold_waker): one node per fleet wakes alarms of cells that
|
|
181
|
+
// nobody owns or whose owner is dead — an advisory CAS lease in wake/waker.json; every node
|
|
182
|
+
// wakes its own cells; a live owner's cells are never hinted.
|
|
183
|
+
const holdWaker = async (ttlMs) => {
|
|
184
|
+
const key = "wake/waker.json";
|
|
185
|
+
const cur = await readJson(key);
|
|
186
|
+
const t = now();
|
|
187
|
+
if (cur && cur.value.node !== opts.node && cur.value.expires_ms > t)
|
|
188
|
+
return false;
|
|
189
|
+
try {
|
|
190
|
+
await store.put(key, enc.encode(JSON.stringify({ node: opts.node, expires_ms: t + ttlMs })), cur ? { ifMatch: cur.etag } : { ifNoneMatch: "*" });
|
|
191
|
+
return true;
|
|
192
|
+
}
|
|
193
|
+
catch (e) {
|
|
194
|
+
if (!(e instanceof PreconditionFailedError))
|
|
195
|
+
return false;
|
|
196
|
+
const again = await readJson(key);
|
|
197
|
+
return again?.value.node === opts.node && again.value.expires_ms > t;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
200
|
+
const allWakes = log.wakes.bind(log);
|
|
201
|
+
const durability = Object.assign(log, {
|
|
202
|
+
async wakes() {
|
|
203
|
+
const due = await allWakes();
|
|
204
|
+
let elected;
|
|
205
|
+
const out = [];
|
|
206
|
+
for (const w of due) {
|
|
207
|
+
const own = await readJson(`cells/${scopeOf(w.cellKey)}/own.json`);
|
|
208
|
+
const owner = own?.value.node;
|
|
209
|
+
if (owner === opts.node) {
|
|
210
|
+
out.push(w); // our own cell: always
|
|
211
|
+
continue;
|
|
212
|
+
}
|
|
213
|
+
let ownerExpires = 0;
|
|
214
|
+
if (owner) {
|
|
215
|
+
const lease = await readJson(`nodes/${owner}.json`);
|
|
216
|
+
ownerExpires = lease?.value.expires_ms ?? 0;
|
|
217
|
+
}
|
|
218
|
+
elected ??= await holdWaker(opts.wakerTtlMs ?? 90_000);
|
|
219
|
+
if (!elected)
|
|
220
|
+
continue; // unowned or dead owners are the elected waker's job
|
|
221
|
+
// A live owner fires its own alarms; the waker looks again when that owner's lease
|
|
222
|
+
// would have run out (a stopped owner's cells are then due here).
|
|
223
|
+
out.push(ownerExpires > now() ? { ...w, at: Math.max(w.at, ownerExpires + 1) } : w);
|
|
224
|
+
}
|
|
225
|
+
return out;
|
|
226
|
+
},
|
|
227
|
+
});
|
|
228
|
+
return {
|
|
229
|
+
ownership,
|
|
230
|
+
durability,
|
|
231
|
+
backup: durability,
|
|
232
|
+
/** The fleet bucket itself (R2 objects live directly in it, under `r2/<bucket>/`). */
|
|
233
|
+
objects: store,
|
|
234
|
+
/**
|
|
235
|
+
* The address celld nodes reach this node at (its celld peer server): published in the
|
|
236
|
+
* node record now, so celld routes requests for our cells here instead of refusing them.
|
|
237
|
+
*/
|
|
238
|
+
/**
|
|
239
|
+
* Answers celld's signed peer probe (peer_probe.rs): an Ed25519 signature over
|
|
240
|
+
* "cells-peer-probe-v1\n<node>\n<advertised addr>\n<challenge>" by this node's probe key.
|
|
241
|
+
*/
|
|
242
|
+
async probe(challenge) {
|
|
243
|
+
await probeKeyReady;
|
|
244
|
+
if (!probeKey || !/^[0-9a-f]{64}$/.test(challenge))
|
|
245
|
+
throw new Error("cannot answer the probe");
|
|
246
|
+
const payload = `cells-peer-probe-v1\n${opts.node}\n${addr}\n${challenge}`;
|
|
247
|
+
const signature = new Uint8Array(await crypto.subtle.sign("Ed25519", probeKey, new TextEncoder().encode(payload)));
|
|
248
|
+
return {
|
|
249
|
+
version: 1,
|
|
250
|
+
node: opts.node,
|
|
251
|
+
advertise: addr,
|
|
252
|
+
challenge,
|
|
253
|
+
signature: bytesToHex(signature),
|
|
254
|
+
};
|
|
255
|
+
},
|
|
256
|
+
async advertise(address) {
|
|
257
|
+
addr = address;
|
|
258
|
+
if (leaseWritten)
|
|
259
|
+
await writeLease();
|
|
260
|
+
},
|
|
261
|
+
/** Clean shutdown: stop renewing, remove this node's lease record, close the store. */
|
|
262
|
+
async close() {
|
|
263
|
+
if (renew)
|
|
264
|
+
clearTimeout(renew);
|
|
265
|
+
if (deadline)
|
|
266
|
+
clearTimeout(deadline);
|
|
267
|
+
if (leaseWritten)
|
|
268
|
+
await store.delete(`nodes/${opts.node}.json`).catch(() => { });
|
|
269
|
+
store.close?.();
|
|
270
|
+
},
|
|
271
|
+
};
|
|
272
|
+
}
|
|
273
|
+
//# sourceMappingURL=celld-cells.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"celld-cells.js","sourceRoot":"","sources":["../../src/bucket/celld-cells.ts"],"names":[],"mappings":"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","sourcesContent":["import { bytesToHex } from \"@noble/hashes/utils\";\nimport { scopeOf } from \"../core/celld-schema.js\";\nimport { decodeLtx, encodeLtx, ltxFileName } from \"../ltx/ltx.js\";\nimport type { BackupInfo, CellBackup, CellDurability } from \"../ports.js\";\nimport { equal, pad16, pageSizeOf, toRollback } from \"./celld-format.js\";\nimport { ltxLog } from \"./ltx-log.js\";\nimport { type ObjectStore, PreconditionFailedError } from \"./object-store.js\";\n\n/**\n * Cells stored the way celld stores them (docs/guarantees.md, and the keys celld dev writes\n * in `.celld/dev/objects.sqlite3`), on any ObjectStore:\n *\n * cells/<Class>:<idHex>/own.json {\"node\",\"epoch\"}: ownership record\n * cells/<Class>:<idHex>/ltx/e<epoch>/0000/<min>-<max>.ltx LTX v3, legacy LZ4 frames\n * nodes/<node>.json {\"node\",\"expires_ms\",\"addr\",…}: node lease\n *\n * Ownership: an activation creates the record (If-None-Match) or advances its epoch with a\n * compare-and-swap (If-Match), taking over only from a node whose lease expired. Durability:\n * each committed transaction becomes an LTX file in the cell's current epoch prefix — the\n * first one a snapshot (TXID 1), then the changed pages — and the output gate waits for the\n * PUT. Restore: the newest epoch that starts with a snapshot, its files in TXID order.\n */\nexport interface CelldBucketOptions {\n /** This node's name (celld: `dev-…` / the fleet node name). */\n node: string;\n addr?: string;\n leaseTtlMs?: number;\n now?: () => number;\n /** The waker-role lease (celld: 3 × its due-scan tick); match 3 × the runtime's wakePollMs. */\n wakerTtlMs?: number;\n}\n\nconst enc = new TextEncoder();\nconst dec = new TextDecoder();\n\n/** celld 0.6.3: how long a node refuses a cell whose ownership record it found lost. */\nexport const LOST_RECORD_RECHECK_MS = 10_000;\nconst lostRecordError = (scope: string) =>\n Object.assign(\n new Error(\n `ownership record lost: cells/${scope}/own.json is missing, but the bucket holds LTX under ` +\n `cells/${scope}/ltx/; refusing to claim ${scope} as a new cell (celld docs/guarantees.md: ` +\n `write a released record {\"node\": \"\", \"epoch\": <newest epoch>} to restore it)`,\n ),\n { name: \"OwnershipRecordLostError\" },\n );\n\n/** The bucket's `close()` also closes `store` (when it has a `close`): pass a store of its own. */\nexport function celldBucket(store: ObjectStore & { close?(): void }, opts: CelldBucketOptions) {\n const now = opts.now ?? (() => Date.now());\n const ttl = opts.leaseTtlMs ?? 10_000;\n const lost = new AbortController();\n let renew: ReturnType<typeof setTimeout> | undefined;\n let deadline: ReturnType<typeof setTimeout> | undefined;\n let leaseWritten: Promise<void> | undefined;\n\n /** nodes/<node>.json, renewed every ttl/3; a failed renewal stops every cell (self-fencing). */\n // celld decodes every node record (ownership_store.rs NodeLeaseWire / NodeLoadWire): a record\n // missing a required load field breaks its fleet posture repair. This one is complete. It\n // reports pressure and zero placement weight, so celld never offers this node a cell — it\n // takes cells only when a request arrives.\n //\n // The address must not be empty: celld treats a live owner WITHOUT an address as dead and\n // takes its cells over (logic/lib.rs apply_node_lease_result) — a split brain. With an\n // address celld routes a request for our cell to it; a node that does not accept celld's\n // peer protocol (a browser) gets an unroutable placeholder, so the request fails instead.\n let addr = opts.addr || `${opts.node}.cellb.invalid:9`;\n // celld's per-process generation is its signed-probe public key (Ed25519, peer_probe.rs):\n // `probe_public_key` = `ownership_index_generation`. A real key lets `celld diagnose --peer`\n // verify that a probe reached this node.\n let generation = bytesToHex(crypto.getRandomValues(new Uint8Array(32)));\n let probeKey: CryptoKey | undefined;\n const probeKeyReady = (async () => {\n try {\n const pair = (await crypto.subtle.generateKey({ name: \"Ed25519\" }, true, [\n \"sign\",\n \"verify\",\n ])) as CryptoKeyPair;\n probeKey = pair.privateKey;\n generation = bytesToHex(new Uint8Array(await crypto.subtle.exportKey(\"raw\", pair.publicKey)));\n } catch {\n // no Ed25519 in this WebCrypto: the record keeps a random generation (probes then fail)\n }\n })();\n let validUntil = 0;\n const writeLease = async () => {\n const t = now();\n const body = {\n node: opts.node,\n expires_ms: t + ttl,\n addr,\n probe_public_key: generation,\n peer_protocol: 5,\n paced_handoff: false,\n ownership_index_generation: generation,\n load: {\n sampled_ms: t,\n owned_cells: owned.size,\n placement_weight: 0,\n bucket_format: 2,\n resident_cells: owned.size,\n host_websockets: 0,\n rss_bytes: 0,\n in_use_bytes: 0,\n cpu_percent_x100: 0,\n open_fds: 0,\n fd_limit: 0,\n pressured: true,\n memory_headroom: false,\n shed_cells: 0,\n restoring: 0,\n rebalance_paused: false,\n draining: false,\n },\n };\n await store.put(`nodes/${opts.node}.json`, enc.encode(JSON.stringify(body)));\n // Our authority lasts until the record we just wrote expires — minus a margin, so a node\n // that stalls (a paused process, a frozen tab) stops acknowledging before celld may take\n // over: celld waits for expires_ms; we stop at expires_ms - margin.\n validUntil = t + ttl - Math.min(1_000, ttl / 4);\n };\n const owned = new Set<string>();\n /** Cells whose ownership record was found lost, with the time their refusal ends. */\n const lostRecords = new Map<string, number>();\n const ensureLease = () => {\n leaseWritten ??= probeKeyReady.then(writeLease).then(() => {\n const tick = () => {\n renew = setTimeout(() => {\n writeLease().then(tick, (e) => lost.abort(e));\n }, ttl / 3);\n // If renewals stop arriving (a stalled process, an unreachable bucket), every cell\n // stops at the local deadline — before celld may take it over.\n if (deadline) clearTimeout(deadline);\n deadline = setTimeout(\n () => {\n if (now() >= validUntil)\n lost.abort(new Error(`node ${opts.node}: lease deadline passed`));\n },\n Math.max(0, validUntil - now()),\n );\n (deadline as { unref?: () => void }).unref?.();\n (renew as { unref?: () => void }).unref?.();\n };\n tick();\n });\n return leaseWritten;\n };\n\n const readJson = async <T>(key: string) => {\n const o = await store.get(key);\n return o ? { value: JSON.parse(dec.decode(o.body)) as T, etag: o.etag } : undefined;\n };\n\n // One LTX writer (ltx-log.ts) holds each cell's epoch, TXID and page mirror.\n // A write is acknowledged only while our node lease is still ours (self-fencing).\n const log = ltxLog(store, { now, authorized: () => now() < validUntil });\n\n const ownership = {\n async tryAcquire(cellKey: string) {\n await ensureLease();\n const scope = scopeOf(cellKey);\n const key = `cells/${scope}/own.json`;\n const t0 = now();\n if ((lostRecords.get(scope) ?? 0) > t0) throw lostRecordError(scope); // still refused\n let cur = await readJson<{ node: string; epoch: number }>(key);\n // celld 0.6.3 (actor.rs read_owner_for_claim): a missing record next to LTX means the\n // record was LOST (e.g. a lifecycle rule) — claiming epoch 1 would start the cell empty.\n // Read once more, then refuse the cell for 10 s without asking the bucket again.\n if (!cur && (await store.list(`cells/${scope}/ltx`)).length > 0) {\n cur = await readJson<{ node: string; epoch: number }>(key);\n if (!cur) {\n lostRecords.set(scope, t0 + LOST_RECORD_RECHECK_MS);\n throw lostRecordError(scope);\n }\n }\n let epoch: number;\n try {\n if (!cur) {\n epoch = 1;\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifNoneMatch: \"*\",\n });\n } else {\n const holder = cur.value.node;\n if (holder && holder !== opts.node) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${holder}.json`);\n if (lease && lease.value.expires_ms > now()) return undefined; // a live owner\n }\n epoch = cur.value.epoch + 1; // every activation advances the epoch\n await store.put(key, enc.encode(JSON.stringify({ node: opts.node, epoch })), {\n ifMatch: cur.etag,\n });\n }\n } catch (e) {\n if (e instanceof PreconditionFailedError) return undefined; // another node won the race\n throw e;\n }\n log.begin(cellKey, epoch); // a new owner epoch starts a new generation\n owned.add(cellKey);\n return {\n name: cellKey,\n epoch,\n lost: lost.signal,\n release: async () => {\n const now2 = await readJson<{ node: string; epoch: number }>(key);\n if (now2?.value.node === opts.node && now2.value.epoch === epoch)\n await store\n .put(key, enc.encode(JSON.stringify({ node: \"\", epoch })), { ifMatch: now2.etag })\n .catch(() => {});\n log.forget(cellKey);\n owned.delete(cellKey);\n },\n };\n },\n };\n\n // celld's waker role (wake.rs try_hold_waker): one node per fleet wakes alarms of cells that\n // nobody owns or whose owner is dead — an advisory CAS lease in wake/waker.json; every node\n // wakes its own cells; a live owner's cells are never hinted.\n const holdWaker = async (ttlMs: number) => {\n const key = \"wake/waker.json\";\n const cur = await readJson<{ node: string; expires_ms: number }>(key);\n const t = now();\n if (cur && cur.value.node !== opts.node && cur.value.expires_ms > t) return false;\n try {\n await store.put(\n key,\n enc.encode(JSON.stringify({ node: opts.node, expires_ms: t + ttlMs })),\n cur ? { ifMatch: cur.etag } : { ifNoneMatch: \"*\" },\n );\n return true;\n } catch (e) {\n if (!(e instanceof PreconditionFailedError)) return false;\n const again = await readJson<{ node: string; expires_ms: number }>(key);\n return again?.value.node === opts.node && again.value.expires_ms > t;\n }\n };\n const allWakes = log.wakes!.bind(log);\n const durability = Object.assign(log, {\n async wakes() {\n const due = await allWakes();\n let elected: boolean | undefined;\n const out: typeof due = [];\n for (const w of due) {\n const own = await readJson<{ node: string }>(`cells/${scopeOf(w.cellKey)}/own.json`);\n const owner = own?.value.node;\n if (owner === opts.node) {\n out.push(w); // our own cell: always\n continue;\n }\n let ownerExpires = 0;\n if (owner) {\n const lease = await readJson<{ expires_ms: number }>(`nodes/${owner}.json`);\n ownerExpires = lease?.value.expires_ms ?? 0;\n }\n elected ??= await holdWaker(opts.wakerTtlMs ?? 90_000);\n if (!elected) continue; // unowned or dead owners are the elected waker's job\n // A live owner fires its own alarms; the waker looks again when that owner's lease\n // would have run out (a stopped owner's cells are then due here).\n out.push(ownerExpires > now() ? { ...w, at: Math.max(w.at, ownerExpires + 1) } : w);\n }\n return out;\n },\n });\n\n return {\n ownership,\n durability,\n backup: durability,\n /** The fleet bucket itself (R2 objects live directly in it, under `r2/<bucket>/`). */\n objects: store,\n /**\n * The address celld nodes reach this node at (its celld peer server): published in the\n * node record now, so celld routes requests for our cells here instead of refusing them.\n */\n /**\n * Answers celld's signed peer probe (peer_probe.rs): an Ed25519 signature over\n * \"cells-peer-probe-v1\\n<node>\\n<advertised addr>\\n<challenge>\" by this node's probe key.\n */\n async probe(challenge: string) {\n await probeKeyReady;\n if (!probeKey || !/^[0-9a-f]{64}$/.test(challenge))\n throw new Error(\"cannot answer the probe\");\n const payload = `cells-peer-probe-v1\\n${opts.node}\\n${addr}\\n${challenge}`;\n const signature = new Uint8Array(\n await crypto.subtle.sign(\"Ed25519\", probeKey, new TextEncoder().encode(payload)),\n );\n return {\n version: 1,\n node: opts.node,\n advertise: addr,\n challenge,\n signature: bytesToHex(signature),\n };\n },\n async advertise(address: string) {\n addr = address;\n if (leaseWritten) await writeLease();\n },\n /** Clean shutdown: stop renewing, remove this node's lease record, close the store. */\n async close() {\n if (renew) clearTimeout(renew);\n if (deadline) clearTimeout(deadline);\n if (leaseWritten) await store.delete(`nodes/${opts.node}.json`).catch(() => {});\n store.close?.();\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
/** Byte and name helpers shared by the celld bucket layout (celld-cells) and its LTX log. */
|
|
2
|
+
export declare const pad16: (n: bigint) => string;
|
|
3
|
+
export declare const equal: (a: Uint8Array, b: Uint8Array) => boolean;
|
|
4
|
+
export declare const pageSizeOf: (image: Uint8Array) => number;
|
|
5
|
+
/**
|
|
6
|
+
* celld's cells run in WAL mode (header bytes 18–19 = 2); a restored image is switched to a
|
|
7
|
+
* rollback journal (1/1) so any driver can open it from memory; SQLite rewrites them when a
|
|
8
|
+
* runtime sets WAL again.
|
|
9
|
+
*/
|
|
10
|
+
export declare const toRollback: (image: Uint8Array) => Uint8Array<ArrayBufferLike>;
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/** Byte and name helpers shared by the celld bucket layout (celld-cells) and its LTX log. */
|
|
2
|
+
export const pad16 = (n) => n.toString(16).padStart(16, "0");
|
|
3
|
+
export const equal = (a, b) => {
|
|
4
|
+
if (a.length !== b.length)
|
|
5
|
+
return false;
|
|
6
|
+
for (let i = 0; i < a.length; i++)
|
|
7
|
+
if (a[i] !== b[i])
|
|
8
|
+
return false;
|
|
9
|
+
return true;
|
|
10
|
+
};
|
|
11
|
+
export const pageSizeOf = (image) => {
|
|
12
|
+
const v = (image[16] << 8) | image[17];
|
|
13
|
+
return v === 1 ? 65536 : v;
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* celld's cells run in WAL mode (header bytes 18–19 = 2); a restored image is switched to a
|
|
17
|
+
* rollback journal (1/1) so any driver can open it from memory; SQLite rewrites them when a
|
|
18
|
+
* runtime sets WAL again.
|
|
19
|
+
*/
|
|
20
|
+
export const toRollback = (image) => {
|
|
21
|
+
if (image.length > 19 && image[18] === 2) {
|
|
22
|
+
image[18] = 1;
|
|
23
|
+
image[19] = 1;
|
|
24
|
+
}
|
|
25
|
+
return image;
|
|
26
|
+
};
|
|
27
|
+
//# sourceMappingURL=celld-format.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"celld-format.js","sourceRoot":"","sources":["../../src/bucket/celld-format.ts"],"names":[],"mappings":"AAAA,6FAA6F;AAC7F,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAS,EAAE,EAAE,CAAC,CAAC,CAAC,QAAQ,CAAC,EAAE,CAAC,CAAC,QAAQ,CAAC,EAAE,EAAE,GAAG,CAAC,CAAC;AACrE,MAAM,CAAC,MAAM,KAAK,GAAG,CAAC,CAAa,EAAE,CAAa,EAAE,EAAE;IACpD,IAAI,CAAC,CAAC,MAAM,KAAK,CAAC,CAAC,MAAM;QAAE,OAAO,KAAK,CAAC;IACxC,KAAK,IAAI,CAAC,GAAG,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC,MAAM,EAAE,CAAC,EAAE;QAAE,IAAI,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC;YAAE,OAAO,KAAK,CAAC;IACnE,OAAO,IAAI,CAAC;AACd,CAAC,CAAC;AACF,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAiB,EAAE,EAAE;IAC9C,MAAM,CAAC,GAAG,CAAC,KAAK,CAAC,EAAE,CAAC,IAAI,CAAC,CAAC,GAAG,KAAK,CAAC,EAAE,CAAC,CAAC;IACvC,OAAO,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAC,CAAC,CAAC;AAC7B,CAAC,CAAC;AACF;;;;GAIG;AACH,MAAM,CAAC,MAAM,UAAU,GAAG,CAAC,KAAiB,EAAE,EAAE;IAC9C,IAAI,KAAK,CAAC,MAAM,GAAG,EAAE,IAAI,KAAK,CAAC,EAAE,CAAC,KAAK,CAAC,EAAE,CAAC;QACzC,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;QACd,KAAK,CAAC,EAAE,CAAC,GAAG,CAAC,CAAC;IAChB,CAAC;IACD,OAAO,KAAK,CAAC;AACf,CAAC,CAAC","sourcesContent":["/** Byte and name helpers shared by the celld bucket layout (celld-cells) and its LTX log. */\nexport const pad16 = (n: bigint) => n.toString(16).padStart(16, \"0\");\nexport const equal = (a: Uint8Array, b: Uint8Array) => {\n if (a.length !== b.length) return false;\n for (let i = 0; i < a.length; i++) if (a[i] !== b[i]) return false;\n return true;\n};\nexport const pageSizeOf = (image: Uint8Array) => {\n const v = (image[16] << 8) | image[17];\n return v === 1 ? 65536 : v;\n};\n/**\n * celld's cells run in WAL mode (header bytes 18–19 = 2); a restored image is switched to a\n * rollback journal (1/1) so any driver can open it from memory; SQLite rewrites them when a\n * runtime sets WAL again.\n */\nexport const toRollback = (image: Uint8Array) => {\n if (image.length > 19 && image[18] === 2) {\n image[18] = 1;\n image[19] = 1;\n }\n return image;\n};\n"]}
|
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bucket in IndexedDB: an ObjectStore whose conditional writes are atomic across every tab
|
|
3
|
+
* and worker of the origin (each put checks its condition and writes in one readwrite
|
|
4
|
+
* transaction, and IndexedDB serializes overlapping readwrite transactions). No synchronous
|
|
5
|
+
* file handle is involved, so any number of workers can use it at once: with in-memory SQLite
|
|
6
|
+
* per cell and the bucket's page-level logs (LTX), cells are not bound to one OPFS pool.
|
|
7
|
+
*/
|
|
8
|
+
import { type ObjectStore } from "./object-store.js";
|
|
9
|
+
export declare function idbObjectStore(name: string): ObjectStore;
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* A bucket in IndexedDB: an ObjectStore whose conditional writes are atomic across every tab
|
|
3
|
+
* and worker of the origin (each put checks its condition and writes in one readwrite
|
|
4
|
+
* transaction, and IndexedDB serializes overlapping readwrite transactions). No synchronous
|
|
5
|
+
* file handle is involved, so any number of workers can use it at once: with in-memory SQLite
|
|
6
|
+
* per cell and the bucket's page-level logs (LTX), cells are not bound to one OPFS pool.
|
|
7
|
+
*/
|
|
8
|
+
import { PreconditionFailedError } from "./object-store.js";
|
|
9
|
+
const STORE = "objects";
|
|
10
|
+
function open(name) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
const r = indexedDB.open(name, 1);
|
|
13
|
+
r.onupgradeneeded = () => r.result.createObjectStore(STORE);
|
|
14
|
+
r.onsuccess = () => resolve(r.result);
|
|
15
|
+
r.onerror = () => reject(r.error);
|
|
16
|
+
});
|
|
17
|
+
}
|
|
18
|
+
const done = (tx) => new Promise((resolve, reject) => {
|
|
19
|
+
tx.oncomplete = () => resolve();
|
|
20
|
+
tx.onerror = () => reject(tx.error);
|
|
21
|
+
tx.onabort = () => reject(tx.error ?? new Error("IndexedDB transaction aborted"));
|
|
22
|
+
});
|
|
23
|
+
const req = (r) => new Promise((resolve, reject) => {
|
|
24
|
+
r.onsuccess = () => resolve(r.result);
|
|
25
|
+
r.onerror = () => reject(r.error);
|
|
26
|
+
});
|
|
27
|
+
export function idbObjectStore(name) {
|
|
28
|
+
let db;
|
|
29
|
+
const conn = () => (db ??= open(name));
|
|
30
|
+
return {
|
|
31
|
+
async get(key) {
|
|
32
|
+
const tx = (await conn()).transaction(STORE, "readonly");
|
|
33
|
+
return (await req(tx.objectStore(STORE).get(key)));
|
|
34
|
+
},
|
|
35
|
+
async put(key, body, cond, attrs) {
|
|
36
|
+
const tx = (await conn()).transaction(STORE, "readwrite");
|
|
37
|
+
const store = tx.objectStore(STORE);
|
|
38
|
+
const etag = crypto.randomUUID();
|
|
39
|
+
let failed = false;
|
|
40
|
+
// Condition and write in the same transaction: atomic for the whole origin.
|
|
41
|
+
store.get(key).onsuccess = (e) => {
|
|
42
|
+
const cur = e.target.result;
|
|
43
|
+
if ((cond?.ifNoneMatch === "*" && cur) ||
|
|
44
|
+
(cond?.ifMatch !== undefined && cur?.etag !== cond.ifMatch)) {
|
|
45
|
+
failed = true;
|
|
46
|
+
tx.abort();
|
|
47
|
+
return;
|
|
48
|
+
}
|
|
49
|
+
store.put({ body: body.slice(), etag, attrs, modifiedMs: Date.now() }, key);
|
|
50
|
+
};
|
|
51
|
+
try {
|
|
52
|
+
await done(tx);
|
|
53
|
+
}
|
|
54
|
+
catch (err) {
|
|
55
|
+
if (failed)
|
|
56
|
+
throw new PreconditionFailedError(key);
|
|
57
|
+
throw err;
|
|
58
|
+
}
|
|
59
|
+
return etag;
|
|
60
|
+
},
|
|
61
|
+
async list(prefix) {
|
|
62
|
+
const tx = (await conn()).transaction(STORE, "readonly");
|
|
63
|
+
const range = IDBKeyRange.bound(prefix, `${prefix}\uffff`, false, false);
|
|
64
|
+
return (await req(tx.objectStore(STORE).getAllKeys(range))).sort();
|
|
65
|
+
},
|
|
66
|
+
async delete(key) {
|
|
67
|
+
const tx = (await conn()).transaction(STORE, "readwrite");
|
|
68
|
+
tx.objectStore(STORE).delete(key);
|
|
69
|
+
await done(tx);
|
|
70
|
+
},
|
|
71
|
+
};
|
|
72
|
+
}
|
|
73
|
+
//# sourceMappingURL=idb-object-store.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"idb-object-store.js","sourceRoot":"","sources":["../../src/bucket/idb-object-store.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * A bucket in IndexedDB: an ObjectStore whose conditional writes are atomic across every tab\n * and worker of the origin (each put checks its condition and writes in one readwrite\n * transaction, and IndexedDB serializes overlapping readwrite transactions). No synchronous\n * file handle is involved, so any number of workers can use it at once: with in-memory SQLite\n * per cell and the bucket's page-level logs (LTX), cells are not bound to one OPFS pool.\n */\nimport { type ObjectStore, PreconditionFailedError, type StoredObject } from \"./object-store.js\";\n\nconst STORE = \"objects\";\n\nfunction open(name: string): Promise<IDBDatabase> {\n return new Promise((resolve, reject) => {\n const r = indexedDB.open(name, 1);\n r.onupgradeneeded = () => r.result.createObjectStore(STORE);\n r.onsuccess = () => resolve(r.result);\n r.onerror = () => reject(r.error);\n });\n}\n\nconst done = (tx: IDBTransaction) =>\n new Promise<void>((resolve, reject) => {\n tx.oncomplete = () => resolve();\n tx.onerror = () => reject(tx.error);\n tx.onabort = () => reject(tx.error ?? new Error(\"IndexedDB transaction aborted\"));\n });\nconst req = <T>(r: IDBRequest<T>) =>\n new Promise<T>((resolve, reject) => {\n r.onsuccess = () => resolve(r.result);\n r.onerror = () => reject(r.error);\n });\n\nexport function idbObjectStore(name: string): ObjectStore {\n let db: Promise<IDBDatabase> | undefined;\n const conn = () => (db ??= open(name));\n return {\n async get(key) {\n const tx = (await conn()).transaction(STORE, \"readonly\");\n return (await req(tx.objectStore(STORE).get(key))) as StoredObject | undefined;\n },\n async put(key, body, cond, attrs) {\n const tx = (await conn()).transaction(STORE, \"readwrite\");\n const store = tx.objectStore(STORE);\n const etag = crypto.randomUUID();\n let failed = false;\n // Condition and write in the same transaction: atomic for the whole origin.\n store.get(key).onsuccess = (e) => {\n const cur = (e.target as IDBRequest<StoredObject | undefined>).result;\n if (\n (cond?.ifNoneMatch === \"*\" && cur) ||\n (cond?.ifMatch !== undefined && cur?.etag !== cond.ifMatch)\n ) {\n failed = true;\n tx.abort();\n return;\n }\n store.put(\n { body: body.slice(), etag, attrs, modifiedMs: Date.now() } satisfies StoredObject,\n key,\n );\n };\n try {\n await done(tx);\n } catch (err) {\n if (failed) throw new PreconditionFailedError(key);\n throw err;\n }\n return etag;\n },\n async list(prefix) {\n const tx = (await conn()).transaction(STORE, \"readonly\");\n const range = IDBKeyRange.bound(prefix, `${prefix}\\uffff`, false, false);\n return ((await req(tx.objectStore(STORE).getAllKeys(range))) as string[]).sort();\n },\n async delete(key) {\n const tx = (await conn()).transaction(STORE, \"readwrite\");\n tx.objectStore(STORE).delete(key);\n await done(tx);\n },\n };\n}\n"]}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { CellBackup, CellDurability } from "../ports.js";
|
|
2
|
+
import type { ObjectStore } from "./object-store.js";
|
|
3
|
+
/**
|
|
4
|
+
* THE LTX writer of cellb, in celld's bucket layout, on any ObjectStore:
|
|
5
|
+
*
|
|
6
|
+
* cells/<Class>:<idHex>/ltx/e<epoch>/<level>/<min>-<max>.ltx LTX v3 (src/ltx)
|
|
7
|
+
* wake/entries/…, wake/retired/… celld's alarm wake index
|
|
8
|
+
*
|
|
9
|
+
* Each committed transaction becomes one LTX file in the cell's epoch — the first one of an
|
|
10
|
+
* epoch a snapshot (TXID 1), then only the changed pages (from the SQLite WASM page hook, or a
|
|
11
|
+
* diff of the image) — and the output gate waits for the PUT; a cell's commits are written
|
|
12
|
+
* strictly in order. Restore: the newest epoch whose files form a chain from a snapshot.
|
|
13
|
+
* `compact` makes save() replace an epoch's files with one snapshot file 1..N.
|
|
14
|
+
*
|
|
15
|
+
* Used by celldBucket (with celld's ownership protocol: begin()/forget() per acquisition) and by
|
|
16
|
+
* filesApiLtxLog (a FilesApi, a fixed epoch).
|
|
17
|
+
*/
|
|
18
|
+
export interface LtxLogOptions {
|
|
19
|
+
now?: () => number;
|
|
20
|
+
/** Epoch used when a commit names none (no ownership). */
|
|
21
|
+
defaultEpoch?: number;
|
|
22
|
+
/** save() compacts an epoch into one snapshot file 1..N. */
|
|
23
|
+
compact?: boolean;
|
|
24
|
+
/** Checked before each write: false refuses it (this node's authority has lapsed). */
|
|
25
|
+
authorized?: () => boolean;
|
|
26
|
+
}
|
|
27
|
+
export declare function ltxLog(store: ObjectStore, o?: LtxLogOptions): CellDurability & CellBackup & {
|
|
28
|
+
logCommit: CellDurability["committed"];
|
|
29
|
+
} & {
|
|
30
|
+
/** A new owner epoch for the cell: its next commit opens the generation with a snapshot. */
|
|
31
|
+
begin(cellKey: string, epoch: number): void;
|
|
32
|
+
/** The cell is no longer ours (lease lost): forget its writer state. */
|
|
33
|
+
forget(cellKey: string): void;
|
|
34
|
+
};
|