@optimystic/db-p2p 0.13.5 → 0.14.1
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,308 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity recover RPC transport — the live libp2p backfill/resume recovery protocol
|
|
3
|
+
* (`docs/reactivity.md` §Backfill RPC, §Resume).
|
|
4
|
+
*
|
|
5
|
+
* This is the **pull** companion to the one-way notify transport: a subscriber that detected a gap (or
|
|
6
|
+
* woke from sleep) asks a serving cohort member "what did I miss?" and is brought current. The protocol is
|
|
7
|
+
* **request-reply** — one {@link RecoverRequestV1} frame out, one {@link RecoverReplyV1} frame back over a
|
|
8
|
+
* single stream — so it rides the cohort-topic {@link requestResponse} / {@link handleRequestResponse}
|
|
9
|
+
* helpers (NOT the one-way notify helpers, which would desync the dialer).
|
|
10
|
+
*
|
|
11
|
+
* Two pieces live here, both decoupled from the live node assembly so they unit-test in isolation:
|
|
12
|
+
*
|
|
13
|
+
* - {@link Libp2pReactivityRecoverTransport} — the **outbound** side. Supplies the two db-core seams
|
|
14
|
+
* (`BackfillTransport` / `ResumeTransport`) against one node: pick a target (sticky cohort-hint primary
|
|
15
|
+
* first for the one-RT happy path, else a cohort-walk member), frame the request, exchange it over the
|
|
16
|
+
* recover protocol, and return the inner reply. The wire exchange is injected as a {@link RecoverDialer}
|
|
17
|
+
* so the target-selection + framing logic tests without a real socket; {@link createLibp2pRecoverDialer}
|
|
18
|
+
* is the production libp2p-backed implementation.
|
|
19
|
+
* - {@link createRecoverRequestHandler} / {@link registerRecoverHandler} — the **inbound** serve handler.
|
|
20
|
+
* Decode (bounded) → verify the request's peer-key signature against the dialing peer → reject a
|
|
21
|
+
* replay/stale request → resolve the live `PushState` → `serveBackfill` / `serveResume` → reply. Any
|
|
22
|
+
* failure produces **no reply** (the stream aborts) rather than throwing out of the handler.
|
|
23
|
+
*
|
|
24
|
+
* ## Dial-target encoding bridge (load-bearing)
|
|
25
|
+
*
|
|
26
|
+
* The sticky {@link import("@optimystic/db-core").ReactivityCohortHint}`.primary` is **base64url of
|
|
27
|
+
* cohort member-id bytes** (`base64url(utf8(peerIdString))`), per its db-core JSDoc, whereas
|
|
28
|
+
* {@link requestResponse} needs a `peerIdFromString`-parseable peer-id **string**. Feeding the raw
|
|
29
|
+
* base64url straight to `peerIdFromString` throws → the dial is swallowed → recovery silently never
|
|
30
|
+
* reaches the cohort. {@link decodeCohortHintTarget} pins the conversion
|
|
31
|
+
* (`bytesToPeerIdString(b64urlToBytes(primary))`); the `resolveCohort` walk, by contrast, already returns
|
|
32
|
+
* peer-id strings (the cohort-topic `CohortPeerResolver` space) and is used as-is.
|
|
33
|
+
*/
|
|
34
|
+
import { peerIdFromString } from "@libp2p/peer-id";
|
|
35
|
+
import { b64urlToBytes, bytesToB64url, reactivityTopicId, serveBackfill, serveResume, backfillSigningPayload, resumeSigningPayload, encodeRecoverRequestV1, decodeRecoverRequestV1, encodeRecoverReplyV1, decodeRecoverReplyV1, } from "@optimystic/db-core";
|
|
36
|
+
import { requestResponse, handleRequestResponse, DEFAULT_STREAM_MAX_BYTES } from "../cohort-topic/stream-util.js";
|
|
37
|
+
import { verifyPeerSig, signPeerSig } from "../cohort-topic/peer-sig.js";
|
|
38
|
+
import { peerIdToBytes, bytesToPeerIdString } from "../cohort-topic/peer-codec.js";
|
|
39
|
+
import { PROTOCOL_REACTIVITY_RECOVER } from "./protocols.js";
|
|
40
|
+
import { createLogger } from "../logger.js";
|
|
41
|
+
const log = createLogger("reactivity-recover");
|
|
42
|
+
/**
|
|
43
|
+
* Decode a sticky-cohort-hint `primary` (base64url of member-id bytes = `base64url(utf8(peerIdString))`)
|
|
44
|
+
* back to the dialable peer-id string. The inverse trap of feeding the raw value to `peerIdFromString`
|
|
45
|
+
* (which expects a peer-id string, not base64url-of-its-utf8) — pinned by the dial-target encoding test.
|
|
46
|
+
*/
|
|
47
|
+
export function decodeCohortHintTarget(primary) {
|
|
48
|
+
return bytesToPeerIdString(b64urlToBytes(primary));
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Build the subscriber's recover request signers from the node's libp2p Ed25519 private key.
|
|
52
|
+
*
|
|
53
|
+
* This resolves the recover wiring's one design point: the subscription-manager `signBackfill` /
|
|
54
|
+
* `signResume` ports are **synchronous** (`(unsigned) => string`) — the db-core driver builds the
|
|
55
|
+
* unsigned image internally, so a pre-signed value is impossible — but libp2p `PrivateKey.sign` is
|
|
56
|
+
* **async**. Rather than make the seam (and `createBackfillRequester`'s `sign`, a db-core change) async,
|
|
57
|
+
* sign with the synchronous {@link signPeerSig} (noble, over the raw Ed25519 seed) over db-core's
|
|
58
|
+
* canonical signing payloads. The produced signature verifies on the serving side under
|
|
59
|
+
* {@link verifyPeerSig} over the same bytes (both noble, RFC8032), so the round trip is symmetric.
|
|
60
|
+
*/
|
|
61
|
+
export function createRecoverRequestSigners(privateKey) {
|
|
62
|
+
return {
|
|
63
|
+
signBackfill: (unsigned) => bytesToB64url(signPeerSig(privateKey, backfillSigningPayload(unsigned))),
|
|
64
|
+
signResume: (unsigned) => bytesToB64url(signPeerSig(privateKey, resumeSigningPayload(unsigned))),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
// --- outbound transport ---
|
|
68
|
+
/**
|
|
69
|
+
* Thrown out of {@link Libp2pReactivityRecoverTransport.backfillTransport} / `resumeTransport` when the
|
|
70
|
+
* dialed cohort answered with a `kind: "rotated"` recover reply: the outgoing tail this request reached has
|
|
71
|
+
* rotated and is draining, so it bounced the request to the new tree (`docs/reactivity.md` §Tail rotation).
|
|
72
|
+
* It carries the {@link RotationRedirectV1} so the subscription manager can move itself to the successor.
|
|
73
|
+
*
|
|
74
|
+
* A `kind: "rotated"` reply is **terminal** for the cohort-walk — the dialed member spoke authoritatively for
|
|
75
|
+
* the cohort, so the transport stops rather than falling through to the next target (contrast a *dial
|
|
76
|
+
* failure*, which still falls through). The subscriber honors it like a notification-driven rotation.
|
|
77
|
+
*/
|
|
78
|
+
export class RotationRedirectError extends Error {
|
|
79
|
+
/** The drain-window redirect the serving cohort returned (`newTailId` / `newTopicId` / `effectiveAtRevision`). */
|
|
80
|
+
redirect;
|
|
81
|
+
constructor(redirect) {
|
|
82
|
+
super(`reactivity recover: cohort rotated to tail ${redirect.newTailId} (effectiveAtRevision ${redirect.effectiveAtRevision})`);
|
|
83
|
+
this.name = "RotationRedirectError";
|
|
84
|
+
this.redirect = redirect;
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
/** Build the production libp2p-backed {@link RecoverDialer} over {@link requestResponse}. */
|
|
88
|
+
export function createLibp2pRecoverDialer(node, recoverProtocol = PROTOCOL_REACTIVITY_RECOVER, maxBytes = DEFAULT_STREAM_MAX_BYTES) {
|
|
89
|
+
return {
|
|
90
|
+
exchange: (target, frame) => requestResponse(node, peerIdFromString(target), recoverProtocol, frame, maxBytes),
|
|
91
|
+
};
|
|
92
|
+
}
|
|
93
|
+
/**
|
|
94
|
+
* The outbound recover transport: one instance per node, exposing the two db-core function seams against it.
|
|
95
|
+
* Each returned transport dials the **sticky primary first** (one round trip after a brief flap), falling
|
|
96
|
+
* back to a **cohort-walk** member on a dial failure (any member that holds the topic's gossiped `PushState`
|
|
97
|
+
* can answer). A kind mismatch or all-targets-failed surfaces as a rejection so the caller's retry/escalation
|
|
98
|
+
* policy (the subscription manager's backfill escalation, or `manager.resume()`'s caller) takes over.
|
|
99
|
+
*/
|
|
100
|
+
export class Libp2pReactivityRecoverTransport {
|
|
101
|
+
dialer;
|
|
102
|
+
selfPeerId;
|
|
103
|
+
cohortHintCache;
|
|
104
|
+
resolveCohort;
|
|
105
|
+
maxBytes;
|
|
106
|
+
constructor(options) {
|
|
107
|
+
this.dialer = options.dialer;
|
|
108
|
+
this.selfPeerId = options.selfPeerId;
|
|
109
|
+
this.cohortHintCache = options.cohortHintCache;
|
|
110
|
+
this.resolveCohort = options.resolveCohort;
|
|
111
|
+
this.maxBytes = options.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
112
|
+
}
|
|
113
|
+
/** The db-core {@link BackfillTransport} for `(topicId, collectionId)` — frames + dials a signed {@link BackfillV1}. */
|
|
114
|
+
backfillTransport(topicId, collectionId) {
|
|
115
|
+
return async (req) => {
|
|
116
|
+
const frame = encodeRecoverRequestV1({ v: 1, kind: "backfill", backfill: req }, this.maxBytes);
|
|
117
|
+
const reply = await this.exchange("backfill", frame, topicId, collectionId);
|
|
118
|
+
if (reply.backfillReply === undefined) {
|
|
119
|
+
throw new Error("reactivity recover: backfill reply missing its body");
|
|
120
|
+
}
|
|
121
|
+
return reply.backfillReply;
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
/** The {@link ResumeTransport} for `(topicId, collectionId)` — frames + dials a signed {@link ResumeV1}. */
|
|
125
|
+
resumeTransport(topicId, collectionId) {
|
|
126
|
+
return async (req) => {
|
|
127
|
+
const frame = encodeRecoverRequestV1({ v: 1, kind: "resume", resume: req }, this.maxBytes);
|
|
128
|
+
const reply = await this.exchange("resume", frame, topicId, collectionId);
|
|
129
|
+
if (reply.resumeReply === undefined) {
|
|
130
|
+
throw new Error("reactivity recover: resume reply missing its body");
|
|
131
|
+
}
|
|
132
|
+
return reply.resumeReply;
|
|
133
|
+
};
|
|
134
|
+
}
|
|
135
|
+
/**
|
|
136
|
+
* Exchange one recover frame with the first reachable target: sticky primary, then each cohort-walk
|
|
137
|
+
* member. A **dial failure** falls through to the next candidate; a successful dial is **terminal** (the
|
|
138
|
+
* member answered for the cohort) — a `kind: "rotated"` redirect throws {@link RotationRedirectError}, and
|
|
139
|
+
* a reply decoding to the wrong `kind` is a protocol error. Throws when no candidate succeeds.
|
|
140
|
+
*/
|
|
141
|
+
async exchange(kind, frame, topicId, collectionId) {
|
|
142
|
+
const targets = this.selectTargets(topicId, collectionId);
|
|
143
|
+
if (targets.length === 0) {
|
|
144
|
+
throw new Error("reactivity recover: no serving cohort target resolved");
|
|
145
|
+
}
|
|
146
|
+
let lastErr;
|
|
147
|
+
for (const target of targets) {
|
|
148
|
+
let replyFrame;
|
|
149
|
+
try {
|
|
150
|
+
replyFrame = await this.dialer.exchange(target, frame);
|
|
151
|
+
}
|
|
152
|
+
catch (err) {
|
|
153
|
+
lastErr = err; // dial failure → fall back to the next candidate (sticky → walk)
|
|
154
|
+
log("recover dial to %s failed, trying next target: %o", target, err);
|
|
155
|
+
continue;
|
|
156
|
+
}
|
|
157
|
+
const reply = decodeRecoverReplyV1(replyFrame, this.maxBytes); // a decode failure here is terminal
|
|
158
|
+
// A rotated cohort answered authoritatively: surface the redirect and stop the walk (terminal, never
|
|
159
|
+
// a fallthrough — the dialed member spoke for the cohort).
|
|
160
|
+
if (reply.kind === "rotated") {
|
|
161
|
+
throw new RotationRedirectError(reply.rotated);
|
|
162
|
+
}
|
|
163
|
+
if (reply.kind !== kind) {
|
|
164
|
+
throw new Error(`reactivity recover: reply kind "${reply.kind}" does not match request kind "${kind}"`);
|
|
165
|
+
}
|
|
166
|
+
return reply;
|
|
167
|
+
}
|
|
168
|
+
throw lastErr ?? new Error("reactivity recover: all targets failed");
|
|
169
|
+
}
|
|
170
|
+
/**
|
|
171
|
+
* Ordered dial candidates for a recover: the sticky primary (decoded from its base64url-of-bytes form)
|
|
172
|
+
* first, then the cohort-walk members. Self is never dialed (a co-located serve is the node wiring's
|
|
173
|
+
* concern), and duplicates collapse.
|
|
174
|
+
*/
|
|
175
|
+
selectTargets(topicId, collectionId) {
|
|
176
|
+
const seen = new Set();
|
|
177
|
+
const targets = [];
|
|
178
|
+
const add = (target) => {
|
|
179
|
+
if (target === undefined || target === this.selfPeerId || seen.has(target)) {
|
|
180
|
+
return;
|
|
181
|
+
}
|
|
182
|
+
seen.add(target);
|
|
183
|
+
targets.push(target);
|
|
184
|
+
};
|
|
185
|
+
const hint = this.cohortHintCache.get(collectionId);
|
|
186
|
+
if (hint?.primary !== undefined) {
|
|
187
|
+
try {
|
|
188
|
+
add(decodeCohortHintTarget(hint.primary));
|
|
189
|
+
}
|
|
190
|
+
catch (err) {
|
|
191
|
+
log("recover: malformed sticky primary for %s, skipping to cohort-walk: %o", collectionId, err);
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
for (const member of this.resolveCohort(topicId)) {
|
|
195
|
+
add(member);
|
|
196
|
+
}
|
|
197
|
+
return targets;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
/**
|
|
201
|
+
* Verify the request's peer-key signature against the dialing peer, then admit it through the freshness +
|
|
202
|
+
* anti-replay guard. The dialing peer's id **is** the signer (no signer-id field on the wire); the signature
|
|
203
|
+
* bytes are the anti-replay key (globally unique + authenticated). Returns `false` (reject, no reply) on a
|
|
204
|
+
* bad signature, a stale/future timestamp, or a replay.
|
|
205
|
+
*/
|
|
206
|
+
function verifyAndAdmit(deps, signerId, payload, signatureB64, timestamp, now) {
|
|
207
|
+
const signature = b64urlToBytes(signatureB64);
|
|
208
|
+
if (!verifyPeerSig(signerId, payload, signature)) {
|
|
209
|
+
log("recover serve: signature verification failed for %s (no reply)", signerId);
|
|
210
|
+
return false;
|
|
211
|
+
}
|
|
212
|
+
if (!deps.replayGuard.accept(signature, peerIdToBytes(signerId), timestamp, now)) {
|
|
213
|
+
log("recover serve: replay/stale request from %s (no reply)", signerId);
|
|
214
|
+
return false;
|
|
215
|
+
}
|
|
216
|
+
return true;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* Serve a backfill from the collection's current served tail, or `undefined` if this node serves none. When
|
|
220
|
+
* the node serves **only** the outgoing (draining) tail, `rotationFor` returns the drain redirect and the
|
|
221
|
+
* reply is `kind: "rotated"` instead — a best-effort secondary path (the primary mechanism for an active
|
|
222
|
+
* subscriber is notify-driven detection). Once both tails coexist `pushStateForCollection` resolves the new
|
|
223
|
+
* tail, so the redirect is emitted only while the old tail is the sole served state (see §Tail rotation).
|
|
224
|
+
*/
|
|
225
|
+
function serveBackfillReply(deps, req, now) {
|
|
226
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
227
|
+
const redirect = deps.rotationFor?.({ collectionId: req.collectionId }, now);
|
|
228
|
+
if (redirect !== undefined) {
|
|
229
|
+
return encodeRecoverReplyV1({ v: 1, kind: "rotated", rotated: redirect }, maxBytes);
|
|
230
|
+
}
|
|
231
|
+
const ps = deps.pushStateForCollection(req.collectionId);
|
|
232
|
+
if (ps === undefined) {
|
|
233
|
+
return undefined; // not a serving member for this collection → no reply (subscriber walks/chain-reads)
|
|
234
|
+
}
|
|
235
|
+
const backfillReply = serveBackfill(ps.replayBuffer, req, ps.collectionId);
|
|
236
|
+
return encodeRecoverReplyV1({ v: 1, kind: "backfill", backfillReply }, maxBytes);
|
|
237
|
+
}
|
|
238
|
+
/**
|
|
239
|
+
* Serve a resume against the live `PushState`. A resume reaching the **outgoing (draining)** tail — its
|
|
240
|
+
* `latestKnownTailId` anchors a topic this node has marked rotated — is answered with the drain redirect
|
|
241
|
+
* (`kind: "rotated"`), moving the subscriber to the new tree. Otherwise prefer the exact topic the request's
|
|
242
|
+
* `latestKnownTailId` anchors (so a non-rotated subscriber classifies into backfill/checkpoint/out_of_window);
|
|
243
|
+
* if this node no longer serves that tail's topic, fall back to the collection's current tail so the cohort
|
|
244
|
+
* can still answer `tail_rotated` (its `currentTailId` differs from the request's stale tail) or, for a span
|
|
245
|
+
* that crosses a rotation, serve from the new tail's `inheritedCheckpoint`. `undefined` ⇒ no served state.
|
|
246
|
+
*/
|
|
247
|
+
function serveResumeReply(deps, req, now) {
|
|
248
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
249
|
+
const staleTopic = reactivityTopicId(b64urlToBytes(req.latestKnownTailId));
|
|
250
|
+
const redirect = deps.rotationFor?.({ topicId: staleTopic, collectionId: req.collectionId }, now);
|
|
251
|
+
if (redirect !== undefined) {
|
|
252
|
+
return encodeRecoverReplyV1({ v: 1, kind: "rotated", rotated: redirect }, maxBytes);
|
|
253
|
+
}
|
|
254
|
+
const ps = deps.pushStateFor(staleTopic) ?? deps.pushStateForCollection(req.collectionId);
|
|
255
|
+
if (ps === undefined) {
|
|
256
|
+
return undefined;
|
|
257
|
+
}
|
|
258
|
+
const resumeReply = serveResume(req, {
|
|
259
|
+
buffer: ps.replayBuffer,
|
|
260
|
+
checkpoint: ps.checkpoint,
|
|
261
|
+
inheritedCheckpoint: ps.inheritedCheckpoint,
|
|
262
|
+
currentTailId: ps.tailIdAtJoin,
|
|
263
|
+
currentRevision: ps.lastRevision,
|
|
264
|
+
rotationRevision: ps.lastRevision,
|
|
265
|
+
expectedCollectionId: ps.collectionId,
|
|
266
|
+
});
|
|
267
|
+
return encodeRecoverReplyV1({ v: 1, kind: "resume", resumeReply }, maxBytes);
|
|
268
|
+
}
|
|
269
|
+
/**
|
|
270
|
+
* Build the recover serve callback for {@link handleRequestResponse}: it returns the reply frame, or
|
|
271
|
+
* `undefined` for **no reply** (a decode/verify/replay/resolve failure aborts the stream). It never throws
|
|
272
|
+
* out of the handler.
|
|
273
|
+
*/
|
|
274
|
+
export function createRecoverRequestHandler(deps) {
|
|
275
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
276
|
+
const clock = deps.clock ?? (() => Date.now());
|
|
277
|
+
return (frame, fromPeer) => {
|
|
278
|
+
try {
|
|
279
|
+
const req = decodeRecoverRequestV1(frame, maxBytes);
|
|
280
|
+
const signerId = fromPeer.toString();
|
|
281
|
+
const now = clock();
|
|
282
|
+
if (req.kind === "backfill") {
|
|
283
|
+
const b = req.backfill;
|
|
284
|
+
if (b === undefined || !verifyAndAdmit(deps, signerId, backfillSigningPayload(b), b.signature, b.timestamp, now)) {
|
|
285
|
+
return Promise.resolve(undefined);
|
|
286
|
+
}
|
|
287
|
+
return Promise.resolve(serveBackfillReply(deps, b, now));
|
|
288
|
+
}
|
|
289
|
+
const r = req.resume;
|
|
290
|
+
if (r === undefined || !verifyAndAdmit(deps, signerId, resumeSigningPayload(r), r.signature, r.timestamp, now)) {
|
|
291
|
+
return Promise.resolve(undefined);
|
|
292
|
+
}
|
|
293
|
+
return Promise.resolve(serveResumeReply(deps, r, now));
|
|
294
|
+
}
|
|
295
|
+
catch (err) {
|
|
296
|
+
// A malformed/foreign request (decode failure, foreign collectionId from serve*) must never throw
|
|
297
|
+
// out of the stream handler: log + no reply (the stream aborts, the subscriber falls back).
|
|
298
|
+
log("recover serve: dropping request (no reply): %o", err);
|
|
299
|
+
return Promise.resolve(undefined);
|
|
300
|
+
}
|
|
301
|
+
};
|
|
302
|
+
}
|
|
303
|
+
/** Register the inbound recover protocol handler on `node` (request-reply over the recover protocol). */
|
|
304
|
+
export function registerRecoverHandler(node, protocol, deps) {
|
|
305
|
+
const maxBytes = deps.maxBytes ?? DEFAULT_STREAM_MAX_BYTES;
|
|
306
|
+
handleRequestResponse(node, protocol, createRecoverRequestHandler(deps), maxBytes);
|
|
307
|
+
}
|
|
308
|
+
//# sourceMappingURL=recover-transport.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"recover-transport.js","sourceRoot":"","sources":["../../../src/reactivity/recover-transport.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,142 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — tail-rotation re-registration timer host seam (`docs/reactivity.md` §Tail rotation step 3).
|
|
3
|
+
*
|
|
4
|
+
* The {@link import("./subscription-manager.js").ReactivitySubscriptionManager} surfaces a
|
|
5
|
+
* {@link import("./subscription-manager.js").RotationNotice} **once per successor tail** (both the
|
|
6
|
+
* notify-driven pre-announce and the recover-driven `RotationRedirectError` converge on the same
|
|
7
|
+
* `surfaceRotation` seam), but it neither schedules the timer nor performs the move. This host component
|
|
8
|
+
* does: it consumes a notice, schedules a one-shot timer for `max(0, plan.fireAt - now())`, and on fire
|
|
9
|
+
* invokes an injected `reRegister(plan)` seam that re-subscribes the watcher at the rotated tree. The node
|
|
10
|
+
* composition that constructs the manager + binds this scheduler to its `onRotation` observer lives in
|
|
11
|
+
* `reactivity-rotation-host-wiring-e2e`; this module is the standalone, unit-testable piece.
|
|
12
|
+
*
|
|
13
|
+
* **Where the stagger lives.** The spread that keeps the new tail from being flooded is entirely in
|
|
14
|
+
* `plan.fireAt`, drawn by the **manager's** `rejoinJitter`. This scheduler only *consumes* `plan.fireAt`; it
|
|
15
|
+
* never sees the jitter. The manager uses the *single*-subscriber planner `planReRegistration` →
|
|
16
|
+
* `RejoinJitter.scheduleRejoin`, which draws a **uniform** offset `now + ⌊U[0, T_rejoin_jitter)⌋` — so the
|
|
17
|
+
* load-bearing knob for this path is the **window** `T_rejoin_jitter` (default 30 s), not a `capPromote`. Each
|
|
18
|
+
* subscriber jitters independently, giving the new tail an inbound rate of ≈ `subscribers / T_rejoin_jitter`
|
|
19
|
+
* in expectation; the burst is absorbed on the *receiving* side by the new tail cohort's `cap_promote_fast`
|
|
20
|
+
* fast-promotion (a cohort-topic promotion mechanism — see `docs/reactivity.md` §Tail rotation rotation-cost
|
|
21
|
+
* and the Worked scenario), which is independent of the jitter's `capPromote`.
|
|
22
|
+
*
|
|
23
|
+
* Note that `RejoinJitter`'s `capPromote` is consulted **only** by the *wave* planner
|
|
24
|
+
* `scheduleWave` / `planReRegistrationWave` (a single host staggering a whole wave with a hard per-window
|
|
25
|
+
* ceiling — used by the mesh harness, not by the production manager). If the composing site
|
|
26
|
+
* (`reactivity-rotation-host-wiring-e2e`) ever switches the manager onto the wave planner, *then* it must
|
|
27
|
+
* build the jitter as `createRejoinJitter({ capPromote: DEFAULT_CAP_PROMOTE_FAST })` (= 32), since the default
|
|
28
|
+
* `createRejoinJitter()` cap is the cohort-failure `cap_promote = 64`. For the current single-planner path,
|
|
29
|
+
* setting `capPromote` has **no effect** on `fireAt`.
|
|
30
|
+
*
|
|
31
|
+
* **Determinism.** An injected `setTimer(fn, delayMs) => cancel` and `now()` clock make the scheduler
|
|
32
|
+
* testable with a fake clock (no wall clock). They default to a production binding: an **unref'd**
|
|
33
|
+
* `setTimeout`/`clearTimeout` and `Date.now`. The unref is load-bearing — an idle re-registration timer must
|
|
34
|
+
* not pin an otherwise-idle process, mirroring the push-state-gossip driver's unref'd timer
|
|
35
|
+
* ({@link import("./push-state-gossip.js").ReactivityPushStateGossipDriver}).
|
|
36
|
+
*
|
|
37
|
+
* **Idempotence.** De-duped by successor `newTopicId` (base64url): a second notice for a successor already
|
|
38
|
+
* scheduled or fired is ignored. The manager already fires once per successor (its `rotationHandledFor`
|
|
39
|
+
* guard), but a redirect and a pre-announce can surface the **same** successor near-simultaneously, so the
|
|
40
|
+
* scheduler is independently idempotent. The de-dupe ledger (`seen`) survives the timer fire (so a late
|
|
41
|
+
* re-surface of an already-fired successor stays a no-op), which on the normal schedule→fire path would let it
|
|
42
|
+
* grow without bound across a long-lived, fast-rotating subscription. It is therefore **bounded**: capped at
|
|
43
|
+
* `SEEN_LEDGER_CAP` entries with oldest-first eviction (insertion order), and eviction **never** drops a key
|
|
44
|
+
* still in `pending` — that would break the "every pending key ∈ `seen`" invariant and could let a duplicate
|
|
45
|
+
* notice for a pending successor arm a second timer over the live one. A re-surface older than the cap (≥ cap
|
|
46
|
+
* distinct successors later) degrades to at most one harmless, idempotent re-register.
|
|
47
|
+
*
|
|
48
|
+
* **Chained rotation OLD→A→B** before A's timer fires → A and B are distinct successors → two independent
|
|
49
|
+
* timers; **both may fire**. Re-registering at A and then immediately rotating A→B is self-correcting via the
|
|
50
|
+
* manager's `rotationHandledFor` guard, so a superseded timer is intentionally **not** cancelled (a
|
|
51
|
+
* superseded re-register is harmless and rare).
|
|
52
|
+
*/
|
|
53
|
+
import { type ReRegistrationPlan } from "@optimystic/db-core";
|
|
54
|
+
import type { RotationNotice } from "./subscription-manager.js";
|
|
55
|
+
/**
|
|
56
|
+
* Upper bound on the idempotence ledger (`seen`). Far above any realistic count of *concurrently-pending*
|
|
57
|
+
* successors (single digits: 1 in the normal case, a handful across a chained rotation), so it never evicts a
|
|
58
|
+
* still-live entry in practice while keeping the ledger's memory to tens of KB of short base64url strings at the
|
|
59
|
+
* ceiling. Tunable — raising it widens the late-re-surface de-dupe window at a proportional memory cost.
|
|
60
|
+
* Exported for the regression test that asserts the bound; production callers don't read it.
|
|
61
|
+
*/
|
|
62
|
+
export declare const SEEN_LEDGER_CAP = 1024;
|
|
63
|
+
/** Cancel handle returned by an injected timer; cancels a not-yet-fired timer (safe no-op after fire/cancel). */
|
|
64
|
+
export type RotationTimerCancel = () => void;
|
|
65
|
+
/** Construction inputs for a {@link RotationReRegistrationScheduler}. */
|
|
66
|
+
export interface RotationReRegistrationSchedulerOptions {
|
|
67
|
+
/**
|
|
68
|
+
* Perform the actual move: re-subscribe the watcher at the rotated tree (the new `topicId`, carrying the
|
|
69
|
+
* plan's `lastRevision`). A rejection is isolated + logged — a failed move must never throw out of the
|
|
70
|
+
* timer callback. Not retried this pass (see the class doc).
|
|
71
|
+
*/
|
|
72
|
+
readonly reRegister: (plan: ReRegistrationPlan) => Promise<void>;
|
|
73
|
+
/**
|
|
74
|
+
* Schedule a one-shot timer; returns a cancel handle. Defaults to an **unref'd** `setTimeout`. Tests inject
|
|
75
|
+
* a fake timer queue for determinism.
|
|
76
|
+
*/
|
|
77
|
+
readonly setTimer?: (fn: () => void, delayMs: number) => RotationTimerCancel;
|
|
78
|
+
/** Clock (Unix ms). Defaults to `Date.now`; tests inject a fake clock. */
|
|
79
|
+
readonly now?: () => number;
|
|
80
|
+
}
|
|
81
|
+
/**
|
|
82
|
+
* Hosts the per-successor one-shot timers that move a subscriber to the rotated tree. Construct one per
|
|
83
|
+
* subscription manager and bind {@link schedule} to the manager's
|
|
84
|
+
* {@link import("./subscription-manager.js").ReactivitySubscriptionManagerOptions.onRotation} observer.
|
|
85
|
+
*
|
|
86
|
+
* See the module doc for where the re-registration stagger actually lives (the manager's `rejoinJitter`; this
|
|
87
|
+
* scheduler only consumes `plan.fireAt`, it does not draw it).
|
|
88
|
+
*/
|
|
89
|
+
export declare class RotationReRegistrationScheduler {
|
|
90
|
+
private readonly reRegister;
|
|
91
|
+
private readonly setTimer;
|
|
92
|
+
private readonly now;
|
|
93
|
+
/** Successors with a still-pending timer, keyed by base64url `newTopicId` → its cancel handle. */
|
|
94
|
+
private readonly pending;
|
|
95
|
+
/**
|
|
96
|
+
* Successors recently scheduled (pending **or** already fired) — the idempotence ledger that survives fire.
|
|
97
|
+
* Insertion-ordered and **bounded** at {@link SEEN_LEDGER_CAP}: {@link evictSeenOverCap} drops the oldest
|
|
98
|
+
* non-pending entries once over cap (never a still-pending key — see that method's doc).
|
|
99
|
+
*/
|
|
100
|
+
private readonly seen;
|
|
101
|
+
private stopped;
|
|
102
|
+
constructor(options: RotationReRegistrationSchedulerOptions);
|
|
103
|
+
/** Successors with a timer still pending (not yet fired/cancelled). Diagnostic / test seam. */
|
|
104
|
+
get pendingCount(): number;
|
|
105
|
+
/**
|
|
106
|
+
* Size of the idempotence ledger (pending + recently-fired-and-retained). Diagnostic / test seam; the
|
|
107
|
+
* regression test asserts this stays ≤ {@link SEEN_LEDGER_CAP} under unbounded sequential rotations.
|
|
108
|
+
*/
|
|
109
|
+
get seenCount(): number;
|
|
110
|
+
/**
|
|
111
|
+
* Schedule the re-registration timer for a rotation notice. De-duped by successor `newTopicId`: a second
|
|
112
|
+
* notice for an already-scheduled-or-fired successor is a no-op (a redirect and a pre-announce can surface
|
|
113
|
+
* the same successor). The delay is `max(0, plan.fireAt - now())`, so a `fireAt` already in the past fires
|
|
114
|
+
* on the next tick (clamped to 0, never negative). A no-op once {@link stop} has run.
|
|
115
|
+
*/
|
|
116
|
+
schedule(notice: RotationNotice): void;
|
|
117
|
+
/**
|
|
118
|
+
* Cancel a single pending timer (by successor `newTopicId`) or, when called with no argument, **all**
|
|
119
|
+
* pending timers (teardown). Cancelling an already-fired or unknown successor is a safe no-op. A cancelled
|
|
120
|
+
* successor is forgotten (dropped from the idempotence ledger), so a later notice for it would reschedule;
|
|
121
|
+
* use {@link stop} for permanent teardown.
|
|
122
|
+
*/
|
|
123
|
+
cancel(newTopicId?: Uint8Array): void;
|
|
124
|
+
/**
|
|
125
|
+
* Stop the scheduler permanently (teardown): cancel every pending timer and refuse all further scheduling
|
|
126
|
+
* and firing. A timer that races `stop` (its callback already queued) finds the `stopped` gate and never
|
|
127
|
+
* invokes `reRegister`. Idempotent.
|
|
128
|
+
*/
|
|
129
|
+
stop(): void;
|
|
130
|
+
/**
|
|
131
|
+
* Bound the idempotence ledger: evict the oldest entries (insertion order) that are **not** still pending,
|
|
132
|
+
* until back within {@link SEEN_LEDGER_CAP}. Never evicts a pending key — that would break the "every pending
|
|
133
|
+
* key ∈ `seen`" invariant and could double-fire a successor whose later duplicate notice then re-armed a timer
|
|
134
|
+
* over the live one. If every over-cap entry is somehow still pending (`pending.size > cap` — not reachable in
|
|
135
|
+
* practice, since pending successors are the most recently scheduled), the loop bails and the ledger is left to
|
|
136
|
+
* grow with `pending` this round; it returns within cap as soon as those timers fire and a later schedule runs.
|
|
137
|
+
*/
|
|
138
|
+
private evictSeenOverCap;
|
|
139
|
+
/** Timer callback: drop the pending entry, then invoke the isolated re-registration (unless stopped). */
|
|
140
|
+
private fire;
|
|
141
|
+
}
|
|
142
|
+
//# sourceMappingURL=rotation-rereg-scheduler.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation-rereg-scheduler.d.ts","sourceRoot":"","sources":["../../../src/reactivity/rotation-rereg-scheduler.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAmDG;AAEH,OAAO,EAAiB,KAAK,kBAAkB,EAAE,MAAM,qBAAqB,CAAC;AAC7E,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,2BAA2B,CAAC;AAKhE;;;;;;GAMG;AACH,eAAO,MAAM,eAAe,OAAO,CAAC;AAEpC,iHAAiH;AACjH,MAAM,MAAM,mBAAmB,GAAG,MAAM,IAAI,CAAC;AAc7C,yEAAyE;AACzE,MAAM,WAAW,sCAAsC;IACtD;;;;OAIG;IACH,QAAQ,CAAC,UAAU,EAAE,CAAC,IAAI,EAAE,kBAAkB,KAAK,OAAO,CAAC,IAAI,CAAC,CAAC;IACjE;;;OAGG;IACH,QAAQ,CAAC,QAAQ,CAAC,EAAE,CAAC,EAAE,EAAE,MAAM,IAAI,EAAE,OAAO,EAAE,MAAM,KAAK,mBAAmB,CAAC;IAC7E,0EAA0E;IAC1E,QAAQ,CAAC,GAAG,CAAC,EAAE,MAAM,MAAM,CAAC;CAC5B;AAED;;;;;;;GAOG;AACH,qBAAa,+BAA+B;IAC3C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAA8C;IACzE,OAAO,CAAC,QAAQ,CAAC,QAAQ,CAA2D;IACpF,OAAO,CAAC,QAAQ,CAAC,GAAG,CAAe;IAEnC,kGAAkG;IAClG,OAAO,CAAC,QAAQ,CAAC,OAAO,CAA0C;IAClE;;;;OAIG;IACH,OAAO,CAAC,QAAQ,CAAC,IAAI,CAAqB;IAC1C,OAAO,CAAC,OAAO,CAAS;gBAEZ,OAAO,EAAE,sCAAsC;IAM3D,+FAA+F;IAC/F,IAAI,YAAY,IAAI,MAAM,CAEzB;IAED;;;OAGG;IACH,IAAI,SAAS,IAAI,MAAM,CAEtB;IAED;;;;;OAKG;IACH,QAAQ,CAAC,MAAM,EAAE,cAAc,GAAG,IAAI;IAwBtC;;;;;OAKG;IACH,MAAM,CAAC,UAAU,CAAC,EAAE,UAAU,GAAG,IAAI;IAkBrC;;;;OAIG;IACH,IAAI,IAAI,IAAI;IASZ;;;;;;;OAOG;IACH,OAAO,CAAC,gBAAgB;IAiBxB,yGAAyG;IACzG,OAAO,CAAC,IAAI;CAmBZ"}
|