@optimystic/db-core 0.14.0 → 0.16.2

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 (301) hide show
  1. package/dist/src/blocks/block-store.d.ts +18 -1
  2. package/dist/src/blocks/block-store.d.ts.map +1 -1
  3. package/dist/src/blocks/helpers.d.ts +2 -2
  4. package/dist/src/blocks/helpers.d.ts.map +1 -1
  5. package/dist/src/blocks/helpers.js +2 -2
  6. package/dist/src/blocks/helpers.js.map +1 -1
  7. package/dist/src/btree/btree.d.ts +27 -6
  8. package/dist/src/btree/btree.d.ts.map +1 -1
  9. package/dist/src/btree/btree.js +104 -61
  10. package/dist/src/btree/btree.js.map +1 -1
  11. package/dist/src/chain/chain.d.ts.map +1 -1
  12. package/dist/src/chain/chain.js +11 -1
  13. package/dist/src/chain/chain.js.map +1 -1
  14. package/dist/src/cluster/index.d.ts +1 -0
  15. package/dist/src/cluster/index.d.ts.map +1 -1
  16. package/dist/src/cluster/index.js +1 -0
  17. package/dist/src/cluster/index.js.map +1 -1
  18. package/dist/src/cluster/membership.d.ts +49 -0
  19. package/dist/src/cluster/membership.d.ts.map +1 -0
  20. package/dist/src/cluster/membership.js +75 -0
  21. package/dist/src/cluster/membership.js.map +1 -0
  22. package/dist/src/cluster/structs.d.ts +50 -1
  23. package/dist/src/cluster/structs.d.ts.map +1 -1
  24. package/dist/src/cluster/structs.js +9 -1
  25. package/dist/src/cluster/structs.js.map +1 -1
  26. package/dist/src/cohort-topic/addressing.d.ts +8 -2
  27. package/dist/src/cohort-topic/addressing.d.ts.map +1 -1
  28. package/dist/src/cohort-topic/addressing.js +11 -3
  29. package/dist/src/cohort-topic/addressing.js.map +1 -1
  30. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +13 -7
  31. package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -1
  32. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +17 -6
  33. package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -1
  34. package/dist/src/cohort-topic/antidos/replay-guard.d.ts +17 -0
  35. package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -1
  36. package/dist/src/cohort-topic/antidos/replay-guard.js +33 -0
  37. package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -1
  38. package/dist/src/cohort-topic/antidos/topic-budget.d.ts +7 -0
  39. package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -1
  40. package/dist/src/cohort-topic/antidos/topic-budget.js +8 -2
  41. package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -1
  42. package/dist/src/cohort-topic/coldstart.d.ts +34 -14
  43. package/dist/src/cohort-topic/coldstart.d.ts.map +1 -1
  44. package/dist/src/cohort-topic/coldstart.js +25 -11
  45. package/dist/src/cohort-topic/coldstart.js.map +1 -1
  46. package/dist/src/cohort-topic/gossip/bus.d.ts +12 -1
  47. package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -1
  48. package/dist/src/cohort-topic/gossip/bus.js +27 -1
  49. package/dist/src/cohort-topic/gossip/bus.js.map +1 -1
  50. package/dist/src/cohort-topic/gossip/view.d.ts +8 -0
  51. package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -1
  52. package/dist/src/cohort-topic/gossip/view.js +17 -1
  53. package/dist/src/cohort-topic/gossip/view.js.map +1 -1
  54. package/dist/src/cohort-topic/member-engine.d.ts +10 -3
  55. package/dist/src/cohort-topic/member-engine.d.ts.map +1 -1
  56. package/dist/src/cohort-topic/member-engine.js +29 -14
  57. package/dist/src/cohort-topic/member-engine.js.map +1 -1
  58. package/dist/src/cohort-topic/membership/publisher.d.ts +14 -7
  59. package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -1
  60. package/dist/src/cohort-topic/membership/publisher.js +15 -27
  61. package/dist/src/cohort-topic/membership/publisher.js.map +1 -1
  62. package/dist/src/cohort-topic/membership/verifier.d.ts +35 -0
  63. package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -1
  64. package/dist/src/cohort-topic/membership/verifier.js +88 -3
  65. package/dist/src/cohort-topic/membership/verifier.js.map +1 -1
  66. package/dist/src/cohort-topic/promotion.d.ts +7 -0
  67. package/dist/src/cohort-topic/promotion.d.ts.map +1 -1
  68. package/dist/src/cohort-topic/promotion.js +16 -2
  69. package/dist/src/cohort-topic/promotion.js.map +1 -1
  70. package/dist/src/cohort-topic/registration/renewal.d.ts +18 -0
  71. package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -1
  72. package/dist/src/cohort-topic/registration/renewal.js +54 -0
  73. package/dist/src/cohort-topic/registration/renewal.js.map +1 -1
  74. package/dist/src/cohort-topic/registration/types.d.ts +12 -0
  75. package/dist/src/cohort-topic/registration/types.d.ts.map +1 -1
  76. package/dist/src/cohort-topic/registration/types.js +14 -0
  77. package/dist/src/cohort-topic/registration/types.js.map +1 -1
  78. package/dist/src/cohort-topic/service.d.ts +4 -3
  79. package/dist/src/cohort-topic/service.d.ts.map +1 -1
  80. package/dist/src/cohort-topic/service.js +38 -12
  81. package/dist/src/cohort-topic/service.js.map +1 -1
  82. package/dist/src/cohort-topic/sig/payloads.d.ts +23 -7
  83. package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -1
  84. package/dist/src/cohort-topic/sig/payloads.js +22 -6
  85. package/dist/src/cohort-topic/sig/payloads.js.map +1 -1
  86. package/dist/src/cohort-topic/traffic.d.ts +6 -0
  87. package/dist/src/cohort-topic/traffic.d.ts.map +1 -1
  88. package/dist/src/cohort-topic/traffic.js +16 -1
  89. package/dist/src/cohort-topic/traffic.js.map +1 -1
  90. package/dist/src/cohort-topic/walk.d.ts +22 -7
  91. package/dist/src/cohort-topic/walk.d.ts.map +1 -1
  92. package/dist/src/cohort-topic/walk.js +78 -18
  93. package/dist/src/cohort-topic/walk.js.map +1 -1
  94. package/dist/src/cohort-topic/wire/codec.d.ts +3 -1
  95. package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -1
  96. package/dist/src/cohort-topic/wire/codec.js +7 -1
  97. package/dist/src/cohort-topic/wire/codec.js.map +1 -1
  98. package/dist/src/cohort-topic/wire/index.d.ts +1 -1
  99. package/dist/src/cohort-topic/wire/index.d.ts.map +1 -1
  100. package/dist/src/cohort-topic/wire/index.js +1 -1
  101. package/dist/src/cohort-topic/wire/index.js.map +1 -1
  102. package/dist/src/cohort-topic/wire/payloads.d.ts +5 -4
  103. package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -1
  104. package/dist/src/cohort-topic/wire/payloads.js +14 -5
  105. package/dist/src/cohort-topic/wire/payloads.js.map +1 -1
  106. package/dist/src/cohort-topic/wire/primitives.d.ts +65 -0
  107. package/dist/src/cohort-topic/wire/primitives.d.ts.map +1 -0
  108. package/dist/src/cohort-topic/wire/primitives.js +173 -0
  109. package/dist/src/cohort-topic/wire/primitives.js.map +1 -0
  110. package/dist/src/cohort-topic/wire/types.d.ts +132 -5
  111. package/dist/src/cohort-topic/wire/types.d.ts.map +1 -1
  112. package/dist/src/cohort-topic/wire/validate.d.ts +15 -5
  113. package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -1
  114. package/dist/src/cohort-topic/wire/validate.js +146 -123
  115. package/dist/src/cohort-topic/wire/validate.js.map +1 -1
  116. package/dist/src/collection/collection.d.ts +23 -12
  117. package/dist/src/collection/collection.d.ts.map +1 -1
  118. package/dist/src/collection/collection.js +111 -35
  119. package/dist/src/collection/collection.js.map +1 -1
  120. package/dist/src/collection/struct.d.ts +37 -2
  121. package/dist/src/collection/struct.d.ts.map +1 -1
  122. package/dist/src/collection/struct.js +16 -1
  123. package/dist/src/collection/struct.js.map +1 -1
  124. package/dist/src/collections/tree/tree.d.ts +18 -1
  125. package/dist/src/collections/tree/tree.d.ts.map +1 -1
  126. package/dist/src/collections/tree/tree.js +36 -6
  127. package/dist/src/collections/tree/tree.js.map +1 -1
  128. package/dist/src/index.d.ts +1 -0
  129. package/dist/src/index.d.ts.map +1 -1
  130. package/dist/src/index.js +1 -0
  131. package/dist/src/index.js.map +1 -1
  132. package/dist/src/log/log.d.ts +26 -3
  133. package/dist/src/log/log.d.ts.map +1 -1
  134. package/dist/src/log/log.js +34 -7
  135. package/dist/src/log/log.js.map +1 -1
  136. package/dist/src/log/struct.d.ts +5 -4
  137. package/dist/src/log/struct.d.ts.map +1 -1
  138. package/dist/src/log/struct.js.map +1 -1
  139. package/dist/src/matchmaking/wire.d.ts +2 -1
  140. package/dist/src/matchmaking/wire.d.ts.map +1 -1
  141. package/dist/src/matchmaking/wire.js +13 -92
  142. package/dist/src/matchmaking/wire.js.map +1 -1
  143. package/dist/src/network/i-key-network.d.ts +9 -0
  144. package/dist/src/network/i-key-network.d.ts.map +1 -1
  145. package/dist/src/network/i-repo.d.ts +8 -0
  146. package/dist/src/network/i-repo.d.ts.map +1 -1
  147. package/dist/src/network/struct.d.ts +10 -0
  148. package/dist/src/network/struct.d.ts.map +1 -1
  149. package/dist/src/reactivity/push-state.d.ts.map +1 -1
  150. package/dist/src/reactivity/push-state.js +4 -33
  151. package/dist/src/reactivity/push-state.js.map +1 -1
  152. package/dist/src/reactivity/wire-validate.d.ts +7 -22
  153. package/dist/src/reactivity/wire-validate.d.ts.map +1 -1
  154. package/dist/src/reactivity/wire-validate.js +7 -73
  155. package/dist/src/reactivity/wire-validate.js.map +1 -1
  156. package/dist/src/reactivity/wire.js +3 -3
  157. package/dist/src/reactivity/wire.js.map +1 -1
  158. package/dist/src/testing/async-wait.d.ts +23 -0
  159. package/dist/src/testing/async-wait.d.ts.map +1 -0
  160. package/dist/src/testing/async-wait.js +42 -0
  161. package/dist/src/testing/async-wait.js.map +1 -0
  162. package/dist/src/testing/index.d.ts +3 -0
  163. package/dist/src/testing/index.d.ts.map +1 -0
  164. package/dist/src/testing/index.js +3 -0
  165. package/dist/src/testing/index.js.map +1 -0
  166. package/dist/src/testing/test-transactor.d.ts +44 -0
  167. package/dist/src/testing/test-transactor.d.ts.map +1 -0
  168. package/dist/src/testing/test-transactor.js +423 -0
  169. package/dist/src/testing/test-transactor.js.map +1 -0
  170. package/dist/src/transaction/actions-engine.d.ts +6 -3
  171. package/dist/src/transaction/actions-engine.d.ts.map +1 -1
  172. package/dist/src/transaction/actions-engine.js +8 -7
  173. package/dist/src/transaction/actions-engine.js.map +1 -1
  174. package/dist/src/transaction/coordinator.d.ts +43 -22
  175. package/dist/src/transaction/coordinator.d.ts.map +1 -1
  176. package/dist/src/transaction/coordinator.js +443 -178
  177. package/dist/src/transaction/coordinator.js.map +1 -1
  178. package/dist/src/transaction/errors.d.ts +82 -0
  179. package/dist/src/transaction/errors.d.ts.map +1 -0
  180. package/dist/src/transaction/errors.js +93 -0
  181. package/dist/src/transaction/errors.js.map +1 -0
  182. package/dist/src/transaction/index.d.ts +6 -3
  183. package/dist/src/transaction/index.d.ts.map +1 -1
  184. package/dist/src/transaction/index.js +4 -2
  185. package/dist/src/transaction/index.js.map +1 -1
  186. package/dist/src/transaction/operations-hash.d.ts +119 -0
  187. package/dist/src/transaction/operations-hash.d.ts.map +1 -0
  188. package/dist/src/transaction/operations-hash.js +180 -0
  189. package/dist/src/transaction/operations-hash.js.map +1 -0
  190. package/dist/src/transaction/read-dependency-collector.d.ts +43 -0
  191. package/dist/src/transaction/read-dependency-collector.d.ts.map +1 -0
  192. package/dist/src/transaction/read-dependency-collector.js +69 -0
  193. package/dist/src/transaction/read-dependency-collector.js.map +1 -0
  194. package/dist/src/transaction/session.d.ts +12 -3
  195. package/dist/src/transaction/session.d.ts.map +1 -1
  196. package/dist/src/transaction/session.js +35 -9
  197. package/dist/src/transaction/session.js.map +1 -1
  198. package/dist/src/transaction/transaction.d.ts +162 -1
  199. package/dist/src/transaction/transaction.d.ts.map +1 -1
  200. package/dist/src/transaction/transaction.js +85 -3
  201. package/dist/src/transaction/transaction.js.map +1 -1
  202. package/dist/src/transaction/validator.d.ts +3 -11
  203. package/dist/src/transaction/validator.d.ts.map +1 -1
  204. package/dist/src/transaction/validator.js +69 -28
  205. package/dist/src/transaction/validator.js.map +1 -1
  206. package/dist/src/transactor/network-transactor.d.ts +35 -0
  207. package/dist/src/transactor/network-transactor.d.ts.map +1 -1
  208. package/dist/src/transactor/network-transactor.js +146 -40
  209. package/dist/src/transactor/network-transactor.js.map +1 -1
  210. package/dist/src/transactor/transactor-source.d.ts +19 -5
  211. package/dist/src/transactor/transactor-source.d.ts.map +1 -1
  212. package/dist/src/transactor/transactor-source.js +43 -11
  213. package/dist/src/transactor/transactor-source.js.map +1 -1
  214. package/dist/src/transform/atomic-proxy.d.ts +35 -5
  215. package/dist/src/transform/atomic-proxy.d.ts.map +1 -1
  216. package/dist/src/transform/atomic-proxy.js +39 -7
  217. package/dist/src/transform/atomic-proxy.js.map +1 -1
  218. package/dist/src/transform/cache-source.d.ts +36 -5
  219. package/dist/src/transform/cache-source.d.ts.map +1 -1
  220. package/dist/src/transform/cache-source.js +84 -8
  221. package/dist/src/transform/cache-source.js.map +1 -1
  222. package/dist/src/transform/helpers.d.ts.map +1 -1
  223. package/dist/src/transform/helpers.js +8 -2
  224. package/dist/src/transform/helpers.js.map +1 -1
  225. package/dist/src/transform/tracker.d.ts +17 -2
  226. package/dist/src/transform/tracker.d.ts.map +1 -1
  227. package/dist/src/transform/tracker.js +66 -11
  228. package/dist/src/transform/tracker.js.map +1 -1
  229. package/dist/src/utility/backoff.d.ts +48 -0
  230. package/dist/src/utility/backoff.d.ts.map +1 -0
  231. package/dist/src/utility/backoff.js +60 -0
  232. package/dist/src/utility/backoff.js.map +1 -0
  233. package/dist/src/utility/pending.d.ts +1 -0
  234. package/dist/src/utility/pending.d.ts.map +1 -1
  235. package/dist/src/utility/pending.js +3 -1
  236. package/dist/src/utility/pending.js.map +1 -1
  237. package/package.json +4 -4
  238. package/src/blocks/block-store.ts +19 -1
  239. package/src/blocks/helpers.ts +3 -3
  240. package/src/btree/btree.ts +108 -63
  241. package/src/chain/chain.ts +11 -1
  242. package/src/cluster/index.ts +1 -0
  243. package/src/cluster/membership.ts +85 -0
  244. package/src/cluster/structs.ts +51 -1
  245. package/src/cohort-topic/addressing.ts +12 -4
  246. package/src/cohort-topic/antidos/bootstrap-evidence.ts +21 -9
  247. package/src/cohort-topic/antidos/replay-guard.ts +37 -0
  248. package/src/cohort-topic/antidos/topic-budget.ts +17 -2
  249. package/src/cohort-topic/coldstart.ts +48 -15
  250. package/src/cohort-topic/gossip/bus.ts +39 -2
  251. package/src/cohort-topic/gossip/view.ts +26 -1
  252. package/src/cohort-topic/member-engine.ts +36 -15
  253. package/src/cohort-topic/membership/publisher.ts +25 -33
  254. package/src/cohort-topic/membership/verifier.ts +119 -3
  255. package/src/cohort-topic/promotion.ts +23 -2
  256. package/src/cohort-topic/registration/renewal.ts +73 -1
  257. package/src/cohort-topic/registration/types.ts +15 -0
  258. package/src/cohort-topic/service.ts +41 -15
  259. package/src/cohort-topic/sig/payloads.ts +29 -9
  260. package/src/cohort-topic/traffic.ts +23 -1
  261. package/src/cohort-topic/walk.ts +95 -24
  262. package/src/cohort-topic/wire/codec.ts +12 -0
  263. package/src/cohort-topic/wire/index.ts +2 -0
  264. package/src/cohort-topic/wire/payloads.ts +14 -5
  265. package/src/cohort-topic/wire/primitives.ts +188 -0
  266. package/src/cohort-topic/wire/types.ts +136 -4
  267. package/src/cohort-topic/wire/validate.ts +168 -134
  268. package/src/collection/collection.ts +123 -40
  269. package/src/collection/struct.ts +44 -2
  270. package/src/collections/tree/tree.ts +42 -5
  271. package/src/index.ts +1 -0
  272. package/src/log/log.ts +37 -9
  273. package/src/log/struct.ts +5 -4
  274. package/src/matchmaking/wire.ts +25 -102
  275. package/src/network/i-key-network.ts +10 -0
  276. package/src/network/i-repo.ts +8 -0
  277. package/src/network/struct.ts +10 -0
  278. package/src/reactivity/push-state.ts +4 -37
  279. package/src/reactivity/wire-validate.ts +7 -81
  280. package/src/reactivity/wire.ts +3 -3
  281. package/src/testing/async-wait.ts +65 -0
  282. package/src/testing/index.ts +2 -0
  283. package/src/testing/test-transactor.ts +468 -0
  284. package/src/transaction/actions-engine.ts +8 -6
  285. package/src/transaction/coordinator.ts +498 -229
  286. package/src/transaction/errors.ts +91 -0
  287. package/src/transaction/index.ts +23 -3
  288. package/src/transaction/operations-hash.ts +196 -0
  289. package/src/transaction/read-dependency-collector.ts +78 -0
  290. package/src/transaction/session.ts +37 -9
  291. package/src/transaction/transaction.ts +211 -4
  292. package/src/transaction/validator.ts +73 -48
  293. package/src/transactor/network-transactor.ts +164 -49
  294. package/src/transactor/transactor-source.ts +47 -12
  295. package/src/transform/atomic-proxy.ts +92 -49
  296. package/src/transform/cache-source.ts +87 -9
  297. package/src/transform/helpers.ts +8 -2
  298. package/src/transform/tracker.ts +69 -11
  299. package/src/utility/backoff.ts +95 -0
  300. package/src/utility/pending.ts +3 -1
  301. package/src/transaction/context.ts +0 -103
@@ -1,21 +1,31 @@
1
- import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms } from "../index.js";
1
+ import type { IBlock, Action, ActionType, ActionHandler, BlockId, ITransactor, BlockStore, Transforms, ActionId } from "../index.js";
2
2
  import { Log, Atomic, Tracker, copyTransforms, CacheSource, isTransformsEmpty, TransactorSource } from "../index.js";
3
- import type { CollectionHeaderBlock, CollectionId, ICollection } from "./index.js";
3
+ import type { CollectionHeaderBlock, CollectionId, ICollection, SyncOptions } from "./index.js";
4
+ import { SyncRetryExhaustedError } from "./index.js";
4
5
  import type { ReadDependency } from "../transaction/transaction.js";
6
+ import { clampPriority } from "../transaction/transaction.js";
7
+ import { ReadDependencyCollector } from "../transaction/read-dependency-collector.js";
5
8
  import { randomBytes } from '@noble/hashes/utils.js';
6
9
  import { toString as uint8ArrayToString } from 'uint8arrays/to-string';
7
10
  import { Latches } from "../utility/latches.js";
11
+ import { jitteredBackoffMs, abortableDelay, makeAbortError } from "../utility/backoff.js";
8
12
 
13
+ /** Default base backoff (and historical fixed delay) between sync retries, in ms. */
9
14
  const PendingRetryDelayMs = 100;
15
+ /** Default max consecutive no-progress stale-failure retries before {@link Collection.sync} gives up. */
16
+ const DefaultMaxAttempts = 10;
17
+ /** Default ceiling on a single exponential-backoff sleep, in ms. */
18
+ const DefaultMaxBackoffMs = 5000;
10
19
 
11
20
  export type CollectionInitOptions<TAction> = {
12
21
  modules: Record<ActionType, ActionHandler<TAction>>;
13
22
  createHeaderBlock: (id: BlockId, store: BlockStore<IBlock>) => IBlock;
14
23
  /** Called for each local action that is potentially in conflict with a remote action.
15
24
  * @param action - The local action to check
16
- * @param potential - The remote action that is potentially in conflict
17
- * @returns The original action, a replacement action (return a new instance; will be
18
- * applied through act()), or undefined to discard this action
25
+ * @param potential - The remote actions that are potentially in conflict
26
+ * @returns The original action (return the same instance to keep it as-is), a replacement
27
+ * action (return a new instance to apply instead of the original — it is re-staged via
28
+ * the conflict replay), or undefined to discard this action
19
29
  */
20
30
  filterConflict?: (action: Action<TAction>, potential: Action<TAction>[]) => Action<TAction> | undefined
21
31
  }
@@ -48,9 +58,12 @@ export class Collection<TAction> implements ICollection<TAction> {
48
58
  }
49
59
 
50
60
  static async createOrOpen<TAction>(transactor: ITransactor, id: CollectionId, init: CollectionInitOptions<TAction>) {
51
- // Start with a context that has an infinite revision number to ensure that we always fetch the latest log information
52
- const source = new TransactorSource(id, transactor, undefined);
53
- const sourceCache = new CacheSource(source);
61
+ // Start with a context that has an infinite revision number to ensure that we always fetch the latest log information.
62
+ // One shared read-dependency collector feeds both the source (direct structural reads) and the cache (every
63
+ // cache hit/miss), so a block read from either layer records a dependency — cache hits included.
64
+ const collector = new ReadDependencyCollector();
65
+ const source = new TransactorSource(id, transactor, undefined, collector);
66
+ const sourceCache = new CacheSource(source, undefined, collector);
54
67
  const tracker = new Tracker(sourceCache);
55
68
  const header = await source.tryGet(id) as CollectionHeaderBlock | undefined;
56
69
 
@@ -129,11 +142,22 @@ export class Collection<TAction> implements ICollection<TAction> {
129
142
  // Process the entries and track the blocks they affect
130
143
  let anyConflicts = false;
131
144
  for (const entry of latest?.entries ?? []) {
132
- // Filter any pending actions that conflict with the remote actions
133
- this.pending = this.pending.map(p => this.doFilterConflict(p, entry.actions) ? p : undefined)
134
- .filter(Boolean) as Action<TAction>[];
145
+ // Filter any pending actions that conflict with the remote actions. Each pending
146
+ // action maps to its effective form: the original, a replacement, or dropped.
147
+ const before = this.pending;
148
+ const after = before
149
+ .map(p => this.doFilterConflict(p, entry.actions))
150
+ .filter((a): a is Action<TAction> => a !== undefined);
151
+ // A replacement or a discard changes the pending set; the tracker still holds the
152
+ // pre-filter transforms, so force a replay to re-stage against the effective actions.
153
+ // Identity comparison per the contract: keep => same instance, replace => new instance.
154
+ // NOTE: a filterConflict hook that always allocates a fresh (but equal) instance instead
155
+ // of returning the same one forces a replay on every update — if that ever shows up as a
156
+ // hot path, compare by value/id here instead of by reference.
157
+ const mutated = after.length !== before.length || after.some((a, i) => a !== before[i]);
158
+ this.pending = after;
135
159
  this.sourceCache.clear(entry.blockIds);
136
- anyConflicts = anyConflicts || this.tracker.conflicts(new Set(entry.blockIds)).length > 0;
160
+ anyConflicts = anyConflicts || mutated || this.tracker.conflicts(new Set(entry.blockIds)).length > 0;
137
161
  }
138
162
 
139
163
  // React to durable invalidations that landed since we last synced. getFrom intentionally skips
@@ -223,26 +247,67 @@ export class Collection<TAction> implements ICollection<TAction> {
223
247
  * this, a collection that already had committed state (e.g. a pre-synced index
224
248
  * tree, or any collection on its second commit) keeps serving the stale prior
225
249
  * revision because {@link update} sees its rev is already current and refetches
226
- * nothing. Call BEFORE resetting the tracker (the transforms are read live). */
227
- applyCommittedToCache(transforms: Transforms): void {
228
- this.sourceCache.transformCache(transforms);
250
+ * nothing. Call BEFORE resetting the tracker (the transforms are read live).
251
+ *
252
+ * @param revision - the committed revision these transforms land at (from
253
+ * {@link recordCommitted}), so cached read-dependency revisions advance to it. */
254
+ applyCommittedToCache(transforms: Transforms, revision: number): void {
255
+ this.sourceCache.transformCache(transforms, revision);
256
+ }
257
+
258
+ /** Next revision this collection would commit at (current committed rev + 1). */
259
+ getNextRev(): number {
260
+ return (this.source.actionContext?.rev ?? 0) + 1;
261
+ }
262
+
263
+ /** Record a just-committed action: append its ActionRev to the committed list
264
+ * and advance the revision. Returns the new revision. Mirrors the inline bump
265
+ * in {@link syncInternal}. */
266
+ recordCommitted(actionId: ActionId): number {
267
+ const rev = this.getNextRev();
268
+ this.source.actionContext = {
269
+ committed: [...(this.source.actionContext?.committed ?? []), { actionId, rev }],
270
+ rev,
271
+ };
272
+ return rev;
229
273
  }
230
274
 
231
275
  /** Push our pending actions to the transactor */
232
- async sync() {
276
+ async sync(options?: SyncOptions) {
233
277
  const release = await Latches.acquire(this.latchId);
234
278
  try {
235
- await this.syncInternal();
279
+ await this.syncInternal(options);
236
280
  } finally {
237
281
  release();
238
282
  }
239
283
  }
240
284
 
241
- private async syncInternal() {
285
+ private async syncInternal(options?: SyncOptions) {
242
286
  const bytes = randomBytes(16);
243
287
  const actionId = uint8ArrayToString(bytes, 'base64url');
244
288
 
289
+ const maxAttempts = options?.maxAttempts ?? DefaultMaxAttempts;
290
+ const baseBackoffMs = options?.baseBackoffMs ?? PendingRetryDelayMs;
291
+ const maxBackoffMs = options?.maxBackoffMs ?? DefaultMaxBackoffMs;
292
+ const deadlineMs = options?.deadlineMs;
293
+ const signal = options?.signal;
294
+ const startedAt = Date.now();
295
+
296
+ // Count of consecutive stale failures that made no forward progress. Reset to 0 on every
297
+ // successful transact, so the cap bounds only a persistently-failing sync — a legitimate
298
+ // large multi-batch sync (which iterates many times committing progress) never trips it.
299
+ let consecutiveFailures = 0;
300
+ let lastReason: string | undefined;
301
+
245
302
  while (this.pending.length || !isTransformsEmpty(this.tracker.transforms)) {
303
+ if (signal?.aborted) {
304
+ throw makeAbortError(signal);
305
+ }
306
+ // Progress-agnostic ceiling: give up if the wall-clock deadline passed.
307
+ if (deadlineMs !== undefined && Date.now() - startedAt >= deadlineMs) {
308
+ throw new SyncRetryExhaustedError(this.id, consecutiveFailures, lastReason ?? 'deadline exceeded');
309
+ }
310
+
246
311
  // Snapshot the pending actions so that any new actions aren't assumed to be part of this action
247
312
  const pending = [...this.pending];
248
313
 
@@ -258,22 +323,46 @@ export class Collection<TAction> implements ICollection<TAction> {
258
323
  const newRev = (this.source.actionContext?.rev ?? 0) + 1;
259
324
  const addResult = await log.addActions(pending, actionId, newRev, () => tracker.transformedBlockIds());
260
325
 
261
- // Commit the action to the transactor
262
- const staleFailure = await this.source.transact(tracker.transforms, actionId, newRev, this.id, addResult.tailPath.block.header.id);
326
+ // Commit the action to the transactor. Carry the aged retry priority derived from the
327
+ // consecutive-failure count so a sync that keeps losing concurrent races out-ranks fresh
328
+ // (priority-0) rivals in the cluster's resolveRace (fairness-only; capped at MaxPriority).
329
+ // First attempt has consecutiveFailures == 0, so priority 0 — the common pend is unchanged.
330
+ const staleFailure = await this.source.transact(tracker.transforms, actionId, newRev, this.id, addResult.tailPath.block.header.id, clampPriority(consecutiveFailures));
263
331
  if (staleFailure) {
264
- if (staleFailure.pending) {
265
- // Wait for short time to allow the pending actions to commit (bounded backoff)
266
- await new Promise(resolve => setTimeout(resolve, PendingRetryDelayMs));
332
+ consecutiveFailures++;
333
+ lastReason = staleFailure.reason ?? lastReason;
334
+ // Give up once the consecutive no-progress budget is exhausted, so a transactor that
335
+ // persistently rejects the sync can no longer hold the collection latch forever.
336
+ // NOTE: this also bounds the legitimate `pending`-wait case (retrying the same action
337
+ // while another commit is in flight), which used to retry indefinitely. Default 10
338
+ // attempts ≈ 21s of exponential backoff. If a high-contention workload legitimately
339
+ // needs to wait longer for a pending commit to clear, raise maxAttempts for that caller.
340
+ if (consecutiveFailures >= maxAttempts) {
341
+ throw new SyncRetryExhaustedError(this.id, consecutiveFailures, lastReason);
267
342
  }
343
+ // Back off before every retry (any stale failure — reason/missing/pending), growing
344
+ // exponentially from the base delay up to the cap, with proportional random jitter so a
345
+ // herd of clients that lost the same race does not re-collide on the next tick (see
346
+ // utility/backoff.ts). The abortable sleep lets an aborted sync reject promptly instead
347
+ // of finishing the sleep.
348
+ // NOTE: the `missing`/`reason` conflict paths now pay this backoff too (they previously
349
+ // retried with zero delay); that is what stops the persistent-`reason` hot spin. If a
350
+ // high-contention workload ever shows this base delay as recovery latency, lower
351
+ // baseBackoffMs for that caller rather than reintroducing the zero-delay retry.
352
+ const delay = jitteredBackoffMs(consecutiveFailures - 1, { baseMs: baseBackoffMs, capMs: maxBackoffMs }, options?.rand);
353
+ await abortableDelay(delay, signal);
268
354
  // Fetch latest state - updateInternal() will call replayActions() if there are conflicts
269
355
  await this.updateInternal();
270
356
  } else {
357
+ // Forward progress: reset the no-progress budget.
358
+ consecutiveFailures = 0;
359
+ lastReason = undefined;
271
360
  // Clear the pending actions that were part of this action
272
361
  this.pending = this.pending.slice(pending.length);
273
362
  // Reset cache and replay any actions that were added during the action
274
363
  const transforms = tracker.reset();
275
364
  await this.replayActions();
276
- this.sourceCache.transformCache(transforms);
365
+ this.sourceCache.transformCache(transforms, newRev);
277
366
  this.source.actionContext = this.source.actionContext
278
367
  ? { committed: [...this.source.actionContext.committed, { actionId, rev: newRev }], rev: newRev }
279
368
  : { committed: [{ actionId, rev: newRev }], rev: newRev };
@@ -281,11 +370,11 @@ export class Collection<TAction> implements ICollection<TAction> {
281
370
  }
282
371
  }
283
372
 
284
- async updateAndSync() {
373
+ async updateAndSync(options?: SyncOptions) {
285
374
  const release = await Latches.acquire(this.latchId);
286
375
  try {
287
376
  await this.updateInternal();
288
- await this.syncInternal();
377
+ await this.syncInternal(options);
289
378
  } finally {
290
379
  release();
291
380
  }
@@ -298,7 +387,9 @@ export class Collection<TAction> implements ICollection<TAction> {
298
387
  }
299
388
  for await (const entry of log.select(undefined, forward)) {
300
389
  if (entry.action) {
301
- yield* forward ? entry.action.actions : entry.action.actions.reverse();
390
+ // NOTE: copy-then-reverse to avoid mutating the stored log entry array.
391
+ // Once tsconfig targets ES2023, `entry.action.actions.toReversed()` is cleaner.
392
+ yield* forward ? entry.action.actions : [...entry.action.actions].reverse();
302
393
  }
303
394
  }
304
395
  }
@@ -321,20 +412,12 @@ export class Collection<TAction> implements ICollection<TAction> {
321
412
 
322
413
  /** Called for each local action that may be in conflict with a remote action (always called under latch).
323
414
  * @param action - The local action to check
324
- * @param potential - The remote action that is potentially in conflict
325
- * @returns true if the action should be kept, false to discard it
415
+ * @param potential - The remote actions that are potentially in conflict
416
+ * @returns The effective action to keep: the original (unchanged), a replacement
417
+ * instance (applied instead of the original), or undefined to discard it.
326
418
  */
327
- protected doFilterConflict(action: Action<TAction>, potential: Action<TAction>[]) {
328
- if (this.filterConflict) {
329
- const replacement = this.filterConflict(action, potential);
330
- if (!replacement) {
331
- return false;
332
- } else if (replacement !== action) {
333
- // Queue replacement - it will be applied in replayActions()
334
- this.pending.push(replacement);
335
- }
336
- }
337
- return true;
419
+ protected doFilterConflict(action: Action<TAction>, potential: Action<TAction>[]): Action<TAction> | undefined {
420
+ return this.filterConflict ? this.filterConflict(action, potential) : action;
338
421
  }
339
422
 
340
423
  /** Bootstrap ActionContext from the committed tail block's state.
@@ -1,16 +1,58 @@
1
1
  import type { IBlock, BlockId, Action } from "../index.js";
2
2
  import type { IChainHeader } from "../chain/chain-nodes.js";
3
+ import type { RandFn } from "../utility/backoff.js";
3
4
 
4
5
  export type CollectionId = BlockId;
5
6
 
6
7
  export type CollectionHeaderBlock = IBlock & Partial<IChainHeader>;
7
8
 
9
+ /** Bounds the retry loop inside {@link ICollection.sync} / {@link ICollection.updateAndSync}
10
+ * so a transactor that keeps rejecting the sync can no longer spin the collection latch forever.
11
+ * All fields are optional; unset fields fall back to conservative defaults. */
12
+ export interface SyncOptions {
13
+ /** Max consecutive stale-failure retries that make no progress before giving up.
14
+ * The counter resets to 0 on every successful commit, so a legitimate large multi-batch
15
+ * sync (which iterates many times making forward progress) is never falsely tripped.
16
+ * Default 10. */
17
+ maxAttempts?: number;
18
+ /** Optional wall-clock deadline in ms measured from the start of the sync call. Independent
19
+ * of the attempt count — a progress-agnostic ceiling. Unset means no deadline. */
20
+ deadlineMs?: number;
21
+ /** Base backoff delay in ms applied before the first retry; subsequent retries grow the delay
22
+ * exponentially up to {@link maxBackoffMs}. Default 100. */
23
+ baseBackoffMs?: number;
24
+ /** Upper bound on any single backoff sleep, in ms. Default 5000. */
25
+ maxBackoffMs?: number;
26
+ /** Optional abort signal. Checked at the top of each loop iteration and raced against the
27
+ * backoff sleep, so an aborted sync rejects promptly (with an AbortError) rather than finishing
28
+ * the current sleep. */
29
+ signal?: AbortSignal;
30
+ /** Advanced/testing hook: source of uniform [0,1) randomness for the backoff jitter. Defaults to
31
+ * the package CSPRNG; inject a deterministic sequence to assert exact retry delays. */
32
+ rand?: RandFn;
33
+ }
34
+
35
+ /** Thrown by {@link ICollection.sync} / {@link ICollection.updateAndSync} when the retry budget
36
+ * (attempt count or deadline) is exhausted while the transactor keeps returning stale failures.
37
+ * Catchable so callers can surface a clear "gave up syncing" condition instead of hanging. */
38
+ export class SyncRetryExhaustedError extends Error {
39
+ constructor(
40
+ readonly collectionId: CollectionId,
41
+ readonly attempts: number,
42
+ readonly lastReason?: string,
43
+ ) {
44
+ super(`sync for collection ${collectionId} exhausted ${attempts} retries` +
45
+ (lastReason ? `: ${lastReason}` : ''));
46
+ this.name = 'SyncRetryExhaustedError';
47
+ }
48
+ }
49
+
8
50
  export interface ICollection<TAction> {
9
51
  readonly id: CollectionId;
10
52
  act(...actions: Action<TAction>[]): Promise<void>;
11
53
  update(): Promise<void>;
12
- sync(): Promise<void>;
13
- updateAndSync(): Promise<void>;
54
+ sync(options?: SyncOptions): Promise<void>;
55
+ updateAndSync(options?: SyncOptions): Promise<void>;
14
56
  selectLog(forward?: boolean): AsyncIterableIterator<Action<TAction>>;
15
57
  }
16
58
 
@@ -36,20 +36,49 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
36
36
  id: CollectionId,
37
37
  keyFromEntry = (entry: TEntry) => entry as unknown as TKey,
38
38
  compare = (a: TKey, b: TKey) => a < b ? -1 : a > b ? 1 : 0,
39
+ /** B-tree node fan-out. Defaults to the BTree default (64). Exposed mainly so tests can
40
+ * force a multi-level tree with few entries (a small capacity), which is what exercises the
41
+ * interior-navigation read-exclusion path — with the default you would need thousands of
42
+ * entries before a descent has any interior branch between root and leaf.
43
+ *
44
+ * NOTE: fan-out is NOT persisted in the collection header — it is a per-call construction
45
+ * param. Creating a tree with a non-default capacity and later reopening it (a separate
46
+ * createOrOpen call) without the SAME capacity silently uses 64, so subsequent writes split
47
+ * at a fan-out the existing nodes were not built for. Fine today (only tests pass it, and
48
+ * they pass it consistently); if a persisted tree ever needs a custom fan-out, persist it in
49
+ * the header and read it back on reopen rather than trusting the caller to re-supply it. */
50
+ nodeCapacity?: number,
39
51
  ): Promise<Tree<TKey, TEntry>> {
40
52
  // Tricky bootstrapping here:
41
53
  // We need the root id to initialize the collection header, so we create the btree in the create collection header callback.
42
54
  let btree: BTree<TKey, TEntry> | undefined;
43
55
  const init: CollectionInitOptions<TreeReplaceAction<TKey, TEntry>> = {
44
56
  modules: {
45
- "replace": async ({ data: actions }, _trx) => {
57
+ "replace": async ({ data: actions }, trx) => {
58
+ // Write through the Atomic store the handler is handed (`trx`), NOT the captured
59
+ // read btree, so `internalTransact`'s all-or-nothing wrapper actually governs this
60
+ // action: if any entry throws, `atomic.commit()` is skipped and every staged node
61
+ // write from this action is discarded (whole-action rollback) — identically for
62
+ // freshly created and reopened trees. Binding a throwaway BTree to `trx` reuses the
63
+ // public constructor; no btree API change needed.
64
+ const actionTree = new BTree<TKey, TEntry>(
65
+ trx,
66
+ new CollectionTrunk(trx, id),
67
+ keyFromEntry,
68
+ compare,
69
+ nodeCapacity, // keep the write btree's fan-out in lock-step with the read btree
70
+ );
46
71
  for (const [key, entry] of actions) {
47
72
  if (entry) {
48
- await btree!.upsert(entry);
73
+ await actionTree.upsert(entry);
49
74
  } else {
50
- await btree!.deleteAt((await btree!.find(key)));
75
+ await actionTree.deleteAt((await actionTree.find(key)));
51
76
  }
52
77
  }
78
+ // Mutations landed in `trx`, not the read btree, so its version counter never moved.
79
+ // Bump it to invalidate any Path a caller still holds — preserving the path-invalidation
80
+ // the previous in-place handler gave for free.
81
+ btree?.invalidatePaths();
53
82
  }
54
83
  },
55
84
  createHeaderBlock: (id: BlockId, store: BlockStore<IBlock>) => { // Only called if the collection does not exist
@@ -57,7 +86,7 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
57
86
  btree = BTree.create<TKey, TEntry>(store, (_s, r) => {
58
87
  rootId = r;
59
88
  return new CollectionTrunk(store, id);
60
- }, keyFromEntry, compare);
89
+ }, keyFromEntry, compare, nodeCapacity);
61
90
  return {
62
91
  header: store.createBlockHeader(TreeHeaderBlockType, id),
63
92
  rootId: rootId!,
@@ -66,7 +95,7 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
66
95
  };
67
96
 
68
97
  const collection = await Collection.createOrOpen<TreeReplaceAction<TKey, TEntry>>(network, id, init);
69
- btree = btree ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare);
98
+ btree = btree ?? new BTree<TKey, TEntry>(collection.tracker, new CollectionTrunk(collection.tracker, collection.id), keyFromEntry, compare, nodeCapacity);
70
99
  return new Tree<TKey, TEntry>(collection, btree, keyFromEntry, compare);
71
100
  }
72
101
 
@@ -137,6 +166,14 @@ export class Tree<TKey, TEntry> implements TreeReadView<TKey, TEntry> {
137
166
  return this.collection;
138
167
  }
139
168
 
169
+ /** This tree's collection id, as a plain string. Used by consumers that flush
170
+ * several trees together (e.g. the Quereus adapter's legacy commit sweep) to
171
+ * name a specific tree in diagnostics when a partial flush leaves trees out of
172
+ * sync. Structurally satisfies the adapter's `DirtyTree.describe()`. */
173
+ describe(): string {
174
+ return String(this.collection.id);
175
+ }
176
+
140
177
  /**
141
178
  * Update the local state from the network.
142
179
  * Call this before reading to ensure you have the latest data.
package/src/index.ts CHANGED
@@ -20,3 +20,4 @@ export * from "./utility/ensured.js";
20
20
  export * from "./utility/pending.js";
21
21
  export * from "./utility/block-id-to-bytes.js";
22
22
  export * from "./utility/lru-map.js";
23
+ export * from "./utility/backoff.js";
package/src/log/log.ts CHANGED
@@ -10,12 +10,32 @@ import type { GetFromResult } from './struct.js';
10
10
 
11
11
  export type LogBlock<TAction> = ChainDataNode<LogEntry<TAction>>
12
12
  & {
13
- /** Base64url encoded Sha256 hash of the next block - present on every block except the head */
13
+ /** Base64url encoded Sha256 hash of the prior (predecessor) block - present on every block except the head */
14
14
  priorHash?: string,
15
15
  };
16
16
 
17
17
  export const priorHash$ = nameof<LogBlock<any>>("priorHash");
18
18
 
19
+ /**
20
+ * Canonical byte payload covered by {@link LogBlock.priorHash}: every field of a log block EXCEPT
21
+ * the mutable structural links (`nextId`/`priorId`). Those links are rewritten *after* the hash is
22
+ * taken — `nextId` when the following block is appended ({@link Chain.add}), and `priorId`/`nextId`
23
+ * when blocks are removed ({@link Chain.pop}/{@link Chain.dequeue}) — so hashing the raw block would
24
+ * not match the bytes that end up stored. The content-bearing fields (`header`, `entries`,
25
+ * `priorHash`) are covered. Any verifier that re-hashes a stored block to check `priorHash` MUST
26
+ * reproduce exactly this payload (strip `nextId` and `priorId`; hash the rest).
27
+ *
28
+ * NOTE: the caller hashes `JSON.stringify` of this object, which is key-order dependent and not a
29
+ * canonical encoding. Fine today (blocks are built with a stable key order and the in-process store
30
+ * preserves it), but if a block is ever re-serialized by a store/codec that reorders keys or drops
31
+ * `undefined`s differently, a re-hash won't match the recorded priorHash — switch to a canonical
32
+ * encoding (sorted keys / dag-cbor) before that happens.
33
+ */
34
+ export function logBlockHashPayload<TAction>(block: LogBlock<TAction>) {
35
+ const { nextId, priorId, ...covered } = block;
36
+ return covered;
37
+ }
38
+
19
39
  export class Log<TAction> {
20
40
  protected constructor(
21
41
  private readonly chain: Chain<LogEntry<TAction>>,
@@ -151,8 +171,9 @@ export class Log<TAction> {
151
171
 
152
172
  /** Gets the actions from startRev (exclusive), to latest in the log. */
153
173
  async getFrom(startRev: number | undefined): Promise<GetFromResult<TAction>> {
154
- const entries: ActionEntry<TAction>[] = [];
155
- const pendings: ActionRev[] = [];
174
+ const entriesFromTail: ActionEntry<TAction>[] = [];
175
+ const pendingActions: ActionRev[] = [];
176
+ let checkpointPendings: ActionRev[] = [];
156
177
  let rev: number | undefined;
157
178
  let checkpointPath: ChainPath<LogEntry<TAction>> | undefined;
158
179
  // Step through collecting both pending and entries until a checkpoint is found
@@ -161,7 +182,7 @@ export class Log<TAction> {
161
182
  rev = rev ?? entry.rev;
162
183
  if (entry.checkpoint) {
163
184
  checkpointPath = path;
164
- pendings.unshift(...entry.checkpoint.pendings);
185
+ checkpointPendings = entry.checkpoint.pendings;
165
186
  break;
166
187
  }
167
188
  // Invalidation entries take a rev slot but are not pending actions — skip them in
@@ -169,24 +190,27 @@ export class Log<TAction> {
169
190
  if (!entry.action) {
170
191
  continue;
171
192
  }
172
- pendings.unshift({ actionId: entry.action.actionId, rev: entry.rev });
193
+ pendingActions.push({ actionId: entry.action.actionId, rev: entry.rev });
173
194
  if (startRev !== undefined && entry.rev > startRev) {
174
- entries.unshift(entry.action);
195
+ entriesFromTail.push(entry.action);
175
196
  } // Can't stop at rev, because we need to collect all pending actions for the context
176
197
  }
177
198
  // Continue stepping past the checkpoint until the given rev is reached
199
+ const entriesFromCheckpoint: ActionEntry<TAction>[] = [];
178
200
  if (checkpointPath) {
179
201
  for await (const path of this.chain.select(checkpointPath, false)) {
180
202
  const entry = entryAt<LogEntry<TAction>>(path)!;
181
203
  if (startRev !== undefined && entry.rev > startRev) {
182
204
  if (entry.action) {
183
- entries.unshift(entry.action!);
205
+ entriesFromCheckpoint.push(entry.action);
184
206
  }
185
207
  } else {
186
208
  break;
187
209
  }
188
210
  }
189
211
  }
212
+ const pendings = [...checkpointPendings, ...pendingActions.reverse()];
213
+ const entries = [...entriesFromCheckpoint.reverse(), ...entriesFromTail.reverse()];
190
214
  return { context: rev ? { committed: pendings, rev } : undefined, entries };
191
215
  }
192
216
 
@@ -224,13 +248,17 @@ export class Log<TAction> {
224
248
  return pendings;
225
249
  }
226
250
 
227
- private static getChainOptions<TAction>(store: BlockStore<IBlock>) {
251
+ /** Chain options used by every log: block type factories plus the priorHash `newBlock` hook.
252
+ * Exposed (not private) so tests can drive a raw {@link Chain} through the exact same hook. */
253
+ static getChainOptions<TAction>(store: BlockStore<IBlock>) {
228
254
  return {
229
255
  createDataBlock: () => ({ header: store.createBlockHeader(LogDataBlockType) }),
230
256
  createHeaderBlock: (id?: BlockId) => ({ header: store.createBlockHeader(LogHeaderBlockType, id) }),
231
257
  newBlock: async (newTail: LogBlock<TAction>, oldTail: LogBlock<TAction> | undefined) => {
232
258
  if (oldTail) {
233
- const hash = await sha256.digest(new TextEncoder().encode(JSON.stringify(oldTail)));
259
+ // Hash the canonical payload (excludes the mutable nextId/priorId links) so the
260
+ // recorded priorHash matches a later re-hash of the stored predecessor block.
261
+ const hash = await sha256.digest(new TextEncoder().encode(JSON.stringify(logBlockHashPayload(oldTail))));
234
262
  newTail.priorHash = uint8ArrayToString(hash.digest, 'base64url');
235
263
  }
236
264
  },
package/src/log/struct.ts CHANGED
@@ -105,10 +105,11 @@ export type DisputeResolutionProof = {
105
105
  */
106
106
  readonly challengerPeerId: string;
107
107
  /**
108
- * The legitimately-selected arbitrator set — the K peer-ids the challenger computed via
109
- * `selectArbitrators` (the next K peers beyond the original cluster by XOR distance to the disputed
110
- * block). Every counted vote's `arbitratorPeerId` must be a member; the set is bound to the originator
111
- * by {@link arbitratorSetSignature} and, when feasible, re-derived from the verifying member's topology.
108
+ * The legitimately-selected arbitrator set — the K peer-ids the challenger computed via `sampleArbitrators`
109
+ * (verifiable dispersed sampling: the peers nearest K pseudo-random ring coordinates derived from
110
+ * `hash(blockId round epoch i)`, spread across the whole keyspace rather than drawn from the disputed
111
+ * block's neighborhood). Every counted vote's `arbitratorPeerId` must be a member; the set is bound to the
112
+ * originator by {@link arbitratorSetSignature} and, when feasible, re-derived from the verifying member's topology.
112
113
  */
113
114
  readonly arbitratorSet: ReadonlyArray<string>;
114
115
  /**