@optimystic/db-core 1.0.0-beta.2 → 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.
Files changed (180) hide show
  1. package/dist/src/cluster/structs.d.ts +62 -16
  2. package/dist/src/cluster/structs.d.ts.map +1 -1
  3. package/dist/src/cluster/structs.js +5 -3
  4. package/dist/src/cluster/structs.js.map +1 -1
  5. package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +2 -1
  6. package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -1
  7. package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +10 -3
  8. package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -1
  9. package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
  10. package/dist/src/cohort-topic/wire/codec.js +8 -2
  11. package/dist/src/cohort-topic/wire/codec.js.map +1 -1
  12. package/dist/src/collection/collection.d.ts +467 -35
  13. package/dist/src/collection/collection.d.ts.map +1 -1
  14. package/dist/src/collection/collection.js +879 -149
  15. package/dist/src/collection/collection.js.map +1 -1
  16. package/dist/src/collection/struct.d.ts +109 -4
  17. package/dist/src/collection/struct.d.ts.map +1 -1
  18. package/dist/src/collection/struct.js +88 -1
  19. package/dist/src/collection/struct.js.map +1 -1
  20. package/dist/src/collections/diary/diary.d.ts +6 -2
  21. package/dist/src/collections/diary/diary.d.ts.map +1 -1
  22. package/dist/src/collections/diary/diary.js +5 -2
  23. package/dist/src/collections/diary/diary.js.map +1 -1
  24. package/dist/src/collections/tree/struct.d.ts +192 -5
  25. package/dist/src/collections/tree/struct.d.ts.map +1 -1
  26. package/dist/src/collections/tree/struct.js +134 -0
  27. package/dist/src/collections/tree/struct.js.map +1 -1
  28. package/dist/src/collections/tree/tree.d.ts +13 -3
  29. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  30. package/dist/src/collections/tree/tree.js +109 -6
  31. package/dist/src/collections/tree/tree.js.map +1 -1
  32. package/dist/src/index.d.ts +2 -1
  33. package/dist/src/index.d.ts.map +1 -1
  34. package/dist/src/index.js +2 -1
  35. package/dist/src/index.js.map +1 -1
  36. package/dist/src/logger-registry.d.ts +57 -0
  37. package/dist/src/logger-registry.d.ts.map +1 -0
  38. package/dist/src/logger-registry.js +168 -0
  39. package/dist/src/logger-registry.js.map +1 -0
  40. package/dist/src/logger.d.ts.map +1 -1
  41. package/dist/src/logger.js +3 -0
  42. package/dist/src/logger.js.map +1 -1
  43. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  44. package/dist/src/matchmaking/wire.js +8 -2
  45. package/dist/src/matchmaking/wire.js.map +1 -1
  46. package/dist/src/network/durability.d.ts +45 -0
  47. package/dist/src/network/durability.d.ts.map +1 -0
  48. package/dist/src/network/durability.js +114 -0
  49. package/dist/src/network/durability.js.map +1 -0
  50. package/dist/src/network/i-key-network.d.ts +14 -6
  51. package/dist/src/network/i-key-network.d.ts.map +1 -1
  52. package/dist/src/network/index.d.ts +3 -0
  53. package/dist/src/network/index.d.ts.map +1 -1
  54. package/dist/src/network/index.js +3 -0
  55. package/dist/src/network/index.js.map +1 -1
  56. package/dist/src/network/lineage.d.ts +39 -0
  57. package/dist/src/network/lineage.d.ts.map +1 -0
  58. package/dist/src/network/lineage.js +61 -0
  59. package/dist/src/network/lineage.js.map +1 -0
  60. package/dist/src/network/routing-key.d.ts +24 -0
  61. package/dist/src/network/routing-key.d.ts.map +1 -0
  62. package/dist/src/network/routing-key.js +9 -0
  63. package/dist/src/network/routing-key.js.map +1 -0
  64. package/dist/src/network/stale-failure.d.ts +5 -1
  65. package/dist/src/network/stale-failure.d.ts.map +1 -1
  66. package/dist/src/network/stale-failure.js +5 -1
  67. package/dist/src/network/stale-failure.js.map +1 -1
  68. package/dist/src/network/struct.d.ts +151 -8
  69. package/dist/src/network/struct.d.ts.map +1 -1
  70. package/dist/src/network/struct.js.map +1 -1
  71. package/dist/src/reactivity/wire.d.ts.map +1 -1
  72. package/dist/src/reactivity/wire.js +8 -2
  73. package/dist/src/reactivity/wire.js.map +1 -1
  74. package/dist/src/testing/test-transactor.d.ts +64 -7
  75. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  76. package/dist/src/testing/test-transactor.js +150 -23
  77. package/dist/src/testing/test-transactor.js.map +1 -1
  78. package/dist/src/transaction/coordinator.d.ts +141 -21
  79. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  80. package/dist/src/transaction/coordinator.js +408 -122
  81. package/dist/src/transaction/coordinator.js.map +1 -1
  82. package/dist/src/transaction/errors.d.ts +39 -10
  83. package/dist/src/transaction/errors.d.ts.map +1 -1
  84. package/dist/src/transaction/errors.js +34 -9
  85. package/dist/src/transaction/errors.js.map +1 -1
  86. package/dist/src/transaction/index.d.ts +1 -0
  87. package/dist/src/transaction/index.d.ts.map +1 -1
  88. package/dist/src/transaction/index.js.map +1 -1
  89. package/dist/src/transactor/block-floors.d.ts +78 -0
  90. package/dist/src/transactor/block-floors.d.ts.map +1 -0
  91. package/dist/src/transactor/block-floors.js +62 -0
  92. package/dist/src/transactor/block-floors.js.map +1 -0
  93. package/dist/src/transactor/change-notifier.d.ts +43 -0
  94. package/dist/src/transactor/change-notifier.d.ts.map +1 -1
  95. package/dist/src/transactor/change-notifier.js +3 -0
  96. package/dist/src/transactor/change-notifier.js.map +1 -1
  97. package/dist/src/transactor/index.d.ts +1 -0
  98. package/dist/src/transactor/index.d.ts.map +1 -1
  99. package/dist/src/transactor/index.js +1 -0
  100. package/dist/src/transactor/index.js.map +1 -1
  101. package/dist/src/transactor/network-transactor.d.ts +43 -6
  102. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  103. package/dist/src/transactor/network-transactor.js +279 -81
  104. package/dist/src/transactor/network-transactor.js.map +1 -1
  105. package/dist/src/transactor/transactor-source.d.ts +94 -4
  106. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  107. package/dist/src/transactor/transactor-source.js +120 -42
  108. package/dist/src/transactor/transactor-source.js.map +1 -1
  109. package/dist/src/transactor/transactor.d.ts +14 -1
  110. package/dist/src/transactor/transactor.d.ts.map +1 -1
  111. package/dist/src/transform/atomic.d.ts.map +1 -1
  112. package/dist/src/transform/atomic.js +12 -9
  113. package/dist/src/transform/atomic.js.map +1 -1
  114. package/dist/src/transform/base-pins.d.ts +50 -17
  115. package/dist/src/transform/base-pins.d.ts.map +1 -1
  116. package/dist/src/transform/base-pins.js +58 -11
  117. package/dist/src/transform/base-pins.js.map +1 -1
  118. package/dist/src/transform/cache-source.d.ts +75 -11
  119. package/dist/src/transform/cache-source.d.ts.map +1 -1
  120. package/dist/src/transform/cache-source.js +162 -36
  121. package/dist/src/transform/cache-source.js.map +1 -1
  122. package/dist/src/transform/digest.d.ts +8 -1
  123. package/dist/src/transform/digest.d.ts.map +1 -1
  124. package/dist/src/transform/digest.js +19 -9
  125. package/dist/src/transform/digest.js.map +1 -1
  126. package/dist/src/transform/helpers.d.ts +6 -2
  127. package/dist/src/transform/helpers.d.ts.map +1 -1
  128. package/dist/src/transform/helpers.js +8 -3
  129. package/dist/src/transform/helpers.js.map +1 -1
  130. package/dist/src/transform/tracker.d.ts +94 -14
  131. package/dist/src/transform/tracker.d.ts.map +1 -1
  132. package/dist/src/transform/tracker.js +222 -68
  133. package/dist/src/transform/tracker.js.map +1 -1
  134. package/dist/src/utility/structural-equals.d.ts +38 -0
  135. package/dist/src/utility/structural-equals.d.ts.map +1 -0
  136. package/dist/src/utility/structural-equals.js +77 -0
  137. package/dist/src/utility/structural-equals.js.map +1 -0
  138. package/package.json +2 -2
  139. package/src/cluster/structs.ts +298 -255
  140. package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +260 -253
  141. package/src/cohort-topic/wire/codec.ts +222 -216
  142. package/src/collection/collection.ts +996 -158
  143. package/src/collection/struct.ts +99 -4
  144. package/src/collections/diary/diary.ts +71 -68
  145. package/src/collections/tree/struct.ts +248 -26
  146. package/src/collections/tree/tree.ts +130 -9
  147. package/src/index.ts +2 -1
  148. package/src/logger-registry.ts +224 -0
  149. package/src/logger.ts +4 -0
  150. package/src/matchmaking/wire.ts +8 -2
  151. package/src/network/durability.ts +123 -0
  152. package/src/network/i-key-network.ts +60 -52
  153. package/src/network/index.ts +10 -7
  154. package/src/network/lineage.ts +80 -0
  155. package/src/network/routing-key.ts +27 -0
  156. package/src/network/stale-failure.ts +5 -1
  157. package/src/network/struct.ts +156 -8
  158. package/src/reactivity/wire.ts +230 -224
  159. package/src/testing/test-transactor.ts +152 -24
  160. package/src/transaction/coordinator.ts +470 -131
  161. package/src/transaction/errors.ts +34 -9
  162. package/src/transaction/index.ts +1 -0
  163. package/src/transactor/block-floors.ts +105 -0
  164. package/src/transactor/change-notifier.ts +129 -80
  165. package/src/transactor/index.ts +1 -0
  166. package/src/transactor/network-transactor.ts +297 -84
  167. package/src/transactor/transactor-source.ts +131 -42
  168. package/src/transactor/transactor.ts +13 -1
  169. package/src/transform/atomic.ts +25 -22
  170. package/src/transform/base-pins.ts +76 -18
  171. package/src/transform/cache-source.ts +327 -194
  172. package/src/transform/digest.ts +21 -10
  173. package/src/transform/helpers.ts +8 -3
  174. package/src/transform/tracker.ts +423 -270
  175. package/src/utility/structural-equals.ts +72 -0
  176. package/dist/src/utility/block-id-to-bytes.d.ts +0 -3
  177. package/dist/src/utility/block-id-to-bytes.d.ts.map +0 -1
  178. package/dist/src/utility/block-id-to-bytes.js +0 -7
  179. package/dist/src/utility/block-id-to-bytes.js.map +0 -1
  180. 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 via consensus before the failure (NOT rolled back). */
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 this attempt (local state reverted for retry). */
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 underlying commit-phase failure that aborted the commit. */
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 can. The
74
- * coordinator's built-in backoff+jitter retry catches this internally and re-drives after re-reading
75
- * fresh revisions; it only escapes to the caller once the retry budget (`maxAttempts` / `deadlineMs`)
76
- * is exhausted, at which point it signals "gave up after a clean loss" rather than a partial split.
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 failed on a clean stale loss (no collection durably committed) ` +
87
- `for [${failedCollections.join(', ')}]` + (reason ? ` — ${reason}` : '')
111
+ `Multi-collection commit lost a stale race for [${failedCollections.join(', ')}]` +
112
+ (reason ? ` — ${reason}` : '')
88
113
  );
89
114
  this.name = 'CoordinatorStaleLossError';
90
115
  }
@@ -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
+ }
@@ -1,3 +1,4 @@
1
+ export * from "./block-floors.js";
1
2
  export * from "./change-notifier.js";
2
3
  export * from "./network-transactor.js";
3
4
  export * from "./transactor-source.js";