☰
jev-trader 实时数据流架构:从 SSE 服务端到 Next.js 前端的 3.3 次/秒事件推送全解析
2026/10/1 19:48:36 网站建设 项目流程

jev-trader 实时数据流架构:从 SSE 服务端到 Next.js 前端的 3.3 次/秒事件推送全解析

【免费下载链接】jev-traderOne AI trade decision every Monad block. Jev on Kuru MON-USDC.项目地址: https://gitcode.com/gh_mirrors/je/jev-trader

jev-trader 是一个 AI 链上交易机器人项目:AI 模型每 300 ms(即每秒约 3.3 次)在 Monad 链上做出一次买入或卖出决策,并真实挂出 post-only 限价单,同时通过SSE(Server-Sent Events)服务端推送把每一块区块的决策、报价、成交实时流式推送到Next.js 实时交易仪表盘前端。本文带你完整拆解这条从Bun.serve()SSE 服务端到浏览器EventSource的数据流架构,看懂它如何在 3.3 次/秒的高频节奏下做到低延迟、可断线重连、且前端不卡顿。

一、项目速览:每 300ms 一次 AI 交易决策

先建立整体认知。jev-trader 的核心卖点可以用一句话概括(见 SPEC.md):

每 300 ms 的 Monad 区块,AI 只做一件事:回答"买还是卖"。

系统分为三层,正好对应本文要拆解的数据流:

层次模块职责
数据源src/chain.tsWebSocket 订阅新区块 + 轮询兜底,只取最新块
决策与广播src/trader.ts每个区块读盘口→AI 决策→下单,产出BlockEvent
SSE 服务端src/server.tsBun.serve()提供/events事件流,广播给所有客户端
前端消费web/src/lib/useFeed.tsEventSource订阅 + 状态管理,驱动整个仪表盘

入口在 src/index.ts:先启动 SSE 服务端拿到server句柄,再创建Trader,每出一个新块就调用trader.onBlock,事件通过server.broadcast(e)发出去。这就是 3.3 次/秒事件的"生产端"。

二、SSE 服务端:为什么选 SSE 而不是 WebSocket?

打开 src/server.ts,你会发现整个服务端不到 40 行,却非常干净。关键设计有 4 个:

1. 一个Set管理所有订阅者

const clients = new Set<ReadableStreamDefaultController<Uint8Array>>();

每个连上/events的客户端就是一个ReadableStream控制器。广播时遍历集合逐个enqueue,写失败(连接已断)就从集合中删掉,天然实现了惰性清理,不需要额外的连接管理代码。

2. 标准 SSE 帧格式,浏览器原生可解析

event: block data: { "block": 105488269, "ts": 1789593630676, ... }

content-type: text/event-stream加上cache-control: no-cache,配合浏览器原生的EventSourceAPI,前端零依赖即可消费——这是相比 WebSocket 的第一个优势。

3. 事件单向 + 自动重连

交易仪表盘是典型的只读推送场景:服务端 → 前端单向流动,不需要双向通信。SSE 天然支持断线自动重连,且基于 HTTP,穿透代理/CDN 更容易。这也是它比 WebSocket 更适合"3.3 次/秒仪表盘"的原因。

4. 15 秒心跳保活

setInterval(() => clients.forEach((c) => send(c, "ping", Date.now())), 15_000);

每 15 秒发一次ping事件,前端用它判断"连接还活着",配合超时机制强制重连(见下文 useFeed.ts 的STALE_MS)。

广播接口:三种事件类型

src/server.ts 对外暴露三个广播函数,分别对应三种"事实":

  • broadcast(e)→block事件:每个区块的完整决策快照(3.3 次/秒)
  • broadcastQuote(block, quote)→quote事件:交易回执到达(上链成功/回滚),比下单晚 1-2 个区块
  • broadcastFill(block, fill)→fill事件:有 taker 吃掉我们的挂单,真实成交

加上连接时的snapshot(元信息 + 最近 1000 条历史),一共5 种事件类型,覆盖了前端渲染所需的全部状态变化。

三、每个事件携带什么:BlockEvent 全解析

事件的数据结构定义在 src/trader.ts,前端有一份完全对应的 wire 类型 web/src/lib/types.ts。每个block事件都是一个"自包含快照":

字段含义用途
block/ts区块号、时间戳去重键(前端按区块号排序)
mid/bestBid/bestAsk/spreadBps中间价、买一、卖一、点差价格走势图
decisionAI 的buy/sell、概率、耗时latencyMs、是否迟到late决策面板的"闪烁"
quote本块挂出的订单:价格、数量、txHash、status、撤单列表订单回执状态更新
fill本块发生的成交(可能为null)成交带(trade tape)
position当前仓位:方向、数量、浮动盈亏持仓展示
totals累计指标:块数、成交数、Gas 花费、P&L统计卡片行

这里有一个值得学习的设计:fire-and-forget 的异步事实合并。交易下单是"发射后不管"的,回执要晚 1-2 个区块才到,所以block事件先带status: "sent"(意图),之后单独的quote事件再回填placed/reverted(详见 README.md)。前端不需要处理任何竞态——按区块号找到对应事件,替换quote字段即可(useFeed.ts 的quotecase)。

四、Next.js 前端:一个 Hook 消费整条数据流

前端在 web/ 目录下,Next.js App Router + TypeScript + CSS Modules。页面组装非常简单,见 web/src/app/page.tsx:

const feed = useFeed(API_URL); return ( <div className="card"> <Header meta={feed.meta} latest={feed.latest} connection={feed.connection} /> <StatsRow latest={feed.latest} avgLatencyMs={feed.avgLatencyMs} meta={feed.meta} /> <FlowChart events={feed.events} latest={feed.latest} /> <DecisionPanel latest={feed.latest} /> <Feed events={feed.events} /> </div> );

整个实时性都押在 web/src/lib/useFeed.ts 这一个 Hook 上,它做对了几件高频流场景的关键事。

1. EventSource + 命名事件监听

es = new EventSource(`${base}/events`); es.addEventListener("block", ...); // 追加新区块 es.addEventListener("snapshot", ...); // 首屏:元信息 + 历史 es.addEventListener("quote", ...); // 回执回填 es.addEventListener("fill", ...); // 成交回填 es.addEventListener("ping", ...); // 保活确认

连接建立瞬间收到snapshot(含最近 1000 条历史),页面无需任何 REST 补数据请求即可直接画出 5 分钟的价格曲线。这是 SSE 相比轮询体验最好的地方。

2. 指数退避重连:1s → 10s

useFeed.ts 中,断线后按1s × 2^n退避重试,上限 10 秒;重连成功后attempt归零。连接状态(connecting/live/reconnecting)直接暴露给 UI——Header 组件据此显示红色断线指示和"reconnecting"遮罩。

3. 45 秒静默超时,防止"假连接"

TCP 层面"没断"但数据流已死的场景很常见(代理吞包、服务端半死)。useFeed.ts 定义了STALE_MS = 45_000:任何事件(包括ping)到达都会重置计时器,45 秒无任何数据就主动触发重连。服务端 15 秒一次的ping恰好落在安全区间内。

4. 1000 条事件滑动窗口 + 去重

前端内存只保留最近CAP = 1000条事件(约 5 分钟 ≈ 1000 个区块)。重发的区块不是简单丢弃,而是原地替换旧数据(useFeed.ts),保证同一区块号永远只有一份最新状态。

五、性能关键:3.3 次/秒下如何做到"不卡"

SPEC 明确要求前端以 3.3 次/秒无限期更新且动画只能用 transform/opacity(SPEC.md)。useFeed.ts 里有两个很克制的性能优化:

  • O(1) 平均延迟计算:avgLatencyMs不每次遍历 1000 条事件求平均,而是维护累加器latSum/latCount,新增/替换/滑出窗口时增量更新。3.3 次/秒下每次渲染都省掉一次 O(n)。
  • 不可变更新 + 单次 slice:useReducer状态更新中只在真正需要改的那条事件上做对象替换,避免整表重渲染。

配合 CSS Modules 中pulse/breathe关键帧只动 transform 和 opacity(web/src/app/globals.css),页面即使挂机数小时也不会出现布局抖动——数字用等宽(tabular)字体是同一原因。

六、自己动手跑起来

# 后端(无 PRIVATE_KEY 时自动 dry-run:真实盘口、真实决策、模拟成交) bun install bun run start # 前端 cd web bun install bun run dev # http://localhost:3000

前端通过环境变量NEXT_PUBLIC_API_URL指向后端(web/README.md)。跑起来后打开浏览器 DevTools 的 Network 面板,能看到一条/events长连接持续接收block事件——这就是本文拆解过的整条数据流在你机器上的样子。

七、总结:一条值得借鉴的实时流架构

jev-trader 的数据流架构可以用五句话总结:

  1. 单向高频推送选 SSE:Bun.serve()+ReadableStream,一个Set管所有订阅者,惰性清理断连客户端;
  2. 事件自包含:每个block事件携带完整快照(盘口、决策、报价、仓位、累计指标),前端无需组合多个接口;
  3. 异步事实分离:下单意图(block)与上链回执(quote)、成交(fill)分事件推送,按区块号回填,彻底消除竞态;
  4. 前端单 Hook 消费:EventSource+ 指数退避重连 + 静默超时 + 1000 条滑动窗口 + 增量平均,一个useFeed驱动全站;
  5. 性能约束前置:3.3 次/秒的节奏在设计阶段(SPEC)就写死,O(1) 累加器和 transform-only 动画是必然结果。

这套"服务端 5 种事件 + 前端单一状态源"的模式,对任何高频实时仪表盘(行情、监控、直播统计)都有直接参考价值。

关键文件导航:SSE 服务端 src/server.ts · 事件类型 src/trader.ts · 前端 Hook web/src/lib/useFeed.ts · wire 类型 web/src/lib/types.ts · 页面组装 web/src/app/page.tsx

【免费下载链接】jev-traderOne AI trade decision every Monad block. Jev on Kuru MON-USDC.项目地址: https://gitcode.com/gh_mirrors/je/jev-trader

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询