← [ BACK_TO_NOTES ]

TanStack Query 与 SSE 流式数据集成

AI 对话中将 TanStack Query 的 durable server-state projection 与 SSE/Runtime 的 transient projection 分离,并通过 durable event replay、精确 cache patch 与 invalidation 收敛。

#synthesis #growing #frontend #react

[!info] related notes

TanStack Query 与 SSE 流式数据集成

范围

AI 对话页面通常同时存在:

TanStack Query
→ 已持久化/可重新获取的 server read-model projection

Streaming Runtime
→ 当前 run 的低延迟 transient projection

本文关心的是两者怎样协同而不抢 ownership,并在断线、重放、Mutation 和终态时收敛到服务器事实。

最重要的修正:流式期间前端也不是 Durable Truth

旧的简化说法:

流中以前端为准
流结束后以后端为准

不够准确。

更正确的模型是:

Server durable state / Runtime Event Log
= authority

Live SSE / replay
= delivery mechanisms

ActiveTurn
= current transient projection

TanStack Query
= durable server read-model 的 client cache/projection

即使流正在进行,前端也只是根据事件维护即时 projection;业务 authority 并没有转移到浏览器。

为什么 Streaming Runtime 不应该每个 token 都写 Query Cache

当前生成中的 text delta 具有:

  • 高频;
  • 短生命周期;
  • 尚未最终收敛;
  • 可能需要 replay/dedup;
  • 会和历史 durable messages 有不同生命周期。

因此更自然的是:

message.text.delta
→ ActiveTurn reducer
→ streaming UI

而不是:

每个 token
→ setQueryData
→ durable-history query cache

这样可以避免频繁 cache 写、历史与 current-run state 混杂,以及 replay 时难以判断哪一层负责去重。

当前更完整的数据路径

Agent / Runtime
      ↓
Durable Runtime Event Log
      ├────────→ Live SSE
      │
      └────────→ after_seq Replay
                       ↓
               validated StreamEvent
                       ↓
                  same reducer
                       ↓
                ActiveTurnState
                       ↓
                streaming UI

Run terminal / durable state changed
      ↓
patch or invalidate relevant TanStack Query entries
      ↓
fetch/reconcile durable read model

关键点是:Live 和 Replay 应尽量进入同一套 validation/reducer semantics。 Transport 来源不同,不应该产生不同 UI truth。

Query 与 Runtime 的 ownership 边界

TanStack Query 适合

conversation list
persisted thread/messages
HealthWorkspace
historical diagnosis/treatment/read models

这些值的客户端副本可以被 refetch、invalidate 和 cache。

Streaming Runtime 适合

current text delta
current tool lifecycle
current citation/red flag
current pending interaction
current run status projection

它回答“这一轮此刻发生到哪里”,终态后应被 durable read model 接管并最终 reset/reconcile。

SSE 事件何时可以 setQueryData

并不是“SSE 事件都不该碰 Query Cache”。如果一个事件携带了足够稳定、明确的 server fact,可以选择精确 patch cache,例如:

title.generated
→ 已知 authoritative title
→ patch list/detail projection

但条件仍然是:

客户端是否有足够证据构造正确的新 projection?

如果一个 event 只是部分变化,而后端还会计算其他字段,则更稳的是:

apply current-run projection
→ invalidate affected durable query
→ refetch full read model

见 TanStack Query 缓存失效模式。

cancelQueries + setQueryData 是一个模式,不是系统总架构

旧实现/一些场景会采用:

cancel in-flight query
→ setQueryData with known stream update

它可以避免旧 fetch 返回覆盖刚写的 projection,尤其适合乐观更新或明确 cache patch。

但不要将其提升为通用原则:

Streaming active
→ Query must always be disabled
→ every SSE event must cancel query
→ every event must write cache

更重要的是保持:

stable ownership
+ correct query identity
+ durable recovery
+ idempotent projection
+ final reconciliation

断线时为什么不能靠 Query Cache 补当前 Run

TanStack Query 的 persisted-thread/read-model query 未必包含当前 run 所有高频 runtime event。SSE 连接断线后,如果只 refetch普通页面数据,可能看不到尚未 finalise 的:

tool lifecycle
interaction.required
partial delta
runtime terminal transition

因此 production Agent streaming 需要独立的 durable runtime recovery:

Live consumed seq 1..5
network drop
server continues 6..10

client remembers cursor=5
→ GET events?after_seq=5
→ replay 6..10

见 Durable SSE Recovery with after_seq。

Replay 与 Query Cache Update 都需要幂等边界

恢复可能出现 overlap:

live received 1..5
replay returns 4..8
live reconnect also receives 7..10

如果每个事件直接 append Query data,会很容易重复。

更合理:

(run_id, seq) guard
+ keyed reducer/upsert
+ declared effects
+ cache patches designed to converge

BodySense 当前 ActiveTurn reducer 用 run-aware sequence guard,Live/Replay 都通过同一事件语义投影。

Terminal 后如何与 Query 收敛

终态通常应形成:

run.completed / run.failed / run.cancelled / stream.done
→ finish current runtime projection
→ update/invalidate affected durable queries
→ refetch/reconcile authoritative read model
→ reset transient ActiveTurn when safe

这里不能把“socket EOF”当成终态。网络断开只说明 transport 不可用,不证明 run 已结束。

Query Cache 竞态仍然存在

例如:

Q1 starts fetching old thread
stream event patches cache
Q1 old response later arrives

如果架构选择直接 patch Query Cache,就必须考虑旧 fetch 覆盖问题,可以通过:

  • 取消/避免冲突的 in-flight query;
  • 不在 stream 中直接写该 cache,而只维护 ActiveTurn;
  • 使用 revision/version 证据;
  • terminal 时统一 refetch;
  • 设计精确 cache patch 与 invalidation。

重点是根据 ownership 和 evidence 选策略,而不是机械调用某个 API。

BodySense 当前心智模型

POST consultation
→ fetch + ReadableStream consumes SSE
→ parse/validate StreamEvent
→ ActiveTurn reducer
→ low-latency UI

同时:
Runtime Event Log persists public events
→ after_seq recovery
→ same handlers/reducer

server-state projections
→ TanStack Query

terminal/domain changes
→ precise setQueryData and/or invalidate
→ reconcile server truth

对应的主要代码边界:

apps/web/src/features/consultation/hooks/useSSEProcessor.ts
apps/web/src/features/consultation/runtime/activeTurnReducer.ts
apps/web/src/features/consultation/runtime/durableRunRecovery.ts
apps/web/src/features/consultation/hooks/useThreadProjectionActions.ts

常见误区

误区更准确的理解
流中“以前端为准”前端只是 live projection,authority 仍在服务器
每个 token 都写 Query Cache高频 current-run state 更适合 Runtime projection
SSE 断了就 refetch thread 即可active run 可能需要 durable event catch-up
setQueryData 就是服务器更新它只更新 client cache
Socket EOF = run completedtransport lifetime ≠ run lifetime
Live 和 Replay 可以各写一套 UI 逻辑同一 event history 应收敛到同一 reducer/projection

最短记忆

TanStack Query
= durable server read model 的客户端 cache

ActiveTurn
= current run 的 transient projection

SSE
= low-latency transport

Runtime Event Log
= disconnect 后的 durable recovery source

最终原则:
transport 可以断,cache 可以 stale,
但 client projection 必须重新收敛到 server truth。
创建于 2026/7/2 更新于 2026/9/16
Digital Biome
Access

键盘导航

j / k / ↑ / ↓
选择对象
Enter / o
打开选择
/ 或 Ctrl+K
全站搜索
i / f / 1–4
筛选当前列表
g → h / l / p / t / a
首页 / 笔记 / 项目 / 工具 / About
r → 1–9
打开所选对象的关联
u
返回上一页
Esc
关闭浮层 / 取消命令;输入框内清空文本