@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.
- package/README.md +6 -6
- package/dist/src/cluster/block-transfer-service.d.ts +67 -8
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +101 -62
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/client.d.ts +18 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +55 -39
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-error.d.ts +38 -0
- package/dist/src/cluster/cluster-error.d.ts.map +1 -0
- package/dist/src/cluster/cluster-error.js +51 -0
- package/dist/src/cluster/cluster-error.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +135 -3
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +333 -58
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-cert.d.ts +63 -0
- package/dist/src/cluster/commit-cert.d.ts.map +1 -0
- package/dist/src/cluster/commit-cert.js +108 -0
- package/dist/src/cluster/commit-cert.js.map +1 -0
- package/dist/src/cluster/service.d.ts +46 -11
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +97 -33
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +13 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -4
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
- package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
- package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
- package/dist/src/cohort-topic/change-bridge.js +76 -0
- package/dist/src/cohort-topic/change-bridge.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
- package/dist/src/cohort-topic/host.d.ts +573 -0
- package/dist/src/cohort-topic/host.d.ts.map +1 -0
- package/dist/src/cohort-topic/host.js +1460 -0
- package/dist/src/cohort-topic/host.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +14 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +14 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
- package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
- package/dist/src/cohort-topic/membership-source.d.ts +36 -0
- package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-source.js +58 -0
- package/dist/src/cohort-topic/membership-source.js.map +1 -0
- package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-codec.js +26 -0
- package/dist/src/cohort-topic/peer-codec.js.map +1 -0
- package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
- package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-sig.js +83 -0
- package/dist/src/cohort-topic/peer-sig.js.map +1 -0
- package/dist/src/cohort-topic/protocols.d.ts +49 -0
- package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
- package/dist/src/cohort-topic/protocols.js +58 -0
- package/dist/src/cohort-topic/protocols.js.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
- package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
- package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
- package/dist/src/cohort-topic/size-estimator.js +16 -0
- package/dist/src/cohort-topic/size-estimator.js.map +1 -0
- package/dist/src/cohort-topic/stream-util.d.ts +19 -0
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
- package/dist/src/cohort-topic/stream-util.js +76 -0
- package/dist/src/cohort-topic/stream-util.js.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.js +178 -0
- package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
- package/dist/src/cohort-topic/topic-router.d.ts +41 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
- package/dist/src/cohort-topic/topic-router.js +63 -0
- package/dist/src/cohort-topic/topic-router.js.map +1 -0
- package/dist/src/dispute/cascade.d.ts +179 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -0
- package/dist/src/dispute/cascade.js +315 -0
- package/dist/src/dispute/cascade.js.map +1 -0
- package/dist/src/dispute/client.d.ts +2 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +12 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +41 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +147 -21
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +2 -0
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +2 -0
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +266 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -0
- package/dist/src/dispute/invalidation.js +405 -0
- package/dist/src/dispute/invalidation.js.map +1 -0
- package/dist/src/dispute/types.d.ts +14 -0
- package/dist/src/dispute/types.d.ts.map +1 -1
- package/dist/src/dispute/types.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +8 -1
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +67 -8
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +57 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +523 -6
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
- package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
- package/dist/src/matchmaking/aggregate-counts.js +60 -0
- package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +20 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +20 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/module.d.ts +181 -0
- package/dist/src/matchmaking/module.d.ts.map +1 -0
- package/dist/src/matchmaking/module.js +221 -0
- package/dist/src/matchmaking/module.js.map +1 -0
- package/dist/src/matchmaking/protocols.d.ts +37 -0
- package/dist/src/matchmaking/protocols.d.ts.map +1 -0
- package/dist/src/matchmaking/protocols.js +42 -0
- package/dist/src/matchmaking/protocols.js.map +1 -0
- package/dist/src/matchmaking/provider-manager.d.ts +52 -0
- package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/provider-manager.js +74 -0
- package/dist/src/matchmaking/provider-manager.js.map +1 -0
- package/dist/src/matchmaking/query-handler.d.ts +34 -0
- package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
- package/dist/src/matchmaking/query-handler.js +62 -0
- package/dist/src/matchmaking/query-handler.js.map +1 -0
- package/dist/src/matchmaking/query-transport.d.ts +180 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
- package/dist/src/matchmaking/query-transport.js +315 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -0
- package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
- package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-manager.js +49 -0
- package/dist/src/matchmaking/seeker-manager.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.js +189 -0
- package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
- package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
- package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
- package/dist/src/matchmaking/traffic-validation.js +105 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -0
- package/dist/src/network/network-manager-service.d.ts +4 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +9 -3
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/protocol-client.d.ts +25 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +126 -4
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +198 -0
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder-host.js +355 -0
- package/dist/src/reactivity/forwarder-host.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +19 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +19 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +70 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
- package/dist/src/reactivity/notify-transport.js +111 -0
- package/dist/src/reactivity/notify-transport.js.map +1 -0
- package/dist/src/reactivity/origination-manager.d.ts +110 -0
- package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
- package/dist/src/reactivity/origination-manager.js +129 -0
- package/dist/src/reactivity/origination-manager.js.map +1 -0
- package/dist/src/reactivity/protocols.d.ts +43 -0
- package/dist/src/reactivity/protocols.d.ts.map +1 -0
- package/dist/src/reactivity/protocols.js +50 -0
- package/dist/src/reactivity/protocols.js.map +1 -0
- package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
- package/dist/src/reactivity/push-state-gossip.js +231 -0
- package/dist/src/reactivity/push-state-gossip.js.map +1 -0
- package/dist/src/reactivity/recover-transport.d.ts +167 -0
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
- package/dist/src/reactivity/recover-transport.js +308 -0
- package/dist/src/reactivity/recover-transport.js.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
- package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
- package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber-registry.js +87 -0
- package/dist/src/reactivity/subscriber-registry.js.map +1 -0
- package/dist/src/reactivity/subscription-manager.d.ts +221 -0
- package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
- package/dist/src/reactivity/subscription-manager.js +302 -0
- package/dist/src/reactivity/subscription-manager.js.map +1 -0
- package/dist/src/reactivity/topic-bytes.d.ts +32 -0
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
- package/dist/src/reactivity/topic-bytes.js +34 -0
- package/dist/src/reactivity/topic-bytes.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +51 -19
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/cluster-coordinator.d.ts +9 -0
- package/dist/src/repo/cluster-coordinator.d.ts.map +1 -1
- package/dist/src/repo/cluster-coordinator.js +65 -22
- package/dist/src/repo/cluster-coordinator.js.map +1 -1
- package/dist/src/repo/coordinator-repo.d.ts +20 -0
- package/dist/src/repo/coordinator-repo.d.ts.map +1 -1
- package/dist/src/repo/coordinator-repo.js +101 -16
- package/dist/src/repo/coordinator-repo.js.map +1 -1
- package/dist/src/repo/service.d.ts +46 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +79 -45
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/rpc-deadline.d.ts +37 -0
- package/dist/src/rpc-deadline.d.ts.map +1 -0
- package/dist/src/rpc-deadline.js +31 -0
- package/dist/src/rpc-deadline.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +2 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +95 -2
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +25 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +53 -2
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +193 -9
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts +4 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -2
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
- package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +23 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
- package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/reactivity-mesh-harness.js +663 -0
- package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
- package/package.json +7 -4
- package/src/cluster/block-transfer-service.ts +133 -68
- package/src/cluster/client.ts +64 -45
- package/src/cluster/cluster-error.ts +64 -0
- package/src/cluster/cluster-repo.ts +405 -55
- package/src/cluster/commit-cert.ts +139 -0
- package/src/cluster/service.ts +116 -44
- package/src/cluster/spread-on-churn.ts +328 -285
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
- package/src/cohort-topic/change-bridge.ts +109 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
- package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
- package/src/cohort-topic/fret-trust-anchor.ts +153 -0
- package/src/cohort-topic/host.ts +2175 -0
- package/src/cohort-topic/index.ts +13 -0
- package/src/cohort-topic/membership-publish-sink.ts +20 -0
- package/src/cohort-topic/membership-source.ts +68 -0
- package/src/cohort-topic/peer-codec.ts +31 -0
- package/src/cohort-topic/peer-sig.ts +86 -0
- package/src/cohort-topic/protocols.ts +71 -0
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
- package/src/cohort-topic/size-estimator.ts +16 -0
- package/src/cohort-topic/stream-util.ts +87 -0
- package/src/cohort-topic/threshold-crypto.ts +239 -0
- package/src/cohort-topic/topic-router.ts +77 -0
- package/src/dispute/cascade.ts +517 -0
- package/src/dispute/client.ts +12 -2
- package/src/dispute/dispute-service.ts +171 -24
- package/src/dispute/index.ts +42 -0
- package/src/dispute/invalidation.ts +587 -0
- package/src/dispute/types.ts +14 -0
- package/src/index.ts +4 -0
- package/src/libp2p-key-network.ts +67 -9
- package/src/libp2p-node-base.ts +633 -14
- package/src/matchmaking/aggregate-counts.ts +104 -0
- package/src/matchmaking/index.ts +20 -0
- package/src/matchmaking/module.ts +363 -0
- package/src/matchmaking/protocols.ts +51 -0
- package/src/matchmaking/provider-manager.ts +95 -0
- package/src/matchmaking/query-handler.ts +88 -0
- package/src/matchmaking/query-transport.ts +483 -0
- package/src/matchmaking/seeker-manager.ts +64 -0
- package/src/matchmaking/seeker-walk-client.ts +293 -0
- package/src/matchmaking/traffic-validation.ts +195 -0
- package/src/network/network-manager-service.ts +9 -2
- package/src/protocol-client.ts +119 -5
- package/src/reactivity/forwarder-host.ts +438 -0
- package/src/reactivity/index.ts +19 -0
- package/src/reactivity/notify-transport.ts +144 -0
- package/src/reactivity/origination-manager.ts +192 -0
- package/src/reactivity/protocols.ts +61 -0
- package/src/reactivity/push-state-gossip.ts +291 -0
- package/src/reactivity/recover-transport.ts +408 -0
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
- package/src/reactivity/subscriber-registry.ts +96 -0
- package/src/reactivity/subscription-manager.ts +450 -0
- package/src/reactivity/topic-bytes.ts +37 -0
- package/src/repo/client.ts +54 -19
- package/src/repo/cluster-coordinator.ts +66 -26
- package/src/repo/coordinator-repo.ts +99 -15
- package/src/repo/service.ts +90 -42
- package/src/rpc-deadline.ts +45 -0
- package/src/storage/block-storage.ts +106 -3
- package/src/storage/i-block-storage.ts +27 -0
- package/src/storage/storage-repo.ts +209 -13
- package/src/sync/client.ts +5 -2
- package/src/testing/cohort-topic-mesh-harness.ts +613 -0
- package/src/testing/matchmaking-mesh-harness.ts +475 -0
- package/src/testing/mesh-harness.ts +22 -2
- 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
|
+
}
|