@optimystic/db-p2p 0.13.5 → 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 +7 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +49 -5
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +45 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +522 -5
- 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 +13 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +74 -2
- 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 -23
- package/dist/src/repo/client.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 +49 -6
- package/src/libp2p-node-base.ts +620 -12
- 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 +64 -3
- 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 -23
- 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,663 @@
|
|
|
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
|
+
import { Tier, reactivityTopicId, createReactivityForwarder, createNotificationVerifier, PushState, serveBackfill, serveResume, buildRotationHint, BlockFillTracker, TailDrainGate, planReRegistrationWave, buildRotationHandoffCheckpoint, applyRotationHandoff, createRejoinJitter, mayServeAsReactivityForwarder, deltaMaxForProfile, coreProfile, edgeProfile, bytesToB64url, DEFAULT_CAP_PROMOTE_FAST, T_REJOIN_JITTER_MS, WARM_THRESHOLD_DEFAULT, } from "@optimystic/db-core";
|
|
41
|
+
import { StorageRepo } from "../storage/storage-repo.js";
|
|
42
|
+
import { BlockStorage } from "../storage/block-storage.js";
|
|
43
|
+
import { MemoryRawStorage } from "../storage/memory-storage.js";
|
|
44
|
+
import { makeCohortTopicChangeNotifier } from "../cohort-topic/change-bridge.js";
|
|
45
|
+
import { buildCommitCert } from "../cluster/commit-cert.js";
|
|
46
|
+
import { ReactivitySubscriptionManager } from "../reactivity/subscription-manager.js";
|
|
47
|
+
import { ReactivityOriginationManager } from "../reactivity/origination-manager.js";
|
|
48
|
+
import { RotationReRegistrationScheduler } from "../reactivity/rotation-rereg-scheduler.js";
|
|
49
|
+
import { RotationRedirectError } from "../reactivity/recover-transport.js";
|
|
50
|
+
import { addressing, buildMesh, delay, makeMembers, setupTopic, } from "./cohort-topic-mesh-harness.js";
|
|
51
|
+
export { addressing, delay };
|
|
52
|
+
const utf8 = new TextEncoder();
|
|
53
|
+
/**
|
|
54
|
+
* The reactivity integration mesh. Build with {@link buildReactivityMesh}; register a collection with
|
|
55
|
+
* {@link registerCollection}; drive subscribers/commits/recovery with {@link subscribe} / {@link commit} /
|
|
56
|
+
* {@link resume} / {@link rotateTail}.
|
|
57
|
+
*/
|
|
58
|
+
export class ReactivityMesh {
|
|
59
|
+
mesh;
|
|
60
|
+
members;
|
|
61
|
+
wantK;
|
|
62
|
+
minSigs;
|
|
63
|
+
profiles;
|
|
64
|
+
collections = new Map();
|
|
65
|
+
vtime = 1_700_000_000_000;
|
|
66
|
+
corr = 0;
|
|
67
|
+
/** Pending one-shot timers armed against the virtual clock (the rotation scheduler's `setTimer` binding). */
|
|
68
|
+
virtualTimers = [];
|
|
69
|
+
constructor(mesh, members, wantK, minSigs, profiles) {
|
|
70
|
+
this.mesh = mesh;
|
|
71
|
+
this.members = members;
|
|
72
|
+
this.wantK = wantK;
|
|
73
|
+
this.minSigs = minSigs;
|
|
74
|
+
this.profiles = profiles;
|
|
75
|
+
}
|
|
76
|
+
/** Stand up an N-node reactivity mesh and start every host. */
|
|
77
|
+
static async build(opts = {}) {
|
|
78
|
+
const nodeCount = opts.nodeCount ?? 12;
|
|
79
|
+
const wantK = opts.wantK ?? Math.min(nodeCount, 8);
|
|
80
|
+
const minSigs = opts.minSigs ?? Math.max(1, wantK - 2);
|
|
81
|
+
const sizeEstimate = opts.sizeEstimate ?? 256;
|
|
82
|
+
const members = await makeMembers(nodeCount);
|
|
83
|
+
const meshOpts = {
|
|
84
|
+
wantK,
|
|
85
|
+
minSigs,
|
|
86
|
+
sizeEstimate,
|
|
87
|
+
// Single-tier-0 by default (same rationale as the matchmaking harness): test artifacts register
|
|
88
|
+
// many subscribers within a few ms, which would otherwise trip the pre-promotion slope predictor.
|
|
89
|
+
capPromote: opts.capPromote ?? 1_000_000,
|
|
90
|
+
// This mesh is virtual-time: a subscribe batch lands at one fixed virtual instant, but the cohort
|
|
91
|
+
// host stamps each register with real `Date.now()`. The growth-slope pre-promotion is a
|
|
92
|
+
// wall-clock-rate heuristic, so those real-time-separated samples make it fire *before* the
|
|
93
|
+
// `cap_promote` count is reached (redirecting the rest of the batch up a tier-1 tree the
|
|
94
|
+
// single-tier-0 milestone never instantiates). Disable slope here so promotion is driven purely by
|
|
95
|
+
// the count threshold — the same posture the cohort-topic scale specs get by stamping a constant
|
|
96
|
+
// `now` on every `handleRegister`. The slope heuristic itself is covered by the promotion unit specs.
|
|
97
|
+
promotion: { tPromoteLookaheadMs: 0 },
|
|
98
|
+
// Permissive register ceiling — the harness re-probes a cohort across many subscribes/commits.
|
|
99
|
+
// Also widen the replay-guard freshness window. The cohort host stamps each register with real
|
|
100
|
+
// `Date.now()` and evaluates the anti-DoS freshness gate against real `Date.now()` at processing
|
|
101
|
+
// time, but this mesh drives the real participant walk in-process: under full-suite CPU load the
|
|
102
|
+
// event loop can stall for far longer than the production 60 s staleness window between a register
|
|
103
|
+
// being stamped and the deciding cohort processing it. A stalled-but-legitimate register then
|
|
104
|
+
// trips the freshness gate (`no_state`), the cold-start bootstrap walk exhausts, and `register`
|
|
105
|
+
// throws `CohortBackoffError` — a pure load-induced harness flake. The wall-clock freshness
|
|
106
|
+
// defense is meaningless here (same posture as the disabled rate limiter above), so make the
|
|
107
|
+
// window effectively unbounded so test-induced stalls never read as stale/future replays.
|
|
108
|
+
// Disable the cold-start proof-of-work cost. Reactivity subscribers register at tier T3, so the
|
|
109
|
+
// participant-side bootstrap-evidence builder mints a PoW puzzle (default 20 bits ≈ 1 M hashes) on
|
|
110
|
+
// *every* register/re-probe — by far the dominant cost in this mesh (a 6-subscriber promotion test
|
|
111
|
+
// burns ~60-100 s almost entirely in `randomFillSync`/SHA-256 minting nonces). The cohort's
|
|
112
|
+
// bootstrap-evidence policy is unconfigured here, so the evidence is never even required; minting it
|
|
113
|
+
// is pure wasted work. `bits: 0` solves on the first nonce (the documented test escape hatch in
|
|
114
|
+
// `meetsDifficulty`) — same posture as the neutralized rate limiter / replay guard above, and it
|
|
115
|
+
// removes the timing-sensitivity that tips these tests into a timeout under full-suite load.
|
|
116
|
+
antiDos: { rateLimiter: { ratePerWindow: 1_000_000 }, replayGuard: { maxAgeMs: 86_400_000, maxFutureSkewMs: 86_400_000 }, powDifficultyBits: 0 },
|
|
117
|
+
...(opts.profiles === undefined ? {} : { profiles: opts.profiles }),
|
|
118
|
+
};
|
|
119
|
+
const mesh = await buildMesh(members, meshOpts);
|
|
120
|
+
const profiles = Array.from({ length: nodeCount }, (_v, i) => opts.profiles?.[i] ?? "core");
|
|
121
|
+
return new ReactivityMesh(mesh, members, wantK, minSigs, profiles);
|
|
122
|
+
}
|
|
123
|
+
/** The current virtual time (unix ms). */
|
|
124
|
+
get now() {
|
|
125
|
+
return this.vtime;
|
|
126
|
+
}
|
|
127
|
+
/**
|
|
128
|
+
* Advance the virtual clock by `ms` (drives `T_drain` / rotation timing deterministically), then fire any
|
|
129
|
+
* virtual timers now due. A fired timer may arm another (a chained rotation), so fire one-at-a-time until
|
|
130
|
+
* the queue is quiescent — the rotation re-registration scheduler's `setTimer` binding rides this.
|
|
131
|
+
*/
|
|
132
|
+
advanceTime(ms) {
|
|
133
|
+
this.vtime += ms;
|
|
134
|
+
for (;;) {
|
|
135
|
+
const idx = this.virtualTimers.findIndex((t) => !t.cancelled && t.fireAt <= this.vtime);
|
|
136
|
+
if (idx === -1) {
|
|
137
|
+
break;
|
|
138
|
+
}
|
|
139
|
+
const [timer] = this.virtualTimers.splice(idx, 1);
|
|
140
|
+
if (!timer.cancelled) {
|
|
141
|
+
timer.fn();
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
/** Arm a one-shot timer against the virtual clock; the cancel handle drops it (the scheduler's `setTimer`). */
|
|
146
|
+
armVirtualTimer(fn, delayMs) {
|
|
147
|
+
const timer = { fireAt: this.vtime + Math.max(0, delayMs), fn, cancelled: false };
|
|
148
|
+
this.virtualTimers.push(timer);
|
|
149
|
+
return () => { timer.cancelled = true; };
|
|
150
|
+
}
|
|
151
|
+
profileOf(index) {
|
|
152
|
+
return this.profiles[index] === "edge" ? edgeProfile() : coreProfile();
|
|
153
|
+
}
|
|
154
|
+
/**
|
|
155
|
+
* Register a collection on the mesh: instantiate its tail-anchored tier-0 cohort (+willingness quorum),
|
|
156
|
+
* install the real origination manager behind the real change-notifier bridge over a fresh `StorageRepo`,
|
|
157
|
+
* build the tail `PushState` (replay ring + rolling checkpoint + backpressure), and cache the tail
|
|
158
|
+
* cohort's `MembershipCertV1` into every node's verifier so end-to-end verification is real Ed25519.
|
|
159
|
+
*/
|
|
160
|
+
async registerCollection(name, opts = {}) {
|
|
161
|
+
const collectionId = utf8.encode(`reactivity:${name}`);
|
|
162
|
+
const collectionIdB64 = bytesToB64url(collectionId);
|
|
163
|
+
const tailId = this.pinTailToCore(`${name}:tail-0`);
|
|
164
|
+
const topicId = reactivityTopicId(tailId);
|
|
165
|
+
const coord0 = addressing.coord0(topicId);
|
|
166
|
+
const setup = await setupTopic(this.mesh, topicId);
|
|
167
|
+
const tailCohort = this.cohortMembersAround(coord0);
|
|
168
|
+
const deltaMaxBytes = opts.deltaMaxBytes ?? deltaMaxForProfile(coreProfile());
|
|
169
|
+
const w = opts.w ?? 256;
|
|
170
|
+
const wCheckpoint = opts.wCheckpoint ?? 4096;
|
|
171
|
+
const queueMax = opts.queueMax ?? 32;
|
|
172
|
+
const rawStorage = new MemoryRawStorage();
|
|
173
|
+
const repo = new StorageRepo((blockId) => new BlockStorage(blockId, rawStorage));
|
|
174
|
+
// Origination is the tail cohort primary's responsibility — installed on the node nearest coord_0
|
|
175
|
+
// (the routed tail primary), exactly the node a commit on the tail cluster lands on in production.
|
|
176
|
+
const originationService = setup.deciding.host.service;
|
|
177
|
+
const certByAction = new Map();
|
|
178
|
+
const emitQueue = [];
|
|
179
|
+
const pushState = this.makePushState(collectionIdB64, topicId, tailId, w, wCheckpoint, queueMax, deltaMaxBytes);
|
|
180
|
+
const state = {
|
|
181
|
+
name,
|
|
182
|
+
collectionId,
|
|
183
|
+
collectionIdB64,
|
|
184
|
+
tailId,
|
|
185
|
+
topicId,
|
|
186
|
+
coord0,
|
|
187
|
+
tailCohort,
|
|
188
|
+
setup,
|
|
189
|
+
repo,
|
|
190
|
+
originationService,
|
|
191
|
+
pushState,
|
|
192
|
+
forwarder: createReactivityForwarder({ state: pushState, verifier: this.notificationVerifierFor(originationService) }),
|
|
193
|
+
emitQueue,
|
|
194
|
+
rev: 0,
|
|
195
|
+
blockSeq: 0,
|
|
196
|
+
deltaMaxBytes,
|
|
197
|
+
w,
|
|
198
|
+
wCheckpoint,
|
|
199
|
+
queueMax,
|
|
200
|
+
fillTracker: new BlockFillTracker({
|
|
201
|
+
...(opts.blockFillSize === undefined ? {} : { blockFillSize: opts.blockFillSize }),
|
|
202
|
+
// warm_threshold must be < block_fill_size; clamp the default down for small (test-scaled) blocks.
|
|
203
|
+
warmThreshold: opts.warmThreshold ?? (opts.blockFillSize === undefined ? WARM_THRESHOLD_DEFAULT : Math.min(WARM_THRESHOLD_DEFAULT, Math.max(0, opts.blockFillSize - 1))),
|
|
204
|
+
}),
|
|
205
|
+
subscribers: [],
|
|
206
|
+
certByAction,
|
|
207
|
+
};
|
|
208
|
+
const origination = new ReactivityOriginationManager({
|
|
209
|
+
service: originationService,
|
|
210
|
+
resolveContext: () => ({
|
|
211
|
+
tailId: state.tailId,
|
|
212
|
+
deltaMaxBytes: state.deltaMaxBytes,
|
|
213
|
+
...(state.rotationHint === undefined ? {} : { rotationHint: state.rotationHint }),
|
|
214
|
+
}),
|
|
215
|
+
emit: (n) => { emitQueue.push(n); },
|
|
216
|
+
clock: () => this.vtime,
|
|
217
|
+
});
|
|
218
|
+
origination.install();
|
|
219
|
+
state.origination = origination;
|
|
220
|
+
// The real local-change-notifier bridge: a StorageRepo commit → onLocalCommit → origination.
|
|
221
|
+
makeCohortTopicChangeNotifier({
|
|
222
|
+
source: repo,
|
|
223
|
+
service: originationService,
|
|
224
|
+
selfIsCohortMember: (e) => e.collectionId === collectionIdB64,
|
|
225
|
+
extractCommitCert: (e) => certByAction.get(e.actionId),
|
|
226
|
+
});
|
|
227
|
+
this.collections.set(name, state);
|
|
228
|
+
this.cacheTailCert(state);
|
|
229
|
+
}
|
|
230
|
+
/** The reactivity notification verifier over a node's service verifier (real collected-multisig at T3). */
|
|
231
|
+
notificationVerifierFor(service) {
|
|
232
|
+
return createNotificationVerifier({ verifier: service.verifier(), tier: Tier.T3 });
|
|
233
|
+
}
|
|
234
|
+
makePushState(collectionIdB64, topicId, tailId, w, wCheckpoint, queueMax, deltaMaxBytes) {
|
|
235
|
+
return new PushState({
|
|
236
|
+
collectionId: collectionIdB64,
|
|
237
|
+
topicId: bytesToB64url(topicId),
|
|
238
|
+
tailIdAtJoin: bytesToB64url(tailId),
|
|
239
|
+
w,
|
|
240
|
+
wCheckpoint,
|
|
241
|
+
queueMax,
|
|
242
|
+
deltaMaxBytes,
|
|
243
|
+
});
|
|
244
|
+
}
|
|
245
|
+
/** The FRET cohort (the `wantK` nearest members) around a coord. */
|
|
246
|
+
cohortMembersAround(coord) {
|
|
247
|
+
const ids = new Set(this.mesh.assembleCohort(coord, this.wantK));
|
|
248
|
+
return this.members.filter((m) => ids.has(m.idStr));
|
|
249
|
+
}
|
|
250
|
+
/** Whether the node FRET-routes nearest to `coord` (the cohort's routed primary) is an Edge profile. */
|
|
251
|
+
nearestIsEdge(coord) {
|
|
252
|
+
const nearest = this.mesh.nearest(coord);
|
|
253
|
+
const idx = this.members.findIndex((m) => m.idStr === nearest.idStr);
|
|
254
|
+
return idx >= 0 && this.profiles[idx] === "edge";
|
|
255
|
+
}
|
|
256
|
+
/**
|
|
257
|
+
* Choose a reactivity tail id derived from `baseTail` whose `coord_0` routes nearest a **Core** node.
|
|
258
|
+
* Reactivity topics serve at tier T3, and an Edge node declines a T3 cold-start by design (it serves no
|
|
259
|
+
* forwarder duty — pinned by `cohort-topic-scale-lifecycle` "Edge serves no T3"), so a topic whose
|
|
260
|
+
* `coord_0` routes nearest an Edge node can never instantiate its tail cohort: the routed primary returns
|
|
261
|
+
* `no_state`, the walk exhausts, and `register` throws `CohortBackoffError`. Because {@link makeMembers}
|
|
262
|
+
* re-randomizes the member ring layout every run, an Edge-profile mesh would otherwise *intermittently*
|
|
263
|
+
* seat the Edge node as a collection's tail primary (~1/nodeCount of runs) and flake. In production a
|
|
264
|
+
* reactivity tail cohort IS the Core cluster that served the tail, so pinning the primary to a Core node
|
|
265
|
+
* models reality. A no-op (returns `utf8(baseTail)` at the first try) when the mesh has no Edge nodes —
|
|
266
|
+
* the common case — so every all-Core suite is byte-for-byte unaffected.
|
|
267
|
+
*/
|
|
268
|
+
pinTailToCore(baseTail) {
|
|
269
|
+
for (let nonce = 0;; nonce++) {
|
|
270
|
+
const tailId = utf8.encode(nonce === 0 ? baseTail : `${baseTail}#${nonce}`);
|
|
271
|
+
if (!this.nearestIsEdge(addressing.coord0(reactivityTopicId(tailId)))) {
|
|
272
|
+
return tailId;
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
collection(name) {
|
|
277
|
+
const c = this.collections.get(name);
|
|
278
|
+
if (c === undefined) {
|
|
279
|
+
throw new Error(`reactivity mesh: collection "${name}" is not registered (call registerCollection first)`);
|
|
280
|
+
}
|
|
281
|
+
return c;
|
|
282
|
+
}
|
|
283
|
+
/**
|
|
284
|
+
* Cache the tail cohort's `MembershipCertV1` (over the FRET cohort around the current tail's coord_0) into
|
|
285
|
+
* every node's participant verifier, so a subscriber's notification verify is real Ed25519 against the
|
|
286
|
+
* tail membership — mirroring `reactivity-real-crypto.spec.ts`, now over the mesh's real cohort members.
|
|
287
|
+
*/
|
|
288
|
+
cacheTailCert(c) {
|
|
289
|
+
const members = c.tailCohort.map((m) => bytesToB64url(m.bytes));
|
|
290
|
+
const cert = {
|
|
291
|
+
v: 1,
|
|
292
|
+
cohortCoord: bytesToB64url(c.coord0),
|
|
293
|
+
cohortEpoch: bytesToB64url(utf8.encode(`${c.name}:${bytesToB64url(c.tailId)}`)),
|
|
294
|
+
members,
|
|
295
|
+
stabilizedAt: this.vtime,
|
|
296
|
+
thresholdSig: bytesToB64url(new Uint8Array([0])),
|
|
297
|
+
signers: members,
|
|
298
|
+
};
|
|
299
|
+
for (const node of this.mesh.nodes) {
|
|
300
|
+
node.host.service.verifier().cache(cert);
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
/** The tail cohort member ids (base64url member bytes) serving a collection's current tail. */
|
|
304
|
+
tailCohortIds(name) {
|
|
305
|
+
return this.collection(name).tailCohort.map((m) => bytesToB64url(m.bytes));
|
|
306
|
+
}
|
|
307
|
+
/** This collection's tier-0 cohort registration records carrying a reactivity subscribe payload. */
|
|
308
|
+
cohortSubscriberCount(name) {
|
|
309
|
+
const c = this.collection(name);
|
|
310
|
+
return c.setup.decidingEngine.records(c.topicId).filter((r) => r.appState !== undefined).length;
|
|
311
|
+
}
|
|
312
|
+
/** The current committed revision for a collection. */
|
|
313
|
+
currentRevision(name) {
|
|
314
|
+
return this.collection(name).rev;
|
|
315
|
+
}
|
|
316
|
+
/**
|
|
317
|
+
* Drive the deciding cohort's membership stabilization (publish its `MembershipCertV1`). Promotion is
|
|
318
|
+
* fire-and-forget off the register path; stabilizing publishes the cohort cert so a crossed-`cap_promote`
|
|
319
|
+
* decision lands deterministically (no wall-clock wait) — the same pre-step the cohort-topic scale specs use.
|
|
320
|
+
*/
|
|
321
|
+
async stabilizeCohort(name) {
|
|
322
|
+
await this.collection(name).setup.decidingEngine.onStabilized(this.vtime);
|
|
323
|
+
}
|
|
324
|
+
/** Whether a collection's tier-0 cohort has promoted (its direct participants crossed `cap_promote`). */
|
|
325
|
+
isPromoted(name) {
|
|
326
|
+
const c = this.collection(name);
|
|
327
|
+
return c.setup.decidingEngine.isPromoted(c.topicId);
|
|
328
|
+
}
|
|
329
|
+
/** Whether a node profile may serve as a reactivity forwarder (T3 producer) — Edge never can. */
|
|
330
|
+
mayForward(nodeIndex) {
|
|
331
|
+
return mayServeAsReactivityForwarder(this.profileOf(nodeIndex));
|
|
332
|
+
}
|
|
333
|
+
/**
|
|
334
|
+
* Register node `nodeIndex` as a reactivity subscriber for `collection` via the **real**
|
|
335
|
+
* {@link ReactivitySubscriptionManager} → `CohortTopicService.register` walk at tier T3. The subscribe
|
|
336
|
+
* record lands in the real tier-0 cohort store; the manager owns the real verify/deliver/backfill/resume
|
|
337
|
+
* path. Returns a handle whose `delivered` array accumulates the surfaced notifications.
|
|
338
|
+
*/
|
|
339
|
+
async subscribe(nodeIndex, collection, opts = {}) {
|
|
340
|
+
const c = this.collection(collection);
|
|
341
|
+
const member = this.members[nodeIndex];
|
|
342
|
+
const service = this.mesh.nodes[nodeIndex].host.service;
|
|
343
|
+
const profile = opts.profile ?? this.profileOf(nodeIndex);
|
|
344
|
+
const subId = bytesToB64url(member.bytes);
|
|
345
|
+
const handle = {
|
|
346
|
+
nodeIndex,
|
|
347
|
+
member,
|
|
348
|
+
subId,
|
|
349
|
+
manager: undefined,
|
|
350
|
+
delivered: [],
|
|
351
|
+
attachedTailB64: bytesToB64url(c.tailId),
|
|
352
|
+
collectionName: collection,
|
|
353
|
+
autoDrain: opts.autoDrain ?? true,
|
|
354
|
+
asleep: false,
|
|
355
|
+
backfills: 0,
|
|
356
|
+
chainRead: false,
|
|
357
|
+
checkpointDigests: [],
|
|
358
|
+
rotationNotices: [],
|
|
359
|
+
scheduler: undefined,
|
|
360
|
+
};
|
|
361
|
+
// The host re-registration scheduler bound to this subscriber, driven over the harness virtual clock. On
|
|
362
|
+
// fire it re-attaches the subscriber under the rotated tail — the production move the deferred Quereus
|
|
363
|
+
// `Database.watch` factory performs (`reactivity-rotation-host-wiring-e2e` §C). The single-subscriber
|
|
364
|
+
// `planReRegistration` path draws `fireAt` over `T_rejoin_jitter`, so a `advanceTime` past that window
|
|
365
|
+
// fires it deterministically.
|
|
366
|
+
const scheduler = new RotationReRegistrationScheduler({
|
|
367
|
+
reRegister: (plan) => { this.reAttachToNewTail(handle, plan); return Promise.resolve(); },
|
|
368
|
+
setTimer: (fn, delayMs) => this.armVirtualTimer(fn, delayMs),
|
|
369
|
+
now: () => this.vtime,
|
|
370
|
+
});
|
|
371
|
+
handle.scheduler = scheduler;
|
|
372
|
+
const manager = new ReactivitySubscriptionManager({
|
|
373
|
+
service,
|
|
374
|
+
collectionId: c.collectionId,
|
|
375
|
+
tailIdAtAttach: c.tailId,
|
|
376
|
+
deliver: (n) => { handle.delivered.push(n); },
|
|
377
|
+
profile,
|
|
378
|
+
lastKnownRev: opts.lastKnownRev ?? 0,
|
|
379
|
+
clock: () => this.vtime,
|
|
380
|
+
// Backfill RPC served from the tail cohort's live replay buffer (real serveBackfill).
|
|
381
|
+
signBackfill: (req) => bytesToB64url(utf8.encode(`bf:${req.fromRevision}:${req.toRevision}`)),
|
|
382
|
+
backfillTransport: (req) => { handle.backfills += 1; return Promise.resolve(serveBackfill(c.pushState.replayBuffer, req, c.collectionIdB64)); },
|
|
383
|
+
// Resume RPC served from the tail cohort's stacked windows (real serveResume).
|
|
384
|
+
signResume: () => bytesToB64url(utf8.encode("resume")),
|
|
385
|
+
subscriberCoord: subId,
|
|
386
|
+
resumeTransport: (req) => {
|
|
387
|
+
// Model the live recover serve's drain redirect (the running node's `markRotated` →
|
|
388
|
+
// `rotationRedirectFor` → `RotationRedirectError`): a resume whose `latestKnownTailId` anchors a
|
|
389
|
+
// rotated, still-draining tail is bounced to the new tree instead of served stale data.
|
|
390
|
+
const gate = c.rotationGate;
|
|
391
|
+
if (gate !== undefined && req.latestKnownTailId === c.rotatedFromTailB64 && gate.isDraining(this.vtime)) {
|
|
392
|
+
return Promise.reject(new RotationRedirectError(gate.rotationRedirect));
|
|
393
|
+
}
|
|
394
|
+
return Promise.resolve(serveResume(req, {
|
|
395
|
+
buffer: c.pushState.replayBuffer,
|
|
396
|
+
checkpoint: c.pushState.checkpoint,
|
|
397
|
+
// The cross-rotation inherited window the new tail holds after a handoff: a resume below the new
|
|
398
|
+
// ring but within it is served `checkpoint_window` (not `out_of_window`) while it abuts the ring.
|
|
399
|
+
inheritedCheckpoint: c.pushState.inheritedCheckpoint,
|
|
400
|
+
currentTailId: bytesToB64url(c.tailId),
|
|
401
|
+
currentRevision: c.rev,
|
|
402
|
+
rotationRevision: c.rev,
|
|
403
|
+
expectedCollectionId: c.collectionIdB64,
|
|
404
|
+
}));
|
|
405
|
+
},
|
|
406
|
+
onChainRead: () => { handle.chainRead = true; },
|
|
407
|
+
onTailRotated: (newTailId, rev) => { handle.tailRotated = [newTailId, rev]; },
|
|
408
|
+
onCheckpointDigest: (summary) => { handle.checkpointDigests.push(summary); },
|
|
409
|
+
// Surface the jittered re-registration plan from a delivered rotation pre-announce / hard rotation.
|
|
410
|
+
rejoinJitter: createRejoinJitter({ capPromote: DEFAULT_CAP_PROMOTE_FAST, random: this.deterministicRandom() }),
|
|
411
|
+
onRotation: (notice) => { handle.rotationNotices.push(notice); scheduler.schedule(notice); },
|
|
412
|
+
});
|
|
413
|
+
handle.manager = manager;
|
|
414
|
+
handle.registration = await manager.register();
|
|
415
|
+
c.subscribers.push(handle);
|
|
416
|
+
return handle;
|
|
417
|
+
}
|
|
418
|
+
/**
|
|
419
|
+
* Commit `count` transactions to `collection` through the real `StorageRepo` → change-bridge →
|
|
420
|
+
* origination path, then fan the originated notifications out to current-tail subscribers. Returns the
|
|
421
|
+
* new current revision. Each commit pre-signs a **real** threshold {@link CommitCert} with the tail
|
|
422
|
+
* cohort's Ed25519 keys (the bridge's synchronous extractor resolves it by `actionId`).
|
|
423
|
+
*/
|
|
424
|
+
async commit(collection, count = 1) {
|
|
425
|
+
const c = this.collection(collection);
|
|
426
|
+
for (let i = 0; i < count; i++) {
|
|
427
|
+
const rev = c.rev + 1;
|
|
428
|
+
const actionId = `rx-${c.collectionIdB64}-${rev}-${this.corr++}`;
|
|
429
|
+
const blockId = `${c.name}-blk-${c.blockSeq++}`;
|
|
430
|
+
// Anticipatory warm-up / block-filling: the filling commit carries the rotation pre-announce.
|
|
431
|
+
const signal = c.fillTracker.onCommit();
|
|
432
|
+
if (signal.kind === "filling") {
|
|
433
|
+
const nextTail = utf8.encode(`${c.name}:tail-${rev}`);
|
|
434
|
+
c.rotationHint = buildRotationHint(bytesToB64url(nextTail), rev);
|
|
435
|
+
}
|
|
436
|
+
const cert = await this.buildTailCert(c, rev);
|
|
437
|
+
c.certByAction.set(actionId, cert);
|
|
438
|
+
await c.repo.pend({ actionId: actionId, transforms: this.insertTransforms(blockId, c.collectionIdB64), policy: "c" });
|
|
439
|
+
const result = await c.repo.commit({ actionId: actionId, blockIds: [blockId], tailId: blockId, rev });
|
|
440
|
+
if (!result.success) {
|
|
441
|
+
throw new Error(`reactivity mesh: commit failed for ${collection} rev ${rev}`);
|
|
442
|
+
}
|
|
443
|
+
c.rev = rev;
|
|
444
|
+
// The pre-announce is one-shot: it rode the filling notification; clear it for subsequent commits.
|
|
445
|
+
c.rotationHint = undefined;
|
|
446
|
+
}
|
|
447
|
+
await this.drainFanOut(c);
|
|
448
|
+
return c.rev;
|
|
449
|
+
}
|
|
450
|
+
/** Build a real threshold commit cert: every tail-cohort member signs `utf8(commitHash + ":approve")`. */
|
|
451
|
+
async buildTailCert(c, rev) {
|
|
452
|
+
const commitHash = `${c.collectionIdB64}:${rev}`;
|
|
453
|
+
const signedPayload = utf8.encode(`${commitHash}:approve`);
|
|
454
|
+
const commits = {};
|
|
455
|
+
for (const m of c.tailCohort) {
|
|
456
|
+
const sig = await m.key.sign(signedPayload);
|
|
457
|
+
commits[m.idStr] = { type: "approve", signature: bytesToB64url(sig) };
|
|
458
|
+
}
|
|
459
|
+
const record = {
|
|
460
|
+
messageHash: `mh-${commitHash}`,
|
|
461
|
+
message: { operations: [{ get: { blockIds: [] } }], expiration: this.vtime + 30_000 },
|
|
462
|
+
peers: this.clusterPeers(c.tailCohort),
|
|
463
|
+
promises: {},
|
|
464
|
+
commits,
|
|
465
|
+
};
|
|
466
|
+
return buildCommitCert(record, this.minSigs, signedPayload);
|
|
467
|
+
}
|
|
468
|
+
clusterPeers(cohort) {
|
|
469
|
+
const peers = {};
|
|
470
|
+
for (const m of cohort) {
|
|
471
|
+
peers[m.idStr] = { multiaddrs: ["/ip4/127.0.0.1/tcp/8000"], publicKey: bytesToB64url(m.peerId.publicKey.raw) };
|
|
472
|
+
}
|
|
473
|
+
return peers;
|
|
474
|
+
}
|
|
475
|
+
insertTransforms(blockId, collectionId) {
|
|
476
|
+
const block = { header: { id: blockId, type: "test", collectionId: collectionId } };
|
|
477
|
+
return { inserts: { [blockId]: block }, updates: {}, deletes: [] };
|
|
478
|
+
}
|
|
479
|
+
/**
|
|
480
|
+
* Run the originated notifications through the real forwarder receive path (populating the tail replay
|
|
481
|
+
* ring + rolling checkpoint + dedupe), then fan each out to every current-tail subscriber via its real
|
|
482
|
+
* per-subscriber bounded queue. A subscriber with `autoDrain` drains immediately; a slow subscriber's
|
|
483
|
+
* queue accumulates (drop-oldest under pressure) until it is woken.
|
|
484
|
+
*/
|
|
485
|
+
async drainFanOut(c) {
|
|
486
|
+
const notifs = c.emitQueue.splice(0, c.emitQueue.length);
|
|
487
|
+
const tailB64 = bytesToB64url(c.tailId);
|
|
488
|
+
for (const n of notifs) {
|
|
489
|
+
// Forwarder receive: verify (real) → dedupe → append to the replay ring (and roll the checkpoint).
|
|
490
|
+
await c.forwarder.receive(n, this.vtime);
|
|
491
|
+
for (const s of c.subscribers) {
|
|
492
|
+
if (s.attachedTailB64 !== tailB64 || s.asleep) {
|
|
493
|
+
continue;
|
|
494
|
+
}
|
|
495
|
+
c.pushState.perSubscriberQueue.enqueue(s.subId, n);
|
|
496
|
+
if (s.autoDrain) {
|
|
497
|
+
await this.drainSubscriber(s);
|
|
498
|
+
}
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
/** Drain a subscriber's bounded queue (FIFO) through the real manager delivery path. */
|
|
503
|
+
async drainSubscriber(s) {
|
|
504
|
+
const c = this.collection(s.collectionName);
|
|
505
|
+
const queue = c.pushState.perSubscriberQueue.peekQueue(s.subId);
|
|
506
|
+
if (queue === undefined) {
|
|
507
|
+
return;
|
|
508
|
+
}
|
|
509
|
+
for (const n of queue.drain()) {
|
|
510
|
+
await s.manager.onNotification(n);
|
|
511
|
+
}
|
|
512
|
+
}
|
|
513
|
+
/** Wake a slow subscriber: drain its accumulated (drop-oldest) queue through the delivery path. */
|
|
514
|
+
async wakeSubscriber(s) {
|
|
515
|
+
await this.drainSubscriber(s);
|
|
516
|
+
}
|
|
517
|
+
/** Put a subscriber to sleep: fan-out skips it (its tail replay buffer still fills) until it resumes. */
|
|
518
|
+
sleepSubscriber(s) {
|
|
519
|
+
s.asleep = true;
|
|
520
|
+
}
|
|
521
|
+
/** The drop counter on a subscriber's bounded queue (drop-oldest under backpressure). */
|
|
522
|
+
droppedFor(s) {
|
|
523
|
+
const c = this.collection(s.collectionName);
|
|
524
|
+
return c.pushState.perSubscriberQueue.peekQueue(s.subId)?.dropped ?? 0;
|
|
525
|
+
}
|
|
526
|
+
/** Drive the manager's real `resume()` (one `ResumeV1` → `serveResume` → `applyResumeReply`). */
|
|
527
|
+
async resume(s) {
|
|
528
|
+
s.asleep = false;
|
|
529
|
+
return s.manager.resume();
|
|
530
|
+
}
|
|
531
|
+
/**
|
|
532
|
+
* The re-registration move the scheduler fires on a rotation notice: re-attach the subscriber under the
|
|
533
|
+
* rotated tail so subsequent fan-out targets it. The production factory builds a *fresh* manager under
|
|
534
|
+
* `plan.newTopicId`; the harness keeps the same manager (its `lastRevision` is already contiguous and its
|
|
535
|
+
* `rotationHandledFor` guard dedupes the new-tail deliveries' re-detection), so re-attaching the existing
|
|
536
|
+
* subscriber is the equivalent move with no gap.
|
|
537
|
+
*/
|
|
538
|
+
reAttachToNewTail(s, plan) {
|
|
539
|
+
const c = this.collection(s.collectionName);
|
|
540
|
+
s.attachedTailB64 = bytesToB64url(plan.newTailId);
|
|
541
|
+
// Defensive: the plan's successor must be the collection's current tail (single-rotation harness model).
|
|
542
|
+
if (s.attachedTailB64 !== bytesToB64url(c.tailId)) {
|
|
543
|
+
s.attachedTailB64 = bytesToB64url(c.tailId);
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
/**
|
|
547
|
+
* Rotate a collection's tail: advance `tailId` → new `topicId`/coord/cohort, re-establish the new cohort
|
|
548
|
+
* (re-seed willingness so a post-rotation subscribe can register), re-cache the tail cert, rebuild the tail
|
|
549
|
+
* `PushState`, fold the outgoing ring into a handoff checkpoint onto the new tail, and plan the jittered
|
|
550
|
+
* re-registration wave bounded by `cap_promote_fast`. Async because re-forming the new cohort is (the real
|
|
551
|
+
* willingness convergence). With `{ autoReattach: false }` it instead models the running node's
|
|
552
|
+
* `markRotated` (drain gate) so subscribers move via the recover-redirect path. Models §Tail rotation steps 2–5.
|
|
553
|
+
*/
|
|
554
|
+
async rotateTail(collection, opts = {}) {
|
|
555
|
+
const c = this.collection(collection);
|
|
556
|
+
const autoReattach = opts.autoReattach ?? true;
|
|
557
|
+
const rotationRevision = c.rev;
|
|
558
|
+
// Pin the rotated tail's routed primary to a Core node too (Edge nodes decline a T3 cold-start); a
|
|
559
|
+
// no-op for the all-Core rotation suites, matching the initial-tail pinning in `registerCollection`.
|
|
560
|
+
const newTailId = this.pinTailToCore(`${c.name}:tail-${rotationRevision}`);
|
|
561
|
+
const newTailIdB64 = bytesToB64url(newTailId);
|
|
562
|
+
const oldTailB64 = bytesToB64url(c.tailId);
|
|
563
|
+
// Buffer-to-checkpoint handoff — the ONLY state migrated across a rotation (§Tail rotation step 5).
|
|
564
|
+
const handoff = buildRotationHandoffCheckpoint(c.pushState, { rotationRevision });
|
|
565
|
+
// Active subscribers re-register under the new tree, carrying their lastRevision (continuity).
|
|
566
|
+
const live = c.subscribers.filter((s) => s.attachedTailB64 === oldTailB64);
|
|
567
|
+
const jitter = createRejoinJitter({ capPromote: DEFAULT_CAP_PROMOTE_FAST, random: this.deterministicRandom() });
|
|
568
|
+
const plans = planReRegistrationWave({
|
|
569
|
+
hint: { newTailId: newTailIdB64 },
|
|
570
|
+
subscribers: live.map((s) => ({ lastRevision: s.manager.lastRevision })),
|
|
571
|
+
now: this.vtime,
|
|
572
|
+
jitter,
|
|
573
|
+
});
|
|
574
|
+
// Roll the topic to the new tail.
|
|
575
|
+
c.tailId = newTailId;
|
|
576
|
+
c.topicId = reactivityTopicId(newTailId);
|
|
577
|
+
c.coord0 = addressing.coord0(c.topicId);
|
|
578
|
+
c.tailCohort = this.cohortMembersAround(c.coord0);
|
|
579
|
+
// Re-establish the new tail's cohort (re-seed willingness) so a subscriber can register under it after the
|
|
580
|
+
// rotation — the new tree forming, modeled deterministically (the real cohort-topic willingness convergence).
|
|
581
|
+
c.setup = await setupTopic(this.mesh, c.topicId);
|
|
582
|
+
this.cacheTailCert(c);
|
|
583
|
+
c.pushState = this.makePushState(c.collectionIdB64, c.topicId, c.tailId, c.w, c.wCheckpoint, c.queueMax, c.deltaMaxBytes);
|
|
584
|
+
c.forwarder = createReactivityForwarder({ state: c.pushState, verifier: this.notificationVerifierFor(c.originationService) });
|
|
585
|
+
if (handoff !== undefined) {
|
|
586
|
+
applyRotationHandoff(c.pushState, handoff);
|
|
587
|
+
}
|
|
588
|
+
if (autoReattach) {
|
|
589
|
+
// Re-attach the live subscribers under the new tail so subsequent fan-out targets them (the
|
|
590
|
+
// continuity/handoff model — subscribers migrate directly, no redirect involved).
|
|
591
|
+
for (const s of live) {
|
|
592
|
+
s.attachedTailB64 = newTailIdB64;
|
|
593
|
+
}
|
|
594
|
+
}
|
|
595
|
+
else {
|
|
596
|
+
// Transport-driven: model `markRotated` by arming the outgoing tail's drain gate. Live subscribers
|
|
597
|
+
// stay on the old tail until each resumes, gets the `kind:"rotated"` redirect, and re-attaches via its
|
|
598
|
+
// own onRotation → scheduler → reRegister path (§Tail rotation step 2–3).
|
|
599
|
+
c.rotationGate = new TailDrainGate({ rotatedAt: this.vtime, newTailId: newTailIdB64, effectiveAtRevision: rotationRevision + 1 });
|
|
600
|
+
c.rotatedFromTailB64 = oldTailB64;
|
|
601
|
+
}
|
|
602
|
+
return {
|
|
603
|
+
newTailId,
|
|
604
|
+
newTailIdB64,
|
|
605
|
+
rotationRevision,
|
|
606
|
+
plans,
|
|
607
|
+
peakWindowArrivals: this.peakWindowArrivals(plans),
|
|
608
|
+
...(handoff?.checkpoint === undefined ? {} : { handoff: handoff.checkpoint }),
|
|
609
|
+
};
|
|
610
|
+
}
|
|
611
|
+
/** Peak re-registration arrivals in any `T_rejoin_jitter`-long window of a planned wave. */
|
|
612
|
+
peakWindowArrivals(plans) {
|
|
613
|
+
if (plans.length === 0) {
|
|
614
|
+
return 0;
|
|
615
|
+
}
|
|
616
|
+
const fireAts = plans.map((p) => p.fireAt).sort((a, b) => a - b);
|
|
617
|
+
const window = T_REJOIN_JITTER_MS; // the same jitter span `createRejoinJitter` spreads the wave over
|
|
618
|
+
let peak = 0;
|
|
619
|
+
for (let i = 0; i < fireAts.length; i++) {
|
|
620
|
+
let j = i;
|
|
621
|
+
while (j < fireAts.length && fireAts[j] - fireAts[i] < window) {
|
|
622
|
+
j++;
|
|
623
|
+
}
|
|
624
|
+
peak = Math.max(peak, j - i);
|
|
625
|
+
}
|
|
626
|
+
return peak;
|
|
627
|
+
}
|
|
628
|
+
/** A deterministic pseudo-random in [0,1) so the jitter wave is reproducible across runs. */
|
|
629
|
+
deterministicRandom() {
|
|
630
|
+
let seed = 0x2545f491;
|
|
631
|
+
return () => {
|
|
632
|
+
seed = (seed * 1103515245 + 12345) & 0x7fffffff;
|
|
633
|
+
return seed / 0x7fffffff;
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
/**
|
|
637
|
+
* The tail cohort `PushState` for a collection (its live replay ring / checkpoint / backpressure). Exposed
|
|
638
|
+
* so a suite can assert the replay/dedupe/partition state directly.
|
|
639
|
+
*/
|
|
640
|
+
pushStateOf(collection) {
|
|
641
|
+
return this.collection(collection).pushState;
|
|
642
|
+
}
|
|
643
|
+
/** A real forwarder receive over the tail `PushState` — used to model a forwarder-hop / partition replay. */
|
|
644
|
+
async forwarderReceive(collection, n) {
|
|
645
|
+
const c = this.collection(collection);
|
|
646
|
+
return c.forwarder.receive(n, this.vtime);
|
|
647
|
+
}
|
|
648
|
+
async stop() {
|
|
649
|
+
// Tear down every subscriber's re-registration scheduler (drops pending virtual timers + the idempotence
|
|
650
|
+
// ledger) before stopping the mesh.
|
|
651
|
+
for (const c of this.collections.values()) {
|
|
652
|
+
for (const s of c.subscribers) {
|
|
653
|
+
s.scheduler?.stop();
|
|
654
|
+
}
|
|
655
|
+
}
|
|
656
|
+
await this.mesh.stop();
|
|
657
|
+
}
|
|
658
|
+
}
|
|
659
|
+
/** Build and start a reactivity integration mesh. */
|
|
660
|
+
export async function buildReactivityMesh(opts = {}) {
|
|
661
|
+
return ReactivityMesh.build(opts);
|
|
662
|
+
}
|
|
663
|
+
//# sourceMappingURL=reactivity-mesh-harness.js.map
|