3.8 KiB
interruption-classifier
(no submodules)
Purpose
InterruptionClassifier maps a structural disconnect cause to exactly
one of network | media-permission | signalling | peer | unknown.
Classification is deterministic — every rule is a typed equality or
membership check, no probabilistic heuristics — so the same cause
record always produces the same verdict regardless of timing or
ordering. This is essential for the auto-reconnect policy that lands in
chunk D: the policy decides whether to retry by inspecting the verdict
label, and a flaky classifier would translate into flaky reconnect
behaviour for users.
The classifier is intentionally state-free. Every consumer (the
StreamSession, future React hooks, integration tests) constructs one
instance and calls classify on every disconnect — there is no
internal counter, ring buffer, or memo, so a single instance can be
shared across sessions without contention.
Features
- Five disjoint labels covering every observed interruption shape.
- Priority-ordered rules with explicit precedence:
peerbeatsmedia-permissionbeatsnetworkbeatssignalling. - WebRTC-friendly: keys directly off
iceConnectionStateandconnectionState. - WebSocket-friendly: keys off RFC 6455 close codes, skipping the clean-close codes (1000, 1001) so a graceful server-side close does not trigger reconnect.
shouldAutoReconnect(class)helper exposes the chunk-D reconnect policy in a single place.- Zero allocations on the hot path beyond the verdict object.
Usage: Basic
import { InterruptionClassifier } from "@suaveplan/react-audio-stream/session";
const classifier = new InterruptionClassifier();
const verdict = classifier.classify({ iceConnectionState: "failed" });
// verdict.class === "network"
Usage: Advanced
import {
InterruptionClassifier,
type InterruptionCause,
} from "@suaveplan/react-audio-stream/session";
const classifier = new InterruptionClassifier();
function onDisconnect(rawCause: InterruptionCause) {
const verdict = classifier.classify(rawCause);
if (classifier.shouldAutoReconnect(verdict.class)) {
scheduleReconnect(verdict);
} else {
surfaceToUi(verdict);
}
}
// Hard browser-permission revoke
onDisconnect({
mediaErrorName: "NotAllowedError",
reason: "user revoked microphone permission",
});
// → verdict.class === "media-permission"; no reconnect.
// Server-initiated abnormal close
onDisconnect({ signallingCloseCode: 1006, reason: "abrupt server close" });
// → verdict.class === "signalling"; reconnect scheduled.
API Reference
class InterruptionClassifier— stateless classifier.classify(cause: InterruptionCause): InterruptionVerdict— apply the rule mapping and return{ class, cause }.shouldAutoReconnect(class: InterruptionClass): boolean— true iff the class isnetworkorsignalling.InterruptionClass,InterruptionCause,InterruptionVerdict— re-exported types.
Implementation Notes
The rule ordering is the only piece of business logic with subtle
consequences. peer is evaluated first because an intentional remote
leave should never be treated as a network glitch — even if the ICE
state also happens to read disconnected during the closing handshake.
Permission errors take precedence over ICE because a user-revoked
microphone often surfaces as both a getUserMedia rejection and an
ICE failure milliseconds later; the permission failure is the
actionable cause. Close codes 1000 and 1001 are explicitly skipped so a
clean server shutdown (going away) does not show up as a signalling
interruption — those map to unknown instead, which the reconnect
policy treats as non-retryable. The classifier deliberately holds no
state so it composes cleanly into the FSM-driven session flow without
spreading hidden coupling.