You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
129 lines
3.3 KiB
129 lines
3.3 KiB
import log4js from "logger";
|
|
|
|
const logger = log4js.getLogger("APP");
|
|
|
|
const BUFFER_TTL_MS = 5 * 60 * 1000;
|
|
const CLEANUP_INTERVAL_MS = 60 * 1000;
|
|
|
|
export interface StreamBuffer {
|
|
chunks: Uint8Array[];
|
|
done: boolean;
|
|
createdAt: number;
|
|
doneAt: number | null;
|
|
modelId: number | null;
|
|
userMessageId: string | null;
|
|
subscribers: Array<(chunk: Uint8Array) => void>;
|
|
doneSubscribers: Array<() => void>;
|
|
}
|
|
|
|
const buffers = new Map<string, StreamBuffer>();
|
|
|
|
let cleanupTimer: ReturnType<typeof setInterval> | null = null;
|
|
|
|
function ensureCleanupTimer() {
|
|
if (cleanupTimer) return;
|
|
cleanupTimer = setInterval(() => {
|
|
const now = Date.now();
|
|
for (const [sid, buf] of buffers) {
|
|
const expireAt = buf.doneAt ?? buf.createdAt + BUFFER_TTL_MS;
|
|
if (now > expireAt) {
|
|
buffers.delete(sid);
|
|
logger.info("[STREAM-BUFFER] expired and removed sessionId=%s", sid);
|
|
}
|
|
}
|
|
if (buffers.size === 0 && cleanupTimer) {
|
|
clearInterval(cleanupTimer);
|
|
cleanupTimer = null;
|
|
}
|
|
}, CLEANUP_INTERVAL_MS);
|
|
cleanupTimer.unref?.();
|
|
}
|
|
|
|
export function createStreamBuffer(
|
|
sessionId: string,
|
|
meta: { modelId?: number | null; userMessageId?: string | null },
|
|
): StreamBuffer {
|
|
const buf: StreamBuffer = {
|
|
chunks: [],
|
|
done: false,
|
|
createdAt: Date.now(),
|
|
doneAt: null,
|
|
modelId: meta.modelId ?? null,
|
|
userMessageId: meta.userMessageId ?? null,
|
|
subscribers: [],
|
|
doneSubscribers: [],
|
|
};
|
|
buffers.set(sessionId, buf);
|
|
ensureCleanupTimer();
|
|
logger.info("[STREAM-BUFFER] created sessionId=%s", sessionId);
|
|
return buf;
|
|
}
|
|
|
|
export function appendChunk(sessionId: string, chunk: Uint8Array): void {
|
|
const buf = buffers.get(sessionId);
|
|
if (!buf || buf.done) return;
|
|
buf.chunks.push(chunk);
|
|
for (const sub of buf.subscribers) {
|
|
try {
|
|
sub(chunk);
|
|
} catch (e) {
|
|
logger.error("[STREAM-BUFFER] subscriber error: %s", e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
}
|
|
|
|
export function markBufferDone(sessionId: string): void {
|
|
const buf = buffers.get(sessionId);
|
|
if (!buf) return;
|
|
buf.done = true;
|
|
buf.doneAt = Date.now();
|
|
const doneSubs = buf.doneSubscribers;
|
|
buf.doneSubscribers = [];
|
|
buf.subscribers = [];
|
|
for (const sub of doneSubs) {
|
|
try {
|
|
sub();
|
|
} catch (e) {
|
|
logger.error("[STREAM-BUFFER] done subscriber error: %s", e instanceof Error ? e.message : String(e));
|
|
}
|
|
}
|
|
logger.info("[STREAM-BUFFER] done sessionId=%s chunks=%d", sessionId, buf.chunks.length);
|
|
}
|
|
|
|
export function getStreamBuffer(sessionId: string): StreamBuffer | undefined {
|
|
return buffers.get(sessionId);
|
|
}
|
|
|
|
export function removeStreamBuffer(sessionId: string): void {
|
|
buffers.delete(sessionId);
|
|
}
|
|
|
|
export function hasActiveStream(sessionId: string): boolean {
|
|
const buf = buffers.get(sessionId);
|
|
return !!buf && !buf.done;
|
|
}
|
|
|
|
export function subscribeToBuffer(
|
|
sessionId: string,
|
|
onChunk: (chunk: Uint8Array) => void,
|
|
onDone: () => void,
|
|
): () => void {
|
|
const buf = buffers.get(sessionId);
|
|
if (!buf) {
|
|
onDone();
|
|
return () => {};
|
|
}
|
|
|
|
if (buf.done) {
|
|
onDone();
|
|
return () => {};
|
|
}
|
|
|
|
buf.subscribers.push(onChunk);
|
|
buf.doneSubscribers.push(onDone);
|
|
|
|
return () => {
|
|
buf.subscribers = buf.subscribers.filter((s) => s !== onChunk);
|
|
buf.doneSubscribers = buf.doneSubscribers.filter((s) => s !== onDone);
|
|
};
|
|
}
|
|
|