@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
@@ -0,0 +1,922 @@
1
+ /**
2
+ * Reactivity **mock-transport mesh harness** — the in-process, many-logical-node substrate for the
3
+ * reactivity *e2e* tier (`docs/reactivity.md`), layered on the cohort-topic mesh harness
4
+ * ({@link import("./cohort-topic-mesh-harness.js")}) rather than forking it (the same relationship the
5
+ * {@link import("./matchmaking-mesh-harness.js").MatchmakingMesh} has to it). It drives the **real**
6
+ * reactivity hot path end-to-end over a network of real-Ed25519-keyed cohort-topic hosts:
7
+ *
8
+ * - **Origination** rides the real {@link makeCohortTopicChangeNotifier} over a real {@link StorageRepo}:
9
+ * a `pend`+`commit` fires the catch-all change feed → the bridge hands the {@link CollectionChangeEvent}
10
+ * + a **real** threshold {@link CommitCert} (each tail-cohort member signs the commit-vote payload with
11
+ * its real Ed25519 key, assembled by {@link buildCommitCert}) to the real
12
+ * {@link ReactivityOriginationManager}, which builds the {@link NotificationV1} reusing the cert's
13
+ * threshold signature **unchanged** — exactly as in production (`local-change-notifier-bridge`).
14
+ * - **Forwarding** runs the real {@link createReactivityForwarder} receive path (verify → dedupe → buffer)
15
+ * over a real {@link PushState} (replay ring + rolling checkpoint + per-subscriber backpressure).
16
+ * - **Delivery** runs the real {@link ReactivitySubscriptionManager} (register at cohort-topic tier **T3**,
17
+ * verify against the cached tail-cohort {@link MembershipCertV1} with **real** collected-multisig crypto,
18
+ * contiguity check, gap → backfill, `(collectionId, revision)` dedupe, surface).
19
+ * - **Recovery** serves real {@link serveBackfill} / {@link serveResume} from the tail `PushState`, applied
20
+ * subscriber-side by the manager's `resume()` / backfill seam.
21
+ * - **Rotation** drives the real {@link BlockFillTracker} / {@link buildRotationHint} /
22
+ * {@link planReRegistrationWave} / {@link buildRotationHandoffCheckpoint} lifecycle; **backpressure**
23
+ * drives the real {@link PushState.enqueueForSubscribers}.
24
+ *
25
+ * **What is real vs. modeled (honest).** Every signature, registration record, replay-buffer entry, dedupe
26
+ * decision, checkpoint fold, and resume classification is the real db-core/db-p2p code. What the harness
27
+ * *models* is the **notification transport** (the application protocol that would dial each subscriber's
28
+ * primary and each child cohort) — it fans the originated notification to the tracked direct subscribers
29
+ * in-process via the real per-subscriber bounded queues — and, like the matchmaking harness, the
30
+ * **single-tier-0 reach**: the cohort-topic substrate serves a single tier-0 cohort (multi-tier *serving*
31
+ * promotion to a tier-`d ≥ 1` forwarder cohort is gated on the cohort-topic follow-ons), so a deep
32
+ * forwarder *tree* fan-out is tagged-unimplemented in the suites, not faked here. The tail cohort fanning
33
+ * to every direct subscriber — the heart of contiguous/verified delivery — is fully real.
34
+ *
35
+ * **Virtual time.** Like the sibling harnesses this is not a wall-clock simulator: `now` is an explicit
36
+ * virtual clock the harness advances ({@link ReactivityMesh.advanceTime}), so `T_drain` / rotation timing
37
+ * resolve deterministically without sleeping. The only real-time waits are the cohort harness's tiny
38
+ * async-settle polls.
39
+ */
40
+
41
+ import type { PrivateKey } from "@libp2p/interface";
42
+ import {
43
+ Tier,
44
+ reactivityTopicId,
45
+ createReactivityForwarder,
46
+ createNotificationVerifier,
47
+ PushState,
48
+ serveBackfill,
49
+ serveResume,
50
+ buildRotationHint,
51
+ BlockFillTracker,
52
+ TailDrainGate,
53
+ planReRegistrationWave,
54
+ buildRotationHandoffCheckpoint,
55
+ applyRotationHandoff,
56
+ createRejoinJitter,
57
+ mayServeAsReactivityForwarder,
58
+ deltaMaxForProfile,
59
+ coreProfile,
60
+ edgeProfile,
61
+ bytesToB64url,
62
+ DEFAULT_CAP_PROMOTE_FAST,
63
+ T_REJOIN_JITTER_MS,
64
+ WARM_THRESHOLD_DEFAULT,
65
+ type ActionId,
66
+ type BlockId,
67
+ type CohortTopicService,
68
+ type CollectionChangeEvent,
69
+ type CommitCert,
70
+ type ClusterRecord,
71
+ type ClusterPeers,
72
+ type Signature,
73
+ type ForwardDecision,
74
+ type IBlock,
75
+ type MembershipCertV1,
76
+ type NotificationV1,
77
+ type NotificationVerifier,
78
+ type NodeProfile,
79
+ type ReactivityForwarder,
80
+ type RegistrationHandle,
81
+ type ResumeApplyOutcome,
82
+ type ResumeReplyV1,
83
+ type ResumeV1,
84
+ type RotationHintV1,
85
+ type CheckpointSummary,
86
+ type ReRegistrationPlan,
87
+ type Transforms,
88
+ } from "@optimystic/db-core";
89
+ import { StorageRepo } from "../storage/storage-repo.js";
90
+ import { BlockStorage } from "../storage/block-storage.js";
91
+ import { MemoryRawStorage } from "../storage/memory-storage.js";
92
+ import { makeCohortTopicChangeNotifier } from "../cohort-topic/change-bridge.js";
93
+ import { buildCommitCert } from "../cluster/commit-cert.js";
94
+ import { ReactivitySubscriptionManager, type RotationNotice } from "../reactivity/subscription-manager.js";
95
+ import { ReactivityOriginationManager } from "../reactivity/origination-manager.js";
96
+ import { RotationReRegistrationScheduler, type RotationTimerCancel } from "../reactivity/rotation-rereg-scheduler.js";
97
+ import { RotationRedirectError } from "../reactivity/recover-transport.js";
98
+ import {
99
+ addressing,
100
+ buildMesh,
101
+ delay,
102
+ makeMembers,
103
+ setupTopic,
104
+ type CohortMesh,
105
+ type Member,
106
+ type MeshOptions,
107
+ type TopicSetup,
108
+ } from "./cohort-topic-mesh-harness.js";
109
+
110
+ export { addressing, delay };
111
+ export type { Member, NotificationV1 };
112
+
113
+ const utf8 = new TextEncoder();
114
+
115
+ /** Construction inputs for a {@link ReactivityMesh}. */
116
+ export interface ReactivityMeshOptions {
117
+ /** Node count. Default 12. */
118
+ readonly nodeCount?: number;
119
+ /** Cohort size `wantK`. Default `min(nodeCount, 8)`. */
120
+ readonly wantK?: number;
121
+ /** Verifier threshold (notifications are signed by all `wantK` tail members; the verifier needs `>= minSigs`). Default `max(1, wantK - 2)`. */
122
+ readonly minSigs?: number;
123
+ /** FRET network-size estimate (drives the default walk start tier). Default 256 → `d_max = 1`. */
124
+ readonly sizeEstimate?: number;
125
+ /**
126
+ * `cap_promote` applied to every cohort. Default very high so the topic stays a single tier-0 cohort for
127
+ * the life of a test (multi-tier *serving* promotion is a cohort-topic follow-on; see the module note).
128
+ * Lower it to drive the promotion *signal* (the cold-to-hot growth suite).
129
+ */
130
+ readonly capPromote?: number;
131
+ /** Per-node profile by index (Edge nodes never forward T3). Default all Core. */
132
+ readonly profiles?: readonly ("edge" | "core")[];
133
+ }
134
+
135
+ /** Per-collection knobs (scaled down so resume/checkpoint suites need only a few dozen commits). */
136
+ export interface CollectionOptions {
137
+ /** Replay-buffer depth `W` (default 256; scale down for fast resume tests). */
138
+ readonly w?: number;
139
+ /** Parent-checkpoint span `W_checkpoint` (default 4096). */
140
+ readonly wCheckpoint?: number;
141
+ /** Per-subscriber bounded-queue depth `queue_max` (default 32). */
142
+ readonly queueMax?: number;
143
+ /** Per-collection delta budget (bytes); default Core `delta_max`. */
144
+ readonly deltaMaxBytes?: number;
145
+ /** Block-fill size driving tail rotation (default 64). */
146
+ readonly blockFillSize?: number;
147
+ /** Anticipatory warm-up threshold (default 8). */
148
+ readonly warmThreshold?: number;
149
+ }
150
+
151
+ /** Per-subscription tuning. */
152
+ export interface SubscribeOptions {
153
+ /** Last revision already held; `0` (default) ⇒ fresh subscribe, adopt the first notification as baseline. */
154
+ readonly lastKnownRev?: number;
155
+ /** Node profile for the subscriber (Edge ⇒ shorter TTL, declines deltas, never forwards). Default the node's mesh profile. */
156
+ readonly profile?: NodeProfile;
157
+ /**
158
+ * When `false`, the subscriber does **not** drain its bounded queue after each fan-out — modeling a slow
159
+ * subscriber on a flaky link whose queue fills and drops-oldest. Default `true` (drain immediately).
160
+ */
161
+ readonly autoDrain?: boolean;
162
+ }
163
+
164
+ /** A live reactivity subscription in the mesh — the real manager + its delivery bookkeeping. */
165
+ export interface SubscriptionHandle {
166
+ readonly nodeIndex: number;
167
+ readonly member: Member;
168
+ /** Per-subscriber id (its dialable member bytes, base64url) — the backpressure-map key. */
169
+ readonly subId: string;
170
+ /** The real subscription manager (assigned once, immediately after the handle is built). */
171
+ manager: ReactivitySubscriptionManager;
172
+ /** Verified, contiguous, deduped notifications the manager surfaced (in delivery order). */
173
+ readonly delivered: NotificationV1[];
174
+ /** The tail (base64url) this subscriber currently tracks — fan-out targets only current-tail subscribers. */
175
+ attachedTailB64: string;
176
+ readonly collectionName: string;
177
+ /** Whether the subscriber drains its bounded queue automatically (false ⇒ slow-subscriber model). */
178
+ autoDrain: boolean;
179
+ /** Whether the subscriber is asleep (skipped by fan-out; its replay buffer still fills). */
180
+ asleep: boolean;
181
+ /** Registration handle from the real `service.register` walk. */
182
+ registration?: RegistrationHandle;
183
+ /** Number of backfill RPCs the subscriber's gap-detection seam drove. */
184
+ backfills: number;
185
+ /** Set true if a resume escalated to a chain read (out_of_window / untrusted checkpoint). */
186
+ chainRead: boolean;
187
+ /** Set to `[newTailId, newRevisionAtRotation]` if a resume returned tail_rotated. */
188
+ tailRotated?: readonly [string, number];
189
+ /** Checkpoint summaries applied on a checkpoint-window resume (the merged-digest hint). */
190
+ readonly checkpointDigests: CheckpointSummary[];
191
+ /** Rotation notices the manager surfaced from a delivered pre-announce / hard rotation (once per successor). */
192
+ readonly rotationNotices: RotationNotice[];
193
+ /**
194
+ * Per-subscriber re-registration scheduler bound to the manager's `onRotation` observer, driven over the
195
+ * harness virtual clock ({@link ReactivityMesh.advanceTime}). On fire it re-attaches this subscriber under
196
+ * the new tail (the production move the deferred Quereus `Database.watch` factory performs) — so the
197
+ * notify/recover rotation seams run end-to-end (`reactivity-rotation-host-wiring-e2e` §C).
198
+ */
199
+ scheduler: RotationReRegistrationScheduler;
200
+ }
201
+
202
+ /** Per-collection state the harness threads through origination, fan-out, and recovery. */
203
+ interface CollectionState {
204
+ readonly name: string;
205
+ readonly collectionId: Uint8Array;
206
+ readonly collectionIdB64: string;
207
+ tailId: Uint8Array;
208
+ topicId: Uint8Array;
209
+ coord0: Uint8Array;
210
+ tailCohort: Member[];
211
+ /** Cohort setup for the **current** tail; re-established (willingness re-seeded) on each {@link ReactivityMesh.rotateTail}. */
212
+ setup: TopicSetup;
213
+ readonly repo: StorageRepo;
214
+ readonly originationService: CohortTopicService;
215
+ origination?: ReactivityOriginationManager;
216
+ pushState: PushState;
217
+ forwarder: ReactivityForwarder;
218
+ readonly emitQueue: NotificationV1[];
219
+ rev: number;
220
+ blockSeq: number;
221
+ readonly deltaMaxBytes: number;
222
+ readonly w: number;
223
+ readonly wCheckpoint: number;
224
+ readonly queueMax: number;
225
+ readonly fillTracker: BlockFillTracker;
226
+ rotationHint?: RotationHintV1;
227
+ /**
228
+ * The outgoing tail's drain gate, set by a transport-driven {@link ReactivityMesh.rotateTail} (`{
229
+ * autoReattach: false }`) — models the running node's `ReactivityForwarderHost.markRotated`. A resume whose
230
+ * `latestKnownTailId` anchors {@link rotatedFromTailB64} is bounced to the new tree while it `isDraining`.
231
+ */
232
+ rotationGate?: TailDrainGate;
233
+ /** The tail (base64url) {@link rotationGate} rotated away from — the stale tail a resume gets redirected from. */
234
+ rotatedFromTailB64?: string;
235
+ readonly subscribers: SubscriptionHandle[];
236
+ /** Certs cached for actionId so the synchronous bridge extractor can resolve a pre-signed real cert. */
237
+ readonly certByAction: Map<string, CommitCert>;
238
+ }
239
+
240
+ /** Options for {@link ReactivityMesh.rotateTail}. */
241
+ export interface RotateTailOptions {
242
+ /**
243
+ * When `true` (default) the harness migrates live subscribers to the new tail directly (the wave/handoff
244
+ * continuity model). When `false` it instead models the running node's `ReactivityForwarderHost.markRotated`
245
+ * — arming the outgoing tail's {@link TailDrainGate} — so a subscriber resuming against the old tail is
246
+ * **redirected** to the new tree and re-attaches through its own `onRotation` → scheduler → reRegister path
247
+ * (the transport-driven e2e proof, `reactivity-rotation-host-wiring-e2e` §C).
248
+ */
249
+ readonly autoReattach?: boolean;
250
+ }
251
+
252
+ /** The outcome of a {@link ReactivityMesh.rotateTail} — the planned re-registration wave + handoff. */
253
+ export interface RotationResult {
254
+ readonly newTailId: Uint8Array;
255
+ readonly newTailIdB64: string;
256
+ readonly rotationRevision: number;
257
+ readonly plans: readonly ReRegistrationPlan[];
258
+ /** Peak re-registration arrivals in any `T_rejoin_jitter`-long window of the wave (the fast-promote bound). */
259
+ readonly peakWindowArrivals: number;
260
+ /** The buffer-to-checkpoint handoff folded onto the new tail (undefined if the old ring was empty). */
261
+ readonly handoff?: CheckpointSummary;
262
+ }
263
+
264
+ /**
265
+ * The reactivity integration mesh. Build with {@link buildReactivityMesh}; register a collection with
266
+ * {@link registerCollection}; drive subscribers/commits/recovery with {@link subscribe} / {@link commit} /
267
+ * {@link resume} / {@link rotateTail}.
268
+ */
269
+ export class ReactivityMesh {
270
+ private readonly collections = new Map<string, CollectionState>();
271
+ private vtime = 1_700_000_000_000;
272
+ private corr = 0;
273
+ /** Pending one-shot timers armed against the virtual clock (the rotation scheduler's `setTimer` binding). */
274
+ private readonly virtualTimers: { fireAt: number; fn: () => void; cancelled: boolean }[] = [];
275
+
276
+ private constructor(
277
+ readonly mesh: CohortMesh,
278
+ readonly members: Member[],
279
+ private readonly wantK: number,
280
+ private readonly minSigs: number,
281
+ private readonly profiles: readonly ("edge" | "core")[],
282
+ ) {}
283
+
284
+ /** Stand up an N-node reactivity mesh and start every host. */
285
+ static async build(opts: ReactivityMeshOptions = {}): Promise<ReactivityMesh> {
286
+ const nodeCount = opts.nodeCount ?? 12;
287
+ const wantK = opts.wantK ?? Math.min(nodeCount, 8);
288
+ const minSigs = opts.minSigs ?? Math.max(1, wantK - 2);
289
+ const sizeEstimate = opts.sizeEstimate ?? 256;
290
+ const members = await makeMembers(nodeCount);
291
+ const meshOpts: MeshOptions = {
292
+ wantK,
293
+ minSigs,
294
+ sizeEstimate,
295
+ // Single-tier-0 by default (same rationale as the matchmaking harness): test artifacts register
296
+ // many subscribers within a few ms, which would otherwise trip the pre-promotion slope predictor.
297
+ capPromote: opts.capPromote ?? 1_000_000,
298
+ // This mesh is virtual-time: a subscribe batch lands at one fixed virtual instant, but the cohort
299
+ // host stamps each register with real `Date.now()`. The growth-slope pre-promotion is a
300
+ // wall-clock-rate heuristic, so those real-time-separated samples make it fire *before* the
301
+ // `cap_promote` count is reached (redirecting the rest of the batch up a tier-1 tree the
302
+ // single-tier-0 milestone never instantiates). Disable slope here so promotion is driven purely by
303
+ // the count threshold — the same posture the cohort-topic scale specs get by stamping a constant
304
+ // `now` on every `handleRegister`. The slope heuristic itself is covered by the promotion unit specs.
305
+ promotion: { tPromoteLookaheadMs: 0 },
306
+ // Permissive register ceiling — the harness re-probes a cohort across many subscribes/commits.
307
+ // Also widen the replay-guard freshness window. The cohort host stamps each register with real
308
+ // `Date.now()` and evaluates the anti-DoS freshness gate against real `Date.now()` at processing
309
+ // time, but this mesh drives the real participant walk in-process: under full-suite CPU load the
310
+ // event loop can stall for far longer than the production 60 s staleness window between a register
311
+ // being stamped and the deciding cohort processing it. A stalled-but-legitimate register then
312
+ // trips the freshness gate (`no_state`), the cold-start bootstrap walk exhausts, and `register`
313
+ // throws `CohortBackoffError` — a pure load-induced harness flake. The wall-clock freshness
314
+ // defense is meaningless here (same posture as the disabled rate limiter above), so make the
315
+ // window effectively unbounded so test-induced stalls never read as stale/future replays.
316
+ // Disable the cold-start proof-of-work cost. Reactivity subscribers register at tier T3, so the
317
+ // participant-side bootstrap-evidence builder mints a PoW puzzle (default 20 bits ≈ 1 M hashes) on
318
+ // *every* register/re-probe — by far the dominant cost in this mesh (a 6-subscriber promotion test
319
+ // burns ~60-100 s almost entirely in `randomFillSync`/SHA-256 minting nonces). The cohort's
320
+ // bootstrap-evidence policy is unconfigured here, so the evidence is never even required; minting it
321
+ // is pure wasted work. `bits: 0` solves on the first nonce (the documented test escape hatch in
322
+ // `meetsDifficulty`) — same posture as the neutralized rate limiter / replay guard above, and it
323
+ // removes the timing-sensitivity that tips these tests into a timeout under full-suite load.
324
+ antiDos: { rateLimiter: { ratePerWindow: 1_000_000 }, replayGuard: { maxAgeMs: 86_400_000, maxFutureSkewMs: 86_400_000 }, powDifficultyBits: 0 },
325
+ ...(opts.profiles === undefined ? {} : { profiles: opts.profiles }),
326
+ };
327
+ const mesh = await buildMesh(members, meshOpts);
328
+ const profiles = Array.from({ length: nodeCount }, (_v, i) => opts.profiles?.[i] ?? "core");
329
+ return new ReactivityMesh(mesh, members, wantK, minSigs, profiles);
330
+ }
331
+
332
+ /** The current virtual time (unix ms). */
333
+ get now(): number {
334
+ return this.vtime;
335
+ }
336
+
337
+ /**
338
+ * Advance the virtual clock by `ms` (drives `T_drain` / rotation timing deterministically), then fire any
339
+ * virtual timers now due. A fired timer may arm another (a chained rotation), so fire one-at-a-time until
340
+ * the queue is quiescent — the rotation re-registration scheduler's `setTimer` binding rides this.
341
+ */
342
+ advanceTime(ms: number): void {
343
+ this.vtime += ms;
344
+ for (;;) {
345
+ const idx = this.virtualTimers.findIndex((t) => !t.cancelled && t.fireAt <= this.vtime);
346
+ if (idx === -1) {
347
+ break;
348
+ }
349
+ const [timer] = this.virtualTimers.splice(idx, 1);
350
+ if (!timer!.cancelled) {
351
+ timer!.fn();
352
+ }
353
+ }
354
+ }
355
+
356
+ /** Arm a one-shot timer against the virtual clock; the cancel handle drops it (the scheduler's `setTimer`). */
357
+ private armVirtualTimer(fn: () => void, delayMs: number): RotationTimerCancel {
358
+ const timer = { fireAt: this.vtime + Math.max(0, delayMs), fn, cancelled: false };
359
+ this.virtualTimers.push(timer);
360
+ return (): void => { timer.cancelled = true; };
361
+ }
362
+
363
+ private profileOf(index: number): NodeProfile {
364
+ return this.profiles[index] === "edge" ? edgeProfile() : coreProfile();
365
+ }
366
+
367
+ /**
368
+ * Register a collection on the mesh: instantiate its tail-anchored tier-0 cohort (+willingness quorum),
369
+ * install the real origination manager behind the real change-notifier bridge over a fresh `StorageRepo`,
370
+ * build the tail `PushState` (replay ring + rolling checkpoint + backpressure), and cache the tail
371
+ * cohort's `MembershipCertV1` into every node's verifier so end-to-end verification is real Ed25519.
372
+ */
373
+ async registerCollection(name: string, opts: CollectionOptions = {}): Promise<void> {
374
+ const collectionId = utf8.encode(`reactivity:${name}`);
375
+ const collectionIdB64 = bytesToB64url(collectionId);
376
+ const tailId = this.pinTailToCore(`${name}:tail-0`);
377
+ const topicId = reactivityTopicId(tailId);
378
+ const coord0 = addressing.coord0(topicId);
379
+ const setup = await setupTopic(this.mesh, topicId);
380
+ const tailCohort = this.cohortMembersAround(coord0);
381
+
382
+ const deltaMaxBytes = opts.deltaMaxBytes ?? deltaMaxForProfile(coreProfile());
383
+ const w = opts.w ?? 256;
384
+ const wCheckpoint = opts.wCheckpoint ?? 4096;
385
+ const queueMax = opts.queueMax ?? 32;
386
+
387
+ const rawStorage = new MemoryRawStorage();
388
+ const repo = new StorageRepo((blockId) => new BlockStorage(blockId, rawStorage));
389
+
390
+ // Origination is the tail cohort primary's responsibility — installed on the node nearest coord_0
391
+ // (the routed tail primary), exactly the node a commit on the tail cluster lands on in production.
392
+ const originationService = setup.deciding.host.service;
393
+ const certByAction = new Map<string, CommitCert>();
394
+ const emitQueue: NotificationV1[] = [];
395
+ const pushState = this.makePushState(collectionIdB64, topicId, tailId, w, wCheckpoint, queueMax, deltaMaxBytes);
396
+
397
+ const state: CollectionState = {
398
+ name,
399
+ collectionId,
400
+ collectionIdB64,
401
+ tailId,
402
+ topicId,
403
+ coord0,
404
+ tailCohort,
405
+ setup,
406
+ repo,
407
+ originationService,
408
+ pushState,
409
+ forwarder: createReactivityForwarder({ state: pushState, verifier: this.notificationVerifierFor(originationService) }),
410
+ emitQueue,
411
+ rev: 0,
412
+ blockSeq: 0,
413
+ deltaMaxBytes,
414
+ w,
415
+ wCheckpoint,
416
+ queueMax,
417
+ fillTracker: new BlockFillTracker({
418
+ ...(opts.blockFillSize === undefined ? {} : { blockFillSize: opts.blockFillSize }),
419
+ // warm_threshold must be < block_fill_size; clamp the default down for small (test-scaled) blocks.
420
+ warmThreshold: opts.warmThreshold ?? (opts.blockFillSize === undefined ? WARM_THRESHOLD_DEFAULT : Math.min(WARM_THRESHOLD_DEFAULT, Math.max(0, opts.blockFillSize - 1))),
421
+ }),
422
+ subscribers: [],
423
+ certByAction,
424
+ };
425
+
426
+ const origination = new ReactivityOriginationManager({
427
+ service: originationService,
428
+ resolveContext: (): { tailId: Uint8Array; deltaMaxBytes: number; rotationHint?: RotationHintV1 } => ({
429
+ tailId: state.tailId,
430
+ deltaMaxBytes: state.deltaMaxBytes,
431
+ ...(state.rotationHint === undefined ? {} : { rotationHint: state.rotationHint }),
432
+ }),
433
+ emit: (n): void => { emitQueue.push(n); },
434
+ clock: (): number => this.vtime,
435
+ });
436
+ origination.install();
437
+ state.origination = origination;
438
+
439
+ // The real local-change-notifier bridge: a StorageRepo commit → onLocalCommit → origination.
440
+ makeCohortTopicChangeNotifier({
441
+ source: repo,
442
+ service: originationService,
443
+ selfIsCohortMember: (e): boolean => e.collectionId === (collectionIdB64 as unknown as CollectionChangeEvent["collectionId"]),
444
+ extractCommitCert: (e): CommitCert | undefined => certByAction.get(e.actionId),
445
+ });
446
+
447
+ this.collections.set(name, state);
448
+ this.cacheTailCert(state);
449
+ }
450
+
451
+ /** The reactivity notification verifier over a node's service verifier (real collected-multisig at T3). */
452
+ private notificationVerifierFor(service: CohortTopicService): NotificationVerifier {
453
+ return createNotificationVerifier({ verifier: service.verifier(), tier: Tier.T3 });
454
+ }
455
+
456
+ private makePushState(collectionIdB64: string, topicId: Uint8Array, tailId: Uint8Array, w: number, wCheckpoint: number, queueMax: number, deltaMaxBytes: number): PushState {
457
+ return new PushState({
458
+ collectionId: collectionIdB64,
459
+ topicId: bytesToB64url(topicId),
460
+ tailIdAtJoin: bytesToB64url(tailId),
461
+ w,
462
+ wCheckpoint,
463
+ queueMax,
464
+ deltaMaxBytes,
465
+ });
466
+ }
467
+
468
+ /** The FRET cohort (the `wantK` nearest members) around a coord. */
469
+ private cohortMembersAround(coord: Uint8Array): Member[] {
470
+ const ids = new Set(this.mesh.assembleCohort(coord, this.wantK));
471
+ return this.members.filter((m) => ids.has(m.idStr));
472
+ }
473
+
474
+ /** Whether the node FRET-routes nearest to `coord` (the cohort's routed primary) is an Edge profile. */
475
+ private nearestIsEdge(coord: Uint8Array): boolean {
476
+ const nearest = this.mesh.nearest(coord);
477
+ const idx = this.members.findIndex((m) => m.idStr === nearest.idStr);
478
+ return idx >= 0 && this.profiles[idx] === "edge";
479
+ }
480
+
481
+ /**
482
+ * Choose a reactivity tail id derived from `baseTail` whose `coord_0` routes nearest a **Core** node.
483
+ * Reactivity topics serve at tier T3, and an Edge node declines a T3 cold-start by design (it serves no
484
+ * forwarder duty — pinned by `cohort-topic-scale-lifecycle` "Edge serves no T3"), so a topic whose
485
+ * `coord_0` routes nearest an Edge node can never instantiate its tail cohort: the routed primary returns
486
+ * `no_state`, the walk exhausts, and `register` throws `CohortBackoffError`. Because {@link makeMembers}
487
+ * re-randomizes the member ring layout every run, an Edge-profile mesh would otherwise *intermittently*
488
+ * seat the Edge node as a collection's tail primary (~1/nodeCount of runs) and flake. In production a
489
+ * reactivity tail cohort IS the Core cluster that served the tail, so pinning the primary to a Core node
490
+ * models reality. A no-op (returns `utf8(baseTail)` at the first try) when the mesh has no Edge nodes —
491
+ * the common case — so every all-Core suite is byte-for-byte unaffected.
492
+ */
493
+ private pinTailToCore(baseTail: string): Uint8Array {
494
+ for (let nonce = 0; ; nonce++) {
495
+ const tailId = utf8.encode(nonce === 0 ? baseTail : `${baseTail}#${nonce}`);
496
+ if (!this.nearestIsEdge(addressing.coord0(reactivityTopicId(tailId)))) {
497
+ return tailId;
498
+ }
499
+ }
500
+ }
501
+
502
+ private collection(name: string): CollectionState {
503
+ const c = this.collections.get(name);
504
+ if (c === undefined) {
505
+ throw new Error(`reactivity mesh: collection "${name}" is not registered (call registerCollection first)`);
506
+ }
507
+ return c;
508
+ }
509
+
510
+ /**
511
+ * Cache the tail cohort's `MembershipCertV1` (over the FRET cohort around the current tail's coord_0) into
512
+ * every node's participant verifier, so a subscriber's notification verify is real Ed25519 against the
513
+ * tail membership — mirroring `reactivity-real-crypto.spec.ts`, now over the mesh's real cohort members.
514
+ */
515
+ private cacheTailCert(c: CollectionState): void {
516
+ const members = c.tailCohort.map((m) => bytesToB64url(m.bytes));
517
+ const cert: MembershipCertV1 = {
518
+ v: 1,
519
+ cohortCoord: bytesToB64url(c.coord0),
520
+ cohortEpoch: bytesToB64url(utf8.encode(`${c.name}:${bytesToB64url(c.tailId)}`)),
521
+ members,
522
+ stabilizedAt: this.vtime,
523
+ thresholdSig: bytesToB64url(new Uint8Array([0])),
524
+ signers: members,
525
+ };
526
+ for (const node of this.mesh.nodes) {
527
+ node.host.service.verifier().cache(cert);
528
+ }
529
+ }
530
+
531
+ /** The tail cohort member ids (base64url member bytes) serving a collection's current tail. */
532
+ tailCohortIds(name: string): readonly string[] {
533
+ return this.collection(name).tailCohort.map((m) => bytesToB64url(m.bytes));
534
+ }
535
+
536
+ /** This collection's tier-0 cohort registration records carrying a reactivity subscribe payload. */
537
+ cohortSubscriberCount(name: string): number {
538
+ const c = this.collection(name);
539
+ return c.setup.decidingEngine.records(c.topicId).filter((r) => r.appState !== undefined).length;
540
+ }
541
+
542
+ /** The current committed revision for a collection. */
543
+ currentRevision(name: string): number {
544
+ return this.collection(name).rev;
545
+ }
546
+
547
+ /**
548
+ * Drive the deciding cohort's membership stabilization (publish its `MembershipCertV1`). Promotion is
549
+ * fire-and-forget off the register path; stabilizing publishes the cohort cert so a crossed-`cap_promote`
550
+ * decision lands deterministically (no wall-clock wait) — the same pre-step the cohort-topic scale specs use.
551
+ */
552
+ async stabilizeCohort(name: string): Promise<void> {
553
+ await this.collection(name).setup.decidingEngine.onStabilized(this.vtime);
554
+ }
555
+
556
+ /** Whether a collection's tier-0 cohort has promoted (its direct participants crossed `cap_promote`). */
557
+ isPromoted(name: string): boolean {
558
+ const c = this.collection(name);
559
+ return c.setup.decidingEngine.isPromoted(c.topicId);
560
+ }
561
+
562
+ /** Whether a node profile may serve as a reactivity forwarder (T3 producer) — Edge never can. */
563
+ mayForward(nodeIndex: number): boolean {
564
+ return mayServeAsReactivityForwarder(this.profileOf(nodeIndex));
565
+ }
566
+
567
+ /**
568
+ * Register node `nodeIndex` as a reactivity subscriber for `collection` via the **real**
569
+ * {@link ReactivitySubscriptionManager} → `CohortTopicService.register` walk at tier T3. The subscribe
570
+ * record lands in the real tier-0 cohort store; the manager owns the real verify/deliver/backfill/resume
571
+ * path. Returns a handle whose `delivered` array accumulates the surfaced notifications.
572
+ */
573
+ async subscribe(nodeIndex: number, collection: string, opts: SubscribeOptions = {}): Promise<SubscriptionHandle> {
574
+ const c = this.collection(collection);
575
+ const member = this.members[nodeIndex]!;
576
+ const service = this.mesh.nodes[nodeIndex]!.host.service;
577
+ const profile = opts.profile ?? this.profileOf(nodeIndex);
578
+ const subId = bytesToB64url(member.bytes);
579
+
580
+ const handle: SubscriptionHandle = {
581
+ nodeIndex,
582
+ member,
583
+ subId,
584
+ manager: undefined as unknown as ReactivitySubscriptionManager,
585
+ delivered: [],
586
+ attachedTailB64: bytesToB64url(c.tailId),
587
+ collectionName: collection,
588
+ autoDrain: opts.autoDrain ?? true,
589
+ asleep: false,
590
+ backfills: 0,
591
+ chainRead: false,
592
+ checkpointDigests: [],
593
+ rotationNotices: [],
594
+ scheduler: undefined as unknown as RotationReRegistrationScheduler,
595
+ };
596
+
597
+ // The host re-registration scheduler bound to this subscriber, driven over the harness virtual clock. On
598
+ // fire it re-attaches the subscriber under the rotated tail — the production move the deferred Quereus
599
+ // `Database.watch` factory performs (`reactivity-rotation-host-wiring-e2e` §C). The single-subscriber
600
+ // `planReRegistration` path draws `fireAt` over `T_rejoin_jitter`, so a `advanceTime` past that window
601
+ // fires it deterministically.
602
+ const scheduler = new RotationReRegistrationScheduler({
603
+ reRegister: (plan): Promise<void> => { this.reAttachToNewTail(handle, plan); return Promise.resolve(); },
604
+ setTimer: (fn, delayMs): RotationTimerCancel => this.armVirtualTimer(fn, delayMs),
605
+ now: (): number => this.vtime,
606
+ });
607
+ handle.scheduler = scheduler;
608
+
609
+ const manager = new ReactivitySubscriptionManager({
610
+ service,
611
+ collectionId: c.collectionId,
612
+ tailIdAtAttach: c.tailId,
613
+ deliver: (n): void => { handle.delivered.push(n); },
614
+ profile,
615
+ lastKnownRev: opts.lastKnownRev ?? 0,
616
+ clock: (): number => this.vtime,
617
+ // Backfill RPC served from the tail cohort's live replay buffer (real serveBackfill).
618
+ signBackfill: (req): string => bytesToB64url(utf8.encode(`bf:${req.fromRevision}:${req.toRevision}`)),
619
+ backfillTransport: (req) => { handle.backfills += 1; return Promise.resolve(serveBackfill(c.pushState.replayBuffer, req, c.collectionIdB64)); },
620
+ // Resume RPC served from the tail cohort's stacked windows (real serveResume).
621
+ signResume: (): string => bytesToB64url(utf8.encode("resume")),
622
+ subscriberCoord: subId,
623
+ resumeTransport: (req: ResumeV1): Promise<ResumeReplyV1> => {
624
+ // Model the live recover serve's drain redirect (the running node's `markRotated` →
625
+ // `rotationRedirectFor` → `RotationRedirectError`): a resume whose `latestKnownTailId` anchors a
626
+ // rotated, still-draining tail is bounced to the new tree instead of served stale data.
627
+ const gate = c.rotationGate;
628
+ if (gate !== undefined && req.latestKnownTailId === c.rotatedFromTailB64 && gate.isDraining(this.vtime)) {
629
+ return Promise.reject(new RotationRedirectError(gate.rotationRedirect));
630
+ }
631
+ return Promise.resolve(serveResume(req, {
632
+ buffer: c.pushState.replayBuffer,
633
+ checkpoint: c.pushState.checkpoint,
634
+ // The cross-rotation inherited window the new tail holds after a handoff: a resume below the new
635
+ // ring but within it is served `checkpoint_window` (not `out_of_window`) while it abuts the ring.
636
+ inheritedCheckpoint: c.pushState.inheritedCheckpoint,
637
+ currentTailId: bytesToB64url(c.tailId),
638
+ currentRevision: c.rev,
639
+ rotationRevision: c.rev,
640
+ expectedCollectionId: c.collectionIdB64,
641
+ }));
642
+ },
643
+ onChainRead: (): void => { handle.chainRead = true; },
644
+ onTailRotated: (newTailId, rev): void => { handle.tailRotated = [newTailId, rev]; },
645
+ onCheckpointDigest: (summary): void => { handle.checkpointDigests.push(summary); },
646
+ // Surface the jittered re-registration plan from a delivered rotation pre-announce / hard rotation.
647
+ rejoinJitter: createRejoinJitter({ capPromote: DEFAULT_CAP_PROMOTE_FAST, random: this.deterministicRandom() }),
648
+ onRotation: (notice): void => { handle.rotationNotices.push(notice); scheduler.schedule(notice); },
649
+ });
650
+ handle.manager = manager;
651
+
652
+ handle.registration = await manager.register();
653
+ c.subscribers.push(handle);
654
+ return handle;
655
+ }
656
+
657
+ /**
658
+ * Commit `count` transactions to `collection` through the real `StorageRepo` → change-bridge →
659
+ * origination path, then fan the originated notifications out to current-tail subscribers. Returns the
660
+ * new current revision. Each commit pre-signs a **real** threshold {@link CommitCert} with the tail
661
+ * cohort's Ed25519 keys (the bridge's synchronous extractor resolves it by `actionId`).
662
+ */
663
+ async commit(collection: string, count = 1): Promise<number> {
664
+ const c = this.collection(collection);
665
+ for (let i = 0; i < count; i++) {
666
+ const rev = c.rev + 1;
667
+ const actionId = `rx-${c.collectionIdB64}-${rev}-${this.corr++}`;
668
+ const blockId = `${c.name}-blk-${c.blockSeq++}`;
669
+ // Anticipatory warm-up / block-filling: the filling commit carries the rotation pre-announce.
670
+ const signal = c.fillTracker.onCommit();
671
+ if (signal.kind === "filling") {
672
+ const nextTail = utf8.encode(`${c.name}:tail-${rev}`);
673
+ c.rotationHint = buildRotationHint(bytesToB64url(nextTail), rev);
674
+ }
675
+ const cert = await this.buildTailCert(c, rev);
676
+ c.certByAction.set(actionId, cert);
677
+ await c.repo.pend({ actionId: actionId as ActionId, transforms: this.insertTransforms(blockId, c.collectionIdB64), policy: "c" });
678
+ const result = await c.repo.commit({ actionId: actionId as ActionId, blockIds: [blockId as BlockId], tailId: blockId as BlockId, rev });
679
+ if (!result.success) {
680
+ throw new Error(`reactivity mesh: commit failed for ${collection} rev ${rev}`);
681
+ }
682
+ c.rev = rev;
683
+ // The pre-announce is one-shot: it rode the filling notification; clear it for subsequent commits.
684
+ c.rotationHint = undefined;
685
+ }
686
+ await this.drainFanOut(c);
687
+ return c.rev;
688
+ }
689
+
690
+ /** Build a real threshold commit cert: every tail-cohort member signs `utf8(commitHash + ":approve")`. */
691
+ private async buildTailCert(c: CollectionState, rev: number): Promise<CommitCert> {
692
+ const commitHash = `${c.collectionIdB64}:${rev}`;
693
+ const signedPayload = utf8.encode(`${commitHash}:approve`);
694
+ const commits: Record<string, Signature> = {};
695
+ for (const m of c.tailCohort) {
696
+ const sig = await (m.key as PrivateKey).sign(signedPayload);
697
+ commits[m.idStr] = { type: "approve", signature: bytesToB64url(sig) };
698
+ }
699
+ const record: ClusterRecord = {
700
+ messageHash: `mh-${commitHash}`,
701
+ message: { operations: [{ get: { blockIds: [] } }], expiration: this.vtime + 30_000 },
702
+ peers: this.clusterPeers(c.tailCohort),
703
+ promises: {},
704
+ commits,
705
+ };
706
+ return buildCommitCert(record, this.minSigs, signedPayload);
707
+ }
708
+
709
+ private clusterPeers(cohort: readonly Member[]): ClusterPeers {
710
+ const peers: ClusterPeers = {};
711
+ for (const m of cohort) {
712
+ peers[m.idStr] = { multiaddrs: ["/ip4/127.0.0.1/tcp/8000"], publicKey: bytesToB64url(m.peerId.publicKey!.raw) };
713
+ }
714
+ return peers;
715
+ }
716
+
717
+ private insertTransforms(blockId: string, collectionId: string): Transforms {
718
+ const block: IBlock = { header: { id: blockId as BlockId, type: "test", collectionId: collectionId as BlockId } };
719
+ return { inserts: { [blockId]: block }, updates: {}, deletes: [] };
720
+ }
721
+
722
+ /**
723
+ * Run the originated notifications through the real forwarder receive path (populating the tail replay
724
+ * ring + rolling checkpoint + dedupe), then fan each out to every current-tail subscriber via its real
725
+ * per-subscriber bounded queue. A subscriber with `autoDrain` drains immediately; a slow subscriber's
726
+ * queue accumulates (drop-oldest under pressure) until it is woken.
727
+ */
728
+ private async drainFanOut(c: CollectionState): Promise<void> {
729
+ const notifs = c.emitQueue.splice(0, c.emitQueue.length);
730
+ const tailB64 = bytesToB64url(c.tailId);
731
+ for (const n of notifs) {
732
+ // Forwarder receive: verify (real) → dedupe → append to the replay ring (and roll the checkpoint).
733
+ await c.forwarder.receive(n, this.vtime);
734
+ for (const s of c.subscribers) {
735
+ if (s.attachedTailB64 !== tailB64 || s.asleep) {
736
+ continue;
737
+ }
738
+ c.pushState.perSubscriberQueue.enqueue(s.subId, n);
739
+ if (s.autoDrain) {
740
+ await this.drainSubscriber(s);
741
+ }
742
+ }
743
+ }
744
+ }
745
+
746
+ /** Drain a subscriber's bounded queue (FIFO) through the real manager delivery path. */
747
+ private async drainSubscriber(s: SubscriptionHandle): Promise<void> {
748
+ const c = this.collection(s.collectionName);
749
+ const queue = c.pushState.perSubscriberQueue.peekQueue(s.subId);
750
+ if (queue === undefined) {
751
+ return;
752
+ }
753
+ for (const n of queue.drain()) {
754
+ await s.manager.onNotification(n);
755
+ }
756
+ }
757
+
758
+ /** Wake a slow subscriber: drain its accumulated (drop-oldest) queue through the delivery path. */
759
+ async wakeSubscriber(s: SubscriptionHandle): Promise<void> {
760
+ await this.drainSubscriber(s);
761
+ }
762
+
763
+ /** Put a subscriber to sleep: fan-out skips it (its tail replay buffer still fills) until it resumes. */
764
+ sleepSubscriber(s: SubscriptionHandle): void {
765
+ s.asleep = true;
766
+ }
767
+
768
+ /** The drop counter on a subscriber's bounded queue (drop-oldest under backpressure). */
769
+ droppedFor(s: SubscriptionHandle): number {
770
+ const c = this.collection(s.collectionName);
771
+ return c.pushState.perSubscriberQueue.peekQueue(s.subId)?.dropped ?? 0;
772
+ }
773
+
774
+ /** Drive the manager's real `resume()` (one `ResumeV1` → `serveResume` → `applyResumeReply`). */
775
+ async resume(s: SubscriptionHandle): Promise<ResumeApplyOutcome> {
776
+ s.asleep = false;
777
+ return s.manager.resume();
778
+ }
779
+
780
+ /**
781
+ * The re-registration move the scheduler fires on a rotation notice: re-attach the subscriber under the
782
+ * rotated tail so subsequent fan-out targets it. The production factory builds a *fresh* manager under
783
+ * `plan.newTopicId`; the harness keeps the same manager (its `lastRevision` is already contiguous and its
784
+ * `rotationHandledFor` guard dedupes the new-tail deliveries' re-detection), so re-attaching the existing
785
+ * subscriber is the equivalent move with no gap.
786
+ */
787
+ private reAttachToNewTail(s: SubscriptionHandle, plan: ReRegistrationPlan): void {
788
+ const c = this.collection(s.collectionName);
789
+ s.attachedTailB64 = bytesToB64url(plan.newTailId);
790
+ // Defensive: the plan's successor must be the collection's current tail (single-rotation harness model).
791
+ if (s.attachedTailB64 !== bytesToB64url(c.tailId)) {
792
+ s.attachedTailB64 = bytesToB64url(c.tailId);
793
+ }
794
+ }
795
+
796
+ /**
797
+ * Rotate a collection's tail: advance `tailId` → new `topicId`/coord/cohort, re-establish the new cohort
798
+ * (re-seed willingness so a post-rotation subscribe can register), re-cache the tail cert, rebuild the tail
799
+ * `PushState`, fold the outgoing ring into a handoff checkpoint onto the new tail, and plan the jittered
800
+ * re-registration wave bounded by `cap_promote_fast`. Async because re-forming the new cohort is (the real
801
+ * willingness convergence). With `{ autoReattach: false }` it instead models the running node's
802
+ * `markRotated` (drain gate) so subscribers move via the recover-redirect path. Models §Tail rotation steps 2–5.
803
+ */
804
+ async rotateTail(collection: string, opts: RotateTailOptions = {}): Promise<RotationResult> {
805
+ const c = this.collection(collection);
806
+ const autoReattach = opts.autoReattach ?? true;
807
+ const rotationRevision = c.rev;
808
+ // Pin the rotated tail's routed primary to a Core node too (Edge nodes decline a T3 cold-start); a
809
+ // no-op for the all-Core rotation suites, matching the initial-tail pinning in `registerCollection`.
810
+ const newTailId = this.pinTailToCore(`${c.name}:tail-${rotationRevision}`);
811
+ const newTailIdB64 = bytesToB64url(newTailId);
812
+ const oldTailB64 = bytesToB64url(c.tailId);
813
+
814
+ // Buffer-to-checkpoint handoff — the ONLY state migrated across a rotation (§Tail rotation step 5).
815
+ const handoff = buildRotationHandoffCheckpoint(c.pushState, { rotationRevision });
816
+
817
+ // Active subscribers re-register under the new tree, carrying their lastRevision (continuity).
818
+ const live = c.subscribers.filter((s) => s.attachedTailB64 === oldTailB64);
819
+ const jitter = createRejoinJitter({ capPromote: DEFAULT_CAP_PROMOTE_FAST, random: this.deterministicRandom() });
820
+ const plans = planReRegistrationWave({
821
+ hint: { newTailId: newTailIdB64 },
822
+ subscribers: live.map((s) => ({ lastRevision: s.manager.lastRevision })),
823
+ now: this.vtime,
824
+ jitter,
825
+ });
826
+
827
+ // Roll the topic to the new tail.
828
+ c.tailId = newTailId;
829
+ c.topicId = reactivityTopicId(newTailId);
830
+ c.coord0 = addressing.coord0(c.topicId);
831
+ c.tailCohort = this.cohortMembersAround(c.coord0);
832
+ // Re-establish the new tail's cohort (re-seed willingness) so a subscriber can register under it after the
833
+ // rotation — the new tree forming, modeled deterministically (the real cohort-topic willingness convergence).
834
+ c.setup = await setupTopic(this.mesh, c.topicId);
835
+ this.cacheTailCert(c);
836
+ c.pushState = this.makePushState(c.collectionIdB64, c.topicId, c.tailId, c.w, c.wCheckpoint, c.queueMax, c.deltaMaxBytes);
837
+ c.forwarder = createReactivityForwarder({ state: c.pushState, verifier: this.notificationVerifierFor(c.originationService) });
838
+ if (handoff !== undefined) {
839
+ applyRotationHandoff(c.pushState, handoff);
840
+ }
841
+
842
+ if (autoReattach) {
843
+ // Re-attach the live subscribers under the new tail so subsequent fan-out targets them (the
844
+ // continuity/handoff model — subscribers migrate directly, no redirect involved).
845
+ for (const s of live) {
846
+ s.attachedTailB64 = newTailIdB64;
847
+ }
848
+ } else {
849
+ // Transport-driven: model `markRotated` by arming the outgoing tail's drain gate. Live subscribers
850
+ // stay on the old tail until each resumes, gets the `kind:"rotated"` redirect, and re-attaches via its
851
+ // own onRotation → scheduler → reRegister path (§Tail rotation step 2–3).
852
+ c.rotationGate = new TailDrainGate({ rotatedAt: this.vtime, newTailId: newTailIdB64, effectiveAtRevision: rotationRevision + 1 });
853
+ c.rotatedFromTailB64 = oldTailB64;
854
+ }
855
+
856
+ return {
857
+ newTailId,
858
+ newTailIdB64,
859
+ rotationRevision,
860
+ plans,
861
+ peakWindowArrivals: this.peakWindowArrivals(plans),
862
+ ...(handoff?.checkpoint === undefined ? {} : { handoff: handoff.checkpoint }),
863
+ };
864
+ }
865
+
866
+ /** Peak re-registration arrivals in any `T_rejoin_jitter`-long window of a planned wave. */
867
+ private peakWindowArrivals(plans: readonly ReRegistrationPlan[]): number {
868
+ if (plans.length === 0) {
869
+ return 0;
870
+ }
871
+ const fireAts = plans.map((p) => p.fireAt).sort((a, b) => a - b);
872
+ const window = T_REJOIN_JITTER_MS; // the same jitter span `createRejoinJitter` spreads the wave over
873
+ let peak = 0;
874
+ for (let i = 0; i < fireAts.length; i++) {
875
+ let j = i;
876
+ while (j < fireAts.length && fireAts[j]! - fireAts[i]! < window) {
877
+ j++;
878
+ }
879
+ peak = Math.max(peak, j - i);
880
+ }
881
+ return peak;
882
+ }
883
+
884
+ /** A deterministic pseudo-random in [0,1) so the jitter wave is reproducible across runs. */
885
+ private deterministicRandom(): () => number {
886
+ let seed = 0x2545f491;
887
+ return (): number => {
888
+ seed = (seed * 1103515245 + 12345) & 0x7fffffff;
889
+ return seed / 0x7fffffff;
890
+ };
891
+ }
892
+
893
+ /**
894
+ * The tail cohort `PushState` for a collection (its live replay ring / checkpoint / backpressure). Exposed
895
+ * so a suite can assert the replay/dedupe/partition state directly.
896
+ */
897
+ pushStateOf(collection: string): PushState {
898
+ return this.collection(collection).pushState;
899
+ }
900
+
901
+ /** A real forwarder receive over the tail `PushState` — used to model a forwarder-hop / partition replay. */
902
+ async forwarderReceive(collection: string, n: NotificationV1): Promise<ForwardDecision> {
903
+ const c = this.collection(collection);
904
+ return c.forwarder.receive(n, this.vtime);
905
+ }
906
+
907
+ async stop(): Promise<void> {
908
+ // Tear down every subscriber's re-registration scheduler (drops pending virtual timers + the idempotence
909
+ // ledger) before stopping the mesh.
910
+ for (const c of this.collections.values()) {
911
+ for (const s of c.subscribers) {
912
+ s.scheduler?.stop();
913
+ }
914
+ }
915
+ await this.mesh.stop();
916
+ }
917
+ }
918
+
919
+ /** Build and start a reactivity integration mesh. */
920
+ export async function buildReactivityMesh(opts: ReactivityMeshOptions = {}): Promise<ReactivityMesh> {
921
+ return ReactivityMesh.build(opts);
922
+ }