refactor: 收口前端 SSE 传输层
This commit is contained in:
@@ -2,6 +2,7 @@ import type {
|
||||
VisualNovelRunSnapshot,
|
||||
VisualNovelRuntimeStreamEvent,
|
||||
} from '../../../packages/shared/src/contracts/visualNovel';
|
||||
import { readSseJsonStream } from '../sseStream';
|
||||
|
||||
type VisualNovelRuntimeSseOptions = {
|
||||
fallbackMessage: string;
|
||||
@@ -9,65 +10,6 @@ type VisualNovelRuntimeSseOptions = {
|
||||
onEvent?: (event: VisualNovelRuntimeStreamEvent) => void;
|
||||
};
|
||||
|
||||
function findSseEventBoundary(buffer: string) {
|
||||
const lfBoundary = buffer.indexOf('\n\n');
|
||||
const crlfBoundary = buffer.indexOf('\r\n\r\n');
|
||||
|
||||
if (lfBoundary === -1 && crlfBoundary === -1) {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (lfBoundary === -1) {
|
||||
return {
|
||||
index: crlfBoundary,
|
||||
length: 4,
|
||||
};
|
||||
}
|
||||
|
||||
if (crlfBoundary === -1 || lfBoundary < crlfBoundary) {
|
||||
return {
|
||||
index: lfBoundary,
|
||||
length: 2,
|
||||
};
|
||||
}
|
||||
|
||||
return {
|
||||
index: crlfBoundary,
|
||||
length: 4,
|
||||
};
|
||||
}
|
||||
|
||||
function parseSseEventBlock(eventBlock: string) {
|
||||
let eventName = 'message';
|
||||
const dataLines: string[] = [];
|
||||
|
||||
for (const rawLine of eventBlock.split(/\r?\n/u)) {
|
||||
const line = rawLine.trim();
|
||||
|
||||
if (line.startsWith('event:')) {
|
||||
eventName = line.slice(6).trim() || 'message';
|
||||
continue;
|
||||
}
|
||||
|
||||
if (line.startsWith('data:')) {
|
||||
dataLines.push(line.slice(5).trim());
|
||||
}
|
||||
}
|
||||
|
||||
return {
|
||||
eventName,
|
||||
data: dataLines.join('\n'),
|
||||
};
|
||||
}
|
||||
|
||||
function parseJsonObject(data: string) {
|
||||
try {
|
||||
return JSON.parse(data) as Record<string, unknown>;
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
function normalizeVisualNovelRuntimeEvent(
|
||||
eventName: string,
|
||||
parsed: Record<string, unknown>,
|
||||
@@ -115,59 +57,19 @@ export async function readVisualNovelRuntimeRunFromSse(
|
||||
response: Response,
|
||||
options: VisualNovelRuntimeSseOptions,
|
||||
) {
|
||||
const streamBody = response.body;
|
||||
if (!streamBody) {
|
||||
throw new Error('streaming response body is unavailable');
|
||||
}
|
||||
|
||||
const reader = streamBody.getReader();
|
||||
const decoder = new TextDecoder('utf-8');
|
||||
let buffer = '';
|
||||
let finalRun: VisualNovelRunSnapshot | null = null;
|
||||
|
||||
const consumeBuffer = () => {
|
||||
for (;;) {
|
||||
const boundary = findSseEventBoundary(buffer);
|
||||
if (!boundary) {
|
||||
break;
|
||||
}
|
||||
|
||||
const eventBlock = buffer.slice(0, boundary.index);
|
||||
buffer = buffer.slice(boundary.index + boundary.length);
|
||||
const { eventName, data } = parseSseEventBlock(eventBlock);
|
||||
if (!data) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const parsed = parseJsonObject(data);
|
||||
if (!parsed) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const event = normalizeVisualNovelRuntimeEvent(eventName, parsed);
|
||||
if (!event) {
|
||||
continue;
|
||||
}
|
||||
|
||||
const nextRun = handleVisualNovelRuntimeEvent(event, options);
|
||||
if (nextRun) {
|
||||
finalRun = nextRun;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
for (;;) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) {
|
||||
break;
|
||||
await readSseJsonStream(response, ({ eventName, parsed }) => {
|
||||
const event = normalizeVisualNovelRuntimeEvent(eventName, parsed);
|
||||
if (!event) {
|
||||
return;
|
||||
}
|
||||
|
||||
buffer += decoder.decode(value, { stream: true });
|
||||
consumeBuffer();
|
||||
}
|
||||
|
||||
buffer += decoder.decode();
|
||||
consumeBuffer();
|
||||
const nextRun = handleVisualNovelRuntimeEvent(event, options);
|
||||
if (nextRun) {
|
||||
finalRun = nextRun;
|
||||
}
|
||||
});
|
||||
|
||||
if (!finalRun) {
|
||||
throw new Error(options.incompleteMessage);
|
||||
|
||||
Reference in New Issue
Block a user