@optimystic/db-p2p 0.13.5 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +6 -6
- package/dist/src/cluster/block-transfer-service.d.ts +67 -8
- package/dist/src/cluster/block-transfer-service.d.ts.map +1 -1
- package/dist/src/cluster/block-transfer-service.js +101 -62
- package/dist/src/cluster/block-transfer-service.js.map +1 -1
- package/dist/src/cluster/client.d.ts +18 -1
- package/dist/src/cluster/client.d.ts.map +1 -1
- package/dist/src/cluster/client.js +55 -39
- package/dist/src/cluster/client.js.map +1 -1
- package/dist/src/cluster/cluster-error.d.ts +38 -0
- package/dist/src/cluster/cluster-error.d.ts.map +1 -0
- package/dist/src/cluster/cluster-error.js +51 -0
- package/dist/src/cluster/cluster-error.js.map +1 -0
- package/dist/src/cluster/cluster-repo.d.ts +135 -3
- package/dist/src/cluster/cluster-repo.d.ts.map +1 -1
- package/dist/src/cluster/cluster-repo.js +333 -58
- package/dist/src/cluster/cluster-repo.js.map +1 -1
- package/dist/src/cluster/commit-cert.d.ts +63 -0
- package/dist/src/cluster/commit-cert.d.ts.map +1 -0
- package/dist/src/cluster/commit-cert.js +108 -0
- package/dist/src/cluster/commit-cert.js.map +1 -0
- package/dist/src/cluster/service.d.ts +46 -11
- package/dist/src/cluster/service.d.ts.map +1 -1
- package/dist/src/cluster/service.js +97 -33
- package/dist/src/cluster/service.js.map +1 -1
- package/dist/src/cluster/spread-on-churn.d.ts +13 -0
- package/dist/src/cluster/spread-on-churn.d.ts.map +1 -1
- package/dist/src/cluster/spread-on-churn.js +34 -4
- package/dist/src/cluster/spread-on-churn.js.map +1 -1
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts +59 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js +79 -0
- package/dist/src/cohort-topic/bootstrap-evidence-builder.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts +81 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js +93 -0
- package/dist/src/cohort-topic/bootstrap-evidence-verifiers.js.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.d.ts.map +1 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js +108 -0
- package/dist/src/cohort-topic/bootstrap-parent-reference.js.map +1 -0
- package/dist/src/cohort-topic/change-bridge.d.ts +65 -0
- package/dist/src/cohort-topic/change-bridge.d.ts.map +1 -0
- package/dist/src/cohort-topic/change-bridge.js +76 -0
- package/dist/src/cohort-topic/change-bridge.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts +74 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js +87 -0
- package/dist/src/cohort-topic/cohort-gossip-driver.js.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts +42 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.d.ts.map +1 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js +57 -0
- package/dist/src/cohort-topic/cohort-gossip-transport.js.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts +100 -0
- package/dist/src/cohort-topic/fret-trust-anchor.d.ts.map +1 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js +115 -0
- package/dist/src/cohort-topic/fret-trust-anchor.js.map +1 -0
- package/dist/src/cohort-topic/host.d.ts +573 -0
- package/dist/src/cohort-topic/host.d.ts.map +1 -0
- package/dist/src/cohort-topic/host.js +1460 -0
- package/dist/src/cohort-topic/host.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +14 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +14 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts +14 -0
- package/dist/src/cohort-topic/membership-publish-sink.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-publish-sink.js +17 -0
- package/dist/src/cohort-topic/membership-publish-sink.js.map +1 -0
- package/dist/src/cohort-topic/membership-source.d.ts +36 -0
- package/dist/src/cohort-topic/membership-source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership-source.js +58 -0
- package/dist/src/cohort-topic/membership-source.js.map +1 -0
- package/dist/src/cohort-topic/peer-codec.d.ts +18 -0
- package/dist/src/cohort-topic/peer-codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-codec.js +26 -0
- package/dist/src/cohort-topic/peer-codec.js.map +1 -0
- package/dist/src/cohort-topic/peer-sig.d.ts +52 -0
- package/dist/src/cohort-topic/peer-sig.d.ts.map +1 -0
- package/dist/src/cohort-topic/peer-sig.js +83 -0
- package/dist/src/cohort-topic/peer-sig.js.map +1 -0
- package/dist/src/cohort-topic/protocols.d.ts +49 -0
- package/dist/src/cohort-topic/protocols.d.ts.map +1 -0
- package/dist/src/cohort-topic/protocols.js +58 -0
- package/dist/src/cohort-topic/protocols.js.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts +49 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.d.ts.map +1 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js +42 -0
- package/dist/src/cohort-topic/reactivity-membership-gate.js.map +1 -0
- package/dist/src/cohort-topic/size-estimator.d.ts +16 -0
- package/dist/src/cohort-topic/size-estimator.d.ts.map +1 -0
- package/dist/src/cohort-topic/size-estimator.js +16 -0
- package/dist/src/cohort-topic/size-estimator.js.map +1 -0
- package/dist/src/cohort-topic/stream-util.d.ts +19 -0
- package/dist/src/cohort-topic/stream-util.d.ts.map +1 -0
- package/dist/src/cohort-topic/stream-util.js +76 -0
- package/dist/src/cohort-topic/stream-util.js.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts +94 -0
- package/dist/src/cohort-topic/threshold-crypto.d.ts.map +1 -0
- package/dist/src/cohort-topic/threshold-crypto.js +178 -0
- package/dist/src/cohort-topic/threshold-crypto.js.map +1 -0
- package/dist/src/cohort-topic/topic-router.d.ts +41 -0
- package/dist/src/cohort-topic/topic-router.d.ts.map +1 -0
- package/dist/src/cohort-topic/topic-router.js +63 -0
- package/dist/src/cohort-topic/topic-router.js.map +1 -0
- package/dist/src/dispute/cascade.d.ts +179 -0
- package/dist/src/dispute/cascade.d.ts.map +1 -0
- package/dist/src/dispute/cascade.js +315 -0
- package/dist/src/dispute/cascade.js.map +1 -0
- package/dist/src/dispute/client.d.ts +2 -1
- package/dist/src/dispute/client.d.ts.map +1 -1
- package/dist/src/dispute/client.js +12 -3
- package/dist/src/dispute/client.js.map +1 -1
- package/dist/src/dispute/dispute-service.d.ts +41 -1
- package/dist/src/dispute/dispute-service.d.ts.map +1 -1
- package/dist/src/dispute/dispute-service.js +147 -21
- package/dist/src/dispute/dispute-service.js.map +1 -1
- package/dist/src/dispute/index.d.ts +2 -0
- package/dist/src/dispute/index.d.ts.map +1 -1
- package/dist/src/dispute/index.js +2 -0
- package/dist/src/dispute/index.js.map +1 -1
- package/dist/src/dispute/invalidation.d.ts +266 -0
- package/dist/src/dispute/invalidation.d.ts.map +1 -0
- package/dist/src/dispute/invalidation.js +405 -0
- package/dist/src/dispute/invalidation.js.map +1 -0
- package/dist/src/dispute/types.d.ts +14 -0
- package/dist/src/dispute/types.d.ts.map +1 -1
- package/dist/src/dispute/types.js.map +1 -1
- package/dist/src/index.d.ts +4 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +4 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/libp2p-key-network.d.ts +7 -0
- package/dist/src/libp2p-key-network.d.ts.map +1 -1
- package/dist/src/libp2p-key-network.js +49 -5
- package/dist/src/libp2p-key-network.js.map +1 -1
- package/dist/src/libp2p-node-base.d.ts +45 -3
- package/dist/src/libp2p-node-base.d.ts.map +1 -1
- package/dist/src/libp2p-node-base.js +522 -5
- package/dist/src/libp2p-node-base.js.map +1 -1
- package/dist/src/matchmaking/aggregate-counts.d.ts +67 -0
- package/dist/src/matchmaking/aggregate-counts.d.ts.map +1 -0
- package/dist/src/matchmaking/aggregate-counts.js +60 -0
- package/dist/src/matchmaking/aggregate-counts.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +20 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +20 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/module.d.ts +181 -0
- package/dist/src/matchmaking/module.d.ts.map +1 -0
- package/dist/src/matchmaking/module.js +221 -0
- package/dist/src/matchmaking/module.js.map +1 -0
- package/dist/src/matchmaking/protocols.d.ts +37 -0
- package/dist/src/matchmaking/protocols.d.ts.map +1 -0
- package/dist/src/matchmaking/protocols.js +42 -0
- package/dist/src/matchmaking/protocols.js.map +1 -0
- package/dist/src/matchmaking/provider-manager.d.ts +52 -0
- package/dist/src/matchmaking/provider-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/provider-manager.js +74 -0
- package/dist/src/matchmaking/provider-manager.js.map +1 -0
- package/dist/src/matchmaking/query-handler.d.ts +34 -0
- package/dist/src/matchmaking/query-handler.d.ts.map +1 -0
- package/dist/src/matchmaking/query-handler.js +62 -0
- package/dist/src/matchmaking/query-handler.js.map +1 -0
- package/dist/src/matchmaking/query-transport.d.ts +180 -0
- package/dist/src/matchmaking/query-transport.d.ts.map +1 -0
- package/dist/src/matchmaking/query-transport.js +315 -0
- package/dist/src/matchmaking/query-transport.js.map +1 -0
- package/dist/src/matchmaking/seeker-manager.d.ts +39 -0
- package/dist/src/matchmaking/seeker-manager.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-manager.js +49 -0
- package/dist/src/matchmaking/seeker-manager.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts +127 -0
- package/dist/src/matchmaking/seeker-walk-client.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk-client.js +189 -0
- package/dist/src/matchmaking/seeker-walk-client.js.map +1 -0
- package/dist/src/matchmaking/traffic-validation.d.ts +128 -0
- package/dist/src/matchmaking/traffic-validation.d.ts.map +1 -0
- package/dist/src/matchmaking/traffic-validation.js +105 -0
- package/dist/src/matchmaking/traffic-validation.js.map +1 -0
- package/dist/src/network/network-manager-service.d.ts +4 -2
- package/dist/src/network/network-manager-service.d.ts.map +1 -1
- package/dist/src/network/network-manager-service.js +9 -3
- package/dist/src/network/network-manager-service.js.map +1 -1
- package/dist/src/protocol-client.d.ts +13 -0
- package/dist/src/protocol-client.d.ts.map +1 -1
- package/dist/src/protocol-client.js +74 -2
- package/dist/src/protocol-client.js.map +1 -1
- package/dist/src/reactivity/forwarder-host.d.ts +198 -0
- package/dist/src/reactivity/forwarder-host.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder-host.js +355 -0
- package/dist/src/reactivity/forwarder-host.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +19 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +19 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notify-transport.d.ts +70 -0
- package/dist/src/reactivity/notify-transport.d.ts.map +1 -0
- package/dist/src/reactivity/notify-transport.js +111 -0
- package/dist/src/reactivity/notify-transport.js.map +1 -0
- package/dist/src/reactivity/origination-manager.d.ts +110 -0
- package/dist/src/reactivity/origination-manager.d.ts.map +1 -0
- package/dist/src/reactivity/origination-manager.js +129 -0
- package/dist/src/reactivity/origination-manager.js.map +1 -0
- package/dist/src/reactivity/protocols.d.ts +43 -0
- package/dist/src/reactivity/protocols.d.ts.map +1 -0
- package/dist/src/reactivity/protocols.js +50 -0
- package/dist/src/reactivity/protocols.js.map +1 -0
- package/dist/src/reactivity/push-state-gossip.d.ts +132 -0
- package/dist/src/reactivity/push-state-gossip.d.ts.map +1 -0
- package/dist/src/reactivity/push-state-gossip.js +231 -0
- package/dist/src/reactivity/push-state-gossip.js.map +1 -0
- package/dist/src/reactivity/recover-transport.d.ts +167 -0
- package/dist/src/reactivity/recover-transport.d.ts.map +1 -0
- package/dist/src/reactivity/recover-transport.js +308 -0
- package/dist/src/reactivity/recover-transport.js.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts +142 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.d.ts.map +1 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js +223 -0
- package/dist/src/reactivity/rotation-rereg-scheduler.js.map +1 -0
- package/dist/src/reactivity/subscriber-registry.d.ts +47 -0
- package/dist/src/reactivity/subscriber-registry.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber-registry.js +87 -0
- package/dist/src/reactivity/subscriber-registry.js.map +1 -0
- package/dist/src/reactivity/subscription-manager.d.ts +221 -0
- package/dist/src/reactivity/subscription-manager.d.ts.map +1 -0
- package/dist/src/reactivity/subscription-manager.js +302 -0
- package/dist/src/reactivity/subscription-manager.js.map +1 -0
- package/dist/src/reactivity/topic-bytes.d.ts +32 -0
- package/dist/src/reactivity/topic-bytes.d.ts.map +1 -0
- package/dist/src/reactivity/topic-bytes.js +34 -0
- package/dist/src/reactivity/topic-bytes.js.map +1 -0
- package/dist/src/repo/client.d.ts.map +1 -1
- package/dist/src/repo/client.js +51 -23
- package/dist/src/repo/client.js.map +1 -1
- package/dist/src/repo/service.d.ts +46 -1
- package/dist/src/repo/service.d.ts.map +1 -1
- package/dist/src/repo/service.js +79 -45
- package/dist/src/repo/service.js.map +1 -1
- package/dist/src/rpc-deadline.d.ts +37 -0
- package/dist/src/rpc-deadline.d.ts.map +1 -0
- package/dist/src/rpc-deadline.js +31 -0
- package/dist/src/rpc-deadline.js.map +1 -0
- package/dist/src/storage/block-storage.d.ts +2 -0
- package/dist/src/storage/block-storage.d.ts.map +1 -1
- package/dist/src/storage/block-storage.js +95 -2
- package/dist/src/storage/block-storage.js.map +1 -1
- package/dist/src/storage/i-block-storage.d.ts +25 -0
- package/dist/src/storage/i-block-storage.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.d.ts +53 -2
- package/dist/src/storage/storage-repo.d.ts.map +1 -1
- package/dist/src/storage/storage-repo.js +193 -9
- package/dist/src/storage/storage-repo.js.map +1 -1
- package/dist/src/sync/client.d.ts +4 -1
- package/dist/src/sync/client.d.ts.map +1 -1
- package/dist/src/sync/client.js +5 -2
- package/dist/src/sync/client.js.map +1 -1
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts +219 -0
- package/dist/src/testing/cohort-topic-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js +467 -0
- package/dist/src/testing/cohort-topic-mesh-harness.js.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts +187 -0
- package/dist/src/testing/matchmaking-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/matchmaking-mesh-harness.js +358 -0
- package/dist/src/testing/matchmaking-mesh-harness.js.map +1 -0
- package/dist/src/testing/mesh-harness.d.ts.map +1 -1
- package/dist/src/testing/mesh-harness.js +23 -1
- package/dist/src/testing/mesh-harness.js.map +1 -1
- package/dist/src/testing/reactivity-mesh-harness.d.ts +298 -0
- package/dist/src/testing/reactivity-mesh-harness.d.ts.map +1 -0
- package/dist/src/testing/reactivity-mesh-harness.js +663 -0
- package/dist/src/testing/reactivity-mesh-harness.js.map +1 -0
- package/package.json +7 -4
- package/src/cluster/block-transfer-service.ts +133 -68
- package/src/cluster/client.ts +64 -45
- package/src/cluster/cluster-error.ts +64 -0
- package/src/cluster/cluster-repo.ts +405 -55
- package/src/cluster/commit-cert.ts +139 -0
- package/src/cluster/service.ts +116 -44
- package/src/cluster/spread-on-churn.ts +328 -285
- package/src/cohort-topic/bootstrap-evidence-builder.ts +122 -0
- package/src/cohort-topic/bootstrap-evidence-verifiers.ts +132 -0
- package/src/cohort-topic/bootstrap-parent-reference.ts +159 -0
- package/src/cohort-topic/change-bridge.ts +109 -0
- package/src/cohort-topic/cohort-gossip-driver.ts +144 -0
- package/src/cohort-topic/cohort-gossip-transport.ts +74 -0
- package/src/cohort-topic/fret-trust-anchor.ts +153 -0
- package/src/cohort-topic/host.ts +2175 -0
- package/src/cohort-topic/index.ts +13 -0
- package/src/cohort-topic/membership-publish-sink.ts +20 -0
- package/src/cohort-topic/membership-source.ts +68 -0
- package/src/cohort-topic/peer-codec.ts +31 -0
- package/src/cohort-topic/peer-sig.ts +86 -0
- package/src/cohort-topic/protocols.ts +71 -0
- package/src/cohort-topic/reactivity-membership-gate.ts +77 -0
- package/src/cohort-topic/size-estimator.ts +16 -0
- package/src/cohort-topic/stream-util.ts +87 -0
- package/src/cohort-topic/threshold-crypto.ts +239 -0
- package/src/cohort-topic/topic-router.ts +77 -0
- package/src/dispute/cascade.ts +517 -0
- package/src/dispute/client.ts +12 -2
- package/src/dispute/dispute-service.ts +171 -24
- package/src/dispute/index.ts +42 -0
- package/src/dispute/invalidation.ts +587 -0
- package/src/dispute/types.ts +14 -0
- package/src/index.ts +4 -0
- package/src/libp2p-key-network.ts +49 -6
- package/src/libp2p-node-base.ts +620 -12
- package/src/matchmaking/aggregate-counts.ts +104 -0
- package/src/matchmaking/index.ts +20 -0
- package/src/matchmaking/module.ts +363 -0
- package/src/matchmaking/protocols.ts +51 -0
- package/src/matchmaking/provider-manager.ts +95 -0
- package/src/matchmaking/query-handler.ts +88 -0
- package/src/matchmaking/query-transport.ts +483 -0
- package/src/matchmaking/seeker-manager.ts +64 -0
- package/src/matchmaking/seeker-walk-client.ts +293 -0
- package/src/matchmaking/traffic-validation.ts +195 -0
- package/src/network/network-manager-service.ts +9 -2
- package/src/protocol-client.ts +64 -3
- package/src/reactivity/forwarder-host.ts +438 -0
- package/src/reactivity/index.ts +19 -0
- package/src/reactivity/notify-transport.ts +144 -0
- package/src/reactivity/origination-manager.ts +192 -0
- package/src/reactivity/protocols.ts +61 -0
- package/src/reactivity/push-state-gossip.ts +291 -0
- package/src/reactivity/recover-transport.ts +408 -0
- package/src/reactivity/rotation-rereg-scheduler.ts +256 -0
- package/src/reactivity/subscriber-registry.ts +96 -0
- package/src/reactivity/subscription-manager.ts +450 -0
- package/src/reactivity/topic-bytes.ts +37 -0
- package/src/repo/client.ts +54 -23
- package/src/repo/service.ts +90 -42
- package/src/rpc-deadline.ts +45 -0
- package/src/storage/block-storage.ts +106 -3
- package/src/storage/i-block-storage.ts +27 -0
- package/src/storage/storage-repo.ts +209 -13
- package/src/sync/client.ts +5 -2
- package/src/testing/cohort-topic-mesh-harness.ts +613 -0
- package/src/testing/matchmaking-mesh-harness.ts +475 -0
- package/src/testing/mesh-harness.ts +22 -2
- package/src/testing/reactivity-mesh-harness.ts +922 -0
|
@@ -0,0 +1,223 @@
|
|
|
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 { bytesToB64url } from "@optimystic/db-core";
|
|
54
|
+
import { createLogger } from "../logger.js";
|
|
55
|
+
const log = createLogger("reactivity-rotation-rereg");
|
|
56
|
+
/**
|
|
57
|
+
* Upper bound on the idempotence ledger (`seen`). Far above any realistic count of *concurrently-pending*
|
|
58
|
+
* successors (single digits: 1 in the normal case, a handful across a chained rotation), so it never evicts a
|
|
59
|
+
* still-live entry in practice while keeping the ledger's memory to tens of KB of short base64url strings at the
|
|
60
|
+
* ceiling. Tunable — raising it widens the late-re-surface de-dupe window at a proportional memory cost.
|
|
61
|
+
* Exported for the regression test that asserts the bound; production callers don't read it.
|
|
62
|
+
*/
|
|
63
|
+
export const SEEN_LEDGER_CAP = 1024;
|
|
64
|
+
/**
|
|
65
|
+
* Production timer binding: a one-shot `setTimeout` whose handle is **unref'd** so a pending re-registration
|
|
66
|
+
* never keeps an otherwise-idle process alive. The returned handle clears the timeout (idempotent — clearing
|
|
67
|
+
* an already-fired/cleared timeout is a no-op).
|
|
68
|
+
*/
|
|
69
|
+
function defaultSetTimer(fn, delayMs) {
|
|
70
|
+
const handle = setTimeout(fn, delayMs);
|
|
71
|
+
// Node timers keep the event loop alive; an idle rotation timer must not pin a process (mirror push-state gossip).
|
|
72
|
+
handle.unref?.();
|
|
73
|
+
return () => clearTimeout(handle);
|
|
74
|
+
}
|
|
75
|
+
/**
|
|
76
|
+
* Hosts the per-successor one-shot timers that move a subscriber to the rotated tree. Construct one per
|
|
77
|
+
* subscription manager and bind {@link schedule} to the manager's
|
|
78
|
+
* {@link import("./subscription-manager.js").ReactivitySubscriptionManagerOptions.onRotation} observer.
|
|
79
|
+
*
|
|
80
|
+
* See the module doc for where the re-registration stagger actually lives (the manager's `rejoinJitter`; this
|
|
81
|
+
* scheduler only consumes `plan.fireAt`, it does not draw it).
|
|
82
|
+
*/
|
|
83
|
+
export class RotationReRegistrationScheduler {
|
|
84
|
+
reRegister;
|
|
85
|
+
setTimer;
|
|
86
|
+
now;
|
|
87
|
+
/** Successors with a still-pending timer, keyed by base64url `newTopicId` → its cancel handle. */
|
|
88
|
+
pending = new Map();
|
|
89
|
+
/**
|
|
90
|
+
* Successors recently scheduled (pending **or** already fired) — the idempotence ledger that survives fire.
|
|
91
|
+
* Insertion-ordered and **bounded** at {@link SEEN_LEDGER_CAP}: {@link evictSeenOverCap} drops the oldest
|
|
92
|
+
* non-pending entries once over cap (never a still-pending key — see that method's doc).
|
|
93
|
+
*/
|
|
94
|
+
seen = new Set();
|
|
95
|
+
stopped = false;
|
|
96
|
+
constructor(options) {
|
|
97
|
+
this.reRegister = options.reRegister;
|
|
98
|
+
this.setTimer = options.setTimer ?? defaultSetTimer;
|
|
99
|
+
this.now = options.now ?? (() => Date.now());
|
|
100
|
+
}
|
|
101
|
+
/** Successors with a timer still pending (not yet fired/cancelled). Diagnostic / test seam. */
|
|
102
|
+
get pendingCount() {
|
|
103
|
+
return this.pending.size;
|
|
104
|
+
}
|
|
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() {
|
|
110
|
+
return this.seen.size;
|
|
111
|
+
}
|
|
112
|
+
/**
|
|
113
|
+
* Schedule the re-registration timer for a rotation notice. De-duped by successor `newTopicId`: a second
|
|
114
|
+
* notice for an already-scheduled-or-fired successor is a no-op (a redirect and a pre-announce can surface
|
|
115
|
+
* the same successor). The delay is `max(0, plan.fireAt - now())`, so a `fireAt` already in the past fires
|
|
116
|
+
* on the next tick (clamped to 0, never negative). A no-op once {@link stop} has run.
|
|
117
|
+
*/
|
|
118
|
+
schedule(notice) {
|
|
119
|
+
if (this.stopped) {
|
|
120
|
+
return;
|
|
121
|
+
}
|
|
122
|
+
const key = bytesToB64url(notice.plan.newTopicId);
|
|
123
|
+
if (this.seen.has(key)) {
|
|
124
|
+
log("rotation re-registration already scheduled/fired for successor topic=%s — ignoring duplicate notice (preAnnounced=%s)", key, notice.preAnnounced);
|
|
125
|
+
return;
|
|
126
|
+
}
|
|
127
|
+
this.seen.add(key);
|
|
128
|
+
const delayMs = Math.max(0, notice.plan.fireAt - this.now());
|
|
129
|
+
const plan = notice.plan;
|
|
130
|
+
const cancel = this.setTimer(() => {
|
|
131
|
+
this.fire(key, plan);
|
|
132
|
+
}, delayMs);
|
|
133
|
+
this.pending.set(key, cancel);
|
|
134
|
+
// Bound `seen` only AFTER the new key is in `pending`, so eviction sees it as live and never drops it (the
|
|
135
|
+
// "every pending key ∈ seen" invariant). Were this run before `pending.set`, the just-added key would be the
|
|
136
|
+
// one non-pending candidate whenever `pending` is at the cap, and would be wrongly evicted then immediately
|
|
137
|
+
// re-added to `pending` — a pending key absent from `seen`, which a duplicate notice could then re-arm over.
|
|
138
|
+
this.evictSeenOverCap();
|
|
139
|
+
log("scheduled rotation re-registration for successor topic=%s in %dms (preAnnounced=%s)", key, delayMs, notice.preAnnounced);
|
|
140
|
+
}
|
|
141
|
+
/**
|
|
142
|
+
* Cancel a single pending timer (by successor `newTopicId`) or, when called with no argument, **all**
|
|
143
|
+
* pending timers (teardown). Cancelling an already-fired or unknown successor is a safe no-op. A cancelled
|
|
144
|
+
* successor is forgotten (dropped from the idempotence ledger), so a later notice for it would reschedule;
|
|
145
|
+
* use {@link stop} for permanent teardown.
|
|
146
|
+
*/
|
|
147
|
+
cancel(newTopicId) {
|
|
148
|
+
if (newTopicId === undefined) {
|
|
149
|
+
for (const cancelTimer of this.pending.values()) {
|
|
150
|
+
cancelTimer();
|
|
151
|
+
}
|
|
152
|
+
this.pending.clear();
|
|
153
|
+
this.seen.clear();
|
|
154
|
+
return;
|
|
155
|
+
}
|
|
156
|
+
const key = bytesToB64url(newTopicId);
|
|
157
|
+
const cancelTimer = this.pending.get(key);
|
|
158
|
+
if (cancelTimer !== undefined) {
|
|
159
|
+
cancelTimer();
|
|
160
|
+
this.pending.delete(key);
|
|
161
|
+
}
|
|
162
|
+
this.seen.delete(key);
|
|
163
|
+
}
|
|
164
|
+
/**
|
|
165
|
+
* Stop the scheduler permanently (teardown): cancel every pending timer and refuse all further scheduling
|
|
166
|
+
* and firing. A timer that races `stop` (its callback already queued) finds the `stopped` gate and never
|
|
167
|
+
* invokes `reRegister`. Idempotent.
|
|
168
|
+
*/
|
|
169
|
+
stop() {
|
|
170
|
+
this.stopped = true;
|
|
171
|
+
for (const cancelTimer of this.pending.values()) {
|
|
172
|
+
cancelTimer();
|
|
173
|
+
}
|
|
174
|
+
this.pending.clear();
|
|
175
|
+
this.seen.clear();
|
|
176
|
+
}
|
|
177
|
+
/**
|
|
178
|
+
* Bound the idempotence ledger: evict the oldest entries (insertion order) that are **not** still pending,
|
|
179
|
+
* until back within {@link SEEN_LEDGER_CAP}. Never evicts a pending key — that would break the "every pending
|
|
180
|
+
* key ∈ `seen`" invariant and could double-fire a successor whose later duplicate notice then re-armed a timer
|
|
181
|
+
* over the live one. If every over-cap entry is somehow still pending (`pending.size > cap` — not reachable in
|
|
182
|
+
* practice, since pending successors are the most recently scheduled), the loop bails and the ledger is left to
|
|
183
|
+
* grow with `pending` this round; it returns within cap as soon as those timers fire and a later schedule runs.
|
|
184
|
+
*/
|
|
185
|
+
evictSeenOverCap() {
|
|
186
|
+
while (this.seen.size > SEEN_LEDGER_CAP) {
|
|
187
|
+
let evicted = false;
|
|
188
|
+
for (const key of this.seen) { // insertion order: oldest first
|
|
189
|
+
if (!this.pending.has(key)) {
|
|
190
|
+
this.seen.delete(key);
|
|
191
|
+
log("rotation re-registration ledger at cap (%d) — evicted oldest fired successor topic=%s", SEEN_LEDGER_CAP, key);
|
|
192
|
+
evicted = true;
|
|
193
|
+
break;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
if (!evicted) {
|
|
197
|
+
break;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
/** Timer callback: drop the pending entry, then invoke the isolated re-registration (unless stopped). */
|
|
202
|
+
fire(key, plan) {
|
|
203
|
+
this.pending.delete(key);
|
|
204
|
+
if (this.stopped) {
|
|
205
|
+
return; // stop() raced the timer callback — no move after teardown.
|
|
206
|
+
}
|
|
207
|
+
// A failed move must never throw out of the timer callback — an escaping throw / unhandled rejection
|
|
208
|
+
// would surface on the host event loop. Guard BOTH a rejected promise and a (mis-implemented) seam that
|
|
209
|
+
// throws synchronously. `seen` retains the key so a duplicate notice still no-ops. No retry this pass —
|
|
210
|
+
// and because `seen` keeps the key, a re-notice for this *same* successor is deduped (the manager's
|
|
211
|
+
// `rotationHandledFor` already holds it too), so the recovery backstop for a failed move is the
|
|
212
|
+
// subscriber's normal recover/re-walk path, not a re-detected rotation to the same tail.
|
|
213
|
+
try {
|
|
214
|
+
void this.reRegister(plan).catch((err) => {
|
|
215
|
+
log("rotation re-registration rejected for successor topic=%s (isolated, not retried): %o", key, err);
|
|
216
|
+
});
|
|
217
|
+
}
|
|
218
|
+
catch (err) {
|
|
219
|
+
log("rotation re-registration threw synchronously for successor topic=%s (isolated): %o", key, err);
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
//# sourceMappingURL=rotation-rereg-scheduler.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"rotation-rereg-scheduler.js","sourceRoot":"","sources":["../../../src/reactivity/rotation-rereg-scheduler.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — node-level subscriber registry (the socket-delivery → subscription-manager seam).
|
|
3
|
+
*
|
|
4
|
+
* A {@link import("./subscription-manager.js").ReactivitySubscriptionManager} owns the verify/dedupe/deliver
|
|
5
|
+
* path for one subscription but is transport-agnostic: it exposes `onNotification(n)` and never touches a
|
|
6
|
+
* socket. This registry is the routing table that connects a socket-delivered {@link NotificationV1} to the
|
|
7
|
+
* right manager(s): the forwarder host's `deliverLocal(topicId, n)` looks topic up here and invokes every
|
|
8
|
+
* manager registered for it, and a constructed manager registers its `onNotification` keyed by the reactivity
|
|
9
|
+
* `topicId` it subscribed under.
|
|
10
|
+
*
|
|
11
|
+
* Keyed by topicId-base64url. A manager subscribes under `topicId = reactivityTopicId(reactivityTailBytes(
|
|
12
|
+
* tailId))` (the same anchor origination derives), so a notification's topic — derived by the forwarder host
|
|
13
|
+
* from the notification's tail anchor — routes to exactly the managers watching that tail's collection.
|
|
14
|
+
*
|
|
15
|
+
* **Scope.** This delivers socket-routed notifications into a manager that was *already constructed* and
|
|
16
|
+
* registered. Constructing the manager from a Quereus `Database.watch` (the application bridge) stays the
|
|
17
|
+
* backlog item `optimystic-network-reactive-watch-integration-test`; this registry is the plug it will use.
|
|
18
|
+
*/
|
|
19
|
+
import { type NotificationV1 } from "@optimystic/db-core";
|
|
20
|
+
/** A subscriber's inbound seam — a manager's `onNotification` (which returns a delivery outcome promise). */
|
|
21
|
+
export type ReactivityNotificationHandler = (n: NotificationV1) => void | Promise<unknown>;
|
|
22
|
+
/**
|
|
23
|
+
* The node's `topicId → subscriber handlers` table. Exposed on the node as `reactivitySubscribers` so a
|
|
24
|
+
* subscribe factory can register a {@link import("./subscription-manager.js").ReactivitySubscriptionManager}
|
|
25
|
+
* and have socket-delivered notifications routed to it.
|
|
26
|
+
*/
|
|
27
|
+
export declare class ReactivitySubscriberRegistry {
|
|
28
|
+
private readonly byTopic;
|
|
29
|
+
/**
|
|
30
|
+
* Register `onNotification` to receive every notification the node delivers for `topicId`. Multiple
|
|
31
|
+
* managers may register for one topic (each gets every notification). Returns an idempotent unregister.
|
|
32
|
+
*/
|
|
33
|
+
register(topicId: Uint8Array, onNotification: ReactivityNotificationHandler): () => void;
|
|
34
|
+
/** Drop a previously-registered handler; idempotent (a no-op if it was never registered / already gone). */
|
|
35
|
+
unregister(topicId: Uint8Array, onNotification: ReactivityNotificationHandler): void;
|
|
36
|
+
/**
|
|
37
|
+
* Deliver `n` to every handler registered for `topicId` (the forwarder host's `deliverLocal` seam). Each
|
|
38
|
+
* handler is isolated: a synchronous throw or a rejected delivery promise is logged, never propagated —
|
|
39
|
+
* one faulty subscriber must not break delivery to the rest or the fan-out loop that called this.
|
|
40
|
+
*/
|
|
41
|
+
deliver(topicId: Uint8Array, n: NotificationV1): void;
|
|
42
|
+
/** Number of topics with at least one registered subscriber (diagnostic / test). */
|
|
43
|
+
get topicCount(): number;
|
|
44
|
+
/** Whether any subscriber is registered for `topicId` (diagnostic / test). */
|
|
45
|
+
has(topicId: Uint8Array): boolean;
|
|
46
|
+
}
|
|
47
|
+
//# sourceMappingURL=subscriber-registry.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber-registry.d.ts","sourceRoot":"","sources":["../../../src/reactivity/subscriber-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAiB,KAAK,cAAc,EAAE,MAAM,qBAAqB,CAAC;AAKzE,6GAA6G;AAC7G,MAAM,MAAM,6BAA6B,GAAG,CAAC,CAAC,EAAE,cAAc,KAAK,IAAI,GAAG,OAAO,CAAC,OAAO,CAAC,CAAC;AAE3F;;;;GAIG;AACH,qBAAa,4BAA4B;IACxC,OAAO,CAAC,QAAQ,CAAC,OAAO,CAAyD;IAEjF;;;OAGG;IACH,QAAQ,CAAC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,6BAA6B,GAAG,MAAM,IAAI;IAWxF,4GAA4G;IAC5G,UAAU,CAAC,OAAO,EAAE,UAAU,EAAE,cAAc,EAAE,6BAA6B,GAAG,IAAI;IAYpF;;;;OAIG;IACH,OAAO,CAAC,OAAO,EAAE,UAAU,EAAE,CAAC,EAAE,cAAc,GAAG,IAAI;IAkBrD,oFAAoF;IACpF,IAAI,UAAU,IAAI,MAAM,CAEvB;IAED,8EAA8E;IAC9E,GAAG,CAAC,OAAO,EAAE,UAAU,GAAG,OAAO;CAGjC"}
|
|
@@ -0,0 +1,87 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — node-level subscriber registry (the socket-delivery → subscription-manager seam).
|
|
3
|
+
*
|
|
4
|
+
* A {@link import("./subscription-manager.js").ReactivitySubscriptionManager} owns the verify/dedupe/deliver
|
|
5
|
+
* path for one subscription but is transport-agnostic: it exposes `onNotification(n)` and never touches a
|
|
6
|
+
* socket. This registry is the routing table that connects a socket-delivered {@link NotificationV1} to the
|
|
7
|
+
* right manager(s): the forwarder host's `deliverLocal(topicId, n)` looks topic up here and invokes every
|
|
8
|
+
* manager registered for it, and a constructed manager registers its `onNotification` keyed by the reactivity
|
|
9
|
+
* `topicId` it subscribed under.
|
|
10
|
+
*
|
|
11
|
+
* Keyed by topicId-base64url. A manager subscribes under `topicId = reactivityTopicId(reactivityTailBytes(
|
|
12
|
+
* tailId))` (the same anchor origination derives), so a notification's topic — derived by the forwarder host
|
|
13
|
+
* from the notification's tail anchor — routes to exactly the managers watching that tail's collection.
|
|
14
|
+
*
|
|
15
|
+
* **Scope.** This delivers socket-routed notifications into a manager that was *already constructed* and
|
|
16
|
+
* registered. Constructing the manager from a Quereus `Database.watch` (the application bridge) stays the
|
|
17
|
+
* backlog item `optimystic-network-reactive-watch-integration-test`; this registry is the plug it will use.
|
|
18
|
+
*/
|
|
19
|
+
import { bytesToB64url } from "@optimystic/db-core";
|
|
20
|
+
import { createLogger } from "../logger.js";
|
|
21
|
+
const log = createLogger("reactivity-subscribers");
|
|
22
|
+
/**
|
|
23
|
+
* The node's `topicId → subscriber handlers` table. Exposed on the node as `reactivitySubscribers` so a
|
|
24
|
+
* subscribe factory can register a {@link import("./subscription-manager.js").ReactivitySubscriptionManager}
|
|
25
|
+
* and have socket-delivered notifications routed to it.
|
|
26
|
+
*/
|
|
27
|
+
export class ReactivitySubscriberRegistry {
|
|
28
|
+
byTopic = new Map();
|
|
29
|
+
/**
|
|
30
|
+
* Register `onNotification` to receive every notification the node delivers for `topicId`. Multiple
|
|
31
|
+
* managers may register for one topic (each gets every notification). Returns an idempotent unregister.
|
|
32
|
+
*/
|
|
33
|
+
register(topicId, onNotification) {
|
|
34
|
+
const key = bytesToB64url(topicId);
|
|
35
|
+
let handlers = this.byTopic.get(key);
|
|
36
|
+
if (handlers === undefined) {
|
|
37
|
+
handlers = new Set();
|
|
38
|
+
this.byTopic.set(key, handlers);
|
|
39
|
+
}
|
|
40
|
+
handlers.add(onNotification);
|
|
41
|
+
return () => this.unregister(topicId, onNotification);
|
|
42
|
+
}
|
|
43
|
+
/** Drop a previously-registered handler; idempotent (a no-op if it was never registered / already gone). */
|
|
44
|
+
unregister(topicId, onNotification) {
|
|
45
|
+
const key = bytesToB64url(topicId);
|
|
46
|
+
const handlers = this.byTopic.get(key);
|
|
47
|
+
if (handlers === undefined) {
|
|
48
|
+
return;
|
|
49
|
+
}
|
|
50
|
+
handlers.delete(onNotification);
|
|
51
|
+
if (handlers.size === 0) {
|
|
52
|
+
this.byTopic.delete(key);
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
/**
|
|
56
|
+
* Deliver `n` to every handler registered for `topicId` (the forwarder host's `deliverLocal` seam). Each
|
|
57
|
+
* handler is isolated: a synchronous throw or a rejected delivery promise is logged, never propagated —
|
|
58
|
+
* one faulty subscriber must not break delivery to the rest or the fan-out loop that called this.
|
|
59
|
+
*/
|
|
60
|
+
deliver(topicId, n) {
|
|
61
|
+
const handlers = this.byTopic.get(bytesToB64url(topicId));
|
|
62
|
+
if (handlers === undefined) {
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
// Snapshot: a handler that unregisters itself on delivery would otherwise mutate the live set.
|
|
66
|
+
for (const handler of [...handlers]) {
|
|
67
|
+
try {
|
|
68
|
+
const result = handler(n);
|
|
69
|
+
if (result instanceof Promise) {
|
|
70
|
+
result.catch((err) => log("subscriber delivery rejected (isolated) for rev=%d: %o", n.revision, err));
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
catch (err) {
|
|
74
|
+
log("subscriber handler threw (isolated) for rev=%d: %o", n.revision, err);
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
/** Number of topics with at least one registered subscriber (diagnostic / test). */
|
|
79
|
+
get topicCount() {
|
|
80
|
+
return this.byTopic.size;
|
|
81
|
+
}
|
|
82
|
+
/** Whether any subscriber is registered for `topicId` (diagnostic / test). */
|
|
83
|
+
has(topicId) {
|
|
84
|
+
return this.byTopic.has(bytesToB64url(topicId));
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
//# sourceMappingURL=subscriber-registry.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscriber-registry.js","sourceRoot":"","sources":["../../../src/reactivity/subscriber-registry.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAEH,OAAO,EAAE,aAAa,EAAuB,MAAM,qBAAqB,CAAC;AACzE,OAAO,EAAE,YAAY,EAAE,MAAM,cAAc,CAAC;AAE5C,MAAM,GAAG,GAAG,YAAY,CAAC,wBAAwB,CAAC,CAAC;AAKnD;;;;GAIG;AACH,MAAM,OAAO,4BAA4B;IACvB,OAAO,GAAG,IAAI,GAAG,EAA8C,CAAC;IAEjF;;;OAGG;IACH,QAAQ,CAAC,OAAmB,EAAE,cAA6C;QAC1E,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACnC,IAAI,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACrC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,QAAQ,GAAG,IAAI,GAAG,EAAE,CAAC;YACrB,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,EAAE,QAAQ,CAAC,CAAC;QACjC,CAAC;QACD,QAAQ,CAAC,GAAG,CAAC,cAAc,CAAC,CAAC;QAC7B,OAAO,GAAS,EAAE,CAAC,IAAI,CAAC,UAAU,CAAC,OAAO,EAAE,cAAc,CAAC,CAAC;IAC7D,CAAC;IAED,4GAA4G;IAC5G,UAAU,CAAC,OAAmB,EAAE,cAA6C;QAC5E,MAAM,GAAG,GAAG,aAAa,CAAC,OAAO,CAAC,CAAC;QACnC,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,GAAG,CAAC,CAAC;QACvC,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO;QACR,CAAC;QACD,QAAQ,CAAC,MAAM,CAAC,cAAc,CAAC,CAAC;QAChC,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC,EAAE,CAAC;YACzB,IAAI,CAAC,OAAO,CAAC,MAAM,CAAC,GAAG,CAAC,CAAC;QAC1B,CAAC;IACF,CAAC;IAED;;;;OAIG;IACH,OAAO,CAAC,OAAmB,EAAE,CAAiB;QAC7C,MAAM,QAAQ,GAAG,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;QAC1D,IAAI,QAAQ,KAAK,SAAS,EAAE,CAAC;YAC5B,OAAO;QACR,CAAC;QACD,+FAA+F;QAC/F,KAAK,MAAM,OAAO,IAAI,CAAC,GAAG,QAAQ,CAAC,EAAE,CAAC;YACrC,IAAI,CAAC;gBACJ,MAAM,MAAM,GAAG,OAAO,CAAC,CAAC,CAAC,CAAC;gBAC1B,IAAI,MAAM,YAAY,OAAO,EAAE,CAAC;oBAC/B,MAAM,CAAC,KAAK,CAAC,CAAC,GAAY,EAAE,EAAE,CAAC,GAAG,CAAC,wDAAwD,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC,CAAC;gBAChH,CAAC;YACF,CAAC;YAAC,OAAO,GAAG,EAAE,CAAC;gBACd,GAAG,CAAC,oDAAoD,EAAE,CAAC,CAAC,QAAQ,EAAE,GAAG,CAAC,CAAC;YAC5E,CAAC;QACF,CAAC;IACF,CAAC;IAED,oFAAoF;IACpF,IAAI,UAAU;QACb,OAAO,IAAI,CAAC,OAAO,CAAC,IAAI,CAAC;IAC1B,CAAC;IAED,8EAA8E;IAC9E,GAAG,CAAC,OAAmB;QACtB,OAAO,IAAI,CAAC,OAAO,CAAC,GAAG,CAAC,aAAa,CAAC,OAAO,CAAC,CAAC,CAAC;IACjD,CAAC;CACD"}
|
|
@@ -0,0 +1,221 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Reactivity — subscription manager (db-p2p, wires to the cohort-topic substrate).
|
|
3
|
+
*
|
|
4
|
+
* Drives a subscriber's lifecycle against the participant-facing {@link CohortTopicService}: register at
|
|
5
|
+
* cohort-topic tier **T3 (luxury)** with the reactivity `appPayload`, renew to keep the registration
|
|
6
|
+
* alive within its TTL (Edge 60 s / Core 90 s), and withdraw by ceasing renewal — all the cohort-topic
|
|
7
|
+
* standard (`docs/reactivity.md` §Subscription). The reactivity-specific shape lives in db-core: the
|
|
8
|
+
* tail-anchored `topicId = H(tailId ‖ "reactivity")`, the {@link SubscribeAppPayloadV1}, and the
|
|
9
|
+
* subscriber-side verify/deliver path ({@link ReactivitySubscriber}).
|
|
10
|
+
*
|
|
11
|
+
* Inbound notifications are handed to {@link ReactivitySubscriptionManager.onNotification}, which runs
|
|
12
|
+
* the db-core delivery path (verify against the cached tail-cohort `MembershipCertV1` with one
|
|
13
|
+
* fetch-and-retry, revision-contiguity, gap → backfill seam, `(collectionId, revision)` dedupe, surface).
|
|
14
|
+
* The notification transport (the reactivity application protocol that delivers `NotificationV1` frames
|
|
15
|
+
* to a subscriber's primary) is the sibling tickets' concern; this manager owns attach + delivery logic.
|
|
16
|
+
*/
|
|
17
|
+
import { type CohortTopicService, type NodeProfile, type NotificationV1, type DeliveryOutcome, type RegistrationHandle, type BackfillV1, type BackfillTransport, type ResumeV1, type ResumeReplyV1, type ResumeApplyOutcome, type StickyCohortHintCache, type CheckpointSummary, type RejoinJitter, type ReRegistrationPlan } from "@optimystic/db-core";
|
|
18
|
+
/** Sends a {@link ResumeV1} to the serving cohort and awaits its classified {@link ResumeReplyV1}. */
|
|
19
|
+
export type ResumeTransport = (req: ResumeV1) => Promise<ResumeReplyV1>;
|
|
20
|
+
/**
|
|
21
|
+
* A detected tail rotation surfaced to the host so it can schedule the jittered re-registration timer
|
|
22
|
+
* (`docs/reactivity.md` §Tail rotation). The manager has already invalidated the sticky cohort-hint cache
|
|
23
|
+
* (the cached primary is under the old tree).
|
|
24
|
+
*/
|
|
25
|
+
export interface RotationNotice {
|
|
26
|
+
/** The new tail block id the topic rotated to, base64url. */
|
|
27
|
+
readonly newTailId: string;
|
|
28
|
+
/** True iff this was a pre-announce (`rotationHint` on a still-current-tail notification). */
|
|
29
|
+
readonly preAnnounced: boolean;
|
|
30
|
+
/** The jittered re-registration plan (new topicId + `fireAt` + carried `lastRevision`) for the host to schedule. */
|
|
31
|
+
readonly plan: ReRegistrationPlan;
|
|
32
|
+
}
|
|
33
|
+
/** Construction inputs for a {@link ReactivitySubscriptionManager}. */
|
|
34
|
+
export interface ReactivitySubscriptionManagerOptions {
|
|
35
|
+
/** Participant-facing cohort-topic substrate API. */
|
|
36
|
+
readonly service: CohortTopicService;
|
|
37
|
+
/** Stable collection identity (the collection's id block id, raw bytes). */
|
|
38
|
+
readonly collectionId: Uint8Array;
|
|
39
|
+
/**
|
|
40
|
+
* Tail block id at attach time (raw bytes); anchors the rotating topic (`reactivityTopicId` is applied to
|
|
41
|
+
* it) and detects rotation.
|
|
42
|
+
*
|
|
43
|
+
* **Load-bearing encoding contract.** When a production subscribe factory converts a `BlockId` tail to
|
|
44
|
+
* these bytes it MUST use `reactivityTailBytes(tailId)` (`reactivity/topic-bytes.ts`) — the SAME function
|
|
45
|
+
* origination's membership gate uses — NOT db-core's `blockIdToBytes` (which `sha256`s first → a double
|
|
46
|
+
* hash). Origination derives the topic's `coord_0` cohort from `reactivityTopicId(reactivityTailBytes(
|
|
47
|
+
* tailId))`; if this side feeds differently-encoded bytes it subscribes to a *different* coord and
|
|
48
|
+
* origination silently never reaches it (the `topic-bytes-encoding` spec pins the coord-equality).
|
|
49
|
+
*/
|
|
50
|
+
readonly tailIdAtAttach: Uint8Array;
|
|
51
|
+
/** Surface a verified, contiguous notification to the application. */
|
|
52
|
+
readonly deliver: (n: NotificationV1) => void;
|
|
53
|
+
/**
|
|
54
|
+
* Explicit backfill seam — used only when {@link backfillTransport} is **not** supplied. When a
|
|
55
|
+
* transport is given, the manager builds the db-core {@link createBackfillRequester} driver instead and
|
|
56
|
+
* this is ignored.
|
|
57
|
+
*/
|
|
58
|
+
readonly requestBackfill?: (from: number, to: number) => void;
|
|
59
|
+
/**
|
|
60
|
+
* Backfill RPC transport (the reactivity application protocol dialing the serving cohort). When
|
|
61
|
+
* supplied, the manager wires the subscriber's gap-detection seam to the {@link BackfillV1} RPC,
|
|
62
|
+
* replaying the reply through the delivery path. Requires {@link signBackfill}.
|
|
63
|
+
*/
|
|
64
|
+
readonly backfillTransport?: BackfillTransport;
|
|
65
|
+
/** Sign a {@link BackfillV1} over its unsigned image (subscriber peer key); base64url. */
|
|
66
|
+
readonly signBackfill?: (req: Omit<BackfillV1, "signature">) => string;
|
|
67
|
+
/** Resume RPC transport (mobile wake). When supplied, {@link resume} is available. Requires {@link signResume}. */
|
|
68
|
+
readonly resumeTransport?: ResumeTransport;
|
|
69
|
+
/** Sign a {@link ResumeV1} over its unsigned image (subscriber peer key); base64url. */
|
|
70
|
+
readonly signResume?: (req: Omit<ResumeV1, "signature">) => string;
|
|
71
|
+
/**
|
|
72
|
+
* The subscriber's **real ring coordinate**, base64url (the `participantCoord` it registers under at the
|
|
73
|
+
* cohort-topic tier), carried in the signed {@link ResumeV1}. The recover transport replies on the same
|
|
74
|
+
* stream, so this is not used for reply routing today; still, the production factory should source it
|
|
75
|
+
* correctly so the signed field is meaningful and a future out-of-band reply path is unblocked. Absent ⇒
|
|
76
|
+
* the manager falls back to the collection id as a placeholder and logs (the signed field is then merely
|
|
77
|
+
* a stable per-collection token, not the ring coord).
|
|
78
|
+
*/
|
|
79
|
+
readonly subscriberCoord?: string;
|
|
80
|
+
/**
|
|
81
|
+
* Application-level re-attempts the backfill escalation makes after a **transport failure** before giving
|
|
82
|
+
* up to a resume/chain-read (the sticky-hint dial + one cohort-walk fallback already live in the
|
|
83
|
+
* transport, so this is a small outer retry). Default `1`. Distinct from the `available`-window underflow
|
|
84
|
+
* path ({@link onBackfillUnderflow}), which escalates immediately without retrying.
|
|
85
|
+
*/
|
|
86
|
+
readonly backfillMaxRetries?: number;
|
|
87
|
+
/** Apply a verified checkpoint's merged digest on a `checkpoint_window` resume. */
|
|
88
|
+
readonly onCheckpointDigest?: (summary: CheckpointSummary) => void;
|
|
89
|
+
/** Chain-read + fresh subscribe fallback (out_of_window, or an untrusted checkpoint). */
|
|
90
|
+
readonly onChainRead?: (currentTailId: string | undefined, currentRevision: number | undefined) => void;
|
|
91
|
+
/** Re-register under the rotated tail (tail_rotated); also invalidates the sticky cohort-hint cache. */
|
|
92
|
+
readonly onTailRotated?: (newTailId: string, newRevisionAtRotation: number) => void;
|
|
93
|
+
/** Escalation when a backfill's `available` window fell past the gap's low edge (escalate to resume/chain). */
|
|
94
|
+
readonly onBackfillUnderflow?: (requested: {
|
|
95
|
+
from: number;
|
|
96
|
+
to: number;
|
|
97
|
+
}, available: {
|
|
98
|
+
fromRevision: number;
|
|
99
|
+
toRevision: number;
|
|
100
|
+
}) => void;
|
|
101
|
+
/** Sticky cohort-hint cache for one-RT resume after a flap; defaults to a fresh per-manager cache (Edge). */
|
|
102
|
+
readonly cohortHintCache?: StickyCohortHintCache;
|
|
103
|
+
/**
|
|
104
|
+
* Tail-rotation observer (`docs/reactivity.md` §Tail rotation). Fired once per successor tail when an
|
|
105
|
+
* inbound notification reveals a rotation (delivered `tailId` differs, or a `rotationHint` pre-announce):
|
|
106
|
+
* the manager invalidates the sticky cohort-hint cache and hands the host a jittered re-registration plan
|
|
107
|
+
* to schedule. Absent ⇒ rotation is detected and the cache invalidated, but no plan is surfaced.
|
|
108
|
+
*/
|
|
109
|
+
readonly onRotation?: (notice: RotationNotice) => void;
|
|
110
|
+
/** Re-registration jitter for the rotation plan's `fireAt`; defaults to the `T_rejoin_jitter` curve. */
|
|
111
|
+
readonly rejoinJitter?: RejoinJitter;
|
|
112
|
+
/** Unix-ms clock for resume timestamps (injected for deterministic tests). Default `Date.now`. */
|
|
113
|
+
readonly clock?: () => number;
|
|
114
|
+
/** Last revision already held; `0` (default) for a fresh subscribe. */
|
|
115
|
+
readonly lastKnownRev?: number;
|
|
116
|
+
/** Max delta bytes accepted; defaults to `0` on Edge, `delta_max` on Core via {@link profile}. */
|
|
117
|
+
readonly deltaMaxBytes?: number;
|
|
118
|
+
/** Subscription TTL (ms). Default: derived from {@link profile} (Core 90 s / Edge 60 s). */
|
|
119
|
+
readonly ttlMs?: number;
|
|
120
|
+
/** Node profile used to derive the TTL / delta budget when not given explicitly. */
|
|
121
|
+
readonly profile?: NodeProfile;
|
|
122
|
+
}
|
|
123
|
+
/** Wires one reactivity subscription to the cohort-topic substrate at tier T3. */
|
|
124
|
+
export declare class ReactivitySubscriptionManager {
|
|
125
|
+
private readonly service;
|
|
126
|
+
private readonly collectionId;
|
|
127
|
+
private readonly collectionIdB64;
|
|
128
|
+
private readonly tailIdAtAttach;
|
|
129
|
+
private readonly topicId;
|
|
130
|
+
private readonly ttlMs;
|
|
131
|
+
private readonly deltaMaxBytes;
|
|
132
|
+
private readonly lastKnownRev;
|
|
133
|
+
private readonly subscriber;
|
|
134
|
+
private readonly verifier;
|
|
135
|
+
private readonly options;
|
|
136
|
+
private readonly cohortHintCache;
|
|
137
|
+
private readonly clock;
|
|
138
|
+
private readonly tailIdAtAttachB64;
|
|
139
|
+
private readonly rejoinJitter;
|
|
140
|
+
/** Resolved ring coordinate signed into a {@link ResumeV1} (real coord, or the collectionId placeholder). */
|
|
141
|
+
private readonly subscriberCoord;
|
|
142
|
+
/** True iff {@link subscriberCoord} fell back to the collectionId placeholder (no real coord supplied). */
|
|
143
|
+
private readonly subscriberCoordIsFallback;
|
|
144
|
+
/** The successor tail a rotation has already been surfaced for, so the notice fires once per rotation. */
|
|
145
|
+
private rotationHandledFor?;
|
|
146
|
+
/** Memoized db-core backfill driver (built on first gap, once `this.subscriber` is assigned). */
|
|
147
|
+
private backfillRequester?;
|
|
148
|
+
private handle?;
|
|
149
|
+
constructor(options: ReactivitySubscriptionManagerOptions);
|
|
150
|
+
/**
|
|
151
|
+
* The subscriber detected a revision gap. When a {@link ReactivitySubscriptionManagerOptions.backfillTransport}
|
|
152
|
+
* + signer are configured, drive the db-core {@link createBackfillRequester} (build → sign → send →
|
|
153
|
+
* replay reply through delivery → underflow escalation, built lazily once); otherwise fall back to the
|
|
154
|
+
* explicit {@link ReactivitySubscriptionManagerOptions.requestBackfill} callback (or no-op).
|
|
155
|
+
*/
|
|
156
|
+
private onBackfillGap;
|
|
157
|
+
/**
|
|
158
|
+
* A backfill RPC failed at the transport (the sticky-hint dial + cohort-walk fallback inside the
|
|
159
|
+
* transport were already exhausted). Re-attempt up to {@link ReactivitySubscriptionManagerOptions.backfillMaxRetries}
|
|
160
|
+
* times, then escalate to {@link resume} (when a resume transport is configured) and finally to
|
|
161
|
+
* {@link ReactivitySubscriptionManagerOptions.onChainRead}. Never rejects (it runs detached off the gap
|
|
162
|
+
* seam) and never touches the commit/delivery path.
|
|
163
|
+
*/
|
|
164
|
+
private escalateBackfill;
|
|
165
|
+
/** The live registration handle, or `undefined` before the first {@link register}. */
|
|
166
|
+
get registration(): RegistrationHandle | undefined;
|
|
167
|
+
/** Last contiguously-delivered revision. */
|
|
168
|
+
get lastRevision(): number;
|
|
169
|
+
/** Register the subscriber at tier T3 with the reactivity `appPayload`. */
|
|
170
|
+
register(): Promise<RegistrationHandle>;
|
|
171
|
+
/** Run one renewal cycle (keep-alive touch). No-op before the first {@link register}. */
|
|
172
|
+
renew(): Promise<void>;
|
|
173
|
+
/** Withdraw: stop renewing and send a best-effort signed tombstone so the cohort frees the
|
|
174
|
+
* registration immediately (TTL expiry remains the fallback if the primary is unreachable). */
|
|
175
|
+
withdraw(): Promise<void>;
|
|
176
|
+
/**
|
|
177
|
+
* Resume after a sleep/flap (`docs/reactivity.md` §Resume). Sends one {@link ResumeV1} from
|
|
178
|
+
* `lastRevision + 1` to the serving cohort over the injected {@link ResumeTransport} and applies the
|
|
179
|
+
* classified reply via the db-core {@link applyResumeReply}: a `backfill` / `checkpoint_window` reply
|
|
180
|
+
* replays its entries through the delivery path (verified, deduped); `out_of_window` and an untrusted
|
|
181
|
+
* checkpoint escalate to {@link ReactivitySubscriptionManagerOptions.onChainRead}; `tail_rotated`
|
|
182
|
+
* escalates to {@link ReactivitySubscriptionManagerOptions.onTailRotated} and invalidates the sticky
|
|
183
|
+
* cohort-hint cache (the cached primary is under the old tree). Throws if no resume transport/signer
|
|
184
|
+
* was configured.
|
|
185
|
+
*
|
|
186
|
+
* The sticky cohort-hint cache (Edge) lets a resume after a brief flap dial the cached primary directly
|
|
187
|
+
* for a one-RT recovery instead of re-walking from `d_max`; it is the transport's to consult via
|
|
188
|
+
* {@link cohortHint}.
|
|
189
|
+
*/
|
|
190
|
+
resume(): Promise<ResumeApplyOutcome>;
|
|
191
|
+
/** The sticky cohort-hint cache backing one-RT resume after a flap (Edge). */
|
|
192
|
+
get cohortHint(): StickyCohortHintCache;
|
|
193
|
+
/**
|
|
194
|
+
* Run the db-core delivery path for one inbound notification, then check for tail rotation
|
|
195
|
+
* (`docs/reactivity.md` §Tail rotation): a delivered `tailId` that differs from `tailIdAtAttach`, or a
|
|
196
|
+
* `rotationHint` pre-announce, invalidates the sticky cohort-hint cache (the cached primary is under the
|
|
197
|
+
* old tree) and surfaces a jittered re-registration plan via {@link ReactivitySubscriptionManagerOptions.onRotation}.
|
|
198
|
+
* Fired at most once per successor tail.
|
|
199
|
+
*/
|
|
200
|
+
onNotification(n: NotificationV1): Promise<DeliveryOutcome>;
|
|
201
|
+
/** Detect a tail rotation from an inbound notification and surface it once per successor tail. */
|
|
202
|
+
private checkRotation;
|
|
203
|
+
/**
|
|
204
|
+
* Surface a rotation to the host **once per successor tail** (`docs/reactivity.md` §Tail rotation):
|
|
205
|
+
* invalidate the sticky cohort-hint cache (the cached primary is under the now-stale tree, so a later
|
|
206
|
+
* resume re-walks) and — when an {@link ReactivitySubscriptionManagerOptions.onRotation} observer is
|
|
207
|
+
* configured — hand it a jittered re-registration plan carrying `lastRevision` (continuous across the
|
|
208
|
+
* rotation). The single seam both the notification-driven detection ({@link checkRotation}) and the
|
|
209
|
+
* recover-driven {@link RotationRedirectError} ({@link honorRotationRedirect}) end in; the
|
|
210
|
+
* {@link rotationHandledFor} guard self-corrects across a chained OLD→A→B rotation.
|
|
211
|
+
*/
|
|
212
|
+
private surfaceRotation;
|
|
213
|
+
/**
|
|
214
|
+
* Honor a recover-surfaced {@link RotationRedirectError}: the serving cohort's outgoing tail rotated and
|
|
215
|
+
* bounced this request to the new tree. Route it through the **same** {@link surfaceRotation} seam a
|
|
216
|
+
* delivered pre-announce uses (`preAnnounced: false`), so both the notify-driven and recover-driven
|
|
217
|
+
* rotation paths converge on one `RotationNotice` for the host's re-registration scheduler to consume.
|
|
218
|
+
*/
|
|
219
|
+
private honorRotationRedirect;
|
|
220
|
+
}
|
|
221
|
+
//# sourceMappingURL=subscription-manager.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"subscription-manager.d.ts","sourceRoot":"","sources":["../../../src/reactivity/subscription-manager.ts"],"names":[],"mappings":"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"}
|