@optimystic/db-core 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/dist/src/cluster/structs.d.ts +13 -0
- package/dist/src/cluster/structs.d.ts.map +1 -1
- package/dist/src/cohort-topic/addressing.d.ts +53 -0
- package/dist/src/cohort-topic/addressing.d.ts.map +1 -0
- package/dist/src/cohort-topic/addressing.js +95 -0
- package/dist/src/cohort-topic/addressing.js.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts +105 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js +197 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence-envelope.js.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts +50 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js +62 -0
- package/dist/src/cohort-topic/antidos/bootstrap-evidence.js.map +1 -0
- package/dist/src/cohort-topic/antidos/index.d.ts +6 -0
- package/dist/src/cohort-topic/antidos/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/index.js +6 -0
- package/dist/src/cohort-topic/antidos/index.js.map +1 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.d.ts +95 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.js +146 -0
- package/dist/src/cohort-topic/antidos/rate-limiter.js.map +1 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts +37 -0
- package/dist/src/cohort-topic/antidos/replay-guard.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js +78 -0
- package/dist/src/cohort-topic/antidos/replay-guard.js.map +1 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts +50 -0
- package/dist/src/cohort-topic/antidos/topic-budget.d.ts.map +1 -0
- package/dist/src/cohort-topic/antidos/topic-budget.js +95 -0
- package/dist/src/cohort-topic/antidos/topic-budget.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/index.d.ts +3 -0
- package/dist/src/cohort-topic/antiflood/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/index.js +3 -0
- package/dist/src/cohort-topic/antiflood/index.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/invariants.d.ts +70 -0
- package/dist/src/cohort-topic/antiflood/invariants.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/invariants.js +86 -0
- package/dist/src/cohort-topic/antiflood/invariants.js.map +1 -0
- package/dist/src/cohort-topic/antiflood/jitter.d.ts +65 -0
- package/dist/src/cohort-topic/antiflood/jitter.d.ts.map +1 -0
- package/dist/src/cohort-topic/antiflood/jitter.js +75 -0
- package/dist/src/cohort-topic/antiflood/jitter.js.map +1 -0
- package/dist/src/cohort-topic/coldstart.d.ts +117 -0
- package/dist/src/cohort-topic/coldstart.d.ts.map +1 -0
- package/dist/src/cohort-topic/coldstart.js +127 -0
- package/dist/src/cohort-topic/coldstart.js.map +1 -0
- package/dist/src/cohort-topic/dmax.d.ts +44 -0
- package/dist/src/cohort-topic/dmax.d.ts.map +1 -0
- package/dist/src/cohort-topic/dmax.js +70 -0
- package/dist/src/cohort-topic/dmax.js.map +1 -0
- package/dist/src/cohort-topic/gossip/bus.d.ts +85 -0
- package/dist/src/cohort-topic/gossip/bus.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/bus.js +146 -0
- package/dist/src/cohort-topic/gossip/bus.js.map +1 -0
- package/dist/src/cohort-topic/gossip/index.d.ts +4 -0
- package/dist/src/cohort-topic/gossip/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/index.js +4 -0
- package/dist/src/cohort-topic/gossip/index.js.map +1 -0
- package/dist/src/cohort-topic/gossip/records.d.ts +12 -0
- package/dist/src/cohort-topic/gossip/records.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/records.js +41 -0
- package/dist/src/cohort-topic/gossip/records.js.map +1 -0
- package/dist/src/cohort-topic/gossip/view.d.ts +39 -0
- package/dist/src/cohort-topic/gossip/view.d.ts.map +1 -0
- package/dist/src/cohort-topic/gossip/view.js +30 -0
- package/dist/src/cohort-topic/gossip/view.js.map +1 -0
- package/dist/src/cohort-topic/index.d.ts +21 -0
- package/dist/src/cohort-topic/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/index.js +21 -0
- package/dist/src/cohort-topic/index.js.map +1 -0
- package/dist/src/cohort-topic/load/barometer.d.ts +78 -0
- package/dist/src/cohort-topic/load/barometer.d.ts.map +1 -0
- package/dist/src/cohort-topic/load/barometer.js +93 -0
- package/dist/src/cohort-topic/load/barometer.js.map +1 -0
- package/dist/src/cohort-topic/load/index.d.ts +2 -0
- package/dist/src/cohort-topic/load/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/load/index.js +2 -0
- package/dist/src/cohort-topic/load/index.js.map +1 -0
- package/dist/src/cohort-topic/member-engine.d.ts +134 -0
- package/dist/src/cohort-topic/member-engine.d.ts.map +1 -0
- package/dist/src/cohort-topic/member-engine.js +264 -0
- package/dist/src/cohort-topic/member-engine.js.map +1 -0
- package/dist/src/cohort-topic/membership/index.d.ts +4 -0
- package/dist/src/cohort-topic/membership/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/index.js +4 -0
- package/dist/src/cohort-topic/membership/index.js.map +1 -0
- package/dist/src/cohort-topic/membership/publisher.d.ts +82 -0
- package/dist/src/cohort-topic/membership/publisher.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/publisher.js +107 -0
- package/dist/src/cohort-topic/membership/publisher.js.map +1 -0
- package/dist/src/cohort-topic/membership/source.d.ts +31 -0
- package/dist/src/cohort-topic/membership/source.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/source.js +32 -0
- package/dist/src/cohort-topic/membership/source.js.map +1 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts +96 -0
- package/dist/src/cohort-topic/membership/verifier.d.ts.map +1 -0
- package/dist/src/cohort-topic/membership/verifier.js +250 -0
- package/dist/src/cohort-topic/membership/verifier.js.map +1 -0
- package/dist/src/cohort-topic/ports.d.ts +149 -0
- package/dist/src/cohort-topic/ports.d.ts.map +1 -0
- package/dist/src/cohort-topic/ports.js +25 -0
- package/dist/src/cohort-topic/ports.js.map +1 -0
- package/dist/src/cohort-topic/promotion.d.ts +135 -0
- package/dist/src/cohort-topic/promotion.d.ts.map +1 -0
- package/dist/src/cohort-topic/promotion.js +269 -0
- package/dist/src/cohort-topic/promotion.js.map +1 -0
- package/dist/src/cohort-topic/registration/bytes.d.ts +16 -0
- package/dist/src/cohort-topic/registration/bytes.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/bytes.js +32 -0
- package/dist/src/cohort-topic/registration/bytes.js.map +1 -0
- package/dist/src/cohort-topic/registration/handoff.d.ts +69 -0
- package/dist/src/cohort-topic/registration/handoff.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/handoff.js +98 -0
- package/dist/src/cohort-topic/registration/handoff.js.map +1 -0
- package/dist/src/cohort-topic/registration/index.d.ts +7 -0
- package/dist/src/cohort-topic/registration/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/index.js +7 -0
- package/dist/src/cohort-topic/registration/index.js.map +1 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts +115 -0
- package/dist/src/cohort-topic/registration/renewal.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/renewal.js +306 -0
- package/dist/src/cohort-topic/registration/renewal.js.map +1 -0
- package/dist/src/cohort-topic/registration/sharding.d.ts +31 -0
- package/dist/src/cohort-topic/registration/sharding.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/sharding.js +48 -0
- package/dist/src/cohort-topic/registration/sharding.js.map +1 -0
- package/dist/src/cohort-topic/registration/store.d.ts +13 -0
- package/dist/src/cohort-topic/registration/store.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/store.js +72 -0
- package/dist/src/cohort-topic/registration/store.js.map +1 -0
- package/dist/src/cohort-topic/registration/types.d.ts +71 -0
- package/dist/src/cohort-topic/registration/types.d.ts.map +1 -0
- package/dist/src/cohort-topic/registration/types.js +27 -0
- package/dist/src/cohort-topic/registration/types.js.map +1 -0
- package/dist/src/cohort-topic/ring-hash.d.ts +27 -0
- package/dist/src/cohort-topic/ring-hash.d.ts.map +1 -0
- package/dist/src/cohort-topic/ring-hash.js +45 -0
- package/dist/src/cohort-topic/ring-hash.js.map +1 -0
- package/dist/src/cohort-topic/service.d.ts +157 -0
- package/dist/src/cohort-topic/service.d.ts.map +1 -0
- package/dist/src/cohort-topic/service.js +237 -0
- package/dist/src/cohort-topic/service.js.map +1 -0
- package/dist/src/cohort-topic/sig/index.d.ts +3 -0
- package/dist/src/cohort-topic/sig/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/index.js +3 -0
- package/dist/src/cohort-topic/sig/index.js.map +1 -0
- package/dist/src/cohort-topic/sig/payloads.d.ts +25 -0
- package/dist/src/cohort-topic/sig/payloads.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/payloads.js +25 -0
- package/dist/src/cohort-topic/sig/payloads.js.map +1 -0
- package/dist/src/cohort-topic/sig/threshold.d.ts +33 -0
- package/dist/src/cohort-topic/sig/threshold.d.ts.map +1 -0
- package/dist/src/cohort-topic/sig/threshold.js +51 -0
- package/dist/src/cohort-topic/sig/threshold.js.map +1 -0
- package/dist/src/cohort-topic/tiers.d.ts +58 -0
- package/dist/src/cohort-topic/tiers.d.ts.map +1 -0
- package/dist/src/cohort-topic/tiers.js +49 -0
- package/dist/src/cohort-topic/tiers.js.map +1 -0
- package/dist/src/cohort-topic/traffic.d.ts +90 -0
- package/dist/src/cohort-topic/traffic.d.ts.map +1 -0
- package/dist/src/cohort-topic/traffic.js +146 -0
- package/dist/src/cohort-topic/traffic.js.map +1 -0
- package/dist/src/cohort-topic/walk.d.ts +123 -0
- package/dist/src/cohort-topic/walk.d.ts.map +1 -0
- package/dist/src/cohort-topic/walk.js +150 -0
- package/dist/src/cohort-topic/walk.js.map +1 -0
- package/dist/src/cohort-topic/willingness.d.ts +123 -0
- package/dist/src/cohort-topic/willingness.d.ts.map +1 -0
- package/dist/src/cohort-topic/willingness.js +143 -0
- package/dist/src/cohort-topic/willingness.js.map +1 -0
- package/dist/src/cohort-topic/wire/codec.d.ts +42 -0
- package/dist/src/cohort-topic/wire/codec.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/codec.js +163 -0
- package/dist/src/cohort-topic/wire/codec.js.map +1 -0
- package/dist/src/cohort-topic/wire/index.d.ts +6 -0
- package/dist/src/cohort-topic/wire/index.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/index.js +6 -0
- package/dist/src/cohort-topic/wire/index.js.map +1 -0
- package/dist/src/cohort-topic/wire/payloads.d.ts +38 -0
- package/dist/src/cohort-topic/wire/payloads.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/payloads.js +102 -0
- package/dist/src/cohort-topic/wire/payloads.js.map +1 -0
- package/dist/src/cohort-topic/wire/types.d.ts +304 -0
- package/dist/src/cohort-topic/wire/types.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/types.js +16 -0
- package/dist/src/cohort-topic/wire/types.js.map +1 -0
- package/dist/src/cohort-topic/wire/validate.d.ts +25 -0
- package/dist/src/cohort-topic/wire/validate.d.ts.map +1 -0
- package/dist/src/cohort-topic/wire/validate.js +427 -0
- package/dist/src/cohort-topic/wire/validate.js.map +1 -0
- package/dist/src/collection/collection.d.ts +58 -1
- package/dist/src/collection/collection.d.ts.map +1 -1
- package/dist/src/collection/collection.js +76 -0
- package/dist/src/collection/collection.js.map +1 -1
- package/dist/src/collections/tree/tree.d.ts +58 -2
- package/dist/src/collections/tree/tree.d.ts.map +1 -1
- package/dist/src/collections/tree/tree.js +59 -2
- package/dist/src/collections/tree/tree.js.map +1 -1
- package/dist/src/index.d.ts +3 -0
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/index.js +3 -0
- package/dist/src/index.js.map +1 -1
- package/dist/src/log/log.d.ts +42 -4
- package/dist/src/log/log.d.ts.map +1 -1
- package/dist/src/log/log.js +71 -3
- package/dist/src/log/log.js.map +1 -1
- package/dist/src/log/struct.d.ts +122 -2
- package/dist/src/log/struct.d.ts.map +1 -1
- package/dist/src/log/struct.js.map +1 -1
- package/dist/src/matchmaking/capability-filter.d.ts +24 -0
- package/dist/src/matchmaking/capability-filter.d.ts.map +1 -0
- package/dist/src/matchmaking/capability-filter.js +37 -0
- package/dist/src/matchmaking/capability-filter.js.map +1 -0
- package/dist/src/matchmaking/config.d.ts +77 -0
- package/dist/src/matchmaking/config.d.ts.map +1 -0
- package/dist/src/matchmaking/config.js +69 -0
- package/dist/src/matchmaking/config.js.map +1 -0
- package/dist/src/matchmaking/index.d.ts +21 -0
- package/dist/src/matchmaking/index.d.ts.map +1 -0
- package/dist/src/matchmaking/index.js +21 -0
- package/dist/src/matchmaking/index.js.map +1 -0
- package/dist/src/matchmaking/multi-cohort-seeker.d.ts +133 -0
- package/dist/src/matchmaking/multi-cohort-seeker.d.ts.map +1 -0
- package/dist/src/matchmaking/multi-cohort-seeker.js +142 -0
- package/dist/src/matchmaking/multi-cohort-seeker.js.map +1 -0
- package/dist/src/matchmaking/provider.d.ts +70 -0
- package/dist/src/matchmaking/provider.d.ts.map +1 -0
- package/dist/src/matchmaking/provider.js +93 -0
- package/dist/src/matchmaking/provider.js.map +1 -0
- package/dist/src/matchmaking/query-eval.d.ts +53 -0
- package/dist/src/matchmaking/query-eval.d.ts.map +1 -0
- package/dist/src/matchmaking/query-eval.js +69 -0
- package/dist/src/matchmaking/query-eval.js.map +1 -0
- package/dist/src/matchmaking/seeker-walk.d.ts +95 -0
- package/dist/src/matchmaking/seeker-walk.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker-walk.js +75 -0
- package/dist/src/matchmaking/seeker-walk.js.map +1 -0
- package/dist/src/matchmaking/seeker.d.ts +51 -0
- package/dist/src/matchmaking/seeker.d.ts.map +1 -0
- package/dist/src/matchmaking/seeker.js +62 -0
- package/dist/src/matchmaking/seeker.js.map +1 -0
- package/dist/src/matchmaking/topic-anchor.d.ts +54 -0
- package/dist/src/matchmaking/topic-anchor.d.ts.map +1 -0
- package/dist/src/matchmaking/topic-anchor.js +67 -0
- package/dist/src/matchmaking/topic-anchor.js.map +1 -0
- package/dist/src/matchmaking/voting-quorum.d.ts +233 -0
- package/dist/src/matchmaking/voting-quorum.d.ts.map +1 -0
- package/dist/src/matchmaking/voting-quorum.js +238 -0
- package/dist/src/matchmaking/voting-quorum.js.map +1 -0
- package/dist/src/matchmaking/wire.d.ts +259 -0
- package/dist/src/matchmaking/wire.d.ts.map +1 -0
- package/dist/src/matchmaking/wire.js +484 -0
- package/dist/src/matchmaking/wire.js.map +1 -0
- package/dist/src/network/i-repo.d.ts +9 -0
- package/dist/src/network/i-repo.d.ts.map +1 -1
- package/dist/src/network/repo-protocol.d.ts +3 -1
- package/dist/src/network/repo-protocol.d.ts.map +1 -1
- package/dist/src/network/struct.d.ts +23 -1
- package/dist/src/network/struct.d.ts.map +1 -1
- package/dist/src/reactivity/backfill.d.ts +118 -0
- package/dist/src/reactivity/backfill.d.ts.map +1 -0
- package/dist/src/reactivity/backfill.js +137 -0
- package/dist/src/reactivity/backfill.js.map +1 -0
- package/dist/src/reactivity/backpressure.d.ts +99 -0
- package/dist/src/reactivity/backpressure.d.ts.map +1 -0
- package/dist/src/reactivity/backpressure.js +153 -0
- package/dist/src/reactivity/backpressure.js.map +1 -0
- package/dist/src/reactivity/checkpoint.d.ts +165 -0
- package/dist/src/reactivity/checkpoint.d.ts.map +1 -0
- package/dist/src/reactivity/checkpoint.js +232 -0
- package/dist/src/reactivity/checkpoint.js.map +1 -0
- package/dist/src/reactivity/config.d.ts +131 -0
- package/dist/src/reactivity/config.d.ts.map +1 -0
- package/dist/src/reactivity/config.js +135 -0
- package/dist/src/reactivity/config.js.map +1 -0
- package/dist/src/reactivity/dedupe.d.ts +51 -0
- package/dist/src/reactivity/dedupe.d.ts.map +1 -0
- package/dist/src/reactivity/dedupe.js +90 -0
- package/dist/src/reactivity/dedupe.js.map +1 -0
- package/dist/src/reactivity/forwarder.d.ts +50 -0
- package/dist/src/reactivity/forwarder.d.ts.map +1 -0
- package/dist/src/reactivity/forwarder.js +52 -0
- package/dist/src/reactivity/forwarder.js.map +1 -0
- package/dist/src/reactivity/index.d.ts +34 -0
- package/dist/src/reactivity/index.d.ts.map +1 -0
- package/dist/src/reactivity/index.js +34 -0
- package/dist/src/reactivity/index.js.map +1 -0
- package/dist/src/reactivity/notification.d.ts +72 -0
- package/dist/src/reactivity/notification.d.ts.map +1 -0
- package/dist/src/reactivity/notification.js +92 -0
- package/dist/src/reactivity/notification.js.map +1 -0
- package/dist/src/reactivity/policy.d.ts +59 -0
- package/dist/src/reactivity/policy.d.ts.map +1 -0
- package/dist/src/reactivity/policy.js +64 -0
- package/dist/src/reactivity/policy.js.map +1 -0
- package/dist/src/reactivity/push-state.d.ts +140 -0
- package/dist/src/reactivity/push-state.d.ts.map +1 -0
- package/dist/src/reactivity/push-state.js +270 -0
- package/dist/src/reactivity/push-state.js.map +1 -0
- package/dist/src/reactivity/recover.d.ts +75 -0
- package/dist/src/reactivity/recover.d.ts.map +1 -0
- package/dist/src/reactivity/recover.js +105 -0
- package/dist/src/reactivity/recover.js.map +1 -0
- package/dist/src/reactivity/replay-buffer.d.ts +57 -0
- package/dist/src/reactivity/replay-buffer.d.ts.map +1 -0
- package/dist/src/reactivity/replay-buffer.js +88 -0
- package/dist/src/reactivity/replay-buffer.js.map +1 -0
- package/dist/src/reactivity/resume.d.ts +214 -0
- package/dist/src/reactivity/resume.d.ts.map +1 -0
- package/dist/src/reactivity/resume.js +358 -0
- package/dist/src/reactivity/resume.js.map +1 -0
- package/dist/src/reactivity/rotation.d.ts +283 -0
- package/dist/src/reactivity/rotation.d.ts.map +1 -0
- package/dist/src/reactivity/rotation.js +265 -0
- package/dist/src/reactivity/rotation.js.map +1 -0
- package/dist/src/reactivity/subscriber.d.ts +66 -0
- package/dist/src/reactivity/subscriber.d.ts.map +1 -0
- package/dist/src/reactivity/subscriber.js +75 -0
- package/dist/src/reactivity/subscriber.js.map +1 -0
- package/dist/src/reactivity/subscription.d.ts +52 -0
- package/dist/src/reactivity/subscription.d.ts.map +1 -0
- package/dist/src/reactivity/subscription.js +28 -0
- package/dist/src/reactivity/subscription.js.map +1 -0
- package/dist/src/reactivity/topic-anchor.d.ts +46 -0
- package/dist/src/reactivity/topic-anchor.d.ts.map +1 -0
- package/dist/src/reactivity/topic-anchor.js +61 -0
- package/dist/src/reactivity/topic-anchor.js.map +1 -0
- package/dist/src/reactivity/verify.d.ts +41 -0
- package/dist/src/reactivity/verify.d.ts.map +1 -0
- package/dist/src/reactivity/verify.js +48 -0
- package/dist/src/reactivity/verify.js.map +1 -0
- package/dist/src/reactivity/wire-validate.d.ts +28 -0
- package/dist/src/reactivity/wire-validate.d.ts.map +1 -0
- package/dist/src/reactivity/wire-validate.js +79 -0
- package/dist/src/reactivity/wire-validate.js.map +1 -0
- package/dist/src/reactivity/wire.d.ts +102 -0
- package/dist/src/reactivity/wire.d.ts.map +1 -0
- package/dist/src/reactivity/wire.js +129 -0
- package/dist/src/reactivity/wire.js.map +1 -0
- package/dist/src/transaction/coordinator.d.ts +9 -5
- package/dist/src/transaction/coordinator.d.ts.map +1 -1
- package/dist/src/transaction/coordinator.js +48 -25
- package/dist/src/transaction/coordinator.js.map +1 -1
- package/dist/src/transactor/change-notifier.d.ts +74 -0
- package/dist/src/transactor/change-notifier.d.ts.map +1 -0
- package/dist/src/transactor/change-notifier.js +4 -0
- package/dist/src/transactor/change-notifier.js.map +1 -0
- package/dist/src/transactor/index.d.ts +1 -0
- package/dist/src/transactor/index.d.ts.map +1 -1
- package/dist/src/transactor/index.js +1 -0
- package/dist/src/transactor/index.js.map +1 -1
- package/dist/src/transactor/network-transactor.d.ts +34 -2
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +113 -13
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/package.json +2 -2
- package/src/cluster/structs.ts +13 -0
- package/src/cohort-topic/addressing.ts +112 -0
- package/src/cohort-topic/antidos/bootstrap-evidence-envelope.ts +253 -0
- package/src/cohort-topic/antidos/bootstrap-evidence.ts +94 -0
- package/src/cohort-topic/antidos/index.ts +5 -0
- package/src/cohort-topic/antidos/rate-limiter.ts +210 -0
- package/src/cohort-topic/antidos/replay-guard.ts +109 -0
- package/src/cohort-topic/antidos/topic-budget.ts +145 -0
- package/src/cohort-topic/antiflood/index.ts +2 -0
- package/src/cohort-topic/antiflood/invariants.ts +108 -0
- package/src/cohort-topic/antiflood/jitter.ts +117 -0
- package/src/cohort-topic/coldstart.ts +204 -0
- package/src/cohort-topic/dmax.ts +88 -0
- package/src/cohort-topic/gossip/bus.ts +217 -0
- package/src/cohort-topic/gossip/index.ts +3 -0
- package/src/cohort-topic/gossip/records.ts +45 -0
- package/src/cohort-topic/gossip/view.ts +66 -0
- package/src/cohort-topic/index.ts +20 -0
- package/src/cohort-topic/load/barometer.ts +134 -0
- package/src/cohort-topic/load/index.ts +1 -0
- package/src/cohort-topic/member-engine.ts +409 -0
- package/src/cohort-topic/membership/index.ts +3 -0
- package/src/cohort-topic/membership/publisher.ts +171 -0
- package/src/cohort-topic/membership/source.ts +41 -0
- package/src/cohort-topic/membership/verifier.ts +345 -0
- package/src/cohort-topic/ports.ts +157 -0
- package/src/cohort-topic/promotion.ts +384 -0
- package/src/cohort-topic/registration/bytes.ts +37 -0
- package/src/cohort-topic/registration/handoff.ts +154 -0
- package/src/cohort-topic/registration/index.ts +6 -0
- package/src/cohort-topic/registration/renewal.ts +423 -0
- package/src/cohort-topic/registration/sharding.ts +61 -0
- package/src/cohort-topic/registration/store.ts +81 -0
- package/src/cohort-topic/registration/types.ts +76 -0
- package/src/cohort-topic/ring-hash.ts +50 -0
- package/src/cohort-topic/service.ts +390 -0
- package/src/cohort-topic/sig/index.ts +2 -0
- package/src/cohort-topic/sig/payloads.ts +39 -0
- package/src/cohort-topic/sig/threshold.ts +64 -0
- package/src/cohort-topic/tiers.ts +74 -0
- package/src/cohort-topic/traffic.ts +211 -0
- package/src/cohort-topic/walk.ts +255 -0
- package/src/cohort-topic/willingness.ts +237 -0
- package/src/cohort-topic/wire/codec.ts +204 -0
- package/src/cohort-topic/wire/index.ts +16 -0
- package/src/cohort-topic/wire/payloads.ts +117 -0
- package/src/cohort-topic/wire/types.ts +343 -0
- package/src/cohort-topic/wire/validate.ts +478 -0
- package/src/collection/collection.ts +93 -1
- package/src/collections/tree/tree.ts +84 -3
- package/src/index.ts +3 -0
- package/src/log/log.ts +86 -7
- package/src/log/struct.ts +126 -2
- package/src/matchmaking/capability-filter.ts +45 -0
- package/src/matchmaking/config.ts +98 -0
- package/src/matchmaking/index.ts +21 -0
- package/src/matchmaking/multi-cohort-seeker.ts +234 -0
- package/src/matchmaking/provider.ts +123 -0
- package/src/matchmaking/query-eval.ts +105 -0
- package/src/matchmaking/seeker-walk.ts +127 -0
- package/src/matchmaking/seeker.ts +86 -0
- package/src/matchmaking/topic-anchor.ts +90 -0
- package/src/matchmaking/voting-quorum.ts +394 -0
- package/src/matchmaking/wire.ts +680 -0
- package/src/network/i-repo.ts +9 -0
- package/src/network/repo-protocol.ts +3 -2
- package/src/network/struct.ts +24 -1
- package/src/reactivity/backfill.ts +220 -0
- package/src/reactivity/backpressure.ts +191 -0
- package/src/reactivity/checkpoint.ts +308 -0
- package/src/reactivity/config.ts +172 -0
- package/src/reactivity/dedupe.ts +132 -0
- package/src/reactivity/forwarder.ts +87 -0
- package/src/reactivity/index.ts +34 -0
- package/src/reactivity/notification.ts +123 -0
- package/src/reactivity/policy.ts +79 -0
- package/src/reactivity/push-state.ts +343 -0
- package/src/reactivity/recover.ts +153 -0
- package/src/reactivity/replay-buffer.ts +141 -0
- package/src/reactivity/resume.ts +549 -0
- package/src/reactivity/rotation.ts +415 -0
- package/src/reactivity/subscriber.ts +132 -0
- package/src/reactivity/subscription.ts +66 -0
- package/src/reactivity/topic-anchor.ts +71 -0
- package/src/reactivity/verify.ts +73 -0
- package/src/reactivity/wire-validate.ts +87 -0
- package/src/reactivity/wire.ts +224 -0
- package/src/transaction/coordinator.ts +53 -27
- package/src/transactor/change-notifier.ts +80 -0
- package/src/transactor/index.ts +1 -0
- package/src/transactor/network-transactor.ts +133 -15
|
@@ -0,0 +1,145 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — per-cohort topic budget (anti-DoS).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Anti-DoS bullet 2. A cohort holds forwarder state for at
|
|
5
|
+
* most `topics_max` (default 2048) topics. Once the budget is full a request to instantiate a **new**
|
|
6
|
+
* topic is refused (the caller answers `UnwillingCohort`); topics already in the budget continue to
|
|
7
|
+
* serve. To keep room for genuinely active topics, the budget evicts cold ones: eviction order is
|
|
8
|
+
* **LRU by participant count**, with **zero-recent-registration topics dropped first** — a topic that
|
|
9
|
+
* carries no direct participants is the cheapest to shed and is shed before any populated topic.
|
|
10
|
+
*
|
|
11
|
+
* `admit` only ever evicts a *zero-participant* topic. If every resident topic still carries
|
|
12
|
+
* participants the budget is genuinely saturated and the new instantiation is refused — an attacker
|
|
13
|
+
* cannot, by flooding new topic ids, evict a topic that real participants are using. LRU ordering
|
|
14
|
+
* uses a monotonic access counter bumped on every `admit`/`touch`, so it is deterministic and needs
|
|
15
|
+
* no wall clock.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import { bytesKey } from "../registration/bytes.js";
|
|
19
|
+
import { createLogger } from "../../logger.js";
|
|
20
|
+
|
|
21
|
+
const log = createLogger("cohort-topic:antidos");
|
|
22
|
+
|
|
23
|
+
/** Default per-cohort forwarder-topic ceiling — `topics_max`. */
|
|
24
|
+
export const DEFAULT_TOPICS_MAX = 2048;
|
|
25
|
+
|
|
26
|
+
export interface TopicBudgetConfig {
|
|
27
|
+
/** Max topics with forwarder state. Default {@link DEFAULT_TOPICS_MAX}. */
|
|
28
|
+
topicsMax?: number;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
/** Per-cohort cap on the number of topics with forwarder state, with LRU eviction of cold topics. */
|
|
32
|
+
export interface TopicBudget {
|
|
33
|
+
/**
|
|
34
|
+
* Admit `topicId` to the forwarder-state budget. Returns `true` if it is already resident or there
|
|
35
|
+
* is (or can be freed) room; `false` when the budget is full of populated topics, in which case the
|
|
36
|
+
* caller answers `UnwillingCohort` for the new instantiation. Admitting a *new* topic when full
|
|
37
|
+
* evicts the coldest zero-participant resident first.
|
|
38
|
+
*/
|
|
39
|
+
admit(topicId: Uint8Array): boolean;
|
|
40
|
+
/**
|
|
41
|
+
* Record the current direct-participant count for a resident topic (LRU bookkeeping). Updates the
|
|
42
|
+
* eviction key and bumps the topic's recency; a no-op for a topic that is not resident.
|
|
43
|
+
*/
|
|
44
|
+
touch(topicId: Uint8Array, participantCount: number): void;
|
|
45
|
+
/** Whether `topicId` currently holds forwarder state. */
|
|
46
|
+
has(topicId: Uint8Array): boolean;
|
|
47
|
+
/**
|
|
48
|
+
* The direct-participant count last reported for a resident `topicId` (the eviction key), or
|
|
49
|
+
* `undefined` when the topic is not resident. A resident reporting `0` is cold/evictable. Read-only
|
|
50
|
+
* introspection for tests/diagnostics — eviction policy reads the same field internally.
|
|
51
|
+
*/
|
|
52
|
+
participantCount(topicId: Uint8Array): number | undefined;
|
|
53
|
+
/** Number of resident topics. */
|
|
54
|
+
readonly size: number;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
/** Per-resident eviction bookkeeping. */
|
|
58
|
+
interface ResidentState {
|
|
59
|
+
/** Direct-participant count last reported via `touch` (eviction primary key; ascending = colder). */
|
|
60
|
+
participantCount: number;
|
|
61
|
+
/** Monotonic access sequence (LRU tiebreaker; lower = least-recently used). */
|
|
62
|
+
seq: number;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
class LruTopicBudget implements TopicBudget {
|
|
66
|
+
private readonly residents = new Map<string, ResidentState>();
|
|
67
|
+
private readonly topicsMax: number;
|
|
68
|
+
private seqCounter = 0;
|
|
69
|
+
|
|
70
|
+
constructor(config: TopicBudgetConfig = {}) {
|
|
71
|
+
this.topicsMax = config.topicsMax ?? DEFAULT_TOPICS_MAX;
|
|
72
|
+
if (!Number.isInteger(this.topicsMax) || this.topicsMax <= 0) {
|
|
73
|
+
throw new RangeError(`topicsMax must be a positive integer, got ${this.topicsMax}`);
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
get size(): number {
|
|
78
|
+
return this.residents.size;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
has(topicId: Uint8Array): boolean {
|
|
82
|
+
return this.residents.has(bytesKey(topicId));
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
participantCount(topicId: Uint8Array): number | undefined {
|
|
86
|
+
return this.residents.get(bytesKey(topicId))?.participantCount;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
admit(topicId: Uint8Array): boolean {
|
|
90
|
+
const key = bytesKey(topicId);
|
|
91
|
+
const existing = this.residents.get(key);
|
|
92
|
+
if (existing !== undefined) {
|
|
93
|
+
existing.seq = ++this.seqCounter; // already resident — refresh recency, always allowed
|
|
94
|
+
return true;
|
|
95
|
+
}
|
|
96
|
+
if (this.residents.size < this.topicsMax) {
|
|
97
|
+
this.residents.set(key, { participantCount: 0, seq: ++this.seqCounter });
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
// Full: only a zero-participant resident may be evicted to make room for a new topic.
|
|
101
|
+
const victim = this.coldestEvictable();
|
|
102
|
+
if (victim === undefined) {
|
|
103
|
+
log("topic-budget full size=%d max=%d — refuse new topic", this.residents.size, this.topicsMax);
|
|
104
|
+
return false;
|
|
105
|
+
}
|
|
106
|
+
this.residents.delete(victim);
|
|
107
|
+
this.residents.set(key, { participantCount: 0, seq: ++this.seqCounter });
|
|
108
|
+
log("topic-budget evicted cold topic to admit new (size=%d max=%d)", this.residents.size, this.topicsMax);
|
|
109
|
+
return true;
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
touch(topicId: Uint8Array, participantCount: number): void {
|
|
113
|
+
const key = bytesKey(topicId);
|
|
114
|
+
const state = this.residents.get(key);
|
|
115
|
+
if (state === undefined) {
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
state.participantCount = participantCount;
|
|
119
|
+
state.seq = ++this.seqCounter;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* The coldest evictable resident key: among zero-participant topics, the least-recently used.
|
|
124
|
+
* `undefined` when every resident still carries participants (nothing may be evicted for a new topic).
|
|
125
|
+
*/
|
|
126
|
+
private coldestEvictable(): string | undefined {
|
|
127
|
+
let victim: string | undefined;
|
|
128
|
+
let victimSeq = Infinity;
|
|
129
|
+
for (const [key, state] of this.residents) {
|
|
130
|
+
if (state.participantCount > 0) {
|
|
131
|
+
continue; // populated topics continue — never evicted for a new instantiation
|
|
132
|
+
}
|
|
133
|
+
if (state.seq < victimSeq) {
|
|
134
|
+
victim = key;
|
|
135
|
+
victimSeq = state.seq;
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
return victim;
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
/** Build a {@link TopicBudget} over the configured `topics_max`. */
|
|
143
|
+
export function createTopicBudget(config: TopicBudgetConfig = {}): TopicBudget {
|
|
144
|
+
return new LruTopicBudget(config);
|
|
145
|
+
}
|
|
@@ -0,0 +1,108 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — anti-flood structural invariants (centralized for the e2e suite).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Anti-flood properties. Three of the five named floods are
|
|
5
|
+
* defended *by construction* in the walk / promotion machinery already; this module does not
|
|
6
|
+
* re-implement that machinery — it provides pure, dependency-free predicates over a recorded walk
|
|
7
|
+
* trace so the unit and e2e suites can assert the invariants hold on the real engine's behaviour,
|
|
8
|
+
* the same way `packages/substrate-simulator/src/walk-metrics.ts` instruments
|
|
9
|
+
* `outwardMovesArePromoted` / `unwillingRetriesRestartAtDMax` on the simulator side.
|
|
10
|
+
*
|
|
11
|
+
* The caller records one {@link WalkProbe} per probe RPC the {@link import("../walk.js").WalkEngine}
|
|
12
|
+
* issues (its `treeTier` and the reply `result`), grouping the probes of one
|
|
13
|
+
* {@link import("../walk.js").WalkEngine.register} call into a {@link WalkTrace}. The predicates then
|
|
14
|
+
* check the anti-flood discipline:
|
|
15
|
+
*
|
|
16
|
+
* - **claim 3 — no speculative outward probe:** {@link outwardMovesArePromoted}. The only tier
|
|
17
|
+
* *increase* between consecutive probes is one taken immediately after a `promoted` reply.
|
|
18
|
+
* - **walk discipline — inward only on `no_state`:** {@link inwardStepsFollowNoState}. Every single
|
|
19
|
+
* tier *decrease* is the response to a `no_state` (no inward move on any other reply).
|
|
20
|
+
* - **claim 4 — inward retry restarts at `d_max`:** {@link retriesRestartAtDMax}. A fresh walk (the
|
|
21
|
+
* restart after an `unwilling_cohort` / cohort-level back-off) begins at `d_max`, never re-hitting
|
|
22
|
+
* the declined coord.
|
|
23
|
+
* - **claim 5 — sticky promotion:** {@link DEFAULT_T_PROMOTE_STICKY_MS} re-exported as the canonical
|
|
24
|
+
* sticky window; the promotion lifecycle (`promotion.ts`) enforces it and `promotion.spec.ts`
|
|
25
|
+
* covers the no-flap behaviour. {@link stickyHolds} is the predicate the e2e suite uses to confirm
|
|
26
|
+
* a freshly-promoted cohort refuses demotion within the window.
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
import type { RegisterResult } from "../wire/types.js";
|
|
30
|
+
import { DEFAULT_T_PROMOTE_STICKY_MS } from "../promotion.js";
|
|
31
|
+
|
|
32
|
+
export { DEFAULT_T_PROMOTE_STICKY_MS };
|
|
33
|
+
|
|
34
|
+
/** One probe RPC in a walk: the tier it was issued at and the reply result it drew. */
|
|
35
|
+
export interface WalkProbe {
|
|
36
|
+
/** Walk position `d` the probe was issued at. */
|
|
37
|
+
readonly treeTier: number;
|
|
38
|
+
/** The cohort reply classification. */
|
|
39
|
+
readonly result: RegisterResult;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** The ordered probe log of a single {@link import("../walk.js").WalkEngine.register} call. */
|
|
43
|
+
export interface WalkTrace {
|
|
44
|
+
/** `d_max` the walk started from. */
|
|
45
|
+
readonly dMax: number;
|
|
46
|
+
/** Probes in issue order. */
|
|
47
|
+
readonly probes: readonly WalkProbe[];
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Claim 3 (no speculative outward probe): every tier *increase* between consecutive probes is taken
|
|
52
|
+
* only in response to a `promoted` redirect on the preceding probe. Any outward move not preceded by
|
|
53
|
+
* `promoted` is a speculative deeper probe — the flood this invariant forbids.
|
|
54
|
+
*/
|
|
55
|
+
export function outwardMovesArePromoted(trace: WalkTrace): boolean {
|
|
56
|
+
const { probes } = trace;
|
|
57
|
+
for (let i = 1; i < probes.length; i++) {
|
|
58
|
+
const moveOutward = probes[i]!.treeTier > probes[i - 1]!.treeTier;
|
|
59
|
+
if (moveOutward && probes[i - 1]!.result !== "promoted") {
|
|
60
|
+
return false;
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return true;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* Walk discipline: every single inward step (`treeTier` decreased by exactly 1, or reset to 0 at the
|
|
68
|
+
* root cold-start) is the response to a `no_state`. The walk never moves toward the root on any other
|
|
69
|
+
* reply, so inward traffic is one-hop-per-`no_state`, not a storm.
|
|
70
|
+
*/
|
|
71
|
+
export function inwardStepsFollowNoState(trace: WalkTrace): boolean {
|
|
72
|
+
const { probes } = trace;
|
|
73
|
+
for (let i = 1; i < probes.length; i++) {
|
|
74
|
+
const moveInward = probes[i]!.treeTier < probes[i - 1]!.treeTier;
|
|
75
|
+
if (moveInward && probes[i - 1]!.result !== "no_state") {
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
/**
|
|
83
|
+
* Claim 4 (inward retry restarts at `d_max`): each fresh walk in a re-registration sequence starts at
|
|
84
|
+
* its `d_max`, so a participant that drew `unwilling_cohort` backs off in *time* and re-walks from the
|
|
85
|
+
* top rather than re-hitting the declined coord. `traces` are consecutive `register` calls by one
|
|
86
|
+
* participant; the first probe of each must sit at that walk's `d_max`.
|
|
87
|
+
*/
|
|
88
|
+
export function retriesRestartAtDMax(traces: readonly WalkTrace[]): boolean {
|
|
89
|
+
for (const trace of traces) {
|
|
90
|
+
const first = trace.probes[0];
|
|
91
|
+
if (first === undefined) {
|
|
92
|
+
continue;
|
|
93
|
+
}
|
|
94
|
+
if (first.treeTier !== trace.dMax) {
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
return true;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* Claim 5 (sticky promotion): a cohort promoted at `promotedAt` must not be reconsidered for demotion
|
|
103
|
+
* before `promotedAt + stickyMs`. Returns whether `now` is still inside the sticky window — the e2e
|
|
104
|
+
* suite asserts the promotion lifecycle refuses demotion exactly while this is `true`.
|
|
105
|
+
*/
|
|
106
|
+
export function stickyHolds(promotedAt: number, now: number, stickyMs: number = DEFAULT_T_PROMOTE_STICKY_MS): boolean {
|
|
107
|
+
return now - promotedAt < stickyMs;
|
|
108
|
+
}
|
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — re-registration jitter (anti-flood claim 2).
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Anti-flood properties (claim 2: "Re-registration storm
|
|
5
|
+
* after cohort failure") and folded back from the simulator-validated
|
|
6
|
+
* `packages/substrate-simulator/src/walk.ts` (`rejoinStagger` / `rateLimitedStagger`). When a cohort
|
|
7
|
+
* fails, every participant it served re-registers at once; without staggering, the recovering or
|
|
8
|
+
* replacement cohort takes the whole wave in a single instant and is shoved straight past
|
|
9
|
+
* `cap_promote`. Spreading the wave over `T_rejoin_jitter` (default 30 s, widened with the observed
|
|
10
|
+
* FRET cohort-failure rate) bounds the recovering cohort's inbound rate to
|
|
11
|
+
* `cap_promote / T_rejoin_jitter`.
|
|
12
|
+
*
|
|
13
|
+
* Two staggering forms, both exposed here:
|
|
14
|
+
* - {@link RejoinJitter.scheduleRejoin} — a single participant draws a uniform offset over the
|
|
15
|
+
* jitter window. Decorrelates one participant's retry from its peers (the production API: a
|
|
16
|
+
* participant that loses its cohort calls this once). Random, so the bound holds *in expectation*.
|
|
17
|
+
* - {@link RejoinJitter.scheduleWave} — a whole wave of `count` rejoiners is placed at a fixed
|
|
18
|
+
* interval `windowMs / cap_promote`, so **any** `windowMs`-long window contains at most
|
|
19
|
+
* `cap_promote` arrivals *by construction*. This is the hard-bound form the e2e suite asserts
|
|
20
|
+
* against; it widens the span past one window when `count > cap_promote` so the rate ceiling
|
|
21
|
+
* `cap_promote / T_rejoin_jitter` is never exceeded.
|
|
22
|
+
*
|
|
23
|
+
* This module is FRET-free and side-effect-free apart from the injected RNG: the caller schedules a
|
|
24
|
+
* timer at the returned timestamp.
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
import { createLogger } from "../../logger.js";
|
|
28
|
+
|
|
29
|
+
const log = createLogger("cohort-topic:antiflood");
|
|
30
|
+
|
|
31
|
+
/** Default jitter window `T_rejoin_jitter` (ms) — `docs/cohort-topic.md` §Configuration. */
|
|
32
|
+
export const DEFAULT_T_REJOIN_JITTER_MS = 30_000;
|
|
33
|
+
/** Default `cap_promote` — sets the inbound-rate ceiling `cap_promote / T_rejoin_jitter`. */
|
|
34
|
+
export const DEFAULT_REJOIN_CAP_PROMOTE = 64;
|
|
35
|
+
|
|
36
|
+
export interface RejoinJitterConfig {
|
|
37
|
+
/** Base jitter window `T_rejoin_jitter` (ms). Default {@link DEFAULT_T_REJOIN_JITTER_MS}. */
|
|
38
|
+
tRejoinJitterMs?: number;
|
|
39
|
+
/** Acceptance ceiling per window; the rate bound is `capPromote / T_rejoin_jitter`. Default 64. */
|
|
40
|
+
capPromote?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Multiplier applied to the base window to track the observed FRET cohort-failure rate (claim 2:
|
|
43
|
+
* "scaled with cohort failure rate observed from FRET"). A higher failure rate → a wider window so
|
|
44
|
+
* the larger expected re-registration wave still respects the rate ceiling. Default 1 (steady FRET).
|
|
45
|
+
*/
|
|
46
|
+
failureRateScale?: number;
|
|
47
|
+
/** Uniform RNG in `[0, 1)`. Injected for determinism in tests; defaults to `Math.random`. */
|
|
48
|
+
random?: () => number;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Staggers post-cohort-failure re-registration so the recovering cohort isn't stormed. */
|
|
52
|
+
export interface RejoinJitter {
|
|
53
|
+
/**
|
|
54
|
+
* Jittered re-registration timestamp for **one** participant: `now + ⌊U[0, windowMs)⌋`. Random,
|
|
55
|
+
* so a wave of independent callers spreads roughly uniformly over the window.
|
|
56
|
+
*/
|
|
57
|
+
scheduleRejoin(now: number): number;
|
|
58
|
+
/**
|
|
59
|
+
* Rate-bounded timestamps for a whole re-registration wave of `count` participants. Arrivals are
|
|
60
|
+
* evenly spaced at `windowMs / capPromote`, so any `windowMs`-long sliding window holds at most
|
|
61
|
+
* `capPromote` of them — the `cap_promote / T_rejoin_jitter` ceiling, exactly rather than in
|
|
62
|
+
* expectation. Returns one timestamp per participant, ascending.
|
|
63
|
+
*/
|
|
64
|
+
scheduleWave(count: number, now: number): number[];
|
|
65
|
+
/** Effective jitter window (ms) = base `T_rejoin_jitter` × `failureRateScale`. */
|
|
66
|
+
readonly windowMs: number;
|
|
67
|
+
/** Per-window acceptance ceiling (`capPromote`); the inbound-rate bound's numerator. */
|
|
68
|
+
readonly capPromote: number;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
class StaggeredRejoinJitter implements RejoinJitter {
|
|
72
|
+
readonly windowMs: number;
|
|
73
|
+
readonly capPromote: number;
|
|
74
|
+
private readonly random: () => number;
|
|
75
|
+
|
|
76
|
+
constructor(config: RejoinJitterConfig = {}) {
|
|
77
|
+
const base = config.tRejoinJitterMs ?? DEFAULT_T_REJOIN_JITTER_MS;
|
|
78
|
+
if (!(base > 0)) {
|
|
79
|
+
throw new RangeError(`tRejoinJitterMs must be > 0, got ${base}`);
|
|
80
|
+
}
|
|
81
|
+
const scale = config.failureRateScale ?? 1;
|
|
82
|
+
if (!(scale >= 1)) {
|
|
83
|
+
throw new RangeError(`failureRateScale must be >= 1, got ${scale}`);
|
|
84
|
+
}
|
|
85
|
+
this.capPromote = config.capPromote ?? DEFAULT_REJOIN_CAP_PROMOTE;
|
|
86
|
+
if (!Number.isInteger(this.capPromote) || this.capPromote <= 0) {
|
|
87
|
+
throw new RangeError(`capPromote must be a positive integer, got ${this.capPromote}`);
|
|
88
|
+
}
|
|
89
|
+
this.windowMs = base * scale;
|
|
90
|
+
this.random = config.random ?? ((): number => Math.random());
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
scheduleRejoin(now: number): number {
|
|
94
|
+
const offset = Math.floor(this.random() * this.windowMs);
|
|
95
|
+
return now + offset;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
scheduleWave(count: number, now: number): number[] {
|
|
99
|
+
if (!Number.isInteger(count) || count < 0) {
|
|
100
|
+
throw new RangeError(`count must be a non-negative integer, got ${count}`);
|
|
101
|
+
}
|
|
102
|
+
// Even spacing at windowMs/capPromote guarantees ≤ capPromote arrivals in any windowMs window;
|
|
103
|
+
// for count > capPromote the wave necessarily spans more than one window, holding the rate.
|
|
104
|
+
const interval = this.windowMs / this.capPromote;
|
|
105
|
+
const out = new Array<number>(count);
|
|
106
|
+
for (let i = 0; i < count; i++) {
|
|
107
|
+
out[i] = now + Math.floor(i * interval);
|
|
108
|
+
}
|
|
109
|
+
log("scheduleWave count=%d window=%d cap=%d span=%d", count, this.windowMs, this.capPromote, count > 0 ? out[count - 1]! - now : 0);
|
|
110
|
+
return out;
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/** Build a {@link RejoinJitter} over the configured window, rate ceiling, and (test-injectable) RNG. */
|
|
115
|
+
export function createRejoinJitter(config: RejoinJitterConfig = {}): RejoinJitter {
|
|
116
|
+
return new StaggeredRejoinJitter(config);
|
|
117
|
+
}
|
|
@@ -0,0 +1,204 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — cold-start instantiation.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Cold-start instantiation and folded back from the
|
|
5
|
+
* simulator-validated `packages/substrate-simulator/src/walk.ts` (the `followOn` / `bootstrap`
|
|
6
|
+
* instantiation paths). A **cold** cohort — one holding no forwarder state for a topic — instantiates
|
|
7
|
+
* as a forwarder for `T` when:
|
|
8
|
+
*
|
|
9
|
+
* - it receives a `RegisterV1` for `T` it does not yet serve, AND
|
|
10
|
+
* - the registration is a legitimate growth point: its `bootstrap: true` flag is set (the root
|
|
11
|
+
* cold-start case) **or** it arrived as a follow-on to a parent cohort's `Promoted` redirect, AND
|
|
12
|
+
* - a quorum of cohort members is willing to serve `T` at the registration's tier.
|
|
13
|
+
*
|
|
14
|
+
* The `followOn` signal is **not** carried on the wire (`RegisterV1` has only `bootstrap`); a cold
|
|
15
|
+
* high-tier cohort returns `NoState` to a *speculative* `d_max` probe, but the single-direction walk
|
|
16
|
+
* means the only registrations that reach a cold tier-`(d+1)` cohort are those that followed the
|
|
17
|
+
* parent's `Promoted` redirect. Determining `followOn` for a given inbound register therefore belongs
|
|
18
|
+
* to the db-p2p cohort host (it has the routing context); db-core takes it as an explicit input to
|
|
19
|
+
* {@link shouldInstantiate}, keeping this module FRET-free. (See the implement handoff for the wiring
|
|
20
|
+
* gap this leaves for the db-p2p binding.)
|
|
21
|
+
*
|
|
22
|
+
* Once instantiated, the new forwarder **registers itself with its tier-`(d − 1)` parent at first
|
|
23
|
+
* opportunity**; until that registration is acked it {@link Forwarder.acceptsParticipants | accepts
|
|
24
|
+
* participants} but {@link Forwarder.servesParentOps | holds} notifications/queries that need parent
|
|
25
|
+
* involvement. The root (tree tier 0) has no parent and is serving immediately.
|
|
26
|
+
*
|
|
27
|
+
* **Just-promoted burst (GROUNDING-resolved: bounce, don't buffer).** A cohort that has just promoted
|
|
28
|
+
* but whose tier-`(d+1)` isn't fully instantiated yet, on a burst of new same-tier registrations,
|
|
29
|
+
* replies `Promoted(d+1)` ({@link promotedRedirectReply}) — a cheap single RPC. It does **not** buffer
|
|
30
|
+
* the registrations and does **not** decline with `UnwillingCohort`; the promotion sticky window
|
|
31
|
+
* (`T_promote_sticky`) keeps it in promoted mode through the burst.
|
|
32
|
+
*/
|
|
33
|
+
|
|
34
|
+
import { attachTopicTraffic } from "./traffic.js";
|
|
35
|
+
import { bytesKey } from "./registration/bytes.js";
|
|
36
|
+
import type { RegisterReplyV1, TopicTrafficV1 } from "./wire/types.js";
|
|
37
|
+
|
|
38
|
+
/** Inputs to the cold-start admission gate. */
|
|
39
|
+
export interface ColdStartTrigger {
|
|
40
|
+
/** `RegisterV1.bootstrap` — the root cold-start request flag. */
|
|
41
|
+
readonly bootstrap: boolean;
|
|
42
|
+
/** This register arrived as a follow-on to a parent's `Promoted` redirect (db-p2p-determined). */
|
|
43
|
+
readonly followOn: boolean;
|
|
44
|
+
/** A quorum of cohort members is willing to serve the topic at this tier. */
|
|
45
|
+
readonly quorumWilling: boolean;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Whether a cold cohort should instantiate forwarder state for an inbound register: it must be a
|
|
50
|
+
* legitimate growth point (`bootstrap` root case **or** a `Promoted` follow-on) **and** have a willing
|
|
51
|
+
* quorum. A speculative `d_max` probe (neither flag set) yields `false`, so the walk gets `NoState`
|
|
52
|
+
* and steps toward the root instead of forking a parallel branch (§Cold-start; §Why the caller
|
|
53
|
+
* doesn't walk on UnwillingCohort).
|
|
54
|
+
*/
|
|
55
|
+
export function shouldInstantiate(trigger: ColdStartTrigger): boolean {
|
|
56
|
+
return (trigger.bootstrap || trigger.followOn) && trigger.quorumWilling;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/** Lifecycle phase of a freshly-instantiated forwarder. */
|
|
60
|
+
export type ForwarderPhase =
|
|
61
|
+
/** Instantiated, registering with the parent; accepts participants, holds parent-involving ops. */
|
|
62
|
+
| "awaiting_parent"
|
|
63
|
+
/** Fully linked (parent acked, or root with no parent); serves everything. */
|
|
64
|
+
| "serving";
|
|
65
|
+
|
|
66
|
+
/**
|
|
67
|
+
* A freshly cold-started forwarder for one topic at this cohort. Holds the "registered with parent
|
|
68
|
+
* yet?" state that gates parent-involving operations during the link-up window.
|
|
69
|
+
*/
|
|
70
|
+
export interface Forwarder {
|
|
71
|
+
/** Tree tier `d` this forwarder serves the topic at. */
|
|
72
|
+
readonly tier: number;
|
|
73
|
+
/** Current lifecycle phase. */
|
|
74
|
+
phase(): ForwarderPhase;
|
|
75
|
+
/** Accept new participants? True from instantiation onward (even before the parent ack). */
|
|
76
|
+
acceptsParticipants(): boolean;
|
|
77
|
+
/** Serve an operation needing parent involvement (notifications/queries)? Only once `serving`. */
|
|
78
|
+
servesParentOps(): boolean;
|
|
79
|
+
/** Mark the parent-registration RPC acked → transition to `serving`. Idempotent; no-op at the root. */
|
|
80
|
+
onParentAck(): void;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
class ForwarderState implements Forwarder {
|
|
84
|
+
private serving: boolean;
|
|
85
|
+
|
|
86
|
+
constructor(readonly tier: number, hasParent: boolean) {
|
|
87
|
+
// The root (tree tier 0 / no parent) has nothing to hand off to — it serves immediately.
|
|
88
|
+
this.serving = !hasParent;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
phase(): ForwarderPhase {
|
|
92
|
+
return this.serving ? "serving" : "awaiting_parent";
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
acceptsParticipants(): boolean {
|
|
96
|
+
return true;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
servesParentOps(): boolean {
|
|
100
|
+
return this.serving;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
onParentAck(): void {
|
|
104
|
+
this.serving = true;
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Construct a forwarder. A tier-`0` forwarder (the root) has no parent and starts `serving`; a deeper
|
|
110
|
+
* forwarder starts `awaiting_parent` until {@link Forwarder.onParentAck} fires.
|
|
111
|
+
*/
|
|
112
|
+
export function createForwarder(tier: number): Forwarder {
|
|
113
|
+
if (!Number.isInteger(tier) || tier < 0) {
|
|
114
|
+
throw new RangeError(`forwarder tier must be a non-negative integer, got ${tier}`);
|
|
115
|
+
}
|
|
116
|
+
return new ForwarderState(tier, tier > 0);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Build a `Promoted(targetTier)` redirect reply — used both for the normal promotion redirect and for
|
|
121
|
+
* the just-promoted-burst bounce (the registrations arriving at a promoted cohort while its child tier
|
|
122
|
+
* is still instantiating). The outgoing cohort's `topicTraffic` is attached when supplied, so the
|
|
123
|
+
* redirected participant can estimate whether the target tier is hot (§Topic traffic signal).
|
|
124
|
+
*/
|
|
125
|
+
export function promotedRedirectReply(targetTier: number, traffic?: TopicTrafficV1): RegisterReplyV1 {
|
|
126
|
+
if (!Number.isInteger(targetTier) || targetTier < 1) {
|
|
127
|
+
throw new RangeError(`promoted targetTier must be an integer ≥ 1, got ${targetTier}`);
|
|
128
|
+
}
|
|
129
|
+
const reply: RegisterReplyV1 = { v: 1, result: "promoted", targetTier };
|
|
130
|
+
return traffic !== undefined ? attachTopicTraffic(reply, traffic) : reply;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/** Registers a newly-instantiated forwarder with its tier-`(d − 1)` parent; resolves on the parent ack. */
|
|
134
|
+
export interface ParentRegistrar {
|
|
135
|
+
/**
|
|
136
|
+
* Register the forwarder for `topicId` (served at tree tier `tier`, i.e. `d`) with the cohort at
|
|
137
|
+
* `parentCoord`; resolves on ack. `opTier` is the topic's *capacity* tier (T0–T3), threaded so the
|
|
138
|
+
* transport can stamp a well-formed forwarder-link frame; absent when the caller has no op-tier
|
|
139
|
+
* context (the cohort host always supplies it from the instantiating `RegisterV1`).
|
|
140
|
+
*/
|
|
141
|
+
registerWithParent(topicId: Uint8Array, parentCoord: Uint8Array, tier: number, opTier?: number): Promise<void>;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
export interface ColdStartManagerDeps {
|
|
145
|
+
/** Drives the parent-registration RPC (db-p2p binds it to the router). */
|
|
146
|
+
parentRegistrar: ParentRegistrar;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
/**
|
|
150
|
+
* Tracks cold-started forwarders for a cohort and drives each one's parent registration. Keyed by
|
|
151
|
+
* `topicId`; idempotent per topic (a second instantiate for an already-tracked topic returns the
|
|
152
|
+
* existing forwarder).
|
|
153
|
+
*/
|
|
154
|
+
export interface ColdStartManager {
|
|
155
|
+
/**
|
|
156
|
+
* Instantiate (or return the existing) forwarder for `topicId` at tree tier `tier` (`d`). For a
|
|
157
|
+
* deeper-than-root forwarder this kicks off parent registration in the background and flips the
|
|
158
|
+
* forwarder to `serving` on ack. The returned forwarder accepts participants immediately. `opTier`
|
|
159
|
+
* (the topic's T0–T3 capacity tier) is forwarded to the parent registrar for the link frame.
|
|
160
|
+
*/
|
|
161
|
+
instantiate(topicId: Uint8Array, tier: number, parentCoord?: Uint8Array, opTier?: number): Forwarder;
|
|
162
|
+
/** The tracked forwarder for `topicId`, or `undefined`. */
|
|
163
|
+
get(topicId: Uint8Array): Forwarder | undefined;
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
class TrackingColdStartManager implements ColdStartManager {
|
|
167
|
+
private readonly forwarders = new Map<string, Forwarder>();
|
|
168
|
+
|
|
169
|
+
constructor(private readonly deps: ColdStartManagerDeps) {}
|
|
170
|
+
|
|
171
|
+
instantiate(topicId: Uint8Array, tier: number, parentCoord?: Uint8Array, opTier?: number): Forwarder {
|
|
172
|
+
const key = bytesKey(topicId);
|
|
173
|
+
const existing = this.forwarders.get(key);
|
|
174
|
+
if (existing !== undefined) {
|
|
175
|
+
return existing;
|
|
176
|
+
}
|
|
177
|
+
const forwarder = createForwarder(tier);
|
|
178
|
+
this.forwarders.set(key, forwarder);
|
|
179
|
+
if (tier > 0) {
|
|
180
|
+
if (parentCoord === undefined) {
|
|
181
|
+
throw new Error(`cold-start of a tier-${tier} forwarder requires a parentCoord`);
|
|
182
|
+
}
|
|
183
|
+
// Register with the parent at first opportunity; flip to serving on ack. A failed parent
|
|
184
|
+
// registration leaves the forwarder accepting participants but holding parent-involving ops,
|
|
185
|
+
// so a later retry (driven by the host) can complete the link-up — surfaced, not swallowed.
|
|
186
|
+
void this.deps.parentRegistrar
|
|
187
|
+
.registerWithParent(topicId, parentCoord, tier, opTier)
|
|
188
|
+
.then(() => forwarder.onParentAck())
|
|
189
|
+
.catch((err: unknown) => {
|
|
190
|
+
console.warn(`cohort-topic cold-start: parent registration for tier-${tier} forwarder failed`, err);
|
|
191
|
+
});
|
|
192
|
+
}
|
|
193
|
+
return forwarder;
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
get(topicId: Uint8Array): Forwarder | undefined {
|
|
197
|
+
return this.forwarders.get(bytesKey(topicId));
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
/** Build a {@link ColdStartManager} over the injected parent registrar. */
|
|
202
|
+
export function createColdStartManager(deps: ColdStartManagerDeps): ColdStartManager {
|
|
203
|
+
return new TrackingColdStartManager(deps);
|
|
204
|
+
}
|
|
@@ -0,0 +1,88 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — network-size-driven `d_max`.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Maximum useful depth:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* d_max = max(0, ⌊log_F(n_est)⌋ − 1)
|
|
8
|
+
* ```
|
|
9
|
+
*
|
|
10
|
+
* If `n_est` confidence falls below `confidence_min` (default 0.3) the computation caps `d_max` at
|
|
11
|
+
* `⌊d_max_cap / 2⌋` (`d_max_cap` default 60) as an upper bound — i.e. `min(formula, ⌊d_max_cap / 2⌋)` —
|
|
12
|
+
* to avoid pathological deep probes from an over-estimated population. Small/sparse populations
|
|
13
|
+
* (where the formula already yields a small value) are unaffected.
|
|
14
|
+
*
|
|
15
|
+
* `d_max` is the start tier of the walk-toward-root and is recomputed **lazily** — participants
|
|
16
|
+
* don't need it precise. The size estimate is read through the injected {@link ISizeEstimator}
|
|
17
|
+
* (db-p2p wraps FRET's `estimateSizeAndConfidence`; db-core never imports FRET).
|
|
18
|
+
*/
|
|
19
|
+
|
|
20
|
+
import type { ISizeEstimator } from "./ports.js";
|
|
21
|
+
|
|
22
|
+
/** Lazily computes the walk start tier `d_max` from the current network-size estimate. */
|
|
23
|
+
export interface DMaxComputer {
|
|
24
|
+
/** Reads the size estimate, applies the low-confidence cap, returns `d_max`. */
|
|
25
|
+
dMax(): number;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/** Default below which `n_est` confidence triggers the `d_max` cap. */
|
|
29
|
+
export const DEFAULT_CONFIDENCE_MIN = 0.3;
|
|
30
|
+
/** Default hard cap on the walk-toward-root start tier. */
|
|
31
|
+
export const DEFAULT_D_MAX_CAP = 60;
|
|
32
|
+
|
|
33
|
+
export interface DMaxConfig {
|
|
34
|
+
/** Network-size estimate source (db-p2p wraps FRET `estimateSizeAndConfidence`). */
|
|
35
|
+
estimator: ISizeEstimator;
|
|
36
|
+
/** Fan-out per tier (the `F` in `log_F`). */
|
|
37
|
+
F: number;
|
|
38
|
+
/** Confidence floor; below it `d_max` is capped at `⌊d_max_cap / 2⌋` (upper bound). Default 0.3. */
|
|
39
|
+
confidenceMin?: number;
|
|
40
|
+
/** Hard cap on `d_max`; also drives the low-confidence cap value. Default 60. */
|
|
41
|
+
dMaxCap?: number;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* `⌊log_F(n)⌋` computed without floating-point drift near exact powers of `F`. Multiplies back up to
|
|
46
|
+
* confirm the floor (`Math.log` can return e.g. `2.9999999` for `log_16(4096)`), so the result is
|
|
47
|
+
* exact for all `n` representable as a JS integer.
|
|
48
|
+
*/
|
|
49
|
+
function floorLogF(n: number, F: number): number {
|
|
50
|
+
if (n < 1) return 0;
|
|
51
|
+
let d = Math.floor(Math.log(n) / Math.log(F));
|
|
52
|
+
if (d < 0) d = 0;
|
|
53
|
+
// Correct floating-point error at the boundary in both directions.
|
|
54
|
+
while (Math.pow(F, d + 1) <= n) d++;
|
|
55
|
+
while (d > 0 && Math.pow(F, d) > n) d--;
|
|
56
|
+
return d;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Build a {@link DMaxComputer}. The estimate is read on every {@link DMaxComputer.dMax} call so the
|
|
61
|
+
* value tracks the latest FRET sample; callers cache it themselves if they want stability.
|
|
62
|
+
*/
|
|
63
|
+
export function makeDMaxComputer(config: DMaxConfig): DMaxComputer {
|
|
64
|
+
const { estimator, F } = config;
|
|
65
|
+
if (!Number.isInteger(F) || F < 2) {
|
|
66
|
+
throw new RangeError(`fan-out F must be an integer ≥ 2, got ${F}`);
|
|
67
|
+
}
|
|
68
|
+
const confidenceMin = config.confidenceMin ?? DEFAULT_CONFIDENCE_MIN;
|
|
69
|
+
const dMaxCap = config.dMaxCap ?? DEFAULT_D_MAX_CAP;
|
|
70
|
+
if (!Number.isInteger(dMaxCap) || dMaxCap < 0) {
|
|
71
|
+
throw new RangeError(`d_max_cap must be a non-negative integer, got ${dMaxCap}`);
|
|
72
|
+
}
|
|
73
|
+
const capValue = Math.floor(dMaxCap / 2);
|
|
74
|
+
|
|
75
|
+
return {
|
|
76
|
+
dMax(): number {
|
|
77
|
+
const { nEst, confidence } = estimator.estimate();
|
|
78
|
+
const formula = Math.min(Math.max(0, floorLogF(nEst, F) - 1), dMaxCap);
|
|
79
|
+
// Low confidence caps d_max as an upper bound (not a set-to): an over-estimated
|
|
80
|
+
// population can't push the walk deeper than ⌊d_max_cap / 2⌋, while small
|
|
81
|
+
// populations keep their (smaller) formula value.
|
|
82
|
+
if (confidence < confidenceMin) {
|
|
83
|
+
return Math.min(formula, capValue);
|
|
84
|
+
}
|
|
85
|
+
return formula;
|
|
86
|
+
},
|
|
87
|
+
};
|
|
88
|
+
}
|