import { type Multiaddr } from "@multiformats/multiaddr"; import { DirectStream, type DirectStreamComponents, type DirectStreamOptions, type PeerStreams } from "@peerbit/stream"; import { DataMessage, type FanoutRouteTokenHint, type StreamEvents } from "@peerbit/stream-interface"; import { TopicRootControlPlane } from "./topic-root-control-plane.js"; export type FanoutTreeChannelId = { root: string; topic: string; key: Uint8Array; suffixKey: string; }; export type FanoutTreeChannelRole = "root" | "node"; export type FanoutTreeStreamOptions = DirectStreamOptions & { /** * Optional RNG hook used for shuffling/join request ids. * * This is primarily intended for deterministic simulation harnesses. */ random?: () => number; /** * Optional topic-root resolver for consumers (e.g. shared-log) that need to * resolve a root when joining a channel without an explicit root. * * FanoutTree does not currently use this internally, but we expose it as a * shared control-plane hook so applications do not need to hang root * resolution off of a separate pubsub implementation. */ topicRootControlPlane?: TopicRootControlPlane; }; export type FanoutTreeChannelOptions = { role: FanoutTreeChannelRole; msgRate: number; msgSize: number; uploadLimitBps: number; maxChildren: number; /** * Extra per-child overhead (bytes) to include when estimating upload usage. * * This should include stream/protocol overhead and any framing/signature/etc * that is not part of `msgSize`. */ uploadOverheadBytes?: number; /** * Token bucket burst window for upload shaping. * * Capacity = `uploadLimitBps * (uploadBurstMs/1000)`. */ uploadBurstMs?: number; bidPerByte?: number; allowKick?: boolean; /** * Enable bounded pull-repair (tree-first delivery, parent-based backfill). * * Defaults to `true`. Set to `false` to disable caching/repair logic for the channel. */ repair?: boolean; repairWindowMessages?: number; /** * How far back (in messages) we will attempt to repair missing sequences. * * Defaults to `repairWindowMessages` (no extra pruning). For "live" workloads you * may want to set this lower so repair doesn't waste work on stale data. */ repairMaxBackfillMessages?: number; repairIntervalMs?: number; repairMaxPerReq?: number; /** * If enabled, try to repair missing sequences by querying a small number of * additional peers (not only the current parent). This is a stepping stone * towards Plumtree-style neighbor-assisted repair. */ neighborRepair?: boolean; neighborRepairPeers?: number; /** * Target number of "lazy" mesh peers to keep connected for neighbor-assisted repair. * These peers are discovered via trackers and are not part of the parent/child tree. */ neighborMeshPeers?: number; /** * How often to send an IHAVE-style cache summary to mesh peers. * This is throttled and sent only when the node has seen new data. */ neighborAnnounceIntervalMs?: number; /** * How often to refresh mesh peers via tracker queries. */ neighborMeshRefreshIntervalMs?: number; /** * How long to trust an IHAVE summary from a peer when choosing fetch targets. */ neighborHaveTtlMs?: number; /** * Optional budget (token bucket) for neighbor-assisted repair `FETCH_REQ` control traffic. * * If `<= 0`, no budget is applied. */ neighborRepairBudgetBps?: number; neighborRepairBurstMs?: number; /** * Optional ingress budget (token bucket) for proxy publishes (`MSG_PUBLISH_PROXY`). * * This is enforced per established child link to cap amplification/DoS at the root * and on intermediate relays. Cost is measured in payload bytes. * * If `<= 0`, no budget is applied. */ proxyPublishBudgetBps?: number; proxyPublishBurstMs?: number; /** * Optional ingress budget (token bucket) for relaying unicast control traffic * (`MSG_UNICAST`, `MSG_UNICAST_ACK`). * * This is enforced per established child link. Cost is measured in payload bytes. * * If `<= 0`, no budget is applied. */ unicastBudgetBps?: number; unicastBurstMs?: number; /** * If set (>0), do not forward data that is older than this many milliseconds, * based on the message header timestamp (origin publish time, when forwarding * without re-signing). * * This is primarily intended for "live" workloads where late data is worse * than missing data. */ maxDataAgeMs?: number; /** * Max number of cached route tokens kept per channel for targeted unicast. * * `0` disables route caching. */ routeCacheMaxEntries?: number; /** * TTL for cached route tokens (milliseconds). * * `0` disables expiry. */ routeCacheTtlMs?: number; /** * Best-effort "peer hints" cache size bound (per channel). * * This tracks peer hashes observed on the channel control-plane and is used * for route proxy fanout and neighbor-assisted repair. It is intentionally * bounded so it cannot grow toward channel size at large scale. * * `0` disables peer hint tracking. */ peerHintMaxEntries?: number; /** * TTL for peer hint entries (milliseconds). * * `0` disables expiry. */ peerHintTtlMs?: number; }; export type FanoutTreeJoinOptions = { timeoutMs?: number; retryMs?: number; signal?: AbortSignal; staleAfterMs?: number; /** * How long to wait for a JOIN_ACCEPT/JOIN_REJECT after sending a JOIN_REQ. */ joinReqTimeoutMs?: number; /** * Optional bootstrap nodes used as rendezvous/tracker servers for joining. * If omitted, `FanoutTree.setBootstraps()` (if configured) is used. */ bootstrap?: Array; /** * Max time to wait for a bootstrapped peer to become a `@peerbit/stream` * neighbor (protocol streams established) after dialing. */ bootstrapDialTimeoutMs?: number; /** * Max number of bootstrap peers to dial and keep as tracker candidates while joining. * * Set to `0` to use all provided bootstraps. */ bootstrapMaxPeers?: number; /** * How many candidate parents to request per tracker query. */ trackerCandidates?: number; /** * Shuffle only within the first K ranked candidates to spread load without * destroying the bias towards low-level/high-capacity parents. * * Set to `0` to disable shuffling. */ candidateShuffleTopK?: number; /** * How long to wait for a tracker reply before proceeding. */ trackerQueryTimeoutMs?: number; /** * How often to announce parent capacity to trackers (keep-alive). */ announceIntervalMs?: number; /** * TTL for announcements stored by trackers. * Trackers should treat entries as stale after this. */ announceTtlMs?: number; /** * Min interval between re-dial attempts to bootstrap peers when joining. */ bootstrapEnsureIntervalMs?: number; /** * Min interval between tracker queries while joining. */ trackerQueryIntervalMs?: number; /** * Min interval between parent-improvement checks while already attached. * * Set to `0` to preserve the current stability-first behavior where a healthy * parent is only replaced after disconnect/staleness/kick. */ parentUpgradeIntervalMs?: number; /** * Restrict proactive parent upgrades to leaves (nodes with no children). * * Defaults to `true` so parent improvement can be evaluated without * introducing relay churn higher in the tree. */ parentUpgradeLeafOnly?: boolean; /** * Minimum tree-level improvement required before replacing a healthy parent. * * `1` means only strictly better parents are eligible. Higher values add * hysteresis against marginal topology churn. */ parentUpgradeMinLevelGain?: number; /** * Minimum tree-level improvement required for proactive upgrades directly to * the root. * * Defaults to `3`. Root fanout is the scarce sender-side resource for a topic, * so direct-root upgrades require a stronger benefit than ordinary relay * improvements. */ parentUpgradeRootMinLevelGain?: number; /** * Minimum local branch improvement required for proactive upgrades directly to * the root. * * This is `levelGain * (1 + directChildren)`. It lets a relay-to-root move be * admitted when it improves a branch, even if the relay's own level gain is * smaller than `parentUpgradeRootMinLevelGain`. Defaults to * `parentUpgradeRootMinLevelGain`. */ parentUpgradeRootMinSubtreeGain?: number; /** * Minimum tree-level improvement required for proactive upgrades to non-root * relay parents. * * Defaults to `2` so a late direct root edge can still repair a one-level * detour, while marginal relay-to-relay moves are avoided under load. */ parentUpgradeNonRootMinLevelGain?: number; /** * Minimum advertised free child slots required for a proactive upgrade target. * * Defaults to `8` so a target still has several spare slots after accepting * this child; this avoids stampeding nearly-full parents under constrained * fanout. */ parentUpgradeMinFreeSlots?: number; /** * Minimum advertised free child slots required for a proactive direct-root * upgrade target. * * Defaults to `parentUpgradeMinFreeSlots`. This can be tuned independently * because root spare capacity protects sender-side fanout pressure, while * relay spare capacity protects intermediate branch stability. */ parentUpgradeRootMinFreeSlots?: number; /** * Maximum child-load ratio allowed for a live-probed proactive upgrade target * after accepting this peer. * * Defaults to `0.5`. Set to `0` to disable the ratio check. */ parentUpgradeMaxChildLoadRatio?: number; /** * Maximum child-load ratio allowed for a live-probed direct-root upgrade target * after accepting this peer. * * Defaults to `min(parentUpgradeMaxChildLoadRatio, 0.4)`. Root fanout is the * scarce sender-side resource, so root pressure stays more conservative than * relay parent pressure. Peers maintaining multiple local channels apply an * additional effective cap of `0.2` for direct-root upgrades so concurrent * writer trees do not concentrate on the same root at once. */ parentUpgradeRootMaxChildLoadRatio?: number; /** * Cooldown after a successful proactive upgrade before trying another one. */ parentUpgradeCooldownMs?: number; /** * Initial backoff after a failed probe/shadow parent-upgrade round. * * Failed rounds usually mean stale tracker state or saturated candidate * parents. Backoff is per channel and doubles up to * `parentUpgradeFailedBackoffMaxMs`. */ parentUpgradeFailedBackoffMinMs?: number; /** * Maximum per-channel backoff after repeated failed probe/shadow rounds. */ parentUpgradeFailedBackoffMaxMs?: number; /** * Minimum quiet time since parent attachment/data before a proactive upgrade. * * Defaults to 5s. This avoids voluntarily probing or switching parents during * active delivery or directly after a repair/rejoin event. */ parentUpgradeQuietMs?: number; /** * Minimum quiet time since the last local repair request before a proactive * upgrade check. * * Defaults to `parentUpgradeQuietMs` so upgrade probing stays silent while the * peer is still recovering missing data. */ parentUpgradeRepairQuietMs?: number; /** * Max successful proactive upgrades for one channel peer. Set to `0` for no cap. */ parentUpgradeMaxPerPeer?: number; /** * Skip proactive upgrades while this peer is actively missing data. */ parentUpgradeRepairGuard?: boolean; /** * Skip proactive upgrades until a finite channel end has been received and caught up. */ parentUpgradeDataGuard?: boolean; /** * Parent-upgrade strategy. * * - `direct`: join a better advertised parent directly. * - `probe`: first ask candidate parents for fresh level/capacity/data-state, * then only join a bounded winner. * - `shadow` (default): keep the current parent active, repeatedly probe one * candidate over an observation window, then promote only after it remains * healthy. */ parentUpgradeMode?: "direct" | "probe" | "shadow"; /** * Allow shadow mode to probe a direct root candidate even when tracker * capacity says the root is full. * * Defaults to `true` in shadow mode and `false` otherwise. Shadow mode keeps * this bounded by `parentUpgradeStaleRootProbeProbability` and live capacity * probes, while direct/probe modes avoid stale-root pressure unless explicitly * enabled. */ parentUpgradeVerifyStaleRootCapacity?: boolean; /** * Deterministic per-peer base sampling probability for stale-root verification. * * This applies only when `parentUpgradeVerifyStaleRootCapacity` is enabled and * tracker capacity says the root is full. Defaults to `0.015625` to avoid every * eligible peer probing the same advertised-full root at once. Single-channel * branch peers may use a bounded sample boost, capped at `0.25`; quiet, * completed single-channel leaves may use a `0.5` sample. Settled leaves that * maintain multiple local channels require fresh tracker capacity, recent slow * parent-delivery evidence, and a low-rate deterministic sample at the default * probability; raise this value explicitly to opt into more pressure. */ parentUpgradeStaleRootProbeProbability?: number; /** * How long to wait for a parent probe reply before skipping that candidate. */ parentProbeTimeoutMs?: number; /** * Max candidate parents to probe per upgrade check. */ parentProbeMaxPerRound?: number; /** * Max sequence lag tolerated for a probed parent candidate. * * `0` requires the candidate to be at least as fresh as this peer. */ parentProbeMaxLagMessages?: number; /** * Cooldown for a candidate after a probe proves it cannot safely become * our parent. This bounds repeated probes against stale tracker entries. */ parentProbeRejectCooldownMs?: number; /** * Maximum adaptive cooldown for a candidate after repeated rejected parent * probes. */ parentProbeRejectCooldownMaxMs?: number; /** * Minimum time a shadow parent candidate must stay healthy before promotion. */ parentShadowObserveMs?: number; /** * Minimum successful shadow observations required before promotion. */ parentShadowMinObservations?: number; /** * In shadow mode, keep the old parent attached for this long after a * candidate accepts JOIN before promoting during active data flow. * * Defaults to `5000` in shadow mode and `0` otherwise. Set to `0` to use * immediate post-probe promotion. */ parentShadowDualPathMs?: number; /** * Minimum live messages that must arrive from a shadow-attached candidate * before active-flow promotion is allowed. * * Defaults to `32` in shadow mode and `1` otherwise. */ parentShadowDualPathMinMessages?: number; /** * Max number of join candidates to try per retry "round". * * This prevents a long tail of sequential JOIN_REQ timeouts from blocking * the join loop for tens of seconds under overload. */ joinAttemptsPerRound?: number; /** * Cooldown applied to a candidate parent after dial/join failures. * * This reduces hot-spotting (everyone hammering the same few parents) and * keeps control-plane traffic bounded at large scale. */ candidateCooldownMs?: number; /** * Candidate scoring mode for selecting parent join targets. * * - `ranked-shuffle` (default): rank by (level, freeSlots, bid, source) and * shuffle within `candidateShuffleTopK` to spread load. * - `ranked-strict`: try ranked candidates in order (no shuffle). * - `weighted`: weighted shuffle within `candidateShuffleTopK` using * `candidateScoringWeights` (defaults bias low level + free slots). */ candidateScoringMode?: "ranked-shuffle" | "ranked-strict" | "weighted"; /** * Weights used when `candidateScoringMode="weighted"`. * * Larger values increase the influence of that signal. */ candidateScoringWeights?: { level?: number; freeSlots?: number; connected?: number; bidPerByte?: number; source?: number; }; }; /** * Tracker-backed provider discovery for targeted pulls (blocks/RPC). * * Providers announce under a bounded namespace key, and consumers query for K candidates. * This avoids any search/flood behaviors at large scale. */ export type FanoutProviderCandidate = { hash: string; addrs: Multiaddr[]; }; export type FanoutProviderAnnounceOptions = { ttlMs?: number; announceIntervalMs?: number; bootstrap?: Array; bootstrapDialTimeoutMs?: number; bootstrapMaxPeers?: number; }; export type FanoutProviderQueryOptions = { want?: number; seed?: number; timeoutMs?: number; queryTimeoutMs?: number; cacheTtlMs?: number; signal?: AbortSignal; bootstrap?: Array; bootstrapDialTimeoutMs?: number; bootstrapMaxPeers?: number; }; export type FanoutProviderWatchOptions = { want?: number; signal?: AbortSignal; onProviders: (providers: FanoutProviderCandidate[]) => void; bootstrap?: Array; bootstrapDialTimeoutMs?: number; bootstrapMaxPeers?: number; ttlMs?: number; renewIntervalMs?: number; }; export type FanoutProviderHandle = { close: () => void; }; export type FanoutTreeDataEvent = { topic: string; root: string; seq: number; payload: Uint8Array; from: string; origin: string; timestamp: bigint; message: DataMessage; }; export type FanoutTreeUnicastEvent = { topic: string; root: string; route: string[]; payload: Uint8Array; from: string; origin: string; to: string; timestamp: bigint; message: DataMessage; }; export interface FanoutTreeEvents extends StreamEvents { "fanout:data": CustomEvent; "fanout:unicast": CustomEvent; "fanout:joined": CustomEvent<{ topic: string; root: string; parent: string; }>; "fanout:kicked": CustomEvent<{ topic: string; root: string; from: string; }>; "fanout:provider": CustomEvent<{ namespace: string; providers: FanoutProviderCandidate[]; }>; "fanout:peer-unreachable": CustomEvent<{ topic: string; root: string; publicKeyHash: string; }>; } export declare class FanoutTree extends DirectStream { private channelsBySuffixKey; private readonly cachedSuffixKey; private readonly metricsBySuffixKey; private readonly joinTimeoutStreakByPeer; private readonly joinResetCooldownUntilByPeer; private bootstraps; private trackerBySuffixKey; private trackerNamespaceLru; private providerBySuffixKey; private providerNamespaceLru; private providerNamespaceBySuffixKey; private providerWatchersBySuffixKey; private providerWatchesBySuffixKey; private underlayPeerDisconnectHandler?; private pendingProviderQueryBySuffixKey; private providerAnnounceBySuffixKey; private parentUpgradeShadowInFlightSuffixKey?; private readonly defaultUploadOverheadBytes; private readonly random; private readonly unicastAckNodeTag32; private unicastAckSeq; readonly topicRootControlPlane: TopicRootControlPlane; private readonly nativeFanout?; private readonly codec; constructor(components: DirectStreamComponents, opts?: FanoutTreeStreamOptions); stop(): Promise; setBootstraps(addrs: Array): void; addBootstraps(addrs: Array): void; private touchTrackerNamespace; private pruneTrackerNamespaceIfEmpty; private touchProviderNamespace; private pruneProviderNamespaceIfEmpty; private pruneProviderWatchersIfEmpty; private getProviderCandidatesFromCache; /** Decoded provider entries (raw multiaddr bytes) -> candidates. Invalid * multiaddrs are dropped exactly like the historical inline parsing. */ private toProviderCandidates; private rememberProviderCandidates; private emitProviderUpdate; private publishProviderWatchUpdate; private getProviderNamespaceId; private getProviderNamespaceIdFromKey; provide(namespace: string, options?: FanoutProviderAnnounceOptions): FanoutProviderHandle; /** * Announce provider presence once (no background loop). * * This is useful for "on-demand" discovery where the caller wants to publish a * short-lived provider hint (e.g. after putting a block) without keeping a * per-namespace timer alive. */ announceProvider(namespace: string, options?: Omit): Promise; private _providerAnnounceLoop; private announceProviderOnce; private nextProviderReqId; queryProviderCandidates(namespace: string, options?: FanoutProviderQueryOptions): Promise; queryProviders(namespace: string, options?: FanoutProviderQueryOptions): Promise; watchProviders(namespace: string, options: FanoutProviderWatchOptions): FanoutProviderHandle; getChannelId(topic: string, root: string): FanoutTreeChannelId; openChannel(topic: string, root: string, opts: FanoutTreeChannelOptions): FanoutTreeChannelId; private abortPendingUnicastAcks; private resetParentDataLatency; private noteParentDataLatency; private clearParentUpgradeGrace; private commitParentUpgradeGrace; private rollbackParentUpgradeGrace; private startParentUpgradeGrace; private detachFromParent; private onPeerDisconnectedFromUnderlay; closeChannel(topic: string, root: string, options?: { notifyParent?: boolean; kickChildren?: boolean; }): Promise; getChannelStats(topic: string, root: string): { topic: string; root: string; parent?: string; level: number; children: number; effectiveMaxChildren: number; uploadLimitBps: number; droppedForwards: number; peerHintEntries: number; peerHintMaxEntries: number; routeCacheEntries: number; routeCacheMaxEntries: number; } | undefined; getChannelPeerHashes(topic: string, root: string, options?: { includeSelf?: boolean; }): string[]; joinChannel(topic: string, root: string, channelOpts: Omit, joinOpts?: FanoutTreeJoinOptions): Promise; /** * Returns this node's current route token for a channel, if attached. * * The token is a source-route path `[root, ..., self]` and can be shared * out-of-band to allow economical unicast within the channel. */ getRouteToken(topic: string, root: string): string[] | undefined; getRouteHint(topic: string, root: string, targetHash: string): FanoutRouteTokenHint | undefined; private cacheRoute; private cacheKnownCandidateAddrs; private touchPeerHint; private prunePeerHints; private pruneRouteCache; private getCachedRoute; private isRouteValidForChannel; private nextReqId; private completeRouteProxy; /** * Coalesce onto an in-flight same-target/same-direction proxy search, if one * exists. Returns true when the requester was attached as a waiter (or * answered empty on waiter overflow) and no new search should start. */ private coalesceRouteProxy; /** * Drop all in-flight route-proxy state for a channel (detach/kick/close). * Local resolvers are settled with `undefined` so root-origin * `resolveRouteToken()` callers never hang; remote requesters time out on * their own deadlines, as before. */ private clearRouteProxies; private proxyRouteQuery; resolveRouteToken(topic: string, root: string, targetHash: string, options?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; private nextUnicastAckToken; unicastTo(topic: string, root: string, targetHash: string, payload: Uint8Array, options?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; unicastToAck(topic: string, root: string, targetHash: string, payload: Uint8Array, options?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; unicastAck(topic: string, root: string, toRoute: string[], targetHash: string, payload: Uint8Array, options?: { timeoutMs?: number; signal?: AbortSignal; }): Promise; /** * Economical unicast within an existing fanout channel. * * Any sender can send to `toRoute` by forwarding the message *up* to the root, * and then letting the root forward it *down* the provided route. * * `toRoute` must be a route token `[root, ..., target]`. */ unicast(topic: string, root: string, toRoute: string[], payload: Uint8Array): Promise; publishData(topic: string, root: string, payload: Uint8Array): Promise; /** * Publishes payload to all channel members. * * Root publishes directly on the data-plane. Non-root members proxy the publish * upstream to the root, which assigns a sequence number and broadcasts. */ publishToChannel(topic: string, root: string, payload: Uint8Array): Promise; publishToChannelMaybe(topic: string, root: string, payload: Uint8Array): Promise; private waitForChannelAttachment; publishEnd(topic: string, root: string, lastSeqExclusive: number): Promise; private makeDataId; private getSuffixKeyFromId; private getMetricsForSuffixKey; private recordControlSend; private recordControlReceive; private markCached; private getCached; private noteJoinResponse; private noteJoinTimeout; private _sendControl; private _sendControlMany; private refillUploadTokens; private refillNeighborRepairTokens; private takeIngressBudget; private _sendData; private _forwardDataMessage; private noteReceivedSeq; private noteEnd; private tickRepair; private getBootstrapsForChannel; private getSelfAnnounceAddrs; private ensureBootstrapPeers; private announceToTrackers; private _announceLoop; private _repairLoop; private pruneLazyPeers; private pruneDisconnectedChildren; private pruneHaveByPeer; private getHaveRange; private maybeSendIHave; private ensureMeshPeers; private refreshMeshCandidates; private _meshLoop; private queryTrackers; private ensurePeerConnection; private getPeerAddrsBytes; private pickJoinRejectRedirects; private sendJoinReject; private sendTrackerFeedback; private pruneParentUpgradeReservations; private parentUpgradeReservationCount; private createParentUpgradeReservation; private consumeParentUpgradeReservation; private isRootedForParentProbe; private encodeParentProbeReplyForChannel; private probeParentCandidate; private _joinLoop; private maybeImproveParent; private tryJoinOnce; private kickChildHashes; private kickChildren; onDataMessage(from: any, peerStream: PeerStreams, message: DataMessage, seenBefore: number): Promise; } //# sourceMappingURL=fanout-tree.d.ts.map