@optimystic/db-p2p 0.24.2 → 0.25.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/{readme.md → README.md} +688 -668
- package/dist/src/cluster/block-transfer-service.d.ts +133 -10
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +155 -9
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +67 -7
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +127 -24
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/certified-claims.d.ts +170 -0
- package/dist/src/cluster/certified-claims.d.ts.map +1 -0
- package/dist/src/cluster/certified-claims.js +200 -0
- package/dist/src/cluster/certified-claims.js.map +1 -0
- package/dist/src/cluster/client-signature-verifier.d.ts +23 -0
- package/dist/src/cluster/client-signature-verifier.d.ts.map +1 -0
- package/dist/src/cluster/client-signature-verifier.js +33 -0
- package/dist/src/cluster/client-signature-verifier.js.map +1 -0
- package/dist/src/cluster/cluster-policy.d.ts +9 -3
- package/dist/src/cluster/cluster-policy.d.ts.map +1 -1
- package/dist/src/cluster/cluster-policy.js +111 -27
- package/dist/src/cluster/cluster-policy.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +140 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +485 -29
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-proof.d.ts +134 -0
- package/dist/src/cluster/commit-proof.d.ts.map +1 -0
- package/dist/src/cluster/commit-proof.js +263 -0
- package/dist/src/cluster/commit-proof.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +95 -8
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -1
- package/dist/src/cluster/quorum-restore.js +113 -14
- package/dist/src/cluster/quorum-restore.js.map +1 -1
- package/dist/src/cluster/rebalance-monitor.d.ts +84 -0
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +207 -5
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/reconcile-block.d.ts +27 -7
- package/dist/src/cluster/reconcile-block.d.ts.map +1 -1
- package/dist/src/cluster/reconcile-block.js +179 -38
- package/dist/src/cluster/reconcile-block.js.map +1 -1
- package/dist/src/cluster/service.d.ts +13 -4
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +17 -18
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +8 -2
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +7 -7
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +8 -7
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/cohort-topic/stream-util.d.ts +7 -6
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -1
- package/dist/src/cohort-topic/stream-util.js +14 -41
- package/dist/src/cohort-topic/stream-util.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -8
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +24 -7
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +5 -0
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +13 -3
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +33 -15
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +127 -34
- package/dist/src/dispute/invalidation.js.map +1 -1
- package/dist/src/dispute/service.d.ts.map +1 -1
- package/dist/src/dispute/service.js +2 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +7 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +14 -10
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +28 -58
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +38 -7
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +148 -63
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/network/open-protocol-stream.d.ts +56 -0
- package/dist/src/network/open-protocol-stream.d.ts.map +1 -0
- package/dist/src/network/open-protocol-stream.js +60 -0
- package/dist/src/network/open-protocol-stream.js.map +1 -0
- package/dist/src/network/register-protocol-handler.d.ts +66 -0
- package/dist/src/network/register-protocol-handler.d.ts.map +1 -0
- package/dist/src/network/register-protocol-handler.js +51 -0
- package/dist/src/network/register-protocol-handler.js.map +1 -0
- package/dist/src/peer-address-book.d.ts +60 -2
- package/dist/src/peer-address-book.d.ts.map +1 -1
- package/dist/src/peer-address-book.js +89 -1
- package/dist/src/peer-address-book.js.map +1 -1
- package/dist/src/pend-validation.d.ts +65 -0
- package/dist/src/pend-validation.d.ts.map +1 -0
- package/dist/src/pend-validation.js +69 -0
- package/dist/src/pend-validation.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -1
- package/dist/src/reactivity/notify-transport.js +2 -1
- package/dist/src/reactivity/notify-transport.js.map +1 -1
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -1
- package/dist/src/reactivity/push-state-gossip.js +2 -1
- package/dist/src/reactivity/push-state-gossip.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +32 -6
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +44 -10
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +181 -25
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +650 -62
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/served-repo-proxy.d.ts +30 -0
- package/dist/src/repo/served-repo-proxy.d.ts.map +1 -0
- package/dist/src/repo/served-repo-proxy.js +50 -0
- package/dist/src/repo/served-repo-proxy.js.map +1 -0
- package/dist/src/repo/service.d.ts +16 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +18 -16
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/types.d.ts +3 -1
- package/dist/src/reputation/types.d.ts.map +1 -1
- package/dist/src/reputation/types.js +3 -1
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +24 -2
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +33 -4
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/block-archive.d.ts +153 -0
- package/dist/src/storage/block-archive.d.ts.map +1 -0
- package/dist/src/storage/block-archive.js +194 -0
- package/dist/src/storage/block-archive.js.map +1 -0
- package/dist/src/storage/block-latch.d.ts +92 -0
- package/dist/src/storage/block-latch.d.ts.map +1 -0
- package/dist/src/storage/block-latch.js +132 -0
- package/dist/src/storage/block-latch.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +169 -12
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +415 -152
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/cached-raw-storage.d.ts +5 -1
- package/dist/src/storage/cached-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/cached-raw-storage.js +16 -1
- package/dist/src/storage/cached-raw-storage.js.map +1 -1
- package/dist/src/storage/cached-store-driver.d.ts +14 -4
- package/dist/src/storage/cached-store-driver.d.ts.map +1 -1
- package/dist/src/storage/cached-store-driver.js +39 -5
- package/dist/src/storage/cached-store-driver.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +70 -19
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-block-storage.js +21 -1
- package/dist/src/storage/i-block-storage.js.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +38 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +14 -8
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.js +29 -9
- package/dist/src/storage/kv-raw-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +4 -1
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -1
- package/dist/src/storage/memory-store-driver.js +22 -2
- package/dist/src/storage/memory-store-driver.js.map +1 -1
- package/dist/src/storage/raw-store-codec.js +2 -2
- package/dist/src/storage/raw-store-driver.d.ts +44 -3
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.d.ts +60 -3
- package/dist/src/storage/shared-cache-pool.d.ts.map +1 -1
- package/dist/src/storage/shared-cache-pool.js +68 -4
- package/dist/src/storage/shared-cache-pool.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +204 -26
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +422 -119
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/storage/store-identity.d.ts +21 -0
- package/dist/src/storage/store-identity.d.ts.map +1 -0
- package/dist/src/storage/store-identity.js +33 -0
- package/dist/src/storage/store-identity.js.map +1 -0
- package/dist/src/storage/struct.d.ts +15 -0
- package/dist/src/storage/struct.d.ts.map +1 -1
- package/dist/src/storage/with-read-cache.d.ts +92 -0
- package/dist/src/storage/with-read-cache.d.ts.map +1 -0
- package/dist/src/storage/with-read-cache.js +132 -0
- package/dist/src/storage/with-read-cache.js.map +1 -0
- package/dist/src/sync/service.d.ts +13 -3
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +7 -30
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts +75 -2
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +193 -81
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +8 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -1
- package/dist/src/testing/raw-storage-conformance.js +97 -2
- package/dist/src/testing/raw-storage-conformance.js.map +1 -1
- package/package.json +5 -6
- package/src/cluster/block-transfer-service.ts +528 -312
- package/src/cluster/block-transfer.ts +547 -424
- package/src/cluster/certified-claims.ts +319 -0
- package/src/cluster/client-signature-verifier.ts +34 -0
- package/src/cluster/cluster-policy.ts +294 -203
- package/src/cluster/cluster-repo.ts +2501 -2034
- package/src/cluster/commit-proof.ts +344 -0
- package/src/cluster/quorum-restore.ts +348 -223
- package/src/cluster/rebalance-monitor.ts +539 -254
- package/src/cluster/reconcile-block.ts +404 -203
- package/src/cluster/service.ts +23 -20
- package/src/cluster/spread-on-churn.ts +352 -346
- package/src/cohort-topic/host.ts +8 -7
- package/src/cohort-topic/stream-util.ts +14 -43
- package/src/dispute/cascade.ts +31 -15
- package/src/dispute/dispute-service.ts +706 -696
- package/src/dispute/invalidation.ts +162 -52
- package/src/dispute/service.ts +126 -125
- package/src/index.ts +7 -0
- package/src/libp2p-key-network.ts +1204 -1235
- package/src/libp2p-node-base.ts +185 -70
- package/src/network/open-protocol-stream.ts +87 -0
- package/src/network/register-protocol-handler.ts +85 -0
- package/src/peer-address-book.ts +96 -2
- package/src/pend-validation.ts +89 -0
- package/src/reactivity/notify-transport.ts +2 -1
- package/src/reactivity/push-state-gossip.ts +2 -1
- package/src/repo/cluster-coordinator.ts +65 -11
- package/src/repo/coordinator-repo.ts +729 -71
- package/src/repo/served-repo-proxy.ts +55 -0
- package/src/repo/service.ts +25 -16
- package/src/reputation/types.ts +3 -1
- package/src/rn.ts +34 -4
- package/src/storage/block-archive.ts +242 -0
- package/src/storage/block-latch.ts +144 -0
- package/src/storage/block-storage.ts +787 -498
- package/src/storage/cached-raw-storage.ts +21 -1
- package/src/storage/cached-store-driver.ts +898 -859
- package/src/storage/i-block-storage.ts +172 -116
- package/src/storage/i-raw-storage.ts +40 -0
- package/src/storage/kv-raw-storage.ts +34 -9
- package/src/storage/memory-store-driver.ts +26 -2
- package/src/storage/raw-store-codec.ts +42 -42
- package/src/storage/raw-store-driver.ts +45 -3
- package/src/storage/shared-cache-pool.ts +516 -452
- package/src/storage/storage-repo.ts +1421 -1014
- package/src/storage/store-identity.ts +41 -0
- package/src/storage/struct.ts +52 -34
- package/src/storage/with-read-cache.ts +182 -0
- package/src/sync/service.ts +178 -198
- package/src/testing/mesh-harness.ts +524 -344
- package/src/testing/raw-storage-conformance.ts +114 -2
|
@@ -1,116 +1,172 @@
|
|
|
1
|
-
import type { IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
*
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
*
|
|
43
|
-
*
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
54
|
-
*
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
|
|
66
|
-
|
|
67
|
-
/**
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
*
|
|
89
|
-
*
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
*
|
|
98
|
-
*
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
*
|
|
104
|
-
* the
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
1
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockCommitProof } from "../cluster/commit-proof.js";
|
|
3
|
+
import type { BlockWriteLatch } from "./block-latch.js";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Thrown by {@link IBlockStorage.getBlock} when the block has metadata here but the target revision
|
|
7
|
+
* lies outside the locally held revision ranges — this node holds no local records that can serve
|
|
8
|
+
* it. Not a fault: it is the signal a caller that is allowed to heal (only `StorageRepo.get`, under
|
|
9
|
+
* the block's write latch) turns into a {@link IBlockStorage.restoreRevision}; every other caller
|
|
10
|
+
* treats it like any other unreadable-base condition.
|
|
11
|
+
*
|
|
12
|
+
* It lives beside the interface rather than beside `BlockStorage` because it is part of the
|
|
13
|
+
* `getBlock` contract: an alternate `IBlockStorage` implementation must be able to throw it, and
|
|
14
|
+
* `StorageRepo` must be able to catch it, without either depending on the concrete implementation.
|
|
15
|
+
*/
|
|
16
|
+
export class RevisionNotCoveredError extends Error {
|
|
17
|
+
constructor(readonly blockId: BlockId, readonly rev: number) {
|
|
18
|
+
super(`Block ${blockId} revision ${rev} is not covered by local records`);
|
|
19
|
+
this.name = 'RevisionNotCoveredError';
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Interface for block-level storage operations.
|
|
25
|
+
*
|
|
26
|
+
* **One block, one write lock.** Every method that writes — metadata, revision records, action
|
|
27
|
+
* transforms, pending records, materializations, proofs — takes a {@link BlockWriteLatch} token as
|
|
28
|
+
* its LAST parameter. The token is proof the caller holds this block's write latch
|
|
29
|
+
* (`blockWriteLatchKey(blockId)`, see `block-latch.ts`), which is the only thing that keeps two
|
|
30
|
+
* writers' read-modify-writes of the metadata blob from silently undoing each other. Only
|
|
31
|
+
* `acquireBlockWriteLatch` / `withBlockWriteLatch` can mint a token, and an implementation must
|
|
32
|
+
* reject a token minted for a different block.
|
|
33
|
+
*/
|
|
34
|
+
export interface IBlockStorage {
|
|
35
|
+
/** Gets the latest revision information for this block */
|
|
36
|
+
getLatest(): Promise<ActionRev | undefined>;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Gets a materialized block at the given revision, from LOCAL records only — this never fetches
|
|
40
|
+
* from a peer. Returns `undefined` when this node has never seen the block (no metadata) or when
|
|
41
|
+
* it is pending-only (metadata seeded by a pending transaction, nothing committed) and no `rev`
|
|
42
|
+
* was named. Throws {@link RevisionNotCoveredError} when the target revision (`rev`, or `latest.rev`)
|
|
43
|
+
* lies outside `meta.ranges` — the caller decides whether to heal that gap with
|
|
44
|
+
* {@link restoreRevision} under the block's write latch (`StorageRepo.get` does; the commit path
|
|
45
|
+
* deliberately does not). Throws a plain `Error` when the revision is covered but cannot be
|
|
46
|
+
* materialized from the records held (truncated history — genuine corruption).
|
|
47
|
+
*/
|
|
48
|
+
getBlock(rev?: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined>;
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Fill a gap in local revision history by fetching `rev` from a peer (the restore wire) and
|
|
52
|
+
* recording the vetted coverage. No-op when `rev` is already covered. Throws when the block has
|
|
53
|
+
* no metadata here (a never-seen block is not restored at this layer — see the note in
|
|
54
|
+
* `BlockStorage.getBlock`) or when no peer could supply an acceptable archive.
|
|
55
|
+
*/
|
|
56
|
+
restoreRevision(rev: number, latch: BlockWriteLatch): Promise<void>;
|
|
57
|
+
|
|
58
|
+
/** Gets an action by ID */
|
|
59
|
+
getTransaction(actionId: ActionId): Promise<Transform | undefined>;
|
|
60
|
+
|
|
61
|
+
/** The commit proof stored for a revision, if one was retained (see IRawStorage.getBlockProof). */
|
|
62
|
+
getBlockProof(rev: number): Promise<BlockCommitProof | undefined>;
|
|
63
|
+
|
|
64
|
+
/** Persists the commit proof for a revision (see IRawStorage.saveBlockProof). */
|
|
65
|
+
saveBlockProof(rev: number, proof: BlockCommitProof, latch: BlockWriteLatch): Promise<void>;
|
|
66
|
+
|
|
67
|
+
/** Gets a pending action by ID */
|
|
68
|
+
getPendingTransaction(actionId: ActionId): Promise<Transform | undefined>;
|
|
69
|
+
|
|
70
|
+
/** Lists all pending action IDs */
|
|
71
|
+
listPendingTransactions(): AsyncIterable<ActionId>;
|
|
72
|
+
|
|
73
|
+
/** Saves a pending action (seeding this block's metadata when it has none). */
|
|
74
|
+
savePendingTransaction(actionId: ActionId, transform: Transform, latch: BlockWriteLatch): Promise<void>;
|
|
75
|
+
|
|
76
|
+
/** Deletes a pending action */
|
|
77
|
+
deletePendingTransaction(actionId: ActionId, latch: BlockWriteLatch): Promise<void>;
|
|
78
|
+
|
|
79
|
+
/** Lists revisions in ascending or descending order between startRev and endRev (inclusive) */
|
|
80
|
+
listRevisions(startRev: number, endRev: number): AsyncIterable<ActionRev>;
|
|
81
|
+
|
|
82
|
+
/** Saves a materialized block */
|
|
83
|
+
saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined, latch: BlockWriteLatch): Promise<void>;
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delete the materialized copy at `prior` if it is now redundant under the checkpoint
|
|
87
|
+
* retention policy (not the tip, not its range floor, not a checkpoint rev). The forward
|
|
88
|
+
* transform for `prior.rev` is retained, so the rev stays reconstructible by replay.
|
|
89
|
+
* No-op if `prior.rev` must be retained or has no materialization (e.g. a tombstone rev).
|
|
90
|
+
*/
|
|
91
|
+
pruneSupersededMaterialization(prior: ActionRev, latch: BlockWriteLatch): Promise<void>;
|
|
92
|
+
|
|
93
|
+
/** Saves a revision */
|
|
94
|
+
saveRevision(rev: number, actionId: ActionId, latch: BlockWriteLatch): Promise<void>;
|
|
95
|
+
|
|
96
|
+
/**
|
|
97
|
+
* Promotes a pending action to committed, MOVING the record from the pending namespace to the
|
|
98
|
+
* committed one in a single atomic step.
|
|
99
|
+
*
|
|
100
|
+
* **Invariant P** — a block never holds a pending record and a committed record for the same
|
|
101
|
+
* action id at the same time. This method maintains it on the commit path; every OTHER writer of
|
|
102
|
+
* a committed transform for a block ({@link saveReplica}, {@link saveDeletion}, and any forward
|
|
103
|
+
* path added later) must maintain it too, by deleting that action's pending record when it
|
|
104
|
+
* writes the committed one. A pending record left beside a committed one can never be promoted,
|
|
105
|
+
* and is reported as a phantom conflicting action by `StorageRepo.pend` on every later write to
|
|
106
|
+
* the block.
|
|
107
|
+
*/
|
|
108
|
+
promotePendingTransaction(actionId: ActionId, latch: BlockWriteLatch): Promise<void>;
|
|
109
|
+
|
|
110
|
+
/** Sets the latest revision information */
|
|
111
|
+
setLatest(latest: ActionRev, latch: BlockWriteLatch): Promise<void>;
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Persist a replica of a block received out-of-band (churn re-replication).
|
|
115
|
+
*
|
|
116
|
+
* Seeds metadata if absent, writes `rev → actionId`, the action transform, and the
|
|
117
|
+
* materialized block, merges `[rev, rev+1]` into `ranges`, and advances `latest`
|
|
118
|
+
* monotonically. When `source` is provided its `rev`/`actionId` are used; otherwise
|
|
119
|
+
* it falls back to `rev = 1` and a deterministic `actionId` derived from the block
|
|
120
|
+
* (so retries stay idempotent — never random).
|
|
121
|
+
*
|
|
122
|
+
* Maintains **Invariant P** (see {@link promotePendingTransaction}): writing the committed
|
|
123
|
+
* transform for `actionId` also deletes that action's pending record on this block, so a node
|
|
124
|
+
* that pended the action but diverged before committing it does not keep an unpromotable record.
|
|
125
|
+
* Only on the write path — the monotonic no-op below deletes nothing.
|
|
126
|
+
*
|
|
127
|
+
* No-op (still durable) when an equal-or-newer revision is already present: `latest`
|
|
128
|
+
* is never downgraded. Idempotent for a fixed `(rev, actionId)`. Returns the
|
|
129
|
+
* effective latest `ActionRev`.
|
|
130
|
+
*
|
|
131
|
+
* `proof` is persisted for `source.rev` and MUST already be verified by the caller against
|
|
132
|
+
* these exact bytes (`verifyBlockCommitProofContent` — the digest check is what binds a proof
|
|
133
|
+
* to the block content). An unverified proof passed here would be re-served onward as evidence
|
|
134
|
+
* this node never checked. The monotonic no-op persists nothing, proof included — a proof for an
|
|
135
|
+
* already-held revision is back-filled one layer up, by `StorageRepo.saveReplicatedBlock`, which
|
|
136
|
+
* first checks the declared digest against LOCAL content (these bytes may not be the held bytes).
|
|
137
|
+
*
|
|
138
|
+
* `source` and `proof` are positional-but-optional (pass `undefined` when absent) so the latch
|
|
139
|
+
* token can stay in the last position like every other writer.
|
|
140
|
+
*/
|
|
141
|
+
saveReplica(block: IBlock, source: ActionRev | undefined, proof: BlockCommitProof | undefined, latch: BlockWriteLatch): Promise<ActionRev>;
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Writes a forward TOMBSTONE revision that reverses a block creation: persists `rev → actionId`,
|
|
145
|
+
* a `{ delete: true }` transform, and NO materialized block, then merges `[rev, rev+1]` into
|
|
146
|
+
* `ranges` and advances `latest` monotonically. The reverse-apply path treats the absent
|
|
147
|
+
* materialization as a deletion, so a `getBlock()` after a tombstone reads back as *absent*
|
|
148
|
+
* (`undefined`) while a historical `getBlock(creationRev)` still materializes the created content.
|
|
149
|
+
*
|
|
150
|
+
* Maintains **Invariant P** (see {@link promotePendingTransaction}) on the write path, exactly as
|
|
151
|
+
* {@link saveReplica} does: the tombstone's `actionId` loses its pending record on this block.
|
|
152
|
+
*
|
|
153
|
+
* Idempotent for a fixed `(rev, actionId)`; never downgrades `latest` (a no-op — still durable —
|
|
154
|
+
* when an equal-or-newer revision is already present). Returns the effective latest `ActionRev`.
|
|
155
|
+
*/
|
|
156
|
+
saveDeletion(source: ActionRev, latch: BlockWriteLatch): Promise<ActionRev>;
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Reconciles `metadata.latest` with the highest contiguous fully-promoted revision in
|
|
160
|
+
* the revisions table. Intended for post-crash recovery of the Crash-D3 gap, where
|
|
161
|
+
* `promotePendingTransaction` succeeded but `setLatest` did not: the revision and
|
|
162
|
+
* committed-log entry are durable, but `meta.latest` still points at the prior rev
|
|
163
|
+
* (or is undefined), and retry-commit is rejected because the pending record is gone.
|
|
164
|
+
*
|
|
165
|
+
* Stops at the first rev whose action is not yet in the committed log, preserving the
|
|
166
|
+
* Crash-D2 invariant that retry-commit — not recovery — owns advancement past a half-
|
|
167
|
+
* promoted state.
|
|
168
|
+
*
|
|
169
|
+
* Idempotent and monotonic (latest only advances forward).
|
|
170
|
+
*/
|
|
171
|
+
recover(latch: BlockWriteLatch): Promise<{ reconciled: boolean; latest?: ActionRev }>;
|
|
172
|
+
}
|
|
@@ -1,5 +1,7 @@
|
|
|
1
1
|
import type { BlockId, ActionId, ActionRev, Transform, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockCommitProof } from "../cluster/commit-proof.js";
|
|
2
3
|
import type { BlockMetadata } from "./struct.js";
|
|
4
|
+
import type { StoreIdentity } from "./store-identity.js";
|
|
3
5
|
|
|
4
6
|
export interface IRawStorage {
|
|
5
7
|
// Metadata operations
|
|
@@ -21,6 +23,17 @@ export interface IRawStorage {
|
|
|
21
23
|
getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined>;
|
|
22
24
|
saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void>;
|
|
23
25
|
|
|
26
|
+
/**
|
|
27
|
+
* The commit proof stored for a revision, if one was retained. Keyed by revision: the proof lives
|
|
28
|
+
* and dies with the REVISION record, not the materialization, so the checkpoint sweep
|
|
29
|
+
* (`pruneSupersededMaterialization`) never touches it — that is what lets it outlive the
|
|
30
|
+
* 60-second in-memory commit-cert TTL. Whatever deletes a revision record must delete its proof;
|
|
31
|
+
* no such site exists today (`RawStoreDriver` has no revision delete at all — invalidations write
|
|
32
|
+
* compensating FORWARD revisions), so this is a contract note, not a wired path.
|
|
33
|
+
*/
|
|
34
|
+
getBlockProof(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined>;
|
|
35
|
+
saveBlockProof(blockId: BlockId, rev: number, proof: BlockCommitProof): Promise<void>;
|
|
36
|
+
|
|
24
37
|
// Block materialization operations
|
|
25
38
|
getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined>;
|
|
26
39
|
saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void>;
|
|
@@ -28,6 +41,33 @@ export interface IRawStorage {
|
|
|
28
41
|
// Promote a pending action to a committed action
|
|
29
42
|
promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void>;
|
|
30
43
|
|
|
44
|
+
/**
|
|
45
|
+
* A stable, process-scoped string naming what this storage is ultimately backed by (a
|
|
46
|
+
* resolved directory, an open database handle). Wrappers pass it through from whatever they
|
|
47
|
+
* wrap, so a cache and the storage it fronts name the same store — e.g.
|
|
48
|
+
* `new CachedRawStorage(new FileRawStorage(dir)).getStoreIdentity()` is `'file:<resolved dir>'`.
|
|
49
|
+
*
|
|
50
|
+
* Contract — note it is ONE-DIRECTIONAL; read both halves before consuming it:
|
|
51
|
+
* - Two storages over DIFFERENT locations MUST NOT return equal strings. So **equality
|
|
52
|
+
* proves sameness**, and a consumer may merge on it. (The fs backend documents one exotic
|
|
53
|
+
* Windows exception at its own site.)
|
|
54
|
+
* - Two storages over the SAME location SHOULD return equal strings, but this is
|
|
55
|
+
* best-effort and several backends knowingly under-approximate: two handles opened over
|
|
56
|
+
* one database, or two spellings of one directory that cannot be collapsed synchronously,
|
|
57
|
+
* read as two identities. So **inequality proves nothing** — never treat it as evidence
|
|
58
|
+
* that two storages are distinct stores. Each backend's `NOTE:` states its own gaps.
|
|
59
|
+
* - Every string is scheme-prefixed so backends cannot collide: `file:`, `sqlite-handle:`,
|
|
60
|
+
* `idb-handle:`, `leveldb-handle:`. Compared for equality only — never parsed.
|
|
61
|
+
* - Stable for the storage object's whole life; fixed at construction.
|
|
62
|
+
* - OPTIONAL BY DESIGN. A backend that cannot honour the contract omits the method entirely
|
|
63
|
+
* and callers fall back to per-object behavior. Never install a stub that returns
|
|
64
|
+
* `undefined` — feature-detection (`typeof storage.getStoreIdentity === 'function'`) must
|
|
65
|
+
* see the backend's true capability, the same trap `KvRawStorage`'s class doc calls out
|
|
66
|
+
* for `listBlockIds`.
|
|
67
|
+
* - It identifies the STORE, not its contents.
|
|
68
|
+
*/
|
|
69
|
+
getStoreIdentity?(): StoreIdentity;
|
|
70
|
+
|
|
31
71
|
/**
|
|
32
72
|
* Approximate bytes currently stored by this backend.
|
|
33
73
|
*
|
|
@@ -1,34 +1,41 @@
|
|
|
1
1
|
import type { BlockId, ActionId, ActionRev, Transform, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockCommitProof } from "../cluster/commit-proof.js";
|
|
2
3
|
import type { BlockMetadata } from "./struct.js";
|
|
3
4
|
import type { IRawStorage } from "./i-raw-storage.js";
|
|
4
5
|
import type { RawStoreDriver } from "./raw-store-driver.js";
|
|
6
|
+
import type { StoreIdentity } from "./store-identity.js";
|
|
5
7
|
import { encodeJson, decodeJson, encodeActionId, decodeActionId } from "./raw-store-codec.js";
|
|
6
8
|
|
|
7
9
|
/**
|
|
8
10
|
* Shared ordered-KV storage kernel. Implements the full {@link IRawStorage}
|
|
9
11
|
* surface over a {@link RawStoreDriver}, owning all value (de)serialization and
|
|
10
|
-
* call orchestration so each backend only has to expose its
|
|
12
|
+
* call orchestration so each backend only has to expose its six logical stores
|
|
11
13
|
* as bytes-valued maps over its native mechanism.
|
|
12
14
|
*
|
|
13
15
|
* The genuinely-shared logic lives ABOVE the storage primitive: the JSON/string
|
|
14
|
-
* codec for the
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* directory tree.
|
|
16
|
+
* codec for the value types, `listRevisions`' lo/hi/reverse bound computation,
|
|
17
|
+
* `saveMaterializedBlock`'s put-or-delete branch, and the passthroughs. Key
|
|
18
|
+
* layout / storage topology stays in the driver, because the backends do NOT
|
|
19
|
+
* share one — LevelDB is a single ordered byte keyspace, SQLite six relational
|
|
20
|
+
* tables, IndexedDB six object stores, the filesystem a directory tree.
|
|
20
21
|
*/
|
|
21
22
|
export class KvRawStorage implements IRawStorage {
|
|
22
23
|
/**
|
|
23
24
|
* Optional passthroughs are wired in the constructor ONLY when the driver
|
|
24
25
|
* provides them, so a `StorageMonitor` / owned-block seed that feature-detects
|
|
25
26
|
* (`typeof storage.listBlockIds === 'function'`) sees the driver's true
|
|
26
|
-
* capability instead of a stub that silently reports 0 / no seed.
|
|
27
|
+
* capability instead of a stub that silently reports 0 / no seed. The same rule
|
|
28
|
+
* governs `getStoreIdentity`: a stub returning `undefined` would make every store
|
|
29
|
+
* look identity-less to consumers that dedupe on it.
|
|
27
30
|
*/
|
|
28
31
|
listBlockIds?: () => AsyncIterable<BlockId>;
|
|
29
32
|
getApproximateBytesUsed?: () => Promise<number>;
|
|
33
|
+
getStoreIdentity?: () => StoreIdentity;
|
|
30
34
|
|
|
31
35
|
constructor(private readonly driver: RawStoreDriver) {
|
|
36
|
+
if (driver.storeIdentity) {
|
|
37
|
+
this.getStoreIdentity = () => driver.storeIdentity!();
|
|
38
|
+
}
|
|
32
39
|
if (driver.listBlockIds) {
|
|
33
40
|
this.listBlockIds = () => driver.listBlockIds!();
|
|
34
41
|
}
|
|
@@ -46,7 +53,7 @@ export class KvRawStorage implements IRawStorage {
|
|
|
46
53
|
|
|
47
54
|
// NOTE: every value write funnels through the driver put/delete calls in the
|
|
48
55
|
// methods below (saveMetadata / saveRevision / save*Transaction /
|
|
49
|
-
// saveMaterializedBlock). This is the single choke point where an incremental
|
|
56
|
+
// saveBlockProof / saveMaterializedBlock). This is the single choke point where an incremental
|
|
50
57
|
// byte counter would hook in, replacing the per-driver full-scan
|
|
51
58
|
// getApproximateBytesUsed. The chosen capacity-estimate mechanism is instead a
|
|
52
59
|
// short-TTL cache over the full scan in StorageMonitor (see storage-monitor.ts
|
|
@@ -111,6 +118,24 @@ export class KvRawStorage implements IRawStorage {
|
|
|
111
118
|
await this.driver.putTransaction(blockId, actionId, encodeJson(transform));
|
|
112
119
|
}
|
|
113
120
|
|
|
121
|
+
// --- Commit proofs (their OWN (blockId, rev)-keyed store — see RawStoreDriver.getProof) ---
|
|
122
|
+
//
|
|
123
|
+
// Proofs deliberately do NOT share the transactions keyspace. Action ids are
|
|
124
|
+
// chosen by whoever originates a write and are never re-derived or format-checked
|
|
125
|
+
// by the storing node, so no reserved action-id prefix can be relied on: a client
|
|
126
|
+
// pending an action literally named `~proof:5`, or a peer's restore archive naming
|
|
127
|
+
// one, used to land on the same key as revision 5's proof and silently overwrite it
|
|
128
|
+
// (or be overwritten by it). Separate stores make that collision unrepresentable.
|
|
129
|
+
|
|
130
|
+
async getBlockProof(blockId: BlockId, rev: number): Promise<BlockCommitProof | undefined> {
|
|
131
|
+
const bytes = await this.driver.getProof(blockId, rev);
|
|
132
|
+
return bytes === undefined ? undefined : decodeJson<BlockCommitProof>(bytes);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
async saveBlockProof(blockId: BlockId, rev: number, proof: BlockCommitProof): Promise<void> {
|
|
136
|
+
await this.driver.putProof(blockId, rev, encodeJson(proof));
|
|
137
|
+
}
|
|
138
|
+
|
|
114
139
|
// --- Materialized blocks ---
|
|
115
140
|
|
|
116
141
|
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
@@ -2,7 +2,7 @@ import type { BlockId, ActionId } from "@optimystic/db-core";
|
|
|
2
2
|
import type { RawStoreDriver } from "./raw-store-driver.js";
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* In-memory {@link RawStoreDriver}: the
|
|
5
|
+
* In-memory {@link RawStoreDriver}: the six logical block-storage stores as
|
|
6
6
|
* `Map`s of `Uint8Array` values. `KvRawStorage` hands this driver bytes produced
|
|
7
7
|
* by `JSON`-encode and reads them back via `JSON`-decode, so every get yields a
|
|
8
8
|
* fresh object and every save stored an independent byte snapshot BY
|
|
@@ -16,6 +16,7 @@ export class MemoryStoreDriver implements RawStoreDriver {
|
|
|
16
16
|
private readonly revisions = new Map<string, Uint8Array>(); // `${blockId}:${rev}` -> actionId bytes
|
|
17
17
|
private readonly pending = new Map<string, Uint8Array>(); // `${blockId}:${actionId}` -> transform bytes
|
|
18
18
|
private readonly transactions = new Map<string, Uint8Array>();
|
|
19
|
+
private readonly proofs = new Map<string, Uint8Array>(); // `${blockId}:${rev}` -> proof bytes
|
|
19
20
|
private readonly materialized = new Map<string, Uint8Array>();
|
|
20
21
|
|
|
21
22
|
private revisionKey(blockId: BlockId, rev: number): string {
|
|
@@ -104,6 +105,16 @@ export class MemoryStoreDriver implements RawStoreDriver {
|
|
|
104
105
|
this.transactions.set(this.actionKey(blockId, actionId), value);
|
|
105
106
|
}
|
|
106
107
|
|
|
108
|
+
// --- proofs (keyed like revisions, NOT like actions — see RawStoreDriver.getProof) ---
|
|
109
|
+
|
|
110
|
+
async getProof(blockId: BlockId, rev: number): Promise<Uint8Array | undefined> {
|
|
111
|
+
return this.proofs.get(this.revisionKey(blockId, rev));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async putProof(blockId: BlockId, rev: number, value: Uint8Array): Promise<void> {
|
|
115
|
+
this.proofs.set(this.revisionKey(blockId, rev), value);
|
|
116
|
+
}
|
|
117
|
+
|
|
107
118
|
// --- materialized ---
|
|
108
119
|
|
|
109
120
|
async getMaterialized(blockId: BlockId, actionId: ActionId): Promise<Uint8Array | undefined> {
|
|
@@ -132,6 +143,19 @@ export class MemoryStoreDriver implements RawStoreDriver {
|
|
|
132
143
|
|
|
133
144
|
// --- optional passthroughs ---
|
|
134
145
|
|
|
146
|
+
// Deliberately NO storeIdentity(): two MemoryStoreDrivers hold two independent Maps and are
|
|
147
|
+
// genuinely two different stores, so there is no shared "location" for them to name.
|
|
148
|
+
//
|
|
149
|
+
// NOTE: one driver object shared by two `KvRawStorage` wrappers is NOT covered. `withReadCache`
|
|
150
|
+
// falls back to keying on the STORAGE object, not the driver, so those two wrappers are two
|
|
151
|
+
// keys and get two read caches over one Map — the divergence dedupe exists to remove. Harmless
|
|
152
|
+
// today: this driver is a test fixture and every shipping backend's driver reports an identity,
|
|
153
|
+
// so the identity-less path only ever sees genuinely distinct stores. If a host ever wires
|
|
154
|
+
// `rawStorageFactory: () => new KvRawStorage(oneSharedDriver)` for two consumers, give this
|
|
155
|
+
// driver `identityForHandle('memory', this)` (store-identity.ts) — that makes the two wrappers
|
|
156
|
+
// name one store — and re-premise `local-transactor-read-cache.spec.ts`, which currently
|
|
157
|
+
// asserts the non-dedupe.
|
|
158
|
+
|
|
135
159
|
async *listBlockIds(): AsyncIterable<BlockId> {
|
|
136
160
|
// Snapshot the keys before yielding so a concurrent putMetadata during the scan
|
|
137
161
|
// doesn't invalidate a live map iterator. Fresh in-memory storage is empty, so at
|
|
@@ -147,7 +171,7 @@ export class MemoryStoreDriver implements RawStoreDriver {
|
|
|
147
171
|
for (const [blockId, value] of this.metadata) {
|
|
148
172
|
total += blockId.length + value.byteLength;
|
|
149
173
|
}
|
|
150
|
-
for (const store of [this.revisions, this.pending, this.transactions, this.materialized]) {
|
|
174
|
+
for (const store of [this.revisions, this.pending, this.transactions, this.proofs, this.materialized]) {
|
|
151
175
|
for (const [key, value] of store) {
|
|
152
176
|
total += key.length + value.byteLength;
|
|
153
177
|
}
|
|
@@ -1,42 +1,42 @@
|
|
|
1
|
-
import type { ActionId } from "@optimystic/db-core";
|
|
2
|
-
|
|
3
|
-
/**
|
|
4
|
-
* Value codec shared by every `KvRawStorage`-backed store. Values cross the
|
|
5
|
-
* `RawStoreDriver` boundary as `Uint8Array`, so this module is the single place
|
|
6
|
-
* that turns `BlockMetadata`/`Transform`/`IBlock` (via JSON)
|
|
7
|
-
* string (via UTF-8) into bytes and back.
|
|
8
|
-
*
|
|
9
|
-
* Because a get always decodes fresh bytes into a NEW object and a save always
|
|
10
|
-
* encodes an independent byte copy, the clone-on-store / clone-on-read invariant
|
|
11
|
-
* that in-memory storage used to enforce by hand (`structuredClone`) is now
|
|
12
|
-
* structural for kernel-backed stores — see docs/internals.md "Storage Returns
|
|
13
|
-
* References".
|
|
14
|
-
*
|
|
15
|
-
* Round-trip fidelity matters for `BlockMetadata`: an open-ended `RevisionRange`
|
|
16
|
-
* is encoded `[E]` (one element, upper bound `undefined`). `JSON.stringify([5])`
|
|
17
|
-
* → `"[5]"` → `JSON.parse` → `[5]`, so the open-ended encoding survives byte-exact.
|
|
18
|
-
* Do NOT normalize ranges here.
|
|
19
|
-
*/
|
|
20
|
-
|
|
21
|
-
const encoder = new TextEncoder();
|
|
22
|
-
const decoder = new TextDecoder();
|
|
23
|
-
|
|
24
|
-
/** JSON-encode a value (`BlockMetadata` / `Transform` / `IBlock`) to UTF-8 bytes. */
|
|
25
|
-
export function encodeJson<T>(value: T): Uint8Array {
|
|
26
|
-
return encoder.encode(JSON.stringify(value));
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
/** Decode UTF-8 JSON bytes back into a value. Callers guard the miss (`undefined`) first. */
|
|
30
|
-
export function decodeJson<T>(bytes: Uint8Array): T {
|
|
31
|
-
return JSON.parse(decoder.decode(bytes)) as T;
|
|
32
|
-
}
|
|
33
|
-
|
|
34
|
-
/** Encode an `ActionId` string to UTF-8 bytes (stored as the revisions-store value). */
|
|
35
|
-
export function encodeActionId(actionId: ActionId): Uint8Array {
|
|
36
|
-
return encoder.encode(actionId);
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/** Decode UTF-8 bytes back into an `ActionId` string. */
|
|
40
|
-
export function decodeActionId(bytes: Uint8Array): ActionId {
|
|
41
|
-
return decoder.decode(bytes) as ActionId;
|
|
42
|
-
}
|
|
1
|
+
import type { ActionId } from "@optimystic/db-core";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Value codec shared by every `KvRawStorage`-backed store. Values cross the
|
|
5
|
+
* `RawStoreDriver` boundary as `Uint8Array`, so this module is the single place
|
|
6
|
+
* that turns `BlockMetadata`/`Transform`/`IBlock`/`BlockCommitProof` (via JSON)
|
|
7
|
+
* and the `ActionId` string (via UTF-8) into bytes and back.
|
|
8
|
+
*
|
|
9
|
+
* Because a get always decodes fresh bytes into a NEW object and a save always
|
|
10
|
+
* encodes an independent byte copy, the clone-on-store / clone-on-read invariant
|
|
11
|
+
* that in-memory storage used to enforce by hand (`structuredClone`) is now
|
|
12
|
+
* structural for kernel-backed stores — see docs/internals.md "Storage Returns
|
|
13
|
+
* References".
|
|
14
|
+
*
|
|
15
|
+
* Round-trip fidelity matters for `BlockMetadata`: an open-ended `RevisionRange`
|
|
16
|
+
* is encoded `[E]` (one element, upper bound `undefined`). `JSON.stringify([5])`
|
|
17
|
+
* → `"[5]"` → `JSON.parse` → `[5]`, so the open-ended encoding survives byte-exact.
|
|
18
|
+
* Do NOT normalize ranges here.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const encoder = new TextEncoder();
|
|
22
|
+
const decoder = new TextDecoder();
|
|
23
|
+
|
|
24
|
+
/** JSON-encode a value (`BlockMetadata` / `Transform` / `IBlock`) to UTF-8 bytes. */
|
|
25
|
+
export function encodeJson<T>(value: T): Uint8Array {
|
|
26
|
+
return encoder.encode(JSON.stringify(value));
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** Decode UTF-8 JSON bytes back into a value. Callers guard the miss (`undefined`) first. */
|
|
30
|
+
export function decodeJson<T>(bytes: Uint8Array): T {
|
|
31
|
+
return JSON.parse(decoder.decode(bytes)) as T;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/** Encode an `ActionId` string to UTF-8 bytes (stored as the revisions-store value). */
|
|
35
|
+
export function encodeActionId(actionId: ActionId): Uint8Array {
|
|
36
|
+
return encoder.encode(actionId);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Decode UTF-8 bytes back into an `ActionId` string. */
|
|
40
|
+
export function decodeActionId(bytes: Uint8Array): ActionId {
|
|
41
|
+
return decoder.decode(bytes) as ActionId;
|
|
42
|
+
}
|