sse.ts 2.1 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758
  1. // SSE:补发 seq > lastId 的事件,然后实时推送(docs/design/04-api.md §5)
  2. import type { FastifyReply, FastifyRequest } from 'fastify'
  3. import type { App } from '../app'
  4. const PING_MS = 15_000
  5. /** 正在推送的 SSE 连接:服务关闭时主动结束(否则 server.close() 会一直等它们断开),客户端按 retry 自动重连到新进程 */
  6. const openStreams = new Set<() => void>()
  7. export function closeAllStreams() {
  8. for (const end of [...openStreams]) end()
  9. }
  10. export function streamSessionEvents(app: App, req: FastifyRequest, reply: FastifyReply, sessionId: string, lastId: number) {
  11. streamEvents(req, reply, lastId, after => app.events.list(sessionId, after), fn => app.events.subscribe(sessionId, fn))
  12. }
  13. /** 通用的 SSE 推送:会话事件与工作组事件共用(都带递增的 seq) */
  14. export function streamEvents<E extends { seq: number; type: string }>(
  15. req: FastifyRequest, reply: FastifyReply, lastId: number,
  16. list: (after: number) => E[], subscribe: (fn: (e: E) => void) => () => void,
  17. ) {
  18. reply.hijack()
  19. const res = reply.raw
  20. // hijack 后 reply.header 不再生效:带上前面钩子设置的头(CORS 等)
  21. res.writeHead(200, {
  22. ...(reply.getHeaders() as Record<string, string>),
  23. 'Content-Type': 'text/event-stream; charset=utf-8',
  24. 'Cache-Control': 'no-cache, no-transform',
  25. Connection: 'keep-alive',
  26. 'X-Accel-Buffering': 'no',
  27. })
  28. res.write('retry: 2000\n\n')
  29. let sent = lastId
  30. const write = (e: E) => {
  31. if (e.seq <= sent) return
  32. sent = e.seq
  33. res.write(`id: ${e.seq}\nevent: ${e.type}\ndata: ${JSON.stringify(e)}\n\n`)
  34. }
  35. // 先订阅再补发,补发与实时之间的事件靠 seq 去重
  36. const pending: E[] = []
  37. let replaying = true
  38. const unsubscribe = subscribe(e => (replaying ? pending.push(e) : write(e)))
  39. for (const e of list(lastId)) write(e)
  40. replaying = false
  41. for (const e of pending) write(e)
  42. const ping = setInterval(() => res.write(': ping\n\n'), PING_MS)
  43. const end = () => res.end()
  44. openStreams.add(end)
  45. req.raw.on('close', () => {
  46. clearInterval(ping)
  47. unsubscribe()
  48. openStreams.delete(end)
  49. })
  50. }