// Minimal, dependency-free SSE frame parser for the companion SDK. The browser's // EventSource can't set an Authorization header, so the SDK consumes the stream // over fetch() instead and parses frames itself — this is that parser, kept pure // so it unit-tests in isolation. // // An SSE frame is a block of `field: value` lines terminated by a blank line. // Comment lines (starting ":") are keep-alives and ignored. A chunk off the wire // may end mid-frame, so the parser returns the leftover for the next call. export interface SseEvent { /** The `event:` field; defaults to "message" per the SSE spec. */ event: string; /** The joined `data:` field(s). */ data: string; } /** Parse all COMPLETE frames out of an accumulated buffer. Returns the parsed * events plus the unparsed remainder (an incomplete trailing frame). */ export function parseSse(buffer: string): { events: SseEvent[]; rest: string } { const events: SseEvent[] = []; let working = buffer.replace(/\r\n/g, '\n'); let sep = working.indexOf('\n\n'); while (sep !== -1) { const block = working.slice(0, sep); working = working.slice(sep + 2); let event = 'message'; const dataLines: string[] = []; for (const line of block.split('\n')) { if (!line || line.startsWith(':')) continue; // blank or comment (keep-alive) const ci = line.indexOf(':'); const field = ci === -1 ? line : line.slice(0, ci); let value = ci === -1 ? '' : line.slice(ci + 1); if (value.startsWith(' ')) value = value.slice(1); // single leading space stripped if (field === 'event') event = value; else if (field === 'data') dataLines.push(value); } if (dataLines.length) events.push({ event, data: dataLines.join('\n') }); sep = working.indexOf('\n\n'); } return { events, rest: working }; }