@optimystic/db-core 0.21.0 → 0.24.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 +336 -336
- package/dist/src/btree/btree.d.ts +2 -1
- package/dist/src/btree/btree.d.ts.map +1 -1
- package/dist/src/btree/btree.js +1 -1
- package/dist/src/btree/btree.js.map +1 -1
- package/dist/src/chain/chain.d.ts +1 -1
- package/dist/src/chain/chain.d.ts.map +1 -1
- package/dist/src/chain/chain.js +1 -1
- package/dist/src/chain/chain.js.map +1 -1
- package/dist/src/cluster/structs.d.ts +39 -1
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +24 -0
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/collection/collection.d.ts +20 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +31 -4
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +2 -1
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/diary/struct.js +1 -1
- package/dist/src/collections/diary/struct.js.map +1 -1
- package/dist/src/collections/tree/collection-trunk.js +1 -1
- package/dist/src/collections/tree/collection-trunk.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +1 -1
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +2 -1
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +5 -0
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +7 -0
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/log/log.d.ts +1 -1
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +2 -2
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/network/i-peer-network.d.ts +16 -0
- package/dist/src/network/i-peer-network.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +39 -2
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js +18 -0
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +95 -8
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +133 -12
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +2 -1
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/transaction.d.ts +1 -1
- package/dist/src/transaction/transaction.js +1 -1
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +57 -18
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +25 -2
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transform/cache-source.js +1 -1
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -1
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +7 -6
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +2 -1
- package/dist/src/transform/tracker.js.map +1 -1
- package/package.json +1 -1
- package/src/btree/btree.ts +2 -1
- package/src/chain/chain.ts +2 -1
- package/src/cluster/membership.ts +85 -85
- package/src/cluster/structs.ts +43 -4
- package/src/cohort-topic/addressing.ts +120 -120
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -253
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +106 -106
- package/src/cohort-topic/antidos/index.ts +5 -5
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -210
- package/src/cohort-topic/antidos/replay-guard.ts +146 -146
- package/src/cohort-topic/antidos/topic-budget.ts +160 -160
- package/src/cohort-topic/antiflood/index.ts +2 -2
- package/src/cohort-topic/antiflood/invariants.ts +108 -108
- package/src/cohort-topic/antiflood/jitter.ts +117 -117
- package/src/cohort-topic/coldstart.ts +237 -237
- package/src/cohort-topic/dmax.ts +88 -88
- package/src/cohort-topic/gossip/bus.ts +254 -254
- package/src/cohort-topic/gossip/index.ts +3 -3
- package/src/cohort-topic/gossip/records.ts +45 -45
- package/src/cohort-topic/gossip/view.ts +91 -91
- package/src/cohort-topic/index.ts +20 -20
- package/src/cohort-topic/load/barometer.ts +134 -134
- package/src/cohort-topic/load/index.ts +1 -1
- package/src/cohort-topic/member-engine.ts +430 -430
- package/src/cohort-topic/membership/index.ts +3 -3
- package/src/cohort-topic/membership/publisher.ts +163 -163
- package/src/cohort-topic/membership/source.ts +41 -41
- package/src/cohort-topic/membership/verifier.ts +461 -461
- package/src/cohort-topic/ports.ts +157 -157
- package/src/cohort-topic/promotion.ts +405 -405
- package/src/cohort-topic/registration/bytes.ts +37 -37
- package/src/cohort-topic/registration/handoff.ts +154 -154
- package/src/cohort-topic/registration/index.ts +6 -6
- package/src/cohort-topic/registration/renewal.ts +495 -495
- package/src/cohort-topic/registration/sharding.ts +61 -61
- package/src/cohort-topic/registration/store.ts +81 -81
- package/src/cohort-topic/registration/types.ts +91 -91
- package/src/cohort-topic/ring-hash.ts +50 -50
- package/src/cohort-topic/service.ts +416 -416
- package/src/cohort-topic/sig/index.ts +2 -2
- package/src/cohort-topic/sig/payloads.ts +59 -59
- package/src/cohort-topic/sig/threshold.ts +64 -64
- package/src/cohort-topic/tiers.ts +74 -74
- package/src/cohort-topic/traffic.ts +233 -233
- package/src/cohort-topic/walk.ts +326 -326
- package/src/cohort-topic/willingness.ts +237 -237
- package/src/cohort-topic/wire/codec.ts +216 -216
- package/src/cohort-topic/wire/index.ts +18 -18
- package/src/cohort-topic/wire/payloads.ts +126 -126
- package/src/cohort-topic/wire/primitives.ts +188 -188
- package/src/cohort-topic/wire/types.ts +475 -475
- package/src/cohort-topic/wire/validate.ts +512 -512
- package/src/collection/collection-type-registry.ts +37 -37
- package/src/collection/collection.ts +32 -4
- package/src/collections/diary/diary.ts +68 -67
- package/src/collections/diary/struct.ts +1 -1
- package/src/collections/tree/collection-trunk.ts +1 -1
- package/src/collections/tree/readme.md +4 -0
- package/src/collections/tree/struct.ts +3 -1
- package/src/collections/tree/tree.ts +320 -312
- package/src/log/log.ts +2 -2
- package/src/matchmaking/capability-filter.ts +45 -45
- package/src/matchmaking/config.ts +98 -98
- package/src/matchmaking/index.ts +21 -21
- package/src/matchmaking/multi-cohort-seeker.ts +234 -234
- package/src/matchmaking/provider.ts +123 -123
- package/src/matchmaking/query-eval.ts +105 -105
- package/src/matchmaking/seeker-walk.ts +127 -127
- package/src/matchmaking/seeker.ts +86 -86
- package/src/matchmaking/topic-anchor.ts +90 -90
- package/src/matchmaking/voting-quorum.ts +394 -394
- package/src/matchmaking/wire.ts +603 -603
- package/src/network/i-peer-network.ts +17 -0
- package/src/network/stale-failure.ts +43 -43
- package/src/network/struct.ts +41 -2
- package/src/network/types.ts +37 -37
- package/src/reactivity/backfill.ts +220 -220
- package/src/reactivity/backpressure.ts +191 -191
- package/src/reactivity/checkpoint.ts +308 -308
- package/src/reactivity/config.ts +172 -172
- package/src/reactivity/dedupe.ts +132 -132
- package/src/reactivity/forwarder.ts +87 -87
- package/src/reactivity/index.ts +34 -34
- package/src/reactivity/notification.ts +123 -123
- package/src/reactivity/policy.ts +79 -79
- package/src/reactivity/push-state.ts +310 -310
- package/src/reactivity/recover.ts +153 -153
- package/src/reactivity/replay-buffer.ts +141 -141
- package/src/reactivity/resume.ts +549 -549
- package/src/reactivity/rotation.ts +415 -415
- package/src/reactivity/subscriber.ts +132 -132
- package/src/reactivity/subscription.ts +66 -66
- package/src/reactivity/topic-anchor.ts +71 -71
- package/src/reactivity/verify.ts +73 -73
- package/src/reactivity/wire-validate.ts +13 -13
- package/src/reactivity/wire.ts +224 -224
- package/src/testing/async-wait.ts +65 -65
- package/src/testing/index.ts +2 -2
- package/src/testing/test-transactor.ts +638 -489
- package/src/transaction/coordinator.ts +2 -1
- package/src/transaction/errors.ts +91 -91
- package/src/transaction/operations-hash.ts +196 -196
- package/src/transaction/read-dependency-collector.ts +78 -78
- package/src/transaction/transaction.ts +1 -1
- package/src/transactor/change-notifier.ts +80 -80
- package/src/transactor/index.ts +5 -5
- package/src/transactor/network-transactor.ts +58 -19
- package/src/transactor/transactor-source.ts +25 -2
- package/src/transform/atomic-proxy.ts +92 -92
- package/src/transform/cache-source.ts +1 -1
- package/src/transform/helpers.ts +159 -158
- package/src/transform/tracker.ts +2 -1
- package/src/utility/backoff.ts +95 -95
- package/src/utility/batch-coordinator.ts +191 -191
- package/dist/src/transaction/context.d.ts +0 -60
- package/dist/src/transaction/context.d.ts.map +0 -1
- package/dist/src/transaction/context.js +0 -91
- package/dist/src/transaction/context.js.map +0 -1
|
@@ -1,78 +1,78 @@
|
|
|
1
|
-
import type { BlockId, ReadPurpose } from "../blocks/index.js";
|
|
2
|
-
import type { ReadDependency } from "./transaction.js";
|
|
3
|
-
|
|
4
|
-
/** One captured read: the highest revision observed for the block, plus its {@link ReadPurpose}. */
|
|
5
|
-
type ReadEntry = { revision: number; purpose: ReadPurpose };
|
|
6
|
-
|
|
7
|
-
/** Accumulates the read dependencies of one transaction. Keyed by block id. For each id it keeps:
|
|
8
|
-
*
|
|
9
|
-
* - the HIGHEST revision observed (never downgrades — a re-read from cache must not overwrite a
|
|
10
|
-
* higher revision seen earlier), and
|
|
11
|
-
* - the read's PURPOSE with value-wins semantics: once a block is recorded as a `value` read from
|
|
12
|
-
* ANY path it stays `value`; `navigation` sticks only for a block that was never read as a value.
|
|
13
|
-
* This is the exact analogue of the max-wins rule on revision, and it makes the classification
|
|
14
|
-
* order-independent (so it is deterministic regardless of cache/timing — required for the
|
|
15
|
-
* transaction id and client signature to match across a re-executing validator; Theorem 4).
|
|
16
|
-
*
|
|
17
|
-
* {@link getReadDependencies} returns only the CONFLICT set (value reads); purely-structural
|
|
18
|
-
* `navigation` reads — interior B-tree branches walked through to reach a captured leaf — are
|
|
19
|
-
* dropped. See {@link markValue} and docs/correctness.md Theorem 5.
|
|
20
|
-
*
|
|
21
|
-
* One instance is shared by a collection's {@link TransactorSource} (direct structural reads —
|
|
22
|
-
* bootstrap, header) and its {@link CacheSource} (every cache hit/miss), so a block read from
|
|
23
|
-
* either layer produces a dependency. Because both feed the same collector, a cache miss records
|
|
24
|
-
* the id once from each layer at the same revision/purpose; the merge collapses those to one
|
|
25
|
-
* entry. Cleared at each txn boundary. */
|
|
26
|
-
export class ReadDependencyCollector {
|
|
27
|
-
private reads = new Map<BlockId, ReadEntry>();
|
|
28
|
-
|
|
29
|
-
/** Record a read of `blockId` at `revision`. `purpose` defaults to `value` (retained); pass
|
|
30
|
-
* `navigation` for an interior structural read that a later {@link markValue} may or may not
|
|
31
|
-
* upgrade. Revision is max-wins; purpose is value-wins (see class doc). */
|
|
32
|
-
record(blockId: BlockId, revision: number, purpose: ReadPurpose = 'value'): void {
|
|
33
|
-
const prev = this.reads.get(blockId);
|
|
34
|
-
if (prev === undefined) {
|
|
35
|
-
this.reads.set(blockId, { revision, purpose });
|
|
36
|
-
return;
|
|
37
|
-
}
|
|
38
|
-
if (revision > prev.revision) {
|
|
39
|
-
prev.revision = revision;
|
|
40
|
-
}
|
|
41
|
-
// value-wins: a value read anywhere pins the block as value; navigation never downgrades it.
|
|
42
|
-
if (purpose === 'value') {
|
|
43
|
-
prev.purpose = 'value';
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
/** Upgrade an already-recorded read to `value` (keeping its revision), retaining it in the
|
|
48
|
-
* conflict set. The B-tree point-lookup path uses this to mark the terminal leaf — the
|
|
49
|
-
* load-bearing read whose content the result depends on — after its interior descent nodes
|
|
50
|
-
* were recorded as `navigation`. No-op if the id was never recorded (e.g. a leaf served from
|
|
51
|
-
* an uncommitted staged insert, which records no dependency at all). */
|
|
52
|
-
markValue(blockId: BlockId): void {
|
|
53
|
-
const prev = this.reads.get(blockId);
|
|
54
|
-
if (prev) {
|
|
55
|
-
prev.purpose = 'value';
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
|
|
59
|
-
/** The optimistic-concurrency conflict (read) set: every `value` read, with `navigation`
|
|
60
|
-
* reads excluded. Dropping a covered navigation read cannot admit a lost update — any
|
|
61
|
-
* concurrent change to the queried result also bumps a retained value read (the target
|
|
62
|
-
* leaf) — but it removes the false-positive stale rejections that structural block reads
|
|
63
|
-
* otherwise caused (Theorem 5 Bound). Deterministic in query shape, so a coordinator and a
|
|
64
|
-
* re-executing validator derive the identical set. */
|
|
65
|
-
getReadDependencies(): ReadDependency[] {
|
|
66
|
-
const result: ReadDependency[] = [];
|
|
67
|
-
for (const [blockId, entry] of this.reads) {
|
|
68
|
-
if (entry.purpose === 'value') {
|
|
69
|
-
result.push({ blockId, revision: entry.revision });
|
|
70
|
-
}
|
|
71
|
-
}
|
|
72
|
-
return result;
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
clear(): void {
|
|
76
|
-
this.reads.clear();
|
|
77
|
-
}
|
|
78
|
-
}
|
|
1
|
+
import type { BlockId, ReadPurpose } from "../blocks/index.js";
|
|
2
|
+
import type { ReadDependency } from "./transaction.js";
|
|
3
|
+
|
|
4
|
+
/** One captured read: the highest revision observed for the block, plus its {@link ReadPurpose}. */
|
|
5
|
+
type ReadEntry = { revision: number; purpose: ReadPurpose };
|
|
6
|
+
|
|
7
|
+
/** Accumulates the read dependencies of one transaction. Keyed by block id. For each id it keeps:
|
|
8
|
+
*
|
|
9
|
+
* - the HIGHEST revision observed (never downgrades — a re-read from cache must not overwrite a
|
|
10
|
+
* higher revision seen earlier), and
|
|
11
|
+
* - the read's PURPOSE with value-wins semantics: once a block is recorded as a `value` read from
|
|
12
|
+
* ANY path it stays `value`; `navigation` sticks only for a block that was never read as a value.
|
|
13
|
+
* This is the exact analogue of the max-wins rule on revision, and it makes the classification
|
|
14
|
+
* order-independent (so it is deterministic regardless of cache/timing — required for the
|
|
15
|
+
* transaction id and client signature to match across a re-executing validator; Theorem 4).
|
|
16
|
+
*
|
|
17
|
+
* {@link getReadDependencies} returns only the CONFLICT set (value reads); purely-structural
|
|
18
|
+
* `navigation` reads — interior B-tree branches walked through to reach a captured leaf — are
|
|
19
|
+
* dropped. See {@link markValue} and docs/correctness.md Theorem 5.
|
|
20
|
+
*
|
|
21
|
+
* One instance is shared by a collection's {@link TransactorSource} (direct structural reads —
|
|
22
|
+
* bootstrap, header) and its {@link CacheSource} (every cache hit/miss), so a block read from
|
|
23
|
+
* either layer produces a dependency. Because both feed the same collector, a cache miss records
|
|
24
|
+
* the id once from each layer at the same revision/purpose; the merge collapses those to one
|
|
25
|
+
* entry. Cleared at each txn boundary. */
|
|
26
|
+
export class ReadDependencyCollector {
|
|
27
|
+
private reads = new Map<BlockId, ReadEntry>();
|
|
28
|
+
|
|
29
|
+
/** Record a read of `blockId` at `revision`. `purpose` defaults to `value` (retained); pass
|
|
30
|
+
* `navigation` for an interior structural read that a later {@link markValue} may or may not
|
|
31
|
+
* upgrade. Revision is max-wins; purpose is value-wins (see class doc). */
|
|
32
|
+
record(blockId: BlockId, revision: number, purpose: ReadPurpose = 'value'): void {
|
|
33
|
+
const prev = this.reads.get(blockId);
|
|
34
|
+
if (prev === undefined) {
|
|
35
|
+
this.reads.set(blockId, { revision, purpose });
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
if (revision > prev.revision) {
|
|
39
|
+
prev.revision = revision;
|
|
40
|
+
}
|
|
41
|
+
// value-wins: a value read anywhere pins the block as value; navigation never downgrades it.
|
|
42
|
+
if (purpose === 'value') {
|
|
43
|
+
prev.purpose = 'value';
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Upgrade an already-recorded read to `value` (keeping its revision), retaining it in the
|
|
48
|
+
* conflict set. The B-tree point-lookup path uses this to mark the terminal leaf — the
|
|
49
|
+
* load-bearing read whose content the result depends on — after its interior descent nodes
|
|
50
|
+
* were recorded as `navigation`. No-op if the id was never recorded (e.g. a leaf served from
|
|
51
|
+
* an uncommitted staged insert, which records no dependency at all). */
|
|
52
|
+
markValue(blockId: BlockId): void {
|
|
53
|
+
const prev = this.reads.get(blockId);
|
|
54
|
+
if (prev) {
|
|
55
|
+
prev.purpose = 'value';
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** The optimistic-concurrency conflict (read) set: every `value` read, with `navigation`
|
|
60
|
+
* reads excluded. Dropping a covered navigation read cannot admit a lost update — any
|
|
61
|
+
* concurrent change to the queried result also bumps a retained value read (the target
|
|
62
|
+
* leaf) — but it removes the false-positive stale rejections that structural block reads
|
|
63
|
+
* otherwise caused (Theorem 5 Bound). Deterministic in query shape, so a coordinator and a
|
|
64
|
+
* re-executing validator derive the identical set. */
|
|
65
|
+
getReadDependencies(): ReadDependency[] {
|
|
66
|
+
const result: ReadDependency[] = [];
|
|
67
|
+
for (const [blockId, entry] of this.reads) {
|
|
68
|
+
if (entry.purpose === 'value') {
|
|
69
|
+
result.push({ blockId, revision: entry.revision });
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
return result;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
clear(): void {
|
|
76
|
+
this.reads.clear();
|
|
77
|
+
}
|
|
78
|
+
}
|
|
@@ -128,7 +128,7 @@ export const DEFAULT_TRANSACTION_TTL_MS = 30_000;
|
|
|
128
128
|
* Upper bound on a transaction's aged {@link Transaction.priority}. Capping is essential: it
|
|
129
129
|
* bounds the fairness effect (an aged transaction cannot accrue unlimited advantage) and bounds
|
|
130
130
|
* the throughput cost (it can impose at most `MaxPriority` extra race-losses on rivals before it
|
|
131
|
-
* commits). Two transactions capped out at `MaxPriority` fall back to the existing
|
|
131
|
+
* commits). Two transactions capped out at `MaxPriority` fall back to the existing approval-count /
|
|
132
132
|
* message-hash tiebreak, so behavior degrades gracefully to the pre-priority status quo.
|
|
133
133
|
*/
|
|
134
134
|
export const MaxPriority = 8;
|
|
@@ -1,80 +1,80 @@
|
|
|
1
|
-
import type { BlockId, CollectionId } from '../index.js';
|
|
2
|
-
import type { ActionId } from '../collection/action.js';
|
|
3
|
-
|
|
4
|
-
/** A commit landed on this node mutating one collection's blocks. */
|
|
5
|
-
export type CollectionChangeEvent = {
|
|
6
|
-
/** Header/collection id of the affected collection (block.header.collectionId). */
|
|
7
|
-
readonly collectionId: CollectionId;
|
|
8
|
-
/** Blocks within that collection mutated by this commit. */
|
|
9
|
-
readonly blockIds: readonly BlockId[];
|
|
10
|
-
readonly actionId: ActionId;
|
|
11
|
-
readonly rev: number;
|
|
12
|
-
/**
|
|
13
|
-
* The collection's chain tail block id at the time of this commit (the `CommitRequest.tailId`).
|
|
14
|
-
* Anchors the rotating reactivity topic `H(tailId ‖ "reactivity")`. Present on commit-driven
|
|
15
|
-
* events; `undefined` on read-driven promotions (the `StorageRepo.get` path has no commit tail —
|
|
16
|
-
* those never originate anyway, and are cert-gated out downstream).
|
|
17
|
-
*/
|
|
18
|
-
readonly tailId?: BlockId;
|
|
19
|
-
/**
|
|
20
|
-
* `true` iff this change event is a durable **invalidation** — a compensating revision + appended
|
|
21
|
-
* `InvalidationEntry` reversing a previously-committed action proven invalid by dispute
|
|
22
|
-
* (`docs/right-is-right.md` §Durable Invalidation), rather than an ordinary commit. An invalidation
|
|
23
|
-
* is a committed collection change like any other, so it flows through the same notification path;
|
|
24
|
-
* this flag lets a subscriber distinguish it (drop derived results + resubmit) from a plain commit
|
|
25
|
-
* (refresh). A hint only — correctness never depends on it: the subscriber always re-reads the
|
|
26
|
-
* authoritative reverted state. Absent (falsy) on ordinary commits.
|
|
27
|
-
*/
|
|
28
|
-
readonly invalidation?: boolean;
|
|
29
|
-
/**
|
|
30
|
-
* When {@link invalidation} is set, the `actionId` of the original committed action that was
|
|
31
|
-
* reversed (the `InvalidationEntry.invalidatedActionId`). Lets an invalidation-aware client
|
|
32
|
-
* dedup/coalesce multiple cascade notifications by the action they reverse, and resubmit exactly
|
|
33
|
-
* the affected work. Absent on ordinary commits.
|
|
34
|
-
*/
|
|
35
|
-
readonly invalidatedActionId?: ActionId;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
export type CollectionChangeListener = (event: CollectionChangeEvent) => void;
|
|
39
|
-
|
|
40
|
-
/**
|
|
41
|
-
* The cluster-consensus commit certificate for a committed action — the **authoritative** proof
|
|
42
|
-
* that the cohort agreed the commit. It is the cross-package currency the local change-notifier
|
|
43
|
-
* bridge forwards into the cohort-topic substrate (`CohortTopicService.onLocalCommit`) so reactivity
|
|
44
|
-
* can originate notifications **without re-signing**: a notification's signature is bit-for-bit this
|
|
45
|
-
* `thresholdSig`. The bridge and every downstream consumer treat the bytes as opaque and pass them
|
|
46
|
-
* through unchanged — only the cluster layer that produced consensus ever mints one.
|
|
47
|
-
*
|
|
48
|
-
* Peer ids are carried as their string form (the cluster keys signatures by peer-id string); db-core
|
|
49
|
-
* stays cross-platform and never references a libp2p `PeerId` type here.
|
|
50
|
-
*/
|
|
51
|
-
export type CommitCert = {
|
|
52
|
-
/** Threshold signature bytes proving `signers` agreed the commit. Forwarded UNCHANGED; never re-signed. */
|
|
53
|
-
readonly thresholdSig: Uint8Array;
|
|
54
|
-
/** Peer-id strings whose signatures compose {@link thresholdSig} (a distinct set of size ≥ {@link minSigs}). */
|
|
55
|
-
readonly signers: readonly string[];
|
|
56
|
-
/** Threshold the signer set satisfies (the cluster super-majority / cohort `k − x`). */
|
|
57
|
-
readonly minSigs: number;
|
|
58
|
-
/**
|
|
59
|
-
* The exact byte preimage each `signers[i]` signed to produce its 64-byte chunk of
|
|
60
|
-
* {@link thresholdSig} — the cluster's per-member commit-vote payload `utf8(commitHash + ":approve")`,
|
|
61
|
-
* identical across all approving signers. Reactivity sets a notification's `digest` to
|
|
62
|
-
* base64url(signedPayload) so a subscriber's threshold-verify over `digest` reproduces the exact
|
|
63
|
-
* signed image. Opaque to db-core; minted only by the cluster layer.
|
|
64
|
-
*/
|
|
65
|
-
readonly signedPayload: Uint8Array;
|
|
66
|
-
};
|
|
67
|
-
|
|
68
|
-
export interface IBlockChangeNotifier {
|
|
69
|
-
/**
|
|
70
|
-
* Subscribe to commits that mutate the given collection. Returns an
|
|
71
|
-
* idempotent unsubscribe. Listeners are invoked AFTER the commit's critical
|
|
72
|
-
* section (locks released), synchronously in commit order; a throwing
|
|
73
|
-
* listener must not break the commit or other listeners (log + continue).
|
|
74
|
-
*/
|
|
75
|
-
onCollectionChange(collectionId: CollectionId, listener: CollectionChangeListener): () => void;
|
|
76
|
-
}
|
|
77
|
-
|
|
78
|
-
export function isBlockChangeNotifier(x: unknown): x is IBlockChangeNotifier {
|
|
79
|
-
return !!x && typeof (x as IBlockChangeNotifier).onCollectionChange === 'function';
|
|
80
|
-
}
|
|
1
|
+
import type { BlockId, CollectionId } from '../index.js';
|
|
2
|
+
import type { ActionId } from '../collection/action.js';
|
|
3
|
+
|
|
4
|
+
/** A commit landed on this node mutating one collection's blocks. */
|
|
5
|
+
export type CollectionChangeEvent = {
|
|
6
|
+
/** Header/collection id of the affected collection (block.header.collectionId). */
|
|
7
|
+
readonly collectionId: CollectionId;
|
|
8
|
+
/** Blocks within that collection mutated by this commit. */
|
|
9
|
+
readonly blockIds: readonly BlockId[];
|
|
10
|
+
readonly actionId: ActionId;
|
|
11
|
+
readonly rev: number;
|
|
12
|
+
/**
|
|
13
|
+
* The collection's chain tail block id at the time of this commit (the `CommitRequest.tailId`).
|
|
14
|
+
* Anchors the rotating reactivity topic `H(tailId ‖ "reactivity")`. Present on commit-driven
|
|
15
|
+
* events; `undefined` on read-driven promotions (the `StorageRepo.get` path has no commit tail —
|
|
16
|
+
* those never originate anyway, and are cert-gated out downstream).
|
|
17
|
+
*/
|
|
18
|
+
readonly tailId?: BlockId;
|
|
19
|
+
/**
|
|
20
|
+
* `true` iff this change event is a durable **invalidation** — a compensating revision + appended
|
|
21
|
+
* `InvalidationEntry` reversing a previously-committed action proven invalid by dispute
|
|
22
|
+
* (`docs/right-is-right.md` §Durable Invalidation), rather than an ordinary commit. An invalidation
|
|
23
|
+
* is a committed collection change like any other, so it flows through the same notification path;
|
|
24
|
+
* this flag lets a subscriber distinguish it (drop derived results + resubmit) from a plain commit
|
|
25
|
+
* (refresh). A hint only — correctness never depends on it: the subscriber always re-reads the
|
|
26
|
+
* authoritative reverted state. Absent (falsy) on ordinary commits.
|
|
27
|
+
*/
|
|
28
|
+
readonly invalidation?: boolean;
|
|
29
|
+
/**
|
|
30
|
+
* When {@link invalidation} is set, the `actionId` of the original committed action that was
|
|
31
|
+
* reversed (the `InvalidationEntry.invalidatedActionId`). Lets an invalidation-aware client
|
|
32
|
+
* dedup/coalesce multiple cascade notifications by the action they reverse, and resubmit exactly
|
|
33
|
+
* the affected work. Absent on ordinary commits.
|
|
34
|
+
*/
|
|
35
|
+
readonly invalidatedActionId?: ActionId;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
export type CollectionChangeListener = (event: CollectionChangeEvent) => void;
|
|
39
|
+
|
|
40
|
+
/**
|
|
41
|
+
* The cluster-consensus commit certificate for a committed action — the **authoritative** proof
|
|
42
|
+
* that the cohort agreed the commit. It is the cross-package currency the local change-notifier
|
|
43
|
+
* bridge forwards into the cohort-topic substrate (`CohortTopicService.onLocalCommit`) so reactivity
|
|
44
|
+
* can originate notifications **without re-signing**: a notification's signature is bit-for-bit this
|
|
45
|
+
* `thresholdSig`. The bridge and every downstream consumer treat the bytes as opaque and pass them
|
|
46
|
+
* through unchanged — only the cluster layer that produced consensus ever mints one.
|
|
47
|
+
*
|
|
48
|
+
* Peer ids are carried as their string form (the cluster keys signatures by peer-id string); db-core
|
|
49
|
+
* stays cross-platform and never references a libp2p `PeerId` type here.
|
|
50
|
+
*/
|
|
51
|
+
export type CommitCert = {
|
|
52
|
+
/** Threshold signature bytes proving `signers` agreed the commit. Forwarded UNCHANGED; never re-signed. */
|
|
53
|
+
readonly thresholdSig: Uint8Array;
|
|
54
|
+
/** Peer-id strings whose signatures compose {@link thresholdSig} (a distinct set of size ≥ {@link minSigs}). */
|
|
55
|
+
readonly signers: readonly string[];
|
|
56
|
+
/** Threshold the signer set satisfies (the cluster super-majority / cohort `k − x`). */
|
|
57
|
+
readonly minSigs: number;
|
|
58
|
+
/**
|
|
59
|
+
* The exact byte preimage each `signers[i]` signed to produce its 64-byte chunk of
|
|
60
|
+
* {@link thresholdSig} — the cluster's per-member commit-vote payload `utf8(commitHash + ":approve")`,
|
|
61
|
+
* identical across all approving signers. Reactivity sets a notification's `digest` to
|
|
62
|
+
* base64url(signedPayload) so a subscriber's threshold-verify over `digest` reproduces the exact
|
|
63
|
+
* signed image. Opaque to db-core; minted only by the cluster layer.
|
|
64
|
+
*/
|
|
65
|
+
readonly signedPayload: Uint8Array;
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export interface IBlockChangeNotifier {
|
|
69
|
+
/**
|
|
70
|
+
* Subscribe to commits that mutate the given collection. Returns an
|
|
71
|
+
* idempotent unsubscribe. Listeners are invoked AFTER the commit's critical
|
|
72
|
+
* section (locks released), synchronously in commit order; a throwing
|
|
73
|
+
* listener must not break the commit or other listeners (log + continue).
|
|
74
|
+
*/
|
|
75
|
+
onCollectionChange(collectionId: CollectionId, listener: CollectionChangeListener): () => void;
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
export function isBlockChangeNotifier(x: unknown): x is IBlockChangeNotifier {
|
|
79
|
+
return !!x && typeof (x as IBlockChangeNotifier).onCollectionChange === 'function';
|
|
80
|
+
}
|
package/src/transactor/index.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export * from "./change-notifier.js";
|
|
2
|
-
export * from "./network-transactor.js";
|
|
3
|
-
export * from "./transactor-source.js";
|
|
4
|
-
export * from "./transactor.js";
|
|
5
|
-
|
|
1
|
+
export * from "./change-notifier.js";
|
|
2
|
+
export * from "./network-transactor.js";
|
|
3
|
+
export * from "./transactor-source.js";
|
|
4
|
+
export * from "./transactor.js";
|
|
5
|
+
|
|
@@ -1,10 +1,15 @@
|
|
|
1
1
|
import { peerIdFromString } from "../network/types.js";
|
|
2
2
|
import type { PeerId } from "../network/types.js";
|
|
3
3
|
import { highestStaleAt, isConflictFailure } from "../network/stale-failure.js";
|
|
4
|
-
import { BlockUnavailableError } from "../network/struct.js";
|
|
5
|
-
import type { ActionTransforms, ActionBlocks, BlockActionStatus, ITransactor, PendSuccess, StaleFailure, IKeyNetwork, BlockId, GetBlockResults, PendResult, CommitResult, PendRequest, IRepo, BlockGets, Transforms, CommitRequest, ActionId, RepoCommitRequest, ClusterNomineesResult, CollectionId, IBlock, CoordinatorIntent } from "../index.js";
|
|
4
|
+
import { BlockUnavailableError, BlockPossiblyStaleError } from "../network/struct.js";
|
|
5
|
+
import type { ActionTransforms, ActionBlocks, BlockActionStatus, ITransactor, PendSuccess, StaleFailure, IKeyNetwork, BlockId, GetBlockResults, PendResult, CommitResult, PendRequest, IRepo, BlockGets, Transforms, CommitRequest, ActionId, RepoCommitRequest, ClusterNomineesResult, CollectionId, IBlock, CoordinatorIntent, BlockUnavailableReason } from "../index.js";
|
|
6
6
|
import type { IBlockChangeNotifier, CollectionChangeListener } from "./change-notifier.js";
|
|
7
|
-
import { transformForBlockId,
|
|
7
|
+
import { transformForBlockId, concatTransforms, concatTransform, transformsFromTransform, blockIdsForTransforms } from "../transform/helpers.js";
|
|
8
|
+
import { Tracker } from "../transform/tracker.js";
|
|
9
|
+
import { CacheSource } from "../transform/cache-source.js";
|
|
10
|
+
import { TransactorSource } from "./transactor-source.js";
|
|
11
|
+
import { Log } from "../log/log.js";
|
|
12
|
+
import { groupBy } from "../utility/groupby.js";
|
|
8
13
|
import { blockIdToBytes } from "../utility/block-id-to-bytes.js";
|
|
9
14
|
import { isRecordEmpty } from "../utility/is-record-empty.js";
|
|
10
15
|
import { type CoordinatorBatch, makeBatchesByPeer, incompleteBatches, everyBatch, allBatches, mergeBlocks, processBatches, createBatchesForPayload } from "../utility/batch-coordinator.js";
|
|
@@ -156,18 +161,29 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
156
161
|
};
|
|
157
162
|
|
|
158
163
|
// A batch is answered when its response carries an entry for EVERY requested
|
|
159
|
-
// block id and none of those entries
|
|
164
|
+
// block id and none of those entries carries a doubt marker. An entry present
|
|
160
165
|
// with only `state` (no `block`) is an authoritative "absent", which counts as
|
|
161
|
-
// answered — not a gap. An `unavailable` entry is the peer saying
|
|
162
|
-
//
|
|
166
|
+
// answered — not a gap. An `unavailable` entry is the peer saying it could not
|
|
167
|
+
// find out whether the block EXISTS; an `unconfirmedAheadRev` entry is the peer
|
|
168
|
+
// saying it could not confirm the content it served is CURRENT (a cohort claim
|
|
169
|
+
// sits ahead of it, unsettled). Neither counts as answered, so both earn the
|
|
170
|
+
// second-chance retry against a different coordinator.
|
|
163
171
|
const isAuthoritative = (b: CoordinatorBatch<BlockId[], GetBlockResults>) => {
|
|
164
172
|
if (!hasValidResponse(b)) return false;
|
|
165
173
|
const resp = b.request!.response! as GetBlockResults;
|
|
166
|
-
return b.payload.every(bid => resp[bid] !== undefined
|
|
174
|
+
return b.payload.every(bid => resp[bid] !== undefined
|
|
175
|
+
&& resp[bid]!.unavailable === undefined
|
|
176
|
+
&& resp[bid]!.unconfirmedAheadRev === undefined);
|
|
167
177
|
};
|
|
168
178
|
|
|
169
179
|
// Retry only genuine no-response / partial-response batches. An authoritative
|
|
170
180
|
// absent answer is not retried.
|
|
181
|
+
// NOTE: a 'cohort-unreachable' entry earns this retry like any other flagged entry,
|
|
182
|
+
// and on a genuinely isolated node the retry re-picks the same node (the
|
|
183
|
+
// findCoordinator:all-excluded path) and repeats the same futile consult. Fine
|
|
184
|
+
// today — one extra bounded consult on an already failing read. If isolated-node
|
|
185
|
+
// read latency ever matters, skip the retry for that reason rather than widening
|
|
186
|
+
// isAuthoritative.
|
|
171
187
|
const retryable = Array.from(allBatches(batches)).filter(b =>
|
|
172
188
|
!isAuthoritative(b as any)
|
|
173
189
|
) as CoordinatorBatch<BlockId[], GetBlockResults>[];
|
|
@@ -211,22 +227,40 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
211
227
|
// Cache the completed batches that had actual responses (not just coordinator not found)
|
|
212
228
|
const completedBatches = Array.from(allBatches(batches, b => b.request?.isResponse as boolean && !isRecordEmpty(b.request!.response!)));
|
|
213
229
|
|
|
214
|
-
//
|
|
215
|
-
//
|
|
216
|
-
//
|
|
217
|
-
//
|
|
230
|
+
// Ranking per block id: a block the answering repo could confirm is current beats
|
|
231
|
+
// one it marked possibly-behind (`unconfirmedAheadRev`), which beats an
|
|
232
|
+
// authoritative absent, which beats an absent marked possibly-behind (a doubted
|
|
233
|
+
// tombstone), which beats an `unavailable` guess. Content still beats absence at
|
|
234
|
+
// every confidence level, and one peer that positively knows outranks another
|
|
235
|
+
// that could not find out. Without the confirmed-over-unconfirmed split, a stale
|
|
236
|
+
// flagged block and the fresh confirmed block fetched by its own retry round tie
|
|
237
|
+
// — and only strictly-greater rank replaces, so first-arrival (the stale one)
|
|
238
|
+
// would win the very merge the retry exists to fix. Non-object junk ranks below
|
|
239
|
+
// everything so any real entry replaces it.
|
|
218
240
|
// NOTE: `materializedRev` is not part of the ranking, so two peers answering the same
|
|
219
241
|
// pinned get with block-carrying entries at DIFFERENT materialized revisions resolve
|
|
220
242
|
// to whichever arrived first. Not a concern today — cohort peers share the block's
|
|
221
243
|
// revision log, so they agree on the highest committed rev at or below a pin — and the
|
|
222
244
|
// failure direction is safe (a lower recorded revision spuriously stale-rejects rather
|
|
223
245
|
// than wrongly accepting). If peers are ever seen to disagree here, break the tie on
|
|
224
|
-
// the HIGHEST materializedRev among rank
|
|
246
|
+
// the HIGHEST materializedRev among top-rank entries.
|
|
247
|
+
// `unavailable` answers rank among THEMSELVES by how much they establish, so the merged
|
|
248
|
+
// entry never presents a weaker doubt than some peer actually returned. This matters
|
|
249
|
+
// because the reason travels out verbatim on `BlockUnavailableError` and callers act on
|
|
250
|
+
// it: 'cohort-unreachable' is the one reason a caller may treat permissively (the
|
|
251
|
+
// answering node reached nobody, so its own view is all it has), and a partitioned
|
|
252
|
+
// coordinator answering first must not mask a well-connected one that positively
|
|
253
|
+
// established the block EXISTS ('claimed-elsewhere', or 'unmaterializable' — records
|
|
254
|
+
// held here). 'peers-unreachable' sits between: it establishes that some of the cohort
|
|
255
|
+
// was reachable, without settling existence.
|
|
256
|
+
const unavailableRank = (reason: BlockUnavailableReason): number =>
|
|
257
|
+
reason === 'cohort-unreachable' ? 0 : reason === 'peers-unreachable' ? 1 : 2;
|
|
225
258
|
const rankOf = (r: unknown): number => {
|
|
226
259
|
if (!r || typeof r !== 'object') return -1;
|
|
227
260
|
const entry = r as GetBlockResults[BlockId];
|
|
228
|
-
if (entry.block != null) return
|
|
229
|
-
|
|
261
|
+
if (entry.block != null) return entry.unconfirmedAheadRev === undefined ? 6 : 5;
|
|
262
|
+
if (entry.unavailable !== undefined) return unavailableRank(entry.unavailable);
|
|
263
|
+
return entry.unconfirmedAheadRev === undefined ? 4 : 3;
|
|
230
264
|
};
|
|
231
265
|
|
|
232
266
|
// Create a lookup map from successful responses only
|
|
@@ -275,12 +309,18 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
275
309
|
// A block whose repo could not determine whether it exists carries no status either:
|
|
276
310
|
// its empty `state` would read below as `aborted`, turning "I could not find out" into
|
|
277
311
|
// a definite verdict on someone's action. Fail loudly instead, like every other read of
|
|
278
|
-
// an unavailable block (see BlockUnavailableError).
|
|
312
|
+
// an unavailable block (see BlockUnavailableError). The same goes for a state the repo
|
|
313
|
+
// could not confirm is CURRENT (`unconfirmedAheadRev`, surviving the retry round above):
|
|
314
|
+
// an action committed at the claimed-ahead revision would read out of the stale state as
|
|
315
|
+
// a definite `aborted` — this read is unpinned, so the doubt always applies.
|
|
279
316
|
for (const blockId of allBlockIds) {
|
|
280
317
|
const entry = blockStates[blockId];
|
|
281
318
|
if (entry?.unavailable !== undefined && entry.block == null) {
|
|
282
319
|
throw new BlockUnavailableError(blockId, entry.unavailable);
|
|
283
320
|
}
|
|
321
|
+
if (entry?.unconfirmedAheadRev !== undefined) {
|
|
322
|
+
throw new BlockPossiblyStaleError(blockId, entry.unconfirmedAheadRev);
|
|
323
|
+
}
|
|
284
324
|
}
|
|
285
325
|
|
|
286
326
|
// Determine status for each action ref
|
|
@@ -641,7 +681,6 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
641
681
|
async commit(request: CommitRequest): Promise<CommitResult> {
|
|
642
682
|
const t0 = Date.now();
|
|
643
683
|
log('commit actionId=%s rev=%d blockIds=%d', request.actionId, request.rev, request.blockIds.length);
|
|
644
|
-
const allBlockIds = [...new Set([...request.blockIds, request.tailId])];
|
|
645
684
|
|
|
646
685
|
// Commit the header block if provided and not already in blockIds.
|
|
647
686
|
// `request.tailId` is threaded into every per-block commit so the coordinator carries it into the
|
|
@@ -649,14 +688,14 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
649
688
|
// CollectionChangeEvent (the reactivity topic anchor). Without this the per-block RepoCommitRequest
|
|
650
689
|
// drops the collection tail and reactivity origination is gated off (undefined tail → non-member).
|
|
651
690
|
if (request.headerId && !request.blockIds.includes(request.headerId)) {
|
|
652
|
-
const headerResult = await this.commitBlock(request.headerId,
|
|
691
|
+
const headerResult = await this.commitBlock(request.headerId, request.actionId, request.rev, request.tailId);
|
|
653
692
|
if (!headerResult.success) {
|
|
654
693
|
return headerResult;
|
|
655
694
|
}
|
|
656
695
|
}
|
|
657
696
|
|
|
658
697
|
// Commit the tail block
|
|
659
|
-
const tailResult = await this.commitBlock(request.tailId,
|
|
698
|
+
const tailResult = await this.commitBlock(request.tailId, request.actionId, request.rev, request.tailId);
|
|
660
699
|
if (!tailResult.success) {
|
|
661
700
|
return tailResult;
|
|
662
701
|
}
|
|
@@ -679,7 +718,7 @@ export class NetworkTransactor implements ITransactor, IBlockChangeNotifier {
|
|
|
679
718
|
return { success: true };
|
|
680
719
|
}
|
|
681
720
|
|
|
682
|
-
private async commitBlock(blockId: BlockId,
|
|
721
|
+
private async commitBlock(blockId: BlockId, actionId: ActionId, rev: number, tailId?: BlockId): Promise<CommitResult> {
|
|
683
722
|
const { batches: tailBatches, error: tailError } = await this.commitBlocks({ blockIds: [blockId], actionId, rev, tailId });
|
|
684
723
|
if (tailError) {
|
|
685
724
|
// commit is a pure attempt: stale → { success:false }, transient → throw. Cancellation
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { randomBytes } from '@noble/hashes/utils.js'
|
|
2
2
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
3
|
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, StaleFailure, ActionContext, BlockType, BlockSource, ReadPurpose, Transforms } from "../index.js";
|
|
4
|
-
import { BlockUnavailableError } from "../network/struct.js";
|
|
4
|
+
import { BlockUnavailableError, BlockPossiblyStaleError } from "../network/struct.js";
|
|
5
5
|
import type { ReadDependency } from "../transaction/transaction.js";
|
|
6
6
|
import { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
|
|
7
7
|
|
|
@@ -43,7 +43,7 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
43
43
|
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
44
44
|
const entry = result?.[id];
|
|
45
45
|
if (entry) {
|
|
46
|
-
const { block, state, materializedRev, unavailable } = entry;
|
|
46
|
+
const { block, state, materializedRev, unavailable, unconfirmedAheadRev } = entry;
|
|
47
47
|
// An entry flagged `unavailable` with no block is the repo saying "I could not find
|
|
48
48
|
// out whether this exists" — an answer that must not be read as absent. Throw rather
|
|
49
49
|
// than return undefined, and record no read dependency (dependencies are recorded
|
|
@@ -51,6 +51,29 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
51
51
|
if (!block && unavailable) {
|
|
52
52
|
throw new BlockUnavailableError(id, unavailable);
|
|
53
53
|
}
|
|
54
|
+
// A read whose surviving answer is marked possibly-behind (`unconfirmedAheadRev`
|
|
55
|
+
// outlived the transactor's retry round: every reachable coordinator served content it
|
|
56
|
+
// could not confirm current) must not pose as an answer for a view that should CONTAIN
|
|
57
|
+
// the claimed revision. Two such views, the same test the coordinator applies when it
|
|
58
|
+
// stamps: an UNPINNED "give me latest" read — the tail read is the one seam where a
|
|
59
|
+
// lagging collection can learn the truth (Collection.bootstrapContext), and silently
|
|
60
|
+
// serving doubted content there is exactly how a collection view freezes forever — and
|
|
61
|
+
// a read PINNED AT OR ABOVE the claim, whose snapshot is missing a revision the cohort
|
|
62
|
+
// says exists inside it. A read pinned strictly BELOW the claim keeps working: it
|
|
63
|
+
// legitimately asks for an older view, which is being served correctly.
|
|
64
|
+
// No read dependency is recorded: the throw means nothing was read.
|
|
65
|
+
// NOTE: accepted tradeoff — this converts a silent wrong answer into a loud failure. A
|
|
66
|
+
// node partitioned from every coordinator able to confirm currency used to read (stale)
|
|
67
|
+
// data indefinitely without any signal; it now raises BlockPossiblyStaleError on the
|
|
68
|
+
// reads that should contain the claim, until the partition heals or the claim is
|
|
69
|
+
// settled. Deliberate: the
|
|
70
|
+
// silent alternative is a collection view that forks and freezes with no report
|
|
71
|
+
// (ticket coordinator-serves-stale-data-as-if-confirmed). Revisit only if a
|
|
72
|
+
// degraded-read mode (serve-with-warning) becomes a product requirement.
|
|
73
|
+
if (unconfirmedAheadRev !== undefined
|
|
74
|
+
&& (this.actionContext === undefined || this.actionContext.rev >= unconfirmedAheadRev)) {
|
|
75
|
+
throw new BlockPossiblyStaleError(id, unconfirmedAheadRev);
|
|
76
|
+
}
|
|
54
77
|
// Record a read dependency only for a block that actually exists. A transactor may return a
|
|
55
78
|
// populated entry with `block: undefined` for a genuinely-missing block (TestTransactor does;
|
|
56
79
|
// the Network transactor always populates the key); recording there would add a phantom
|