/** * T4 · Credit-based per-stream flow control (§4.1) — pure, in-memory, deny-by-default. * * Each stream has an independent send window: exhausting stream A's credit MUST NOT affect * stream B (heavy `vim`/`top` redraw can't starve another stream). Unknown/released streams * carry no credit (deny-by-default: prevents send-after-close write amplification). * Immutable-style updates (replace the record, never mutate in place). */ export const DEFAULT_INITIAL_WINDOW = 256 * 1024 export const WINDOW_REPLENISH_THRESHOLD = 0.5 // emit WINDOW_UPDATE when half consumed interface StreamCredit { readonly window: number // remaining SEND credit readonly initial: number // initial window (for replenish threshold) readonly delivered: number // RECEIVED bytes since last replenish } export interface FlowController { registerStream(streamId: number, initialWindow: number): void releaseStream(streamId: number): void canSend(streamId: number, bytes: number): boolean consumeSendCredit(streamId: number, bytes: number): void grantCredit(streamId: number, credit: number): void creditFor(streamId: number): number onDelivered(streamId: number, bytes: number): number } export function createFlowController(): FlowController { const streams = new Map() return { registerStream(streamId, initialWindow) { streams.set(streamId, { window: initialWindow, initial: initialWindow, delivered: 0 }) }, releaseStream(streamId) { streams.delete(streamId) }, canSend(streamId, bytes) { const c = streams.get(streamId) if (c === undefined) return false // deny-by-default (unknown/released stream) return bytes <= c.window }, consumeSendCredit(streamId, bytes) { const c = streams.get(streamId) if (c === undefined) return const nextWindow = c.window - bytes // Never go negative (canSend gates callers; clamp defensively). streams.set(streamId, { ...c, window: Math.max(0, nextWindow) }) }, grantCredit(streamId, credit) { const c = streams.get(streamId) if (c === undefined) return streams.set(streamId, { ...c, window: c.window + credit }) }, creditFor(streamId) { return streams.get(streamId)?.window ?? 0 }, onDelivered(streamId, bytes) { const c = streams.get(streamId) if (c === undefined) return 0 const delivered = c.delivered + bytes if (delivered >= c.initial * WINDOW_REPLENISH_THRESHOLD) { streams.set(streamId, { ...c, delivered: 0 }) return delivered // replenish this many bytes of credit back to the peer } streams.set(streamId, { ...c, delivered }) return 0 }, } }