@optimystic/db-core 0.13.4 → 0.14.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/src/cluster/structs.d.ts +29 -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 +19 -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 +44 -2
- package/dist/src/transactor/network-transactor.d.ts.map +1 -1
- package/dist/src/transactor/network-transactor.js +132 -17
- package/dist/src/transactor/network-transactor.js.map +1 -1
- package/package.json +3 -2
- package/src/cluster/structs.ts +86 -57
- 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 +19 -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 +162 -19
|
@@ -0,0 +1,154 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — membership-rotation primary handoff.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/cohort-topic.md` §Membership rotation and primary handoff. When membership changes the
|
|
5
|
+
* deterministic slot may move a registration's primary. Each member runs this state machine:
|
|
6
|
+
*
|
|
7
|
+
* 1. New membership stabilizes; all members compute the new `cohortEpoch`.
|
|
8
|
+
* 2. Members exchange a "primary inventory" listing the records they hold as primary ({@link start}).
|
|
9
|
+
* 3. For each record, the new computed primary either already holds it (no-op) or **pulls** it from
|
|
10
|
+
* the previous holder ({@link onInventory} → `transport.pull`).
|
|
11
|
+
* 4. **Resolved (GROUNDING):** the previous primary keeps serving (**dual-serve**) until the new
|
|
12
|
+
* primary acknowledges receipt ({@link onAck}) — closing the delivery gap.
|
|
13
|
+
* 5. Participants discover the new primary on their next ping/delivery (renewal module).
|
|
14
|
+
*
|
|
15
|
+
* The gossip *transport* for the inventory comes from a later ticket; it is injected here so the
|
|
16
|
+
* machine is unit-testable with a mock. The machine is purely cohort-local — FRET is unaware of it.
|
|
17
|
+
*/
|
|
18
|
+
|
|
19
|
+
import { bytesEqual, recordKey } from "./bytes.js";
|
|
20
|
+
import type { SlotAssigner } from "./sharding.js";
|
|
21
|
+
import type { RegistrationRecord, RegistrationStore } from "./types.js";
|
|
22
|
+
|
|
23
|
+
/** Identifies one registration in a {@link PrimaryInventory} or pull/ack exchange. */
|
|
24
|
+
export interface RecordRef {
|
|
25
|
+
topicId: Uint8Array;
|
|
26
|
+
participantId: Uint8Array;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/** A member's list of registrations it currently holds as primary, under the new `cohortEpoch`. */
|
|
30
|
+
export interface PrimaryInventory {
|
|
31
|
+
/** The advertising member. */
|
|
32
|
+
from: Uint8Array;
|
|
33
|
+
/** The new cohort epoch this inventory was computed under. */
|
|
34
|
+
epoch: Uint8Array;
|
|
35
|
+
entries: readonly RecordRef[];
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** Transport the handoff drives; supplied by the cohort-gossip ticket, mocked in tests. */
|
|
39
|
+
export interface HandoffTransport {
|
|
40
|
+
/** Broadcast this member's primary inventory to the cohort. */
|
|
41
|
+
sendInventory(inv: PrimaryInventory): void;
|
|
42
|
+
/** Pull a record this member is now primary for from its previous holder. */
|
|
43
|
+
pull(from: Uint8Array, ref: RecordRef): Promise<RegistrationRecord | undefined>;
|
|
44
|
+
/** Acknowledge receipt to the previous holder, ending its dual-serve for `ref`. */
|
|
45
|
+
ack(to: Uint8Array, ref: RecordRef): void;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
export interface HandoffDeps {
|
|
49
|
+
store: RegistrationStore;
|
|
50
|
+
/** This member's own peer id. */
|
|
51
|
+
self: Uint8Array;
|
|
52
|
+
slots: SlotAssigner;
|
|
53
|
+
/** The new (post-rotation) cohort snapshot. */
|
|
54
|
+
cohort: () => { members: readonly Uint8Array[]; cohortEpoch: Uint8Array };
|
|
55
|
+
transport: HandoffTransport;
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/** Per-member primary-handoff driver. One instance handles one rotation. */
|
|
59
|
+
export interface MembershipHandoff {
|
|
60
|
+
/** Step 1+2: recompute under the new epoch, publish this member's inventory, mark hand-offs. */
|
|
61
|
+
start(): void;
|
|
62
|
+
/** Step 3: react to a peer's inventory — pull every record now assigned to this member, then ack. */
|
|
63
|
+
onInventory(inv: PrimaryInventory): Promise<void>;
|
|
64
|
+
/** Serve a pull from the new primary (this member is the previous holder); stays dual-serving. */
|
|
65
|
+
onPull(ref: RecordRef): RegistrationRecord | undefined;
|
|
66
|
+
/** New primary acked receipt: stop dual-serving `ref`. */
|
|
67
|
+
onAck(ref: RecordRef): void;
|
|
68
|
+
/** Whether this member should still answer renews for `(topicId, participantId)` right now. */
|
|
69
|
+
isServing(topicId: Uint8Array, participantId: Uint8Array): boolean;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
class StoreMembershipHandoff implements MembershipHandoff {
|
|
73
|
+
/** Records this member held as primary but no longer is — kept serving until acked. */
|
|
74
|
+
private readonly dualServing = new Set<string>();
|
|
75
|
+
/** In-flight pulls, so a duplicate inventory does not pull twice. */
|
|
76
|
+
private readonly pulling = new Set<string>();
|
|
77
|
+
|
|
78
|
+
constructor(private readonly deps: HandoffDeps) {}
|
|
79
|
+
|
|
80
|
+
start(): void {
|
|
81
|
+
const { members, cohortEpoch } = this.deps.cohort();
|
|
82
|
+
const entries: RecordRef[] = [];
|
|
83
|
+
for (const rec of this.deps.store.listAll()) {
|
|
84
|
+
if (!bytesEqual(rec.primary, this.deps.self)) {
|
|
85
|
+
continue; // only records this member currently holds as primary
|
|
86
|
+
}
|
|
87
|
+
entries.push({ topicId: rec.topicId, participantId: rec.participantId });
|
|
88
|
+
const { primary } = this.deps.slots.assignSlots(rec.participantId, cohortEpoch, members);
|
|
89
|
+
if (!bytesEqual(primary, this.deps.self)) {
|
|
90
|
+
// Primary moved away from this member: keep serving until the new primary acks.
|
|
91
|
+
this.dualServing.add(recordKey(rec.topicId, rec.participantId));
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
this.deps.transport.sendInventory({ from: this.deps.self, epoch: cohortEpoch, entries });
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
async onInventory(inv: PrimaryInventory): Promise<void> {
|
|
98
|
+
if (bytesEqual(inv.from, this.deps.self)) {
|
|
99
|
+
return; // ignore our own inventory
|
|
100
|
+
}
|
|
101
|
+
const { members, cohortEpoch } = this.deps.cohort();
|
|
102
|
+
for (const ref of inv.entries) {
|
|
103
|
+
await this.maybePull(inv.from, ref, members, cohortEpoch);
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
private async maybePull(from: Uint8Array, ref: RecordRef, members: readonly Uint8Array[], cohortEpoch: Uint8Array): Promise<void> {
|
|
108
|
+
const { primary, backups } = this.deps.slots.assignSlots(ref.participantId, cohortEpoch, members);
|
|
109
|
+
if (!bytesEqual(primary, this.deps.self)) {
|
|
110
|
+
return; // not assigned to this member now
|
|
111
|
+
}
|
|
112
|
+
if (this.deps.store.getByParticipant(ref.topicId, ref.participantId) !== undefined) {
|
|
113
|
+
return; // already hold it (no-op handoff)
|
|
114
|
+
}
|
|
115
|
+
const key = recordKey(ref.topicId, ref.participantId);
|
|
116
|
+
if (this.pulling.has(key)) {
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
this.pulling.add(key);
|
|
120
|
+
try {
|
|
121
|
+
const rec = await this.deps.transport.pull(from, ref);
|
|
122
|
+
if (rec !== undefined) {
|
|
123
|
+
// Re-stamp the assignment so a later rotation's `start()` recognises this member as primary.
|
|
124
|
+
this.deps.store.put({ ...rec, primary, backups });
|
|
125
|
+
this.deps.transport.ack(from, ref); // ends dual-serve at the previous holder
|
|
126
|
+
}
|
|
127
|
+
} finally {
|
|
128
|
+
this.pulling.delete(key);
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
onPull(ref: RecordRef): RegistrationRecord | undefined {
|
|
133
|
+
// Previous holder serves the record; it remains in dualServing until the ack arrives.
|
|
134
|
+
return this.deps.store.getByParticipant(ref.topicId, ref.participantId);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
onAck(ref: RecordRef): void {
|
|
138
|
+
this.dualServing.delete(recordKey(ref.topicId, ref.participantId));
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
isServing(topicId: Uint8Array, participantId: Uint8Array): boolean {
|
|
142
|
+
const { members, cohortEpoch } = this.deps.cohort();
|
|
143
|
+
const { primary } = this.deps.slots.assignSlots(participantId, cohortEpoch, members);
|
|
144
|
+
if (bytesEqual(primary, this.deps.self)) {
|
|
145
|
+
return true;
|
|
146
|
+
}
|
|
147
|
+
return this.dualServing.has(recordKey(topicId, participantId));
|
|
148
|
+
}
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/** Build a {@link MembershipHandoff} for one rotation. */
|
|
152
|
+
export function createMembershipHandoff(deps: HandoffDeps): MembershipHandoff {
|
|
153
|
+
return new StoreMembershipHandoff(deps);
|
|
154
|
+
}
|
|
@@ -0,0 +1,423 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — TTL renewal and failover.
|
|
3
|
+
*
|
|
4
|
+
* Per `docs/cohort-topic.md` §TTL and renewal and §Failure modes:
|
|
5
|
+
*
|
|
6
|
+
* - Participant pings `primary` every `ttl/3`. Success → primary touches `lastPing` and gossips it.
|
|
7
|
+
* - Three consecutive ping failures → participant promotes `backups[0]` via a re-attach RPC: a renew
|
|
8
|
+
* carrying a **signed `reattach` flag** (no full re-registration). The backup accepts when it holds
|
|
9
|
+
* the record locally *and* is a computed backup under the current epoch — re-stamping `primary` to
|
|
10
|
+
* itself and serving subsequent plain pings via an epoch-scoped failover override (the unchanged
|
|
11
|
+
* `cohortEpoch` still names the dead node as the computed primary). A re-attach answered
|
|
12
|
+
* `primary_moved` means a real rotation moved primary to a live member: the participant adopts that
|
|
13
|
+
* payload instead of promoting the contacted backup (ignoring a reply that points back at the dead
|
|
14
|
+
* primary). **Resolved (GROUNDING):** the participant's `cohortEpoch` hint refreshes lazily — on the
|
|
15
|
+
* *next* ping/renewal after failover, not eagerly at failover time.
|
|
16
|
+
* - All of primary + backups fail → participant re-runs lookup from `d_max` (walk ticket, injected).
|
|
17
|
+
* - Cohort-side: evict where `now − lastPing > ttl`; eviction is gossiped so members converge.
|
|
18
|
+
*
|
|
19
|
+
* Both sides take their transport/gossip by injection so storage + sharding + TTL stay unit-testable
|
|
20
|
+
* in isolation; db-core never imports FRET or libp2p here.
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import { b64urlToBytes } from "../wire/codec.js";
|
|
24
|
+
import type { RenewReplyV1, RenewV1 } from "../wire/types.js";
|
|
25
|
+
import { bytesEqual, bytesKey, recordKey } from "./bytes.js";
|
|
26
|
+
import type { SlotAssigner } from "./sharding.js";
|
|
27
|
+
import { MAX_PING_FAILURES, pingIntervalMs } from "./types.js";
|
|
28
|
+
import type { RegistrationRecord, RegistrationStore } from "./types.js";
|
|
29
|
+
|
|
30
|
+
// --- participant side ---
|
|
31
|
+
|
|
32
|
+
/** Participant-side TTL renewal: periodic ping with 3-fail backup promotion. */
|
|
33
|
+
export interface RenewalParticipant {
|
|
34
|
+
/** Run one ping cycle: ping the current primary, count failures, promote/relookup as needed. */
|
|
35
|
+
pingLoop(): Promise<void>;
|
|
36
|
+
/** Re-attach to `target` (a backup), carrying the existing record; resolves with the reply. */
|
|
37
|
+
reattach(target: Uint8Array): Promise<RenewReplyV1>;
|
|
38
|
+
/**
|
|
39
|
+
* Best-effort remote tombstone: send a signed withdraw renew to the current primary so the cohort
|
|
40
|
+
* frees the registration immediately rather than holding it for a full TTL. Swallows any transport
|
|
41
|
+
* failure (TTL expiry remains the fallback) and does NOT touch the failure counters or trigger
|
|
42
|
+
* failover — withdraw is one-shot and fire-and-forget.
|
|
43
|
+
*/
|
|
44
|
+
withdraw(): Promise<void>;
|
|
45
|
+
/** `ttl/3` cadence the scheduler should call {@link pingLoop} at. */
|
|
46
|
+
readonly pingIntervalMs: number;
|
|
47
|
+
/** The participant's current view of its registration (primary/backups). */
|
|
48
|
+
readonly record: RegistrationRecord;
|
|
49
|
+
/**
|
|
50
|
+
* Cached cohort-epoch hint (subscriber-side `cohortHint`). Deliberately *not* refreshed at
|
|
51
|
+
* failover; a `primary_moved` reply on the next ping refreshes it lazily.
|
|
52
|
+
*/
|
|
53
|
+
readonly cohortEpochHint: Uint8Array | undefined;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** The unsigned body of a renew/re-attach RPC; the injected signer turns it into a {@link RenewV1}. */
|
|
57
|
+
export type UnsignedRenew = Omit<RenewV1, "signature">;
|
|
58
|
+
|
|
59
|
+
/** Transport the participant drives; supplied by db-p2p (FRET dial / RouteAndMaybeAct underneath). */
|
|
60
|
+
export interface RenewalParticipantTransport {
|
|
61
|
+
/** Send a renew/re-attach to `target`; resolves with the reply, rejects on RPC failure. */
|
|
62
|
+
send(target: Uint8Array, msg: RenewV1): Promise<RenewReplyV1>;
|
|
63
|
+
/** Primary + all backups unreachable: re-run lookup from `d_max` (walk ticket owns the body). */
|
|
64
|
+
relookup(): Promise<void>;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
export interface RenewalParticipantDeps {
|
|
68
|
+
transport: RenewalParticipantTransport;
|
|
69
|
+
/** Monotonic-ish wall clock in unix ms (injected for deterministic tests). */
|
|
70
|
+
clock: () => number;
|
|
71
|
+
/** Signs the renew body; db-p2p supplies the (async) peer-key signature. */
|
|
72
|
+
sign: (body: UnsignedRenew) => Promise<string>;
|
|
73
|
+
/** Correlation id matching the original RegisterV1, base64url. */
|
|
74
|
+
correlationId: string;
|
|
75
|
+
/** Initial cohort-epoch hint from the registration reply (refreshed lazily thereafter). */
|
|
76
|
+
initialCohortEpoch?: Uint8Array;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
class TtlRenewalParticipant implements RenewalParticipant {
|
|
80
|
+
private consecutiveFailures = 0;
|
|
81
|
+
private current: RegistrationRecord;
|
|
82
|
+
private epochHint: Uint8Array | undefined;
|
|
83
|
+
|
|
84
|
+
constructor(initial: RegistrationRecord, private readonly deps: RenewalParticipantDeps) {
|
|
85
|
+
this.current = initial;
|
|
86
|
+
this.epochHint = deps.initialCohortEpoch;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
get record(): RegistrationRecord {
|
|
90
|
+
return this.current;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
get cohortEpochHint(): Uint8Array | undefined {
|
|
94
|
+
return this.epochHint;
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
get pingIntervalMs(): number {
|
|
98
|
+
return pingIntervalMs(this.current.ttl);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
async pingLoop(): Promise<void> {
|
|
102
|
+
const reply = await this.trySend(this.current.primary);
|
|
103
|
+
if (reply !== undefined) {
|
|
104
|
+
this.onPingSuccess(reply);
|
|
105
|
+
return;
|
|
106
|
+
}
|
|
107
|
+
this.consecutiveFailures++;
|
|
108
|
+
if (this.consecutiveFailures < MAX_PING_FAILURES) {
|
|
109
|
+
return;
|
|
110
|
+
}
|
|
111
|
+
await this.failover();
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
async reattach(target: Uint8Array): Promise<RenewReplyV1> {
|
|
115
|
+
return this.deps.transport.send(target, await this.buildRenew({ reattach: true }));
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
async withdraw(): Promise<void> {
|
|
119
|
+
// Fire-and-forget signed tombstone to the current primary. A send failure is swallowed — the
|
|
120
|
+
// record TTL-expires as it does today (the local-half fallback) — and crucially we never bump
|
|
121
|
+
// `consecutiveFailures` or fail over: the participant is leaving, not recovering.
|
|
122
|
+
try {
|
|
123
|
+
await this.deps.transport.send(this.current.primary, await this.buildRenew({ withdraw: true }));
|
|
124
|
+
} catch {
|
|
125
|
+
/* best-effort; the cohort TTL bounds the leak */
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/** Send a renew, mapping an RPC rejection to `undefined` (a counted failure). */
|
|
130
|
+
private async trySend(target: Uint8Array): Promise<RenewReplyV1 | undefined> {
|
|
131
|
+
try {
|
|
132
|
+
return await this.deps.transport.send(target, await this.buildRenew({}));
|
|
133
|
+
} catch {
|
|
134
|
+
return undefined;
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
private onPingSuccess(reply: RenewReplyV1): void {
|
|
139
|
+
this.consecutiveFailures = 0;
|
|
140
|
+
if (reply.result === "primary_moved") {
|
|
141
|
+
// Lazy cohortEpoch refresh lands here: a move discovered on a normal ping updates the hint.
|
|
142
|
+
this.applyPrimaryMoved(reply);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* 3-fail path: re-attach to each backup in turn (a signed `reattach=true` renew). An `ok` means the
|
|
148
|
+
* backup accepted the crash-failover promotion → promote it locally. A `primary_moved` means a real
|
|
149
|
+
* rotation moved primary to a different live member → adopt that payload (not the contacted backup),
|
|
150
|
+
* **unless** it points back at the just-failed primary (the bounce guard) — then keep trying. All
|
|
151
|
+
* backups exhausted → re-run lookup from `d_max`.
|
|
152
|
+
*/
|
|
153
|
+
private async failover(): Promise<void> {
|
|
154
|
+
const failedPrimary = this.current.primary;
|
|
155
|
+
for (const backup of this.current.backups) {
|
|
156
|
+
const reply = await this.tryReattach(backup);
|
|
157
|
+
if (reply === undefined) {
|
|
158
|
+
continue;
|
|
159
|
+
}
|
|
160
|
+
if (reply.result === "ok") {
|
|
161
|
+
this.promote(backup);
|
|
162
|
+
return;
|
|
163
|
+
}
|
|
164
|
+
if (reply.result === "primary_moved") {
|
|
165
|
+
// A genuine rotation: adopt the rotated assignment — but never re-adopt the dead primary
|
|
166
|
+
// (the defensive guard against the exact bounce crash-failover is meant to fix).
|
|
167
|
+
if (reply.newPrimary !== undefined && bytesEqual(b64(reply.newPrimary), failedPrimary)) {
|
|
168
|
+
continue;
|
|
169
|
+
}
|
|
170
|
+
this.applyPrimaryMoved(reply);
|
|
171
|
+
// We now have a fresh, live primary to ping — clear the strike count so the next single
|
|
172
|
+
// transient failure doesn't immediately re-failover (matching `promote` and the
|
|
173
|
+
// normal-ping `primary_moved` path, which both reset here).
|
|
174
|
+
this.consecutiveFailures = 0;
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
// unknown_registration (replication lag) or anything else: try the next backup.
|
|
178
|
+
}
|
|
179
|
+
await this.deps.transport.relookup();
|
|
180
|
+
// relookup is a terminal recovery action (it owns re-establishing the registration out of
|
|
181
|
+
// band); reset the counter so a still-dead primary backs off to one relookup per
|
|
182
|
+
// MAX_PING_FAILURES cycles rather than re-running the d_max walk on every subsequent ping.
|
|
183
|
+
this.consecutiveFailures = 0;
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
private async tryReattach(target: Uint8Array): Promise<RenewReplyV1 | undefined> {
|
|
187
|
+
try {
|
|
188
|
+
return await this.reattach(target);
|
|
189
|
+
} catch {
|
|
190
|
+
return undefined;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Promote `target` (a backup) to primary. The `cohortEpoch` hint is deliberately *not* refreshed
|
|
196
|
+
* here — per the resolved open question it refreshes lazily on the next ping/renewal.
|
|
197
|
+
*/
|
|
198
|
+
private promote(target: Uint8Array): void {
|
|
199
|
+
const backups = this.current.backups.filter((b) => !bytesEqual(b, target));
|
|
200
|
+
this.current = { ...this.current, primary: target, backups, lastPing: this.deps.clock() };
|
|
201
|
+
this.consecutiveFailures = 0;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
private applyPrimaryMoved(reply: RenewReplyV1): void {
|
|
205
|
+
const patch: Partial<RegistrationRecord> = { lastPing: this.deps.clock() };
|
|
206
|
+
if (reply.newPrimary !== undefined) {
|
|
207
|
+
patch.primary = b64(reply.newPrimary);
|
|
208
|
+
}
|
|
209
|
+
if (reply.newBackups !== undefined) {
|
|
210
|
+
patch.backups = reply.newBackups.map(b64);
|
|
211
|
+
}
|
|
212
|
+
if (reply.cohortEpoch !== undefined) {
|
|
213
|
+
// Lazy refresh: the epoch hint catches up here, on the ping that discovers the move.
|
|
214
|
+
this.epochHint = b64(reply.cohortEpoch);
|
|
215
|
+
}
|
|
216
|
+
this.current = { ...this.current, ...patch };
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
/**
|
|
220
|
+
* Build a signed renew. The `reattach=true` (crash-failover) / `withdraw=true` (leave tombstone)
|
|
221
|
+
* flags are carried inside the signed body so the accepting member can trust the attestation; a plain
|
|
222
|
+
* ping omits both fields entirely (a stray renew can never silently usurp a live primary or evict a
|
|
223
|
+
* registration). Each flag is set only when true, matching the current plain-ping wire shape.
|
|
224
|
+
*/
|
|
225
|
+
private async buildRenew(opts: { reattach?: boolean; withdraw?: boolean }): Promise<RenewV1> {
|
|
226
|
+
const body: UnsignedRenew = {
|
|
227
|
+
v: 1,
|
|
228
|
+
topicId: bytesKey(this.current.topicId),
|
|
229
|
+
participantId: bytesKey(this.current.participantId),
|
|
230
|
+
correlationId: this.deps.correlationId,
|
|
231
|
+
timestamp: this.deps.clock(),
|
|
232
|
+
};
|
|
233
|
+
if (opts.reattach === true) {
|
|
234
|
+
body.reattach = true;
|
|
235
|
+
}
|
|
236
|
+
if (opts.withdraw === true) {
|
|
237
|
+
body.withdraw = true;
|
|
238
|
+
}
|
|
239
|
+
return { ...body, signature: await this.deps.sign(body) };
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
/** Build a {@link RenewalParticipant} around an initial record. */
|
|
244
|
+
export function createRenewalParticipant(initial: RegistrationRecord, deps: RenewalParticipantDeps): RenewalParticipant {
|
|
245
|
+
return new TtlRenewalParticipant(initial, deps);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
// --- cohort side ---
|
|
249
|
+
|
|
250
|
+
/** Emits cohort gossip so members converge on the active set after a touch or eviction. */
|
|
251
|
+
export interface RenewalGossip {
|
|
252
|
+
/** Gossip a `lastPing` touch for `rec`. */
|
|
253
|
+
touch(rec: RegistrationRecord): void;
|
|
254
|
+
/** Gossip an eviction for `rec`. */
|
|
255
|
+
evicted(rec: RegistrationRecord): void;
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
export interface RenewalCohortSideDeps {
|
|
259
|
+
store: RegistrationStore;
|
|
260
|
+
/** This member's own peer id. */
|
|
261
|
+
self: Uint8Array;
|
|
262
|
+
/** Deterministic slot assignment, shared with the handoff. */
|
|
263
|
+
slots: SlotAssigner;
|
|
264
|
+
/** Current cohort snapshot — db-p2p supplies it from the membership source. */
|
|
265
|
+
cohort: () => { members: readonly Uint8Array[]; cohortEpoch: Uint8Array };
|
|
266
|
+
gossip: RenewalGossip;
|
|
267
|
+
/**
|
|
268
|
+
* Optional dual-serve predicate, wired to the rotation handoff's `isServing`. During a handoff
|
|
269
|
+
* the previous primary is no longer the computed primary but must keep serving until it is acked;
|
|
270
|
+
* when this returns `true` `onRenew` touches and serves instead of replying `primary_moved`.
|
|
271
|
+
*/
|
|
272
|
+
isServing?: (topicId: Uint8Array, participantId: Uint8Array) => boolean;
|
|
273
|
+
/**
|
|
274
|
+
* Optional participant peer-key signature verifier (db-p2p binds it to the peer-sig primitive over
|
|
275
|
+
* {@link import("../wire/payloads.js").renewSigningPayload}). It gates the two privileged
|
|
276
|
+
* participant-attested paths: a `reattach` promotion and a `withdraw` eviction. A renew on either path
|
|
277
|
+
* whose signature does not verify against the claimed `participantId` must never act — a stray/MITM'd
|
|
278
|
+
* ping cannot usurp a live primary or evict someone else's registration (§TTL and renewal). Absent →
|
|
279
|
+
* the gate is skipped (unit tests run without peer crypto); plain pings are never verified here (they
|
|
280
|
+
* only touch `lastPing`).
|
|
281
|
+
*/
|
|
282
|
+
verifyParticipantSig?: (renew: RenewV1) => boolean;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/** Cohort-side TTL handling: touch on renew, redirect on rotation, sweep stale records. */
|
|
286
|
+
export interface RenewalCohortSide {
|
|
287
|
+
/** Handle an inbound renew. Touches the record, or replies `primary_moved` if this member is no longer primary. */
|
|
288
|
+
onRenew(msg: RenewV1, now: number): RenewReplyV1;
|
|
289
|
+
/** Evict every stale record and gossip each eviction; returns the evicted set. */
|
|
290
|
+
sweepStale(now: number): readonly RegistrationRecord[];
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
class StoreRenewalCohortSide implements RenewalCohortSide {
|
|
294
|
+
/**
|
|
295
|
+
* Epoch-scoped crash-failover overrides: `recordKey → cohortEpoch under which this member accepted a
|
|
296
|
+
* promotion`. A matching entry makes a *subsequent plain ping* serve here even though the computed
|
|
297
|
+
* primary is still the dead node, so the migrated participant stops bouncing. Cleared on epoch change
|
|
298
|
+
* (the next rotation handoff reasserts the deterministic assignment). Sibling to the rotation
|
|
299
|
+
* `isServing` dual-serve exception — distinct state, OR-ed into the serve decision.
|
|
300
|
+
*/
|
|
301
|
+
private readonly failoverServing = new Map<string, Uint8Array>();
|
|
302
|
+
|
|
303
|
+
constructor(private readonly deps: RenewalCohortSideDeps) {}
|
|
304
|
+
|
|
305
|
+
onRenew(msg: RenewV1, now: number): RenewReplyV1 {
|
|
306
|
+
const topicId = b64(msg.topicId);
|
|
307
|
+
const participantId = b64(msg.participantId);
|
|
308
|
+
const rec = this.deps.store.getByParticipant(topicId, participantId);
|
|
309
|
+
if (rec === undefined) {
|
|
310
|
+
return { v: 1, result: "unknown_registration" };
|
|
311
|
+
}
|
|
312
|
+
const key = recordKey(topicId, participantId);
|
|
313
|
+
|
|
314
|
+
if (msg.withdraw === true) {
|
|
315
|
+
// Signed leave attestation. A forged/missing signature must never evict someone else's
|
|
316
|
+
// registration → ignore, revealing nothing (`unknown_registration`, the same opaque answer a
|
|
317
|
+
// non-existent record gets). The gate is absent in key-less unit mode, matching reattach.
|
|
318
|
+
// `withdraw` is checked before the slot/primary computation: a withdraw needs no slot or
|
|
319
|
+
// primary check (any holder evicts its replica), and it takes precedence over a (malformed)
|
|
320
|
+
// co-set `reattach` — a record being withdrawn is gone regardless of a promotion request.
|
|
321
|
+
if (this.deps.verifyParticipantSig?.(msg) === false) {
|
|
322
|
+
return { v: 1, result: "unknown_registration" };
|
|
323
|
+
}
|
|
324
|
+
this.deps.store.delete(topicId, participantId);
|
|
325
|
+
this.failoverServing.delete(key); // mirror sweepStale: drop any crash-failover override
|
|
326
|
+
this.deps.gossip.evicted(rec);
|
|
327
|
+
return { v: 1, result: "withdrawn" };
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
const { members, cohortEpoch } = this.deps.cohort();
|
|
331
|
+
const { primary, backups } = this.deps.slots.assignSlots(participantId, cohortEpoch, members);
|
|
332
|
+
const self = this.deps.self;
|
|
333
|
+
|
|
334
|
+
if (msg.reattach === true) {
|
|
335
|
+
// Crash-failover promotion request (participant attests primary unreachable). The signed
|
|
336
|
+
// `reattach` flag is what a backup trusts to promote itself, so a missing/forged signature
|
|
337
|
+
// must never escalate: fall through to a plain redirect (never promote).
|
|
338
|
+
if (this.deps.verifyParticipantSig?.(msg) === false) {
|
|
339
|
+
return this.primaryMoved(primary, backups, cohortEpoch);
|
|
340
|
+
}
|
|
341
|
+
if (bytesEqual(primary, self)) {
|
|
342
|
+
// A rotation already made this member the computed primary; serve, no override needed.
|
|
343
|
+
return this.touchAndServe(rec, now);
|
|
344
|
+
}
|
|
345
|
+
if (backups.some((b) => bytesEqual(b, self))) {
|
|
346
|
+
// Legitimate backup takeover: re-stamp primary, gossip the new assignment, and record an
|
|
347
|
+
// epoch-scoped override so subsequent plain pings keep being served here.
|
|
348
|
+
const restamped: RegistrationRecord = {
|
|
349
|
+
...rec,
|
|
350
|
+
primary: self,
|
|
351
|
+
backups: rec.backups.filter((b) => !bytesEqual(b, self)),
|
|
352
|
+
lastPing: now,
|
|
353
|
+
};
|
|
354
|
+
this.deps.store.put(restamped);
|
|
355
|
+
this.failoverServing.set(key, cohortEpoch);
|
|
356
|
+
this.deps.gossip.touch(restamped);
|
|
357
|
+
return { v: 1, result: "ok" };
|
|
358
|
+
}
|
|
359
|
+
if (this.deps.isServing?.(topicId, participantId) === true) {
|
|
360
|
+
// Rotation dual-serve already covers this record here.
|
|
361
|
+
return this.touchAndServe(rec, now);
|
|
362
|
+
}
|
|
363
|
+
// Not a valid takeover target (stale participant view): redirect, do not promote.
|
|
364
|
+
return this.primaryMoved(primary, backups, cohortEpoch);
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
// Plain ping.
|
|
368
|
+
const isComputedPrimary = bytesEqual(primary, self);
|
|
369
|
+
const override = this.failoverServing.get(key);
|
|
370
|
+
const overrideMatches = override !== undefined && bytesEqual(override, cohortEpoch);
|
|
371
|
+
// Housekeeping: drop a now-redundant override (this member is the computed primary again) or a
|
|
372
|
+
// stale one tagged under a prior epoch (the rotation handoff governs across the epoch change).
|
|
373
|
+
if (override !== undefined && (isComputedPrimary || !overrideMatches)) {
|
|
374
|
+
this.failoverServing.delete(key);
|
|
375
|
+
}
|
|
376
|
+
const serving = isComputedPrimary || this.deps.isServing?.(topicId, participantId) === true || overrideMatches;
|
|
377
|
+
if (serving) {
|
|
378
|
+
return this.touchAndServe(rec, now);
|
|
379
|
+
}
|
|
380
|
+
return this.primaryMoved(primary, backups, cohortEpoch);
|
|
381
|
+
}
|
|
382
|
+
|
|
383
|
+
/** Existing touch path: stamp `lastPing`, gossip the touch, reply `ok`. */
|
|
384
|
+
private touchAndServe(rec: RegistrationRecord, now: number): RenewReplyV1 {
|
|
385
|
+
const touched: RegistrationRecord = { ...rec, lastPing: now };
|
|
386
|
+
this.deps.store.put(touched);
|
|
387
|
+
this.deps.gossip.touch(touched);
|
|
388
|
+
return { v: 1, result: "ok" };
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
private primaryMoved(primary: Uint8Array, backups: readonly Uint8Array[], cohortEpoch: Uint8Array): RenewReplyV1 {
|
|
392
|
+
return {
|
|
393
|
+
v: 1,
|
|
394
|
+
result: "primary_moved",
|
|
395
|
+
newPrimary: bytesKey(primary),
|
|
396
|
+
newBackups: backups.map(bytesKey),
|
|
397
|
+
cohortEpoch: bytesKey(cohortEpoch),
|
|
398
|
+
};
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
sweepStale(now: number): readonly RegistrationRecord[] {
|
|
402
|
+
const evicted = this.deps.store.evictStale(now);
|
|
403
|
+
for (const rec of evicted) {
|
|
404
|
+
// Drop any crash-failover override for an evicted record. Otherwise it leaks (the record gets
|
|
405
|
+
// no more pings, so the plain-ping housekeeping that would clear it never runs) and — if the
|
|
406
|
+
// same `(topic, participant)` re-registers under the unchanged epoch — could wrongly make a
|
|
407
|
+
// non-primary member keep serving via the stale override.
|
|
408
|
+
this.failoverServing.delete(recordKey(rec.topicId, rec.participantId));
|
|
409
|
+
this.deps.gossip.evicted(rec);
|
|
410
|
+
}
|
|
411
|
+
return evicted;
|
|
412
|
+
}
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
/** Build a {@link RenewalCohortSide}. */
|
|
416
|
+
export function createRenewalCohortSide(deps: RenewalCohortSideDeps): RenewalCohortSide {
|
|
417
|
+
return new StoreRenewalCohortSide(deps);
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
/** Decode a base64url wire field to bytes (the canonical byte form on the wire). */
|
|
421
|
+
function b64(s: string): Uint8Array {
|
|
422
|
+
return b64urlToBytes(s);
|
|
423
|
+
}
|
|
@@ -0,0 +1,61 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Cohort-topic substrate — deterministic primary/backup sharding.
|
|
3
|
+
*
|
|
4
|
+
* Transcribed from `docs/cohort-topic.md` §Primary and backup sharding:
|
|
5
|
+
*
|
|
6
|
+
* ```
|
|
7
|
+
* order(cohortMembers) = sort(cohortMembers, by PeerId ascending)
|
|
8
|
+
* slot(participantId, cohortEpoch) = H(participantId ‖ cohortEpoch) mod k
|
|
9
|
+
* primary = order[slot]
|
|
10
|
+
* backups = order[slot+1 .. slot+2 (mod k)]
|
|
11
|
+
* ```
|
|
12
|
+
*
|
|
13
|
+
* Deterministic given `(participantId, cohortEpoch, members)`; shards delivery load roughly evenly
|
|
14
|
+
* across the `~k` members. `H` is the injected {@link IRingHash} (db-core's own SHA-256 — **not** a
|
|
15
|
+
* FRET import), reused so coords stay byte-compatible with the rest of the substrate.
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
import type { IRingHash } from "../ports.js";
|
|
19
|
+
import { compareBytes, concatBytes } from "./bytes.js";
|
|
20
|
+
|
|
21
|
+
/** Deterministic slot assignment over a fixed cohort snapshot. */
|
|
22
|
+
export interface SlotAssigner {
|
|
23
|
+
/**
|
|
24
|
+
* `(primary, backups)` for `participantId` under `cohortEpoch` and `cohortMembers`.
|
|
25
|
+
* Backups are the 1..2 members following the primary in ascending order, wrapping mod `k`.
|
|
26
|
+
*/
|
|
27
|
+
assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] };
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
/** Number of warm-failover backups (capped by available members). */
|
|
31
|
+
const MAX_BACKUPS = 2;
|
|
32
|
+
|
|
33
|
+
/** `coord mod k` over the full digest, MSB-first, without bigint allocation. */
|
|
34
|
+
function modK(coord: Uint8Array, k: number): number {
|
|
35
|
+
let acc = 0;
|
|
36
|
+
for (let i = 0; i < coord.length; i++) {
|
|
37
|
+
acc = (acc * 256 + coord[i]!) % k;
|
|
38
|
+
}
|
|
39
|
+
return acc;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/** Build a {@link SlotAssigner} bound to a hash. */
|
|
43
|
+
export function createSlotAssigner(hash: IRingHash): SlotAssigner {
|
|
44
|
+
return {
|
|
45
|
+
assignSlots(participantId: Uint8Array, cohortEpoch: Uint8Array, cohortMembers: readonly Uint8Array[]): { primary: Uint8Array; backups: Uint8Array[] } {
|
|
46
|
+
const k = cohortMembers.length;
|
|
47
|
+
if (k === 0) {
|
|
48
|
+
throw new RangeError("assignSlots requires a non-empty cohort");
|
|
49
|
+
}
|
|
50
|
+
const order = [...cohortMembers].sort(compareBytes);
|
|
51
|
+
const slot = modK(hash.H(concatBytes(participantId, cohortEpoch)), k);
|
|
52
|
+
const primary = order[slot]!;
|
|
53
|
+
const backups: Uint8Array[] = [];
|
|
54
|
+
const nBackups = Math.min(MAX_BACKUPS, k - 1);
|
|
55
|
+
for (let i = 1; i <= nBackups; i++) {
|
|
56
|
+
backups.push(order[(slot + i) % k]!);
|
|
57
|
+
}
|
|
58
|
+
return { primary, backups };
|
|
59
|
+
},
|
|
60
|
+
};
|
|
61
|
+
}
|