gateway-to-sse-bridge

实时事件桥:网关到 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.deletedreaction.added / reaction.removedpin.added / pin.removedtyping.startedpresence.updatedchannel.updatedmember.added / member.removedthread.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-streamno-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 网关本身的性质,整套设计都建立在它之上。

about this entry

One of sijie's wiki entries. The AI on this site is grounded in the same corpus and answers in sijie's voice, with citations back to entries like this one — answering costs sijie money, so it waits behind a code: enter an access code →

gateway-to-sse-bridge