autosnippet
Version:
Extract code patterns into a knowledge base for AI coding assistants
97 lines (96 loc) • 3.51 kB
JavaScript
/**
* SSE Session Manager — 基于 EventSource 的流式会话管理
*
* 架构:
* POST /chat/stream → 创建 session + 后台执行 AgentRuntime → 返回 { sessionId }
* GET /chat/events/:sessionId → EventSource 端点, 回放缓冲事件 + 实时推送
*
* 为什么不用 fetch + ReadableStream:
* Chrome/Safari 的 fetch() streaming 会缓冲初始响应体(~1-4KB),导致小体积
* SSE 事件滞留在缓冲区中不被交付给 ReadableStream reader。
* 原生 EventSource API 是浏览器专门为 SSE 优化的消费者,不受此限制。
*
* @module lib/http/utils/sse-sessions
*/
import { EventEmitter } from 'node:events';
const _sessions = new Map();
/** Session 自动清理 TTL (5 分钟) */
const SESSION_TTL = 5 * 60 * 1000;
/** 完成后保留时间 (60 秒, 供客户端重连回放) */
const COMPLETED_KEEP = 60 * 1000;
/**
* 创建一个 stream session
*
* @param scene 场景标识
*/
export function createStreamSession(scene) {
const sessionId = `ss_${Date.now().toString(36)}_${Math.random().toString(36).slice(2, 8)}`;
const emitter = new EventEmitter();
emitter.setMaxListeners(20);
const session = {
sessionId,
scene,
/** 事件缓冲区(供 EventSource 连接后回放) */
buffer: [],
/** 会话是否已结束 */
completed: false,
createdAt: Date.now(),
/**
* 缓冲 + 广播一个事件
* @param event 必须包含 type 字段
*/
send(event) {
const payload = { ...event, ts: event.ts || Date.now() };
session.buffer.push(payload);
emitter.emit('event', payload);
},
/** 标记会话完成,发送 stream:done */
end(donePayload = {}) {
if (session.completed) {
return;
}
const payload = { type: 'stream:done', ts: Date.now(), ...donePayload };
session.buffer.push(payload);
emitter.emit('event', payload);
session.completed = true;
// 完成后保留一段时间供客户端重连
const keepTimer = setTimeout(() => _sessions.delete(sessionId), COMPLETED_KEEP);
if (keepTimer.unref) {
keepTimer.unref();
}
},
/** 标记会话错误,发送 stream:error */
error(message, code) {
if (session.completed) {
return;
}
const payload = { type: 'stream:error', ts: Date.now(), message, code };
session.buffer.push(payload);
emitter.emit('event', payload);
session.completed = true;
const keepTimer = setTimeout(() => _sessions.delete(sessionId), COMPLETED_KEEP);
if (keepTimer.unref) {
keepTimer.unref();
}
},
/**
* 订阅实时事件
* @returns unsubscribe 函数
*/
on(handler) {
emitter.on('event', handler);
return () => emitter.removeListener('event', handler);
},
};
_sessions.set(sessionId, session);
// 硬性 TTL: 无论是否完成,5 分钟后强制清理
const ttlTimer = setTimeout(() => _sessions.delete(sessionId), SESSION_TTL);
if (ttlTimer.unref) {
ttlTimer.unref();
}
return session;
}
/** 获取已有的 session */
export function getStreamSession(sessionId) {
return _sessions.get(sessionId);
}