@optimystic/db-p2p 0.13.4 → 0.14.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (352) 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 +8 -1
  134. package/dist/src/libp2p-key-network.d.ts.map +1 -1
  135. package/dist/src/libp2p-key-network.js +67 -8
  136. package/dist/src/libp2p-key-network.js.map +1 -1
  137. package/dist/src/libp2p-node-base.d.ts +57 -3
  138. package/dist/src/libp2p-node-base.d.ts.map +1 -1
  139. package/dist/src/libp2p-node-base.js +523 -6
  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 +25 -0
  186. package/dist/src/protocol-client.d.ts.map +1 -1
  187. package/dist/src/protocol-client.js +126 -4
  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 -19
  235. package/dist/src/repo/client.js.map +1 -1
  236. package/dist/src/repo/cluster-coordinator.d.ts +9 -0
  237. package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
  238. package/dist/src/repo/cluster-coordinator.js +65 -22
  239. package/dist/src/repo/cluster-coordinator.js.map +1 -1
  240. package/dist/src/repo/coordinator-repo.d.ts +20 -0
  241. package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
  242. package/dist/src/repo/coordinator-repo.js +101 -16
  243. package/dist/src/repo/coordinator-repo.js.map +1 -1
  244. package/dist/src/repo/service.d.ts +46 -1
  245. package/dist/src/repo/service.d.ts.map +1 -1
  246. package/dist/src/repo/service.js +79 -45
  247. package/dist/src/repo/service.js.map +1 -1
  248. package/dist/src/rpc-deadline.d.ts +37 -0
  249. package/dist/src/rpc-deadline.d.ts.map +1 -0
  250. package/dist/src/rpc-deadline.js +31 -0
  251. package/dist/src/rpc-deadline.js.map +1 -0
  252. package/dist/src/storage/block-storage.d.ts +2 -0
  253. package/dist/src/storage/block-storage.d.ts.map +1 -1
  254. package/dist/src/storage/block-storage.js +95 -2
  255. package/dist/src/storage/block-storage.js.map +1 -1
  256. package/dist/src/storage/i-block-storage.d.ts +25 -0
  257. package/dist/src/storage/i-block-storage.d.ts.map +1 -1
  258. package/dist/src/storage/storage-repo.d.ts +53 -2
  259. package/dist/src/storage/storage-repo.d.ts.map +1 -1
  260. package/dist/src/storage/storage-repo.js +193 -9
  261. package/dist/src/storage/storage-repo.js.map +1 -1
  262. package/dist/src/sync/client.d.ts +4 -1
  263. package/dist/src/sync/client.d.ts.map +1 -1
  264. package/dist/src/sync/client.js +5 -2
  265. package/dist/src/sync/client.js.map +1 -1
  266. package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
  267. package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
  268. package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
  269. package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
  270. package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
  271. package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
  272. package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
  273. package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
  274. package/dist/src/testing/mesh-harness.d.ts.map +1 -1
  275. package/dist/src/testing/mesh-harness.js +23 -1
  276. package/dist/src/testing/mesh-harness.js.map +1 -1
  277. package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
  278. package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
  279. package/dist/src/testing/reactivity-mesh-harness.js +663 -0
  280. package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
  281. package/package.json +7 -4
  282. package/src/cluster/block-transfer-service.ts +133 -68
  283. package/src/cluster/client.ts +64 -45
  284. package/src/cluster/cluster-error.ts +64 -0
  285. package/src/cluster/cluster-repo.ts +405 -55
  286. package/src/cluster/commit-cert.ts +139 -0
  287. package/src/cluster/service.ts +116 -44
  288. package/src/cluster/spread-on-churn.ts +328 -285
  289. package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
  290. package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
  291. package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
  292. package/src/cohort-topic/change-bridge.ts +109 -0
  293. package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
  294. package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
  295. package/src/cohort-topic/fret-trust-anchor.ts +153 -0
  296. package/src/cohort-topic/host.ts +2175 -0
  297. package/src/cohort-topic/index.ts +13 -0
  298. package/src/cohort-topic/membership-publish-sink.ts +20 -0
  299. package/src/cohort-topic/membership-source.ts +68 -0
  300. package/src/cohort-topic/peer-codec.ts +31 -0
  301. package/src/cohort-topic/peer-sig.ts +86 -0
  302. package/src/cohort-topic/protocols.ts +71 -0
  303. package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
  304. package/src/cohort-topic/size-estimator.ts +16 -0
  305. package/src/cohort-topic/stream-util.ts +87 -0
  306. package/src/cohort-topic/threshold-crypto.ts +239 -0
  307. package/src/cohort-topic/topic-router.ts +77 -0
  308. package/src/dispute/cascade.ts +517 -0
  309. package/src/dispute/client.ts +12 -2
  310. package/src/dispute/dispute-service.ts +171 -24
  311. package/src/dispute/index.ts +42 -0
  312. package/src/dispute/invalidation.ts +587 -0
  313. package/src/dispute/types.ts +14 -0
  314. package/src/index.ts +4 -0
  315. package/src/libp2p-key-network.ts +67 -9
  316. package/src/libp2p-node-base.ts +633 -14
  317. package/src/matchmaking/aggregate-counts.ts +104 -0
  318. package/src/matchmaking/index.ts +20 -0
  319. package/src/matchmaking/module.ts +363 -0
  320. package/src/matchmaking/protocols.ts +51 -0
  321. package/src/matchmaking/provider-manager.ts +95 -0
  322. package/src/matchmaking/query-handler.ts +88 -0
  323. package/src/matchmaking/query-transport.ts +483 -0
  324. package/src/matchmaking/seeker-manager.ts +64 -0
  325. package/src/matchmaking/seeker-walk-client.ts +293 -0
  326. package/src/matchmaking/traffic-validation.ts +195 -0
  327. package/src/network/network-manager-service.ts +9 -2
  328. package/src/protocol-client.ts +119 -5
  329. package/src/reactivity/forwarder-host.ts +438 -0
  330. package/src/reactivity/index.ts +19 -0
  331. package/src/reactivity/notify-transport.ts +144 -0
  332. package/src/reactivity/origination-manager.ts +192 -0
  333. package/src/reactivity/protocols.ts +61 -0
  334. package/src/reactivity/push-state-gossip.ts +291 -0
  335. package/src/reactivity/recover-transport.ts +408 -0
  336. package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
  337. package/src/reactivity/subscriber-registry.ts +96 -0
  338. package/src/reactivity/subscription-manager.ts +450 -0
  339. package/src/reactivity/topic-bytes.ts +37 -0
  340. package/src/repo/client.ts +54 -19
  341. package/src/repo/cluster-coordinator.ts +66 -26
  342. package/src/repo/coordinator-repo.ts +99 -15
  343. package/src/repo/service.ts +90 -42
  344. package/src/rpc-deadline.ts +45 -0
  345. package/src/storage/block-storage.ts +106 -3
  346. package/src/storage/i-block-storage.ts +27 -0
  347. package/src/storage/storage-repo.ts +209 -13
  348. package/src/sync/client.ts +5 -2
  349. package/src/testing/cohort-topic-mesh-harness.ts +613 -0
  350. package/src/testing/matchmaking-mesh-harness.ts +475 -0
  351. package/src/testing/mesh-harness.ts +22 -2
  352. package/src/testing/reactivity-mesh-harness.ts +922 -0
@@ -3,6 +3,7 @@ import type {
3
3
  PendRequest, CommitRequest, BlockGets, IPeerNetwork, PeerId
4
4
  } from "@optimystic/db-core";
5
5
  import type { RepoMessage } from "@optimystic/db-core";
6
+ import { blockIdsForTransforms, blockIdToBytes } from "@optimystic/db-core";
6
7
  import { ProtocolClient } from "../protocol-client.js";
7
8
  import { peerIdFromString } from "@libp2p/peer-id";
8
9
 
@@ -64,16 +65,39 @@ export class RepoClient extends ProtocolClient implements IRepo {
64
65
  };
65
66
  const correlationId = this.extractCorrelationId(operations);
66
67
  const deadline = options.expiration ?? (Date.now() + 30_000)
67
- const msLeft = Math.max(1, deadline - Date.now())
68
- const withTimeout = async <U>(fn: () => Promise<U>): Promise<U> => {
69
- return await Promise.race<U>([
70
- fn(),
71
- new Promise<never>((_, reject) => setTimeout(() => reject(new Error('RepoClient timeout')), msLeft))
72
- ])
73
- }
74
- let response: any
68
+ const deadlineMs = Math.max(1, deadline - Date.now())
75
69
  const preferred = (this.protocolPrefix ?? '/db-p2p') + '/repo/1.0.0'
76
- response = await withTimeout(() => super.processMessage<any>(message, preferred, { signal: options?.signal, correlationId }))
70
+
71
+ // Drive the remaining `expiration` budget through an AbortController whose
72
+ // abort *reason* is the caller-facing `'RepoClient timeout'` Error, combined
73
+ // with any caller signal. processMessage forwards `signal` to both the dial
74
+ // and the response-read and calls `stream.abort(signal.reason)` on abort, so —
75
+ // unlike the old `Promise.race`, whose losing branch left the inner read
76
+ // running and leaked a pending read + stream on every timed-out RPC to a silent
77
+ // peer — the deadline now genuinely cancels the inner read while the caller
78
+ // still observes an error whose `.message === 'RepoClient timeout'`.
79
+ //
80
+ // Intentionally no `responseTimeoutMs`: the combined signal already bounds the
81
+ // read at `deadlineMs`. A second, shorter cap would surface as
82
+ // ResponseTimeoutError and mask the caller-facing 'RepoClient timeout' message.
83
+ const deadlineController = new AbortController()
84
+ const timer = setTimeout(
85
+ () => deadlineController.abort(new Error('RepoClient timeout')),
86
+ deadlineMs
87
+ )
88
+ const combinedSignal = options?.signal
89
+ ? AbortSignal.any([options.signal, deadlineController.signal])
90
+ : deadlineController.signal
91
+ let response: any
92
+ try {
93
+ response = await super.processMessage<any>(message, preferred, {
94
+ signal: combinedSignal,
95
+ correlationId,
96
+ dialTimeoutMs: options?.dialTimeoutMs,
97
+ })
98
+ } finally {
99
+ clearTimeout(timer)
100
+ }
77
101
 
78
102
  if (response?.redirect?.peers?.length) {
79
103
  if (hop >= 2) {
@@ -85,8 +109,10 @@ export class RepoClient extends ProtocolClient implements IRepo {
85
109
  if (next.id === currentIdStr) {
86
110
  throw new Error('Redirect loop detected in RepoClient (same peer)')
87
111
  }
88
- // cache hint
89
- this.recordCoordinatorForOpsIfSupported(operations, nextId)
112
+ // cache hint — await so the recorded key bytes match what findCoordinator
113
+ // later looks up (blockIdToBytes is async); deterministic ordering also
114
+ // keeps the hint testable. The redirect retry below is async anyway.
115
+ await this.recordCoordinatorForOpsIfSupported(operations, nextId)
90
116
  // single-hop retry against target peer using repo protocol
91
117
  const nextClient = RepoClient.create(nextId, this.peerNetwork, this.protocolPrefix)
92
118
  return await nextClient.processRepoMessage<T>(operations, options, hop + 1)
@@ -94,28 +120,37 @@ export class RepoClient extends ProtocolClient implements IRepo {
94
120
  return response as T;
95
121
  }
96
122
 
97
- private extractKeyFromOperations(ops: RepoMessage['operations']): Uint8Array | undefined {
123
+ private async extractKeyFromOperations(ops: RepoMessage['operations']): Promise<Uint8Array | undefined> {
98
124
  const op = ops[0];
125
+ // The recorded key MUST be the sha256 digest produced by blockIdToBytes — the
126
+ // exact bytes findCoordinator/recordCoordinator key the coordinator cache on
127
+ // (NetworkTransactor passes blockIdToBytes(blockId)). Raw utf8 of the id would
128
+ // never match, so the hint would silently never be retrieved.
99
129
  if ('get' in op) {
100
130
  const id = op.get.blockIds[0];
101
- return id ? new TextEncoder().encode(id) : undefined;
131
+ return id ? await blockIdToBytes(id) : undefined;
102
132
  }
103
133
  if ('pend' in op) {
104
- const id = Object.keys(op.pend.transforms)[0];
105
- return id ? new TextEncoder().encode(id) : undefined;
134
+ // Key on a real block id the pend touches, NOT a structural transforms field
135
+ // name ('inserts'/'updates'/'deletes'); see RepoService.deriveBlockKey.
136
+ const id = blockIdsForTransforms(op.pend.transforms)[0];
137
+ return id ? await blockIdToBytes(id) : undefined;
106
138
  }
107
139
  if ('commit' in op) {
108
- return new TextEncoder().encode(op.commit.tailId);
140
+ // Anchor on blockIds[0] (where CoordinatorRepo.commit runs consensus +
141
+ // verifyResponsibility), NOT tailId — they differ for a non-tail batch.
142
+ const id = op.commit.blockIds[0];
143
+ return id ? await blockIdToBytes(id) : undefined;
109
144
  }
110
145
  if ('cancel' in op) {
111
146
  const id = op.cancel.actionRef.blockIds[0];
112
- return id ? new TextEncoder().encode(id) : undefined;
147
+ return id ? await blockIdToBytes(id) : undefined;
113
148
  }
114
149
  return undefined;
115
150
  }
116
151
 
117
- private recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): void {
118
- const keyBytes = this.extractKeyFromOperations(ops)
152
+ private async recordCoordinatorForOpsIfSupported(ops: RepoMessage['operations'], peerId: PeerId): Promise<void> {
153
+ const keyBytes = await this.extractKeyFromOperations(ops)
119
154
  const pn: any = this.peerNetwork as any
120
155
  if (keyBytes != null && typeof pn?.recordCoordinator === 'function') {
121
156
  pn.recordCoordinator(keyBytes, peerId)
@@ -37,10 +37,11 @@ interface ClusterTransactionState {
37
37
  /** Manages distributed transactions across clusters */
38
38
  export class ClusterCoordinator {
39
39
  private transactions: Map<string, ClusterTransactionState> = new Map();
40
- private readonly retryInitialIntervalMs = 2000;
41
- private readonly retryBackoffFactor = 2;
42
- private readonly retryMaxIntervalMs = 30000;
43
- private readonly retryMaxAttempts = 5;
40
+ private readonly retryInitialIntervalMs: number;
41
+ private readonly retryBackoffFactor: number;
42
+ private readonly retryMaxIntervalMs: number;
43
+ private readonly retryMaxAttempts: number;
44
+ private readonly commitBroadcastImmediateRetries: number;
44
45
 
45
46
  constructor(
46
47
  private readonly keyNetwork: IKeyNetwork,
@@ -54,7 +55,13 @@ export class ClusterCoordinator {
54
55
  private readonly fretService?: FretService,
55
56
  private readonly reputation?: IPeerReputation,
56
57
  private readonly stateStore?: ITransactionStateStore
57
- ) { }
58
+ ) {
59
+ this.retryInitialIntervalMs = cfg.commitBroadcastRetryInitialMs ?? 250;
60
+ this.retryBackoffFactor = cfg.commitBroadcastRetryBackoffFactor ?? 2;
61
+ this.retryMaxIntervalMs = cfg.commitBroadcastRetryMaxIntervalMs ?? 8000;
62
+ this.retryMaxAttempts = cfg.commitBroadcastRetryMaxAttempts ?? 5;
63
+ this.commitBroadcastImmediateRetries = cfg.commitBroadcastImmediateRetries ?? 1;
64
+ }
58
65
 
59
66
  /**
60
67
  * Creates a base 58 BTC string hash for a message to uniquely identify a transaction
@@ -528,27 +535,7 @@ export class ClusterCoordinator {
528
535
  // so each peer can independently reach consensus and execute the operations.
529
536
  // Without this, only the coordinator's local cluster executes — remote peers
530
537
  // never see enough commits to reach consensus on their own.
531
- const broadcastResults = await Promise.allSettled(
532
- peerIds.map(peerIdStr => {
533
- const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
534
- return isLocal
535
- ? this.localCluster!.update(record)
536
- : this.createClusterClient(peerIdFromString(peerIdStr)).update(record).catch(err => {
537
- log('cluster-tx:consensus-broadcast-error', { messageHash: record.messageHash, peerId: peerIdStr, error: (err as Error).message });
538
- return null;
539
- });
540
- })
541
- );
542
-
543
- // Check for broadcast failures (excluding local)
544
- const broadcastFailures: string[] = [];
545
- broadcastResults.forEach((result, idx) => {
546
- const peerId = peerIds[idx]!;
547
- const isLocal = this.localCluster && peerId === this.localCluster.peerId.toString();
548
- if (!isLocal && (result.status === 'rejected' || result.value === null)) {
549
- broadcastFailures.push(peerId);
550
- }
551
- });
538
+ const { failures: broadcastFailures } = await this.broadcastMergedRecord(record, peerIds);
552
539
  if (broadcastFailures.length > 0) {
553
540
  this.scheduleCommitRetry(record.messageHash, record, broadcastFailures);
554
541
  } else {
@@ -565,6 +552,59 @@ export class ClusterCoordinator {
565
552
  return record;
566
553
  }
567
554
 
555
+ /**
556
+ * Broadcast the merged commit record to every peer, with `commitBroadcastImmediateRetries`
557
+ * in-line re-attempts per peer before giving up. The libp2p connection used during
558
+ * the prior commit phase is typically still warm, so a single immediate retry recovers
559
+ * most transient stream errors without falling back to the scheduled retry timer.
560
+ * Local cluster is invoked exactly once — local failures are fatal, not transient.
561
+ */
562
+ private async broadcastMergedRecord(record: ClusterRecord, peerIds: string[]): Promise<{ failures: string[] }> {
563
+ const maxAttempts = 1 + Math.max(0, this.commitBroadcastImmediateRetries);
564
+ const results = await Promise.all(peerIds.map(async peerIdStr => {
565
+ const isLocal = this.localCluster && peerIdStr === this.localCluster.peerId.toString();
566
+ if (isLocal) {
567
+ try {
568
+ await this.localCluster!.update(record);
569
+ return { peerId: peerIdStr, success: true as const };
570
+ } catch (err) {
571
+ log('cluster-tx:consensus-broadcast-error', {
572
+ messageHash: record.messageHash,
573
+ peerId: peerIdStr,
574
+ error: (err as Error).message
575
+ });
576
+ return { peerId: peerIdStr, success: false as const };
577
+ }
578
+ }
579
+ let lastError: unknown;
580
+ for (let attempt = 1; attempt <= maxAttempts; attempt++) {
581
+ try {
582
+ await this.createClusterClient(peerIdFromString(peerIdStr)).update(record);
583
+ return { peerId: peerIdStr, success: true as const };
584
+ } catch (err) {
585
+ lastError = err;
586
+ if (attempt < maxAttempts) {
587
+ log('cluster-tx:consensus-broadcast-retry', {
588
+ messageHash: record.messageHash,
589
+ peerId: peerIdStr,
590
+ attempt,
591
+ error: (err as Error).message
592
+ });
593
+ }
594
+ }
595
+ }
596
+ log('cluster-tx:consensus-broadcast-error', {
597
+ messageHash: record.messageHash,
598
+ peerId: peerIdStr,
599
+ attempts: maxAttempts,
600
+ error: lastError instanceof Error ? lastError.message : String(lastError)
601
+ });
602
+ return { peerId: peerIdStr, success: false as const };
603
+ }));
604
+ const failures = results.filter(r => !r.success).map(r => r.peerId);
605
+ return { failures };
606
+ }
607
+
568
608
  private updateTransactionRecord(record: ClusterRecord, stage: string): void {
569
609
  const state = this.transactions.get(record.messageHash);
570
610
  if (!state) {
@@ -65,6 +65,14 @@ export class CoordinatorRepo implements IRepo {
65
65
  private readonly localPeerId?: PeerId;
66
66
  private readonly responsibilityCache = new LruMap<string, { inCluster: boolean, expires: number }>(1000);
67
67
  private static readonly RESPONSIBILITY_TTL_MS = 60_000;
68
+ private readonly lastSeenCommitMs = new LruMap<string, number>(1000);
69
+ private readonly readRepairMode: 'off' | 'lazy' | 'paranoid';
70
+ private readonly readRepairWindowMs: number;
71
+ private readonly readRepairSampleRate: number;
72
+ /** Test seam: overridable clock for window-based read-repair gating. */
73
+ now: () => number = () => Date.now();
74
+ /** Test seam: overridable RNG (0..1) for sample-rate gating. */
75
+ rand: () => number = () => Math.random();
68
76
 
69
77
  constructor(
70
78
  readonly keyNetwork: IKeyNetwork,
@@ -86,8 +94,19 @@ export class CoordinatorRepo implements IRepo {
86
94
  minAbsoluteClusterSize: cfg?.minAbsoluteClusterSize ?? 3,
87
95
  allowClusterDownsize: cfg?.allowClusterDownsize ?? true,
88
96
  clusterSizeTolerance: cfg?.clusterSizeTolerance ?? 0.5,
89
- partitionDetectionWindow: cfg?.partitionDetectionWindow ?? 60000
97
+ partitionDetectionWindow: cfg?.partitionDetectionWindow ?? 60000,
98
+ commitBroadcastRetryInitialMs: cfg?.commitBroadcastRetryInitialMs ?? 250,
99
+ commitBroadcastRetryBackoffFactor: cfg?.commitBroadcastRetryBackoffFactor ?? 2,
100
+ commitBroadcastRetryMaxIntervalMs: cfg?.commitBroadcastRetryMaxIntervalMs ?? 8000,
101
+ commitBroadcastRetryMaxAttempts: cfg?.commitBroadcastRetryMaxAttempts ?? 5,
102
+ commitBroadcastImmediateRetries: cfg?.commitBroadcastImmediateRetries ?? 1,
103
+ readRepairMode: cfg?.readRepairMode ?? 'lazy',
104
+ readRepairWindowMs: cfg?.readRepairWindowMs ?? 10000,
105
+ readRepairSampleRate: cfg?.readRepairSampleRate ?? 0
90
106
  };
107
+ this.readRepairMode = policy.readRepairMode!;
108
+ this.readRepairWindowMs = policy.readRepairWindowMs!;
109
+ this.readRepairSampleRate = policy.readRepairSampleRate!;
91
110
  const localClusterRef = localCluster && localPeerId ? {
92
111
  update: localCluster.update.bind(localCluster),
93
112
  peerId: localPeerId,
@@ -159,24 +178,44 @@ export class CoordinatorRepo implements IRepo {
159
178
  // First try local storage
160
179
  const localResult = await this.storageRepo.get(blockGets, options);
161
180
 
162
- // Check for blocks that weren't found locally - try to fetch from cluster peers
163
- // Skip cluster fetch if this is already a sync request (to prevent recursive queries)
181
+ // Decide per-block whether to consult cluster peers. Two triggers:
182
+ // (a) Missing block isn't present locally at all (legacy behavior).
183
+ // (b) Stale-by-policy — block is present but read-repair policy says verify.
184
+ // Skip cluster fetch if this is already a sync request (to prevent recursive queries).
164
185
  const skipClusterFetch = (options as any)?.skipClusterFetch;
165
186
  if (this.clusterLatestCallback && !skipClusterFetch) {
166
187
  for (const blockId of blockGets.blockIds) {
167
188
  const localEntry = localResult[blockId];
168
- // If block not found locally (no state), try cluster peers
169
- if (!localEntry?.state?.latest) {
170
- try {
171
- await this.fetchBlockFromCluster(blockId, blockGets.context);
172
- // Re-fetch after sync
173
- const refreshed = await this.storageRepo.get({ blockIds: [blockId], context: blockGets.context }, options);
174
- if (refreshed[blockId]) {
175
- localResult[blockId] = refreshed[blockId];
189
+ const localRev = localEntry?.state?.latest?.rev;
190
+ const isMissing = !localEntry?.state?.latest;
191
+ const isStale = !isMissing && this.shouldReadRepair(blockId);
192
+ if (!isMissing && !isStale) continue;
193
+
194
+ if (isStale) {
195
+ log('cluster-tx:read-repair-triggered', {
196
+ blockId,
197
+ mode: this.readRepairMode,
198
+ ageMs: this.ageMs(blockId),
199
+ localRev
200
+ });
201
+ }
202
+
203
+ try {
204
+ await this.fetchBlockFromCluster(blockId, blockGets.context);
205
+ const refreshed = await this.storageRepo.get({ blockIds: [blockId], context: blockGets.context }, options);
206
+ const newRev = refreshed[blockId]?.state?.latest?.rev;
207
+ if (refreshed[blockId]) {
208
+ localResult[blockId] = refreshed[blockId];
209
+ }
210
+ if (isStale) {
211
+ if (typeof newRev === 'number' && typeof localRev === 'number' && newRev > localRev) {
212
+ log('cluster-tx:read-repair-applied', { blockId, oldRev: localRev, newRev });
213
+ } else {
214
+ log('cluster-tx:read-repair-noop', { blockId });
176
215
  }
177
- } catch (err) {
178
- log('cluster-fetch:error', { blockId, error: (err as Error).message });
179
216
  }
217
+ } catch (err) {
218
+ log('cluster-fetch:error', { blockId, error: (err as Error).message });
180
219
  }
181
220
  }
182
221
  }
@@ -184,6 +223,44 @@ export class CoordinatorRepo implements IRepo {
184
223
  return localResult;
185
224
  }
186
225
 
226
+ /** Decide whether the read-repair policy wants us to consult the cluster for a present-but-possibly-stale block. */
227
+ private shouldReadRepair(blockId: BlockId): boolean {
228
+ switch (this.readRepairMode) {
229
+ case 'off': return false;
230
+ case 'paranoid': return true;
231
+ case 'lazy': {
232
+ const lastSeen = this.lastSeenCommitMs.get(blockId);
233
+ if (lastSeen == null) return true;
234
+ if (this.now() - lastSeen > this.readRepairWindowMs) return true;
235
+ if (this.readRepairSampleRate > 0 && this.rand() < this.readRepairSampleRate) return true;
236
+ return false;
237
+ }
238
+ }
239
+ }
240
+
241
+ /** Milliseconds since we last marked this block fresh, or undefined if never. */
242
+ private ageMs(blockId: BlockId): number | undefined {
243
+ const lastSeen = this.lastSeenCommitMs.get(blockId);
244
+ return lastSeen == null ? undefined : this.now() - lastSeen;
245
+ }
246
+
247
+ /** Mark blocks as freshly observed from cluster authority (post-commit or post-fetch). */
248
+ private markBlocksSeen(blockIds: BlockId[]): void {
249
+ const now = this.now();
250
+ for (const id of blockIds) {
251
+ this.lastSeenCommitMs.set(id, now);
252
+ }
253
+ }
254
+
255
+ /**
256
+ * Test seam: directly set the last-seen timestamp for a block. Used by read-repair
257
+ * specs to simulate "the local commit happened at time T" without needing to drive
258
+ * a full pend/commit cycle through the cluster coordinator.
259
+ */
260
+ setLastSeenForTest(blockId: BlockId, ts: number): void {
261
+ this.lastSeenCommitMs.set(blockId, ts);
262
+ }
263
+
187
264
  private async fetchBlockFromCluster(blockId: BlockId, context?: ActionContext): Promise<void> {
188
265
  if (!this.clusterLatestCallback) return;
189
266
 
@@ -210,6 +287,7 @@ export class CoordinatorRepo implements IRepo {
210
287
  // Found on cluster - trigger restoration to sync the block
211
288
  await this.storageRepo.get({ blockIds: [blockId], context: { committed: [clusterLatest], rev: clusterLatest.rev } });
212
289
  log('cluster-fetch:synced', { blockId, rev: clusterLatest.rev });
290
+ this.markBlocksSeen([blockId]);
213
291
  }
214
292
  }
215
293
 
@@ -327,7 +405,9 @@ export class CoordinatorRepo implements IRepo {
327
405
 
328
406
  const peerCount = await this.coordinator.getClusterSize(blockIds[0]!);
329
407
  if (peerCount <= 1) {
330
- return await this.storageRepo.commit(request, options);
408
+ const result = await this.storageRepo.commit(request, options);
409
+ if (result.success) this.markBlocksSeen(blockIds);
410
+ return result;
331
411
  }
332
412
 
333
413
  const message: RepoMessage = {
@@ -338,6 +418,7 @@ export class CoordinatorRepo implements IRepo {
338
418
  try {
339
419
  const { record, localExecuted } = await this.coordinator.executeClusterTransaction(blockIds[0]!, message, options);
340
420
  if (localExecuted) {
421
+ this.markBlocksSeen(blockIds);
341
422
  return { success: true };
342
423
  }
343
424
  // Local cluster didn't execute during consensus. Attempt a local commit,
@@ -346,13 +427,16 @@ export class CoordinatorRepo implements IRepo {
346
427
  // after missing the pend phase (unreachable during pend, fresh join, etc.).
347
428
  // The cluster's majority is authoritative; this peer will catch up via sync.
348
429
  try {
349
- return await this.storageRepo.commit(request, options);
430
+ const result = await this.storageRepo.commit(request, options);
431
+ if (result.success) this.markBlocksSeen(blockIds);
432
+ return result;
350
433
  } catch (err) {
351
434
  if (clusterReachedCommitConsensus(record)) {
352
435
  log('coordinator-repo:commit-local-failed-cluster-succeeded', {
353
436
  actionId: request.actionId,
354
437
  error: (err as Error).message
355
438
  });
439
+ this.markBlocksSeen(blockIds);
356
440
  return { success: true };
357
441
  }
358
442
  throw err;
@@ -1,9 +1,9 @@
1
1
  import { pipe } from 'it-pipe'
2
2
  import { decode as lpDecode, encode as lpEncode } from 'it-length-prefixed'
3
- import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId } from '@libp2p/interface'
3
+ import type { Startable, Logger, Stream, Connection, StreamHandler, PeerId, Libp2p } from '@libp2p/interface'
4
4
  import type { IRepo, RepoMessage } from '@optimystic/db-core'
5
+ import { blockIdsForTransforms } from '@optimystic/db-core'
5
6
  import { peersEqual } from '../peer-utils.js'
6
- import { sha256 } from 'multiformats/hashes/sha2'
7
7
  import { encodePeers, type RedirectPayload } from './redirect.js'
8
8
  import type { Uint8ArrayList } from 'uint8arraylist'
9
9
  import { createLogger } from '../logger.js'
@@ -28,6 +28,13 @@ export type RepoServiceComponents = BaseComponents & {
28
28
  networkManager?: NetworkManagerLike
29
29
  peerId?: PeerId
30
30
  getConnectionAddrs?: (peerId: PeerId) => string[]
31
+ /**
32
+ * Optional libp2p node. The production wiring injects the node post-construction
33
+ * via {@link RepoService.setLibp2p} (the `components.libp2p` proxy does not
34
+ * reliably resolve from inside a service at request time); this field is a
35
+ * best-effort fallback resolver used only when no node has been injected.
36
+ */
37
+ libp2p?: Libp2p
31
38
  }
32
39
 
33
40
  export type RepoServiceInit = {
@@ -65,6 +72,14 @@ export class RepoService implements Startable {
65
72
  private running: boolean
66
73
  /** Responsibility K - how many peers are responsible for a key (for redirect decisions) */
67
74
  private readonly responsibilityK: number
75
+ /**
76
+ * The libp2p node, injected post-construction by the node wiring (see
77
+ * libp2p-node-base.ts, mirroring how `networkManager`/`fret` receive theirs).
78
+ * The libp2p `components.libp2p` proxy does NOT reliably resolve from inside a
79
+ * service at request time, so the redirect path resolves the network manager,
80
+ * self identity, and connection addrs through this explicitly-set reference.
81
+ */
82
+ private libp2pRef: Libp2p | undefined
68
83
 
69
84
  constructor(components: RepoServiceComponents, init: RepoServiceInit = {}) {
70
85
  this.components = components
@@ -80,6 +95,19 @@ export class RepoService implements Startable {
80
95
 
81
96
  readonly [Symbol.toStringTag] = '@libp2p/repo-service'
82
97
 
98
+ /**
99
+ * Inject the running libp2p node. Called once post-construction by the node
100
+ * wiring so the redirect path can resolve the network manager / self id / addrs.
101
+ */
102
+ setLibp2p(libp2p: Libp2p): void {
103
+ this.libp2pRef = libp2p
104
+ }
105
+
106
+ /** Resolve the libp2p node: the injected ref first, then the (best-effort) components proxy. */
107
+ private getLibp2p(): Libp2p | undefined {
108
+ return this.libp2pRef ?? (this.components as any).libp2p
109
+ }
110
+
83
111
  /**
84
112
  * Start the service
85
113
  */
@@ -110,17 +138,17 @@ export class RepoService implements Startable {
110
138
 
111
139
  private getNetworkManager(): NetworkManagerLike | undefined {
112
140
  if (this.components.networkManager) return this.components.networkManager
113
- return (this.components as any).libp2p?.services?.networkManager as NetworkManagerLike | undefined
141
+ return (this.getLibp2p() as any)?.services?.networkManager as NetworkManagerLike | undefined
114
142
  }
115
143
 
116
144
  private getSelfId(): PeerId | undefined {
117
145
  if (this.components.peerId) return this.components.peerId
118
- return (this.components as any).libp2p?.peerId as PeerId | undefined
146
+ return this.getLibp2p()?.peerId as PeerId | undefined
119
147
  }
120
148
 
121
149
  private getPeerAddrs(peerId: PeerId): string[] {
122
150
  if (this.components.getConnectionAddrs) return this.components.getConnectionAddrs(peerId)
123
- const libp2p = (this.components as any).libp2p
151
+ const libp2p = this.getLibp2p() as any
124
152
  if (!libp2p?.getConnections) return []
125
153
  const conns: any[] = libp2p.getConnections(peerId) ?? []
126
154
  const addrs: string[] = []
@@ -131,6 +159,41 @@ export class RepoService implements Startable {
131
159
  return addrs
132
160
  }
133
161
 
162
+ /**
163
+ * Derive the redirect routing key and op name for a single operation.
164
+ *
165
+ * The key MUST be the block the corresponding handler actually coordinates and
166
+ * verifies responsibility on, so redirect routing stays consistent with where the
167
+ * op is executed:
168
+ * - get → blockIds[0]
169
+ * - pend → blockIdsForTransforms(transforms)[0]
170
+ * - cancel → actionRef.blockIds[0]
171
+ * - commit → blockIds[0] (CoordinatorRepo.commit anchors consensus on
172
+ * getClusterSize(blockIds[0]) / executeClusterTransaction(blockIds[0]) and guards
173
+ * with verifyResponsibility(blockIds) — NOT tailId; for a per-block commit batch
174
+ * whose blockIds[0] !== tailId, keying on tailId redirected the commit to the
175
+ * collection tail's cluster, which then fails verifyResponsibility for the non-tail block.)
176
+ *
177
+ * Returns blockKey === undefined when the op carries no routable key (e.g. a cancel
178
+ * with an empty blockIds list), in which case the caller handles it locally without a
179
+ * redirect check.
180
+ */
181
+ deriveBlockKey(operation: RepoMessage['operations'][number]): { blockKey: string | undefined, opName: string } {
182
+ if ('get' in operation) {
183
+ return { blockKey: operation.get.blockIds[0], opName: 'get' }
184
+ }
185
+ if ('pend' in operation) {
186
+ return { blockKey: blockIdsForTransforms(operation.pend.transforms)[0], opName: 'pend' }
187
+ }
188
+ if ('cancel' in operation) {
189
+ return { blockKey: operation.cancel.actionRef.blockIds[0], opName: 'cancel' }
190
+ }
191
+ if ('commit' in operation) {
192
+ return { blockKey: operation.commit.blockIds[0], opName: 'commit' }
193
+ }
194
+ return { blockKey: undefined, opName: 'unknown' }
195
+ }
196
+
134
197
  /**
135
198
  * Check if this node should redirect the request for a given key.
136
199
  * Returns a RedirectPayload if not responsible, null if should handle locally.
@@ -140,8 +203,14 @@ export class RepoService implements Startable {
140
203
  const nm = this.getNetworkManager()
141
204
  if (!nm) return null
142
205
 
143
- const mh = await sha256.digest(new TextEncoder().encode(blockKey))
144
- const key = mh.digest
206
+ // Pass the RAW encoded block-key bytes to getCluster. getCluster hashes
207
+ // internally (hashKey == sha256), so the responsible-set coordinate becomes
208
+ // hashKey(encode(blockKey)) — identical to how the cluster coordinator
209
+ // derives it (ClusterCoordinator.getClusterForBlock → findCluster(encode(blockId))).
210
+ // Pre-hashing here would double-hash (hashKey(sha256(encode(blockKey)))), placing
211
+ // the cohort at an unrelated ring coordinate and redirecting requests the
212
+ // coordinator legitimately routed to this peer.
213
+ const key = new TextEncoder().encode(blockKey)
145
214
  const cluster = await nm.getCluster(key)
146
215
  ;(message as any).cluster = cluster.map((p: PeerId) => p.toString?.() ?? String(p))
147
216
 
@@ -175,46 +244,25 @@ export class RepoService implements Startable {
175
244
  const decoded = new TextDecoder().decode(msg.subarray())
176
245
  const message = JSON.parse(decoded) as RepoMessage
177
246
 
178
- // Process each operation
247
+ // Process each operation. Derive the redirect routing key once (keyed on the
248
+ // block the handler actually coordinates), redirect-check it, then dispatch.
179
249
  const operation = message.operations[0]
180
- let response: any
250
+ const { blockKey, opName } = this.deriveBlockKey(operation)
251
+ const redirect = blockKey !== undefined
252
+ ? await this.checkRedirect(blockKey, opName, message)
253
+ : null
181
254
 
182
- if ('get' in operation) {
183
- const blockKey = operation.get.blockIds[0]!
184
- const redirect = await this.checkRedirect(blockKey, 'get', message)
185
- if (redirect) {
186
- response = redirect
187
- } else {
188
- response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
189
- }
255
+ let response: any
256
+ if (redirect) {
257
+ response = redirect
258
+ } else if ('get' in operation) {
259
+ response = await this.repo.get(operation.get, { expiration: message.expiration, skipClusterFetch: true } as any)
190
260
  } else if ('pend' in operation) {
191
- const blockKey = Object.keys(operation.pend.transforms)[0]!
192
- const redirect = await this.checkRedirect(blockKey, 'pend', message)
193
- if (redirect) {
194
- response = redirect
195
- } else {
196
- response = await this.repo.pend(operation.pend, { expiration: message.expiration })
197
- }
261
+ response = await this.repo.pend(operation.pend, { expiration: message.expiration })
198
262
  } else if ('cancel' in operation) {
199
- const blockKey = operation.cancel.actionRef.blockIds[0]
200
- if (blockKey) {
201
- const redirect = await this.checkRedirect(blockKey, 'cancel', message)
202
- if (redirect) {
203
- response = redirect
204
- } else {
205
- response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
206
- }
207
- } else {
208
- response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
209
- }
263
+ response = await this.repo.cancel(operation.cancel.actionRef, { expiration: message.expiration })
210
264
  } else if ('commit' in operation) {
211
- const blockKey = operation.commit.tailId
212
- const redirect = await this.checkRedirect(blockKey, 'commit', message)
213
- if (redirect) {
214
- response = redirect
215
- } else {
216
- response = await this.repo.commit(operation.commit, { expiration: message.expiration })
217
- }
265
+ response = await this.repo.commit(operation.commit, { expiration: message.expiration })
218
266
  }
219
267
 
220
268
  // Encode and yield the response
@@ -0,0 +1,45 @@
1
+ /**
2
+ * Per-RPC deadline knobs shared by the simple {@link ProtocolClient} subclasses
3
+ * (cluster / sync / dispute). `dialTimeoutMs` bounds connecting; `responseTimeoutMs`
4
+ * bounds waiting for the reply once connected (so a peer that connects then goes
5
+ * silent throws {@link ResponseTimeoutError} instead of hanging the caller forever);
6
+ * `signal` cancels the whole request. All optional.
7
+ */
8
+ export type RpcDeadlineOptions = {
9
+ signal?: AbortSignal;
10
+ dialTimeoutMs?: number;
11
+ responseTimeoutMs?: number;
12
+ };
13
+
14
+ /**
15
+ * Default per-peer dial deadline. Matches `spread-on-churn.ts` `pushDialTimeoutMs`
16
+ * (3000ms) — an unreachable peer fails the dial fast so the caller can re-pick a
17
+ * different coordinator rather than blocking on a dead route.
18
+ */
19
+ export const DEFAULT_DIAL_TIMEOUT_MS = 3000;
20
+
21
+ /**
22
+ * Default per-peer response deadline. Matches `spread-on-churn.ts`
23
+ * `pushResponseTimeoutMs` (10000ms) — a peer that connects then never writes a
24
+ * reply is abandoned instead of hanging the caller forever.
25
+ */
26
+ export const DEFAULT_RESPONSE_TIMEOUT_MS = 10000;
27
+
28
+ /**
29
+ * Merge caller-supplied deadline options with the client-level defaults. An
30
+ * explicitly-supplied value wins (including a deliberate `0`, which
31
+ * {@link ProtocolClient.processMessage} reads as "no cap"); an absent key falls
32
+ * back to the default so a caller that passes nothing still gets a deadline.
33
+ * `signal` has no default — cancellation is always caller-driven.
34
+ *
35
+ * Unlike `BlockTransferClient` (which leaves its defaults to the owning
36
+ * `SpreadOnChurnMonitor` config), these clients have many callers and no single
37
+ * owning monitor, so the deadline default belongs here on the client.
38
+ */
39
+ export function withRpcDeadlineDefaults(options?: RpcDeadlineOptions): RpcDeadlineOptions {
40
+ return {
41
+ dialTimeoutMs: options?.dialTimeoutMs ?? DEFAULT_DIAL_TIMEOUT_MS,
42
+ responseTimeoutMs: options?.responseTimeoutMs ?? DEFAULT_RESPONSE_TIMEOUT_MS,
43
+ signal: options?.signal,
44
+ };
45
+ }