@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,475 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Matchmaking **mock-transport mesh harness** — the in-process, many-logical-node substrate for the
|
|
3
|
+
* matchmaking *integration* tier (`docs/matchmaking.md`), layered on the cohort-topic mesh harness
|
|
4
|
+
* ({@link import("./cohort-topic-mesh-harness.js")}) rather than forking it. It drives the **real**
|
|
5
|
+
* provider manager, the **real** cohort-side `QueryV1` handler, and the **real** seeker walk
|
|
6
|
+
* client over a network of real-Ed25519-keyed {@link CohortTopicHost}s — so a matchmaking flow is
|
|
7
|
+
* exercised end-to-end (register → cohort store → query → seeker re-validation) without a live libp2p
|
|
8
|
+
* stack.
|
|
9
|
+
*
|
|
10
|
+
* **What is real vs. modeled.** Provider registration rides the real {@link MatchmakingProviderManager}
|
|
11
|
+
* → `CohortTopicService.register` walk; the seeker walk drives the real {@link SeekerWalkClient} whose
|
|
12
|
+
* per-tier registrations carry the real {@link MatchmakingSeeker} payload and are dispatched through the
|
|
13
|
+
* real cohort engines (the one-shot `MatchmakingSeekerManager` wrapper is not on this path). Either way
|
|
14
|
+
* records land in the real cohort store and carry real peer-key signatures the seeker re-validates with
|
|
15
|
+
* the real `verifyPeerSig`. The cohort `topicTraffic` barometer, however, is
|
|
16
|
+
* **modeled** ({@link MatchmakingMesh.setTraffic}): the hang-out decision is driven by an injected
|
|
17
|
+
* `arrivalsPerMin` / `queriesPerMin` regime because generating a real arrival *rate* (90/min …)
|
|
18
|
+
* deterministically over a virtual clock is infeasible — the same posture the design simulator takes.
|
|
19
|
+
* The decision *math* against that regime is the db-core `seeker-walk.spec.ts` floor; this harness
|
|
20
|
+
* proves the regime drives the real walk over real records.
|
|
21
|
+
*
|
|
22
|
+
* **Single-tier-0 reach (honest).** The underlying cohort-topic substrate serves a single tier-0 cohort
|
|
23
|
+
* (multi-tier promotion to a *serving* tier-`d ≥ 1` cohort, and membership-rotation primary handoff, are
|
|
24
|
+
* the cohort-topic follow-ons `cohort-topic-followon-derivation` / `cohort-topic-parent-child-link` /
|
|
25
|
+
* rotation-handoff). A seeker walk therefore always falls through tiers `d_max…1` as `NoState` and is
|
|
26
|
+
* `Accepted` at the root — the cold/sparse-walk-to-root shape is fully real; "hot topic, a deep tier
|
|
27
|
+
* suffices" (an `Accepted` above the root) is gated on that follow-on and is tagged-unimplemented in the
|
|
28
|
+
* mesh suites, not faked here.
|
|
29
|
+
*
|
|
30
|
+
* **Virtual time.** Like the cohort harness this is not a wall-clock simulator: each {@link seek} runs on
|
|
31
|
+
* an injected virtual clock (sleep advances `now`), so a 10 s hang-out resolves instantly and
|
|
32
|
+
* deterministically. Cohort TTL sweeps are driven explicitly via {@link sweepTopic}.
|
|
33
|
+
*/
|
|
34
|
+
|
|
35
|
+
import { Tier, providerTtlForProfile } from '@optimystic/db-core';
|
|
36
|
+
import {
|
|
37
|
+
MatchmakingProvider,
|
|
38
|
+
MatchmakingSeeker,
|
|
39
|
+
matchTopicId,
|
|
40
|
+
coreProfile,
|
|
41
|
+
edgeProfile,
|
|
42
|
+
bytesToB64url,
|
|
43
|
+
registerSigningPayload,
|
|
44
|
+
verifyProviderEntry,
|
|
45
|
+
type CapabilityFilter,
|
|
46
|
+
type EntrySigVerifier,
|
|
47
|
+
type MatchTopicKind,
|
|
48
|
+
type NodeProfile,
|
|
49
|
+
type ProviderEntryV1,
|
|
50
|
+
type QueryReplyV1,
|
|
51
|
+
type QueryV1,
|
|
52
|
+
type RegisterV1,
|
|
53
|
+
type RegistrationRecord,
|
|
54
|
+
type TopicTrafficV1,
|
|
55
|
+
} from '@optimystic/db-core';
|
|
56
|
+
import type { CohortTopicHost, CoordEngine } from '../cohort-topic/host.js';
|
|
57
|
+
import { signPeer, verifyPeerSig } from '../cohort-topic/peer-sig.js';
|
|
58
|
+
import { MatchmakingProviderManager } from '../matchmaking/provider-manager.js';
|
|
59
|
+
import { handleMatchmakingQuery } from '../matchmaking/query-handler.js';
|
|
60
|
+
import { SeekerWalkClient, type SeekerProbeReply, type SeekerWalkTransport } from '../matchmaking/seeker-walk-client.js';
|
|
61
|
+
import {
|
|
62
|
+
addressing,
|
|
63
|
+
buildMesh,
|
|
64
|
+
delay,
|
|
65
|
+
makeMembers,
|
|
66
|
+
setupTopic,
|
|
67
|
+
type CohortMesh,
|
|
68
|
+
type Member,
|
|
69
|
+
type MeshOptions,
|
|
70
|
+
} from './cohort-topic-mesh-harness.js';
|
|
71
|
+
|
|
72
|
+
export { addressing, delay };
|
|
73
|
+
export type { Member };
|
|
74
|
+
|
|
75
|
+
/** A matchmaking topic reference resolved to a 32-byte anchor `topicId = H(kind ‖ label ‖ "match")`. */
|
|
76
|
+
export interface MatchTopicRef {
|
|
77
|
+
readonly kind: MatchTopicKind;
|
|
78
|
+
readonly label: string;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** A live provider registration in the mesh — the manager + the node it registered from. */
|
|
82
|
+
export interface ProviderHandle {
|
|
83
|
+
readonly nodeIndex: number;
|
|
84
|
+
readonly member: Member;
|
|
85
|
+
readonly topicId: Uint8Array;
|
|
86
|
+
readonly manager: MatchmakingProviderManager;
|
|
87
|
+
readonly provider: MatchmakingProvider;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
/** The result of a {@link MatchmakingMesh.seek} — the seeker walk outcome plus the harness walk trace. */
|
|
91
|
+
export interface MatchResult {
|
|
92
|
+
/** Matched, re-validated, deduped providers (may be `< wantCount`). */
|
|
93
|
+
readonly providers: ProviderEntryV1[];
|
|
94
|
+
/** Whether `wantCount` was met. */
|
|
95
|
+
readonly metWantCount: boolean;
|
|
96
|
+
/** The tree tier the walk terminated at. */
|
|
97
|
+
readonly terminalTier: number;
|
|
98
|
+
/** Total register hops issued (probes + escalations). */
|
|
99
|
+
readonly hops: number;
|
|
100
|
+
/** Max `topicTraffic.childCohortCount` seen across `Accepted` replies (hot-topic / sweep-escalation signal). */
|
|
101
|
+
readonly maxChildCohortCount: number;
|
|
102
|
+
/** Distinct tree tiers the walk register-probed (the walk depth). */
|
|
103
|
+
readonly tiersVisited: number;
|
|
104
|
+
/** Total virtual time the seeker spent hanging out (sum of poll sleeps). */
|
|
105
|
+
readonly hungOutMs: number;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/** Per-seek tuning (start tier, patience, filter, push opt). */
|
|
109
|
+
export interface SeekOptions {
|
|
110
|
+
/** Walk start tier `d_max`. Default {@link MatchmakingMesh.dMax}. */
|
|
111
|
+
readonly dMax?: number;
|
|
112
|
+
/** Total patience budget (ms). Default 10_000. */
|
|
113
|
+
readonly patienceMs?: number;
|
|
114
|
+
/** Capability filter, re-applied seeker-side. */
|
|
115
|
+
readonly filter?: CapabilityFilter;
|
|
116
|
+
/** Hang-out requery cadence (ms); forwarded to the walk config. */
|
|
117
|
+
readonly requeryIntervalMs?: number;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/** Construction inputs for a {@link MatchmakingMesh}. */
|
|
121
|
+
export interface MatchmakingMeshOptions {
|
|
122
|
+
/** Node count. Default 16 (one production-shaped cohort: `wantK = 16`). */
|
|
123
|
+
readonly nodeCount?: number;
|
|
124
|
+
/** Cohort size `wantK`. Default `min(nodeCount, 16)`. */
|
|
125
|
+
readonly wantK?: number;
|
|
126
|
+
/** Threshold signers. Default `wantK - 2`. */
|
|
127
|
+
readonly minSigs?: number;
|
|
128
|
+
/** FRET network-size estimate (drives the default `d_max`). Default 256 → `d_max = 1`. */
|
|
129
|
+
readonly sizeEstimate?: number;
|
|
130
|
+
/** Per-node profile by index (Edge nodes get the shorter provider TTL). Default all Core. */
|
|
131
|
+
readonly profiles?: readonly ('edge' | 'core')[];
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/** A virtual clock whose `sleep` advances `now` and accumulates total slept time (the hang-out budget). */
|
|
135
|
+
function virtualClock(): { clock: () => number; sleep: (ms: number) => Promise<void>; slept: () => number } {
|
|
136
|
+
let now = 0;
|
|
137
|
+
let totalSlept = 0;
|
|
138
|
+
return {
|
|
139
|
+
clock: (): number => now,
|
|
140
|
+
sleep: (ms: number): Promise<void> => {
|
|
141
|
+
now += ms;
|
|
142
|
+
totalSlept += ms;
|
|
143
|
+
return Promise.resolve();
|
|
144
|
+
},
|
|
145
|
+
slept: (): number => totalSlept,
|
|
146
|
+
};
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* The matchmaking integration mesh. Build with {@link buildMatchmakingMesh}; drive providers/seekers with
|
|
151
|
+
* {@link provide} / {@link seek} / {@link query}; model the hang-out regime with {@link setTraffic}.
|
|
152
|
+
*/
|
|
153
|
+
export class MatchmakingMesh {
|
|
154
|
+
/** The modeled per-topic traffic regime (keyed by base64url topic id); absent ⇒ a quiet default. */
|
|
155
|
+
private readonly traffic = new Map<string, TopicTrafficV1>();
|
|
156
|
+
/** Monotonic correlation-id counter so each routed register/probe is replay-distinct. */
|
|
157
|
+
private corr = 0;
|
|
158
|
+
|
|
159
|
+
private constructor(
|
|
160
|
+
readonly mesh: CohortMesh,
|
|
161
|
+
readonly members: Member[],
|
|
162
|
+
private readonly sizeEstimate: number,
|
|
163
|
+
private readonly profiles: readonly ('edge' | 'core')[],
|
|
164
|
+
) {}
|
|
165
|
+
|
|
166
|
+
/** Stand up an N-node matchmaking mesh and start every host. */
|
|
167
|
+
static async build(opts: MatchmakingMeshOptions = {}): Promise<MatchmakingMesh> {
|
|
168
|
+
const nodeCount = opts.nodeCount ?? 16;
|
|
169
|
+
const wantK = opts.wantK ?? Math.min(nodeCount, 16);
|
|
170
|
+
const minSigs = opts.minSigs ?? Math.max(1, wantK - 2);
|
|
171
|
+
const sizeEstimate = opts.sizeEstimate ?? 256;
|
|
172
|
+
const members = await makeMembers(nodeCount);
|
|
173
|
+
const meshOpts: MeshOptions = {
|
|
174
|
+
wantK,
|
|
175
|
+
minSigs,
|
|
176
|
+
sizeEstimate,
|
|
177
|
+
// Pin `cap_promote` very high so the topic's tree stays a single tier-0 cohort for the life of a
|
|
178
|
+
// test. Without this, the test artifact of registering several providers within a few
|
|
179
|
+
// milliseconds spikes the growth *slope* and trips the pre-promotion predictor (it extrapolates
|
|
180
|
+
// crossing `cap_promote` within the 30 s lookahead), promoting the cohort and bouncing later
|
|
181
|
+
// registrations in a `promoted`→`no_state` walk loop. Real registrations are spread over time, so
|
|
182
|
+
// this never fires in production; multi-tier promotion is the cohort-topic e2e's concern and the
|
|
183
|
+
// matchmaking scenarios that need a *serving* tier-`d ≥ 1` cohort are tagged-unimplemented here.
|
|
184
|
+
capPromote: 1_000_000,
|
|
185
|
+
// A generous register ceiling: the harness re-probes a cohort across many seeks; the default
|
|
186
|
+
// 4/min would rate-limit repeated probes from one node. Leaving `bootstrapEvidence`/`reputation`
|
|
187
|
+
// unset keeps cold-root instantiation permissive (the policy is "configured" only when one of
|
|
188
|
+
// those is supplied — see `createBootstrapEvidencePolicy`).
|
|
189
|
+
//
|
|
190
|
+
// `powDifficultyBits: 0` disables the cold-start proof-of-work cost. Providers/seekers register at
|
|
191
|
+
// the matchmaking tier (≥ T2), so the participant-side bootstrap-evidence builder otherwise mints a
|
|
192
|
+
// PoW puzzle (default 20 bits ≈ 1 M hashes) on every register/re-probe — the dominant cost in this
|
|
193
|
+
// mesh under full-suite load (~25-30 s sweep/walk cases run right at the mocha ceiling). Since the
|
|
194
|
+
// cohort policy is unconfigured (above) the evidence is never required, so minting it is pure waste;
|
|
195
|
+
// `bits: 0` solves on the first nonce (the documented test escape hatch in `meetsDifficulty`).
|
|
196
|
+
antiDos: { rateLimiter: { ratePerWindow: 1_000_000 }, powDifficultyBits: 0 },
|
|
197
|
+
...(opts.profiles === undefined ? {} : { profiles: opts.profiles }),
|
|
198
|
+
};
|
|
199
|
+
const mesh = await buildMesh(members, meshOpts);
|
|
200
|
+
const profiles = Array.from({ length: nodeCount }, (_v, i) => opts.profiles?.[i] ?? 'core');
|
|
201
|
+
return new MatchmakingMesh(mesh, members, sizeEstimate, profiles);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
/** The default walk start tier `d_max = ⌈log_F(sizeEstimate)⌉ − 1` (clamped to ≥ 0). */
|
|
205
|
+
get dMax(): number {
|
|
206
|
+
return Math.max(0, Math.ceil(Math.log(this.sizeEstimate) / Math.log(16)) - 1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
/** The seeker-side per-entry verifier: re-validate each forwarded entry's `registrationSig` for real. */
|
|
210
|
+
readonly verifyEntry: EntrySigVerifier = (participantId, payload, signature) =>
|
|
211
|
+
verifyPeerSig(participantId, payload, signature);
|
|
212
|
+
|
|
213
|
+
private node(index: number): CohortTopicHost {
|
|
214
|
+
return this.mesh.nodes[index]!.host;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
private profileOf(index: number): NodeProfile {
|
|
218
|
+
return this.profiles[index] === 'edge' ? edgeProfile() : coreProfile();
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/** Resolve `(kind, label)` to its anchor. */
|
|
222
|
+
topicId(kind: MatchTopicKind, label: string): Uint8Array {
|
|
223
|
+
return matchTopicId(kind, label);
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Instantiate the topic's tier-0 cohort and seed its willingness quorum so registrations are admitted
|
|
228
|
+
* (mirrors {@link setupTopic}). Call once per topic before {@link provide} / {@link seek}.
|
|
229
|
+
*/
|
|
230
|
+
async registerTopic(kind: MatchTopicKind, label: string): Promise<void> {
|
|
231
|
+
await setupTopic(this.mesh, this.topicId(kind, label));
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/** The coord-0 cohort engine for a topic (the node nearest `coord_0`, where tier-0 registrations land). */
|
|
235
|
+
private rootEngine(topicId: Uint8Array): { engine: CoordEngine; signer: (p: Uint8Array) => Promise<string> } {
|
|
236
|
+
const coord0 = addressing.coord0(topicId);
|
|
237
|
+
const node = this.mesh.nodeNearest(coord0);
|
|
238
|
+
const engine = node.host.registry.forCoord(coord0, 0 as Tier, this.members[0]!.bytes);
|
|
239
|
+
const signer = async (payload: Uint8Array): Promise<string> => bytesToB64url(await signPeer(node.member.key, payload));
|
|
240
|
+
return { engine, signer };
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** This cohort's locally-held provider/seeker registration records for a topic (the query-handler read). */
|
|
244
|
+
cohortRecords(kind: MatchTopicKind, label: string): readonly RegistrationRecord[] {
|
|
245
|
+
return this.rootEngine(this.topicId(kind, label)).engine.records(this.topicId(kind, label));
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
/** The topic's tier-0 cohort epoch (the aggregate-count / query signing-image input). */
|
|
249
|
+
cohortEpochFor(kind: MatchTopicKind, label: string): Uint8Array {
|
|
250
|
+
return this.rootEngine(this.topicId(kind, label)).engine.cohort().cohortEpoch;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
/** Serve the topic's tier-0 provider set as a flat `ProviderEntryV1[]` (the sweep `queryShard` read). */
|
|
254
|
+
async providerEntries(requesterIndex: number, kind: MatchTopicKind, label: string, filter?: CapabilityFilter): Promise<readonly ProviderEntryV1[]> {
|
|
255
|
+
return (await this.query(requesterIndex, kind, label, filter)).providers ?? [];
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
/**
|
|
259
|
+
* Register node `nodeIndex` as a provider at `(kind, label)` via the real provider manager (cohort-topic
|
|
260
|
+
* tier T2). The record lands in the topic's tier-0 cohort store with the provider's real peer-key
|
|
261
|
+
* signature. Returns a handle whose manager drives renew / setCapacity / signalFull / withdraw.
|
|
262
|
+
*/
|
|
263
|
+
async provide(nodeIndex: number, kind: MatchTopicKind, label: string, capabilities: readonly string[], capacityBudget: number, contactHint?: string): Promise<ProviderHandle> {
|
|
264
|
+
const member = this.members[nodeIndex]!;
|
|
265
|
+
const topicId = this.topicId(kind, label);
|
|
266
|
+
const provider = new MatchmakingProvider({
|
|
267
|
+
topicId,
|
|
268
|
+
capabilities,
|
|
269
|
+
capacityBudget,
|
|
270
|
+
contactHint: contactHint ?? `/ip4/127.0.0.1/tcp/4001/p2p/${member.idStr}`,
|
|
271
|
+
sign: async (payload: Uint8Array): Promise<string> => bytesToB64url(await signPeer(member.key, payload)),
|
|
272
|
+
});
|
|
273
|
+
const manager = new MatchmakingProviderManager({ service: this.node(nodeIndex).service, provider, profile: this.profileOf(nodeIndex) });
|
|
274
|
+
await manager.register();
|
|
275
|
+
return { nodeIndex, member, topicId, manager, provider };
|
|
276
|
+
}
|
|
277
|
+
|
|
278
|
+
/** Replicate the topic's tier-0 records to the rest of the cohort via one gossip round. */
|
|
279
|
+
async gossipReplicate(kind: MatchTopicKind, label: string): Promise<void> {
|
|
280
|
+
await this.rootEngine(this.topicId(kind, label)).engine.gossipRound(Date.now());
|
|
281
|
+
await delay(20);
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
/**
|
|
285
|
+
* Sweep the topic's tier-0 cohort at `now` (TTL eviction). Use after a provider {@link ProviderHandle}
|
|
286
|
+
* stops renewing (withdraw) to make the record's expiry observable to a subsequent {@link query}.
|
|
287
|
+
*/
|
|
288
|
+
sweepTopic(kind: MatchTopicKind, label: string, now: number): void {
|
|
289
|
+
this.rootEngine(this.topicId(kind, label)).engine.engine.sweepStale(now);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
/** A provider's profile TTL (ms) — the wall-clock after which an un-renewed record is sweepable. */
|
|
293
|
+
providerTtlMs(nodeIndex: number): number {
|
|
294
|
+
return providerTtlForProfile(this.profileOf(nodeIndex));
|
|
295
|
+
}
|
|
296
|
+
|
|
297
|
+
/**
|
|
298
|
+
* Model the hang-out regime for a topic: the `topicTraffic` the cohort attaches to its `Accepted`
|
|
299
|
+
* probe reply and query reply (`docs/matchmaking.md` §Hang-out vs. continue — Decision inputs). The
|
|
300
|
+
* provider/seeker *population* is real; only the arrival/query *rates* are injected.
|
|
301
|
+
*/
|
|
302
|
+
setTraffic(topicId: Uint8Array, traffic: Partial<TopicTrafficV1>): void {
|
|
303
|
+
this.traffic.set(bytesToB64url(topicId), {
|
|
304
|
+
windowSeconds: 30,
|
|
305
|
+
arrivalsPerMin: 0,
|
|
306
|
+
queriesPerMin: 0,
|
|
307
|
+
directParticipants: 0,
|
|
308
|
+
childCohortCount: 0,
|
|
309
|
+
...traffic,
|
|
310
|
+
});
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
/** The modeled regime for a topic, or a quiet default whose `directParticipants` reflects the real count. */
|
|
314
|
+
private trafficFor(topicId: Uint8Array, realDirectParticipants: number): TopicTrafficV1 {
|
|
315
|
+
const modeled = this.traffic.get(bytesToB64url(topicId));
|
|
316
|
+
if (modeled !== undefined) {
|
|
317
|
+
return modeled;
|
|
318
|
+
}
|
|
319
|
+
return { windowSeconds: 30, arrivalsPerMin: 0, queriesPerMin: 0, directParticipants: realDirectParticipants, childCohortCount: 0 };
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/** Build a one-shot `QueryV1` against the tier-0 cohort and serve it from the real cohort store. */
|
|
323
|
+
async query(requesterIndex: number, kind: MatchTopicKind, label: string, filter?: CapabilityFilter, opts?: { includeSeekers?: boolean; limit?: number }): Promise<QueryReplyV1> {
|
|
324
|
+
const topicId = this.topicId(kind, label);
|
|
325
|
+
return this.queryCohort(topicId, this.members[requesterIndex]!, filter, opts);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
/** Serve a `QueryV1` from the topic's tier-0 cohort store (real records, real primary signature). */
|
|
329
|
+
private async queryCohort(topicId: Uint8Array, requester: Member, filter?: CapabilityFilter, opts?: { includeSeekers?: boolean; limit?: number }): Promise<QueryReplyV1> {
|
|
330
|
+
const { engine, signer } = this.rootEngine(topicId);
|
|
331
|
+
const records = engine.records(topicId);
|
|
332
|
+
const includeSeekers = opts?.includeSeekers ?? false;
|
|
333
|
+
const query: QueryV1 = {
|
|
334
|
+
v: 1,
|
|
335
|
+
topicId: bytesToB64url(topicId),
|
|
336
|
+
includeProviders: true,
|
|
337
|
+
includeSeekers,
|
|
338
|
+
limit: opts?.limit ?? 256,
|
|
339
|
+
requesterId: requester.idStr,
|
|
340
|
+
timestamp: Date.now(),
|
|
341
|
+
signature: 'AA',
|
|
342
|
+
...(filter !== undefined ? { filter } : {}),
|
|
343
|
+
};
|
|
344
|
+
const directParticipants = engine.records(topicId).filter((r) => r.appState !== undefined).length;
|
|
345
|
+
return handleMatchmakingQuery(query, {
|
|
346
|
+
records,
|
|
347
|
+
topicTraffic: this.trafficFor(topicId, directParticipants),
|
|
348
|
+
cohortEpoch: engine.cohort().cohortEpoch,
|
|
349
|
+
sign: signer,
|
|
350
|
+
});
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/** Build a signed seeker `RegisterV1` at `treeTier` carrying the seeker app payload (op-tier T2). */
|
|
354
|
+
private async buildSeekerRegister(seeker: Member, topicId: Uint8Array, treeTier: number, appPayload: Uint8Array): Promise<RegisterV1> {
|
|
355
|
+
const body: Omit<RegisterV1, 'signature'> = {
|
|
356
|
+
v: 1,
|
|
357
|
+
topicId: bytesToB64url(topicId),
|
|
358
|
+
tier: Tier.T2,
|
|
359
|
+
treeTier,
|
|
360
|
+
participantCoord: bytesToB64url(seeker.bytes),
|
|
361
|
+
ttl: 10_000,
|
|
362
|
+
// A tier-0 probe is the cold-root instantiation (bootstrap); a tier-`d>0` probe is a plain walk
|
|
363
|
+
// step that falls through `NoState` on a cohort that does not serve the topic.
|
|
364
|
+
bootstrap: treeTier === 0,
|
|
365
|
+
timestamp: Date.now(),
|
|
366
|
+
correlationId: bytesToB64url(new TextEncoder().encode(`mm-seek-${this.corr++}`)),
|
|
367
|
+
appPayload: bytesToB64url(appPayload),
|
|
368
|
+
};
|
|
369
|
+
return { ...body, signature: bytesToB64url(await signPeer(seeker.key, registerSigningPayload(body))) };
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
/** Map a real `RegisterReplyV1` probe to the seeker walk's {@link SeekerProbeReply}, modeling the regime. */
|
|
373
|
+
private toProbeReply(topicId: Uint8Array, reply: { result: SeekerProbeReply['result']; topicTraffic?: TopicTrafficV1; targetTier?: number }): SeekerProbeReply {
|
|
374
|
+
const out: { result: SeekerProbeReply['result']; topicTraffic?: TopicTrafficV1; targetTier?: number } = { result: reply.result };
|
|
375
|
+
if (reply.result === 'accepted' || reply.result === 'promoted') {
|
|
376
|
+
const real = reply.topicTraffic;
|
|
377
|
+
out.topicTraffic = this.traffic.get(bytesToB64url(topicId)) ?? real ?? { windowSeconds: 30, arrivalsPerMin: 0, queriesPerMin: 0, directParticipants: 0, childCohortCount: 0 };
|
|
378
|
+
}
|
|
379
|
+
if (reply.targetTier !== undefined) {
|
|
380
|
+
out.targetTier = reply.targetTier;
|
|
381
|
+
}
|
|
382
|
+
return out as SeekerProbeReply;
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/** The walk transport: real per-tier register probes + real cohort queries over the mesh. */
|
|
386
|
+
private buildWalkTransport(topicId: Uint8Array, seeker: Member, wantCount: number, filter: CapabilityFilter | undefined, tiersVisited: Set<number>): SeekerWalkTransport {
|
|
387
|
+
const seekerState = new MatchmakingSeeker({
|
|
388
|
+
topicId,
|
|
389
|
+
wantCount,
|
|
390
|
+
contactHint: `/ip4/127.0.0.1/tcp/4002/p2p/${seeker.idStr}`,
|
|
391
|
+
sign: async (payload: Uint8Array): Promise<string> => bytesToB64url(await signPeer(seeker.key, payload)),
|
|
392
|
+
...(filter !== undefined ? { filter } : {}),
|
|
393
|
+
});
|
|
394
|
+
return {
|
|
395
|
+
register: async (treeTier: number): Promise<SeekerProbeReply> => {
|
|
396
|
+
tiersVisited.add(treeTier);
|
|
397
|
+
const coord = treeTier === 0 ? addressing.coord0(topicId) : addressing.coord(treeTier, seeker.bytes, topicId);
|
|
398
|
+
const target = this.mesh.nodeNearest(coord);
|
|
399
|
+
const engine = target.host.registry.forCoord(coord, treeTier as Tier, seeker.bytes);
|
|
400
|
+
const now = Date.now();
|
|
401
|
+
const reg = await this.buildSeekerRegister(seeker, topicId, treeTier, await seekerState.appPayloadBytes());
|
|
402
|
+
const parentCoord = treeTier > 0 ? addressing.coord(treeTier - 1, seeker.bytes, topicId) : undefined;
|
|
403
|
+
const reply = await engine.engine.handleRegister(reg, { followOn: false, treeTier, ...(parentCoord !== undefined ? { parentCoord } : {}) }, now);
|
|
404
|
+
return this.toProbeReply(topicId, reply);
|
|
405
|
+
},
|
|
406
|
+
query: async (_treeTier: number): Promise<QueryReplyV1> => this.queryCohort(topicId, seeker, filter),
|
|
407
|
+
renew: async (): Promise<void> => {
|
|
408
|
+
// The hang-out keep-alive: a real seeker would renew its registration; the record already
|
|
409
|
+
// lives in the cohort store for the seek's virtual duration, so this is a no-op touch.
|
|
410
|
+
},
|
|
411
|
+
withdraw: async (): Promise<void> => {
|
|
412
|
+
// Polite escalation withdrawal (`RenewV1` TTL = 0) is an optimization; the brief seeker record
|
|
413
|
+
// ages out by TTL. Single-tier-0 walks reach the root and never escalate past it.
|
|
414
|
+
},
|
|
415
|
+
};
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
/**
|
|
419
|
+
* Run the real seeker hang-out walk for node `seekerIndex` at `(kind, label)`. The walk uses a virtual
|
|
420
|
+
* clock (instant, deterministic); the hang-out decision is driven by the modeled {@link setTraffic}
|
|
421
|
+
* regime over the real cohort query results.
|
|
422
|
+
*
|
|
423
|
+
* Pick a seeker node that is **not** a provider at this topic — a seeker register and a provider
|
|
424
|
+
* register from the same node collide on `(topicId, participantId)` in the cohort store.
|
|
425
|
+
*/
|
|
426
|
+
async seek(seekerIndex: number, kind: MatchTopicKind, label: string, wantCount: number, opts: SeekOptions = {}): Promise<MatchResult> {
|
|
427
|
+
const seeker = this.members[seekerIndex]!;
|
|
428
|
+
const topicId = this.topicId(kind, label);
|
|
429
|
+
const dMax = opts.dMax ?? this.dMax;
|
|
430
|
+
const patienceMs = opts.patienceMs ?? 10_000;
|
|
431
|
+
const vt = virtualClock();
|
|
432
|
+
const tiersVisited = new Set<number>();
|
|
433
|
+
const client = new SeekerWalkClient({
|
|
434
|
+
transport: this.buildWalkTransport(topicId, seeker, wantCount, opts.filter, tiersVisited),
|
|
435
|
+
topicId,
|
|
436
|
+
wantCount,
|
|
437
|
+
dMax,
|
|
438
|
+
patienceMs,
|
|
439
|
+
verifyEntry: this.verifyEntry,
|
|
440
|
+
clock: vt.clock,
|
|
441
|
+
sleep: vt.sleep,
|
|
442
|
+
...(opts.filter !== undefined ? { filter: opts.filter } : {}),
|
|
443
|
+
...(opts.requeryIntervalMs !== undefined ? { config: { contentionFactorCap: 4.0, requeryIntervalMs: opts.requeryIntervalMs } } : {}),
|
|
444
|
+
});
|
|
445
|
+
const result = await client.run();
|
|
446
|
+
return {
|
|
447
|
+
providers: result.providers,
|
|
448
|
+
metWantCount: result.metWantCount,
|
|
449
|
+
terminalTier: result.terminalTier,
|
|
450
|
+
hops: result.hops,
|
|
451
|
+
maxChildCohortCount: result.maxChildCohortCount,
|
|
452
|
+
tiersVisited: tiersVisited.size,
|
|
453
|
+
hungOutMs: vt.slept(),
|
|
454
|
+
};
|
|
455
|
+
}
|
|
456
|
+
|
|
457
|
+
/** Project a {@link MatchResult}'s walk trace (`docs/matchmaking.md` §Worked example trace). */
|
|
458
|
+
walkTrace(r: MatchResult): { tiersVisited: number; hungOutMs: number; matched: number } {
|
|
459
|
+
return { tiersVisited: r.tiersVisited, hungOutMs: r.hungOutMs, matched: r.providers.length };
|
|
460
|
+
}
|
|
461
|
+
|
|
462
|
+
/** Re-validate a forwarded provider entry the way a seeker does (real `registrationSig` check). */
|
|
463
|
+
verifyEntryFor(topicId: Uint8Array, entry: ProviderEntryV1): boolean {
|
|
464
|
+
return verifyProviderEntry(topicId, entry, this.verifyEntry);
|
|
465
|
+
}
|
|
466
|
+
|
|
467
|
+
async stop(): Promise<void> {
|
|
468
|
+
await this.mesh.stop();
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
/** Build and start a matchmaking integration mesh. */
|
|
473
|
+
export async function buildMatchmakingMesh(opts: MatchmakingMeshOptions = {}): Promise<MatchmakingMesh> {
|
|
474
|
+
return MatchmakingMesh.build(opts);
|
|
475
|
+
}
|
|
@@ -5,7 +5,7 @@ import type { IPeerNetwork } from '@optimystic/db-core';
|
|
|
5
5
|
import { NetworkTransactor } from '@optimystic/db-core';
|
|
6
6
|
import { peerIdFromPrivateKey } from '@libp2p/peer-id';
|
|
7
7
|
import { generateKeyPair } from '@libp2p/crypto/keys';
|
|
8
|
-
import { ClusterMember, clusterMember } from '../cluster/cluster-repo.js';
|
|
8
|
+
import { ClusterMember, clusterMember, type ReconcileBlockCallback } from '../cluster/cluster-repo.js';
|
|
9
9
|
import { StorageRepo } from '../storage/storage-repo.js';
|
|
10
10
|
import { MemoryRawStorage } from '../storage/memory-storage.js';
|
|
11
11
|
import { BlockStorage } from '../storage/block-storage.js';
|
|
@@ -149,12 +149,32 @@ export async function createMesh(nodeCount: number, options: MeshOptions): Promi
|
|
|
149
149
|
partitionDetectionWindow: 60000
|
|
150
150
|
};
|
|
151
151
|
|
|
152
|
+
// Active reconciliation simulation: when a member commits a block it never
|
|
153
|
+
// pended (cohort drift), pull the committed revision from a sibling cohort node
|
|
154
|
+
// that holds it and persist it locally — the mesh analogue of the SyncClient
|
|
155
|
+
// fetch + saveReplicatedBlock path wired in `libp2p-node-base`. `nodes` is captured
|
|
156
|
+
// by reference and fully populated by the time the callback is actually invoked.
|
|
157
|
+
const reconcileBlock: ReconcileBlockCallback = async (blockId, committed, cohortPeerIds) => {
|
|
158
|
+
for (const peerIdStr of cohortPeerIds) {
|
|
159
|
+
if (peerIdStr === peerId.toString()) continue;
|
|
160
|
+
const target = nodes.find(n => n.peerId.toString() === peerIdStr);
|
|
161
|
+
if (!target) continue;
|
|
162
|
+
const result = await target.storageRepo.get({ blockIds: [blockId] }, { skipClusterFetch: true } as any);
|
|
163
|
+
const entry = result[blockId];
|
|
164
|
+
const latest = entry?.state?.latest;
|
|
165
|
+
if (!latest || !entry?.block || latest.rev < committed.rev) continue;
|
|
166
|
+
await storageRepo.saveReplicatedBlock(blockId, entry.block, latest);
|
|
167
|
+
return;
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
|
|
152
171
|
const member = clusterMember({
|
|
153
172
|
storageRepo,
|
|
154
173
|
peerNetwork,
|
|
155
174
|
peerId,
|
|
156
175
|
privateKey,
|
|
157
|
-
consensusConfig
|
|
176
|
+
consensusConfig,
|
|
177
|
+
reconcileBlock
|
|
158
178
|
});
|
|
159
179
|
|
|
160
180
|
nodes.push({
|