UNPKG

assistant-stream

Version:

Streaming utilities for AI assistants

180 lines (179 loc) • 5.82 kB
// src/core/utils/stream/SSEEventDecoder.ts const DEFAULT_MAX_LINE_LENGTH = 16777216; const DEFAULT_MAX_EVENT_LENGTH = 16777216; var SSEEventDecoderError = class extends Error { code; constructor(code, message) { super(message); this.name = "SSEEventDecoderError"; this.code = code; } }; const readLimit = (value, fallback, name) => { const limit = value ?? fallback; if (!Number.isSafeInteger(limit) || limit <= 0) throw new SSEEventDecoderError("invalid-limit", `${name} must be a positive safe integer`); return limit; }; var SSEEventDecoderImpl = class { lineChunks = []; lineLength = 0; linePreview = ""; dataLines = []; eventLength = 0; eventName; lastEventId; retry; pendingLF = false; started = false; finished = false; stopAfter; trailing; maxLineLength; maxEventLength; constructor(options, stopAfter) { this.stopAfter = stopAfter; this.trailing = options?.trailing ?? "drop"; this.maxLineLength = readLimit(options?.maxLineLength, DEFAULT_MAX_LINE_LENGTH, "maxLineLength"); this.maxEventLength = readLimit(options?.maxEventLength, DEFAULT_MAX_EVENT_LENGTH, "maxEventLength"); } push(text) { const events = []; if (this.finished || text === "") return events; if (!this.started) { this.started = true; if (text.startsWith("")) text = text.slice(1); } if (this.pendingLF && text.startsWith("\n")) text = text.slice(1); this.pendingLF = text.endsWith("\r"); const delimiter = /\r\n|\r|\n/g; let lineStart = 0; for (let match = delimiter.exec(text); match !== null; match = delimiter.exec(text)) { const line = text.slice(lineStart, match.index); lineStart = match.index + match[0].length; let completeLine = line; if (this.lineChunks.length > 0) { this.appendLineChunk(line); completeLine = this.lineChunks.join(""); this.resetLine(); } else if (line.length > this.maxLineLength) { this.resetFrame(); this.resetLine(); throw new SSEEventDecoderError("line-too-long", `SSE line exceeds maxLineLength (${line.length} > ${this.maxLineLength})`); } const event = this.processLine(completeLine); if (event) { events.push(event); if (this.stopAfter?.(event)) { this.finished = true; this.pendingLF = false; return events; } } } const remainder = text.slice(lineStart); if (remainder !== "") this.appendLineChunk(remainder); return events; } flush() { if (this.finished || this.trailing === "drop") { this.resetFrame(); this.resetLine(); this.pendingLF = false; return null; } if (this.lineChunks.length > 0) this.processLine(this.lineChunks.join("")); this.resetLine(); this.pendingLF = false; return this.dispatchEvent(); } processLine(line) { if (line === "") return this.dispatchEvent(); if (line.startsWith(":")) return null; const separator = line.indexOf(":"); const field = separator === -1 ? line : line.slice(0, separator); let value = separator === -1 ? "" : line.slice(separator + 1); if (value.startsWith(" ")) value = value.slice(1); switch (field) { case "data": { const nextEventLength = this.eventLength + value.length + (this.dataLines.length > 0 ? 1 : 0); if (nextEventLength > this.maxEventLength) { this.resetFrame(); throw new SSEEventDecoderError("event-too-long", `SSE event exceeds maxEventLength (${nextEventLength} > ${this.maxEventLength})`); } this.eventLength = nextEventLength; this.dataLines.push(value); break; } case "event": this.eventName = value; break; case "id": if (!value.includes("\0")) this.lastEventId = value; break; case "retry": { const retry = Number(value); if (/^\d+$/.test(value) && Number.isSafeInteger(retry)) this.retry = retry; break; } } return null; } dispatchEvent() { if (this.dataLines.length === 0) { this.resetFrame(); return null; } const event = { data: this.dataLines.join("\n") }; if (this.eventName !== void 0) event.event = this.eventName; if (this.lastEventId !== void 0) event.id = this.lastEventId; if (this.retry !== void 0) event.retry = this.retry; this.resetFrame(); return event; } resetFrame() { this.dataLines = []; this.eventLength = 0; this.eventName = void 0; } appendLineChunk(chunk) { const nextLineLength = this.lineLength + chunk.length; if (nextLineLength > this.maxLineLength) { this.resetFrame(); this.resetLine(); throw new SSEEventDecoderError("line-too-long", `SSE line exceeds maxLineLength (${nextLineLength} > ${this.maxLineLength})`); } this.lineChunks.push(chunk); this.lineLength = nextLineLength; if (this.linePreview.length < 6) this.linePreview += chunk.slice(0, 6 - this.linePreview.length); if (this.linePreview.length >= 5 && this.linePreview.startsWith("data:")) { const leadingSpace = this.linePreview.length >= 6 && this.linePreview[5] === " " ? 1 : 0; const valueLength = this.lineLength - 5 - leadingSpace; const nextEventLength = this.eventLength + valueLength + (this.dataLines.length > 0 ? 1 : 0); if (nextEventLength > this.maxEventLength) { this.resetFrame(); this.resetLine(); throw new SSEEventDecoderError("event-too-long", `SSE event exceeds maxEventLength (${nextEventLength} > ${this.maxEventLength})`); } } } resetLine() { this.lineChunks = []; this.lineLength = 0; this.linePreview = ""; } }; const createSSEEventDecoder = (options, stopAfter) => new SSEEventDecoderImpl(options, stopAfter); var SSEEventDecoder = class { decoder; constructor(options) { this.decoder = new SSEEventDecoderImpl(options); } push(text) { return this.decoder.push(text); } flush() { return this.decoder.flush(); } }; export { SSEEventDecoder, SSEEventDecoderError, createSSEEventDecoder }; //# sourceMappingURL=SSEEventDecoder.js.map