@optimystic/db-core 0.14.0 → 0.16.2
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/dist/src/blocks/block-store.d.ts +18 -1
- package/dist/src/blocks/block-store.d.ts.map +1 -1
- package/dist/src/blocks/helpers.d.ts +2 -2
- package/dist/src/blocks/helpers.d.ts.map +1 -1
- package/dist/src/blocks/helpers.js +2 -2
- package/dist/src/blocks/helpers.js.map +1 -1
- package/dist/src/btree/btree.d.ts +27 -6
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +104 -61
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +11 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/index.d.ts +1 -0
- package/dist/src/cluster/index.d.ts.map +1 -1
- package/dist/src/cluster/index.js +1 -0
- package/dist/src/cluster/index.js.map +1 -1
- package/dist/src/cluster/membership.d.ts +49 -0
- package/dist/src/cluster/membership.d.ts.map +1 -0
- package/dist/src/cluster/membership.js +75 -0
- package/dist/src/cluster/membership.js.map +1 -0
- package/dist/src/cluster/structs.d.ts +50 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +9 -1
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/addressing.d.ts +8 -2
- package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
- package/dist/src/cohort-topic/addressing.js +11 -3
- package/dist/src/cohort-topic/addressing.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
- package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
- package/dist/src/cohort-topic/coldstart.d.ts +34 -14
- package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
- package/dist/src/cohort-topic/coldstart.js +25 -11
- package/dist/src/cohort-topic/coldstart.js.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
- package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/bus.js +27 -1
- package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
- package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
- package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
- package/dist/src/cohort-topic/gossip/view.js +17 -1
- package/dist/src/cohort-topic/gossip/view.js.map +1 -1
- package/dist/src/cohort-topic/member-engine.d.ts +10 -3
- package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
- package/dist/src/cohort-topic/member-engine.js +29 -14
- package/dist/src/cohort-topic/member-engine.js.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
- package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/publisher.js +15 -27
- package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
- package/dist/src/cohort-topic/membership/verifier.js +88 -3
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
- package/dist/src/cohort-topic/promotion.d.ts +7 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
- package/dist/src/cohort-topic/promotion.js +16 -2
- package/dist/src/cohort-topic/promotion.js.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/renewal.js +54 -0
- package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
- package/dist/src/cohort-topic/registration/types.d.ts +12 -0
- package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/registration/types.js +14 -0
- package/dist/src/cohort-topic/registration/types.js.map +1 -1
- package/dist/src/cohort-topic/service.d.ts +4 -3
- package/dist/src/cohort-topic/service.d.ts.map +1 -1
- package/dist/src/cohort-topic/service.js +38 -12
- package/dist/src/cohort-topic/service.js.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
- package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/sig/payloads.js +22 -6
- package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
- package/dist/src/cohort-topic/traffic.d.ts +6 -0
- package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
- package/dist/src/cohort-topic/traffic.js +16 -1
- package/dist/src/cohort-topic/traffic.js.map +1 -1
- package/dist/src/cohort-topic/walk.d.ts +22 -7
- package/dist/src/cohort-topic/walk.d.ts.map +1 -1
- package/dist/src/cohort-topic/walk.js +78 -18
- package/dist/src/cohort-topic/walk.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +7 -1
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts +1 -1
- package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/index.js +1 -1
- package/dist/src/cohort-topic/wire/index.js.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
- package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/payloads.js +14 -5
- package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
- package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
- package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/primitives.js +173 -0
- package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
- package/dist/src/cohort-topic/wire/types.d.ts +132 -5
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/validate.js +146 -123
- package/dist/src/cohort-topic/wire/validate.js.map +1 -1
- package/dist/src/collection/collection.d.ts +23 -12
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +111 -35
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +37 -2
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +16 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +18 -1
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +36 -6
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.d.ts +26 -3
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +34 -7
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/log/struct.d.ts +5 -4
- package/dist/src/log/struct.d.ts.map +1 -1
- package/dist/src/log/struct.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts +2 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +13 -92
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/i-key-network.d.ts +9 -0
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/i-repo.d.ts +8 -0
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +10 -0
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.d.ts.map +1 -1
- package/dist/src/reactivity/push-state.js +4 -33
- package/dist/src/reactivity/push-state.js.map +1 -1
- package/dist/src/reactivity/wire-validate.d.ts +7 -22
- package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
- package/dist/src/reactivity/wire-validate.js +7 -73
- package/dist/src/reactivity/wire-validate.js.map +1 -1
- package/dist/src/reactivity/wire.js +3 -3
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/async-wait.d.ts +23 -0
- package/dist/src/testing/async-wait.d.ts.map +1 -0
- package/dist/src/testing/async-wait.js +42 -0
- package/dist/src/testing/async-wait.js.map +1 -0
- package/dist/src/testing/index.d.ts +3 -0
- package/dist/src/testing/index.d.ts.map +1 -0
- package/dist/src/testing/index.js +3 -0
- package/dist/src/testing/index.js.map +1 -0
- package/dist/src/testing/test-transactor.d.ts +44 -0
- package/dist/src/testing/test-transactor.d.ts.map +1 -0
- package/dist/src/testing/test-transactor.js +423 -0
- package/dist/src/testing/test-transactor.js.map +1 -0
- package/dist/src/transaction/actions-engine.d.ts +6 -3
- package/dist/src/transaction/actions-engine.d.ts.map +1 -1
- package/dist/src/transaction/actions-engine.js +8 -7
- package/dist/src/transaction/actions-engine.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +43 -22
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +443 -178
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +82 -0
- package/dist/src/transaction/errors.d.ts.map +1 -0
- package/dist/src/transaction/errors.js +93 -0
- package/dist/src/transaction/errors.js.map +1 -0
- package/dist/src/transaction/index.d.ts +6 -3
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js +4 -2
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transaction/operations-hash.d.ts +119 -0
- package/dist/src/transaction/operations-hash.d.ts.map +1 -0
- package/dist/src/transaction/operations-hash.js +180 -0
- package/dist/src/transaction/operations-hash.js.map +1 -0
- package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
- package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
- package/dist/src/transaction/read-dependency-collector.js +69 -0
- package/dist/src/transaction/read-dependency-collector.js.map +1 -0
- package/dist/src/transaction/session.d.ts +12 -3
- package/dist/src/transaction/session.d.ts.map +1 -1
- package/dist/src/transaction/session.js +35 -9
- package/dist/src/transaction/session.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +162 -1
- package/dist/src/transaction/transaction.d.ts.map +1 -1
- package/dist/src/transaction/transaction.js +85 -3
- package/dist/src/transaction/transaction.js.map +1 -1
- package/dist/src/transaction/validator.d.ts +3 -11
- package/dist/src/transaction/validator.d.ts.map +1 -1
- package/dist/src/transaction/validator.js +69 -28
- package/dist/src/transaction/validator.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +35 -0
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +146 -40
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +19 -5
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +43 -11
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/atomic-proxy.d.ts +35 -5
- package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
- package/dist/src/transform/atomic-proxy.js +39 -7
- package/dist/src/transform/atomic-proxy.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +36 -5
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +84 -8
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -2
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +17 -2
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +66 -11
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/backoff.d.ts +48 -0
- package/dist/src/utility/backoff.d.ts.map +1 -0
- package/dist/src/utility/backoff.js +60 -0
- package/dist/src/utility/backoff.js.map +1 -0
- package/dist/src/utility/pending.d.ts +1 -0
- package/dist/src/utility/pending.d.ts.map +1 -1
- package/dist/src/utility/pending.js +3 -1
- package/dist/src/utility/pending.js.map +1 -1
- package/package.json +4 -4
- package/src/blocks/block-store.ts +19 -1
- package/src/blocks/helpers.ts +3 -3
- package/src/btree/btree.ts +108 -63
- package/src/chain/chain.ts +11 -1
- package/src/cluster/index.ts +1 -0
- package/src/cluster/membership.ts +85 -0
- package/src/cluster/structs.ts +51 -1
- package/src/cohort-topic/addressing.ts +12 -4
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
- package/src/cohort-topic/antidos/replay-guard.ts +37 -0
- package/src/cohort-topic/antidos/topic-budget.ts +17 -2
- package/src/cohort-topic/coldstart.ts +48 -15
- package/src/cohort-topic/gossip/bus.ts +39 -2
- package/src/cohort-topic/gossip/view.ts +26 -1
- package/src/cohort-topic/member-engine.ts +36 -15
- package/src/cohort-topic/membership/publisher.ts +25 -33
- package/src/cohort-topic/membership/verifier.ts +119 -3
- package/src/cohort-topic/promotion.ts +23 -2
- package/src/cohort-topic/registration/renewal.ts +73 -1
- package/src/cohort-topic/registration/types.ts +15 -0
- package/src/cohort-topic/service.ts +41 -15
- package/src/cohort-topic/sig/payloads.ts +29 -9
- package/src/cohort-topic/traffic.ts +23 -1
- package/src/cohort-topic/walk.ts +95 -24
- package/src/cohort-topic/wire/codec.ts +12 -0
- package/src/cohort-topic/wire/index.ts +2 -0
- package/src/cohort-topic/wire/payloads.ts +14 -5
- package/src/cohort-topic/wire/primitives.ts +188 -0
- package/src/cohort-topic/wire/types.ts +136 -4
- package/src/cohort-topic/wire/validate.ts +168 -134
- package/src/collection/collection.ts +123 -40
- package/src/collection/struct.ts +44 -2
- package/src/collections/tree/tree.ts +42 -5
- package/src/index.ts +1 -0
- package/src/log/log.ts +37 -9
- package/src/log/struct.ts +5 -4
- package/src/matchmaking/wire.ts +25 -102
- package/src/network/i-key-network.ts +10 -0
- package/src/network/i-repo.ts +8 -0
- package/src/network/struct.ts +10 -0
- package/src/reactivity/push-state.ts +4 -37
- package/src/reactivity/wire-validate.ts +7 -81
- package/src/reactivity/wire.ts +3 -3
- package/src/testing/async-wait.ts +65 -0
- package/src/testing/index.ts +2 -0
- package/src/testing/test-transactor.ts +468 -0
- package/src/transaction/actions-engine.ts +8 -6
- package/src/transaction/coordinator.ts +498 -229
- package/src/transaction/errors.ts +91 -0
- package/src/transaction/index.ts +23 -3
- package/src/transaction/operations-hash.ts +196 -0
- package/src/transaction/read-dependency-collector.ts +78 -0
- package/src/transaction/session.ts +37 -9
- package/src/transaction/transaction.ts +211 -4
- package/src/transaction/validator.ts +73 -48
- package/src/transactor/network-transactor.ts +164 -49
- package/src/transactor/transactor-source.ts +47 -12
- package/src/transform/atomic-proxy.ts +92 -49
- package/src/transform/cache-source.ts +87 -9
- package/src/transform/helpers.ts +8 -2
- package/src/transform/tracker.ts +69 -11
- package/src/utility/backoff.ts +95 -0
- package/src/utility/pending.ts +3 -1
- package/src/transaction/context.ts +0 -103
|
@@ -1,21 +1,31 @@
|
|
|
1
|
-
import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms } from "../index.js";
|
|
1
|
+
import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms, ActionId } from "../index.js";
|
|
2
2
|
import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource } from "../index.js";
|
|
3
|
-
import type { CollectionHeaderBlock, CollectionId, ICollection } from "./index.js";
|
|
3
|
+
import type { CollectionHeaderBlock, CollectionId, ICollection, SyncOptions } from "./index.js";
|
|
4
|
+
import { SyncRetryExhaustedError } from "./index.js";
|
|
4
5
|
import type { ReadDependency } from "../transaction/transaction.js";
|
|
6
|
+
import { clampPriority } from "../transaction/transaction.js";
|
|
7
|
+
import { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
5
8
|
import { randomBytes } from '@noble/hashes/utils.js';
|
|
6
9
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
|
|
7
10
|
import { Latches } from "../utility/latches.js";
|
|
11
|
+
import { jitteredBackoffMs, abortableDelay, makeAbortError } from "../utility/backoff.js";
|
|
8
12
|
|
|
13
|
+
/** Default base backoff (and historical fixed delay) between sync retries, in ms. */
|
|
9
14
|
const PendingRetryDelayMs = 100;
|
|
15
|
+
/** Default max consecutive no-progress stale-failure retries before {@link Collection.sync} gives up. */
|
|
16
|
+
const DefaultMaxAttempts = 10;
|
|
17
|
+
/** Default ceiling on a single exponential-backoff sleep, in ms. */
|
|
18
|
+
const DefaultMaxBackoffMs = 5000;
|
|
10
19
|
|
|
11
20
|
export type CollectionInitOptions<TAction> = {
|
|
12
21
|
modules: Record<ActionType, ActionHandler<TAction>>;
|
|
13
22
|
createHeaderBlock: (id: BlockId, store: BlockStore<IBlock>) => IBlock;
|
|
14
23
|
/** Called for each local action that is potentially in conflict with a remote action.
|
|
15
24
|
* @param action - The local action to check
|
|
16
|
-
* @param potential - The remote
|
|
17
|
-
* @returns The original action
|
|
18
|
-
*
|
|
25
|
+
* @param potential - The remote actions that are potentially in conflict
|
|
26
|
+
* @returns The original action (return the same instance to keep it as-is), a replacement
|
|
27
|
+
* action (return a new instance to apply instead of the original — it is re-staged via
|
|
28
|
+
* the conflict replay), or undefined to discard this action
|
|
19
29
|
*/
|
|
20
30
|
filterConflict?: (action: Action<TAction>, potential: Action<TAction>[]) => Action<TAction> | undefined
|
|
21
31
|
}
|
|
@@ -48,9 +58,12 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
48
58
|
}
|
|
49
59
|
|
|
50
60
|
static async createOrOpen<TAction>(transactor: ITransactor, id: CollectionId, init: CollectionInitOptions<TAction>) {
|
|
51
|
-
// Start with a context that has an infinite revision number to ensure that we always fetch the latest log information
|
|
52
|
-
|
|
53
|
-
|
|
61
|
+
// Start with a context that has an infinite revision number to ensure that we always fetch the latest log information.
|
|
62
|
+
// One shared read-dependency collector feeds both the source (direct structural reads) and the cache (every
|
|
63
|
+
// cache hit/miss), so a block read from either layer records a dependency — cache hits included.
|
|
64
|
+
const collector = new ReadDependencyCollector();
|
|
65
|
+
const source = new TransactorSource(id, transactor, undefined, collector);
|
|
66
|
+
const sourceCache = new CacheSource(source, undefined, collector);
|
|
54
67
|
const tracker = new Tracker(sourceCache);
|
|
55
68
|
const header = await source.tryGet(id) as CollectionHeaderBlock | undefined;
|
|
56
69
|
|
|
@@ -129,11 +142,22 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
129
142
|
// Process the entries and track the blocks they affect
|
|
130
143
|
let anyConflicts = false;
|
|
131
144
|
for (const entry of latest?.entries ?? []) {
|
|
132
|
-
// Filter any pending actions that conflict with the remote actions
|
|
133
|
-
|
|
134
|
-
|
|
145
|
+
// Filter any pending actions that conflict with the remote actions. Each pending
|
|
146
|
+
// action maps to its effective form: the original, a replacement, or dropped.
|
|
147
|
+
const before = this.pending;
|
|
148
|
+
const after = before
|
|
149
|
+
.map(p => this.doFilterConflict(p, entry.actions))
|
|
150
|
+
.filter((a): a is Action<TAction> => a !== undefined);
|
|
151
|
+
// A replacement or a discard changes the pending set; the tracker still holds the
|
|
152
|
+
// pre-filter transforms, so force a replay to re-stage against the effective actions.
|
|
153
|
+
// Identity comparison per the contract: keep => same instance, replace => new instance.
|
|
154
|
+
// NOTE: a filterConflict hook that always allocates a fresh (but equal) instance instead
|
|
155
|
+
// of returning the same one forces a replay on every update — if that ever shows up as a
|
|
156
|
+
// hot path, compare by value/id here instead of by reference.
|
|
157
|
+
const mutated = after.length !== before.length || after.some((a, i) => a !== before[i]);
|
|
158
|
+
this.pending = after;
|
|
135
159
|
this.sourceCache.clear(entry.blockIds);
|
|
136
|
-
anyConflicts = anyConflicts || this.tracker.conflicts(new Set(entry.blockIds)).length > 0;
|
|
160
|
+
anyConflicts = anyConflicts || mutated || this.tracker.conflicts(new Set(entry.blockIds)).length > 0;
|
|
137
161
|
}
|
|
138
162
|
|
|
139
163
|
// React to durable invalidations that landed since we last synced. getFrom intentionally skips
|
|
@@ -223,26 +247,67 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
223
247
|
* this, a collection that already had committed state (e.g. a pre-synced index
|
|
224
248
|
* tree, or any collection on its second commit) keeps serving the stale prior
|
|
225
249
|
* revision because {@link update} sees its rev is already current and refetches
|
|
226
|
-
* nothing. Call BEFORE resetting the tracker (the transforms are read live).
|
|
227
|
-
|
|
228
|
-
|
|
250
|
+
* nothing. Call BEFORE resetting the tracker (the transforms are read live).
|
|
251
|
+
*
|
|
252
|
+
* @param revision - the committed revision these transforms land at (from
|
|
253
|
+
* {@link recordCommitted}), so cached read-dependency revisions advance to it. */
|
|
254
|
+
applyCommittedToCache(transforms: Transforms, revision: number): void {
|
|
255
|
+
this.sourceCache.transformCache(transforms, revision);
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/** Next revision this collection would commit at (current committed rev + 1). */
|
|
259
|
+
getNextRev(): number {
|
|
260
|
+
return (this.source.actionContext?.rev ?? 0) + 1;
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/** Record a just-committed action: append its ActionRev to the committed list
|
|
264
|
+
* and advance the revision. Returns the new revision. Mirrors the inline bump
|
|
265
|
+
* in {@link syncInternal}. */
|
|
266
|
+
recordCommitted(actionId: ActionId): number {
|
|
267
|
+
const rev = this.getNextRev();
|
|
268
|
+
this.source.actionContext = {
|
|
269
|
+
committed: [...(this.source.actionContext?.committed ?? []), { actionId, rev }],
|
|
270
|
+
rev,
|
|
271
|
+
};
|
|
272
|
+
return rev;
|
|
229
273
|
}
|
|
230
274
|
|
|
231
275
|
/** Push our pending actions to the transactor */
|
|
232
|
-
async sync() {
|
|
276
|
+
async sync(options?: SyncOptions) {
|
|
233
277
|
const release = await Latches.acquire(this.latchId);
|
|
234
278
|
try {
|
|
235
|
-
await this.syncInternal();
|
|
279
|
+
await this.syncInternal(options);
|
|
236
280
|
} finally {
|
|
237
281
|
release();
|
|
238
282
|
}
|
|
239
283
|
}
|
|
240
284
|
|
|
241
|
-
private async syncInternal() {
|
|
285
|
+
private async syncInternal(options?: SyncOptions) {
|
|
242
286
|
const bytes = randomBytes(16);
|
|
243
287
|
const actionId = uint8ArrayToString(bytes, 'base64url');
|
|
244
288
|
|
|
289
|
+
const maxAttempts = options?.maxAttempts ?? DefaultMaxAttempts;
|
|
290
|
+
const baseBackoffMs = options?.baseBackoffMs ?? PendingRetryDelayMs;
|
|
291
|
+
const maxBackoffMs = options?.maxBackoffMs ?? DefaultMaxBackoffMs;
|
|
292
|
+
const deadlineMs = options?.deadlineMs;
|
|
293
|
+
const signal = options?.signal;
|
|
294
|
+
const startedAt = Date.now();
|
|
295
|
+
|
|
296
|
+
// Count of consecutive stale failures that made no forward progress. Reset to 0 on every
|
|
297
|
+
// successful transact, so the cap bounds only a persistently-failing sync — a legitimate
|
|
298
|
+
// large multi-batch sync (which iterates many times committing progress) never trips it.
|
|
299
|
+
let consecutiveFailures = 0;
|
|
300
|
+
let lastReason: string | undefined;
|
|
301
|
+
|
|
245
302
|
while (this.pending.length || !isTransformsEmpty(this.tracker.transforms)) {
|
|
303
|
+
if (signal?.aborted) {
|
|
304
|
+
throw makeAbortError(signal);
|
|
305
|
+
}
|
|
306
|
+
// Progress-agnostic ceiling: give up if the wall-clock deadline passed.
|
|
307
|
+
if (deadlineMs !== undefined && Date.now() - startedAt >= deadlineMs) {
|
|
308
|
+
throw new SyncRetryExhaustedError(this.id, consecutiveFailures, lastReason ?? 'deadline exceeded');
|
|
309
|
+
}
|
|
310
|
+
|
|
246
311
|
// Snapshot the pending actions so that any new actions aren't assumed to be part of this action
|
|
247
312
|
const pending = [...this.pending];
|
|
248
313
|
|
|
@@ -258,22 +323,46 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
258
323
|
const newRev = (this.source.actionContext?.rev ?? 0) + 1;
|
|
259
324
|
const addResult = await log.addActions(pending, actionId, newRev, () => tracker.transformedBlockIds());
|
|
260
325
|
|
|
261
|
-
// Commit the action to the transactor
|
|
262
|
-
|
|
326
|
+
// Commit the action to the transactor. Carry the aged retry priority derived from the
|
|
327
|
+
// consecutive-failure count so a sync that keeps losing concurrent races out-ranks fresh
|
|
328
|
+
// (priority-0) rivals in the cluster's resolveRace (fairness-only; capped at MaxPriority).
|
|
329
|
+
// First attempt has consecutiveFailures == 0, so priority 0 — the common pend is unchanged.
|
|
330
|
+
const staleFailure = await this.source.transact(tracker.transforms, actionId, newRev, this.id, addResult.tailPath.block.header.id, clampPriority(consecutiveFailures));
|
|
263
331
|
if (staleFailure) {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
332
|
+
consecutiveFailures++;
|
|
333
|
+
lastReason = staleFailure.reason ?? lastReason;
|
|
334
|
+
// Give up once the consecutive no-progress budget is exhausted, so a transactor that
|
|
335
|
+
// persistently rejects the sync can no longer hold the collection latch forever.
|
|
336
|
+
// NOTE: this also bounds the legitimate `pending`-wait case (retrying the same action
|
|
337
|
+
// while another commit is in flight), which used to retry indefinitely. Default 10
|
|
338
|
+
// attempts ≈ 21s of exponential backoff. If a high-contention workload legitimately
|
|
339
|
+
// needs to wait longer for a pending commit to clear, raise maxAttempts for that caller.
|
|
340
|
+
if (consecutiveFailures >= maxAttempts) {
|
|
341
|
+
throw new SyncRetryExhaustedError(this.id, consecutiveFailures, lastReason);
|
|
267
342
|
}
|
|
343
|
+
// Back off before every retry (any stale failure — reason/missing/pending), growing
|
|
344
|
+
// exponentially from the base delay up to the cap, with proportional random jitter so a
|
|
345
|
+
// herd of clients that lost the same race does not re-collide on the next tick (see
|
|
346
|
+
// utility/backoff.ts). The abortable sleep lets an aborted sync reject promptly instead
|
|
347
|
+
// of finishing the sleep.
|
|
348
|
+
// NOTE: the `missing`/`reason` conflict paths now pay this backoff too (they previously
|
|
349
|
+
// retried with zero delay); that is what stops the persistent-`reason` hot spin. If a
|
|
350
|
+
// high-contention workload ever shows this base delay as recovery latency, lower
|
|
351
|
+
// baseBackoffMs for that caller rather than reintroducing the zero-delay retry.
|
|
352
|
+
const delay = jitteredBackoffMs(consecutiveFailures - 1, { baseMs: baseBackoffMs, capMs: maxBackoffMs }, options?.rand);
|
|
353
|
+
await abortableDelay(delay, signal);
|
|
268
354
|
// Fetch latest state - updateInternal() will call replayActions() if there are conflicts
|
|
269
355
|
await this.updateInternal();
|
|
270
356
|
} else {
|
|
357
|
+
// Forward progress: reset the no-progress budget.
|
|
358
|
+
consecutiveFailures = 0;
|
|
359
|
+
lastReason = undefined;
|
|
271
360
|
// Clear the pending actions that were part of this action
|
|
272
361
|
this.pending = this.pending.slice(pending.length);
|
|
273
362
|
// Reset cache and replay any actions that were added during the action
|
|
274
363
|
const transforms = tracker.reset();
|
|
275
364
|
await this.replayActions();
|
|
276
|
-
this.sourceCache.transformCache(transforms);
|
|
365
|
+
this.sourceCache.transformCache(transforms, newRev);
|
|
277
366
|
this.source.actionContext = this.source.actionContext
|
|
278
367
|
? { committed: [...this.source.actionContext.committed, { actionId, rev: newRev }], rev: newRev }
|
|
279
368
|
: { committed: [{ actionId, rev: newRev }], rev: newRev };
|
|
@@ -281,11 +370,11 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
281
370
|
}
|
|
282
371
|
}
|
|
283
372
|
|
|
284
|
-
async updateAndSync() {
|
|
373
|
+
async updateAndSync(options?: SyncOptions) {
|
|
285
374
|
const release = await Latches.acquire(this.latchId);
|
|
286
375
|
try {
|
|
287
376
|
await this.updateInternal();
|
|
288
|
-
await this.syncInternal();
|
|
377
|
+
await this.syncInternal(options);
|
|
289
378
|
} finally {
|
|
290
379
|
release();
|
|
291
380
|
}
|
|
@@ -298,7 +387,9 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
298
387
|
}
|
|
299
388
|
for await (const entry of log.select(undefined, forward)) {
|
|
300
389
|
if (entry.action) {
|
|
301
|
-
|
|
390
|
+
// NOTE: copy-then-reverse to avoid mutating the stored log entry array.
|
|
391
|
+
// Once tsconfig targets ES2023, `entry.action.actions.toReversed()` is cleaner.
|
|
392
|
+
yield* forward ? entry.action.actions : [...entry.action.actions].reverse();
|
|
302
393
|
}
|
|
303
394
|
}
|
|
304
395
|
}
|
|
@@ -321,20 +412,12 @@ export class Collection<TAction> implements ICollection<TAction> {
|
|
|
321
412
|
|
|
322
413
|
/** Called for each local action that may be in conflict with a remote action (always called under latch).
|
|
323
414
|
* @param action - The local action to check
|
|
324
|
-
* @param potential - The remote
|
|
325
|
-
* @returns
|
|
415
|
+
* @param potential - The remote actions that are potentially in conflict
|
|
416
|
+
* @returns The effective action to keep: the original (unchanged), a replacement
|
|
417
|
+
* instance (applied instead of the original), or undefined to discard it.
|
|
326
418
|
*/
|
|
327
|
-
protected doFilterConflict(action: Action<TAction>, potential: Action<TAction>[]) {
|
|
328
|
-
|
|
329
|
-
const replacement = this.filterConflict(action, potential);
|
|
330
|
-
if (!replacement) {
|
|
331
|
-
return false;
|
|
332
|
-
} else if (replacement !== action) {
|
|
333
|
-
// Queue replacement - it will be applied in replayActions()
|
|
334
|
-
this.pending.push(replacement);
|
|
335
|
-
}
|
|
336
|
-
}
|
|
337
|
-
return true;
|
|
419
|
+
protected doFilterConflict(action: Action<TAction>, potential: Action<TAction>[]): Action<TAction> | undefined {
|
|
420
|
+
return this.filterConflict ? this.filterConflict(action, potential) : action;
|
|
338
421
|
}
|
|
339
422
|
|
|
340
423
|
/** Bootstrap ActionContext from the committed tail block's state.
|
package/src/collection/struct.ts
CHANGED
|
@@ -1,16 +1,58 @@
|
|
|
1
1
|
import type { IBlock, BlockId, Action } from "../index.js";
|
|
2
2
|
import type { IChainHeader } from "../chain/chain-nodes.js";
|
|
3
|
+
import type { RandFn } from "../utility/backoff.js";
|
|
3
4
|
|
|
4
5
|
export type CollectionId = BlockId;
|
|
5
6
|
|
|
6
7
|
export type CollectionHeaderBlock = IBlock & Partial<IChainHeader>;
|
|
7
8
|
|
|
9
|
+
/** Bounds the retry loop inside {@link ICollection.sync} / {@link ICollection.updateAndSync}
|
|
10
|
+
* so a transactor that keeps rejecting the sync can no longer spin the collection latch forever.
|
|
11
|
+
* All fields are optional; unset fields fall back to conservative defaults. */
|
|
12
|
+
export interface SyncOptions {
|
|
13
|
+
/** Max consecutive stale-failure retries that make no progress before giving up.
|
|
14
|
+
* The counter resets to 0 on every successful commit, so a legitimate large multi-batch
|
|
15
|
+
* sync (which iterates many times making forward progress) is never falsely tripped.
|
|
16
|
+
* Default 10. */
|
|
17
|
+
maxAttempts?: number;
|
|
18
|
+
/** Optional wall-clock deadline in ms measured from the start of the sync call. Independent
|
|
19
|
+
* of the attempt count — a progress-agnostic ceiling. Unset means no deadline. */
|
|
20
|
+
deadlineMs?: number;
|
|
21
|
+
/** Base backoff delay in ms applied before the first retry; subsequent retries grow the delay
|
|
22
|
+
* exponentially up to {@link maxBackoffMs}. Default 100. */
|
|
23
|
+
baseBackoffMs?: number;
|
|
24
|
+
/** Upper bound on any single backoff sleep, in ms. Default 5000. */
|
|
25
|
+
maxBackoffMs?: number;
|
|
26
|
+
/** Optional abort signal. Checked at the top of each loop iteration and raced against the
|
|
27
|
+
* backoff sleep, so an aborted sync rejects promptly (with an AbortError) rather than finishing
|
|
28
|
+
* the current sleep. */
|
|
29
|
+
signal?: AbortSignal;
|
|
30
|
+
/** Advanced/testing hook: source of uniform [0,1) randomness for the backoff jitter. Defaults to
|
|
31
|
+
* the package CSPRNG; inject a deterministic sequence to assert exact retry delays. */
|
|
32
|
+
rand?: RandFn;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/** Thrown by {@link ICollection.sync} / {@link ICollection.updateAndSync} when the retry budget
|
|
36
|
+
* (attempt count or deadline) is exhausted while the transactor keeps returning stale failures.
|
|
37
|
+
* Catchable so callers can surface a clear "gave up syncing" condition instead of hanging. */
|
|
38
|
+
export class SyncRetryExhaustedError extends Error {
|
|
39
|
+
constructor(
|
|
40
|
+
readonly collectionId: CollectionId,
|
|
41
|
+
readonly attempts: number,
|
|
42
|
+
readonly lastReason?: string,
|
|
43
|
+
) {
|
|
44
|
+
super(`sync for collection ${collectionId} exhausted ${attempts} retries` +
|
|
45
|
+
(lastReason ? `: ${lastReason}` : ''));
|
|
46
|
+
this.name = 'SyncRetryExhaustedError';
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
8
50
|
export interface ICollection<TAction> {
|
|
9
51
|
readonly id: CollectionId;
|
|
10
52
|
act(...actions: Action<TAction>[]): Promise<void>;
|
|
11
53
|
update(): Promise<void>;
|
|
12
|
-
sync(): Promise<void>;
|
|
13
|
-
updateAndSync(): Promise<void>;
|
|
54
|
+
sync(options?: SyncOptions): Promise<void>;
|
|
55
|
+
updateAndSync(options?: SyncOptions): Promise<void>;
|
|
14
56
|
selectLog(forward?: boolean): AsyncIterableIterator<Action<TAction>>;
|
|
15
57
|
}
|
|
16
58
|
|
|
@@ -36,20 +36,49 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
|
|
|
36
36
|
id: CollectionId,
|
|
37
37
|
keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
|
|
38
38
|
compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
|
|
39
|
+
/** B-tree node fan-out. Defaults to the BTree default (64). Exposed mainly so tests can
|
|
40
|
+
* force a multi-level tree with few entries (a small capacity), which is what exercises the
|
|
41
|
+
* interior-navigation read-exclusion path — with the default you would need thousands of
|
|
42
|
+
* entries before a descent has any interior branch between root and leaf.
|
|
43
|
+
*
|
|
44
|
+
* NOTE: fan-out is NOT persisted in the collection header — it is a per-call construction
|
|
45
|
+
* param. Creating a tree with a non-default capacity and later reopening it (a separate
|
|
46
|
+
* createOrOpen call) without the SAME capacity silently uses 64, so subsequent writes split
|
|
47
|
+
* at a fan-out the existing nodes were not built for. Fine today (only tests pass it, and
|
|
48
|
+
* they pass it consistently); if a persisted tree ever needs a custom fan-out, persist it in
|
|
49
|
+
* the header and read it back on reopen rather than trusting the caller to re-supply it. */
|
|
50
|
+
nodeCapacity?: number,
|
|
39
51
|
): Promise<Tree<TKey, TEntry>> {
|
|
40
52
|
// Tricky bootstrapping here:
|
|
41
53
|
// We need the root id to initialize the collection header, so we create the btree in the create collection header callback.
|
|
42
54
|
let btree: BTree<TKey, TEntry> | undefined;
|
|
43
55
|
const init: CollectionInitOptions<TreeReplaceAction<TKey, TEntry>> = {
|
|
44
56
|
modules: {
|
|
45
|
-
"replace": async ({ data: actions },
|
|
57
|
+
"replace": async ({ data: actions }, trx) => {
|
|
58
|
+
// Write through the Atomic store the handler is handed (`trx`), NOT the captured
|
|
59
|
+
// read btree, so `internalTransact`'s all-or-nothing wrapper actually governs this
|
|
60
|
+
// action: if any entry throws, `atomic.commit()` is skipped and every staged node
|
|
61
|
+
// write from this action is discarded (whole-action rollback) — identically for
|
|
62
|
+
// freshly created and reopened trees. Binding a throwaway BTree to `trx` reuses the
|
|
63
|
+
// public constructor; no btree API change needed.
|
|
64
|
+
const actionTree = new BTree<TKey, TEntry>(
|
|
65
|
+
trx,
|
|
66
|
+
new CollectionTrunk(trx, id),
|
|
67
|
+
keyFromEntry,
|
|
68
|
+
compare,
|
|
69
|
+
nodeCapacity, // keep the write btree's fan-out in lock-step with the read btree
|
|
70
|
+
);
|
|
46
71
|
for (const [key, entry] of actions) {
|
|
47
72
|
if (entry) {
|
|
48
|
-
await
|
|
73
|
+
await actionTree.upsert(entry);
|
|
49
74
|
} else {
|
|
50
|
-
await
|
|
75
|
+
await actionTree.deleteAt((await actionTree.find(key)));
|
|
51
76
|
}
|
|
52
77
|
}
|
|
78
|
+
// Mutations landed in `trx`, not the read btree, so its version counter never moved.
|
|
79
|
+
// Bump it to invalidate any Path a caller still holds — preserving the path-invalidation
|
|
80
|
+
// the previous in-place handler gave for free.
|
|
81
|
+
btree?.invalidatePaths();
|
|
53
82
|
}
|
|
54
83
|
},
|
|
55
84
|
createHeaderBlock: (id: BlockId, store: BlockStore<IBlock>) => { // Only called if the collection does not exist
|
|
@@ -57,7 +86,7 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
|
|
|
57
86
|
btree = BTree.create<TKey, TEntry>(store, (_s, r) => {
|
|
58
87
|
rootId = r;
|
|
59
88
|
return new CollectionTrunk(store, id);
|
|
60
|
-
}, keyFromEntry, compare);
|
|
89
|
+
}, keyFromEntry, compare, nodeCapacity);
|
|
61
90
|
return {
|
|
62
91
|
header: store.createBlockHeader(TreeHeaderBlockType, id),
|
|
63
92
|
rootId: rootId!,
|
|
@@ -66,7 +95,7 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
|
|
|
66
95
|
};
|
|
67
96
|
|
|
68
97
|
const collection = await Collection.createOrOpen<TreeReplaceAction<TKey, TEntry>>(network, id, init);
|
|
69
|
-
btree = btree ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare);
|
|
98
|
+
btree = btree ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare, nodeCapacity);
|
|
70
99
|
return new Tree<TKey, TEntry>(collection, btree, keyFromEntry, compare);
|
|
71
100
|
}
|
|
72
101
|
|
|
@@ -137,6 +166,14 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
|
|
|
137
166
|
return this.collection;
|
|
138
167
|
}
|
|
139
168
|
|
|
169
|
+
/** This tree's collection id, as a plain string. Used by consumers that flush
|
|
170
|
+
* several trees together (e.g. the Quereus adapter's legacy commit sweep) to
|
|
171
|
+
* name a specific tree in diagnostics when a partial flush leaves trees out of
|
|
172
|
+
* sync. Structurally satisfies the adapter's `DirtyTree.describe()`. */
|
|
173
|
+
describe(): string {
|
|
174
|
+
return String(this.collection.id);
|
|
175
|
+
}
|
|
176
|
+
|
|
140
177
|
/**
|
|
141
178
|
* Update the local state from the network.
|
|
142
179
|
* Call this before reading to ensure you have the latest data.
|
package/src/index.ts
CHANGED
package/src/log/log.ts
CHANGED
|
@@ -10,12 +10,32 @@ import type { GetFromResult } from './struct.js';
|
|
|
10
10
|
|
|
11
11
|
export type LogBlock<TAction> = ChainDataNode<LogEntry<TAction>>
|
|
12
12
|
& {
|
|
13
|
-
/** Base64url encoded Sha256 hash of the
|
|
13
|
+
/** Base64url encoded Sha256 hash of the prior (predecessor) block - present on every block except the head */
|
|
14
14
|
priorHash?: string,
|
|
15
15
|
};
|
|
16
16
|
|
|
17
17
|
export const priorHash$ = nameof<LogBlock<any>>("priorHash");
|
|
18
18
|
|
|
19
|
+
/**
|
|
20
|
+
* Canonical byte payload covered by {@link LogBlock.priorHash}: every field of a log block EXCEPT
|
|
21
|
+
* the mutable structural links (`nextId`/`priorId`). Those links are rewritten *after* the hash is
|
|
22
|
+
* taken — `nextId` when the following block is appended ({@link Chain.add}), and `priorId`/`nextId`
|
|
23
|
+
* when blocks are removed ({@link Chain.pop}/{@link Chain.dequeue}) — so hashing the raw block would
|
|
24
|
+
* not match the bytes that end up stored. The content-bearing fields (`header`, `entries`,
|
|
25
|
+
* `priorHash`) are covered. Any verifier that re-hashes a stored block to check `priorHash` MUST
|
|
26
|
+
* reproduce exactly this payload (strip `nextId` and `priorId`; hash the rest).
|
|
27
|
+
*
|
|
28
|
+
* NOTE: the caller hashes `JSON.stringify` of this object, which is key-order dependent and not a
|
|
29
|
+
* canonical encoding. Fine today (blocks are built with a stable key order and the in-process store
|
|
30
|
+
* preserves it), but if a block is ever re-serialized by a store/codec that reorders keys or drops
|
|
31
|
+
* `undefined`s differently, a re-hash won't match the recorded priorHash — switch to a canonical
|
|
32
|
+
* encoding (sorted keys / dag-cbor) before that happens.
|
|
33
|
+
*/
|
|
34
|
+
export function logBlockHashPayload<TAction>(block: LogBlock<TAction>) {
|
|
35
|
+
const { nextId, priorId, ...covered } = block;
|
|
36
|
+
return covered;
|
|
37
|
+
}
|
|
38
|
+
|
|
19
39
|
export class Log<TAction> {
|
|
20
40
|
protected constructor(
|
|
21
41
|
private readonly chain: Chain<LogEntry<TAction>>,
|
|
@@ -151,8 +171,9 @@ export class Log<TAction> {
|
|
|
151
171
|
|
|
152
172
|
/** Gets the actions from startRev (exclusive), to latest in the log. */
|
|
153
173
|
async getFrom(startRev: number | undefined): Promise<GetFromResult<TAction>> {
|
|
154
|
-
const
|
|
155
|
-
const
|
|
174
|
+
const entriesFromTail: ActionEntry<TAction>[] = [];
|
|
175
|
+
const pendingActions: ActionRev[] = [];
|
|
176
|
+
let checkpointPendings: ActionRev[] = [];
|
|
156
177
|
let rev: number | undefined;
|
|
157
178
|
let checkpointPath: ChainPath<LogEntry<TAction>> | undefined;
|
|
158
179
|
// Step through collecting both pending and entries until a checkpoint is found
|
|
@@ -161,7 +182,7 @@ export class Log<TAction> {
|
|
|
161
182
|
rev = rev ?? entry.rev;
|
|
162
183
|
if (entry.checkpoint) {
|
|
163
184
|
checkpointPath = path;
|
|
164
|
-
|
|
185
|
+
checkpointPendings = entry.checkpoint.pendings;
|
|
165
186
|
break;
|
|
166
187
|
}
|
|
167
188
|
// Invalidation entries take a rev slot but are not pending actions — skip them in
|
|
@@ -169,24 +190,27 @@ export class Log<TAction> {
|
|
|
169
190
|
if (!entry.action) {
|
|
170
191
|
continue;
|
|
171
192
|
}
|
|
172
|
-
|
|
193
|
+
pendingActions.push({ actionId: entry.action.actionId, rev: entry.rev });
|
|
173
194
|
if (startRev !== undefined && entry.rev > startRev) {
|
|
174
|
-
|
|
195
|
+
entriesFromTail.push(entry.action);
|
|
175
196
|
} // Can't stop at rev, because we need to collect all pending actions for the context
|
|
176
197
|
}
|
|
177
198
|
// Continue stepping past the checkpoint until the given rev is reached
|
|
199
|
+
const entriesFromCheckpoint: ActionEntry<TAction>[] = [];
|
|
178
200
|
if (checkpointPath) {
|
|
179
201
|
for await (const path of this.chain.select(checkpointPath, false)) {
|
|
180
202
|
const entry = entryAt<LogEntry<TAction>>(path)!;
|
|
181
203
|
if (startRev !== undefined && entry.rev > startRev) {
|
|
182
204
|
if (entry.action) {
|
|
183
|
-
|
|
205
|
+
entriesFromCheckpoint.push(entry.action);
|
|
184
206
|
}
|
|
185
207
|
} else {
|
|
186
208
|
break;
|
|
187
209
|
}
|
|
188
210
|
}
|
|
189
211
|
}
|
|
212
|
+
const pendings = [...checkpointPendings, ...pendingActions.reverse()];
|
|
213
|
+
const entries = [...entriesFromCheckpoint.reverse(), ...entriesFromTail.reverse()];
|
|
190
214
|
return { context: rev ? { committed: pendings, rev } : undefined, entries };
|
|
191
215
|
}
|
|
192
216
|
|
|
@@ -224,13 +248,17 @@ export class Log<TAction> {
|
|
|
224
248
|
return pendings;
|
|
225
249
|
}
|
|
226
250
|
|
|
227
|
-
|
|
251
|
+
/** Chain options used by every log: block type factories plus the priorHash `newBlock` hook.
|
|
252
|
+
* Exposed (not private) so tests can drive a raw {@link Chain} through the exact same hook. */
|
|
253
|
+
static getChainOptions<TAction>(store: BlockStore<IBlock>) {
|
|
228
254
|
return {
|
|
229
255
|
createDataBlock: () => ({ header: store.createBlockHeader(LogDataBlockType) }),
|
|
230
256
|
createHeaderBlock: (id?: BlockId) => ({ header: store.createBlockHeader(LogHeaderBlockType, id) }),
|
|
231
257
|
newBlock: async (newTail: LogBlock<TAction>, oldTail: LogBlock<TAction> | undefined) => {
|
|
232
258
|
if (oldTail) {
|
|
233
|
-
|
|
259
|
+
// Hash the canonical payload (excludes the mutable nextId/priorId links) so the
|
|
260
|
+
// recorded priorHash matches a later re-hash of the stored predecessor block.
|
|
261
|
+
const hash = await sha256.digest(new TextEncoder().encode(JSON.stringify(logBlockHashPayload(oldTail))));
|
|
234
262
|
newTail.priorHash = uint8ArrayToString(hash.digest, 'base64url');
|
|
235
263
|
}
|
|
236
264
|
},
|
package/src/log/struct.ts
CHANGED
|
@@ -105,10 +105,11 @@ export type DisputeResolutionProof = {
|
|
|
105
105
|
*/
|
|
106
106
|
readonly challengerPeerId: string;
|
|
107
107
|
/**
|
|
108
|
-
* The legitimately-selected arbitrator set — the K peer-ids the challenger computed via
|
|
109
|
-
*
|
|
110
|
-
*
|
|
111
|
-
*
|
|
108
|
+
* The legitimately-selected arbitrator set — the K peer-ids the challenger computed via `sampleArbitrators`
|
|
109
|
+
* (verifiable dispersed sampling: the peers nearest K pseudo-random ring coordinates derived from
|
|
110
|
+
* `hash(blockId ‖ round ‖ epoch ‖ i)`, spread across the whole keyspace rather than drawn from the disputed
|
|
111
|
+
* block's neighborhood). Every counted vote's `arbitratorPeerId` must be a member; the set is bound to the
|
|
112
|
+
* originator by {@link arbitratorSetSignature} and, when feasible, re-derived from the verifying member's topology.
|
|
112
113
|
*/
|
|
113
114
|
readonly arbitratorSet: ReadonlyArray<string>;
|
|
114
115
|
/**
|