@optimystic/db-p2p 0.13.5 → 0.14.1

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 (342) hide show
  1. package/README.md +6 -6
  2. package/dist/src/cluster/block-transfer-service.d.ts +67 -8
  3. package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
  4. package/dist/src/cluster/block-transfer-service.js +101 -62
  5. package/dist/src/cluster/block-transfer-service.js.map +1 -1
  6. package/dist/src/cluster/client.d.ts +18 -1
  7. package/dist/src/cluster/client.d.ts.map +1 -1
  8. package/dist/src/cluster/client.js +55 -39
  9. package/dist/src/cluster/client.js.map +1 -1
  10. package/dist/src/cluster/cluster-error.d.ts +38 -0
  11. package/dist/src/cluster/cluster-error.d.ts.map +1 -0
  12. package/dist/src/cluster/cluster-error.js +51 -0
  13. package/dist/src/cluster/cluster-error.js.map +1 -0
  14. package/dist/src/cluster/cluster-repo.d.ts +135 -3
  15. package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
  16. package/dist/src/cluster/cluster-repo.js +333 -58
  17. package/dist/src/cluster/cluster-repo.js.map +1 -1
  18. package/dist/src/cluster/commit-cert.d.ts +63 -0
  19. package/dist/src/cluster/commit-cert.d.ts.map +1 -0
  20. package/dist/src/cluster/commit-cert.js +108 -0
  21. package/dist/src/cluster/commit-cert.js.map +1 -0
  22. package/dist/src/cluster/service.d.ts +46 -11
  23. package/dist/src/cluster/service.d.ts.map +1 -1
  24. package/dist/src/cluster/service.js +97 -33
  25. package/dist/src/cluster/service.js.map +1 -1
  26. package/dist/src/cluster/spread-on-churn.d.ts +13 -0
  27. package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
  28. package/dist/src/cluster/spread-on-churn.js +34 -4
  29. package/dist/src/cluster/spread-on-churn.js.map +1 -1
  30. package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
  31. package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
  32. package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
  33. package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
  34. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
  35. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
  36. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
  37. package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
  38. package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
  39. package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
  40. package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
  41. package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
  42. package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
  43. package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
  44. package/dist/src/cohort-topic/change-bridge.js +76 -0
  45. package/dist/src/cohort-topic/change-bridge.js.map +1 -0
  46. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
  47. package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
  48. package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
  49. package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
  50. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
  51. package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
  52. package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
  53. package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
  54. package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
  55. package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
  56. package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
  57. package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
  58. package/dist/src/cohort-topic/host.d.ts +573 -0
  59. package/dist/src/cohort-topic/host.d.ts.map +1 -0
  60. package/dist/src/cohort-topic/host.js +1460 -0
  61. package/dist/src/cohort-topic/host.js.map +1 -0
  62. package/dist/src/cohort-topic/index.d.ts +14 -0
  63. package/dist/src/cohort-topic/index.d.ts.map +1 -0
  64. package/dist/src/cohort-topic/index.js +14 -0
  65. package/dist/src/cohort-topic/index.js.map +1 -0
  66. package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
  67. package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
  68. package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
  69. package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
  70. package/dist/src/cohort-topic/membership-source.d.ts +36 -0
  71. package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
  72. package/dist/src/cohort-topic/membership-source.js +58 -0
  73. package/dist/src/cohort-topic/membership-source.js.map +1 -0
  74. package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
  75. package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
  76. package/dist/src/cohort-topic/peer-codec.js +26 -0
  77. package/dist/src/cohort-topic/peer-codec.js.map +1 -0
  78. package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
  79. package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
  80. package/dist/src/cohort-topic/peer-sig.js +83 -0
  81. package/dist/src/cohort-topic/peer-sig.js.map +1 -0
  82. package/dist/src/cohort-topic/protocols.d.ts +49 -0
  83. package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
  84. package/dist/src/cohort-topic/protocols.js +58 -0
  85. package/dist/src/cohort-topic/protocols.js.map +1 -0
  86. package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
  87. package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
  88. package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
  89. package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
  90. package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
  91. package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
  92. package/dist/src/cohort-topic/size-estimator.js +16 -0
  93. package/dist/src/cohort-topic/size-estimator.js.map +1 -0
  94. package/dist/src/cohort-topic/stream-util.d.ts +19 -0
  95. package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
  96. package/dist/src/cohort-topic/stream-util.js +76 -0
  97. package/dist/src/cohort-topic/stream-util.js.map +1 -0
  98. package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
  99. package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
  100. package/dist/src/cohort-topic/threshold-crypto.js +178 -0
  101. package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
  102. package/dist/src/cohort-topic/topic-router.d.ts +41 -0
  103. package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
  104. package/dist/src/cohort-topic/topic-router.js +63 -0
  105. package/dist/src/cohort-topic/topic-router.js.map +1 -0
  106. package/dist/src/dispute/cascade.d.ts +179 -0
  107. package/dist/src/dispute/cascade.d.ts.map +1 -0
  108. package/dist/src/dispute/cascade.js +315 -0
  109. package/dist/src/dispute/cascade.js.map +1 -0
  110. package/dist/src/dispute/client.d.ts +2 -1
  111. package/dist/src/dispute/client.d.ts.map +1 -1
  112. package/dist/src/dispute/client.js +12 -3
  113. package/dist/src/dispute/client.js.map +1 -1
  114. package/dist/src/dispute/dispute-service.d.ts +41 -1
  115. package/dist/src/dispute/dispute-service.d.ts.map +1 -1
  116. package/dist/src/dispute/dispute-service.js +147 -21
  117. package/dist/src/dispute/dispute-service.js.map +1 -1
  118. package/dist/src/dispute/index.d.ts +2 -0
  119. package/dist/src/dispute/index.d.ts.map +1 -1
  120. package/dist/src/dispute/index.js +2 -0
  121. package/dist/src/dispute/index.js.map +1 -1
  122. package/dist/src/dispute/invalidation.d.ts +266 -0
  123. package/dist/src/dispute/invalidation.d.ts.map +1 -0
  124. package/dist/src/dispute/invalidation.js +405 -0
  125. package/dist/src/dispute/invalidation.js.map +1 -0
  126. package/dist/src/dispute/types.d.ts +14 -0
  127. package/dist/src/dispute/types.d.ts.map +1 -1
  128. package/dist/src/dispute/types.js.map +1 -1
  129. package/dist/src/index.d.ts +4 -0
  130. package/dist/src/index.d.ts.map +1 -1
  131. package/dist/src/index.js +4 -0
  132. package/dist/src/index.js.map +1 -1
  133. package/dist/src/libp2p-key-network.d.ts +7 -0
  134. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  135. package/dist/src/libp2p-key-network.js +49 -5
  136. package/dist/src/libp2p-key-network.js.map +1 -1
  137. package/dist/src/libp2p-node-base.d.ts +45 -3
  138. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  139. package/dist/src/libp2p-node-base.js +522 -5
  140. package/dist/src/libp2p-node-base.js.map +1 -1
  141. package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
  142. package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
  143. package/dist/src/matchmaking/aggregate-counts.js +60 -0
  144. package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
  145. package/dist/src/matchmaking/index.d.ts +20 -0
  146. package/dist/src/matchmaking/index.d.ts.map +1 -0
  147. package/dist/src/matchmaking/index.js +20 -0
  148. package/dist/src/matchmaking/index.js.map +1 -0
  149. package/dist/src/matchmaking/module.d.ts +181 -0
  150. package/dist/src/matchmaking/module.d.ts.map +1 -0
  151. package/dist/src/matchmaking/module.js +221 -0
  152. package/dist/src/matchmaking/module.js.map +1 -0
  153. package/dist/src/matchmaking/protocols.d.ts +37 -0
  154. package/dist/src/matchmaking/protocols.d.ts.map +1 -0
  155. package/dist/src/matchmaking/protocols.js +42 -0
  156. package/dist/src/matchmaking/protocols.js.map +1 -0
  157. package/dist/src/matchmaking/provider-manager.d.ts +52 -0
  158. package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
  159. package/dist/src/matchmaking/provider-manager.js +74 -0
  160. package/dist/src/matchmaking/provider-manager.js.map +1 -0
  161. package/dist/src/matchmaking/query-handler.d.ts +34 -0
  162. package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
  163. package/dist/src/matchmaking/query-handler.js +62 -0
  164. package/dist/src/matchmaking/query-handler.js.map +1 -0
  165. package/dist/src/matchmaking/query-transport.d.ts +180 -0
  166. package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
  167. package/dist/src/matchmaking/query-transport.js +315 -0
  168. package/dist/src/matchmaking/query-transport.js.map +1 -0
  169. package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
  170. package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
  171. package/dist/src/matchmaking/seeker-manager.js +49 -0
  172. package/dist/src/matchmaking/seeker-manager.js.map +1 -0
  173. package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
  174. package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
  175. package/dist/src/matchmaking/seeker-walk-client.js +189 -0
  176. package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
  177. package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
  178. package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
  179. package/dist/src/matchmaking/traffic-validation.js +105 -0
  180. package/dist/src/matchmaking/traffic-validation.js.map +1 -0
  181. package/dist/src/network/network-manager-service.d.ts +4 -2
  182. package/dist/src/network/network-manager-service.d.ts.map +1 -1
  183. package/dist/src/network/network-manager-service.js +9 -3
  184. package/dist/src/network/network-manager-service.js.map +1 -1
  185. package/dist/src/protocol-client.d.ts +13 -0
  186. package/dist/src/protocol-client.d.ts.map +1 -1
  187. package/dist/src/protocol-client.js +74 -2
  188. package/dist/src/protocol-client.js.map +1 -1
  189. package/dist/src/reactivity/forwarder-host.d.ts +198 -0
  190. package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
  191. package/dist/src/reactivity/forwarder-host.js +355 -0
  192. package/dist/src/reactivity/forwarder-host.js.map +1 -0
  193. package/dist/src/reactivity/index.d.ts +19 -0
  194. package/dist/src/reactivity/index.d.ts.map +1 -0
  195. package/dist/src/reactivity/index.js +19 -0
  196. package/dist/src/reactivity/index.js.map +1 -0
  197. package/dist/src/reactivity/notify-transport.d.ts +70 -0
  198. package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
  199. package/dist/src/reactivity/notify-transport.js +111 -0
  200. package/dist/src/reactivity/notify-transport.js.map +1 -0
  201. package/dist/src/reactivity/origination-manager.d.ts +110 -0
  202. package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
  203. package/dist/src/reactivity/origination-manager.js +129 -0
  204. package/dist/src/reactivity/origination-manager.js.map +1 -0
  205. package/dist/src/reactivity/protocols.d.ts +43 -0
  206. package/dist/src/reactivity/protocols.d.ts.map +1 -0
  207. package/dist/src/reactivity/protocols.js +50 -0
  208. package/dist/src/reactivity/protocols.js.map +1 -0
  209. package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
  210. package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
  211. package/dist/src/reactivity/push-state-gossip.js +231 -0
  212. package/dist/src/reactivity/push-state-gossip.js.map +1 -0
  213. package/dist/src/reactivity/recover-transport.d.ts +167 -0
  214. package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
  215. package/dist/src/reactivity/recover-transport.js +308 -0
  216. package/dist/src/reactivity/recover-transport.js.map +1 -0
  217. package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
  218. package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
  219. package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
  220. package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
  221. package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
  222. package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
  223. package/dist/src/reactivity/subscriber-registry.js +87 -0
  224. package/dist/src/reactivity/subscriber-registry.js.map +1 -0
  225. package/dist/src/reactivity/subscription-manager.d.ts +221 -0
  226. package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
  227. package/dist/src/reactivity/subscription-manager.js +302 -0
  228. package/dist/src/reactivity/subscription-manager.js.map +1 -0
  229. package/dist/src/reactivity/topic-bytes.d.ts +32 -0
  230. package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
  231. package/dist/src/reactivity/topic-bytes.js +34 -0
  232. package/dist/src/reactivity/topic-bytes.js.map +1 -0
  233. package/dist/src/repo/client.d.ts.map +1 -1
  234. package/dist/src/repo/client.js +51 -23
  235. package/dist/src/repo/client.js.map +1 -1
  236. package/dist/src/repo/service.d.ts +46 -1
  237. package/dist/src/repo/service.d.ts.map +1 -1
  238. package/dist/src/repo/service.js +79 -45
  239. package/dist/src/repo/service.js.map +1 -1
  240. package/dist/src/rpc-deadline.d.ts +37 -0
  241. package/dist/src/rpc-deadline.d.ts.map +1 -0
  242. package/dist/src/rpc-deadline.js +31 -0
  243. package/dist/src/rpc-deadline.js.map +1 -0
  244. package/dist/src/storage/block-storage.d.ts +2 -0
  245. package/dist/src/storage/block-storage.d.ts.map +1 -1
  246. package/dist/src/storage/block-storage.js +95 -2
  247. package/dist/src/storage/block-storage.js.map +1 -1
  248. package/dist/src/storage/i-block-storage.d.ts +25 -0
  249. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  250. package/dist/src/storage/storage-repo.d.ts +53 -2
  251. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  252. package/dist/src/storage/storage-repo.js +193 -9
  253. package/dist/src/storage/storage-repo.js.map +1 -1
  254. package/dist/src/sync/client.d.ts +4 -1
  255. package/dist/src/sync/client.d.ts.map +1 -1
  256. package/dist/src/sync/client.js +5 -2
  257. package/dist/src/sync/client.js.map +1 -1
  258. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
  259. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
  260. package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
  261. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
  262. package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
  263. package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
  264. package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
  265. package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
  266. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  267. package/dist/src/testing/mesh-harness.js +23 -1
  268. package/dist/src/testing/mesh-harness.js.map +1 -1
  269. package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
  270. package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
  271. package/dist/src/testing/reactivity-mesh-harness.js +663 -0
  272. package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
  273. package/package.json +7 -4
  274. package/src/cluster/block-transfer-service.ts +133 -68
  275. package/src/cluster/client.ts +64 -45
  276. package/src/cluster/cluster-error.ts +64 -0
  277. package/src/cluster/cluster-repo.ts +405 -55
  278. package/src/cluster/commit-cert.ts +139 -0
  279. package/src/cluster/service.ts +116 -44
  280. package/src/cluster/spread-on-churn.ts +328 -285
  281. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
  282. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
  283. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
  284. package/src/cohort-topic/change-bridge.ts +109 -0
  285. package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
  286. package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
  287. package/src/cohort-topic/fret-trust-anchor.ts +153 -0
  288. package/src/cohort-topic/host.ts +2175 -0
  289. package/src/cohort-topic/index.ts +13 -0
  290. package/src/cohort-topic/membership-publish-sink.ts +20 -0
  291. package/src/cohort-topic/membership-source.ts +68 -0
  292. package/src/cohort-topic/peer-codec.ts +31 -0
  293. package/src/cohort-topic/peer-sig.ts +86 -0
  294. package/src/cohort-topic/protocols.ts +71 -0
  295. package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
  296. package/src/cohort-topic/size-estimator.ts +16 -0
  297. package/src/cohort-topic/stream-util.ts +87 -0
  298. package/src/cohort-topic/threshold-crypto.ts +239 -0
  299. package/src/cohort-topic/topic-router.ts +77 -0
  300. package/src/dispute/cascade.ts +517 -0
  301. package/src/dispute/client.ts +12 -2
  302. package/src/dispute/dispute-service.ts +171 -24
  303. package/src/dispute/index.ts +42 -0
  304. package/src/dispute/invalidation.ts +587 -0
  305. package/src/dispute/types.ts +14 -0
  306. package/src/index.ts +4 -0
  307. package/src/libp2p-key-network.ts +49 -6
  308. package/src/libp2p-node-base.ts +620 -12
  309. package/src/matchmaking/aggregate-counts.ts +104 -0
  310. package/src/matchmaking/index.ts +20 -0
  311. package/src/matchmaking/module.ts +363 -0
  312. package/src/matchmaking/protocols.ts +51 -0
  313. package/src/matchmaking/provider-manager.ts +95 -0
  314. package/src/matchmaking/query-handler.ts +88 -0
  315. package/src/matchmaking/query-transport.ts +483 -0
  316. package/src/matchmaking/seeker-manager.ts +64 -0
  317. package/src/matchmaking/seeker-walk-client.ts +293 -0
  318. package/src/matchmaking/traffic-validation.ts +195 -0
  319. package/src/network/network-manager-service.ts +9 -2
  320. package/src/protocol-client.ts +64 -3
  321. package/src/reactivity/forwarder-host.ts +438 -0
  322. package/src/reactivity/index.ts +19 -0
  323. package/src/reactivity/notify-transport.ts +144 -0
  324. package/src/reactivity/origination-manager.ts +192 -0
  325. package/src/reactivity/protocols.ts +61 -0
  326. package/src/reactivity/push-state-gossip.ts +291 -0
  327. package/src/reactivity/recover-transport.ts +408 -0
  328. package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
  329. package/src/reactivity/subscriber-registry.ts +96 -0
  330. package/src/reactivity/subscription-manager.ts +450 -0
  331. package/src/reactivity/topic-bytes.ts +37 -0
  332. package/src/repo/client.ts +54 -23
  333. package/src/repo/service.ts +90 -42
  334. package/src/rpc-deadline.ts +45 -0
  335. package/src/storage/block-storage.ts +106 -3
  336. package/src/storage/i-block-storage.ts +27 -0
  337. package/src/storage/storage-repo.ts +209 -13
  338. package/src/sync/client.ts +5 -2
  339. package/src/testing/cohort-topic-mesh-harness.ts +613 -0
  340. package/src/testing/matchmaking-mesh-harness.ts +475 -0
  341. package/src/testing/mesh-harness.ts +22 -2
  342. package/src/testing/reactivity-mesh-harness.ts +922 -0
@@ -0,0 +1,517 @@
1
+ import type {
2
+ ActionId, BlockId, CollectionId, Log,
3
+ DisputeResolutionProof, RevertedBlock, ReadDependency,
4
+ } from '@optimystic/db-core';
5
+ import type { IBlockStorage } from '../storage/i-block-storage.js';
6
+ import { applyInvalidation, hashBlockContent, DELETED_BLOCK_RESTORE, type CertificateTarget } from './invalidation.js';
7
+ import { createLogger } from '../logger.js';
8
+
9
+ const log = createLogger('cascade');
10
+
11
+ // ─── Configuration ───
12
+
13
+ export type CascadeConfig = {
14
+ /**
15
+ * Maximum number of re-evaluation rounds. Each round rescans the in-scope collections for fresh
16
+ * read-dependents of everything invalidated so far; a linear chain processed in revision order
17
+ * collapses into a single round, so this caps pathological out-of-order dependency graphs.
18
+ */
19
+ readonly maxCascadeDepth: number;
20
+ /** Maximum transactions the cascade may invalidate, counting the root. */
21
+ readonly maxCascadeTransactions: number;
22
+ };
23
+
24
+ export const DEFAULT_CASCADE_CONFIG: CascadeConfig = {
25
+ maxCascadeDepth: 32,
26
+ maxCascadeTransactions: 1000,
27
+ };
28
+
29
+ // ─── Collection environment ───
30
+
31
+ /**
32
+ * Everything the cascade needs from one collection: its append-only log (where child invalidation
33
+ * entries land and where read-dependents are discovered) and a resolver for the per-block storage
34
+ * its actions wrote. The caller supplies one per collection in the cascade's universe — the root's
35
+ * collections plus every collection reachable via a cross-collection read edge. (Discovering that
36
+ * universe requires a block→collection read index; the engine consumes the universe rather than
37
+ * computing it — see the handoff's cross-collection note.)
38
+ */
39
+ export type CollectionEnv = {
40
+ readonly collectionId: CollectionId;
41
+ readonly log: Log<unknown>;
42
+ readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
43
+ };
44
+
45
+ // ─── Invalidated (blockId, revision) pairs ───
46
+
47
+ /**
48
+ * A block revision proven invalid: a committed action wrote `blockId` at `rev`, and that action has
49
+ * been invalidated (the root, or a cascade child). Any committed transaction whose read set contains
50
+ * `(blockId, rev)` observed this now-invalid revision and is a read-dependent.
51
+ *
52
+ * `restoredContentHash` is the content hash of the as-if-absent (reverted) value — the hash the
53
+ * invalidation recorded. The default re-evaluator compares it against the content the dependent
54
+ * actually observed: equal ⇒ the revert did not change what was read ⇒ retain.
55
+ */
56
+ export type InvalidatedPair = {
57
+ readonly blockId: BlockId;
58
+ readonly rev: number;
59
+ readonly restoredContentHash: string;
60
+ /** The collection whose invalidation produced this pair (so same-collection rev ordering is checkable). */
61
+ readonly collectionId: CollectionId;
62
+ /** Resolves storage for this block, from its owning collection — the dependent may live elsewhere. */
63
+ readonly createBlockStorage: (blockId: BlockId) => IBlockStorage;
64
+ };
65
+
66
+ function pairKey(blockId: BlockId, rev: number): string {
67
+ return `${blockId}\0${rev}`;
68
+ }
69
+
70
+ /**
71
+ * Dedup identity for a reverted log entry: the (collectionId, actionId) pair, not the actionId
72
+ * alone. A transaction spanning N collections has one entry per collection (same actionId,
73
+ * different collection/blockIds/rev) — each must be reverted independently, tracked separately.
74
+ * Uses a NUL separator, mirroring {@link pairKey}, so ids containing spaces cannot collide.
75
+ */
76
+ function entryKey(collectionId: CollectionId, actionId: ActionId): string {
77
+ return `${collectionId}\0${actionId}`;
78
+ }
79
+
80
+ // ─── Re-evaluation ───
81
+
82
+ /** A candidate read-dependent under re-evaluation against post-invalidation state. */
83
+ export type CascadeCandidate = {
84
+ readonly env: CollectionEnv;
85
+ readonly collectionId: CollectionId;
86
+ readonly actionId: ActionId;
87
+ readonly rev: number;
88
+ /** Blocks this action wrote (its entry's blockIds) — what gets reverted if it is invalidated. */
89
+ readonly blockIds: ReadonlyArray<BlockId>;
90
+ /** This action's persisted read set, or `undefined` for a legacy (pre-cascade) entry. */
91
+ readonly reads: ReadonlyArray<ReadDependency> | undefined;
92
+ /** The subset of `reads` intersecting an invalidated `(blockId, rev)`. Empty for a legacy candidate. */
93
+ readonly matched: ReadonlyArray<InvalidatedPair>;
94
+ };
95
+
96
+ /**
97
+ * Verdict for a candidate re-evaluated against the reverted state:
98
+ * - `retain` — the read still holds; the transaction stands untouched (the re-evaluation prune).
99
+ * - `invalidate` — the read no longer holds; revert this transaction and recurse into its dependents.
100
+ * - `unevaluable` — cannot decide from available data (e.g. a legacy entry with no persisted reads,
101
+ * and no engine to re-execute). Escalated rather than guessed.
102
+ */
103
+ export type CascadeVerdict = 'retain' | 'invalidate' | 'unevaluable';
104
+
105
+ /** Decides whether a candidate still holds against the reverted state. */
106
+ export type Reevaluate = (candidate: CascadeCandidate) => Promise<CascadeVerdict>;
107
+
108
+ /**
109
+ * The default, engine-free re-evaluator: deterministic from stored revisions alone (no engine replay),
110
+ * matching the compensating-state philosophy of the single-collection core.
111
+ *
112
+ * For each of the candidate's intersecting reads `(blockId, rev)`, it compares the content the
113
+ * candidate *observed* (the immutable historical revision `blockId@rev`) against the `restoredContentHash`
114
+ * the invalidation recorded (the as-if-absent value):
115
+ * - any read whose observed content differs from the restored content ⇒ the read no longer holds ⇒ **invalidate**;
116
+ * - a writer that *created* the block (deleted-block sentinel) ⇒ the observed content no longer exists ⇒ **invalidate**;
117
+ * - all intersecting reads unchanged ⇒ **retain** (e.g. a structural-block false dependent whose content the
118
+ * revert did not actually alter, or a redundant write that reverted to the same bytes).
119
+ * - a legacy entry (no persisted reads) ⇒ **unevaluable** (escalate, never guess independent).
120
+ *
121
+ * This is *sound but conservative* at block granularity: it never wrongly retains, but because reads are
122
+ * block-granular it may invalidate a dependent that read an unchanged *field* of a changed block. That is
123
+ * safe (over-invalidation just resubmits the transaction). Field/operation-granular pruning — re-executing
124
+ * the transaction and checking its operations still reproduce — requires an engine and is provided by
125
+ * injecting a custom {@link Reevaluate} instead (see the handoff).
126
+ */
127
+ export function contentEqualityReevaluator(): Reevaluate {
128
+ return async (candidate: CascadeCandidate): Promise<CascadeVerdict> => {
129
+ if (candidate.reads === undefined) {
130
+ return 'unevaluable';
131
+ }
132
+ // Defensive: a candidate with no intersecting reads is not actually a dependent.
133
+ if (candidate.matched.length === 0) {
134
+ return 'retain';
135
+ }
136
+ for (const pair of candidate.matched) {
137
+ if (pair.restoredContentHash === DELETED_BLOCK_RESTORE) {
138
+ return 'invalidate';
139
+ }
140
+ const observed = await pair.createBlockStorage(pair.blockId).getBlock(pair.rev);
141
+ if (!observed) {
142
+ // Cannot confirm the observed revision still materializes → conservative invalidate.
143
+ return 'invalidate';
144
+ }
145
+ if (await hashBlockContent(observed.block) !== pair.restoredContentHash) {
146
+ return 'invalidate';
147
+ }
148
+ }
149
+ return 'retain';
150
+ };
151
+ }
152
+
153
+ // ─── Cascade input / output ───
154
+
155
+ /** A `(blockId, rev)` the root invalidation proved invalid in a given collection, with its restored hash. */
156
+ export type CascadeSeed = {
157
+ readonly collectionId: CollectionId;
158
+ readonly blockId: BlockId;
159
+ readonly rev: number;
160
+ readonly restoredContentHash: string;
161
+ };
162
+
163
+ export type CascadeInput = {
164
+ /** actionId of the root invalidation — recorded as `cascadeRoot` on every child entry. */
165
+ readonly rootActionId: ActionId;
166
+ /** The root's invalidation certificate; reused to authorize each child invalidation. */
167
+ readonly proof: DisputeResolutionProof;
168
+ /** The `(blockId, rev)` pairs the root reversal produced — seeds the dependency frontier. */
169
+ readonly seed: ReadonlyArray<CascadeSeed>;
170
+ /**
171
+ * The target the root proof's votes are bound to — `(rootActionId, root blockIds)`. Every child
172
+ * reuses the root proof, whose votes were signed over the *root's* targetHash, so each child's
173
+ * certificate verification must be against this target, not the child's own. Defaults to the root
174
+ * action id plus the distinct block ids across {@link seed} (the root's reverted blocks). Pass it
175
+ * explicitly when the seed is not a faithful 1:1 image of the root's commit blockIds.
176
+ */
177
+ readonly certificateTarget?: CertificateTarget;
178
+ /** Every collection the cascade may walk: the root's, plus any reachable via cross-collection reads. */
179
+ readonly envs: ReadonlyArray<CollectionEnv>;
180
+ readonly config?: CascadeConfig;
181
+ /** Re-evaluation strategy; defaults to {@link contentEqualityReevaluator}. */
182
+ readonly reevaluate?: Reevaluate;
183
+ /**
184
+ * Operator-escalation health signal: invoked once when the cascade stops at a hard horizon (or hits
185
+ * an unevaluable candidate) and the affected collection(s) need a full re-sync. Never throws past here.
186
+ */
187
+ readonly onEscalation?: (escalation: CascadeEscalation) => void;
188
+ };
189
+
190
+ export type CascadeChild = {
191
+ readonly collectionId: CollectionId;
192
+ readonly actionId: ActionId;
193
+ readonly rev: number;
194
+ readonly reverted: ReadonlyArray<RevertedBlock>;
195
+ };
196
+
197
+ export type CascadeStanding = {
198
+ readonly collectionId: CollectionId;
199
+ readonly actionId: ActionId;
200
+ readonly rev: number;
201
+ };
202
+
203
+ export type CascadeEscalation = {
204
+ readonly reason: 'max-depth' | 'max-transactions' | 'unevaluable';
205
+ /** Collections to flag for operator-escalated full re-sync. */
206
+ readonly collections: ReadonlyArray<CollectionId>;
207
+ /** Read-dependents left un-cascaded at the horizon — surfaced, never silently dropped. */
208
+ readonly remainder: ReadonlyArray<CascadeStanding>;
209
+ /** Candidates that could not be re-evaluated (e.g. legacy entries with no engine to re-execute them). */
210
+ readonly unevaluable: ReadonlyArray<CascadeStanding>;
211
+ };
212
+
213
+ export type CascadeResult = {
214
+ readonly rootActionId: ActionId;
215
+ /** Read-dependents invalidated by the cascade (children only — the root is the caller's to apply). */
216
+ readonly invalidated: ReadonlyArray<CascadeChild>;
217
+ /** Read-dependents re-evaluated and retained (appeared in the chain but did not actually depend). */
218
+ readonly retained: ReadonlyArray<CascadeStanding>;
219
+ /** Re-evaluation rounds run (dependency-graph depth proxy). */
220
+ readonly rounds: number;
221
+ /** Present iff the cascade stopped at a hard horizon or hit an unevaluable candidate. */
222
+ readonly escalation?: CascadeEscalation;
223
+ };
224
+
225
+ // ─── Engine ───
226
+
227
+ /**
228
+ * Detects and re-evaluates the transitive read-dependents of an already-applied root invalidation,
229
+ * invalidating only those that no longer hold against the reverted state and leaving the rest in place.
230
+ *
231
+ * Preconditions: the caller has already applied the *root* reversal (driven through cluster consensus)
232
+ * and supplies its proven-invalid `(blockId, rev)` pairs as `seed`. This engine owns only the *cascade*:
233
+ * each child invalidation is appended via {@link applyInvalidation} carrying `cascadeRoot`, exactly the
234
+ * deterministic primitive every member runs — so a member replaying the same seed + logs converges on
235
+ * the same children (no per-child consensus round is required while the re-evaluator is deterministic;
236
+ * a non-deterministic/engine-based re-evaluator would need each child driven through consensus instead).
237
+ *
238
+ * Algorithm (fixpoint):
239
+ * 1. Seed the invalidated `(blockId, rev)` frontier from the root.
240
+ * 2. Each round, walk every in-scope collection log forward, collecting unprocessed actions whose read
241
+ * set intersects the frontier (legacy reads-less entries are always candidates — unknown dependency).
242
+ * 3. Process candidates in `(rev, collectionId, actionId)` order, deduped by actionId. Re-evaluate each
243
+ * against the reverted state; **retain** leaves it (revisited next round in case a later ancestor
244
+ * reverts), **invalidate** appends its child entry and feeds its reverted blocks back into the frontier.
245
+ * 4. Repeat until a round makes no progress (fixpoint) or a horizon trips (escalate, applying what it did).
246
+ *
247
+ * Diamonds: a dependent of two invalidated ancestors is evaluated once (dedup), after both are reverted
248
+ * (revision order within a collection; cross-collection retains are re-examined every round until fixpoint).
249
+ * Cycle-freedom: a read observes a strictly earlier write, so within a collection a child's rev exceeds the
250
+ * pair it depends on — a back-edge is corruption and throws.
251
+ */
252
+ export async function cascadeInvalidate(input: CascadeInput): Promise<CascadeResult> {
253
+ const config = input.config ?? DEFAULT_CASCADE_CONFIG;
254
+ const reevaluate = input.reevaluate ?? contentEqualityReevaluator();
255
+ const envByCollection = new Map(input.envs.map(e => [e.collectionId, e] as const));
256
+
257
+ // Invalidated frontier, seeded from the root's reverted blocks.
258
+ const pairs = new Map<string, InvalidatedPair>();
259
+ for (const s of input.seed) {
260
+ const env = envByCollection.get(s.collectionId);
261
+ if (!env) {
262
+ throw new Error(`cascade: seed references collection ${s.collectionId} not present in envs`);
263
+ }
264
+ pairs.set(pairKey(s.blockId, s.rev), {
265
+ blockId: s.blockId, rev: s.rev, restoredContentHash: s.restoredContentHash,
266
+ collectionId: s.collectionId, createBlockStorage: env.createBlockStorage,
267
+ });
268
+ }
269
+
270
+ const rootActionId = input.rootActionId;
271
+ // The root proof's votes are bound to the ROOT's target; every child reuses that proof, so each
272
+ // child's applyInvalidation must verify against the root's target rather than the child's own. The
273
+ // child-specific justification is THIS deterministic read-dependency derivation, replayed identically
274
+ // by every member — not the certificate (which only attests the root is invalid).
275
+ const rootCertificateTarget: CertificateTarget = input.certificateTarget
276
+ ?? { invalidatedActionId: rootActionId, blockIds: [...new Set(input.seed.map(s => s.blockId))] };
277
+ // Dedup identity is per collection-entry (collectionId, actionId), not actionId alone: a
278
+ // multi-collection transaction has one entry per collection and each must be reverted separately.
279
+ const processedEntries = new Set<string>(); // entryKey() of collection-entries reverted this cascade
280
+ // The horizon counts distinct transactions (the root counts once, a multi-collection dependent once),
281
+ // so a transaction is never split across the budget — reverted in some collections, escalated in others.
282
+ const invalidatedTxns = new Set<ActionId>([rootActionId]);
283
+ const children: CascadeChild[] = [];
284
+ const affectedCollections = new Set<CollectionId>(input.seed.map(s => s.collectionId));
285
+ const unevaluable: CascadeStanding[] = [];
286
+ let retained: CascadeStanding[] = [];
287
+ let rounds = 0;
288
+ let escalation: CascadeEscalation | undefined;
289
+ // Set once a new transaction is refused at the transaction horizon. We do NOT break the cascade
290
+ // on that refusal: already-counted transactions must still finish every remaining collection-entry
291
+ // (all-or-nothing), so we skip only the over-budget newcomer and let the round complete. The
292
+ // max-transactions escalation is built once at the end, after the protected transactions drain.
293
+ let horizonReached = false;
294
+
295
+ while (true) {
296
+ // Depth horizon: stop before another rescan, surfacing the un-cascaded frontier.
297
+ if (rounds >= config.maxCascadeDepth) {
298
+ const remainder = await collectCandidates(input.envs, pairs, rootActionId, processedEntries);
299
+ escalation = makeEscalation('max-depth', affectedCollections, remainder, unevaluable);
300
+ break;
301
+ }
302
+ rounds++;
303
+
304
+ const candidates = await collectCandidates(input.envs, pairs, rootActionId, processedEntries);
305
+ if (candidates.length === 0) {
306
+ break; // fixpoint: no remaining read-dependents
307
+ }
308
+
309
+ retained = [];
310
+ let progressed = false;
311
+
312
+ for (const cand of candidates) {
313
+ if (processedEntries.has(entryKey(cand.collectionId, cand.actionId))) {
314
+ continue; // diamond: an ancestor pass already reverted this collection-entry
315
+ }
316
+ // Recompute matches against the live frontier (picks up same-round ancestors, ordered by rev).
317
+ const matched = matchReads(cand.reads, pairs);
318
+ const isLegacy = cand.reads === undefined;
319
+ if (matched.length === 0 && !isLegacy) {
320
+ continue; // no longer a dependent this round — revisit next round
321
+ }
322
+
323
+ assertForwardOnly(cand, matched);
324
+
325
+ const verdict = await reevaluate({ ...cand, matched });
326
+ if (verdict === 'retain') {
327
+ retained.push(standing(cand));
328
+ continue;
329
+ }
330
+ if (verdict === 'unevaluable') {
331
+ if (!unevaluable.some(u => u.collectionId === cand.collectionId && u.actionId === cand.actionId)) {
332
+ unevaluable.push(standing(cand));
333
+ }
334
+ affectedCollections.add(cand.collectionId);
335
+ continue;
336
+ }
337
+
338
+ // invalidate — but never START a new transaction past the horizon. An already-counted
339
+ // transaction's remaining collection-entries pass freely (the `has` short-circuit), so a
340
+ // multi-collection dependent is reverted all-or-nothing and is never split across the budget.
341
+ // Crucially we only SKIP the over-budget newcomer here — we must not break the round, or a
342
+ // counted transaction's later-sorted sibling entry would be abandoned (silent partial revert).
343
+ if (!invalidatedTxns.has(cand.actionId) && invalidatedTxns.size + 1 > config.maxCascadeTransactions) {
344
+ horizonReached = true;
345
+ continue;
346
+ }
347
+
348
+ const env = cand.env;
349
+ const result = await applyInvalidation(
350
+ { log: env.log, createBlockStorage: env.createBlockStorage },
351
+ {
352
+ invalidatedActionId: cand.actionId,
353
+ invalidatedRev: cand.rev,
354
+ blockIds: cand.blockIds,
355
+ proof: input.proof,
356
+ cascadeRoot: input.rootActionId,
357
+ // Verify the reused root proof against the root's target, not this child's own.
358
+ certificateTarget: rootCertificateTarget,
359
+ }
360
+ );
361
+
362
+ // `applied:false, already-applied` is the idempotent re-run path: the child entry already
363
+ // exists (prior cascade run / restart). Treat it as invalidated and reuse its reverted blocks
364
+ // so the frontier still grows and the cascade reconverges without a second entry.
365
+ if (!result.applied && result.reason !== 'already-applied') {
366
+ // invalid-certificate should be impossible: the child verifies the root proof against the
367
+ // root's target (rootCertificateTarget), and the root proof is a valid challenger-wins cert.
368
+ log('child-apply-rejected actionId=%s reason=%s', cand.actionId, result.reason);
369
+ continue;
370
+ }
371
+
372
+ processedEntries.add(entryKey(cand.collectionId, cand.actionId));
373
+ invalidatedTxns.add(cand.actionId);
374
+ progressed = true;
375
+ affectedCollections.add(cand.collectionId);
376
+ children.push({ collectionId: cand.collectionId, actionId: cand.actionId, rev: cand.rev, reverted: result.reverted });
377
+
378
+ for (const rb of result.reverted) {
379
+ pairs.set(pairKey(rb.blockId, cand.rev), {
380
+ blockId: rb.blockId, rev: cand.rev, restoredContentHash: rb.restoredContentHash,
381
+ collectionId: cand.collectionId, createBlockStorage: env.createBlockStorage,
382
+ });
383
+ }
384
+ }
385
+
386
+ if (!progressed) {
387
+ break; // only retains / unevaluable / over-budget newcomers remain — fixpoint
388
+ }
389
+ }
390
+
391
+ // Transaction-horizon escalation, built after the protected transactions have fully drained so the
392
+ // remainder reflects only what was genuinely left un-cascaded (never a half-reverted transaction).
393
+ if (horizonReached && !escalation) {
394
+ const remainder = await collectCandidates(input.envs, pairs, rootActionId, processedEntries);
395
+ escalation = makeEscalation('max-transactions', affectedCollections, remainder, unevaluable);
396
+ }
397
+
398
+ if (!escalation && unevaluable.length > 0) {
399
+ escalation = makeEscalation('unevaluable', affectedCollections, [], unevaluable);
400
+ }
401
+
402
+ if (escalation) {
403
+ log('escalate reason=%s collections=%d remainder=%d unevaluable=%d',
404
+ escalation.reason, escalation.collections.length, escalation.remainder.length, escalation.unevaluable.length);
405
+ try {
406
+ input.onEscalation?.(escalation);
407
+ } catch (err) {
408
+ log('escalation-sink-error error=%s', (err as Error).message);
409
+ }
410
+ }
411
+
412
+ return {
413
+ rootActionId: input.rootActionId,
414
+ invalidated: children,
415
+ retained,
416
+ rounds,
417
+ ...(escalation ? { escalation } : {}),
418
+ };
419
+ }
420
+
421
+ // ─── Walk / match helpers ───
422
+
423
+ /** Walk every in-scope collection log forward, collecting unprocessed actions that are read-dependents. */
424
+ async function collectCandidates(
425
+ envs: ReadonlyArray<CollectionEnv>,
426
+ pairs: Map<string, InvalidatedPair>,
427
+ rootActionId: ActionId,
428
+ processedEntries: Set<string>
429
+ ): Promise<CascadeCandidate[]> {
430
+ const candidates: CascadeCandidate[] = [];
431
+ for (const env of envs) {
432
+ for await (const entry of env.log.select()) {
433
+ const action = entry.action;
434
+ // Exclude the root (invalidated by the caller across all its collections) and any
435
+ // collection-entry already reverted this cascade — but a not-yet-reverted entry of the
436
+ // same transaction in a different collection is still a live candidate.
437
+ if (!action || action.actionId === rootActionId || processedEntries.has(entryKey(env.collectionId, action.actionId))) {
438
+ continue;
439
+ }
440
+ const reads = action.reads;
441
+ const isLegacy = reads === undefined;
442
+ const matched = matchReads(reads, pairs);
443
+ if (matched.length === 0 && !isLegacy) {
444
+ continue;
445
+ }
446
+ candidates.push({
447
+ env,
448
+ collectionId: env.collectionId,
449
+ actionId: action.actionId,
450
+ rev: entry.rev,
451
+ blockIds: action.blockIds,
452
+ reads,
453
+ matched,
454
+ });
455
+ }
456
+ }
457
+ // Deterministic processing order: revision, then collection, then actionId.
458
+ candidates.sort((a, b) =>
459
+ a.rev - b.rev
460
+ || (a.collectionId < b.collectionId ? -1 : a.collectionId > b.collectionId ? 1 : 0)
461
+ || (a.actionId < b.actionId ? -1 : a.actionId > b.actionId ? 1 : 0)
462
+ );
463
+ return candidates;
464
+ }
465
+
466
+ /** The subset of `reads` that intersect the invalidated frontier; `[]` for a legacy (reads-less) entry. */
467
+ function matchReads(reads: ReadonlyArray<ReadDependency> | undefined, pairs: Map<string, InvalidatedPair>): InvalidatedPair[] {
468
+ if (!reads) {
469
+ return [];
470
+ }
471
+ const matched: InvalidatedPair[] = [];
472
+ for (const r of reads) {
473
+ const pair = pairs.get(pairKey(r.blockId, r.revision));
474
+ if (pair) {
475
+ matched.push(pair);
476
+ }
477
+ }
478
+ return matched;
479
+ }
480
+
481
+ /**
482
+ * Cycle-freedom guard. A read observes a strictly earlier write, so within a single collection a
483
+ * dependent's revision must exceed the pair it depends on. A same-collection back-edge means the log
484
+ * is corrupt — throw rather than risk a non-terminating walk. Cross-collection pairs live in
485
+ * independent revision sequences and are not comparable, so they are skipped.
486
+ */
487
+ function assertForwardOnly(cand: CascadeCandidate, matched: ReadonlyArray<InvalidatedPair>): void {
488
+ for (const pair of matched) {
489
+ if (pair.collectionId === cand.collectionId && cand.rev <= pair.rev) {
490
+ throw new Error(
491
+ `cascade: back-edge detected — action ${cand.actionId} at rev ${cand.rev} reads ${pair.blockId}@${pair.rev} in the same collection (dependency graph must be a forward DAG)`
492
+ );
493
+ }
494
+ }
495
+ }
496
+
497
+ function standing(cand: CascadeCandidate): CascadeStanding {
498
+ return { collectionId: cand.collectionId, actionId: cand.actionId, rev: cand.rev };
499
+ }
500
+
501
+ function makeEscalation(
502
+ reason: CascadeEscalation['reason'],
503
+ collections: Set<CollectionId>,
504
+ remainder: ReadonlyArray<CascadeCandidate>,
505
+ unevaluable: ReadonlyArray<CascadeStanding>
506
+ ): CascadeEscalation {
507
+ const affected = new Set(collections);
508
+ for (const r of remainder) {
509
+ affected.add(r.collectionId);
510
+ }
511
+ return {
512
+ reason,
513
+ collections: [...affected],
514
+ remainder: remainder.map(standing),
515
+ unevaluable: [...unevaluable],
516
+ };
517
+ }
@@ -1,5 +1,6 @@
1
1
  import type { PeerId, IPeerNetwork } from '@optimystic/db-core';
2
2
  import { ProtocolClient } from '../protocol-client.js';
3
+ import { DEFAULT_DIAL_TIMEOUT_MS, withRpcDeadlineDefaults, type RpcDeadlineOptions } from '../rpc-deadline.js';
3
4
  import type { DisputeChallenge, DisputeResolution, ArbitrationVote, DisputeMessage } from './types.js';
4
5
 
5
6
  /**
@@ -21,21 +22,30 @@ export class DisputeClient extends ProtocolClient {
21
22
  /** Send a challenge to an arbitrator and get their vote */
22
23
  async sendChallenge(challenge: DisputeChallenge, timeoutMs?: number): Promise<ArbitrationVote> {
23
24
  const message: DisputeMessage = { type: 'challenge', challenge };
25
+ // Preserve the existing `timeoutMs`→`signal` contract callers rely on. Post the
26
+ // processMessage response-deadline work, this `signal` now tears down the
27
+ // *response read* (not merely the dial) — the desired "give up on a silent
28
+ // arbitrator" behavior. Also apply the default dial cap so a challenge to an
29
+ // unreachable arbitrator fails the dial fast even when no `timeoutMs` is given;
30
+ // this does not alter the response semantics (still bounded only by the signal).
24
31
  const signal = timeoutMs ? AbortSignal.timeout(timeoutMs) : undefined;
25
32
  const response = await this.processMessage<{ type: 'vote'; vote: ArbitrationVote }>(
26
33
  message,
27
34
  this.protocol,
28
- { signal }
35
+ { signal, dialTimeoutMs: DEFAULT_DIAL_TIMEOUT_MS }
29
36
  );
30
37
  return response.vote;
31
38
  }
32
39
 
33
40
  /** Send a resolution to a peer (broadcast) */
34
- async sendResolution(resolution: DisputeResolution): Promise<void> {
41
+ async sendResolution(resolution: DisputeResolution, options?: RpcDeadlineOptions): Promise<void> {
35
42
  const message: DisputeMessage = { type: 'resolution', resolution };
43
+ // This is a broadcast/ack, so the response cap is what matters: a peer that
44
+ // connects but never acks must not hang the broadcast. Absent keys default.
36
45
  await this.processMessage<{ type: 'ack' }>(
37
46
  message,
38
47
  this.protocol,
48
+ withRpcDeadlineDefaults(options),
39
49
  );
40
50
  }
41
51
  }