/** * Per-stream flow control — PLAN_RELAY_AGENT T11. CONSUMES the §4.1 WINDOW_UPDATE credit protocol * (P1 owns the protocol). Per-stream credit means one heavy vim/top redraw can't starve another * stream. streamId 0 is the connection-level window applied to the whole link. */ export interface FlowController { consume(streamId: number, bytes: number): boolean grant(streamId: number, credit: number): void initWindow(streamId: number, initialCredit: number): void } const CONNECTION_STREAM_ID = 0 export function createFlowController(): FlowController { const windows = new Map() // Connection-level window is unbounded until explicitly initialized. windows.set(CONNECTION_STREAM_ID, Number.POSITIVE_INFINITY) function remaining(streamId: number): number { return windows.get(streamId) ?? 0 } return { initWindow(streamId: number, initialCredit: number): void { windows.set(streamId, initialCredit) }, grant(streamId: number, credit: number): void { windows.set(streamId, remaining(streamId) + credit) }, consume(streamId: number, bytes: number): boolean { const conn = remaining(CONNECTION_STREAM_ID) const stream = remaining(streamId) if (stream < bytes || conn < bytes) return false // credit exhausted → pause windows.set(streamId, stream - bytes) if (conn !== Number.POSITIVE_INFINITY) { windows.set(CONNECTION_STREAM_ID, conn - bytes) } return true }, } }