assistant-stream
Version:
Streaming utilities for AI assistants
180 lines (179 loc) • 5.82 kB
JavaScript
// 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