实时事件桥:网关到 SSE
这是 discord 服务内部的一条单向管道,把 Discord 服务器里发生的事,实时地送到浏览器。Discord 的网关推来一个事件,片刻之后前端的 EventSource 就收到一个描述它的小 JSON 对象。这条路上没有回流——它是一个只读的推送流。
管道有四段:网关客户端 → 进程内总线 → SSE hub → HTTP 流路由。
网关客户端
BotClient 包着单个 discord.js Client。它用一个 bot token 登录,并声明所需的网关 intents——guilds、消息、消息正文、成员、在线状态、reactions、typing、语音状态。在原始的 discord.js 回调之上,它自己维护每类事件的 handler 列表,并为消息的建/改/删、reaction 的加/removed、频道更新、成员加/退、在线状态更新、thread 建/改、typing 开始、以及频道 pin 更新,各注册一个网关监听器。
每个网关回调在抵达 handler 之前都会先被规范化:pin 更新的监听器会丢掉非文本频道,typing 监听器把 Discord 的 typing 对象压成 { channelId, user, startedTimestamp },其余的以整洁的类型透传。所有 handler 都走一个共用的 runHandlers 守卫,它同时兜住同步抛错和被 reject 的 promise 并记日志——一个行为异常的 handler 绝不会拖垮网关连接。
事件契约
在管道后半段流动的,不是 discord.js 对象,而是一个朴素、可序列化的 DiscordEvent——一个按 type 字符串区分的联合类型,共十四个变体:message.created / message.updated / message.deleted、reaction.added / reaction.removed、pin.added / pin.removed、typing.started、presence.updated、channel.updated、member.added / member.removed、thread.created / thread.updated。多数变体带一个 channelId;消息类事件带一个完整的 ChatMessage。这个联合类型,就是前端唯一需要理解的传输格式。
进程内总线
MessageBus 刻意做得极小:一个装监听函数的 Set,一个逐个调用它们的 publish(兜住并记录任何监听器错误),以及一个返回退订闭包的 subscribe。没有 Redis,没有跨 pod 广播。原因写在代码注释里,而且是硬约束、不是偷懒:Discord 网关对每个 bot token 只允许一条 WebSocket,所以这个服务刻意是单 pod 的。既然永远只有一个进程持有网关连接,进程内总线就够用了——分布式 pubsub 反而是在解一个根本不会出现的问题。
SSE hub
SseHub 在启动时订阅总线,并持有已连接浏览器的集合。客户端连上来时,会拿到一个 nanoid 生成的 id、SSE 响应头(text/event-stream、no-cache、keep-alive,以及 X-Accel-Buffering: no,让代理不缓冲这条流),和一行开场注释。之后每 25 秒写一行心跳注释,防止连接或中间层把空闲的流判超时;客户端关闭时,心跳被清掉、该客户端被移除。
按频道过滤就发生在这里。每个客户端可选地带一组它关心的频道 id;null 表示「全部频道」。在 fanOut 时,hub 先推导事件的目标频道(取消息的 channelId,或事件上的 channelId 字段,或者对 presence、成员变动这类服务器级事件取空),把事件序列化成一个 data: 帧,只写给过滤条件匹配的客户端——而没有频道归属的事件发给所有人。写失败的客户端会当场被剔除。
流路由
单个 Fastify 路由 GET /discord/v1/stream 是这条管道对外的口子。它读取可选的、逗号分隔的 channelIds 查询参数,拆成过滤集合,把 reply 交给 hub 的 connect,然后返回一个永不 resolve 的 promise——正是它把 HTTP 请求一直挂着。结束响应的是 hub 而不是路由,在浏览器断开时收尾。
为什么长这样
三道缝——网关客户端、总线、hub——各干一件事,彼此不知道对方的内部。网关客户端只懂把 Discord 回调变成有类型的 handler;总线只懂把事件递给订阅者;hub 只懂让浏览器保持连接并过滤各自该看到的东西。单 pod 这个事实不是规模上的偶然,而是 Discord 网关本身的性质,整套设计都建立在它之上。