@optimystic/db-core 0.24.2 → 0.25.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 (103) hide show
  1. package/dist/src/blocks/helpers.d.ts +5 -0
  2. package/dist/src/blocks/helpers.d.ts.map +1 -1
  3. package/dist/src/blocks/helpers.js +12 -0
  4. package/dist/src/blocks/helpers.js.map +1 -1
  5. package/dist/src/cluster/membership.d.ts +7 -0
  6. package/dist/src/cluster/membership.d.ts.map +1 -1
  7. package/dist/src/cluster/membership.js +12 -8
  8. package/dist/src/cluster/membership.js.map +1 -1
  9. package/dist/src/cluster/structs.d.ts +27 -1
  10. package/dist/src/cluster/structs.d.ts.map +1 -1
  11. package/dist/src/cluster/structs.js.map +1 -1
  12. package/dist/src/collection/action.d.ts +14 -0
  13. package/dist/src/collection/action.d.ts.map +1 -1
  14. package/dist/src/collection/action.js +16 -1
  15. package/dist/src/collection/action.js.map +1 -1
  16. package/dist/src/collection/collection.d.ts +273 -4
  17. package/dist/src/collection/collection.d.ts.map +1 -1
  18. package/dist/src/collection/collection.js +427 -30
  19. package/dist/src/collection/collection.js.map +1 -1
  20. package/dist/src/collections/tree/tree.d.ts +16 -1
  21. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  22. package/dist/src/collections/tree/tree.js +19 -0
  23. package/dist/src/collections/tree/tree.js.map +1 -1
  24. package/dist/src/index.d.ts +1 -0
  25. package/dist/src/index.d.ts.map +1 -1
  26. package/dist/src/index.js +1 -0
  27. package/dist/src/index.js.map +1 -1
  28. package/dist/src/network/i-repo.d.ts +11 -1
  29. package/dist/src/network/i-repo.d.ts.map +1 -1
  30. package/dist/src/network/stale-failure.d.ts +21 -0
  31. package/dist/src/network/stale-failure.d.ts.map +1 -1
  32. package/dist/src/network/stale-failure.js +22 -0
  33. package/dist/src/network/stale-failure.js.map +1 -1
  34. package/dist/src/network/struct.d.ts +66 -21
  35. package/dist/src/network/struct.d.ts.map +1 -1
  36. package/dist/src/network/struct.js.map +1 -1
  37. package/dist/src/testing/test-transactor.d.ts +22 -0
  38. package/dist/src/testing/test-transactor.d.ts.map +1 -1
  39. package/dist/src/testing/test-transactor.js +44 -5
  40. package/dist/src/testing/test-transactor.js.map +1 -1
  41. package/dist/src/transaction/coordinator.d.ts +20 -0
  42. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  43. package/dist/src/transaction/coordinator.js +256 -113
  44. package/dist/src/transaction/coordinator.js.map +1 -1
  45. package/dist/src/transaction/operations-hash.d.ts +1 -1
  46. package/dist/src/transaction/operations-hash.js +1 -1
  47. package/dist/src/transaction/transaction.d.ts +4 -2
  48. package/dist/src/transaction/transaction.d.ts.map +1 -1
  49. package/dist/src/transactor/network-transactor.d.ts +21 -1
  50. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  51. package/dist/src/transactor/network-transactor.js +122 -29
  52. package/dist/src/transactor/network-transactor.js.map +1 -1
  53. package/dist/src/transactor/transactor-source.d.ts +11 -5
  54. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  55. package/dist/src/transactor/transactor-source.js +16 -8
  56. package/dist/src/transactor/transactor-source.js.map +1 -1
  57. package/dist/src/transform/cache-source.d.ts +13 -0
  58. package/dist/src/transform/cache-source.d.ts.map +1 -1
  59. package/dist/src/transform/cache-source.js +18 -0
  60. package/dist/src/transform/cache-source.js.map +1 -1
  61. package/dist/src/transform/digest.d.ts +18 -0
  62. package/dist/src/transform/digest.d.ts.map +1 -0
  63. package/dist/src/transform/digest.js +65 -0
  64. package/dist/src/transform/digest.js.map +1 -0
  65. package/dist/src/transform/index.d.ts +1 -0
  66. package/dist/src/transform/index.d.ts.map +1 -1
  67. package/dist/src/transform/index.js +1 -0
  68. package/dist/src/transform/index.js.map +1 -1
  69. package/dist/src/transform/tracker.d.ts +16 -0
  70. package/dist/src/transform/tracker.d.ts.map +1 -1
  71. package/dist/src/transform/tracker.js +40 -1
  72. package/dist/src/transform/tracker.js.map +1 -1
  73. package/dist/src/utility/canonical-json.d.ts +11 -0
  74. package/dist/src/utility/canonical-json.d.ts.map +1 -0
  75. package/dist/src/utility/canonical-json.js +15 -0
  76. package/dist/src/utility/canonical-json.js.map +1 -0
  77. package/dist/src/utility/lru-map.d.ts +2 -0
  78. package/dist/src/utility/lru-map.d.ts.map +1 -1
  79. package/dist/src/utility/lru-map.js +4 -0
  80. package/dist/src/utility/lru-map.js.map +1 -1
  81. package/package.json +2 -1
  82. package/src/blocks/helpers.ts +26 -13
  83. package/src/cluster/membership.ts +87 -85
  84. package/src/cluster/structs.ts +28 -1
  85. package/src/collection/action.ts +17 -0
  86. package/src/collection/collection.ts +1129 -688
  87. package/src/collections/tree/tree.ts +341 -320
  88. package/src/index.ts +24 -23
  89. package/src/network/i-repo.ts +59 -46
  90. package/src/network/stale-failure.ts +67 -43
  91. package/src/network/struct.ts +332 -270
  92. package/src/testing/test-transactor.ts +680 -638
  93. package/src/transaction/coordinator.ts +1266 -1110
  94. package/src/transaction/operations-hash.ts +1 -1
  95. package/src/transaction/transaction.ts +4 -2
  96. package/src/transactor/network-transactor.ts +123 -32
  97. package/src/transactor/transactor-source.ts +17 -9
  98. package/src/transform/cache-source.ts +20 -0
  99. package/src/transform/digest.ts +72 -0
  100. package/src/transform/index.ts +1 -0
  101. package/src/transform/tracker.ts +42 -1
  102. package/src/utility/canonical-json.ts +16 -0
  103. package/src/utility/lru-map.ts +5 -0
@@ -1,270 +1,332 @@
1
- import type { CollectionId, BlockId, IBlock, ActionId, Transform, Transforms } from "../index.js";
2
- import type { ActionContext, ActionRev } from "../collection/action.js";
3
- import type { Transaction } from "../transaction/transaction.js";
4
- import type { DisputeResolutionProof } from "../log/struct.js";
5
- import type { PeerId } from "./types.js";
6
-
7
- export type ActionBlocks = {
8
- blockIds: BlockId[];
9
- actionId: ActionId;
10
- };
11
-
12
- export type ActionTransforms = {
13
- actionId: ActionId;
14
- rev?: number;
15
- transforms: Transforms;
16
- };
17
-
18
- export type ActionTransform = {
19
- actionId: ActionId;
20
- rev?: number;
21
- transform: Transform;
22
- };
23
-
24
- export type ActionPending = {
25
- blockId: BlockId;
26
- actionId: ActionId;
27
- transform?: Transform;
28
- };
29
-
30
- export type PendRequest = ActionTransforms & {
31
- /** What to do if there are any pending actions.
32
- * 'c' is continue normally,
33
- * 'f' is fail, returning the pending ActionIds,
34
- * 'r' is return, which fails but returns the pending ActionIds and their transforms */
35
- policy: 'c' | 'f' | 'r';
36
- /** For multi-collection transactions: the full transaction for replay/validation */
37
- transaction?: Transaction;
38
- /** For multi-collection transactions: hash of ALL operations across all blocks */
39
- operationsHash?: string;
40
- /** For multi-collection transactions: supercluster nominees for consensus */
41
- superclusterNominees?: PeerId[];
42
- /**
43
- * Aged, advisory retry priority for the *single-collection* pend path (default 0 when absent).
44
- * The multi-collection path instead carries priority on {@link PendRequest.transaction}
45
- * ({@link Transaction.priority}); this top-level field is the carrier for a `Collection.sync`
46
- * pend, which has no `transaction`. A cluster member reads whichever is present as the first
47
- * `resolveRace` tiebreak. FAIRNESS-ONLY: it rides inside the signed cluster `message` (so it is
48
- * integrity-protected in transit) but MUST NOT affect the operations hash, stale-read checks, or
49
- * validity a stale pend is still rejected regardless of priority.
50
- */
51
- priority?: number;
52
- };
53
-
54
- export type BlockActionStatus = ActionBlocks & {
55
- statuses: ('pending' | 'committed' | 'checkpointed' | 'aborted' | 'committed-invalidated')[];
56
- };
57
-
58
- export type PendSuccess = {
59
- success: true;
60
- /** List of already pending actions that were found on blocks touched by this pend */
61
- pending: ActionPending[];
62
- /** The affected blocks */
63
- blockIds: BlockId[];
64
- };
65
-
66
- export type StaleFailure = {
67
- success: false;
68
- /** The reason for the failure */
69
- reason?: string;
70
- /** List of actions that have already been committed and are newer than our known revision */
71
- missing?: ActionTransforms[];
72
- /** List of actions that are pending on the blocks touched by this pend */
73
- pending?: ActionPending[];
74
- /**
75
- * Explicit retryability. True when this failure is an optimistic-concurrency loss — the
76
- * requested revision was taken, or a rival pend holds the blocks — so a re-read, rebase and
77
- * re-pend can win. Set it only when the producer genuinely classified the failure; leave it
78
- * absent otherwise, and consumers fall back to inferring from `missing`/`pending`.
79
- * Read it through `isConflictFailure` rather than testing it directly.
80
- */
81
- conflict?: boolean;
82
- /**
83
- * The block that already occupies (or is past) the requested revision, and the revision the
84
- * responder holds for it.
85
- *
86
- * CONFIRMED-ONLY: set this only when the producer read the revision out of its own storage.
87
- * A producer that merely suspects staleness or that learned of it from another peer's
88
- * free-form reject text must leave it absent. Absent means "no confirmed number", never
89
- * "not stale".
90
- *
91
- * DIAGNOSTIC, NOT A RETRYABILITY SIGNAL: `conflict` (read via `isConflictFailure`) remains the
92
- * single source of truth for "can a re-read and re-pend win?". Never branch retry decisions on
93
- * the presence of this field.
94
- */
95
- staleAt?: { blockId: BlockId; rev: number };
96
- };
97
-
98
- export type PendResult = PendSuccess | StaleFailure;
99
-
100
- export type CommitRequest = ActionBlocks & {
101
- /** The header block of the collection, if this is a new collection (commit first) */
102
- headerId?: BlockId;
103
- /** The tail block of the log (commit next) */
104
- tailId: BlockId;
105
- /** The new revision for the committed action */
106
- rev: number;
107
- };
108
-
109
- /**
110
- * Originates a compensating invalidation through the same critical-cluster consensus as any
111
- * transaction: it takes a revision slot and serializes against concurrent commits. Each member
112
- * applies it deterministically verifying {@link resolution} as an invalidation certificate, then
113
- * writing the per-block compensating revisions and appending the durable invalidation log entry.
114
- *
115
- * Carries everything a member needs to apply the reversal without trusting the originator: the
116
- * target action, the blocks it wrote, the owning collection's log, and the signed proof.
117
- */
118
- export type InvalidateRequest = {
119
- /** actionId of the committed action being reversed. */
120
- invalidatedActionId: ActionId;
121
- /** rev of the invalidated entry — pins which block revisions to roll back. */
122
- invalidatedRev: number;
123
- /** Blocks the invalidated action wrote (its commit's blockIds). */
124
- blockIds: BlockId[];
125
- /** The collection (log) the invalidated action belongs to — where the compensating entry lands. */
126
- collectionId: CollectionId;
127
- /** The invalidation certificate (challenger-wins + signed 2/3 decisive arbitrator votes). */
128
- resolution: DisputeResolutionProof;
129
- };
130
-
131
- export type CommitResult = CommitSuccess | StaleFailure;
132
-
133
- export type CommitSuccess = {
134
- success: true;
135
- /** If present, the identified collection acts as the coordinator for the multi-collection transaction */
136
- coordinatorId?: CollectionId;
137
- };
138
-
139
- export type BlockActionState = {
140
- /** The latest action that has been committed */
141
- latest?: ActionRev;
142
- /** If present, the specified actions are pending */
143
- pendings?: ActionId[];
144
- };
145
-
146
- export type BlockGets = {
147
- blockIds: BlockId[];
148
- context?: ActionContext; // Latest if this is omitted
149
- };
150
-
151
- /** Why a repo could not establish whether a block exists. Present ONLY when the repo
152
- * knows its own answer is a guess; an absent field is an authoritative answer. */
153
- export type BlockUnavailableReason =
154
- /** Records for this block exist here but it cannot be reconstructed locally — a
155
- * revision was received with no base to apply it to, or its history is truncated. */
156
- | 'unmaterializable'
157
- /** Nothing is held locally; PART of the cohort answered and part could not be asked.
158
- * A silent peer could be the sole holder, so the absence is a guess — but other
159
- * coordinators are reachable, so asking one of them can still settle it. Also the
160
- * fallback when the consult could not run at all (the cohort lookup itself failed):
161
- * a routing failure says nothing about how many cohort members were reachable. */
162
- | 'peers-unreachable'
163
- /** Nothing is held locally and NO cohort member outside the answering node could be
164
- * asked at all. Distinct from `peers-unreachable` in exactly the way that matters to
165
- * a caller: there is no better-connected coordinator to re-ask, so the answer will
166
- * not improve until that node's connectivity does. Its local view is all there is. */
167
- | 'cohort-unreachable'
168
- /** Nothing is held locally, but a cohort peer positively CLAIMED a revision of this
169
- * block, and the answering node could neither corroborate that claim to a quorum nor
170
- * acquire the content. The block is known to exist somewhere; reporting it absent
171
- * would be a lie regardless of whether anyone was silent. */
172
- | 'claimed-elsewhere';
173
-
174
- export type GetBlockResult = {
175
- /** The retrieved block - undefined if the block was deleted */
176
- block?: IBlock;
177
- /** The latest and pending states of the repo that retrieved the block */
178
- state: BlockActionState;
179
- /** The revision the returned `block` was actually materialized at — the highest committed
180
- * revision of THIS block at or below the caller's {@link BlockGets.context}`.rev`. Differs
181
- * from `state.latest.rev` only for a revision-pinned read of a block that has committed
182
- * further since the pin; for an unpinned read the two agree.
183
- *
184
- * This not `state.latest.rev` — is the revision a read observed, so it is what a read
185
- * dependency must record (recording `latest` would claim the reader saw content it never
186
- * read, and the validator's stale-read check would wrongly pass). `state.latest` keeps its
187
- * own meaning: the newest revision the answering repo holds for the block.
188
- *
189
- * Optional: a producer that does not know the materialized revision leaves it absent
190
- * rather than guessing, and consumers fall back to `state.latest?.rev ?? 0`. */
191
- materializedRev?: number;
192
- /** Set when this repo could not determine whether the block exists — its answer is a
193
- * guess, not an authoritative absent. Every producer that omits it (including
194
- * TestTransactor) keeps meaning "authoritative". */
195
- unavailable?: BlockUnavailableReason;
196
- /** Set when this repo served committed content it could NOT confirm is current: its
197
- * freshness consult did not converge AND a cohort peer claimed a strictly higher
198
- * revision than the one served, within the view the caller asked for (unpinned, or
199
- * pinned at or above the claim). Carries that claimed revision. The claim did not
200
- * drive a successful repair — it failed the read-repair corroboration quorum, or was
201
- * corroborated but the content could not be acquired — so it is evidence of DOUBT,
202
- * never a revision to adopt. Distinct from `unavailable`, which is about EXISTENCE:
203
- * the content here is real, it may just be behind. Absent = confirmed, so every
204
- * producer that omits it keeps its meaning. */
205
- unconfirmedAheadRev?: number;
206
- };
207
-
208
- /**
209
- * Thrown by a block read when the responsible repo could not determine whether the
210
- * block exists. Distinct from "the block is absent" (undefined) and from a transport
211
- * failure this node's data is genuinely indeterminate and the caller must not treat
212
- * it as empty. Not a StaleFailure: `Collection.sync` does not retry it.
213
- */
214
- export class BlockUnavailableError extends Error {
215
- constructor(readonly blockId: BlockId, readonly reason: BlockUnavailableReason) {
216
- super(`Block ${blockId} is unavailable (${reason}): the repo could not determine whether it exists`);
217
- this.name = 'BlockUnavailableError';
218
- }
219
- }
220
-
221
- /**
222
- * Thrown by an unpinned ("give me latest") block read whose surviving answer carries
223
- * {@link GetBlockResult.unconfirmedAheadRev}: every reachable coordinator served content
224
- * it could not confirm is current, while a cohort peer claimed a strictly higher revision
225
- * nothing could corroborate or refute. Sibling of {@link BlockUnavailableError} — that one
226
- * is about EXISTENCE (blockless answer, could not find out), this one about CURRENCY (real
227
- * content, possibly behind). Not a StaleFailure: `Collection.sync` does not retry it.
228
- */
229
- export class BlockPossiblyStaleError extends Error {
230
- constructor(readonly blockId: BlockId, readonly claimedRev: number) {
231
- super(`Block ${blockId} may be stale: a cohort peer claimed rev ${claimedRev} that no reachable coordinator could confirm or refute`);
232
- this.name = 'BlockPossiblyStaleError';
233
- }
234
- }
235
-
236
- export type GetBlockResults = Record<BlockId, GetBlockResult>;
237
-
238
- /**
239
- * Result of validating a transaction in a PendRequest.
240
- */
241
- export type PendValidationResult = {
242
- /** Whether validation passed */
243
- valid: boolean;
244
- /** Reason for validation failure (if valid=false) */
245
- reason?: string;
246
- };
247
-
248
- /**
249
- * Hook for validating transactions in PendRequests.
250
- *
251
- * This hook is called by the storage layer when receiving a PendRequest
252
- * that includes a transaction and operationsHash. If validation fails,
253
- * the pend operation is rejected.
254
- *
255
- * If the hook is not provided, validation is skipped (storage-only nodes).
256
- */
257
- export type PendValidationHook = (
258
- transaction: Transaction,
259
- operationsHash: string
260
- ) => Promise<PendValidationResult>;
261
-
262
- // Backward compatibility aliases (deprecated - use Action* names)
263
- /** @deprecated Use ActionBlocks instead */
264
- export type TrxBlocks = ActionBlocks;
265
- /** @deprecated Use ActionTransforms instead */
266
- export type TrxTransforms = ActionTransforms;
267
- /** @deprecated Use ActionTransform instead */
268
- export type TrxTransform = ActionTransform;
269
- /** @deprecated Use ActionPending instead */
270
- export type TrxPending = ActionPending;
1
+ import type { CollectionId, BlockId, IBlock, ActionId, Transform, Transforms } from "../index.js";
2
+ import type { ActionContext, ActionRev } from "../collection/action.js";
3
+ import type { Transaction } from "../transaction/transaction.js";
4
+ import type { DisputeResolutionProof } from "../log/struct.js";
5
+ import type { PeerId } from "./types.js";
6
+
7
+ export type ActionBlocks = {
8
+ blockIds: BlockId[];
9
+ actionId: ActionId;
10
+ };
11
+
12
+ export type ActionTransforms = {
13
+ actionId: ActionId;
14
+ rev?: number;
15
+ transforms: Transforms;
16
+ };
17
+
18
+ export type ActionTransform = {
19
+ actionId: ActionId;
20
+ rev?: number;
21
+ transform: Transform;
22
+ };
23
+
24
+ export type ActionPending = {
25
+ blockId: BlockId;
26
+ actionId: ActionId;
27
+ transform?: Transform;
28
+ };
29
+
30
+ export type PendRequest = ActionTransforms & {
31
+ /** What to do if there are any pending actions.
32
+ * 'c' is continue normally,
33
+ * 'f' is fail, returning the pending ActionIds,
34
+ * 'r' is return, which fails but returns the pending ActionIds and their transforms */
35
+ policy: 'c' | 'f' | 'r';
36
+ /**
37
+ * Present only on the multi-collection path (`TransactionCoordinator.pendCollection`): the
38
+ * transaction to re-execute plus the hash of ALL operations across all blocks it must produce.
39
+ * Absent on the single-collection `Collection.sync` path, which carries bare transforms and is
40
+ * therefore not re-checkable see `ClusterConsensusConfig.unvalidatablePendPolicy` for what a
41
+ * validating receiver does with that shape. ONE optional pair, deliberately: "transaction
42
+ * without its hash" (or the reverse) was a state the old two independent optional fields
43
+ * permitted and no producer ever created and a receiver whose guard required both could be
44
+ * talked out of validating by a sender that omitted one.
45
+ */
46
+ validation?: {
47
+ /** The full transaction for replay/validation. */
48
+ transaction: Transaction;
49
+ /** Hash of ALL operations across all blocks the re-executed transaction must produce. */
50
+ operationsHash: string;
51
+ };
52
+ /** For multi-collection transactions: supercluster nominees for consensus */
53
+ superclusterNominees?: PeerId[];
54
+ /**
55
+ * Aged, advisory retry priority for the *single-collection* pend path (default 0 when absent).
56
+ * The multi-collection path instead carries priority on the transaction inside
57
+ * {@link PendRequest.validation} ({@link Transaction.priority}); this top-level field is the
58
+ * carrier for a `Collection.sync` pend, which has no transaction. A cluster member reads whichever is present as the first
59
+ * `resolveRace` tiebreak. FAIRNESS-ONLY: it rides inside the signed cluster `message` (so it is
60
+ * integrity-protected in transit) but MUST NOT affect the operations hash, stale-read checks, or
61
+ * validity — a stale pend is still rejected regardless of priority.
62
+ */
63
+ priority?: number;
64
+ };
65
+
66
+ export type BlockActionStatus = ActionBlocks & {
67
+ statuses: ('pending' | 'committed' | 'checkpointed' | 'aborted' | 'committed-invalidated')[];
68
+ };
69
+
70
+ export type PendSuccess = {
71
+ success: true;
72
+ /** List of already pending actions that were found on blocks touched by this pend */
73
+ pending: ActionPending[];
74
+ /** The affected blocks */
75
+ blockIds: BlockId[];
76
+ };
77
+
78
+ export type StaleFailure = {
79
+ success: false;
80
+ /** The reason for the failure */
81
+ reason?: string;
82
+ /** List of actions that have already been committed and are newer than our known revision */
83
+ missing?: ActionTransforms[];
84
+ /** List of actions that are pending on the blocks touched by this pend */
85
+ pending?: ActionPending[];
86
+ /**
87
+ * Explicit retryability. True when this failure is an optimistic-concurrency loss the
88
+ * requested revision was taken, or a rival pend holds the blocks so a re-read, rebase and
89
+ * re-pend can win. Set it only when the producer genuinely classified the failure; leave it
90
+ * absent otherwise, and consumers fall back to inferring from `missing`/`pending`.
91
+ * Read it through `isConflictFailure` rather than testing it directly.
92
+ */
93
+ conflict?: boolean;
94
+ /**
95
+ * The block that already occupies (or is past) the requested revision, and the revision the
96
+ * responder holds for it.
97
+ *
98
+ * CONFIRMED-ONLY: set this only when the producer read the revision out of its own storage.
99
+ * A producer that merely suspects staleness — or that learned of it from another peer's
100
+ * free-form reject text must leave it absent. Absent means "no confirmed number", never
101
+ * "not stale".
102
+ *
103
+ * DIAGNOSTIC, NOT A RETRYABILITY SIGNAL: `conflict` (read via `isConflictFailure`) remains the
104
+ * single source of truth for "can a re-read and re-pend win?". Never branch retry decisions on
105
+ * the presence of this field.
106
+ */
107
+ staleAt?: { blockId: BlockId; rev: number };
108
+ };
109
+
110
+ export type PendResult = PendSuccess | StaleFailure;
111
+
112
+ /** What one block will materialize to at the committing revision, declared by the client that
113
+ * authored the transforms. */
114
+ export type BlockContentDigest = {
115
+ /** base64url SHA-256 of canonicalJson(block) - see canonicalBlockHash. */
116
+ digest: string;
117
+ /** Committed revision of the base the digest was computed from. ABSENT when the block's
118
+ * transform carries an insert, which makes the result base-independent and therefore
119
+ * checkable by every member regardless of how far behind it is. */
120
+ baseRev?: number;
121
+ };
122
+
123
+ /** Per-block content declarations riding on a commit. Optional per id: a block the client cannot
124
+ * digest without a network read is simply omitted, and falls back to corroboration downstream. */
125
+ export type BlockContentDigests = Record<BlockId, BlockContentDigest>;
126
+
127
+ export type CommitRequest = ActionBlocks & {
128
+ /** The header block of the collection, if this is a new collection. Nominally "commit first" —
129
+ * but see the NOTE in `NetworkTransactor.commit`: the header-first step is unreachable from the
130
+ * only production producer of this field, so today it only affects hand-built requests. */
131
+ headerId?: BlockId;
132
+ /** The tail block of the log. Nominally "commit next"; since the header-first step above never
133
+ * fires in production, this is in practice the FIRST block committed, which is the ordering
134
+ * `Collection.bootstrapContext` relies on. */
135
+ tailId: BlockId;
136
+ /** The new revision for the committed action */
137
+ rev: number;
138
+ /** Per-block content declarations for the committing action — see {@link BlockContentDigests}.
139
+ * Rides inside the consensus message, so the generic cluster message hash folds it into every
140
+ * cohort signature with no change to the hash helpers. Action-wide here; the transactor narrows
141
+ * it to each per-coordinator batch's own block ids before sending (`RepoCommitRequest`). */
142
+ // NOTE: accepted tradeoff this field stays OPTIONAL even though an omitted declaration has a real
143
+ // cost: `StorageRepo.persistProofIfContentMatches` retains no `BlockCommitProof` for a block that
144
+ // declared no digest (`commit:proof-undeclared`), and a block with no proof is refused by any
145
+ // receiver running the default `requirePushCertificate: true` (`push:reject-uncertified
146
+ // reason=no-proof`). Such a block stays readable and pullable (`handlePull` is not certificate-gated)
147
+ // and still repairs by corroboration while two or more holders remain, but it can never GAIN a
148
+ // holder by push so spread-on-churn and cohort-growth healing silently stop maintaining its
149
+ // replication factor. Kept optional anyway because: (a) required-but-nullable is the strongest form
150
+ // available and still permits `undefined`, so it does not make the bad state unrepresentable, only
151
+ // typed out loud; (b) some commits legitimately declare nothing delete-only/tombstone commits
152
+ // materialize no content, and a member on a lagging base abstains; (c) measured migration cost of
153
+ // required-but-nullable here and on `RepoCommitRequest` is >=194 `tsc --noEmit` errors across >=39
154
+ // files (39/9 in db-core, 155/30 in db-p2p, quereus-plugin-optimystic unmeasured), nearly all fixed
155
+ // by literally writing `undefined`. Revisit if undeclared commits ever become common enough to show
156
+ // up as replication-factor decay.
157
+ blockDigests?: BlockContentDigests;
158
+ };
159
+
160
+ /**
161
+ * Originates a compensating invalidation through the same critical-cluster consensus as any
162
+ * transaction: it takes a revision slot and serializes against concurrent commits. Each member
163
+ * applies it deterministically verifying {@link resolution} as an invalidation certificate, then
164
+ * writing the per-block compensating revisions and appending the durable invalidation log entry.
165
+ *
166
+ * Carries everything a member needs to apply the reversal without trusting the originator: the
167
+ * target action, the blocks it wrote, the owning collection's log, and the signed proof.
168
+ */
169
+ export type InvalidateRequest = {
170
+ /** actionId of the committed action being reversed. */
171
+ invalidatedActionId: ActionId;
172
+ /** rev of the invalidated entry — pins which block revisions to roll back. */
173
+ invalidatedRev: number;
174
+ /** Blocks the invalidated action wrote (its commit's blockIds). */
175
+ blockIds: BlockId[];
176
+ /** The collection (log) the invalidated action belongs to — where the compensating entry lands. */
177
+ collectionId: CollectionId;
178
+ /** The invalidation certificate (challenger-wins + signed 2/3 decisive arbitrator votes). */
179
+ resolution: DisputeResolutionProof;
180
+ };
181
+
182
+ export type CommitResult = CommitSuccess | StaleFailure;
183
+
184
+ export type CommitSuccess = {
185
+ success: true;
186
+ /** If present, the identified collection acts as the coordinator for the multi-collection transaction */
187
+ coordinatorId?: CollectionId;
188
+ };
189
+
190
+ export type BlockActionState = {
191
+ /** The latest action that has been committed */
192
+ latest?: ActionRev;
193
+ /** If present, the specified actions are pending */
194
+ pendings?: ActionId[];
195
+ };
196
+
197
+ export type BlockGets = {
198
+ blockIds: BlockId[];
199
+ context?: ActionContext; // Latest if this is omitted
200
+ };
201
+
202
+ /** Why a repo could not establish whether a block exists. Present ONLY when the repo
203
+ * knows its own answer is a guess; an absent field is an authoritative answer. */
204
+ export type BlockUnavailableReason =
205
+ /** Records for this block exist here but it cannot be reconstructed locally — a
206
+ * revision was received with no base to apply it to, or its history is truncated. */
207
+ | 'unmaterializable'
208
+ /** Nothing is held locally; PART of the cohort answered and part could not be asked.
209
+ * A silent peer could be the sole holder, so the absence is a guess but other
210
+ * coordinators are reachable, so asking one of them can still settle it. Also the
211
+ * fallback when the consult could not run at all (the cohort lookup itself failed):
212
+ * a routing failure says nothing about how many cohort members were reachable. */
213
+ | 'peers-unreachable'
214
+ /** Nothing is held locally and NO cohort member outside the answering node could be
215
+ * asked at all. Distinct from `peers-unreachable` in exactly the way that matters to
216
+ * a caller: there is no better-connected coordinator to re-ask, so the answer will
217
+ * not improve until that node's connectivity does. Its local view is all there is. */
218
+ | 'cohort-unreachable'
219
+ /** Nothing is held locally, but a cohort peer positively CLAIMED a revision of this
220
+ * block, and the answering node could neither corroborate that claim to a quorum nor
221
+ * acquire the content. The block is known to exist somewhere; reporting it absent
222
+ * would be a lie regardless of whether anyone was silent. */
223
+ | 'claimed-elsewhere';
224
+
225
+ export type GetBlockResult = {
226
+ /** The retrieved block - undefined if the block was deleted */
227
+ block?: IBlock;
228
+ /** The latest and pending states of the repo that retrieved the block */
229
+ state: BlockActionState;
230
+ /** The revision the returned `block` actually IS — the `(rev, actionId)` of the highest
231
+ * committed revision of THIS block at or below the caller's {@link BlockGets.context}`.rev`.
232
+ * Differs from `state.latest` only for a revision-pinned read of a block that has committed
233
+ * further since the pin; for an unpinned read the two agree.
234
+ *
235
+ * This — not `state.latest` — is what a read observed, so it is what a read dependency must
236
+ * record (recording `latest` would claim the reader saw content it never read, and the
237
+ * validator's stale-read check would wrongly pass), and it is the only correct label for the
238
+ * content when it is passed on (a block-repair archive, a replica push). `state.latest` keeps
239
+ * its own meaning: the newest revision the answering repo holds for the block.
240
+ *
241
+ * The revision and its action id are ONE field, deliberately: a site that must label content
242
+ * it is holding needs both, and two independently-optional fields could disagree — which is
243
+ * exactly the mislabel this exists to make unrepresentable (old bytes served under a newer
244
+ * revision's number and action id, which a receiver keyed by action id then writes over its
245
+ * own good copy; see `serveBlockArchive`).
246
+ *
247
+ * Optional: a producer that does not know what it materialized leaves it absent rather than
248
+ * guessing, and consumers fall back to `state.latest` (read dependencies record
249
+ * `state.latest?.rev ?? 0`; a labelling site refuses to label a pinned read). */
250
+ materialized?: ActionRev;
251
+ /** Set when this repo could not determine whether the block exists its answer is a
252
+ * guess, not an authoritative absent. Every producer that omits it (including
253
+ * TestTransactor) keeps meaning "authoritative". */
254
+ unavailable?: BlockUnavailableReason;
255
+ /** Set when this repo served committed content it could NOT confirm is current: its
256
+ * freshness consult did not converge AND a cohort peer claimed a strictly higher
257
+ * revision than the one served, within the view the caller asked for (unpinned, or
258
+ * pinned at or above the claim). Carries that claimed revision. The claim did not
259
+ * drive a successful repair — it failed the read-repair corroboration quorum, or was
260
+ * corroborated but the content could not be acquired — so it is evidence of DOUBT,
261
+ * never a revision to adopt. Distinct from `unavailable`, which is about EXISTENCE:
262
+ * the content here is real, it may just be behind. Absent = confirmed, so every
263
+ * producer that omits it keeps its meaning. */
264
+ unconfirmedAheadRev?: number;
265
+ };
266
+
267
+ /**
268
+ * Thrown by a block read when the responsible repo could not determine whether the
269
+ * block exists. Distinct from "the block is absent" (undefined) and from a transport
270
+ * failure this node's data is genuinely indeterminate and the caller must not treat
271
+ * it as empty. Not a StaleFailure: `Collection.sync` does not retry it.
272
+ */
273
+ export class BlockUnavailableError extends Error {
274
+ constructor(readonly blockId: BlockId, readonly reason: BlockUnavailableReason) {
275
+ super(`Block ${blockId} is unavailable (${reason}): the repo could not determine whether it exists`);
276
+ this.name = 'BlockUnavailableError';
277
+ }
278
+ }
279
+
280
+ /**
281
+ * Thrown by an unpinned ("give me latest") block read whose surviving answer carries
282
+ * {@link GetBlockResult.unconfirmedAheadRev}: every reachable coordinator served content
283
+ * it could not confirm is current, while a cohort peer claimed a strictly higher revision
284
+ * nothing could corroborate or refute. Sibling of {@link BlockUnavailableError} — that one
285
+ * is about EXISTENCE (blockless answer, could not find out), this one about CURRENCY (real
286
+ * content, possibly behind). Not a StaleFailure: `Collection.sync` does not retry it.
287
+ */
288
+ export class BlockPossiblyStaleError extends Error {
289
+ constructor(readonly blockId: BlockId, readonly claimedRev: number) {
290
+ super(`Block ${blockId} may be stale: a cohort peer claimed rev ${claimedRev} that no reachable coordinator could confirm or refute`);
291
+ this.name = 'BlockPossiblyStaleError';
292
+ }
293
+ }
294
+
295
+ export type GetBlockResults = Record<BlockId, GetBlockResult>;
296
+
297
+ /**
298
+ * Result of validating a transaction in a PendRequest.
299
+ */
300
+ export type PendValidationResult = {
301
+ /** Whether validation passed */
302
+ valid: boolean;
303
+ /** Reason for validation failure (if valid=false) */
304
+ reason?: string;
305
+ };
306
+
307
+ /**
308
+ * Hook for validating transactions in PendRequests.
309
+ *
310
+ * This hook is called by the storage layer when receiving a PendRequest
311
+ * that carries a `validation` payload (the transaction plus its operations
312
+ * hash). If validation fails — or the hook throws — the pend operation is
313
+ * rejected. What a hook-configured node does with a pend carrying NO
314
+ * `validation` payload is a policy decision
315
+ * (`StorageRepoOptions.unvalidatablePendPolicy` in db-p2p).
316
+ *
317
+ * If the hook is not provided, validation is skipped (storage-only nodes).
318
+ */
319
+ export type PendValidationHook = (
320
+ transaction: Transaction,
321
+ operationsHash: string
322
+ ) => Promise<PendValidationResult>;
323
+
324
+ // Backward compatibility aliases (deprecated - use Action* names)
325
+ /** @deprecated Use ActionBlocks instead */
326
+ export type TrxBlocks = ActionBlocks;
327
+ /** @deprecated Use ActionTransforms instead */
328
+ export type TrxTransforms = ActionTransforms;
329
+ /** @deprecated Use ActionTransform instead */
330
+ export type TrxTransform = ActionTransform;
331
+ /** @deprecated Use ActionPending instead */
332
+ export type TrxPending = ActionPending;