运行时包管理器打包器测试运行器代码检查格式化指南参考博客反馈

使用 Bun 的服务器发送事件(SSE)

服务器发送事件允许你通过单个 HTTP 响应向浏览器推送文本事件流。客户端使用 EventSource 消费这些事件。

要在 Bun 中实现 SSE 端点,请返回一个正文为流式源的 Response,并将 Content-Type 标头设置为 text/event-stream。

使用异步生成器

在 Bun 中,new Response 可直接接受异步生成器函数。异步生成器通常是编写 SSE 端点最简单的方式。每次 yield 都会向客户端刷新一个数据块。如果客户端断开连接,生成器的 finally 块会运行,以便你进行清理。

server.ts
Bun.serve({
  port: 3000,
  routes: {
    "/events": (req, server) => {
      // SSE 流事件间通常比较安静。
      // 默认情况下,Bun.serve 会在 10 秒无活动后关闭连接。
      // 禁用该请求的空闲超时,以保持流无限期打开。
      server.timeout(req, 0);

      return new Response(
        async function* () {
          yield `data: connected at ${Date.now()}\n\n`;

          // 每 5 秒发送一个 tick,直到客户端断开连接。
          // 客户端断开时,生成器被返回(取消),此循环自动停止。
          while (true) {
            await Bun.sleep(5000);
            yield `data: tick ${Date.now()}\n\n`;
          }
        },
        {
          headers: {
            "Content-Type": "text/event-stream",
            "Cache-Control": "no-cache",
          },
        },
      );
    },
  },
});

使用 ReadableStream

如果你的事件来自回调(消息代理、计时器、外部推送),而不是线性的 await 流程,那么 ReadableStream 通常更合适。当客户端断开连接时,Bun 会自动调用流的 cancel() 方法,因此你可以释放在 start() 中设置的所有资源。

server.ts
Bun.serve({
  port: 3000,
  routes: {
    "/events": (req, server) => {
      server.timeout(req, 0);

      let timer: Timer;
      const stream = new ReadableStream({
        start(controller) {
          controller.enqueue(`data: connected at ${Date.now()}\n\n`);

          timer = setInterval(() => {
            controller.enqueue(`data: tick ${Date.now()}\n\n`);
          }, 5000);
        },
        cancel() {
          // 当客户端断开连接时自动调用。
          clearInterval(timer);
        },
      });

      return new Response(stream, {
        headers: {
          "Content-Type": "text/event-stream",
          "Cache-Control": "no-cache",
        },
      });
    },
  },
});