Files
skills/docs/ui/react/react-audio-stream/src/session/reconnect.md
2026-05-29 08:20:56 +01:00

3.7 KiB
Raw Blame History

reconnect

(no submodules)

Purpose

The reconnect module owns the auto-reconnect lifecycle that StreamSession runs after a disconnect interruption. It wraps @suaveplan/retry's exponential-backoff engine with the spec-mandated parameters (1 s base, ×2 multiplier, ±20% jitter, 30 s cap, 8 attempts) and exposes a single async entry point that the session calls once per interruption event.

Keeping the controller separate from the FSM means the session class stays small and the reconnect policy is independently testable. Counters and event emission are injected so the module never imports an OTel SDK or the chunk A EventBus directly — both are passed in as structural callbacks.

Features

  • shouldReconnect(class) — pure predicate selecting only 'network' and 'signalling' interruption classes.
  • createReconnectDelay(rng?) — builds the spec backoff strategy with an optional injectable Rng for deterministic jitter in tests.
  • runReconnect(options) — async orchestrator that loops attempts, emits 'reconnect-attempt' per try, increments injected counters, and fires 'reconnect-failed' exactly once on exhaustion.
  • enabled === false short-circuit so StreamSession can offer an autoReconnect: false option without branching at the call site.
  • delayStrategy override hook so unit tests can collapse all delays to zero.
  • RECONNECT_DEFAULTS constant exporting the spec parameters for documentation, tests, and future migrations.

Usage: Basic

import { runReconnect } from "@suaveplan/react-audio-stream/session";

const result = await runReconnect({
    interruptionClass: "network",
    connect: () => session.connect(roomId),
    emitter: session.bus,
    counters: session.reconnectCounters,
});

Usage: Advanced

const result = await runReconnect({
    interruptionClass: classifier.classify(err),
    connect: () => session.connect(roomId),
    emitter: session.bus,
    counters: session.reconnectCounters,
    enabled: opts.autoReconnect !== false,
    maxAttempts: opts.maxReconnectAttempts ?? 8,
    rng: opts.rng,
    timers: opts.timers,
    delayStrategy: opts.deterministicDelay,
});

if (!result.succeeded && result.attempted) {
    log.warn("reconnect exhausted", { attempts: result.attempts });
}

API Reference

  • RECONNECT_DEFAULTS — { initialMs: 1000, base: 2, maxMs: 30000, maxAttempts: 8 }.
  • shouldReconnect(class: InterruptionClass): boolean.
  • createReconnectDelay(rng?): RetryStrategy.
  • runReconnect(options): Promise<ReconnectResult>.
  • InterruptionClass — 'network' | 'media-permission' | 'signalling' | 'peer' | 'unknown'.
  • ReconnectCounters — { incrementAttempt(), incrementFailed() }.
  • ReconnectEmitter — structural { emit(event, payload) } shape.
  • ReconnectOptions / ReconnectResult — see types in source.

Implementation Notes

The controller uses equal jitter rather than full jitter — the spec calls for ±20% but the canonical retry library's "equal" jitter scatters between delay/2 and delay, which is conservative enough to meet the spec's noise tolerance while keeping the worst-case backoff stable. Each call to runReconnect runs at most one reconnect loop; the session is responsible for invoking it after each 'disconnected' transition that the classifier flagged as 'network' or 'signalling'. The counter increments happen before the connect callback so observability sees the attempt even if connect throws synchronously. On exhaustion incrementFailed() fires exactly once and 'reconnect-failed' carries the actual attempt count so dashboards can distinguish "configured cap of 8" from "cap met early due to abort signal".