@optimystic/db-core 1.0.0-beta.3 → 1.0.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/dist/src/cluster/structs.d.ts +30 -6
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cluster/structs.js +5 -3
- package/dist/src/cluster/structs.js.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +2 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -1
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +10 -3
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -1
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
- package/dist/src/cohort-topic/wire/codec.js +8 -2
- package/dist/src/cohort-topic/wire/codec.js.map +1 -1
- package/dist/src/collection/collection.d.ts +439 -34
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +843 -148
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collection/struct.d.ts +109 -4
- package/dist/src/collection/struct.d.ts.map +1 -1
- package/dist/src/collection/struct.js +88 -1
- package/dist/src/collection/struct.js.map +1 -1
- package/dist/src/collections/diary/diary.d.ts +6 -2
- package/dist/src/collections/diary/diary.d.ts.map +1 -1
- package/dist/src/collections/diary/diary.js +5 -2
- package/dist/src/collections/diary/diary.js.map +1 -1
- package/dist/src/collections/tree/struct.d.ts +124 -16
- package/dist/src/collections/tree/struct.d.ts.map +1 -1
- package/dist/src/collections/tree/struct.js +86 -11
- package/dist/src/collections/tree/struct.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +13 -3
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +106 -40
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +1 -1
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +1 -1
- package/dist/src/index.js.map +1 -1
- package/dist/src/matchmaking/wire.d.ts.map +1 -1
- package/dist/src/matchmaking/wire.js +8 -2
- package/dist/src/matchmaking/wire.js.map +1 -1
- package/dist/src/network/durability.d.ts +45 -0
- package/dist/src/network/durability.d.ts.map +1 -0
- package/dist/src/network/durability.js +114 -0
- package/dist/src/network/durability.js.map +1 -0
- package/dist/src/network/i-key-network.d.ts +14 -6
- package/dist/src/network/i-key-network.d.ts.map +1 -1
- package/dist/src/network/index.d.ts +3 -0
- package/dist/src/network/index.d.ts.map +1 -1
- package/dist/src/network/index.js +3 -0
- package/dist/src/network/index.js.map +1 -1
- package/dist/src/network/lineage.d.ts +39 -0
- package/dist/src/network/lineage.d.ts.map +1 -0
- package/dist/src/network/lineage.js +61 -0
- package/dist/src/network/lineage.js.map +1 -0
- package/dist/src/network/routing-key.d.ts +24 -0
- package/dist/src/network/routing-key.d.ts.map +1 -0
- package/dist/src/network/routing-key.js +9 -0
- package/dist/src/network/routing-key.js.map +1 -0
- package/dist/src/network/stale-failure.d.ts +5 -1
- package/dist/src/network/stale-failure.d.ts.map +1 -1
- package/dist/src/network/stale-failure.js +5 -1
- package/dist/src/network/stale-failure.js.map +1 -1
- package/dist/src/network/struct.d.ts +151 -8
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/network/struct.js.map +1 -1
- package/dist/src/reactivity/wire.d.ts.map +1 -1
- package/dist/src/reactivity/wire.js +8 -2
- package/dist/src/reactivity/wire.js.map +1 -1
- package/dist/src/testing/test-transactor.d.ts +64 -7
- package/dist/src/testing/test-transactor.d.ts.map +1 -1
- package/dist/src/testing/test-transactor.js +150 -23
- package/dist/src/testing/test-transactor.js.map +1 -1
- package/dist/src/transaction/coordinator.d.ts +141 -21
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +408 -122
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transaction/errors.d.ts +39 -10
- package/dist/src/transaction/errors.d.ts.map +1 -1
- package/dist/src/transaction/errors.js +34 -9
- package/dist/src/transaction/errors.js.map +1 -1
- package/dist/src/transaction/index.d.ts +1 -0
- package/dist/src/transaction/index.d.ts.map +1 -1
- package/dist/src/transaction/index.js.map +1 -1
- package/dist/src/transactor/block-floors.d.ts +78 -0
- package/dist/src/transactor/block-floors.d.ts.map +1 -0
- package/dist/src/transactor/block-floors.js +62 -0
- package/dist/src/transactor/block-floors.js.map +1 -0
- package/dist/src/transactor/change-notifier.d.ts +43 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -1
- package/dist/src/transactor/change-notifier.js +3 -0
- package/dist/src/transactor/change-notifier.js.map +1 -1
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +43 -6
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +279 -86
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/dist/src/transactor/transactor-source.d.ts +94 -4
- package/dist/src/transactor/transactor-source.d.ts.map +1 -1
- package/dist/src/transactor/transactor-source.js +120 -42
- package/dist/src/transactor/transactor-source.js.map +1 -1
- package/dist/src/transactor/transactor.d.ts +14 -1
- package/dist/src/transactor/transactor.d.ts.map +1 -1
- package/dist/src/transform/atomic.d.ts.map +1 -1
- package/dist/src/transform/atomic.js +12 -9
- package/dist/src/transform/atomic.js.map +1 -1
- package/dist/src/transform/base-pins.d.ts +50 -17
- package/dist/src/transform/base-pins.d.ts.map +1 -1
- package/dist/src/transform/base-pins.js +58 -11
- package/dist/src/transform/base-pins.js.map +1 -1
- package/dist/src/transform/cache-source.d.ts +75 -11
- package/dist/src/transform/cache-source.d.ts.map +1 -1
- package/dist/src/transform/cache-source.js +162 -36
- package/dist/src/transform/cache-source.js.map +1 -1
- package/dist/src/transform/digest.d.ts +8 -1
- package/dist/src/transform/digest.d.ts.map +1 -1
- package/dist/src/transform/digest.js +19 -9
- package/dist/src/transform/digest.js.map +1 -1
- package/dist/src/transform/helpers.d.ts +6 -2
- package/dist/src/transform/helpers.d.ts.map +1 -1
- package/dist/src/transform/helpers.js +8 -3
- package/dist/src/transform/helpers.js.map +1 -1
- package/dist/src/transform/tracker.d.ts +94 -14
- package/dist/src/transform/tracker.d.ts.map +1 -1
- package/dist/src/transform/tracker.js +222 -68
- package/dist/src/transform/tracker.js.map +1 -1
- package/dist/src/utility/structural-equals.d.ts +38 -0
- package/dist/src/utility/structural-equals.d.ts.map +1 -0
- package/dist/src/utility/structural-equals.js +77 -0
- package/dist/src/utility/structural-equals.js.map +1 -0
- package/package.json +2 -2
- package/src/cluster/structs.ts +28 -7
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +260 -253
- package/src/cohort-topic/wire/codec.ts +222 -216
- package/src/collection/collection.ts +960 -157
- package/src/collection/struct.ts +99 -4
- package/src/collections/diary/diary.ts +71 -68
- package/src/collections/tree/struct.ts +155 -23
- package/src/collections/tree/tree.ts +127 -40
- package/src/index.ts +1 -1
- package/src/matchmaking/wire.ts +8 -2
- package/src/network/durability.ts +123 -0
- package/src/network/i-key-network.ts +60 -52
- package/src/network/index.ts +10 -7
- package/src/network/lineage.ts +80 -0
- package/src/network/routing-key.ts +27 -0
- package/src/network/stale-failure.ts +5 -1
- package/src/network/struct.ts +156 -8
- package/src/reactivity/wire.ts +230 -224
- package/src/testing/test-transactor.ts +152 -24
- package/src/transaction/coordinator.ts +470 -131
- package/src/transaction/errors.ts +34 -9
- package/src/transaction/index.ts +1 -0
- package/src/transactor/block-floors.ts +105 -0
- package/src/transactor/change-notifier.ts +129 -80
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +297 -89
- package/src/transactor/transactor-source.ts +131 -42
- package/src/transactor/transactor.ts +13 -1
- package/src/transform/atomic.ts +25 -22
- package/src/transform/base-pins.ts +76 -18
- package/src/transform/cache-source.ts +327 -194
- package/src/transform/digest.ts +21 -10
- package/src/transform/helpers.ts +8 -3
- package/src/transform/tracker.ts +423 -270
- package/src/utility/structural-equals.ts +72 -0
- package/dist/src/utility/block-id-to-bytes.d.ts +0 -3
- package/dist/src/utility/block-id-to-bytes.d.ts.map +0 -1
- package/dist/src/utility/block-id-to-bytes.js +0 -7
- package/dist/src/utility/block-id-to-bytes.js.map +0 -1
- package/src/utility/block-id-to-bytes.ts +0 -8
|
@@ -1,14 +1,65 @@
|
|
|
1
1
|
import { randomBytes } from '@noble/hashes/utils.js'
|
|
2
2
|
import { toString as uint8ArrayToString } from 'uint8arrays/to-string'
|
|
3
|
-
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId,
|
|
3
|
+
import type { IBlock, BlockId, BlockHeader, ITransactor, ActionId, CommitResult, ActionContext, BlockType, BlockSource, ReadPurpose, Transforms, BlockContentDigests, BlockBaseRevs, GetBlockResult } from "../index.js";
|
|
4
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
|
-
import { blockDigestsField } from "../transform/digest.js";
|
|
7
|
+
import { blockDigestsField, baseRevsField } from "../transform/digest.js";
|
|
8
|
+
import type { BlockFloorCheck } from "./block-floors.js";
|
|
8
9
|
import { createLogger } from "../logger.js";
|
|
9
10
|
|
|
10
11
|
const log = createLogger('transactor-source');
|
|
11
12
|
|
|
13
|
+
/** The block `entry` answers for `id`, for a read at `context` (`undefined` for an unpinned "give me
|
|
14
|
+
* latest" read) — `undefined` when the block is absent — or a throw when the entry is not an answer
|
|
15
|
+
* that read may use. Every read that consumes a raw {@link GetBlockResult} goes through here, so a
|
|
16
|
+
* doubted answer cannot be treated differently depending on which path fetched it.
|
|
17
|
+
*
|
|
18
|
+
* An entry flagged `unavailable` with no block is the repo saying "I could not find out whether this
|
|
19
|
+
* exists" — an answer that must not be read as absent. A repo that omits the flag stays authoritative.
|
|
20
|
+
*
|
|
21
|
+
* A read whose surviving answer is marked possibly-behind (`unconfirmedAheadRev` outlived the
|
|
22
|
+
* transactor's retry round: every reachable coordinator served content it could not confirm current)
|
|
23
|
+
* must not pose as an answer for a view that should CONTAIN the claimed revision. Two such views, the
|
|
24
|
+
* same test the coordinator applies when it stamps: an UNPINNED read — the tail read is the one seam
|
|
25
|
+
* where a lagging collection can learn the truth (Collection.checkedLogTail), and silently serving
|
|
26
|
+
* doubted content there is exactly how a collection view freezes forever — and a read PINNED AT OR
|
|
27
|
+
* ABOVE the claim, whose snapshot is missing a revision the cohort says exists inside it. A read pinned
|
|
28
|
+
* strictly BELOW the claim keeps working: it legitimately asks for an older view, which is being
|
|
29
|
+
* served correctly.
|
|
30
|
+
*
|
|
31
|
+
* NOTE: accepted tradeoff — this converts a silent wrong answer into a loud failure. A node partitioned
|
|
32
|
+
* from every coordinator able to confirm currency used to read (stale) data indefinitely without any
|
|
33
|
+
* signal; it now raises BlockPossiblyStaleError on the reads that should contain the claim, until the
|
|
34
|
+
* partition heals or the claim is settled. Deliberate: the silent alternative is a collection view that
|
|
35
|
+
* forks and freezes with no report (ticket coordinator-serves-stale-data-as-if-confirmed). Revisit only
|
|
36
|
+
* if a degraded-read mode (serve-with-warning) becomes a product requirement. */
|
|
37
|
+
export function answeredBlock(id: BlockId, entry: GetBlockResult, context: ActionContext | undefined): IBlock | undefined {
|
|
38
|
+
const { block, unavailable, unconfirmedAheadRev } = entry;
|
|
39
|
+
if (!block && unavailable) {
|
|
40
|
+
throw new BlockUnavailableError(id, unavailable);
|
|
41
|
+
}
|
|
42
|
+
if (unconfirmedAheadRev !== undefined && (context === undefined || context.rev >= unconfirmedAheadRev)) {
|
|
43
|
+
throw new BlockPossiblyStaleError(id, unconfirmedAheadRev);
|
|
44
|
+
}
|
|
45
|
+
return block;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** The revision a served block's content IS: its materialized revision, falling back to the repo's
|
|
49
|
+
* latest for repos that omit the field (see {@link GetBlockResult.materialized} for why `state.latest`
|
|
50
|
+
* alone is the wrong number). */
|
|
51
|
+
export function servedRevision(entry: GetBlockResult): number {
|
|
52
|
+
return entry.materialized?.rev ?? entry.state.latest?.rev ?? 0;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/** What a source knows about one block it returned — see {@link TransactorSource.describeServed}. */
|
|
56
|
+
export type ServedBlock = {
|
|
57
|
+
/** The revision the content is ({@link servedRevision} of the answer it came from). */
|
|
58
|
+
rev: number;
|
|
59
|
+
/** Whether a cache may keep the block and serve it again without re-asking. */
|
|
60
|
+
mayRetain: boolean;
|
|
61
|
+
};
|
|
62
|
+
|
|
12
63
|
export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlock> {
|
|
13
64
|
/** Shared with this collection's CacheSource so cache hits also record dependencies.
|
|
14
65
|
* Defaults to a private instance so internal log-walk sources (which never need a
|
|
@@ -17,12 +68,20 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
17
68
|
/** Last revision observed per id, so CacheSource can learn the revision on a miss-load
|
|
18
69
|
* (it calls {@link getReadRevision} right after this source serves the block). */
|
|
19
70
|
private readRevisions = new Map<BlockId, number>();
|
|
71
|
+
/** What this source knows about each block OBJECT it returned, for {@link describeServed}. Keyed
|
|
72
|
+
* by the object rather than by id because a cache reads it after an `await`: with two reads of
|
|
73
|
+
* one id in flight, a by-id record holds whichever answer was processed last, and would pair one
|
|
74
|
+
* answer's content with the other's revision and verdict. Weak, so it holds nothing alive. */
|
|
75
|
+
private served = new WeakMap<IBlock, ServedBlock>();
|
|
20
76
|
|
|
21
77
|
constructor(
|
|
22
78
|
private readonly collectionId: BlockId,
|
|
23
79
|
private readonly transactor: ITransactor,
|
|
24
80
|
public actionContext: ActionContext | undefined,
|
|
25
81
|
collector?: ReadDependencyCollector,
|
|
82
|
+
/** The owning collection's floors, shared with every other read source it builds. Omitted by
|
|
83
|
+
* sources that walk the log or are built standalone: their answers are judged against nothing. */
|
|
84
|
+
private readonly floors?: BlockFloorCheck,
|
|
26
85
|
) {
|
|
27
86
|
this.collector = collector ?? new ReadDependencyCollector();
|
|
28
87
|
}
|
|
@@ -41,43 +100,22 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
41
100
|
}
|
|
42
101
|
|
|
43
102
|
async tryGet(id: BlockId, purpose: ReadPurpose = 'value'): Promise<TBlock | undefined> {
|
|
44
|
-
|
|
103
|
+
// Carry the floor that applies to THIS read, so a transactor with more than one machine to
|
|
104
|
+
// ask can re-ask when the first answers under it (`BlockGets.floors`). Advisory: the verdict
|
|
105
|
+
// that matters is still `mayRetain` below, on whatever answer comes back.
|
|
106
|
+
const floor = this.floors?.applicableTo(id, this.actionContext);
|
|
107
|
+
const result = await this.transactor.get({
|
|
108
|
+
blockIds: [id],
|
|
109
|
+
context: this.actionContext,
|
|
110
|
+
...(floor !== undefined ? { floors: { [id]: floor.rev } } : {}),
|
|
111
|
+
});
|
|
45
112
|
// Guard the per-key entry: some transactors return a sparse result that omits `id`
|
|
46
113
|
// entirely (e.g. block genuinely not found), so `result` is a truthy object but
|
|
47
114
|
// `result[id]` is undefined. Destructuring that would throw a TypeError.
|
|
48
115
|
const entry = result?.[id];
|
|
49
116
|
if (entry) {
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
// out whether this exists" — an answer that must not be read as absent. Throw rather
|
|
53
|
-
// than return undefined, and record no read dependency (dependencies are recorded
|
|
54
|
-
// only for blocks that actually exist). A repo that omits the flag stays authoritative.
|
|
55
|
-
if (!block && unavailable) {
|
|
56
|
-
throw new BlockUnavailableError(id, unavailable);
|
|
57
|
-
}
|
|
58
|
-
// A read whose surviving answer is marked possibly-behind (`unconfirmedAheadRev`
|
|
59
|
-
// outlived the transactor's retry round: every reachable coordinator served content it
|
|
60
|
-
// could not confirm current) must not pose as an answer for a view that should CONTAIN
|
|
61
|
-
// the claimed revision. Two such views, the same test the coordinator applies when it
|
|
62
|
-
// stamps: an UNPINNED "give me latest" read — the tail read is the one seam where a
|
|
63
|
-
// lagging collection can learn the truth (Collection.bootstrapContext), and silently
|
|
64
|
-
// serving doubted content there is exactly how a collection view freezes forever — and
|
|
65
|
-
// a read PINNED AT OR ABOVE the claim, whose snapshot is missing a revision the cohort
|
|
66
|
-
// says exists inside it. A read pinned strictly BELOW the claim keeps working: it
|
|
67
|
-
// legitimately asks for an older view, which is being served correctly.
|
|
68
|
-
// No read dependency is recorded: the throw means nothing was read.
|
|
69
|
-
// NOTE: accepted tradeoff — this converts a silent wrong answer into a loud failure. A
|
|
70
|
-
// node partitioned from every coordinator able to confirm currency used to read (stale)
|
|
71
|
-
// data indefinitely without any signal; it now raises BlockPossiblyStaleError on the
|
|
72
|
-
// reads that should contain the claim, until the partition heals or the claim is
|
|
73
|
-
// settled. Deliberate: the
|
|
74
|
-
// silent alternative is a collection view that forks and freezes with no report
|
|
75
|
-
// (ticket coordinator-serves-stale-data-as-if-confirmed). Revisit only if a
|
|
76
|
-
// degraded-read mode (serve-with-warning) becomes a product requirement.
|
|
77
|
-
if (unconfirmedAheadRev !== undefined
|
|
78
|
-
&& (this.actionContext === undefined || this.actionContext.rev >= unconfirmedAheadRev)) {
|
|
79
|
-
throw new BlockPossiblyStaleError(id, unconfirmedAheadRev);
|
|
80
|
-
}
|
|
117
|
+
// A throw here records no read dependency: it means nothing was read.
|
|
118
|
+
const block = answeredBlock(id, entry, this.actionContext);
|
|
81
119
|
// Record a read dependency only for a block that actually exists. A transactor may return a
|
|
82
120
|
// populated entry with `block: undefined` for a genuinely-missing block (TestTransactor does;
|
|
83
121
|
// the Network transactor always populates the key); recording there would add a phantom
|
|
@@ -87,15 +125,14 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
87
125
|
// Record read dependency for optimistic concurrency control, carrying the caller's
|
|
88
126
|
// read purpose (default `value`) so a purely-structural navigation read can later be
|
|
89
127
|
// dropped from the conflict set (see ReadDependencyCollector / Theorem 5).
|
|
90
|
-
// Record the revision the content was MATERIALIZED at, not the newest the repo holds
|
|
91
|
-
// see {@link GetBlockResult.materialized} for why `state.latest` is the wrong number
|
|
92
|
-
// and why the fallback preserves today's behaviour for repos that omit the field.
|
|
128
|
+
// Record the revision the content was MATERIALIZED at, not the newest the repo holds.
|
|
93
129
|
// Both sinks must take the SAME value: CacheSource learns it via getReadRevision on a
|
|
94
130
|
// miss-load and re-emits it on every later hit, so a split would stamp the cache
|
|
95
131
|
// differently from the collector.
|
|
96
|
-
const rev =
|
|
132
|
+
const rev = servedRevision(entry);
|
|
97
133
|
this.collector.record(id, rev, purpose);
|
|
98
134
|
this.readRevisions.set(id, rev);
|
|
135
|
+
this.served.set(block, { rev, mayRetain: this.mayRetain(id, rev) });
|
|
99
136
|
}
|
|
100
137
|
// TODO: if the state reports that there is a pending action, record this so that we are sure to update before syncing
|
|
101
138
|
//state.pendings
|
|
@@ -110,6 +147,46 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
110
147
|
return this.readRevisions.get(id);
|
|
111
148
|
}
|
|
112
149
|
|
|
150
|
+
/** What this source knows about `block`, an object it returned from {@link tryGet}: the revision
|
|
151
|
+
* its content is, and whether a cache may keep it. `undefined` for any other object. CacheSource
|
|
152
|
+
* asks this on a miss-load, in preference to the by-id {@link getReadRevision}. */
|
|
153
|
+
describeServed(block: IBlock): ServedBlock | undefined {
|
|
154
|
+
return this.served.get(block);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/** Whether a cache may keep the block just served for `id` and serve it again without re-asking:
|
|
158
|
+
* `false` exactly for a below-floor answer — content older than a log entry the collection has
|
|
159
|
+
* already walked says the block is (see {@link BlockFloorCheck}). The block is handed to the
|
|
160
|
+
* reader either way.
|
|
161
|
+
*
|
|
162
|
+
* NOTE: accepted tradeoff — a below-floor answer is RETURNED (uncached, and reported through the
|
|
163
|
+
* floors as `collection:block-below-floor`), not refused with BlockPossiblyStaleError. A log entry
|
|
164
|
+
* is not proof its blocks landed: a refused write can leave its entry in the log while the blocks
|
|
165
|
+
* it names never take that revision on any machine
|
|
166
|
+
* (tickets/backlog/bug-a-refused-write-can-leave-its-log-entry-behind). For such an entry the
|
|
167
|
+
* below-floor content is the CORRECT content and no machine can ever meet the floor, so a throw
|
|
168
|
+
* would make the block unreadable through every handle that refreshed past the entry — until the
|
|
169
|
+
* block is next written, which cannot happen through a handle that cannot read it. A throw would
|
|
170
|
+
* also land inside `Collection.updateInternal`'s replay and leave the tracker half re-staged.
|
|
171
|
+
* Returning uncached instead restores the bound the storage layer already documents
|
|
172
|
+
* (docs/transactions.md § Lazy read-repair window): the next read re-asks, so a lagging replica is
|
|
173
|
+
* seen through within one read-repair window rather than never. Revisit if log entries ever become
|
|
174
|
+
* proof that their blocks landed (abandoned entries made distinguishable): then an answer still
|
|
175
|
+
* below its floor after every machine was asked should throw.
|
|
176
|
+
*
|
|
177
|
+
* NOTE: while a floor is unmet, every read of that block costs a transactor request instead of a
|
|
178
|
+
* memory hit — and, since the floor now rides out on the request (`BlockGets.floors`), that
|
|
179
|
+
* request costs a SECOND coordinator round too, because `NetworkTransactor.get` re-asks a
|
|
180
|
+
* different machine for a below-floor answer. Both last at most one read-repair window for a
|
|
181
|
+
* lagging replica, but until the block is next written (or the handle reopened) for an abandoned
|
|
182
|
+
* entry, whose floor no machine can ever meet. Unmeasured. If it ever shows up, drop a floor
|
|
183
|
+
* after some number of consecutive below-floor answers from a coordinator other than this node,
|
|
184
|
+
* which retires the extra round with it (floors are otherwise never dropped — see
|
|
185
|
+
* `BlockFloors`). */
|
|
186
|
+
private mayRetain(id: BlockId, servedRev: number): boolean {
|
|
187
|
+
return !this.floors?.answeredBelowFloor(id, this.actionContext, servedRev);
|
|
188
|
+
}
|
|
189
|
+
|
|
113
190
|
getReadDependencies(): ReadDependency[] {
|
|
114
191
|
return this.collector.getReadDependencies();
|
|
115
192
|
}
|
|
@@ -143,10 +220,22 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
143
220
|
* computed by the caller from the same tracker that produced `transform`. Omitted from the commit request when
|
|
144
221
|
* undefined, so a caller that declares nothing produces exactly the request shape as before — the field rides
|
|
145
222
|
* inside every cohort signature's hash preimage, so keeping the shape clean keeps those preimages clean.
|
|
146
|
-
* @
|
|
223
|
+
* @param baseRevs - Optional per-block base revisions for the pend (see {@link PendRequest.baseRevs}): the
|
|
224
|
+
* committed revision each update-only block's operations were computed against, from the same tracker
|
|
225
|
+
* (`Tracker.stagedBaseRevs`). Omitted from the pend request when empty, for the same preimage reason.
|
|
226
|
+
* @returns The transactor's own verdict, unflattened: a {@link CommitSuccess} carrying the
|
|
227
|
+
* {@link WriteDurability} of the committed revision, or a {@link StaleFailure} if the pend or the commit
|
|
228
|
+
* was refused. Success is deliberately NOT collapsed to `undefined` — the durability is the only thing
|
|
229
|
+
* that tells a write every machine holds from one only this machine holds, and a caller that wants the
|
|
230
|
+
* old boolean reads `result.success`. Test "is this completely saved" through `isFullyDurable`, never
|
|
231
|
+
* by comparing `quorum` (see {@link WriteDurability}).
|
|
147
232
|
*/
|
|
148
|
-
async transact(transform: Transforms, actionId: ActionId, rev: number, headerId: BlockId, tailId: BlockId, priority = 0, blockDigests?: BlockContentDigests): Promise<
|
|
149
|
-
const pendResult = await this.transactor.pend({
|
|
233
|
+
async transact(transform: Transforms, actionId: ActionId, rev: number, headerId: BlockId, tailId: BlockId, priority = 0, blockDigests?: BlockContentDigests, baseRevs?: BlockBaseRevs): Promise<CommitResult> {
|
|
234
|
+
const pendResult = await this.transactor.pend({
|
|
235
|
+
transforms: transform, actionId, rev, policy: 'r',
|
|
236
|
+
...(priority > 0 ? { priority } : {}),
|
|
237
|
+
...baseRevsField(baseRevs)
|
|
238
|
+
});
|
|
150
239
|
if (!pendResult.success) {
|
|
151
240
|
return pendResult;
|
|
152
241
|
}
|
|
@@ -166,8 +255,8 @@ export class TransactorSource<TBlock extends IBlock> implements BlockSource<TBlo
|
|
|
166
255
|
// rebase — letting the cancel's own failure throw over it would turn a routine,
|
|
167
256
|
// recoverable race into a hard failure. So the cancel fault is logged, not raised.
|
|
168
257
|
await this.dischargePend(actionId, pendResult.blockIds);
|
|
169
|
-
return commitResult;
|
|
170
258
|
}
|
|
259
|
+
return commitResult;
|
|
171
260
|
} catch (e) {
|
|
172
261
|
// `e` is the real cause — a transport fault, the thing the caller needs to see. A cancel
|
|
173
262
|
// that also fails must not silently take its place, but it must not be lost either: the
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { GetBlockResults, ActionBlocks, BlockActionStatus, PendResult, CommitResult, PendRequest, CommitRequest, BlockGets, BlockId } from "../index.js";
|
|
1
|
+
import type { GetBlockResults, ActionBlocks, ActionLineage, BlockActionStatus, PendResult, CommitResult, PendRequest, CommitRequest, BlockGets, BlockId } from "../index.js";
|
|
2
2
|
import type { PeerId } from "../network/types.js";
|
|
3
3
|
|
|
4
4
|
export type ClusterNomineesResult = {
|
|
@@ -46,4 +46,16 @@ export type ITransactor = {
|
|
|
46
46
|
- Used to build the supercluster for multi-collection transaction consensus
|
|
47
47
|
*/
|
|
48
48
|
queryClusterNominees?(blockId: BlockId): Promise<ClusterNomineesResult>;
|
|
49
|
+
|
|
50
|
+
/** Whether a COMMITTED action is part of what each named block holds now — the question a
|
|
51
|
+
writer asks when its own write was superseded before it could confirm it (see `BlockLineage`).
|
|
52
|
+
- `getStatus` cannot answer this: it judges a block by who holds its LATEST revision, so a
|
|
53
|
+
write that landed and was then built upon reads there as not committed.
|
|
54
|
+
- `contains` is an acknowledgement and meets the same bar as a commit's: a strict majority of
|
|
55
|
+
the block's cohort holds content built from the action.
|
|
56
|
+
- Read-only. It promotes nothing, so asking can never be what makes a write land.
|
|
57
|
+
- Optional: a transactor (or a wrapper around one) that does not offer it leaves the writer
|
|
58
|
+
unable to establish the outcome, which it reports as such rather than guessing either way.
|
|
59
|
+
*/
|
|
60
|
+
getLineage?(ref: ActionBlocks & { rev: number }): Promise<ActionLineage>;
|
|
49
61
|
}
|
package/src/transform/atomic.ts
CHANGED
|
@@ -1,22 +1,25 @@
|
|
|
1
|
-
import { Tracker } from "./tracker.js";
|
|
2
|
-
import type { IBlock, BlockStore } from "../index.js";
|
|
3
|
-
import { applyTransformToStore } from "./helpers.js";
|
|
4
|
-
|
|
5
|
-
export class Atomic<TBlock extends IBlock> extends Tracker<TBlock> {
|
|
6
|
-
constructor(public readonly store: BlockStore<TBlock>) {
|
|
7
|
-
super(store);
|
|
8
|
-
}
|
|
9
|
-
|
|
10
|
-
commit() {
|
|
11
|
-
//
|
|
12
|
-
//
|
|
13
|
-
//
|
|
14
|
-
//
|
|
15
|
-
//
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
}
|
|
1
|
+
import { Tracker } from "./tracker.js";
|
|
2
|
+
import type { IBlock, BlockStore } from "../index.js";
|
|
3
|
+
import { applyTransformToStore } from "./helpers.js";
|
|
4
|
+
|
|
5
|
+
export class Atomic<TBlock extends IBlock> extends Tracker<TBlock> {
|
|
6
|
+
constructor(public readonly store: BlockStore<TBlock>) {
|
|
7
|
+
super(store);
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
commit() {
|
|
11
|
+
// A parent tracker takes the staged transform AND the bases pinned inside this atomic
|
|
12
|
+
// (Tracker.absorb): without the pins, a single act() carrying more actions than the read
|
|
13
|
+
// cache holds would lose digest coverage, the cache having evicted the early bases by the
|
|
14
|
+
// flush; and without absorb keeping them as the bases of the flushed operations, a base the
|
|
15
|
+
// cache moved on from between the pin and the flush would be re-pinned at the newer revision.
|
|
16
|
+
// Any other store just receives the transform.
|
|
17
|
+
if (this.store instanceof Tracker) {
|
|
18
|
+
this.store.absorb(this);
|
|
19
|
+
} else {
|
|
20
|
+
applyTransformToStore(this.reset(), this.store);
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// rollback = reset
|
|
25
|
+
}
|
|
@@ -1,15 +1,27 @@
|
|
|
1
1
|
import type { BlockId, IBlock } from "../index.js";
|
|
2
2
|
|
|
3
|
-
/** The committed base of one block,
|
|
3
|
+
/** The committed base of one block, fixed at the moment the FIRST update for it was staged in a
|
|
4
|
+
* tracker. `rev` is the number the staged operations were computed against, and it never changes
|
|
5
|
+
* while those operations remain staged: a later change to what the source holds for the id marks
|
|
6
|
+
* the pin {@link moved} rather than replacing it (see {@link Tracker.revalidatePin}). */
|
|
4
7
|
export type PinnedBase = {
|
|
5
|
-
/** Cloned base content.
|
|
6
|
-
*
|
|
7
|
-
|
|
8
|
-
|
|
8
|
+
/** Cloned base content. Absent for a REV-ONLY pin: the block was evicted from the read cache
|
|
9
|
+
* between the read that computed the update and the update itself, so the revision is known
|
|
10
|
+
* (the cache's per-id revision outlives the block) but the content is not — the base can be
|
|
11
|
+
* declared, its digest cannot. Filled in if the same revision is later re-read.
|
|
12
|
+
* Callers MUST clone before applying a transform — applyTransform mutates, and the same pin is
|
|
13
|
+
* re-used by every retry attempt's digest pass. */
|
|
14
|
+
block?: IBlock;
|
|
15
|
+
/** Committed revision of the base (CacheSource.getCachedRevision at pin time). */
|
|
9
16
|
rev: number;
|
|
10
|
-
/** Source drift generation at
|
|
11
|
-
* longer matches the source's
|
|
17
|
+
/** Source drift generation at the last validation (CacheSource.getGeneration). A generation
|
|
18
|
+
* that no longer matches the source's means the source's content for the id changed hands
|
|
19
|
+
* since; whether the BASE moved is decided by comparing revisions, not generations. */
|
|
12
20
|
gen: number;
|
|
21
|
+
/** The source no longer describes this id at `rev`: the staged operations were computed on
|
|
22
|
+
* content that is not the content any commit would now apply them to. Never repaired in
|
|
23
|
+
* place — only a re-stage (which resets the owning tracker) clears it. */
|
|
24
|
+
moved?: true;
|
|
13
25
|
};
|
|
14
26
|
|
|
15
27
|
/** Per-transaction map of block id -> {@link PinnedBase}. Owned by a {@link Tracker}, shared by
|
|
@@ -17,10 +29,11 @@ export type PinnedBase = {
|
|
|
17
29
|
* per-attempt snapshot tracker), so a base pinned when an update was staged is still available
|
|
18
30
|
* when the digest pass runs — regardless of whether the read cache has since evicted it.
|
|
19
31
|
*
|
|
20
|
-
* NOTE: memory shape — one cloned base block per update-carrying block
|
|
21
|
-
* update staged for it until the owning tracker's next
|
|
22
|
-
* to the transaction's own write footprint (the same set
|
|
23
|
-
* request already carries), and every transaction boundary
|
|
32
|
+
* NOTE: memory shape — one cloned base block per update-carrying block (one number per block for
|
|
33
|
+
* a rev-only pin), held from the first update staged for it until the owning tracker's next
|
|
34
|
+
* reset(). Peak retention is proportional to the transaction's own write footprint (the same set
|
|
35
|
+
* of blocks whose ids and ops the commit request already carries), and every transaction boundary
|
|
36
|
+
* reclaims it via reset(). */
|
|
24
37
|
export class BasePins {
|
|
25
38
|
private pins = new Map<BlockId, PinnedBase>();
|
|
26
39
|
/** The base source every `rev`/`gen` in here was read from — see {@link bindAuthority}. */
|
|
@@ -46,6 +59,10 @@ export class BasePins {
|
|
|
46
59
|
return this.pins.get(id);
|
|
47
60
|
}
|
|
48
61
|
|
|
62
|
+
ids(): IterableIterator<BlockId> {
|
|
63
|
+
return this.pins.keys();
|
|
64
|
+
}
|
|
65
|
+
|
|
49
66
|
set(id: BlockId, pin: PinnedBase): void {
|
|
50
67
|
this.pins.set(id, pin);
|
|
51
68
|
}
|
|
@@ -54,9 +71,16 @@ export class BasePins {
|
|
|
54
71
|
this.pins.delete(id);
|
|
55
72
|
}
|
|
56
73
|
|
|
74
|
+
/** Record that the base under `id`'s staged operations has moved (see {@link PinnedBase.moved}).
|
|
75
|
+
* No-op for an unpinned id: with no pin there is no base to have moved from. */
|
|
76
|
+
markMoved(id: BlockId): void {
|
|
77
|
+
const pin = this.pins.get(id);
|
|
78
|
+
if (pin) pin.moved = true;
|
|
79
|
+
}
|
|
80
|
+
|
|
57
81
|
/** Drop every pin whose id is not in `keep`. Called from Tracker.reset with the ids still
|
|
58
|
-
* carried by the new transforms' `updates`, so a rollback keeps its pins
|
|
59
|
-
* clears them. */
|
|
82
|
+
* carried by the new transforms' `updates`, so a rollback keeps its pins — moved marks
|
|
83
|
+
* included, the operations being the same operations — and a plain reset clears them. */
|
|
60
84
|
retainOnly(keep: Iterable<BlockId>): void {
|
|
61
85
|
const keepSet = new Set(keep);
|
|
62
86
|
for (const id of this.pins.keys()) {
|
|
@@ -64,17 +88,51 @@ export class BasePins {
|
|
|
64
88
|
}
|
|
65
89
|
}
|
|
66
90
|
|
|
67
|
-
/**
|
|
68
|
-
* inside the atomic survive into the parent tracker's store.
|
|
69
|
-
*
|
|
70
|
-
*
|
|
91
|
+
/** Fold every entry of `other` in. Called from Tracker.absorb (Atomic.commit) so pins captured
|
|
92
|
+
* inside the atomic survive into the parent tracker's store.
|
|
93
|
+
*
|
|
94
|
+
* An id the parent does not pin takes the atomic's pin. An id it pins at the SAME revision takes
|
|
95
|
+
* the atomic's too — the later observation of the same committed content — except that a clone
|
|
96
|
+
* the parent holds is never traded for a rev-only pin (same revision, same content; the clone is
|
|
97
|
+
* what makes the digest declarable). An id it pins at a DIFFERENT revision is a base that MOVED
|
|
98
|
+
* between the two actions: the parent's operations were computed on one content and the
|
|
99
|
+
* atomic's on another, and the combined list has no single base. The parent's pin is kept (its
|
|
100
|
+
* revision is what the earlier operations were built on) and marked moved, and a mark on either
|
|
101
|
+
* side survives the fold. */
|
|
71
102
|
adopt(other: BasePins): void {
|
|
72
103
|
if (other.authority !== this.authority) {
|
|
73
104
|
throw new Error('BasePins.adopt across different base sources: pin revisions and generations are not comparable across them.');
|
|
74
105
|
}
|
|
75
106
|
for (const [id, pin] of other.pins) {
|
|
76
|
-
this.pins.
|
|
107
|
+
const existing = this.pins.get(id);
|
|
108
|
+
if (existing === undefined || (existing.rev === pin.rev && !existing.moved)) {
|
|
109
|
+
this.pins.set(id, pin.block === undefined && existing?.block !== undefined ? { ...pin, block: existing.block } : pin);
|
|
110
|
+
} else {
|
|
111
|
+
existing.moved = true;
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/** A copy for a staged-state snapshot (Collection.snapshotPending): the same entries, bound to
|
|
117
|
+
* the same source, in a store of its own. Entries are shared, not cloned — a pin's content is
|
|
118
|
+
* never mutated in place (every consumer clones before applying), and a moved mark set on a
|
|
119
|
+
* shared entry after the copy is a fact about the same operations. */
|
|
120
|
+
copy(): BasePins {
|
|
121
|
+
const copy = new BasePins();
|
|
122
|
+
if (this.bound) copy.bindAuthority(this.authority);
|
|
123
|
+
for (const [id, pin] of this.pins) copy.pins.set(id, pin);
|
|
124
|
+
return copy;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Make this store hold exactly `other`'s entries — the restore half of {@link copy}
|
|
128
|
+
* (Collection.restorePending), applied in place because the live tracker's store is shared by
|
|
129
|
+
* reference with every per-attempt tracker of the transaction. */
|
|
130
|
+
replaceWith(other: BasePins): void {
|
|
131
|
+
if (other.bound && other.authority !== this.authority) {
|
|
132
|
+
throw new Error('BasePins.replaceWith across different base sources: pin revisions and generations are not comparable across them.');
|
|
77
133
|
}
|
|
134
|
+
this.pins.clear();
|
|
135
|
+
for (const [id, pin] of other.pins) this.pins.set(id, pin);
|
|
78
136
|
}
|
|
79
137
|
|
|
80
138
|
get size(): number {
|