first commit

This commit is contained in:
2026-05-29 08:20:56 +01:00
commit 949299f58d
6368 changed files with 527176 additions and 0 deletions

View File

@@ -0,0 +1,104 @@
# `interruption-classifier`
<!-- begin:auto-docs -->
_(no submodules)_
<!-- end:auto-docs -->
## 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: `peer` beats
`media-permission` beats `network` beats `signalling`.
- WebRTC-friendly: keys directly off `iceConnectionState` and
`connectionState`.
- 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
```ts
import { InterruptionClassifier } from "@suaveplan/react-audio-stream/session";
const classifier = new InterruptionClassifier();
const verdict = classifier.classify({ iceConnectionState: "failed" });
// verdict.class === "network"
```
## Usage: Advanced
```ts
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 is `network` or `signalling`.
- `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.