import { assertJsonTextStructureWithinLimits } from './json-text-structure-limit' const TERMINAL_STREAM_KIND = 0x74 const TERMINAL_STREAM_VERSION = 1 const HEADER_BYTES = 16 export const TERMINAL_STREAM_JSON_MAX_BYTES = 8 * 1024 * 1024 export const TERMINAL_STREAM_JSON_STRUCTURE_LIMITS = { structuralTokens: 256 * 1024, nestingDepth: 32 } as const export enum TerminalStreamOpcode { Output = 1, SnapshotStart = 2, SnapshotChunk = 3, SnapshotEnd = 4, Resized = 5, Error = 6, Input = 7, Resize = 8, Subscribe = 9, Unsubscribe = 10, SnapshotRequest = 11, Metadata = 12, // Why 13: Metadata=12 shipped to mobile clients in v1.4.120; Ack (branch-only // remote-multiplex flow control) renumbers to stay wire-compatible. Ack = 13, // Why 14: Ack already occupies 13 on current clients; older runtimes ignore // this opcode and still receive the compatibility Resize frame behind it. ClaimViewport = 14, OutputSpan = 15 } export type TerminalStreamFrame = { opcode: TerminalStreamOpcode streamId: number seq: number payload: Uint8Array } export function encodeTerminalStreamFrame(frame: TerminalStreamFrame): Uint8Array { const out = new Uint8Array(HEADER_BYTES + frame.payload.length) const view = new DataView(out.buffer, out.byteOffset, out.byteLength) view.setUint8(0, TERMINAL_STREAM_KIND) view.setUint8(1, TERMINAL_STREAM_VERSION) view.setUint8(2, frame.opcode) view.setUint8(3, 0) view.setUint32(4, frame.streamId, true) const seq = Math.max(0, Math.floor(frame.seq)) view.setUint32(8, Math.floor(seq / 0x100000000), true) view.setUint32(12, seq >>> 0, true) out.set(frame.payload, HEADER_BYTES) return out } export function decodeTerminalStreamFrame(bytes: Uint8Array): TerminalStreamFrame | null { if (bytes.length < HEADER_BYTES) { return null } const view = new DataView(bytes.buffer, bytes.byteOffset, bytes.byteLength) if (view.getUint8(0) !== TERMINAL_STREAM_KIND || view.getUint8(1) !== TERMINAL_STREAM_VERSION) { return null } const opcode = view.getUint8(2) if (!isTerminalStreamOpcode(opcode)) { return null } const high = view.getUint32(8, true) const low = view.getUint32(12, true) return { opcode, streamId: view.getUint32(4, true), seq: high * 0x100000000 + low, payload: bytes.slice(HEADER_BYTES) } } export function encodeTerminalStreamJson(value: unknown): Uint8Array { return new TextEncoder().encode(JSON.stringify(value)) } export function decodeTerminalStreamJson(payload: Uint8Array): T | null { if (payload.byteLength > TERMINAL_STREAM_JSON_MAX_BYTES) { return null } try { const content = new TextDecoder().decode(payload) assertJsonTextStructureWithinLimits(content, TERMINAL_STREAM_JSON_STRUCTURE_LIMITS) return JSON.parse(content) as T } catch { return null } } export function encodeTerminalStreamText(value: string): Uint8Array { return new TextEncoder().encode(value) } export function decodeTerminalStreamText(payload: Uint8Array): string { return new TextDecoder().decode(payload) } function isTerminalStreamOpcode(value: number): value is TerminalStreamOpcode { return ( value === TerminalStreamOpcode.Output || value === TerminalStreamOpcode.SnapshotStart || value === TerminalStreamOpcode.SnapshotChunk || value === TerminalStreamOpcode.SnapshotEnd || value === TerminalStreamOpcode.Resized || value === TerminalStreamOpcode.Error || value === TerminalStreamOpcode.Input || value === TerminalStreamOpcode.Resize || value === TerminalStreamOpcode.Subscribe || value === TerminalStreamOpcode.Unsubscribe || value === TerminalStreamOpcode.SnapshotRequest || value === TerminalStreamOpcode.Metadata || value === TerminalStreamOpcode.Ack || value === TerminalStreamOpcode.ClaimViewport || value === TerminalStreamOpcode.OutputSpan ) }