@optimystic/db-core 0.14.1 → 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
@@ -0,0 +1,91 @@
1
+ import type { CollectionId } from "../collection/index.js";
2
+
3
+ /**
4
+ * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit
5
+ * fails AFTER at least one collection has already DURABLY committed through the
6
+ * distributed consensus path (GATHER/PEND/COMMIT).
7
+ *
8
+ * ## Why this exists (and why we can't just "roll back")
9
+ *
10
+ * The COMMIT phase commits each collection's pended blocks independently (see
11
+ * `commitPhase`). A per-collection commit can *permanently* fail — e.g. a racing
12
+ * transaction advanced that collection's log tail between PEND and COMMIT (a stale
13
+ * loss) — while the other collections commit successfully. Those durable commits
14
+ * are per-collection and there is no cross-collection undo, so a failure on one
15
+ * collection cannot un-commit the ones that already landed.
16
+ *
17
+ * Uniformly restoring every collection's pre-commit local state (as a clean
18
+ * rollback would) is exactly wrong here: for a collection that DID durably commit
19
+ * it would re-stage its already-durable actions as still-pending, making local
20
+ * tracker memory disagree with cluster storage. Instead the coordinator gives the
21
+ * committed collections the success-path local treatment (fold to cache + reset)
22
+ * and only reverts the failed/never-committed collections, then surfaces THIS error
23
+ * naming both sets so the caller knows reconciliation is required and does NOT
24
+ * falsely report a clean rollback.
25
+ *
26
+ * This is the session-mode / distributed-consensus analog of the plugin's legacy
27
+ * `PartialCommitError` (single-node, per-tree `sync()`).
28
+ *
29
+ * ## The design decision is settled (not "still open")
30
+ *
31
+ * The default multi-collection guarantee is formally **atomicity of intent + eventual,
32
+ * reported visibility**, NOT all-or-nothing — see `docs/correctness.md` **Theorem 3** and
33
+ * `docs/transactions.md` (§ "Session-mode (distributed) commit is not atomic across
34
+ * collections"). This error IS that guarantee's reporting surface, not a placeholder for a
35
+ * stronger one. Genuine cross-collection all-or-nothing is a future opt-in strong mode
36
+ * (backlog `feat-cross-collection-atomic-commit`).
37
+ *
38
+ * ## Reconcile contract for the catcher
39
+ *
40
+ * A caller receiving this error MUST NOT blindly retry the whole transaction and MUST NOT
41
+ * treat it as a clean abort: `committedCollections` are durable and cannot be rolled back,
42
+ * so a whole-transaction retry would double-apply them. Reconcile the named committed set
43
+ * against `failedCollections` (re-drive only the failed collections, or repair the split).
44
+ */
45
+ export class CoordinatorPartialCommitError extends Error {
46
+ constructor(
47
+ /** Collections durably committed via consensus before the failure (NOT rolled back). */
48
+ public readonly committedCollections: readonly CollectionId[],
49
+ /** Collections that never committed this attempt (local state reverted for retry). */
50
+ public readonly failedCollections: readonly CollectionId[],
51
+ /** The underlying commit-phase failure that aborted the commit. */
52
+ public readonly reason?: unknown,
53
+ ) {
54
+ super(
55
+ `Multi-collection commit was not atomic: ${committedCollections.length} collection(s) ` +
56
+ `durably committed via distributed consensus before the commit failed and CANNOT be ` +
57
+ `rolled back — reconciliation is required. ` +
58
+ `Committed (durable, now out of sync with the failed collections): [${committedCollections.join(', ')}]. ` +
59
+ `Failed (never committed; local state reverted for retry): [${failedCollections.join(', ')}]. ` +
60
+ `Underlying failure: ${reason instanceof Error ? reason.message : String(reason)}`
61
+ );
62
+ this.name = 'CoordinatorPartialCommitError';
63
+ }
64
+ }
65
+
66
+ /**
67
+ * Thrown by {@link TransactionCoordinator.commit} when a multi-collection commit failed as a
68
+ * CLEAN stale loss — an optimistic-concurrency conflict (a racing transaction advanced a log tail)
69
+ * in which NOTHING durably committed, so every participating collection's local tracker was
70
+ * restored to its pre-append state and the transaction is safe to re-drive.
71
+ *
72
+ * This is the retryable counterpart to {@link CoordinatorPartialCommitError}: a partial landing
73
+ * cannot be blindly retried (it would double-apply the durable half), but a clean loss can. The
74
+ * coordinator's built-in backoff+jitter retry catches this internally and re-drives after re-reading
75
+ * fresh revisions; it only escapes to the caller once the retry budget (`maxAttempts` / `deadlineMs`)
76
+ * is exhausted, at which point it signals "gave up after a clean loss" rather than a partial split.
77
+ */
78
+ export class CoordinatorStaleLossError extends Error {
79
+ constructor(
80
+ /** Collections that lost the race this attempt (all had their local state reverted for retry). */
81
+ public readonly failedCollections: readonly CollectionId[],
82
+ /** The underlying stale/conflict reason surfaced by the failed pend/commit phase. */
83
+ public readonly reason?: string,
84
+ ) {
85
+ super(
86
+ `Multi-collection commit failed on a clean stale loss (no collection durably committed) ` +
87
+ `for [${failedCollections.join(', ')}]` + (reason ? ` — ${reason}` : '')
88
+ );
89
+ this.name = 'CoordinatorStaleLossError';
90
+ }
91
+ }
@@ -1,6 +1,7 @@
1
1
  export type {
2
2
  Transaction,
3
3
  TransactionStamp,
4
+ StampFields,
4
5
  ReadDependency,
5
6
  TransactionRef,
6
7
  ITransactionEngine,
@@ -8,15 +9,22 @@ export type {
8
9
  CollectionActions,
9
10
  ValidationResult,
10
11
  ITransactionValidator,
11
- ActionsStatement
12
+ ActionsStatement,
13
+ TransactionSigner,
14
+ ClientSignatureVerifier
12
15
  } from './transaction.js';
13
16
 
14
17
  export {
15
18
  createTransactionStamp,
19
+ computeStampId,
16
20
  createTransactionId,
17
21
  createActionsStatements,
18
22
  DEFAULT_TRANSACTION_TTL_MS,
19
- isTransactionExpired
23
+ isTransactionExpired,
24
+ clientSignaturePayload,
25
+ CLIENT_SIG_VERSION,
26
+ MaxPriority,
27
+ clampPriority
20
28
  } from './transaction.js';
21
29
 
22
30
  export {
@@ -24,8 +32,20 @@ export {
24
32
  ACTIONS_ENGINE_ID,
25
33
  } from './actions-engine.js';
26
34
 
35
+ export { ReadDependencyCollector } from './read-dependency-collector.js';
27
36
  export { TransactionCoordinator } from './coordinator.js';
28
- export { TransactionContext } from './context.js';
37
+ export { CoordinatorPartialCommitError, CoordinatorStaleLossError } from './errors.js';
29
38
  export { TransactionSession } from './session.js';
30
39
  export { TransactionValidator } from './validator.js';
31
40
  export type { EngineRegistration, ValidationCoordinatorFactory, BlockStateProvider } from './validator.js';
41
+
42
+ export {
43
+ collectOperations,
44
+ hashOperations,
45
+ canonicalStringify,
46
+ canonicalOperationsPayload,
47
+ opsHashVersion,
48
+ OPS_HASH_VERSION,
49
+ OPS_HASH_PREFIX,
50
+ } from './operations-hash.js';
51
+ export type { Operation } from './operations-hash.js';
@@ -0,0 +1,196 @@
1
+ import type { BlockId, IBlock, BlockOperations } from '../blocks/structs.js';
2
+ import type { CollectionId } from '../collection/struct.js';
3
+ import type { Transforms } from '../transform/struct.js';
4
+ import { hashString } from '../utility/hash-string.js';
5
+
6
+ /**
7
+ * Represents an operation on a block within a collection.
8
+ *
9
+ * This is the SINGLE source of truth for the operation shape used by the
10
+ * transaction "operations hash" — the fingerprint a coordinator sends and a
11
+ * validator recomputes. Both {@link TransactionCoordinator} and
12
+ * {@link TransactionValidator} import this type; it must never be duplicated,
13
+ * because the two sides disagreeing is exactly the bug this module prevents.
14
+ */
15
+ export type Operation =
16
+ | { readonly type: 'insert'; readonly collectionId: CollectionId; readonly blockId: BlockId; readonly block: IBlock }
17
+ | { readonly type: 'update'; readonly collectionId: CollectionId; readonly blockId: BlockId; readonly operations: BlockOperations }
18
+ | { readonly type: 'delete'; readonly collectionId: CollectionId; readonly blockId: BlockId };
19
+
20
+ /**
21
+ * Rank of each operation type, used ONLY as the final tiebreaker when the same
22
+ * (collectionId, blockId) legitimately carries more than one operation — a block
23
+ * staged, then mutated, then deleted within one transform (Transforms apply order
24
+ * is insert → update → delete; see transform/struct.ts). This is the semantic
25
+ * apply order. The exact ranking is arbitrary for correctness (both sides use it),
26
+ * but it MUST be defined in exactly one place — here.
27
+ */
28
+ const TYPE_RANK: Record<Operation['type'], number> = {
29
+ insert: 0,
30
+ update: 1,
31
+ delete: 2,
32
+ };
33
+
34
+ /**
35
+ * Collect every block operation across all collections into a flat list.
36
+ *
37
+ * The returned order reflects Map/object insertion order and is therefore NOT
38
+ * canonical — {@link hashOperations} sorts before hashing, so callers do not need
39
+ * to pre-sort. Shared by the coordinator (both commit() and execute()) and the
40
+ * validator so all three collect sites produce the identical logical set.
41
+ */
42
+ export function collectOperations(transforms: Map<CollectionId, Transforms>): Operation[] {
43
+ const operations: Operation[] = [];
44
+ for (const [collectionId, t] of transforms) {
45
+ for (const [blockId, block] of Object.entries(t.inserts ?? {})) {
46
+ operations.push({ type: 'insert', collectionId, blockId, block });
47
+ }
48
+ for (const [blockId, ops] of Object.entries(t.updates ?? {})) {
49
+ operations.push({ type: 'update', collectionId, blockId, operations: ops });
50
+ }
51
+ for (const blockId of t.deletes ?? []) {
52
+ operations.push({ type: 'delete', collectionId, blockId });
53
+ }
54
+ }
55
+ return operations;
56
+ }
57
+
58
+ /**
59
+ * Total order over operations by the tuple (collectionId, blockId, type), using
60
+ * plain string comparison on the first two components and {@link TYPE_RANK} on the
61
+ * third. This is the cross-node ordering contract: two honest nodes that see the
62
+ * same logical set MUST produce the same sorted sequence regardless of the order
63
+ * they happened to collect operations in.
64
+ */
65
+ function compareOperations(a: Operation, b: Operation): number {
66
+ if (a.collectionId !== b.collectionId) return a.collectionId < b.collectionId ? -1 : 1;
67
+ if (a.blockId !== b.blockId) return a.blockId < b.blockId ? -1 : 1;
68
+ return TYPE_RANK[a.type] - TYPE_RANK[b.type];
69
+ }
70
+
71
+ /**
72
+ * Canonical JSON encoder: recursively sorts object keys but PRESERVES array element
73
+ * order, and matches JSON.stringify leaf semantics.
74
+ *
75
+ * - Object keys are emitted in ascending (sorted) order, so two objects with the
76
+ * same content but different key-insertion order encode identically.
77
+ * - Array element order is preserved because BlockOperations is an ordered list of
78
+ * [entity, index, deleteCount, inserted] tuples whose order is semantically
79
+ * meaningful (as are any data arrays nested inside an IBlock).
80
+ * - Leaf semantics mirror JSON.stringify: `undefined`/function/symbol object-values
81
+ * are dropped, the same in arrays encode as `null`, `null` encodes as `null`,
82
+ * non-finite numbers as `null`, and other primitives via JSON.stringify.
83
+ *
84
+ * Exported so the order-independence test can exercise it directly.
85
+ *
86
+ * NOTE: no toJSON/Date special-casing — a value carrying toJSON (e.g. a Date)
87
+ * encodes as its plain enumerable keys ({} for a Date), not JSON.stringify's
88
+ * toJSON string. Harmless today (blocks hold plain JSON) and still deterministic
89
+ * across nodes (both sides run this same encoder); if IBlock content ever grows
90
+ * toJSON-bearing values and matching JSON.stringify exactly matters, add the hook.
91
+ */
92
+ export function canonicalStringify(value: unknown): string {
93
+ if (value === null) return 'null';
94
+
95
+ const type = typeof value;
96
+ if (type === 'number') return Number.isFinite(value) ? JSON.stringify(value) : 'null';
97
+ if (type === 'string' || type === 'boolean') return JSON.stringify(value);
98
+ if (type === 'bigint') throw new TypeError('Do not know how to serialize a BigInt');
99
+ if (type === 'undefined' || type === 'function' || type === 'symbol') return 'null';
100
+
101
+ if (Array.isArray(value)) {
102
+ const items = value.map(element => {
103
+ const et = typeof element;
104
+ // In arrays, undefined/function/symbol serialize as null (JSON.stringify semantics).
105
+ if (element === undefined || et === 'function' || et === 'symbol') return 'null';
106
+ return canonicalStringify(element);
107
+ });
108
+ return `[${items.join(',')}]`;
109
+ }
110
+
111
+ // Plain object: sort keys ascending, drop undefined/function/symbol values.
112
+ const obj = value as Record<string, unknown>;
113
+ const parts: string[] = [];
114
+ for (const key of Object.keys(obj).sort()) {
115
+ const v = obj[key];
116
+ const vt = typeof v;
117
+ if (v === undefined || vt === 'function' || vt === 'symbol') continue;
118
+ parts.push(`${JSON.stringify(key)}:${canonicalStringify(v)}`);
119
+ }
120
+ return `{${parts.join(',')}}`;
121
+ }
122
+
123
+ /**
124
+ * Current operations-hash FORMAT VERSION. Versions the *serialization* of operations
125
+ * into bytes (the sort key, {@link canonicalStringify} rules, and the SHA-256/base64url
126
+ * step) — distinct from `engineId` in the TransactionStamp, which versions the operation
127
+ * *content* an engine produces from the same statements. Two honest nodes must agree on
128
+ * BOTH dimensions to produce the same ops-hash.
129
+ *
130
+ * Bump this whenever a change to the canonical serialization alters the emitted bytes,
131
+ * so a peer running the old format is *detected* (a legible version-skew error) rather
132
+ * than mistaken for a content disagreement or a Byzantine lie. See docs/transactions.md
133
+ * ("Operations Hash — Canonical Serialization").
134
+ */
135
+ export const OPS_HASH_VERSION = 'v1';
136
+
137
+ /**
138
+ * Wire-token prefix carrying {@link OPS_HASH_VERSION}: `ops.v1:`. The `.` delimiter is
139
+ * outside the base64url alphabet (A–Z a–z 0–9 `-` `_`) that follows the trailing `:`, so
140
+ * the version segment can always be sliced back out unambiguously — see {@link opsHashVersion}.
141
+ */
142
+ export const OPS_HASH_PREFIX = `ops.${OPS_HASH_VERSION}:`;
143
+
144
+ /**
145
+ * Extract the format-version segment from an ops-hash token — the `v1` in `ops.v1:<hash>` —
146
+ * or `null` if the string is not a recognizable versioned ops-hash token.
147
+ *
148
+ * Total: never throws. A bare legacy `ops:<hash>` token (no `.` delimiter), an empty
149
+ * string, or any garbage all return `null`. A validator treats a `null` (or a version it
150
+ * does not recognize) as an unsupported/foreign format — a legible version-skew error —
151
+ * never as an accidental content match.
152
+ */
153
+ export function opsHashVersion(token: string): string | null {
154
+ if (typeof token !== 'string') return null;
155
+ if (!token.startsWith('ops.')) return null;
156
+ const colon = token.indexOf(':', 4);
157
+ if (colon <= 4) return null; // no version characters between "ops." and ":"
158
+ return token.slice(4, colon);
159
+ }
160
+
161
+ /**
162
+ * The EXACT canonical byte-string {@link hashOperations} feeds into SHA-256: the operations
163
+ * sorted by {@link compareOperations} and run through {@link canonicalStringify}. The version
164
+ * token is NOT part of this preimage — it wraps the resulting hash, it is not hashed.
165
+ *
166
+ * Exposed so a future client signature (design-client-transaction-signatures) can bind the
167
+ * IDENTICAL bytes the validators hash, rather than re-deriving the serialization and risking
168
+ * drift. Pair it with {@link OPS_HASH_VERSION} to record which format the bytes belong to.
169
+ */
170
+ export function canonicalOperationsPayload(operations: readonly Operation[]): string {
171
+ return canonicalStringify([...operations].sort(compareOperations));
172
+ }
173
+
174
+ /**
175
+ * Compute the transaction operations hash: sort into canonical order, canonically
176
+ * stringify, SHA-256 (base64url) via {@link hashString}, then prefix the versioned
177
+ * {@link OPS_HASH_PREFIX} token (`ops.v1:`) so the wire string is self-describing.
178
+ *
179
+ * This is the fingerprint the coordinator sends in PendRequest.operationsHash and
180
+ * the validator recomputes; equality of this string is the cross-node agreement
181
+ * that a transaction's operations match.
182
+ *
183
+ * NOTE: the ops-hash carries a format-version token, so a mixed-version cluster now
184
+ * *detects* skew — a validator seeing a foreign/legacy version emits a distinct
185
+ * "unsupported operations-hash format version" error instead of the ambiguous
186
+ * "Operations hash mismatch" (see TransactionValidator). What it does NOT do is
187
+ * *cross-compute* older formats: a node only recognizes that a peer speaks a different
188
+ * version, it cannot reproduce that peer's historical bytes. Mixed-version clusters
189
+ * therefore fail LEGIBLY, not interoperably; multi-version compatibility is future
190
+ * work if rolling upgrades ship. The ops-hash is recomputed fresh on both sides and
191
+ * never persisted, so bumping the version cannot retroactively invalidate committed
192
+ * transactions (unlike transaction.id, which IS persisted — leave it untouched).
193
+ */
194
+ export async function hashOperations(operations: readonly Operation[]): Promise<string> {
195
+ return `${OPS_HASH_PREFIX}${await hashString(canonicalOperationsPayload(operations))}`;
196
+ }
@@ -0,0 +1,78 @@
1
+ import type { BlockId, ReadPurpose } from "../blocks/index.js";
2
+ import type { ReadDependency } from "./transaction.js";
3
+
4
+ /** One captured read: the highest revision observed for the block, plus its {@link ReadPurpose}. */
5
+ type ReadEntry = { revision: number; purpose: ReadPurpose };
6
+
7
+ /** Accumulates the read dependencies of one transaction. Keyed by block id. For each id it keeps:
8
+ *
9
+ * - the HIGHEST revision observed (never downgrades — a re-read from cache must not overwrite a
10
+ * higher revision seen earlier), and
11
+ * - the read's PURPOSE with value-wins semantics: once a block is recorded as a `value` read from
12
+ * ANY path it stays `value`; `navigation` sticks only for a block that was never read as a value.
13
+ * This is the exact analogue of the max-wins rule on revision, and it makes the classification
14
+ * order-independent (so it is deterministic regardless of cache/timing — required for the
15
+ * transaction id and client signature to match across a re-executing validator; Theorem 4).
16
+ *
17
+ * {@link getReadDependencies} returns only the CONFLICT set (value reads); purely-structural
18
+ * `navigation` reads — interior B-tree branches walked through to reach a captured leaf — are
19
+ * dropped. See {@link markValue} and docs/correctness.md Theorem 5.
20
+ *
21
+ * One instance is shared by a collection's {@link TransactorSource} (direct structural reads —
22
+ * bootstrap, header) and its {@link CacheSource} (every cache hit/miss), so a block read from
23
+ * either layer produces a dependency. Because both feed the same collector, a cache miss records
24
+ * the id once from each layer at the same revision/purpose; the merge collapses those to one
25
+ * entry. Cleared at each txn boundary. */
26
+ export class ReadDependencyCollector {
27
+ private reads = new Map<BlockId, ReadEntry>();
28
+
29
+ /** Record a read of `blockId` at `revision`. `purpose` defaults to `value` (retained); pass
30
+ * `navigation` for an interior structural read that a later {@link markValue} may or may not
31
+ * upgrade. Revision is max-wins; purpose is value-wins (see class doc). */
32
+ record(blockId: BlockId, revision: number, purpose: ReadPurpose = 'value'): void {
33
+ const prev = this.reads.get(blockId);
34
+ if (prev === undefined) {
35
+ this.reads.set(blockId, { revision, purpose });
36
+ return;
37
+ }
38
+ if (revision > prev.revision) {
39
+ prev.revision = revision;
40
+ }
41
+ // value-wins: a value read anywhere pins the block as value; navigation never downgrades it.
42
+ if (purpose === 'value') {
43
+ prev.purpose = 'value';
44
+ }
45
+ }
46
+
47
+ /** Upgrade an already-recorded read to `value` (keeping its revision), retaining it in the
48
+ * conflict set. The B-tree point-lookup path uses this to mark the terminal leaf — the
49
+ * load-bearing read whose content the result depends on — after its interior descent nodes
50
+ * were recorded as `navigation`. No-op if the id was never recorded (e.g. a leaf served from
51
+ * an uncommitted staged insert, which records no dependency at all). */
52
+ markValue(blockId: BlockId): void {
53
+ const prev = this.reads.get(blockId);
54
+ if (prev) {
55
+ prev.purpose = 'value';
56
+ }
57
+ }
58
+
59
+ /** The optimistic-concurrency conflict (read) set: every `value` read, with `navigation`
60
+ * reads excluded. Dropping a covered navigation read cannot admit a lost update — any
61
+ * concurrent change to the queried result also bumps a retained value read (the target
62
+ * leaf) — but it removes the false-positive stale rejections that structural block reads
63
+ * otherwise caused (Theorem 5 Bound). Deterministic in query shape, so a coordinator and a
64
+ * re-executing validator derive the identical set. */
65
+ getReadDependencies(): ReadDependency[] {
66
+ const result: ReadDependency[] = [];
67
+ for (const [blockId, entry] of this.reads) {
68
+ if (entry.purpose === 'value') {
69
+ result.push({ blockId, revision: entry.revision });
70
+ }
71
+ }
72
+ return result;
73
+ }
74
+
75
+ clear(): void {
76
+ this.reads.clear();
77
+ }
78
+ }
@@ -1,6 +1,7 @@
1
1
  import type { TransactionCoordinator } from "./coordinator.js";
2
- import type { Transaction, ExecutionResult, ITransactionEngine, TransactionStamp, CollectionActions } from "./transaction.js";
3
- import { createTransactionStamp, createTransactionId, isTransactionExpired } from "./transaction.js";
2
+ import type { SyncOptions } from "../collection/index.js";
3
+ import type { Transaction, ExecutionResult, ITransactionEngine, TransactionStamp, CollectionActions, TransactionSigner } from "./transaction.js";
4
+ import { createTransactionStamp, createTransactionId, isTransactionExpired, clientSignaturePayload } from "./transaction.js";
4
5
 
5
6
  /**
6
7
  * TransactionSession manages incremental transaction building.
@@ -30,7 +31,8 @@ export class TransactionSession {
30
31
  private constructor(
31
32
  private readonly coordinator: TransactionCoordinator,
32
33
  private readonly engine: ITransactionEngine,
33
- stamp: TransactionStamp
34
+ stamp: TransactionStamp,
35
+ private readonly signer?: TransactionSigner
34
36
  ) {
35
37
  this.stamp = stamp;
36
38
  }
@@ -38,22 +40,27 @@ export class TransactionSession {
38
40
  /**
39
41
  * Create a new TransactionSession.
40
42
  * Uses async factory because stamp creation requires SHA-256 hashing.
43
+ *
44
+ * @param signer - Optional client signer. When supplied, commit() stamps the built
45
+ * transaction's `signature` over its canonical payload; when omitted the
46
+ * transaction is left unsigned (accepted only by nodes with no verifier wired).
41
47
  */
42
48
  static async create(
43
49
  coordinator: TransactionCoordinator,
44
50
  engine: ITransactionEngine,
45
51
  peerId: string = 'local',
46
52
  schemaHash: string = '',
47
- ttlMs?: number
53
+ ttlMs?: number,
54
+ signer?: TransactionSigner
48
55
  ): Promise<TransactionSession> {
49
56
  const stamp = await createTransactionStamp(
50
57
  peerId,
51
58
  Date.now(),
52
59
  schemaHash,
53
- 'unknown', // TODO: Get engine ID from engine
60
+ engine.id,
54
61
  ttlMs
55
62
  );
56
- return new TransactionSession(coordinator, engine, stamp);
63
+ return new TransactionSession(coordinator, engine, stamp, signer);
57
64
  }
58
65
 
59
66
  /**
@@ -113,8 +120,11 @@ export class TransactionSession {
113
120
  * Commit the transaction.
114
121
  *
115
122
  * Compiles all statements into a complete Transaction and commits through coordinator.
123
+ *
124
+ * @param options - Optional retry knobs forwarded to {@link TransactionCoordinator.commit}
125
+ * (bounded backoff+jitter retry of a clean stale loss). Defaults are safe out of the box.
116
126
  */
117
- async commit(): Promise<ExecutionResult> {
127
+ async commit(options?: SyncOptions): Promise<ExecutionResult> {
118
128
  if (this.committed) {
119
129
  return { success: false, error: 'Transaction already committed' };
120
130
  }
@@ -136,8 +146,26 @@ export class TransactionSession {
136
146
  id: await createTransactionId(this.stamp.id, this.statements, reads)
137
147
  };
138
148
 
139
- // Commit through coordinator (which will orchestrate PEND/COMMIT)
140
- await this.coordinator.commit(transaction);
149
+ // Sign the canonical client-signature payload if a signer was supplied. The
150
+ // payload covers stamp.id + statements + reads, which are all final now. A signer failure
151
+ // (e.g. a wrong-key-type libp2p sign) is caught and surfaced as a clean commit failure BEFORE
152
+ // coordinator.commit runs — the transaction object is local and discarded, so a failed sign can
153
+ // never leak a partially-built unsigned transaction into consensus.
154
+ if (this.signer) {
155
+ const payload = clientSignaturePayload(this.stamp.id, this.statements, reads);
156
+ try {
157
+ transaction.signature = await this.signer(payload);
158
+ } catch (error) {
159
+ return {
160
+ success: false,
161
+ error: `Failed to sign transaction: ${error instanceof Error ? error.message : String(error)}`
162
+ };
163
+ }
164
+ }
165
+
166
+ // Commit through coordinator (which will orchestrate PEND/COMMIT, with bounded
167
+ // backoff+jitter retry of a clean stale loss)
168
+ await this.coordinator.commit(transaction, options);
141
169
 
142
170
  // Clear read dependencies after successful commit
143
171
  this.coordinator.clearReadDependencies();