@optimystic/db-core 1.0.0-beta.3 → 1.1.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
|
@@ -5,6 +5,14 @@ import type { CollectionId } from "../collection/index.js";
|
|
|
5
5
|
* fails AFTER at least one collection has already DURABLY committed through the
|
|
6
6
|
* distributed consensus path (GATHER/PEND/COMMIT).
|
|
7
7
|
*
|
|
8
|
+
* A collection counts as committed whether an attempt committed it or the refresh
|
|
9
|
+
* between attempts saved it (an attempt reported a loss, but the collection's log
|
|
10
|
+
* entry was stored, and the refresh finished the rest of its blocks). Once either has
|
|
11
|
+
* happened, EVERY way the commit can still fail is reported as this error, with the
|
|
12
|
+
* failure that ended it as {@link reason} — a torn sibling (`TornActionError`), a
|
|
13
|
+
* sibling that kept losing until the retry budget ran out
|
|
14
|
+
* ({@link CoordinatorStaleLossError}), an abort, an expiry, a hard error.
|
|
15
|
+
*
|
|
8
16
|
* ## Why this exists (and why we can't just "roll back")
|
|
9
17
|
*
|
|
10
18
|
* The COMMIT phase commits each collection's pended blocks independently (see
|
|
@@ -44,11 +52,17 @@ import type { CollectionId } from "../collection/index.js";
|
|
|
44
52
|
*/
|
|
45
53
|
export class CoordinatorPartialCommitError extends Error {
|
|
46
54
|
constructor(
|
|
47
|
-
/** Collections durably committed
|
|
55
|
+
/** Collections durably committed before the failure (NOT rolled back) — by an attempt, or
|
|
56
|
+
* by the refresh between attempts finishing the collection's own log entry. */
|
|
48
57
|
public readonly committedCollections: readonly CollectionId[],
|
|
49
|
-
/** Collections that never committed
|
|
58
|
+
/** Collections of this transaction that never committed (local state reverted for retry —
|
|
59
|
+
* their staged actions are still in place). Can be empty when the failure came after every
|
|
60
|
+
* collection was already saved. */
|
|
50
61
|
public readonly failedCollections: readonly CollectionId[],
|
|
51
|
-
/** The
|
|
62
|
+
/** The failure that ended the commit: the attempt's commit-phase reason (a string) when an
|
|
63
|
+
* attempt half-landed on a hard failure, otherwise the error that escaped after a collection
|
|
64
|
+
* was committed or saved — a {@link CoordinatorStaleLossError} when the failed collection's
|
|
65
|
+
* refusal never cleared within the retry budget (its own `reason` names the last refusal). */
|
|
52
66
|
public readonly reason?: unknown,
|
|
53
67
|
) {
|
|
54
68
|
super(
|
|
@@ -69,11 +83,20 @@ export class CoordinatorPartialCommitError extends Error {
|
|
|
69
83
|
* in which NOTHING durably committed, so every participating collection's local tracker was
|
|
70
84
|
* restored to its pre-append state and the transaction is safe to re-drive.
|
|
71
85
|
*
|
|
86
|
+
* "Nothing durably committed" holds across the WHOLE commit, not only the last attempt. Inside
|
|
87
|
+
* the coordinator this is also the signal for a retryable ATTEMPT: one that half-landed on
|
|
88
|
+
* returned refusals (a lagging member, a rival that took a revision slot) throws it after
|
|
89
|
+
* recording its committed siblings as saved, and one whose refresh saved a participant carries on
|
|
90
|
+
* under the same budget. In both cases the coordinator reports the budget running out as a
|
|
91
|
+
* {@link CoordinatorPartialCommitError} carrying this error as its `reason`, so this error only
|
|
92
|
+
* ever escapes bare when a re-drive is genuinely safe.
|
|
93
|
+
*
|
|
72
94
|
* This is the retryable counterpart to {@link CoordinatorPartialCommitError}: a partial landing
|
|
73
|
-
* cannot be blindly retried (it would double-apply the durable half), but a clean loss
|
|
74
|
-
* coordinator's
|
|
75
|
-
*
|
|
76
|
-
*
|
|
95
|
+
* cannot be blindly retried by a CALLER (it would double-apply the durable half), but a clean loss
|
|
96
|
+
* can, and the coordinator's own retry re-drives only what is still staged. Its built-in
|
|
97
|
+
* backoff+jitter retry catches this internally and re-drives after re-reading fresh revisions; it
|
|
98
|
+
* only escapes to the caller once the retry budget (`maxAttempts` / `deadlineMs`) is exhausted, at
|
|
99
|
+
* which point it signals "gave up after a clean loss" rather than a partial split.
|
|
77
100
|
*/
|
|
78
101
|
export class CoordinatorStaleLossError extends Error {
|
|
79
102
|
constructor(
|
|
@@ -82,9 +105,11 @@ export class CoordinatorStaleLossError extends Error {
|
|
|
82
105
|
/** The underlying stale/conflict reason surfaced by the failed pend/commit phase. */
|
|
83
106
|
public readonly reason?: string,
|
|
84
107
|
) {
|
|
108
|
+
// Deliberately does not assert that nothing committed: this is also the internal signal a
|
|
109
|
+
// half-landed attempt retries on, and it then surfaces as a partial error's `reason`.
|
|
85
110
|
super(
|
|
86
|
-
`Multi-collection commit
|
|
87
|
-
|
|
111
|
+
`Multi-collection commit lost a stale race for [${failedCollections.join(', ')}]` +
|
|
112
|
+
(reason ? ` — ${reason}` : '')
|
|
88
113
|
);
|
|
89
114
|
this.name = 'CoordinatorStaleLossError';
|
|
90
115
|
}
|
package/src/transaction/index.ts
CHANGED
|
@@ -34,6 +34,7 @@ export {
|
|
|
34
34
|
|
|
35
35
|
export { ReadDependencyCollector } from './read-dependency-collector.js';
|
|
36
36
|
export { TransactionCoordinator } from './coordinator.js';
|
|
37
|
+
export type { CoordinatorOptions, PendValidationMode } from './coordinator.js';
|
|
37
38
|
export { CoordinatorConcurrentStampError, CoordinatorPartialCommitError, CoordinatorStaleLossError } from './errors.js';
|
|
38
39
|
export { TransactionSession } from './session.js';
|
|
39
40
|
export { TransactionValidator } from './validator.js';
|
|
@@ -0,0 +1,105 @@
|
|
|
1
|
+
import type { ActionContext, ActionId, BlockId } from "../index.js";
|
|
2
|
+
|
|
3
|
+
/** The lowest revision a block may be served at, and the action that set it.
|
|
4
|
+
*
|
|
5
|
+
* A refresh walks the log entries that landed since the collection last looked. Each entry says
|
|
6
|
+
* "action `actionId`, committed at revision `rev`, changed these blocks", and every block an action
|
|
7
|
+
* changes is committed at that action's revision. So once an entry has been walked, a read of a
|
|
8
|
+
* block it names — made at a context at or above `rev` — must come back materialized at `rev` or
|
|
9
|
+
* later; anything lower is provably not the view that was asked for. The FLOOR of a block is that
|
|
10
|
+
* bound: the revision and action id of the newest walked entry naming it. */
|
|
11
|
+
export type BlockFloor = {
|
|
12
|
+
readonly rev: number;
|
|
13
|
+
readonly actionId: ActionId;
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
/** A BELOW-FLOOR ANSWER: a block served at a revision under the floor that applied to the read. */
|
|
17
|
+
export type BelowFloorAnswer = {
|
|
18
|
+
blockId: BlockId;
|
|
19
|
+
floor: BlockFloor;
|
|
20
|
+
/** The revision the served content actually is (`servedRevision` of the answer). */
|
|
21
|
+
servedRev: number;
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
/** What a read source needs from a collection's floors: which floor applies to a read, and whether
|
|
25
|
+
* an answer fell under it. The narrow face of {@link BlockFloors} that `TransactorSource` holds. */
|
|
26
|
+
export interface BlockFloorCheck {
|
|
27
|
+
/** The floor a read of `blockId` at `context` must meet, or `undefined` when none applies.
|
|
28
|
+
*
|
|
29
|
+
* A floor applies only to a view that should CONTAIN the entry that set it: an unpinned read, or
|
|
30
|
+
* one pinned at or above the floor's revision. A read pinned strictly below it legitimately asks
|
|
31
|
+
* for an older view and is served — and remembered — exactly as if there were no floor. (The same
|
|
32
|
+
* at-or-above test `answeredBlock` applies to `unconfirmedAheadRev`.) */
|
|
33
|
+
applicableTo(blockId: BlockId, context: ActionContext | undefined): BlockFloor | undefined;
|
|
34
|
+
|
|
35
|
+
/** Weighs a served block against the floor that applies to the read that fetched it.
|
|
36
|
+
*
|
|
37
|
+
* @param servedRev the revision the served content is — `servedRevision` of the answer.
|
|
38
|
+
* @returns `true` for a below-floor answer, which the caller hands on but must not let any cache
|
|
39
|
+
* remember (see `TransactorSource.describeServed`). */
|
|
40
|
+
answeredBelowFloor(blockId: BlockId, context: ActionContext | undefined, servedRev: number): boolean;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/** The floors of one collection handle, shared by EVERY read source that handle builds — its own
|
|
44
|
+
* and each pinned read view's — the way the `ReadDependencyCollector` is shared. Held on one
|
|
45
|
+
* source only, a view created right after a refresh would fetch the changed block through a source
|
|
46
|
+
* that knows no floor.
|
|
47
|
+
*
|
|
48
|
+
* Raised where log entries are consumed (`Collection.updateInternal`) and checked where answers
|
|
49
|
+
* arrive (`TransactorSource.tryGet`). A floor, once raised, STANDS for the life of the handle: an
|
|
50
|
+
* answer that meets it does not remove it.
|
|
51
|
+
*
|
|
52
|
+
* Why a met floor is not dropped. The source that judges an answer cannot know whether any cache
|
|
53
|
+
* went on to keep it, and "an answer met the floor" is not "the cache now holds that answer": the
|
|
54
|
+
* cache drops an answer that was overtaken while in flight (`CacheSource.stillWanted`), and evicts
|
|
55
|
+
* kept content under pressure (128 blocks). Either way the next read of the block goes back to
|
|
56
|
+
* storage, where nothing says the same machine answers twice — and with the floor gone, a too-old
|
|
57
|
+
* answer to THAT read is judged against nothing and kept for good, which is the defect floors exist
|
|
58
|
+
* to prevent. Reproduced with the floor dropped when met: a too-old answer, then a current one that
|
|
59
|
+
* the cache dropped as overtaken (but which removed the floor), then a too-old one — kept, and
|
|
60
|
+
* served after storage had caught up (`refresh-below-floor.spec.ts`, "a current answer the cache
|
|
61
|
+
* dropped..."). A standing floor costs one map lookup per fetched block and is always true of a
|
|
62
|
+
* correct answer: a block never goes back below a revision it was committed at.
|
|
63
|
+
*
|
|
64
|
+
* NOTE: the map is never pruned, so it holds one small entry per distinct block named by entries
|
|
65
|
+
* this handle walked — the same ids, and so the same bound, as `CacheSource`'s never-pruned
|
|
66
|
+
* `generations` map, which `clear(entry.blockIds)` populates beside every `raise`. If that ever
|
|
67
|
+
* grows large enough to matter, prune the two together (oldest floors first); a dropped floor only
|
|
68
|
+
* forfeits the check for that block, it never serves anything wrong by itself. */
|
|
69
|
+
export class BlockFloors implements BlockFloorCheck {
|
|
70
|
+
private readonly floors = new Map<BlockId, BlockFloor>();
|
|
71
|
+
|
|
72
|
+
/** @param onBelowFloor told about every below-floor answer, from any source sharing these floors
|
|
73
|
+
* — the collection reports them as `collection:block-below-floor`. */
|
|
74
|
+
constructor(private readonly onBelowFloor?: (answer: BelowFloorAnswer) => void) {}
|
|
75
|
+
|
|
76
|
+
/** Record that the walked entry `floor` describes changed `blockIds`. Highest revision wins, so
|
|
77
|
+
* entries may be walked in any order and re-walked harmlessly. */
|
|
78
|
+
raise(blockIds: readonly BlockId[], floor: BlockFloor): void {
|
|
79
|
+
for (const blockId of blockIds) {
|
|
80
|
+
const held = this.floors.get(blockId);
|
|
81
|
+
if (held === undefined || floor.rev > held.rev) {
|
|
82
|
+
this.floors.set(blockId, floor);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
applicableTo(blockId: BlockId, context: ActionContext | undefined): BlockFloor | undefined {
|
|
88
|
+
const floor = this.floors.get(blockId);
|
|
89
|
+
return floor !== undefined && (context === undefined || context.rev >= floor.rev) ? floor : undefined;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
answeredBelowFloor(blockId: BlockId, context: ActionContext | undefined, servedRev: number): boolean {
|
|
93
|
+
const floor = this.applicableTo(blockId, context);
|
|
94
|
+
if (floor === undefined || servedRev >= floor.rev) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
this.onBelowFloor?.({ blockId, floor, servedRev });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** How many blocks have a floor. */
|
|
102
|
+
get size(): number {
|
|
103
|
+
return this.floors.size;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
@@ -1,80 +1,129 @@
|
|
|
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
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* One or more blocks reached FULL replication: every member of their cohort is now confirmed to
|
|
84
|
+
* hold the revision. The complement of the durability class on the write's own result — a write
|
|
85
|
+
* acknowledged below `full` (`WriteDurability.quorum`) is pending until these events cover its
|
|
86
|
+
* blocks, at which point a host can move it from "pending" to "saved".
|
|
87
|
+
*
|
|
88
|
+
* **Keyed by block, deliberately, never by action.** The node's record of what is still owed (the
|
|
89
|
+
* under-replication ledger) keeps ONE entry per block, at the block's highest under-replicated
|
|
90
|
+
* revision: a block written again by a LATER action replaces the earlier entry, so the earlier
|
|
91
|
+
* action's shortfall is cleared by the later action's copies and reported under the later
|
|
92
|
+
* action's `actionId` — the earlier action never fires an event of its own. A host that keys its
|
|
93
|
+
* pending state by action alone therefore waits forever for a superseded write. Hold the block ids
|
|
94
|
+
* the write's result names (`PendSuccess.blockIds`) and clear each as an event covers it; `rev`
|
|
95
|
+
* and `actionId` say which write's copies did the covering.
|
|
96
|
+
*/
|
|
97
|
+
export type BlockDurabilityReachedEvent = {
|
|
98
|
+
/** Blocks that reached full replication in this round. Today's producer reports one block per
|
|
99
|
+
* event; the list shape lets a producer report several blocks of one action together. */
|
|
100
|
+
readonly blockIds: readonly BlockId[];
|
|
101
|
+
/** The revision every cohort member now holds — the block's highest under-replicated revision,
|
|
102
|
+
* which supersedes every lower one. */
|
|
103
|
+
readonly rev: number;
|
|
104
|
+
/** The action whose recorded shortfall cleared. See the note above on superseded writes. */
|
|
105
|
+
readonly actionId: ActionId;
|
|
106
|
+
/** Best-effort; absent when the collection could not be resolved from the block's header. */
|
|
107
|
+
readonly collectionId?: CollectionId;
|
|
108
|
+
};
|
|
109
|
+
|
|
110
|
+
export type BlockDurabilityListener = (event: BlockDurabilityReachedEvent) => void;
|
|
111
|
+
|
|
112
|
+
/**
|
|
113
|
+
* The seam a host watches for "and now it is really saved": the SAME object that emits
|
|
114
|
+
* {@link CollectionChangeEvent}s for commits (`StorageRepo` implements both), so a host has one
|
|
115
|
+
* subscription point for change events, not two.
|
|
116
|
+
*/
|
|
117
|
+
export interface IBlockDurabilityNotifier {
|
|
118
|
+
/**
|
|
119
|
+
* Subscribe to full-replication events. Returns an idempotent unsubscribe. Same listener
|
|
120
|
+
* discipline as {@link IBlockChangeNotifier.onCollectionChange}: a throwing listener is logged
|
|
121
|
+
* and the rest still run. An event fires only AFTER the node's own record of the shortfall is
|
|
122
|
+
* gone, so a listener that re-reads that record sees a consistent picture.
|
|
123
|
+
*/
|
|
124
|
+
onBlockDurabilityReached(listener: BlockDurabilityListener): () => void;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
export function isBlockDurabilityNotifier(x: unknown): x is IBlockDurabilityNotifier {
|
|
128
|
+
return !!x && typeof (x as IBlockDurabilityNotifier).onBlockDurabilityReached === 'function';
|
|
129
|
+
}
|
package/src/transactor/index.ts
CHANGED