diff --git a/src/main/daemon/binary-frame.test.ts b/src/main/daemon/binary-frame.test.ts deleted file mode 100644 index 81148f18a59..00000000000 --- a/src/main/daemon/binary-frame.test.ts +++ /dev/null @@ -1,167 +0,0 @@ -import { describe, expect, it, vi } from 'vitest' -import { encodeFrame, createFrameParser, FrameType, FRAME_HEADER_SIZE } from './binary-frame' - -describe('encodeFrame', () => { - it('encodes a data frame with correct header', () => { - const payload = Buffer.from('hello') - const frame = encodeFrame(FrameType.Data, payload) - - expect(frame.length).toBe(FRAME_HEADER_SIZE + payload.length) - expect(frame[0]).toBe(FrameType.Data) - expect(frame.readUInt32BE(1)).toBe(payload.length) - expect(frame.subarray(FRAME_HEADER_SIZE).toString()).toBe('hello') - }) - - it('encodes a resize frame', () => { - const payload = Buffer.from(JSON.stringify({ cols: 120, rows: 40 })) - const frame = encodeFrame(FrameType.Resize, payload) - - expect(frame[0]).toBe(FrameType.Resize) - expect(frame.readUInt32BE(1)).toBe(payload.length) - }) - - it('encodes an exit frame with exit code', () => { - const payload = Buffer.from(JSON.stringify({ code: 0 })) - const frame = encodeFrame(FrameType.Exit, payload) - - expect(frame[0]).toBe(FrameType.Exit) - const decoded = JSON.parse(frame.subarray(FRAME_HEADER_SIZE).toString()) - expect(decoded.code).toBe(0) - }) - - it('encodes empty payload', () => { - const frame = encodeFrame(FrameType.Kill, Buffer.alloc(0)) - - expect(frame.length).toBe(FRAME_HEADER_SIZE) - expect(frame[0]).toBe(FrameType.Kill) - expect(frame.readUInt32BE(1)).toBe(0) - }) - - it('throws on payload exceeding max size', () => { - const oversized = Buffer.alloc(1024 * 1024 + 1) - expect(() => encodeFrame(FrameType.Data, oversized)).toThrow() - }) -}) - -describe('createFrameParser', () => { - it('parses a single complete frame', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const payload = Buffer.from('test data') - const frame = encodeFrame(FrameType.Data, payload) - parser.feed(frame) - - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][0]).toBe(FrameType.Data) - expect(onFrame.mock.calls[0][1].toString()).toBe('test data') - }) - - it('parses multiple frames in one chunk', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const frame1 = encodeFrame(FrameType.Data, Buffer.from('one')) - const frame2 = encodeFrame(FrameType.Data, Buffer.from('two')) - parser.feed(Buffer.concat([frame1, frame2])) - - expect(onFrame).toHaveBeenCalledTimes(2) - expect(onFrame.mock.calls[0][1].toString()).toBe('one') - expect(onFrame.mock.calls[1][1].toString()).toBe('two') - }) - - it('handles frame split across header boundary', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const payload = Buffer.from('split test') - const frame = encodeFrame(FrameType.Data, payload) - - // Split in the middle of the header (3 bytes, then the rest) - parser.feed(frame.subarray(0, 3)) - expect(onFrame).not.toHaveBeenCalled() - - parser.feed(frame.subarray(3)) - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][1].toString()).toBe('split test') - }) - - it('handles frame split in the middle of payload', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const payload = Buffer.from('payload split') - const frame = encodeFrame(FrameType.Data, payload) - - // Split after header + 3 bytes of payload - const splitPoint = FRAME_HEADER_SIZE + 3 - parser.feed(frame.subarray(0, splitPoint)) - expect(onFrame).not.toHaveBeenCalled() - - parser.feed(frame.subarray(splitPoint)) - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][1].toString()).toBe('payload split') - }) - - it('handles byte-by-byte feeding', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const frame = encodeFrame(FrameType.Exit, Buffer.from('{"code":42}')) - - for (let i = 0; i < frame.length; i++) { - parser.feed(frame.subarray(i, i + 1)) - } - - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][0]).toBe(FrameType.Exit) - expect(JSON.parse(onFrame.mock.calls[0][1].toString())).toEqual({ code: 42 }) - }) - - it('handles zero-length payload frame', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const frame = encodeFrame(FrameType.Kill, Buffer.alloc(0)) - parser.feed(frame) - - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][0]).toBe(FrameType.Kill) - expect(onFrame.mock.calls[0][1].length).toBe(0) - }) - - it('parses different frame types correctly', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - const frames = [ - encodeFrame(FrameType.Data, Buffer.from('data')), - encodeFrame(FrameType.Resize, Buffer.from('resize')), - encodeFrame(FrameType.Signal, Buffer.from('signal')) - ] - - parser.feed(Buffer.concat(frames)) - - expect(onFrame).toHaveBeenCalledTimes(3) - expect(onFrame.mock.calls[0][0]).toBe(FrameType.Data) - expect(onFrame.mock.calls[1][0]).toBe(FrameType.Resize) - expect(onFrame.mock.calls[2][0]).toBe(FrameType.Signal) - }) - - it('resets buffer state', () => { - const onFrame = vi.fn() - const parser = createFrameParser(onFrame) - - // Feed partial frame then reset - const frame = encodeFrame(FrameType.Data, Buffer.from('lost')) - parser.feed(frame.subarray(0, 3)) - parser.reset() - - // Feed a new complete frame - const frame2 = encodeFrame(FrameType.Data, Buffer.from('fresh')) - parser.feed(frame2) - - expect(onFrame).toHaveBeenCalledOnce() - expect(onFrame.mock.calls[0][1].toString()).toBe('fresh') - }) -}) diff --git a/src/main/daemon/binary-frame.ts b/src/main/daemon/binary-frame.ts deleted file mode 100644 index e8c4fd6ae90..00000000000 --- a/src/main/daemon/binary-frame.ts +++ /dev/null @@ -1,56 +0,0 @@ -export { FrameType } from './types' -import type { FrameType } from './types' -import { FRAME_HEADER_SIZE, FRAME_MAX_PAYLOAD } from './types' - -export { FRAME_HEADER_SIZE } - -export function encodeFrame(type: FrameType, payload: Buffer): Buffer { - if (payload.length > FRAME_MAX_PAYLOAD) { - throw new Error(`Frame payload ${payload.length} exceeds max ${FRAME_MAX_PAYLOAD}`) - } - - const frame = Buffer.allocUnsafe(FRAME_HEADER_SIZE + payload.length) - frame[0] = type - frame.writeUInt32BE(payload.length, 1) - payload.copy(frame, FRAME_HEADER_SIZE) - return frame -} - -export type FrameParser = { - feed(chunk: Buffer): void - reset(): void -} - -export function createFrameParser( - onFrame: (type: FrameType, payload: Buffer) => void -): FrameParser { - let buffer: Buffer = Buffer.alloc(0) - - function parse(): void { - while (buffer.length >= FRAME_HEADER_SIZE) { - const payloadLength = buffer.readUInt32BE(1) - const totalLength = FRAME_HEADER_SIZE + payloadLength - - if (buffer.length < totalLength) { - break - } - - const type = buffer[0] as FrameType - const payload = buffer.subarray(FRAME_HEADER_SIZE, totalLength) - buffer = buffer.subarray(totalLength) - - onFrame(type, payload) - } - } - - return { - feed(chunk: Buffer): void { - buffer = buffer.length === 0 ? chunk : Buffer.concat([buffer, chunk]) - parse() - }, - - reset(): void { - buffer = Buffer.alloc(0) - } - } -} diff --git a/src/main/daemon/types.ts b/src/main/daemon/types.ts index 865572ea0b1..1f067885467 100644 --- a/src/main/daemon/types.ts +++ b/src/main/daemon/types.ts @@ -383,23 +383,6 @@ export type DaemonSessionInfo = SessionInfo & { // existing importers keep one types entry point. export * from './daemon-stream-events' -// ─── Binary Frame Protocol (Daemon ↔ PTY Subprocess) ──────────────── -// -// 5-byte header: [type:1][length:4 big-endian] -// Followed by `length` bytes of payload. - -export const enum FrameType { - Data = 0x01, - Resize = 0x02, - Exit = 0x03, - Error = 0x04, - Kill = 0x05, - Signal = 0x06 -} - -export const FRAME_HEADER_SIZE = 5 -export const FRAME_MAX_PAYLOAD = 1024 * 1024 // 1MB - // ─── Notify prefix ────────────────────────────────────────────────── // Requests with IDs starting with this prefix are fire-and-forget: // the daemon processes them but does not send a response.