Files
nextcraft/apps/web/lib/sse.ts
T
CIAgent 88a1dab810 docs(milestone): complete v0.2-ai-tutor-architecture
---ci---
phase: 7
milestone: v0.2
status: complete
requirements:
  covered: [REQ-2-001, REQ-2-002, REQ-2-003, REQ-2-004, REQ-2-005, REQ-2-006, REQ-2-007, REQ-2-008, REQ-2-009, REQ-2-010, REQ-2-011, REQ-2-012]
  partial: []
---/ci---

Milestone v0.2 (ai-tutor-architecture) merged to main.

Escalation record (audit remediation, durable): P1 executor
delegation failed twice (empty subagent results, zero files
created); auto-resolved at full autonomy to inline execution with
identical plan fidelity (commit 3271373, reflog-only after phase
branch squash-delete).
2026-09-11 17:34:50 +00:00

28 lines
1.0 KiB
TypeScript

/**
* Shared SSE parsing for the AI service streams.
*
* Normalizes CRLF (sse-starlette's wire terminator is \r\n) to LF, then
* splits frames on blank lines. Multiple `data:` lines within one frame
* are joined with \n per the SSE spec. Frames with no data lines
* (keep-alive `: ping` comments) are ignored (G-1).
*/
export function parseSseEvents(buffer: string): { events: string[]; rest: string } {
const normalized = buffer.replace(/\r\n/g, '\n');
const events: string[] = [];
const separatorIndex = normalized.lastIndexOf('\n\n');
if (separatorIndex === -1) return { events, rest: normalized };
const complete = normalized.slice(0, separatorIndex);
const rest = normalized.slice(separatorIndex + 2);
for (const frame of complete.split('\n\n')) {
const dataLines = frame
.split('\n')
.filter((line) => line.startsWith('data:'))
.map((line) => line.slice(5).trimStart());
if (dataLines.length === 0) continue; // ping/comment frame — ignore (G-1)
events.push(dataLines.join('\n'));
}
return { events, rest };
}