/** * T3 · Stream state machine (§4.1) — a PURE reducer over the per-stream lifecycle. * * Lifecycle: `OPEN → (DATA | WINDOW_UPDATE)* → CLOSE`. An illegal transition returns * `{ illegal: true }` so the caller RSTs that ONE stream (never the tunnel) — this is what * lets P4's end-to-end `seq` monotonicity mean something (INV13). No I/O, no input mutation. */ import type { MuxFrameType } from 'relay-contracts' export type StreamState = | 'idle' | 'open' | 'halfClosedLocal' | 'halfClosedRemote' | 'closed' export type StreamDirection = 'inbound' | 'outbound' export type StreamTransition = { readonly next: StreamState } | { readonly illegal: true } const ILLEGAL: StreamTransition = { illegal: true } export function initialStreamState(): StreamState { return 'idle' } export function isTerminal(s: StreamState): boolean { return s === 'closed' } /** True when the state still accepts DATA/WINDOW_UPDATE flow in at least one direction. */ function isFlowing(s: StreamState): boolean { return s === 'open' || s === 'halfClosedLocal' || s === 'halfClosedRemote' } /** The half-closed state produced when `dir` sends FIN while fully open. */ function halfCloseFor(dir: StreamDirection): StreamState { return dir === 'outbound' ? 'halfClosedLocal' : 'halfClosedRemote' } /** * Total transition function. Returns a NEW state (never mutates). `fin` marks a DATA/CLOSE * frame that half/fully closes the sending direction; `dir` is which side sent the frame. */ export function nextStreamState( current: StreamState, type: MuxFrameType, fin: boolean, dir: StreamDirection, ): StreamTransition { if (current === 'closed') return ILLEGAL switch (type) { case 'open': // OPEN is only legal from idle; re-OPEN of a live stream is illegal. return current === 'idle' ? { next: 'open' } : ILLEGAL case 'data': case 'windowUpdate': { if (current === 'idle') return ILLEGAL // DATA/WU before OPEN if (!isFlowing(current)) return ILLEGAL // WINDOW_UPDATE never carries FIN; only DATA may half-close its direction. if (type === 'windowUpdate') return { next: current } if (!fin) return { next: current } return finTransition(current, dir) } case 'close': { if (current === 'idle') return ILLEGAL // CLOSE before OPEN return { next: 'closed' } } // ping/pong/goaway are connection-level (streamId 0), never per-stream. default: return ILLEGAL } } /** Apply a FIN from `dir` to a flowing state; both-side FIN ⇒ closed. */ function finTransition(current: StreamState, dir: StreamDirection): StreamTransition { const half = halfCloseFor(dir) if (current === 'open') return { next: half } // Already half-closed on the OTHER side, and now this side FINs ⇒ fully closed. if (current === 'halfClosedLocal' && dir === 'inbound') return { next: 'closed' } if (current === 'halfClosedRemote' && dir === 'outbound') return { next: 'closed' } // Re-FIN on the already-closed direction is illegal. return ILLEGAL }