@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
@@ -1,10 +1,54 @@
1
- import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms, ActionId } from "../index.js";
1
+ import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms, ActionId, BlockContentDigests } from "../index.js";
2
2
  import { Tracker } from "../transform/tracker.js";
3
+ import type { BasePins } from "../transform/base-pins.js";
3
4
  import { CacheSource } from "../transform/cache-source.js";
5
+ import type { BlockBaseRevs } from "../network/struct.js";
4
6
  import { TransactorSource } from "../transactor/transactor-source.js";
7
+ import { BlockFloors } from "../transactor/block-floors.js";
8
+ import type { WriteDurability } from "../network/struct.js";
5
9
  import type { CollectionId, ICollection, SyncOptions } from "./index.js";
6
10
  import type { ActionContext } from "./action.js";
7
11
  import type { ReadDependency } from "../transaction/transaction.js";
12
+ /** Exactly what one failed write attempt sent to the transactor — enough to send the same thing
13
+ * again. Retained by {@link Collection.retainInFlightAttempt} so that a refresh which then finds
14
+ * this attempt's own log entry can land the blocks the attempt left behind, at the same action id
15
+ * and revision (see {@link Collection.completeOwnEntry}). */
16
+ export type InFlightAttempt = {
17
+ /** The revision the attempt's log entry was stamped with, and pended and committed at. */
18
+ rev: number;
19
+ /** Every transform the attempt pended — the log blocks included. The caller hands over a copy
20
+ * it will not mutate; the collection keeps it as given. */
21
+ transforms: Transforms;
22
+ /** The attempt's log tail block, committed first. */
23
+ tailId: BlockId;
24
+ /** The per-block content declarations the attempt's commit carried, if any. */
25
+ blockDigests?: BlockContentDigests;
26
+ /** The per-block bases the attempt's pend carried, if any (see `PendRequest.baseRevs`). Kept
27
+ * with the transforms because they describe them: a re-send of these operations must name the
28
+ * same bases they were computed against. */
29
+ baseRevs?: BlockBaseRevs;
30
+ };
31
+ /** What one refresh ({@link Collection.refreshInFlight}, and the refresh inside a sync) found out
32
+ * about the write in flight on the instance's behalf. The caller hands in an empty report and the
33
+ * refresh fills it in AS IT GOES, rather than returning it at the end, so a refresh that throws
34
+ * after it saved the write still says so: the write is saved in storage whatever happens to this
35
+ * instance's local bookkeeping afterwards.
36
+ *
37
+ * NOTE: filled in, not returned, because a return value is lost on a throw — and a refresh CAN
38
+ * throw after finishing its own entry (the invalidation read and the replay both run later). */
39
+ export type RefreshReport = {
40
+ /** Set once the refresh found the in-flight write's own log entry and FINISHED it
41
+ * ({@link Collection.completeOwnEntry}): every block the entry names holds the write, so it is
42
+ * saved. Left unset on every other refresh, including every reader's.
43
+ *
44
+ * `durability` is who holds the finished write, when the refresh learned it. It is absent when
45
+ * finishing found every block already holding the write without re-sending anything (the
46
+ * status-read fallback), which is still a saved write — so test the field, never `durability`,
47
+ * for "saved". */
48
+ ownEntryFinished?: {
49
+ durability?: WriteDurability;
50
+ };
51
+ };
8
52
  export type CollectionInitOptions<TAction> = {
9
53
  modules: Record<ActionType, ActionHandler<TAction>>;
10
54
  createHeaderBlock: (id: BlockId, store: BlockStore<IBlock>) => IBlock;
@@ -42,6 +86,12 @@ export interface ReadViewOptions {
42
86
  export interface CollectionSnapshot<TAction> {
43
87
  /** Deep-cloned tracker transforms at snapshot time. */
44
88
  transforms: Transforms;
89
+ /** The bases the transforms' update operations were computed against (see {@link BasePins}),
90
+ * copied at snapshot time. Restored with the transforms, so a restore never pairs operations
91
+ * with the bases of some later re-stage: the pend would then declare a base the operations
92
+ * were not built on. Absent on a snapshot built by hand; such a restore keeps whatever pins
93
+ * the tracker holds for the restored ids. */
94
+ pins?: BasePins;
45
95
  /** Pending actions queued at snapshot time. */
46
96
  pending: Action<TAction>[];
47
97
  /** The committed boundary (action context) the staged state sat on when captured.
@@ -61,6 +111,10 @@ export declare class Collection<TAction> implements ICollection<TAction> {
61
111
  private readonly sourceCache;
62
112
  /** Tracked Changes */
63
113
  readonly tracker: Tracker<IBlock>;
114
+ /** What each block named by a walked log entry must be at least as new as — raised by
115
+ * {@link updateInternal}, and shared with every read source this handle builds (see
116
+ * {@link BlockFloors}). */
117
+ private readonly floors;
64
118
  private readonly filterConflict?;
65
119
  /** Short random tag naming THIS instance (see {@link newInstanceTag}). Open paths generate
66
120
  * it BEFORE construction (so pre-construction diagnostics such as attachToLog can carry it);
@@ -70,36 +124,66 @@ export declare class Collection<TAction> implements ICollection<TAction> {
70
124
  private readonly latchId;
71
125
  /** The action id of a write currently in flight ON THIS INSTANCE'S BEHALF, or `undefined`
72
126
  * outside a write. Read by {@link updateInternal}: if the committed log now carries an entry
73
- * under this id, that action's work is already durable despite the failure answer that sent us
74
- * back here — `NetworkTransactor.commit` commits the collection header and log tail BEFORE
75
- * sweeping the remaining blocks, so a later sweep block confirming a conflict reports failure
76
- * over an action whose log entry already landed. Such an entry is CONSUMED
77
- * ({@link consumeOwnEntry}) rather than replayed, because replaying re-appends content the
78
- * committed tail already carries, producing a duplicate entry under one action id at two
79
- * revisions.
127
+ * under this id, that action's LOG TAIL landed despite the failure answer that sent us back here
128
+ * — `NetworkTransactor.commit` commits the log tail BEFORE sweeping the remaining blocks, and
129
+ * reports failure both when the tail itself was refused after landing on a minority
130
+ * (`commit-not-durable`, in which case the sweep never ran) and when a later sweep block
131
+ * confirmed a conflict. Such an entry is never REPLAYED, because replaying re-appends content
132
+ * the committed tail already carries, producing a duplicate entry under one action id at two
133
+ * revisions. But it is not proof the write is saved either: the entry proves only that the tail
134
+ * landed, and the writer's own cancel has since dropped the pending records of every block that
135
+ * did not. So the refresh first FINISHES the action ({@link completeOwnEntry}) — landing the
136
+ * remaining blocks at the same action id and revision, from {@link inFlightAttempt} — and only
137
+ * then consumes the entry ({@link consumeOwnEntry}).
80
138
  *
81
139
  * The collection owns this fact rather than taking it as a `updateInternal` argument so that no
82
140
  * refresh path can forget to supply it — {@link update} and {@link updateAndSync} are refreshes
83
141
  * on behalf of a READER, the field is unset for them, and the consume branch cannot fire. Before
84
142
  * this was a field, `TransactionCoordinator.commit`'s inter-attempt refresh went through
85
143
  * `update()` and was therefore indistinguishable from a reader refresh even though the
86
- * coordinator held the very id it was retrying.
144
+ * coordinator held the very id it was retrying. (It now goes through {@link refreshInFlight},
145
+ * which differs from `update()` only in reporting what the refresh saved — the field, not the
146
+ * method, is still what makes the refresh recognise the entry.)
87
147
  *
88
148
  * LIFETIME is the whole attempt CYCLE, not the latched span: it must survive the refresh
89
149
  * BETWEEN a failed attempt and its retry, which is the only moment it is ever read. In
90
150
  * {@link syncInternal} that cycle is contained inside the collection latch `sync()` holds; in
91
- * `TransactionCoordinator.commit` the inter-attempt `update()` runs OUTSIDE the commit latch
151
+ * `TransactionCoordinator.commit` the inter-attempt refresh runs OUTSIDE the commit latch
92
152
  * span by design (`Latches` is non-reentrant), so the coordinator's clear necessarily runs
93
153
  * latch-free. That is safe: this is a single field write, {@link beginInFlightAction}'s
94
154
  * disposer only clears an id it still owns, and the only reader runs under the latch — so the
95
155
  * worst a foreign concurrent refresh can see is a cleared field (it stops consuming), never a
96
156
  * field it should not have consumed. */
97
157
  private inFlightActionId?;
158
+ /** The most recent FAILED attempt made under {@link inFlightActionId} — exactly what it sent,
159
+ * kept so a refresh that finds that attempt's own log entry can finish the action instead of
160
+ * assuming it is finished (see {@link completeOwnEntry}). Set by {@link retainInFlightAttempt},
161
+ * cleared with the mark by {@link beginInFlightAction}'s disposer, and meaningless without it.
162
+ *
163
+ * The transforms are retained VERBATIM, not rebuilt: a rebuilt attempt re-appends the log entry
164
+ * with a fresh timestamp, so its log tail would differ byte-for-byte from the one already
165
+ * stored under the same action id and revision — and any replica that had not yet stored the
166
+ * tail would then store the second version, leaving two contents under one `(action, revision)`.
167
+ *
168
+ * Only the latest attempt is kept. An own entry can only be visible at the revision of an
169
+ * attempt whose tail landed, and a later attempt at a DIFFERENT revision is only made after a
170
+ * refresh adopted somebody else's entry at the earlier one — which is proof the earlier tail did
171
+ * not land. {@link completeOwnEntry} still checks the revision and refuses on a mismatch. */
172
+ private inFlightAttempt?;
173
+ /** The log tail block id the most recent header read named. A refresh asks for this block in the
174
+ * same request as the header ({@link readLogEnds}): the tail id only changes when the tail block
175
+ * fills, so an idle refresh is one request rather than two. A stale value costs one extra request
176
+ * and nothing else. */
177
+ private logTailId?;
98
178
  protected constructor(id: CollectionId, transactor: ITransactor, handlers: Record<ActionType, ActionHandler<TAction>>, source: TransactorSource<IBlock>,
99
179
  /** Cache of unmodified blocks from the source */
100
180
  sourceCache: CacheSource<IBlock>,
101
181
  /** Tracked Changes */
102
- tracker: Tracker<IBlock>, filterConflict?: ((action: Action<TAction>, potential: Action<TAction>[]) => Action<TAction> | undefined) | undefined,
182
+ tracker: Tracker<IBlock>,
183
+ /** What each block named by a walked log entry must be at least as new as — raised by
184
+ * {@link updateInternal}, and shared with every read source this handle builds (see
185
+ * {@link BlockFloors}). */
186
+ floors: BlockFloors, filterConflict?: ((action: Action<TAction>, potential: Action<TAction>[]) => Action<TAction> | undefined) | undefined,
103
187
  /** Short random tag naming THIS instance (see {@link newInstanceTag}). Open paths generate
104
188
  * it BEFORE construction (so pre-construction diagnostics such as attachToLog can carry it);
105
189
  * the default covers direct construction in tests. */
@@ -132,6 +216,13 @@ export declare class Collection<TAction> implements ICollection<TAction> {
132
216
  /** The per-instance read wiring every open path needs, plus the header probe result.
133
217
  * Shared by {@link open} and {@link createOrOpen} so the two cannot drift. */
134
218
  private static probeHeader;
219
+ /** A new handle's floors: none yet (opening walks no entries), wired to report every below-floor
220
+ * answer any of the handle's read sources receives. The line is the only trace such an answer
221
+ * leaves — the read itself succeeds (see the accepted-tradeoff NOTE at
222
+ * `TransactorSource.mayRetain`) — so repeated lines for one block with `servedRev` short of
223
+ * `floorRev` are how an operator sees a machine that has not caught up, and lines that never
224
+ * stop are how they see a log entry whose blocks never landed. */
225
+ private static newFloors;
135
226
  /** Walk an existing collection's log and point the source at its latest action context.
136
227
  * A header we just probed successfully but whose log will not open is a fault, not an
137
228
  * absence — throw rather than let the collection read as empty. (The re-read goes through
@@ -253,8 +344,102 @@ export declare class Collection<TAction> implements ICollection<TAction> {
253
344
  private internalTransact;
254
345
  /** Load external changes and update our context to the latest log revision - resolve any conflicts with our pending actions. */
255
346
  update(): Promise<void>;
256
- /** Drops the pending actions this sync's OWN committed entry already made durable, instead of
257
- * replaying them into a duplicate entry (see {@link inFlightActionId}).
347
+ /** The refresh `TransactionCoordinator.commit` runs between attempts: exactly {@link update},
348
+ * plus a report of whether it finished the write in flight on this instance's behalf (see
349
+ * {@link RefreshReport}). The coordinator needs that fact to tell its caller which participants
350
+ * are already saved when the commit later fails; a reader's `update()` has no write in flight
351
+ * and nothing to report.
352
+ *
353
+ * `report` is REQUIRED so a write path cannot refresh without learning what the refresh saved.
354
+ * It is filled in as the refresh goes, so it is accurate when this throws too.
355
+ *
356
+ * `lastChance` is REQUIRED for the same reason: only the caller knows whether its retry budget
357
+ * ends with this round, and a round that is the last must settle a half-landed write rather than
358
+ * ask for another (see {@link completeOwnEntry}). */
359
+ refreshInFlight(report: RefreshReport, lastChance: boolean): Promise<void>;
360
+ /** Finishes a half-landed write BEFORE its own log entry is consumed, so that consuming never
361
+ * reports a write as saved on the strength of its log entry alone.
362
+ *
363
+ * THE RULE: a write may be reported saved only if EVERY block its log entry names holds the
364
+ * write — at the write's own revision, or at a later revision that was BUILT FROM it. Finding the
365
+ * entry proves only that the log tail landed (see {@link inFlightActionId} for the two ways
366
+ * `NetworkTransactor.commit` answers failure over a stored tail). The writer's cancel has since
367
+ * removed the pending records of every block that did not land, so nothing else will ever land
368
+ * them — if this does not, the entry stands in the log, the blocks stay at their previous
369
+ * revision on every node, and readers materialize blocks, not log entries: the write is
370
+ * silently gone.
371
+ *
372
+ * Finishing is a plain re-send of the retained failed attempt ({@link inFlightAttempt}) — the
373
+ * SAME transforms, action id, revision and tail. Every storage tier treats a block that already
374
+ * holds exactly this action at exactly this revision as satisfied rather than as a rival
375
+ * (`isOwnRevision`: `StorageRepo.pend`/`.commit`, `ClusterMember`, `CoordinatorRepo`), so the
376
+ * re-send rolls forward precisely the blocks that are missing and is a no-op for the rest,
377
+ * including when nothing is missing at all. It is never sent at a new revision: the refresh has
378
+ * already seen the entry at this one, and a second revision would record the entry twice.
379
+ *
380
+ * A refused re-send is NOT yet an answer. Storage refuses it whenever any block has moved past
381
+ * the write's revision — and every later commit to the collection moves the log tail past it —
382
+ * which says a rival was there, not whether the rival built on this write or over it. That is
383
+ * asked separately, of the blocks' own history ({@link settleUnfinished}).
384
+ *
385
+ * Runs at the top of {@link updateInternal}, before that method has changed anything on this
386
+ * instance, so every throw from here leaves the collection exactly as the failed attempt left
387
+ * it — staged actions and transforms intact, revision not advanced.
388
+ *
389
+ * @param lastChance - the caller will not refresh again for this write (its retry budget ends
390
+ * with this round), so a refusal that could clear is settled now instead of asking for another
391
+ * round: the error that escapes then says whether the write can still land.
392
+ * @returns who holds the finished write, for the sync to report.
393
+ * @throws TornActionError — see {@link TornActionReason} for the three causes. Only an unsettled
394
+ * `completion-refused` (always `final: false`) is retried by the write paths.
395
+ *
396
+ * NOTE: the re-send costs a full pend and commit round even when every block had in fact landed
397
+ * (a lost or masked success). That is deliberate — it is the source of the durability the sync
398
+ * reports when nothing has superseded the write — and the case is rare: after a returned failure
399
+ * the network transactor has, by construction, NOT swept every block. If own-entry refreshes ever
400
+ * show up as a cost, ask `getLineage` first and skip the re-send when every block contains the
401
+ * write. */
402
+ private completeOwnEntry;
403
+ /** Decides what a write that can no longer be finished BY RE-SENDING amounts to, and answers
404
+ * one of exactly three things — never a guess between them:
405
+ *
406
+ * - SAVED (returns): every block the entry names holds content built from the write, on a
407
+ * strict majority of its cohort (`ITransactor.getLineage`). The rival that refused the re-send
408
+ * had read this write and added to it. Returns who holds it.
409
+ * - TORN AND FINAL (throws, `final: true`): every block still missing the write answered, for its
410
+ * whole cohort, that it does not hold it — and the write's pending records were confirmed gone
411
+ * BEFORE the blocks were asked. The order is the point: a pending record left standing can be
412
+ * promoted by any later read that knows this write's log entry is committed
413
+ * (`StorageRepo.get`), so a block that answered "not reached" could still take the write
414
+ * afterwards. With the records gone first, nothing is left that could land it.
415
+ * - TORN, OUTCOME NOT ESTABLISHED (throws, `final: false`): anything else — the transactor
416
+ * cannot answer for lineage, a cohort did not all answer or contradicted itself, fewer than a
417
+ * majority hold the write, or the cancel could not be confirmed.
418
+ *
419
+ * `tailId` is the log block this write's entry was appended to, when known. It is never asked
420
+ * about: the refresh has just READ the entry out of it, which is direct evidence that the
421
+ * block's current content was built from the write, whatever any member's records can prove.
422
+ *
423
+ * NOTE: a write recognised as saved here reports the durability `getLineage` assembled. When
424
+ * the log block's own cohort could not vouch for it (members that took it as a replica), that is
425
+ * absent and the sync answers `undefined` for a saved write — the same wart the status-read
426
+ * branch of {@link completeOwnEntry} documents. If it is ever seen, fold the data blocks'
427
+ * reports alone rather than inventing one for the log block. */
428
+ private settleUnfinished;
429
+ /** Cancels every pending record this write may have left, and says whether that is CONFIRMED.
430
+ * `ITransactor.cancel` returns only once the records are gone and throws otherwise; a throw here
431
+ * is reported as "not confirmed" rather than raised, because the caller is already reporting a
432
+ * torn write and that must not be displaced. Cancelling a block that holds no record is a no-op,
433
+ * so naming every block the entry lists is safe. */
434
+ private dischargeOwnPendings;
435
+ /** What the blocks' own history says about this write, or `undefined` when nothing can say: the
436
+ * transactor (or a wrapper around it) does not offer `getLineage`, or the question failed. */
437
+ private lineageOfOwnEntry;
438
+ private throwTorn;
439
+ /** Drops the pending actions this sync's OWN committed entry made durable, instead of replaying
440
+ * them into a duplicate entry (see {@link inFlightActionId}). Only ever called once
441
+ * {@link completeOwnEntry} has returned for this entry — the entry alone is not proof the write
442
+ * is saved.
258
443
  *
259
444
  * `addActions` wrote exactly the snapshot pending list under this action id, and the entry's
260
445
  * actions are therefore the LEADING `entry.actions.length` items of `this.pending` — anything
@@ -283,8 +468,141 @@ export declare class Collection<TAction> implements ICollection<TAction> {
283
468
  *
284
469
  * Takes no in-flight action id — it reads {@link inFlightActionId} off `this`, which is set for
285
470
  * exactly the write attempt cycles that own one (see that field). Callers cannot get this wrong
286
- * by omission. */
471
+ * by omission.
472
+ *
473
+ * @param report - Filled in as the refresh goes (see {@link RefreshReport}): its
474
+ * `ownEntryFinished` is set the moment the refresh has found this write's own log entry and
475
+ * finished it ({@link completeOwnEntry}) — before the entry is consumed, so a later throw from
476
+ * here still leaves it set. Never set on a reader's refresh. A caller with no use for it passes `{}`.
477
+ * @param lastChance - Whether the write in flight will get no further refresh (see
478
+ * {@link completeOwnEntry}). Meaningless, and left false, on a reader's refresh.
479
+ * @throws TornActionError when it found that entry and could not finish the action — thrown
480
+ * before anything on this instance changed, and with `report` untouched. */
287
481
  private updateInternal;
482
+ /** Forget every block the refresh saw change, floor the ones a log entry names, and adopt the
483
+ * revision the log is at — ONE synchronous step, which must stay free of any `await`.
484
+ *
485
+ * Reads are not latched, so one can run while a refresh is under way. Forgetting a block while
486
+ * this handle still reads at the revision it is LEAVING invites exactly the wrong re-read: the
487
+ * block comes back as it was at that revision (correctly — that is what was asked for, and no
488
+ * floor applies to a read below it), the cache keeps it, and the advance that follows turns it
489
+ * into old content nothing will ever clear, the entry that named it having been consumed. That
490
+ * needs no lagging machine; storage can be perfectly current. With no gap between the forgetting
491
+ * and the adopting, a concurrent read either lands before both and is forgotten with the rest,
492
+ * or lands after both and is judged at the adopted revision — against the floor
493
+ * ({@link TransactorSource.tryGet} reads its context when the answer arrives, not when it was
494
+ * asked for) and against the generation the clear moved (`CacheSource.stillWanted`).
495
+ *
496
+ * The advance is monotonic (see {@link advanceContext}): an empty or unopenable log yields no
497
+ * context at all, and a log read that lags what this handle already committed yields an older
498
+ * one; neither is grounds for forgetting the revision held. It also has to precede
499
+ * {@link replayActions}, which re-reads blocks through `this.source` at whatever revision the
500
+ * context names — replaying first would refill the cache at the revision being left. */
501
+ private forgetAndAdopt;
502
+ /** The revision each action in `context` committed at, keyed by action id. `Log.getFrom` returns
503
+ * entries without their revisions; the context the same walk built is where they are restated. */
504
+ private static revisionsByAction;
505
+ /** Records that the walked `entry` changed the blocks it names, so a later answer for one of
506
+ * them that is older than the entry is recognised and never remembered (see {@link BlockFloors}).
507
+ * Beside the cache clear on purpose (see {@link forgetAndAdopt}): the clear is what sends the next
508
+ * read of these blocks back to storage, and that read is the one a lagging machine can answer too
509
+ * old — after which nothing would clear the block again, this entry having been consumed.
510
+ *
511
+ * NOTE: an entry whose revision the walk did not restate sets no floor. That is an entry older
512
+ * than the log's most recent checkpoint, and no checkpoint is written today
513
+ * (tickets/backlog/debt-the-collection-log-never-writes-a-checkpoint). Once they are, such an
514
+ * entry's blocks go unguarded unless `Log.getFrom` starts returning each entry's revision. */
515
+ private raiseFloors;
516
+ /** Whether the log tail a refresh just read proves the log holds nothing newer than `held` —
517
+ * the test that lets a refresh with nothing to find stop after one request instead of walking
518
+ * the log.
519
+ *
520
+ * Sound because every commit and every invalidation appends a log entry, and entries only ever
521
+ * go on the tail block. A tail block that is still the end of the chain (no `nextId`), whose
522
+ * newest entry is at the held revision and names the action `held` names there — and whose own
523
+ * claim (`state.latest`) says the same — has nothing above `held`. The walk would then find no
524
+ * entries, no invalidations, and a context {@link advanceContext} adopts at an unchanged
525
+ * revision, so it would change nothing.
526
+ *
527
+ * Says "no" — sending the refresh down the full walk — whenever the tail and the held context
528
+ * disagree in any way, so everything that walk reports still fires:
529
+ * - a claim above `held` (ordinary catch-up) or below it (a lagging read);
530
+ * - a newest entry that is not the claimed action, or not an action at all: the entries lag the
531
+ * claim (`collection:context-short-of-tail`, or `collection:context-not-lowered` for a handle
532
+ * pinned by an over-claiming tail), or an invalidation or checkpoint took the newest slot;
533
+ * - any action entry in the tail block naming a different action than `held` names at the same
534
+ * revision (`collection:lineage-divergence`, and the walk's adoption of the log's list).
535
+ *
536
+ * NOTE: the lineage comparison sees only the entries the tail block holds. A fork below them, on
537
+ * a log that has not moved, is not looked at again until a refresh finds something new — that
538
+ * walk compares the whole list. Walking every time to look is exactly the cost this avoids.
539
+ *
540
+ * NOTE: an invalidation entry in the newest slot never matches (it names no action), so every
541
+ * refresh after one walks the log until the next commit lands. Fine while disputes are rare; if
542
+ * they are not, match an invalidation slot against the held revision too. */
543
+ private static tailShowsNothingNewer;
544
+ /** Whether any action entry in `entries` names a different action than `held` names at the same
545
+ * revision. A revision only one side names is missing evidence, not disagreement — the rule
546
+ * {@link earliestFork} applies too. */
547
+ private static disagreesWithHeld;
548
+ /** Whether {@link updateInternal} must re-stage `pending` after adopting `latest`, given the
549
+ * context it held BEFORE the refresh.
550
+ *
551
+ * The answer is deliberately NOT just "a conflict was found". A conflict is detected by an
552
+ * incoming log entry naming a block this tracker already holds a transform for — so an action
553
+ * that changed NO block can never register one. A staged delete of a key this instance cannot
554
+ * see is exactly that action: the tree's `replace` handler misses on `find` and `deleteAt`
555
+ * returns false without writing. Gated on conflicts alone, such an action stayed in `pending`
556
+ * unapplied until the commit wrote a log entry listing it whose transforms did nothing — and
557
+ * readers materialize blocks, not log entries, so the action was lost on every node, silently
558
+ * and permanently. The invariant that has to hold is that **a pending action was applied
559
+ * against the revision it commits over**, so a mere revision advance is reason enough.
560
+ *
561
+ * The advance test rests on {@link advanceContext} being monotonic (it refuses to lower), which
562
+ * is what makes "the adopted rev differs from the held one" mean "it went up".
563
+ *
564
+ * The second conjunct is `pending.length` and deliberately NOT {@link hasUnsyncedChanges},
565
+ * which also counts tracker transforms. A collection {@link createOrOpen} just INVENTED holds
566
+ * its staged header/root in the tracker with NO pending action naming them, and
567
+ * {@link replayActions} resets the tracker before re-staging — so counting transforms here
568
+ * would drop those blocks and leave a brand-new collection unreadable (the same hazard
569
+ * {@link snapshotPending} documents). Nothing to re-stage means nothing to replay.
570
+ *
571
+ * NOTE: this makes a refresh that adopts a newer revision O(pending) rather than free, so a
572
+ * read taken mid-transaction while a rival keeps committing re-stages every action staged so
573
+ * far, on each such read. Measured as no change to the storage-op budgets
574
+ * (`index-backfill-cost.spec.ts`, `cold-apply-cost.spec.ts`), because the replay's reads are
575
+ * served from `sourceCache`. If a long transaction's reads ever show up as slow, narrow the
576
+ * replay to the actions whose reads the adopted entries actually invalidated — NOT back to
577
+ * conflicts alone, which is the defect above. */
578
+ private mustReplay;
579
+ /** Re-judge every staged update's pinned base against the read cache and re-stage the pending
580
+ * queue if any has moved. Runs immediately before each pend attempt — {@link syncAttempts} and
581
+ * the coordinator's commit span both call it — because that is the last moment before the
582
+ * base the pend declares is put on the wire, and nothing between a refused attempt and the
583
+ * re-pend reads the block again (the refresh may have moved nothing and so replayed nothing).
584
+ *
585
+ * Two steps. First, one read for each pinned block the cache does NOT retain — content handed
586
+ * through unkept (a below-floor answer, which the cache re-asks for on every read anyway): the
587
+ * pin names the revision last served, storage may have caught up since without the log moving,
588
+ * and only a read can tell. An evicted-but-kept base needs no read: its content met every floor
589
+ * the handle knows, so storage cannot have moved it without a log entry the next refresh will
590
+ * walk (and the pend would be refused as stale on revision alone). Then, one cache probe per
591
+ * pinned block ({@link Tracker.movedBases}) and a replay if any moved.
592
+ *
593
+ * Latch-free by contract, like {@link snapshotPending}: the caller holds this instance's latch
594
+ * ({@link replayActions} is always run under it).
595
+ *
596
+ * NOTE: reads are not latched, so a concurrent read can still move a base between this check
597
+ * and the pend. The pend then declares the pin's (old) revision, storage refuses it, and the
598
+ * next attempt's call here re-stages — one wasted round trip, never a wrong base. If that
599
+ * refusal ever shows up in practice, the closure is to hold the pend under the read latch.
600
+ *
601
+ * @returns whether the pending queue was re-staged. */
602
+ restageIfBasesMoved(): Promise<boolean>;
603
+ /** Report each moved base, naming the revision the operations were computed against and the
604
+ * one the cache describes now, and return how many there were. */
605
+ private logMovedBases;
288
606
  /** Capture the current staged state — tracker transforms plus the pending
289
607
  * action queue — so it can be restored later via {@link restorePending}.
290
608
  *
@@ -301,7 +619,30 @@ export declare class Collection<TAction> implements ICollection<TAction> {
301
619
  snapshotPending(): CollectionSnapshot<TAction>;
302
620
  /** Restore the staged state captured by {@link snapshotPending}, discarding any
303
621
  * mutations staged since. Reads through the collection then observe exactly the
304
- * snapshot state again; storage is untouched because nothing was ever synced. */
622
+ * snapshot state again; storage is untouched because nothing was ever synced.
623
+ *
624
+ * A snapshot is only restorable VERBATIM onto the committed boundary it was captured
625
+ * on. If this collection has since ADOPTED a newer committed revision — a rival's
626
+ * commit folded in by a refresh while the snapshot's transaction was in flight, e.g.
627
+ * the conflict replay that refused a guarded insert (TreeKeyTakenError) — the
628
+ * snapshot's transforms describe block state at the OLD boundary, and reinstalling
629
+ * them would shadow committed blocks with stale structure. The observed case: an
630
+ * INVENTED collection's pre-commit header/root transforms restored over the rival's
631
+ * now-committed collection make every later read descend an empty tree, silently
632
+ * hiding the committed rows. When the snapshot's pending queue is empty (the
633
+ * transaction-rollback shape: the capture predates the transaction's first stage),
634
+ * the correct restore target IS the committed state — reset the tracker empty and
635
+ * let reads flow through to the adopted revision.
636
+ *
637
+ * NOTE: a snapshot that carries PENDING actions across a moved boundary (a
638
+ * mid-transaction savepoint captured before a mid-transaction refresh adopted a
639
+ * rival's commit) still restores verbatim below — rebasing it would require an async
640
+ * replay this synchronous method cannot run. That shape predates this guard and
641
+ * keeps its old behaviour; if it is ever observed producing stale reads, the rebase
642
+ * belongs in an async caller that can replay the pending queue (see replayActions).
643
+ * The WRITE side of that shape is closed here: the snapshot's base pins are restored with
644
+ * its transforms, so the restored operations name the bases they were computed on, and the
645
+ * pre-pend re-validation (restageIfBasesMoved) finds those bases moved and re-stages. */
305
646
  restorePending(snapshot: CollectionSnapshot<TAction>): void;
306
647
  /** A read-only {@link Tracker} pinned to this collection's committed state AS OF the
307
648
  * moment of this call, seeded with a (deep-copied) set of pre-transaction transforms.
@@ -387,7 +728,14 @@ export declare class Collection<TAction> implements ICollection<TAction> {
387
728
  * process can be at different revisions at the same instant. That gap is invisible
388
729
  * from outside the class without this accessor, which is the whole reason it
389
730
  * exists: `docs/debugging.md` (§ "Which revision did a read descend?") explains
390
- * how an operator reads the difference. */
731
+ * how an operator reads the difference.
732
+ *
733
+ * The one exception is `undefined` itself, which is a STATE rather than a revision:
734
+ * "this instance invented the collection and has never adopted a committed revision".
735
+ * For the same reason (only this instance moves it), it stays true until this instance
736
+ * updates, syncs or records a commit, so a caller holding a freshly opened instance may
737
+ * branch on it — the Quereus adapter does, to leave an invented, never-written index tree
738
+ * unflushed exactly as an unwritten table tree is left. Never branch on the NUMBER. */
391
739
  committedRevision(): number | undefined;
392
740
  /** The id of the action that PRODUCED the revision {@link committedRevision} reports —
393
741
  * this collection's lineage marker at that revision — or `undefined` when the action
@@ -484,17 +832,70 @@ export declare class Collection<TAction> implements ICollection<TAction> {
484
832
  * ever allowed to drive the SAME instance concurrently, this must become a per-attempt token (a
485
833
  * mark object compared by identity, refusing to replace a live one) rather than a bare id. */
486
834
  beginInFlightAction(actionId: ActionId): () => void;
487
- /** Push our pending actions to the transactor */
488
- sync(options?: SyncOptions): Promise<void>;
835
+ /** Keep what a FAILED attempt under `actionId` sent, so that if the refresh before its retry
836
+ * finds that attempt's own log entry it can finish the action rather than assume it finished
837
+ * (see {@link inFlightAttempt} and {@link completeOwnEntry}). Every write path that marks an
838
+ * action in flight must call this for each attempt that fails, BEFORE the refresh that follows —
839
+ * a path that does not still never reports a half-landed write as saved (the refresh then
840
+ * refuses with a `transforms-not-held` {@link TornActionError}), but it cannot recover from one.
841
+ *
842
+ * A no-op unless `actionId` is the action currently marked, so a late call from an attempt whose
843
+ * cycle has already ended cannot plant transforms under somebody else's mark. Latch-free and
844
+ * synchronous, like the mark itself: `TransactionCoordinator` calls it from inside its commit
845
+ * span, where it already holds this instance's latch. */
846
+ retainInFlightAttempt(actionId: ActionId, attempt: InFlightAttempt): void;
847
+ /** Push our pending actions to the transactor.
848
+ *
849
+ * @returns who holds what this sync committed, or `undefined` when NOTHING WAS WRITTEN — a sync
850
+ * with no staged changes does no pend and no commit, so there is no durability to report and none
851
+ * is fabricated. That is the one case a caller must handle; every other outcome either returns a
852
+ * class or throws ({@link SyncRetryExhaustedError} for a write that never landed). Present a change
853
+ * as saved only via `isFullyDurable`, never by comparing `quorum`. */
854
+ sync(options?: SyncOptions): Promise<WriteDurability | undefined>;
489
855
  /** Mints the one action id this sync reuses across all of its retry attempts, and owns it for
490
856
  * the WHOLE cycle — including the inter-attempt refresh, which is the only thing that reads it
491
857
  * (see {@link inFlightActionId}). `sync()`/`updateAndSync()` hold the collection latch across
492
858
  * all of this, so the mark's lifetime is contained inside the latched span here; the disposer
493
859
  * runs on every exit, including a throw out of retry exhaustion or an abort. */
494
860
  private syncInternal;
495
- /** The retry loop behind {@link syncInternal}, run with `actionId` already marked in flight. */
861
+ /** The retry loop behind {@link syncInternal}, run with `actionId` already marked in flight.
862
+ *
863
+ * @returns the durability of what this sync committed, or `undefined` when the loop never ran a
864
+ * commit (nothing staged). See {@link sync}. */
496
865
  private syncAttempts;
497
- updateAndSync(options?: SyncOptions): Promise<void>;
866
+ /** Refresh from the transactor, then push. Same return contract as {@link sync}: the durability of
867
+ * what was committed, or `undefined` when nothing was staged and so nothing was written. */
868
+ updateAndSync(options?: SyncOptions): Promise<WriteDurability | undefined>;
869
+ /** Stage `actions` and flush them as one call that either takes effect or leaves NOTHING behind:
870
+ * when the flush throws, the actions this call staged are unstaged again before the error escapes.
871
+ *
872
+ * For callers that own both halves of a write (`Tree.replace`, `Diary.append`). Staging with
873
+ * {@link act} and flushing separately leaves a failed write's actions staged — deliberate for a
874
+ * caller that means to call {@link sync} again, and a trap for one that does not: the actions
875
+ * ride along, unasked, with its NEXT write, so a change the caller was told had failed shows up
876
+ * later, and a caller that reacted to the failure by submitting it again stores it twice. That
877
+ * is how a write reported torn was seen to "appear one write later" (the pending-record route
878
+ * first suspected was ruled out: with the failed writer's collection discarded, 0 of 17 torn
879
+ * rows ever appeared).
880
+ *
881
+ * One latch hold spans staging and flushing, so no other {@link act} on this instance can land
882
+ * between them and the actions to take back are exactly the ones this call put there.
883
+ *
884
+ * The error is rethrown untouched. Unstaging says nothing about storage: a
885
+ * {@link TornActionError} with `final: false` still means the write may be saved or may yet
886
+ * land, and only the staged copy is gone. */
887
+ actAndSync(actions: Action<TAction>[], options?: SyncOptions): Promise<WriteDurability | undefined>;
888
+ /** Takes `actions` back out of the staged queue and rebuilds the tracker without them (always
889
+ * called under latch). `stagedBefore` / `revBefore` are the tracker's transforms and the held
890
+ * revision from before the actions were staged.
891
+ *
892
+ * While the held revision has not moved, the earlier transforms are reinstated verbatim rather
893
+ * than rebuilt by replay: an INVENTED collection keeps its header and root in the tracker with
894
+ * no staged action naming them, and a replay (which resets the tracker first) would drop them —
895
+ * the hazard {@link mustReplay} and {@link snapshotPending} document. Once the revision has
896
+ * moved those transforms describe blocks at a revision this handle has left, so what remains is
897
+ * re-staged against the adopted one, exactly as a refresh would have done. */
898
+ private unstage;
498
899
  selectLog(forward?: boolean): AsyncIterableIterator<Action<TAction>>;
499
900
  private replayActions;
500
901
  getReadDependencies(): ReadDependency[];
@@ -506,25 +907,56 @@ export declare class Collection<TAction> implements ICollection<TAction> {
506
907
  * instance (applied instead of the original), or undefined to discard it.
507
908
  */
508
909
  protected doFilterConflict(action: Action<TAction>, potential: Action<TAction>[]): Action<TAction> | undefined;
910
+ /** The two blocks every refresh starts from — the collection header and the log tail block it
911
+ * names — read unpinned ("latest"), in ONE request when `knownTailId` is the tail the header
912
+ * names. Only when the header names a different tail (the known one filled, or none was known)
913
+ * is that tail fetched in a second request; the out-of-date block's answer is dropped rather
914
+ * than kept, because nothing proves it is current at the revision the refresh will pin to.
915
+ *
916
+ * Both answers pass {@link answeredBlock}'s checks as unpinned reads, header first, so a doubted
917
+ * header or tail throws exactly as it would through {@link TransactorSource.tryGet}.
918
+ *
919
+ * NOTE: a batched get fails as a whole when any block in it gets no answer
920
+ * (`NetworkTransactor.get` throws on a missing id), so an unreachable out-of-date tail fails a
921
+ * refresh that would not have needed it. Harmless today: the log has no checkpoints, so a
922
+ * refresh that finds a new tail walks back through the old one anyway. If checkpoints start
923
+ * letting that walk stop short, read the known tail in its own request instead.
924
+ *
925
+ * @returns undefined when the header is authoritatively absent. */
926
+ private static readLogEnds;
927
+ /** An unpinned read of the log tail block, checked as {@link checkedLogTail} describes. */
928
+ private static readLogTail;
929
+ /** The repo's answer for the log tail, once it has passed {@link answeredBlock}'s unpinned-read
930
+ * checks. The raw entry, not just the block, is what a refresh needs — {@link bootstrapContext}
931
+ * reads `state.latest` off it — which is why the tail is read around {@link TransactorSource}
932
+ * and has to be checked here.
933
+ *
934
+ * Both checks matter at this seam in particular. A tail the repo could not retrieve must not
935
+ * degrade into "no context", which would leave the chain walk unable to see pending non-tail
936
+ * blocks and the collection reading as if they did not exist. And this unpinned tail read is
937
+ * the ONE seam where a lagging collection can learn a newer revision exists — every later data
938
+ * read is pinned to the context seeded from it — so seeding from a tail the repo could not
939
+ * confirm is current would freeze the collection at the stale revision with nothing ever
940
+ * reporting a problem. A tail with no `state.latest` and no flag is a real answer (nothing
941
+ * committed yet). */
942
+ private static checkedLogTail;
509
943
  /** Bootstrap ActionContext from the committed tail block's state.
510
944
  * The tail is always committed first (commit protocol guarantee), so it's readable
511
945
  * with context=undefined. Its state.latest contains the ActionRev of the most recent
512
946
  * committed action — exactly the proof needed for the transactor to serve pending
513
- * non-tail blocks during chain walks.
514
- *
515
- * This read goes to the transactor directly rather than through {@link TransactorSource},
516
- * so it has to honour the `unavailable` flag itself: a tail the repo could not retrieve
517
- * must not degrade into "no context", which would leave the chain walk unable to see
518
- * pending non-tail blocks and the collection reading as if they did not exist. A tail
519
- * with no `state.latest` and NO flag is a real answer (nothing committed yet) and still
520
- * no-ops.
521
- *
522
- * The same goes for `unconfirmedAheadRev`: this unpinned tail read is the ONE seam where a
523
- * lagging collection can learn a newer revision exists every later data read is pinned to
524
- * the context seeded here. Silently seeding from a tail the repo could not confirm is
525
- * current would freeze the collection at the stale revision with nothing ever reporting a
526
- * problem, so it throws the same way TransactorSource.tryGet does for its unpinned reads
527
- * (see the tradeoff NOTE there).
947
+ * non-tail blocks during chain walks. A tail with no `latest` (or no tail) no-ops.
948
+ *
949
+ * NOTE: this number is adopted on trust, and adoption is one-way (advanceContext never
950
+ * lowers it). A tail that over-claims therefore pins the collection at a revision its
951
+ * own log can never reach, permanently: every later refresh walks the log (the claim's newest
952
+ * entry never matches, so {@link tailShowsNothingNewer} never lets it skip), reads the
953
+ * real (lower) revision, and is refused so the instance emits
954
+ * `collection:context-not-lowered` forever while `collection:context-short-of-tail`
955
+ * stays silent (the held revision is at or above what the tail claims). No condition
956
+ * that makes a real tail over-claim has been demonstrated; this was seen only through a
957
+ * test double built to lie (see collection.spec.ts, 'a refresh that lands short of the
958
+ * tail it just read'). If an over-claiming tail is ever observed in the field, the fix
959
+ * belongs here validate the claim against the log before pinning not in the refresh.
528
960
  */
529
961
  private static bootstrapContext;
530
962
  }