@optimystic/db-p2p 0.14.1 → 0.16.3
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 +616 -609
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +4 -3
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/block-transfer.d.ts +52 -3
- package/dist/src/cluster/block-transfer.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer.js +129 -6
- package/dist/src/cluster/block-transfer.js.map +1 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +3 -1
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-repo.d.ts +182 -11
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +429 -69
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/peer-key-binding.d.ts +25 -0
- package/dist/src/cluster/peer-key-binding.d.ts.map +1 -0
- package/dist/src/cluster/peer-key-binding.js +39 -0
- package/dist/src/cluster/peer-key-binding.js.map +1 -0
- package/dist/src/cluster/quorum-restore.d.ts +72 -0
- package/dist/src/cluster/quorum-restore.d.ts.map +1 -0
- package/dist/src/cluster/quorum-restore.js +0 -0
- package/dist/src/cluster/quorum-restore.js.map +1 -0
- package/dist/src/cluster/rebalance-monitor.d.ts +23 -1
- package/dist/src/cluster/rebalance-monitor.d.ts.map +1 -1
- package/dist/src/cluster/rebalance-monitor.js +17 -2
- package/dist/src/cluster/rebalance-monitor.js.map +1 -1
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +7 -1
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +7 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +4 -1
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cluster/supermajority-coupling.d.ts +22 -0
- package/dist/src/cluster/supermajority-coupling.d.ts.map +1 -0
- package/dist/src/cluster/supermajority-coupling.js +27 -0
- package/dist/src/cluster/supermajority-coupling.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +46 -4
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-driver.js +62 -7
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +7 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -1
- package/dist/src/cohort-topic/cohort-gossip-transport.js +9 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -1
- package/dist/src/cohort-topic/host.d.ts +197 -27
- package/dist/src/cohort-topic/host.d.ts.map +1 -1
- package/dist/src/cohort-topic/host.js +643 -87
- package/dist/src/cohort-topic/host.js.map +1 -1
- package/dist/src/dispute/arbitrator-selection.d.ts +68 -7
- package/dist/src/dispute/arbitrator-selection.d.ts.map +1 -1
- package/dist/src/dispute/arbitrator-selection.js +105 -16
- package/dist/src/dispute/arbitrator-selection.js.map +1 -1
- package/dist/src/dispute/cascade.d.ts +7 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -1
- package/dist/src/dispute/cascade.js +1 -1
- package/dist/src/dispute/cascade.js.map +1 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +3 -2
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +32 -3
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +82 -9
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +1 -1
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +1 -1
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +25 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -1
- package/dist/src/dispute/invalidation.js +14 -2
- 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 +7 -1
- package/dist/src/dispute/service.js.map +1 -1
- package/dist/src/index.d.ts +4 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/it-utility.d.ts +1 -1
- package/dist/src/it-utility.d.ts.map +1 -1
- package/dist/src/it-utility.js +4 -13
- package/dist/src/it-utility.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +98 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +272 -17
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +12 -0
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +441 -89
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/query-transport.d.ts +2 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -1
- package/dist/src/matchmaking/query-transport.js +6 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -1
- package/dist/src/matchmaking/traffic-validation.js +1 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -1
- package/dist/src/network/network-manager-service.d.ts +2 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +8 -4
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/owned-block-seed.d.ts +27 -0
- package/dist/src/owned-block-seed.d.ts.map +1 -0
- package/dist/src/owned-block-seed.js +37 -0
- package/dist/src/owned-block-seed.js.map +1 -0
- package/dist/src/protocol-client.d.ts +1 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +9 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/protocol-limits.d.ts +32 -0
- package/dist/src/protocol-limits.d.ts.map +1 -0
- package/dist/src/protocol-limits.js +32 -0
- package/dist/src/protocol-limits.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +3 -0
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +41 -4
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +128 -80
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +38 -1
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +92 -13
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +7 -1
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/reputation/peer-reputation.js +1 -1
- package/dist/src/reputation/peer-reputation.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 -0
- package/dist/src/reputation/types.js.map +1 -1
- package/dist/src/rn.d.ts +5 -1
- package/dist/src/rn.d.ts.map +1 -1
- package/dist/src/rn.js +7 -1
- package/dist/src/rn.js.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts +30 -1
- package/dist/src/storage/arachnode-fret-adapter.d.ts.map +1 -1
- package/dist/src/storage/arachnode-fret-adapter.js +15 -2
- package/dist/src/storage/arachnode-fret-adapter.js.map +1 -1
- package/dist/src/storage/arachnode-partition.d.ts +49 -0
- package/dist/src/storage/arachnode-partition.d.ts.map +1 -0
- package/dist/src/storage/arachnode-partition.js +61 -0
- package/dist/src/storage/arachnode-partition.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +40 -1
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +150 -57
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +8 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/i-raw-storage.d.ts +13 -0
- package/dist/src/storage/i-raw-storage.d.ts.map +1 -1
- package/dist/src/storage/kv-raw-storage.d.ts +45 -0
- package/dist/src/storage/kv-raw-storage.d.ts.map +1 -0
- package/dist/src/storage/kv-raw-storage.js +113 -0
- package/dist/src/storage/kv-raw-storage.js.map +1 -0
- package/dist/src/storage/memory-storage.d.ts +20 -58
- package/dist/src/storage/memory-storage.d.ts.map +1 -1
- package/dist/src/storage/memory-storage.js +22 -148
- package/dist/src/storage/memory-storage.js.map +1 -1
- package/dist/src/storage/memory-store-driver.d.ts +39 -0
- package/dist/src/storage/memory-store-driver.d.ts.map +1 -0
- package/dist/src/storage/memory-store-driver.js +129 -0
- package/dist/src/storage/memory-store-driver.js.map +1 -0
- package/dist/src/storage/raw-store-codec.d.ts +10 -0
- package/dist/src/storage/raw-store-codec.d.ts.map +1 -0
- package/dist/src/storage/raw-store-codec.js +36 -0
- package/dist/src/storage/raw-store-codec.js.map +1 -0
- package/dist/src/storage/raw-store-driver.d.ts +69 -0
- package/dist/src/storage/raw-store-driver.d.ts.map +1 -0
- package/dist/src/storage/raw-store-driver.js +2 -0
- package/dist/src/storage/raw-store-driver.js.map +1 -0
- package/dist/src/storage/restoration-coordinator-v2.d.ts.map +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js +1 -1
- package/dist/src/storage/restoration-coordinator-v2.js.map +1 -1
- package/dist/src/storage/restoration-coordinator.d.ts +73 -0
- package/dist/src/storage/restoration-coordinator.d.ts.map +1 -0
- package/dist/src/storage/restoration-coordinator.js +181 -0
- package/dist/src/storage/restoration-coordinator.js.map +1 -0
- package/dist/src/storage/ring-selector.d.ts +92 -5
- package/dist/src/storage/ring-selector.d.ts.map +1 -1
- package/dist/src/storage/ring-selector.js +153 -51
- package/dist/src/storage/ring-selector.js.map +1 -1
- package/dist/src/storage/ring-shift-coordinator.d.ts +128 -0
- package/dist/src/storage/ring-shift-coordinator.d.ts.map +1 -0
- package/dist/src/storage/ring-shift-coordinator.js +196 -0
- package/dist/src/storage/ring-shift-coordinator.js.map +1 -0
- package/dist/src/storage/storage-monitor.d.ts +28 -0
- package/dist/src/storage/storage-monitor.d.ts.map +1 -1
- package/dist/src/storage/storage-monitor.js +42 -2
- package/dist/src/storage/storage-monitor.js.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +19 -0
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +172 -22
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -1
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/sync/service.d.ts.map +1 -1
- package/dist/src/sync/service.js +2 -1
- package/dist/src/sync/service.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +28 -4
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.js +51 -10
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -1
- package/dist/src/testing/index.d.ts +1 -0
- package/dist/src/testing/index.d.ts.map +1 -1
- package/dist/src/testing/index.js +1 -0
- package/dist/src/testing/index.js.map +1 -1
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +15 -7
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/raw-storage-conformance.d.ts +24 -0
- package/dist/src/testing/raw-storage-conformance.d.ts.map +1 -0
- package/dist/src/testing/raw-storage-conformance.js +313 -0
- package/dist/src/testing/raw-storage-conformance.js.map +1 -0
- package/package.json +4 -4
- package/src/cluster/block-transfer-service.ts +4 -3
- package/src/cluster/block-transfer.ts +424 -265
- package/src/cluster/client.ts +3 -1
- package/src/cluster/cluster-repo.ts +481 -71
- package/src/cluster/peer-key-binding.ts +37 -0
- package/src/cluster/quorum-restore.ts +0 -0
- package/src/cluster/rebalance-monitor.ts +34 -3
- package/src/cluster/service.ts +7 -1
- package/src/cluster/spread-on-churn.ts +11 -1
- package/src/cluster/supermajority-coupling.ts +37 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +96 -9
- package/src/cohort-topic/cohort-gossip-transport.ts +10 -0
- package/src/cohort-topic/host.ts +831 -105
- package/src/dispute/arbitrator-selection.ts +138 -28
- package/src/dispute/cascade.ts +8 -1
- package/src/dispute/client.ts +3 -2
- package/src/dispute/dispute-service.ts +94 -9
- package/src/dispute/index.ts +7 -1
- package/src/dispute/invalidation.ts +40 -2
- package/src/dispute/service.ts +7 -1
- package/src/index.ts +46 -43
- package/src/it-utility.ts +5 -13
- package/src/libp2p-key-network.ts +281 -18
- package/src/libp2p-node-base.ts +511 -81
- package/src/matchmaking/query-transport.ts +9 -0
- package/src/network/network-manager-service.ts +8 -2
- package/src/owned-block-seed.ts +40 -0
- package/src/protocol-client.ts +10 -3
- package/src/protocol-limits.ts +33 -0
- package/src/repo/client.ts +3 -0
- package/src/repo/cluster-coordinator.ts +145 -82
- package/src/repo/coordinator-repo.ts +96 -16
- package/src/repo/service.ts +7 -1
- package/src/reputation/types.ts +3 -0
- package/src/rn.ts +7 -1
- package/src/storage/arachnode-fret-adapter.ts +33 -2
- package/src/storage/arachnode-partition.ts +74 -0
- package/src/storage/block-storage.ts +169 -61
- package/src/storage/i-block-storage.ts +9 -0
- package/src/storage/i-raw-storage.ts +14 -0
- package/src/storage/kv-raw-storage.ts +135 -0
- package/src/storage/memory-storage.ts +25 -173
- package/src/storage/memory-store-driver.ts +157 -0
- package/src/storage/raw-store-codec.ts +42 -0
- package/src/storage/raw-store-driver.ts +80 -0
- package/src/storage/{restoration-coordinator-v2.ts → restoration-coordinator.ts} +43 -37
- package/src/storage/ring-selector.ts +317 -176
- package/src/storage/ring-shift-coordinator.ts +271 -0
- package/src/storage/storage-monitor.ts +70 -3
- package/src/storage/storage-repo.ts +710 -558
- package/src/sync/client.ts +5 -1
- package/src/sync/service.ts +2 -1
- package/src/testing/cohort-topic-mesh-harness.ts +61 -11
- package/src/testing/index.ts +1 -0
- package/src/testing/mesh-harness.ts +14 -6
- package/src/testing/raw-storage-conformance.ts +397 -0
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { BlockId, IBlock, Transform, ActionId, ActionRev } from "@optimystic/db-core";
|
|
1
|
+
import type { BlockId, IBlock, Transform, ActionId, ActionRev, ActionTransform } from "@optimystic/db-core";
|
|
2
2
|
import { Latches, applyTransform, hashString } from "@optimystic/db-core";
|
|
3
3
|
import type { BlockArchive, BlockMetadata, RestoreCallback, RevisionRange } from "./struct.js";
|
|
4
4
|
import type { IRawStorage } from "./i-raw-storage.js";
|
|
@@ -8,11 +8,24 @@ import { createLogger } from "../logger.js";
|
|
|
8
8
|
|
|
9
9
|
const log = createLogger('block-storage');
|
|
10
10
|
|
|
11
|
+
/**
|
|
12
|
+
* Default checkpoint cadence: a full materialization is retained at every `CHECKPOINT_INTERVAL`th
|
|
13
|
+
* revision (plus the tip and each range floor). This bounds the maximum replay depth for any read
|
|
14
|
+
* to at most `CHECKPOINT_INTERVAL` forward transforms. See {@link BlockStorage.pruneSupersededMaterialization}.
|
|
15
|
+
*/
|
|
16
|
+
const CHECKPOINT_INTERVAL = 32;
|
|
17
|
+
|
|
11
18
|
export class BlockStorage implements IBlockStorage {
|
|
12
19
|
constructor(
|
|
13
20
|
private readonly blockId: BlockId,
|
|
14
21
|
private readonly storage: IRawStorage,
|
|
15
|
-
private readonly restoreCallback?: RestoreCallback
|
|
22
|
+
private readonly restoreCallback?: RestoreCallback,
|
|
23
|
+
/**
|
|
24
|
+
* Revisions where `rev % checkpointInterval === 0` retain a full materialization even after they
|
|
25
|
+
* stop being the tip. Optional (default {@link CHECKPOINT_INTERVAL}); tests inject a small value
|
|
26
|
+
* to exercise sweeping without committing 32+ revisions.
|
|
27
|
+
*/
|
|
28
|
+
private readonly checkpointInterval: number = CHECKPOINT_INTERVAL
|
|
16
29
|
) { }
|
|
17
30
|
|
|
18
31
|
async getLatest(): Promise<ActionRev | undefined> {
|
|
@@ -54,7 +67,12 @@ export class BlockStorage implements IBlockStorage {
|
|
|
54
67
|
log('pend blockId=%s actionId=%s', this.blockId, actionId);
|
|
55
68
|
let meta = await this.storage.getMetadata(this.blockId);
|
|
56
69
|
if (!meta) {
|
|
57
|
-
|
|
70
|
+
// A freshly-pended block holds NO committed revision, so it can reconstruct
|
|
71
|
+
// nothing yet: seed empty ranges. The first commit anchors an OPEN-ENDED span at
|
|
72
|
+
// the earliest held rev E ([E, +inf)); later commits/recover merge into it via
|
|
73
|
+
// setLatest/recover. Seeding open-ended `[[0]]` would falsely claim coverage of the
|
|
74
|
+
// un-held revs below E and disable ensureRevision's restore path.
|
|
75
|
+
meta = { latest: undefined, ranges: [] };
|
|
58
76
|
await this.storage.saveMetadata(this.blockId, meta);
|
|
59
77
|
}
|
|
60
78
|
await this.storage.savePendingTransaction(this.blockId, actionId, transform);
|
|
@@ -73,6 +91,25 @@ export class BlockStorage implements IBlockStorage {
|
|
|
73
91
|
await this.storage.saveMaterializedBlock(this.blockId, actionId, block);
|
|
74
92
|
}
|
|
75
93
|
|
|
94
|
+
async pruneSupersededMaterialization(prior: ActionRev): Promise<void> {
|
|
95
|
+
const meta = await this.storage.getMetadata(this.blockId);
|
|
96
|
+
// No metadata / no committed tip yet ⇒ nothing has superseded `prior`; leave it.
|
|
97
|
+
if (!meta || meta.latest === undefined) {
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
// `prior` is the PRIOR latest, so it shares the (latest) range containing meta.latest.rev — its
|
|
101
|
+
// floor is that span's start. Retain if it is the tip, that floor, or a checkpoint rev.
|
|
102
|
+
const rangeFloor = this.rangeFloorOf(meta.latest.rev, meta.ranges);
|
|
103
|
+
if (this.isRetainedRev(prior.rev, meta.latest.rev, rangeFloor)) {
|
|
104
|
+
return;
|
|
105
|
+
}
|
|
106
|
+
// Redundant: `prior`'s forward transform is retained, so it stays reconstructible by replay from
|
|
107
|
+
// the nearest retained materialization below it. Delete routes to the driver's deleteMaterialized;
|
|
108
|
+
// a no-op at the driver when `prior.rev` carried no materialization (e.g. a tombstone rev).
|
|
109
|
+
await this.storage.saveMaterializedBlock(this.blockId, prior.actionId, undefined);
|
|
110
|
+
log('prune blockId=%s rev=%d actionId=%s', this.blockId, prior.rev, prior.actionId);
|
|
111
|
+
}
|
|
112
|
+
|
|
76
113
|
async saveRevision(rev: number, actionId: ActionId): Promise<void> {
|
|
77
114
|
await this.storage.saveRevision(this.blockId, rev, actionId);
|
|
78
115
|
}
|
|
@@ -87,7 +124,24 @@ export class BlockStorage implements IBlockStorage {
|
|
|
87
124
|
if (!meta) {
|
|
88
125
|
throw new Error(`Block ${this.blockId} not found`);
|
|
89
126
|
}
|
|
127
|
+
// Capture the prior latest rev BEFORE overwriting: coverage anchors to the earliest held rev.
|
|
128
|
+
const prevRev = meta.latest?.rev;
|
|
90
129
|
meta.latest = latest;
|
|
130
|
+
// NOTE: re-sorts (mergeRanges) the whole ranges array on every commit; if a block ever
|
|
131
|
+
// accumulates many disjoint ranges and commits show as slow, keep a running merged structure.
|
|
132
|
+
// `getBlock(r)` is served by materializeBlock's DESCENDING walk (highest committed rev <= r).
|
|
133
|
+
// Once this node holds the chain from the block's earliest committed rev E, EVERY rev >= E is
|
|
134
|
+
// serveable locally: a read at any r >= E resolves to the highest committed rev <= r (at worst
|
|
135
|
+
// the latest, which is materialized), so coverage is the OPEN-ENDED span [E, +inf) — not the
|
|
136
|
+
// single point [L, L+1) (which wrongly missed reads above L, e.g. a block read at the collection
|
|
137
|
+
// tip after a later commit touched only its siblings) and not [0, +inf) (which wrongly claimed
|
|
138
|
+
// the un-held revs below E). Claim open-ended from the prior latest (>= E via merge); the first
|
|
139
|
+
// commit (prevRev undefined) anchors the span at E = L. mergeRanges folds it into the existing
|
|
140
|
+
// [E, +inf). Only revs BELOW E miss inRanges, which is exactly the genuine-gap/restore case.
|
|
141
|
+
// Range + latest advance in one saveMetadata write (atomic under the commit latch), so a crash
|
|
142
|
+
// before this call advances neither.
|
|
143
|
+
meta.ranges.unshift([prevRev ?? latest.rev]);
|
|
144
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
91
145
|
await this.storage.saveMetadata(this.blockId, meta);
|
|
92
146
|
}
|
|
93
147
|
|
|
@@ -115,6 +169,13 @@ export class BlockStorage implements IBlockStorage {
|
|
|
115
169
|
if (maxRev > currentRev && maxActionId !== undefined) {
|
|
116
170
|
const advanced: ActionRev = { rev: maxRev, actionId: maxActionId };
|
|
117
171
|
meta.latest = advanced;
|
|
172
|
+
// The lost setLatest would have merged each recovered revision's range; redo that
|
|
173
|
+
// here. Open-ended from currentRev+1 (see setLatest): every rev in (currentRev, maxRev]
|
|
174
|
+
// was verified present in the committed log above, and any rev > maxRev resolves via the
|
|
175
|
+
// descending walk to maxRev's materialization — so [currentRev+1, +inf) is honest. It joins
|
|
176
|
+
// the prior [E, currentRev+1) (from the earlier setLatest) into one open-ended [E, +inf).
|
|
177
|
+
meta.ranges.unshift([currentRev + 1]);
|
|
178
|
+
meta.ranges = mergeRanges(meta.ranges);
|
|
118
179
|
await this.storage.saveMetadata(this.blockId, meta);
|
|
119
180
|
log('recover blockId=%s advanced latest from rev=%d to rev=%d', this.blockId, currentRev, maxRev);
|
|
120
181
|
return { reconciled: true, latest: advanced };
|
|
@@ -130,92 +191,91 @@ export class BlockStorage implements IBlockStorage {
|
|
|
130
191
|
// idempotent. Never random/time-based — that would mint a new revision per retry.
|
|
131
192
|
const actionId = source?.actionId ?? await hashString(`${this.blockId}:${JSON.stringify(block)}`);
|
|
132
193
|
|
|
133
|
-
//
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
log('replica:skip blockId=%s rev=%d held=%d', this.blockId, rev, meta.latest.rev);
|
|
143
|
-
return meta.latest;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
// Write rev → actionId, the action transform, and the materialized block.
|
|
147
|
-
// `{ insert: block }` satisfies saveRestored's write invariants; on the serving
|
|
148
|
-
// path materializeBlock returns the materialized block directly (single rev), so
|
|
149
|
-
// this transform is never applied — see ticket notes.
|
|
150
|
-
const archive: BlockArchive = {
|
|
151
|
-
blockId: this.blockId,
|
|
152
|
-
revisions: {
|
|
153
|
-
[rev]: {
|
|
154
|
-
action: { actionId, rev, transform: { insert: block } },
|
|
155
|
-
block
|
|
156
|
-
}
|
|
157
|
-
},
|
|
158
|
-
range: [rev, rev + 1]
|
|
159
|
-
};
|
|
160
|
-
await this.saveRestored(archive);
|
|
161
|
-
|
|
162
|
-
// Seed metadata when absent, advance latest, and merge the covered range.
|
|
163
|
-
if (!meta) {
|
|
164
|
-
meta = { latest: undefined, ranges: [] };
|
|
165
|
-
}
|
|
166
|
-
meta.latest = { rev, actionId };
|
|
167
|
-
meta.ranges.unshift([rev, rev + 1]);
|
|
168
|
-
meta.ranges = mergeRanges(meta.ranges);
|
|
169
|
-
await this.storage.saveMetadata(this.blockId, meta);
|
|
170
|
-
|
|
171
|
-
log('replica:save blockId=%s rev=%d actionId=%s', this.blockId, rev, actionId);
|
|
172
|
-
return meta.latest;
|
|
173
|
-
} finally {
|
|
174
|
-
release();
|
|
175
|
-
}
|
|
194
|
+
// Replica revision carries the materialized block. `{ insert: block }` satisfies saveRestored's
|
|
195
|
+
// write invariants; on the serving path materializeBlock returns the materialized block directly
|
|
196
|
+
// (single rev), so this transform is never applied — see ticket notes.
|
|
197
|
+
return await this.saveForwardRevision(
|
|
198
|
+
rev,
|
|
199
|
+
actionId,
|
|
200
|
+
{ action: { actionId, rev, transform: { insert: block } }, block },
|
|
201
|
+
'replica'
|
|
202
|
+
);
|
|
176
203
|
}
|
|
177
204
|
|
|
178
205
|
async saveDeletion(source: ActionRev): Promise<ActionRev> {
|
|
179
206
|
const { rev, actionId } = source;
|
|
180
207
|
|
|
181
|
-
//
|
|
182
|
-
//
|
|
208
|
+
// Forward tombstone: a `{ delete: true }` transform and NO materialized block. saveRestored
|
|
209
|
+
// skips materialization when `block` is absent, so the reverse-apply in materializeBlock
|
|
210
|
+
// resolves this revision to an absent block (read-back as undefined).
|
|
211
|
+
return await this.saveForwardRevision(
|
|
212
|
+
rev,
|
|
213
|
+
actionId,
|
|
214
|
+
{ action: { actionId, rev, transform: { delete: true } } },
|
|
215
|
+
'deletion'
|
|
216
|
+
);
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Shared forward-write path for saveReplica and saveDeletion. Both append a single new revision
|
|
221
|
+
* that ADVANCES `latest` (never rewrites history): acquire the block's metadata latch, apply the
|
|
222
|
+
* monotonic guard, saveRestored a one-revision archive, then seed/advance/merge metadata.
|
|
223
|
+
*
|
|
224
|
+
* The only per-caller difference is the revision `body`: a replica carries `{ insert: block }`
|
|
225
|
+
* plus the materialized `block`; a deletion carries `{ delete: true }` and no block. `rev` and
|
|
226
|
+
* `actionId` are passed alongside `body` because the guard and the `latest` advance need them
|
|
227
|
+
* independently of the archive body.
|
|
228
|
+
*/
|
|
229
|
+
private async saveForwardRevision(
|
|
230
|
+
rev: number,
|
|
231
|
+
actionId: ActionId,
|
|
232
|
+
body: { action: ActionTransform; block?: IBlock },
|
|
233
|
+
logLabel: 'replica' | 'deletion'
|
|
234
|
+
): Promise<ActionRev> {
|
|
235
|
+
// Serialize the read-modify-write on this block's metadata (mirrors ensureRevision). saveReplica
|
|
236
|
+
// and saveDeletion deliberately SHARE this one lock id (keyed `saveReplica`, NOT per-method):
|
|
237
|
+
// both do a read-modify-write of `meta.latest`, so they must be mutually exclusive on this block
|
|
238
|
+
// to keep the monotonic guard sound against a concurrent replica+deletion.
|
|
183
239
|
const lockId = `BlockStorage.saveReplica:${this.blockId}`;
|
|
184
240
|
const release = await Latches.acquire(lockId);
|
|
185
241
|
try {
|
|
186
242
|
let meta = await this.storage.getMetadata(this.blockId);
|
|
187
243
|
|
|
188
|
-
// Monotonic guard: an equal-or-newer revision is already held.
|
|
189
|
-
//
|
|
244
|
+
// Monotonic guard: an equal-or-newer revision is already held. The block (or tombstone) is
|
|
245
|
+
// durably present; do not downgrade `latest` or rewrite the metadata.
|
|
190
246
|
if (meta?.latest && meta.latest.rev >= rev) {
|
|
191
|
-
log('
|
|
247
|
+
log('%s:skip blockId=%s rev=%d held=%d', logLabel, this.blockId, rev, meta.latest.rev);
|
|
192
248
|
return meta.latest;
|
|
193
249
|
}
|
|
194
250
|
|
|
195
|
-
//
|
|
196
|
-
// skips materialization when `block` is absent,
|
|
197
|
-
//
|
|
251
|
+
// One-revision archive. A replica's body carries the materialized block; a deletion's body
|
|
252
|
+
// omits it (forward tombstone). saveRestored skips materialization when `block` is absent,
|
|
253
|
+
// so a tombstone reverse-applies to an absent block (read back as undefined).
|
|
198
254
|
const archive: BlockArchive = {
|
|
199
255
|
blockId: this.blockId,
|
|
200
256
|
revisions: {
|
|
201
|
-
[rev]:
|
|
202
|
-
action: { actionId, rev, transform: { delete: true } }
|
|
203
|
-
}
|
|
257
|
+
[rev]: body
|
|
204
258
|
},
|
|
205
259
|
range: [rev, rev + 1]
|
|
206
260
|
};
|
|
207
261
|
await this.saveRestored(archive);
|
|
208
262
|
|
|
209
263
|
// Seed metadata when absent, advance latest, and merge the covered range.
|
|
264
|
+
const prevRev = meta?.latest?.rev;
|
|
210
265
|
if (!meta) {
|
|
211
266
|
meta = { latest: undefined, ranges: [] };
|
|
212
267
|
}
|
|
213
268
|
meta.latest = { rev, actionId };
|
|
214
|
-
|
|
269
|
+
// Open-ended coverage from the earliest held rev (see setLatest): the descending walk serves
|
|
270
|
+
// any rev >= the anchor. A prior latest at prevRev (< rev per the monotonic guard) is a
|
|
271
|
+
// materialized point, so anchor at prevRev; the first write (prevRev undefined) anchors at
|
|
272
|
+
// rev. Freshness of a stale replica is a separate (replication-lag) concern from what this
|
|
273
|
+
// node can locally reconstruct, which is exactly what ranges records.
|
|
274
|
+
meta.ranges.unshift([prevRev ?? rev]);
|
|
215
275
|
meta.ranges = mergeRanges(meta.ranges);
|
|
216
276
|
await this.storage.saveMetadata(this.blockId, meta);
|
|
217
277
|
|
|
218
|
-
log('
|
|
278
|
+
log('%s:save blockId=%s rev=%d actionId=%s', logLabel, this.blockId, rev, actionId);
|
|
219
279
|
return meta.latest;
|
|
220
280
|
} finally {
|
|
221
281
|
release();
|
|
@@ -253,7 +313,7 @@ export class BlockStorage implements IBlockStorage {
|
|
|
253
313
|
}
|
|
254
314
|
}
|
|
255
315
|
|
|
256
|
-
private async materializeBlock(
|
|
316
|
+
private async materializeBlock(meta: BlockMetadata, targetRev: number): Promise<{ block: IBlock, actionRev: ActionRev } | undefined> {
|
|
257
317
|
let block: IBlock | undefined;
|
|
258
318
|
let materializedActionRev: ActionRev | undefined;
|
|
259
319
|
const actions: ActionRev[] = [];
|
|
@@ -293,7 +353,26 @@ export class BlockStorage implements IBlockStorage {
|
|
|
293
353
|
return undefined;
|
|
294
354
|
}
|
|
295
355
|
if (actions.length) {
|
|
296
|
-
|
|
356
|
+
// Re-cache the recomputed materialization ONLY at a retained rev (checkpoint / range floor /
|
|
357
|
+
// tip). Caching unconditionally would let a cold read at a non-checkpoint historical rev
|
|
358
|
+
// re-add a materialization the checkpoint sweep is designed to remove — storage would regrow
|
|
359
|
+
// via reads. Skipping it means a repeated cold read re-replays each time, bounded by
|
|
360
|
+
// `checkpointInterval` transforms.
|
|
361
|
+
// NOTE: cold non-checkpoint historical reads re-replay every time (up to `checkpointInterval`
|
|
362
|
+
// forward transforms). Acceptable — historical reads are rare and replay is depth-bounded. If
|
|
363
|
+
// they ever show as hot, cache at the nearest checkpoint below the target instead of skipping.
|
|
364
|
+
// Read metadata FRESH for the retention decision: the `meta` passed in was captured by getBlock
|
|
365
|
+
// BEFORE ensureRevision, which may have restored the containing range during this same read
|
|
366
|
+
// (ensureRevision mutates its own re-read, not this snapshot). A stale `meta.ranges` would send
|
|
367
|
+
// rangeFloorOf into its fallback (treats the target as its own floor ⇒ wrongly "retained"),
|
|
368
|
+
// re-caching a rev the sweep means to prune — regrowing storage via reads of restored ranges.
|
|
369
|
+
const retentionMeta = (await this.storage.getMetadata(this.blockId)) ?? meta;
|
|
370
|
+
const cacheRev = actions[0]!.rev;
|
|
371
|
+
const latestRev = retentionMeta.latest?.rev ?? cacheRev;
|
|
372
|
+
const rangeFloor = this.rangeFloorOf(cacheRev, retentionMeta.ranges);
|
|
373
|
+
if (this.isRetainedRev(cacheRev, latestRev, rangeFloor)) {
|
|
374
|
+
await this.storage.saveMaterializedBlock(this.blockId, actions[0]!.actionId, block);
|
|
375
|
+
}
|
|
297
376
|
return { block, actionRev: actions[0]! };
|
|
298
377
|
}
|
|
299
378
|
return { block, actionRev: materializedActionRev };
|
|
@@ -323,4 +402,33 @@ export class BlockStorage implements IBlockStorage {
|
|
|
323
402
|
rev >= range[0] && (range[1] === undefined || rev < range[1])
|
|
324
403
|
);
|
|
325
404
|
}
|
|
405
|
+
|
|
406
|
+
/**
|
|
407
|
+
* Checkpoint retention predicate. A materialization at `rev` must be kept iff it is the tip
|
|
408
|
+
* (`latestRev` — the common read target and the replay base for the next commit), the floor of its
|
|
409
|
+
* contiguous range (`rangeFloor` — the descending walk in {@link materializeBlock} has nothing below
|
|
410
|
+
* the floor to fall back to), or a periodic checkpoint (`rev % checkpointInterval === 0`, which bounds
|
|
411
|
+
* replay depth). Otherwise the materialization is prunable — its forward transform is retained, so the
|
|
412
|
+
* rev stays reconstructible by replay from the nearest retained materialization below it. The floor
|
|
413
|
+
* clause is SEPARATE and mandatory: absolute `rev % K` checkpoints do not automatically land on the
|
|
414
|
+
* floor (e.g. floor `E = 1`, `K = 32`).
|
|
415
|
+
*/
|
|
416
|
+
private isRetainedRev(rev: number, latestRev: number, rangeFloor: number): boolean {
|
|
417
|
+
return rev === latestRev
|
|
418
|
+
|| rev === rangeFloor
|
|
419
|
+
|| rev % this.checkpointInterval === 0;
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
/** Start of the contiguous `ranges` span containing `rev`. Falls back to `rev` itself when no span
|
|
423
|
+
* contains it — unreachable for a committed rev (setLatest always merges the containing span before a
|
|
424
|
+
* prune/read runs), and the conservative direction (treats `rev` as its own floor ⇒ retained). */
|
|
425
|
+
private rangeFloorOf(rev: number, ranges: RevisionRange[]): number {
|
|
426
|
+
for (const range of ranges) {
|
|
427
|
+
const [start, end] = range;
|
|
428
|
+
if (rev >= start && (end === undefined || rev < end)) {
|
|
429
|
+
return start;
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
return rev;
|
|
433
|
+
}
|
|
326
434
|
}
|
|
@@ -35,6 +35,15 @@ export interface IBlockStorage {
|
|
|
35
35
|
/** Saves a materialized block */
|
|
36
36
|
saveMaterializedBlock(actionId: ActionId, block: IBlock | undefined): Promise<void>;
|
|
37
37
|
|
|
38
|
+
/**
|
|
39
|
+
* Delete the materialized copy at `prior` if it is now redundant under the checkpoint
|
|
40
|
+
* retention policy (not the tip, not its range floor, not a checkpoint rev). The forward
|
|
41
|
+
* transform for `prior.rev` is retained, so the rev stays reconstructible by replay.
|
|
42
|
+
* No-op if `prior.rev` must be retained or has no materialization (e.g. a tombstone rev).
|
|
43
|
+
* Must be called under the per-block commit latch (serialized against concurrent commit).
|
|
44
|
+
*/
|
|
45
|
+
pruneSupersededMaterialization(prior: ActionRev): Promise<void>;
|
|
46
|
+
|
|
38
47
|
/** Saves a revision */
|
|
39
48
|
saveRevision(rev: number, actionId: ActionId): Promise<void>;
|
|
40
49
|
|
|
@@ -37,4 +37,18 @@ export interface IRawStorage {
|
|
|
37
37
|
* advisory — `StorageMonitor` treats a missing implementation as 0.
|
|
38
38
|
*/
|
|
39
39
|
getApproximateBytesUsed?(): Promise<number>;
|
|
40
|
+
|
|
41
|
+
/**
|
|
42
|
+
* Enumerate the block ids that currently have durable state in this backend
|
|
43
|
+
* (one id per block that has committed/replicated metadata). Used at node
|
|
44
|
+
* startup to seed the resilience monitors' owned-block tracked set from blocks
|
|
45
|
+
* already on disk from a previous run, so churn-spread / rebalance protection
|
|
46
|
+
* covers them without waiting for each to be touched again.
|
|
47
|
+
*
|
|
48
|
+
* Streamed (AsyncIterable) so a large store does not force the whole id list
|
|
49
|
+
* into memory at once. Order is unspecified. Optional: a backend that omits it
|
|
50
|
+
* (or an in-memory backend with nothing durable across a restart) simply yields
|
|
51
|
+
* no seed — the monitors still populate over time via the live change feed.
|
|
52
|
+
*/
|
|
53
|
+
listBlockIds?(): AsyncIterable<BlockId>;
|
|
40
54
|
}
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
import type { BlockId, ActionId, ActionRev, Transform, IBlock } from "@optimystic/db-core";
|
|
2
|
+
import type { BlockMetadata } from "./struct.js";
|
|
3
|
+
import type { IRawStorage } from "./i-raw-storage.js";
|
|
4
|
+
import type { RawStoreDriver } from "./raw-store-driver.js";
|
|
5
|
+
import { encodeJson, decodeJson, encodeActionId, decodeActionId } from "./raw-store-codec.js";
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* Shared ordered-KV storage kernel. Implements the full {@link IRawStorage}
|
|
9
|
+
* surface over a {@link RawStoreDriver}, owning all value (de)serialization and
|
|
10
|
+
* call orchestration so each backend only has to expose its five logical stores
|
|
11
|
+
* as bytes-valued maps over its native mechanism.
|
|
12
|
+
*
|
|
13
|
+
* The genuinely-shared logic lives ABOVE the storage primitive: the JSON/string
|
|
14
|
+
* codec for the four value types, `listRevisions`' lo/hi/reverse bound
|
|
15
|
+
* computation, `saveMaterializedBlock`'s put-or-delete branch, and the
|
|
16
|
+
* passthroughs. Key layout / storage topology stays in the driver, because the
|
|
17
|
+
* backends do NOT share one — LevelDB is a single ordered byte keyspace, SQLite
|
|
18
|
+
* five relational tables, IndexedDB five object stores, the filesystem a
|
|
19
|
+
* directory tree.
|
|
20
|
+
*/
|
|
21
|
+
export class KvRawStorage implements IRawStorage {
|
|
22
|
+
/**
|
|
23
|
+
* Optional passthroughs are wired in the constructor ONLY when the driver
|
|
24
|
+
* provides them, so a `StorageMonitor` / owned-block seed that feature-detects
|
|
25
|
+
* (`typeof storage.listBlockIds === 'function'`) sees the driver's true
|
|
26
|
+
* capability instead of a stub that silently reports 0 / no seed.
|
|
27
|
+
*/
|
|
28
|
+
listBlockIds?: () => AsyncIterable<BlockId>;
|
|
29
|
+
getApproximateBytesUsed?: () => Promise<number>;
|
|
30
|
+
|
|
31
|
+
constructor(private readonly driver: RawStoreDriver) {
|
|
32
|
+
if (driver.listBlockIds) {
|
|
33
|
+
this.listBlockIds = () => driver.listBlockIds!();
|
|
34
|
+
}
|
|
35
|
+
if (driver.approximateBytesUsed) {
|
|
36
|
+
this.getApproximateBytesUsed = () => driver.approximateBytesUsed!();
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// --- Metadata ---
|
|
41
|
+
|
|
42
|
+
async getMetadata(blockId: BlockId): Promise<BlockMetadata | undefined> {
|
|
43
|
+
const bytes = await this.driver.getMetadata(blockId);
|
|
44
|
+
return bytes === undefined ? undefined : decodeJson<BlockMetadata>(bytes);
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// NOTE: every value write funnels through the driver put/delete calls in the
|
|
48
|
+
// methods below (saveMetadata / saveRevision / save*Transaction /
|
|
49
|
+
// saveMaterializedBlock). This is the single choke point where an incremental
|
|
50
|
+
// byte counter would hook in, replacing the per-driver full-scan
|
|
51
|
+
// getApproximateBytesUsed. The chosen capacity-estimate mechanism is instead a
|
|
52
|
+
// short-TTL cache over the full scan in StorageMonitor (see storage-monitor.ts
|
|
53
|
+
// `usedBytesCacheTtlMs`) — backend-agnostic, no write-path changes, and ring
|
|
54
|
+
// selection tolerates the bounded staleness by design. The counter remains a
|
|
55
|
+
// future option only if TTL staleness ever proves insufficient; do NOT
|
|
56
|
+
// implement it here — leave the write path a single seam.
|
|
57
|
+
async saveMetadata(blockId: BlockId, metadata: BlockMetadata): Promise<void> {
|
|
58
|
+
await this.driver.putMetadata(blockId, encodeJson(metadata));
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// --- Revisions ---
|
|
62
|
+
|
|
63
|
+
async getRevision(blockId: BlockId, rev: number): Promise<ActionId | undefined> {
|
|
64
|
+
const bytes = await this.driver.getRevision(blockId, rev);
|
|
65
|
+
return bytes === undefined ? undefined : decodeActionId(bytes);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async saveRevision(blockId: BlockId, rev: number, actionId: ActionId): Promise<void> {
|
|
69
|
+
await this.driver.putRevision(blockId, rev, encodeActionId(actionId));
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
async *listRevisions(blockId: BlockId, startRev: number, endRev: number): AsyncIterable<ActionRev> {
|
|
73
|
+
// Both bounds inclusive; ordering follows startRev→endRev direction. Empty
|
|
74
|
+
// revs are skipped by the driver's range. Preserves the exact semantics of
|
|
75
|
+
// every existing backend.
|
|
76
|
+
const ascending = startRev <= endRev;
|
|
77
|
+
const lo = ascending ? startRev : endRev;
|
|
78
|
+
const hi = ascending ? endRev : startRev;
|
|
79
|
+
for await (const [rev, bytes] of this.driver.rangeRevisions(blockId, lo, hi, !ascending)) {
|
|
80
|
+
yield { rev, actionId: decodeActionId(bytes) };
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// --- Pending transactions ---
|
|
85
|
+
|
|
86
|
+
async getPendingTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
87
|
+
const bytes = await this.driver.getPending(blockId, actionId);
|
|
88
|
+
return bytes === undefined ? undefined : decodeJson<Transform>(bytes);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
async savePendingTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
92
|
+
await this.driver.putPending(blockId, actionId, encodeJson(transform));
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
async deletePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
96
|
+
await this.driver.deletePending(blockId, actionId);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async *listPendingTransactions(blockId: BlockId): AsyncIterable<ActionId> {
|
|
100
|
+
yield* this.driver.listPendingActionIds(blockId);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// --- Committed transactions ---
|
|
104
|
+
|
|
105
|
+
async getTransaction(blockId: BlockId, actionId: ActionId): Promise<Transform | undefined> {
|
|
106
|
+
const bytes = await this.driver.getTransaction(blockId, actionId);
|
|
107
|
+
return bytes === undefined ? undefined : decodeJson<Transform>(bytes);
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
async saveTransaction(blockId: BlockId, actionId: ActionId, transform: Transform): Promise<void> {
|
|
111
|
+
await this.driver.putTransaction(blockId, actionId, encodeJson(transform));
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// --- Materialized blocks ---
|
|
115
|
+
|
|
116
|
+
async getMaterializedBlock(blockId: BlockId, actionId: ActionId): Promise<IBlock | undefined> {
|
|
117
|
+
const bytes = await this.driver.getMaterialized(blockId, actionId);
|
|
118
|
+
return bytes === undefined ? undefined : decodeJson<IBlock>(bytes);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
async saveMaterializedBlock(blockId: BlockId, actionId: ActionId, block?: IBlock): Promise<void> {
|
|
122
|
+
// Present ⇒ put the materialized block; absent ⇒ delete (tombstone / truncation).
|
|
123
|
+
if (block) {
|
|
124
|
+
await this.driver.putMaterialized(blockId, actionId, encodeJson(block));
|
|
125
|
+
} else {
|
|
126
|
+
await this.driver.deleteMaterialized(blockId, actionId);
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// --- Promote (the only cross-key atomic op; driver owns atomicity) ---
|
|
131
|
+
|
|
132
|
+
async promotePendingTransaction(blockId: BlockId, actionId: ActionId): Promise<void> {
|
|
133
|
+
await this.driver.promote(blockId, actionId);
|
|
134
|
+
}
|
|
135
|
+
}
|