/** * Generic account subscription manager using Solana programNotifications. * Framework-agnostic — consumers provide error/log callbacks for observability. * * Features: * - Ref-counted: first listener starts the WS, last unsubscribe tears it down. * - Slot-based ordering + buffer dedup (skips unchanged data, updates slot only). * - Auto-reconnect via {@link runReconnectLoop}. * - Decode/listener throws are reported but DO NOT trigger reconnect. * - Per-listener `onReconnect` for state refresh after WS recovery; cleaned * up on unsubscribe so departed listeners' hooks cannot fire later. */ import { type Address, type Base58EncodedBytes, type Commitment, type RpcSubscriptions, type Slot, type SolanaRpcSubscriptionsApi } from '@solana/kit'; import { type SubscriptionHandle } from './subscriptionHandle'; import type { UpdateScheduler } from './wsUpdateScheduler'; export type SubscriptionCallback = (address: Address, buffer: Uint8Array, slot: Slot) => void; export type SubscriptionMode = 'ws' | 'idle'; export type SubscriptionErrorHandler = (error: unknown) => void; export type SubscriptionLogger = (...args: unknown[]) => void; export type ProgramFilter = Readonly<{ dataSize: bigint; }> | Readonly<{ memcmp: Readonly<{ offset: bigint; bytes: Base58EncodedBytes; encoding: 'base58'; }>; }>; export interface AccountSubscriptionManagerConfig { wsRpc: RpcSubscriptions; onError?: SubscriptionErrorHandler; onLog?: SubscriptionLogger; reconnectDelayMs?: number; } export interface SubscribeProgramAccountsOptions { commitment?: Commitment; /** Called after WS reconnects, before resuming the notification loop. * Use to refresh state that may have drifted during downtime. * * Per-listener: each subscriber's hook is tracked independently and * cleaned up on `unsubscribe()`. The hook invoked on each reconnect is * the most recently registered one still attached (latest wins), so a * subscriber whose hook is currently in use can leave without leaving * a dangling reference. * * The manager wraps the call in retry-with-exponential-backoff (capped * at 30s, 5 attempts). A throwing hook is reported through `onError` * per attempt; after the max, we give up and proceed with cached state. * Aborts cleanly on `unsubscribe()`. */ onReconnect?: () => void | Promise; /** Request a delivery cadence of at most one per N ms. The actual cadence * is the minimum of all subscribers' demands (**shortest wins**) — your * callback may fire faster than your own `throttleMs` if another * subscriber asked for less. * * - A subscriber that omits `throttleMs` adds no demand and inherits the * existing cadence. To guarantee strictly immediate delivery, ensure * no other subscriber on the same `(programId, commitment, filters)` * key sets a `throttleMs` — otherwise their throttle wins. * - A throttled subscriber joining an unthrottled key silently throttles * the original subscriber (intentional — fast wins, no data loss). * * Required when `scheduler` is set (sets the group cadence). */ throttleMs?: number; /** Cross-stream flush scheduler — registers an updater under `throttleMs` * in the scheduler's group map. Drains the buffer on each scheduler tick * instead of running an own setTimeout. * * Per-listener tracking: each subscriber's scheduler is recorded, and * the **active** scheduler is the FIRST one passed (insertion order). * Subscribers that omit `scheduler` don't contribute, so a no-scheduler * subscriber doesn't block a later one from establishing the active * scheduler. If a subscriber passes a scheduler DIFFERENT from the * active one, the manager logs (via `onLog`) and records the demand * anyway — that scheduler will take over only if every listener owning * the currently-active scheduler unsubscribes. * * On unsubscribe, the active scheduler is recomputed and the buffered- * drain registration moves to the next scheduler in line (or falls back * to a self-managed timer if none remain). This prevents holding a dead * reference to a scheduler whose owner has departed. * * Note: this subscription's scheduler-group cadence follows the * shortest-wins effective `throttleMs`. If a later subscriber to the * same key passes a shorter `throttleMs` (with or without scheduler), * the scheduler-group registration moves to the faster cadence — your * other subscriptions registered under the original cadence will no * longer flush together with this one. */ scheduler?: UpdateScheduler; } export declare class AccountSubscriptionManager { private subscriptions; private nextListenerId; private wsRpc; private onError; private onLog; private reconnectDelayMs; constructor(config: AccountSubscriptionManagerConfig); /** * Subscribe to program account changes. First subscriber for a key starts * the WS; last unsubscribe tears it down. Late subscribers replay any * cached account data immediately. */ subscribeProgramAccounts(programId: Address, filters: readonly ProgramFilter[], callback: SubscriptionCallback, options?: SubscribeProgramAccountsOptions): SubscriptionHandle; /** * Drain any buffered notifications immediately. With `commitment`, flushes * just that subscription; without, flushes all matching programId+filters * across all commitments. * * No-op if unbuffered or empty. Cadence timers keep ticking — next firing * may be a no-op since the buffer was just cleared. */ flushPending(programId: Address, filters: readonly ProgramFilter[], commitment?: Commitment): void; getMode(programId: Address, filters: readonly ProgramFilter[], commitment?: Commitment): SubscriptionMode; destroy(): void; /** Resolves when the WS subscription is established. * * Throws if no subscription matches `(programId, filters, commitment)` — * silently resolving used to make caller typos (wrong commitment, missing * prior `subscribeProgramAccounts`, stale filter array) look like a * successful wait. Failing loudly surfaces the sequencing bug at the * call site. */ waitForConnection(programId: Address, filters: readonly ProgramFilter[], commitment?: Commitment): Promise; private buildKey; /** Direct lookup with `commitment`; fan-out scan without. */ private findSubscription; /** Re-wire cadence machinery after a demands change. Fast no-op if effective throttle didn't change. */ private recomputeEffectiveThrottle; /** Re-pick the active scheduler from current demands and re-wire the * group registration if the choice changed. */ private recomputeActiveScheduler; private tearDownActiveCadence; private attachToSchedulerGroup; private flushOrphanedBuffer; private rearmTimerForBufferedData; /** No-op if a timer is already armed. */ private armThrottleDrainTimer; private flushSubscription; private createSubscription; private startWs; private handleNotification; private deliverByCadence; private drainBufferedNotifications; private fireListeners; private teardown; /** Sleep `ms`, but resolve immediately on abort — mirrors the pattern in * runReconnectLoop so teardown cancels in-flight reconnect-hook retries. */ private static abortableSleep; /** Calls the latest registered `onReconnect` hook with bounded retry-with- * backoff. The manager is the documented wrapper for runReconnectLoop's * best-effort `onBeforeReconnect` contract — failing the hook silently * would let listeners resume with stale state (e.g. borrowOrderFills * misclassifying new fills against pre-downtime obligations). * * After {@link RECONNECT_HOOK_MAX_RETRIES} consecutive failures we give * up and proceed anyway: an indefinitely failing hook would block the * WS forever, which is worse than stale state. The error reporter sees * every failure regardless. */ private runReconnectHookWithRetry; } //# sourceMappingURL=accountSubscriptionManager.d.ts.map