跳到正文
曾亮懿 ZengLiangYi Blog

TanStack Query 数据流:缓存键、失效与后台任务

对照 ChatCrystal 的查询钩子和 API,用离线缓存实验验证参数键与前缀失效,再区分请求成功、后台完成和页面刷新。

曾亮懿

曾亮懿

阅读约 4 分钟
目录 展开 收起

列表筛选、笔记详情和后台总结会在不同时间改变页面数据。查询库能管理服务端状态,但只有缓存身份和完成事件都接对了,界面才会在需要时刷新。本文阅读 ChatCrystal 当前的查询路径,再用没有 React、HTTP 或真实数据的实验检查缓存行为。

先区分查询缓存和页面状态

App.tsx在顶层创建 QueryClient,通过 QueryClientProvider 提供给页面。当前默认 staleTime 为 30,000ms,retry 为 1;这不是适合任何应用的通用最佳值,也不表示 30 秒内服务器数据不会改变。

查询结果是服务端状态。搜索输入、选中标签、页码、弹窗是否打开则是界面状态,不必为了统一而全部放进查询缓存。当前笔记页把这些筛选参数传给 useNotes;改动筛选后重置页码,避免沿用上一条件的偏移量。

缓存键必须覆盖请求参数

use-notes.ts使用 ['notes', params] 标识列表,['note', id] 标识详情。标签、搜索、来源类型、offset 和 limit 改变时应对应不同请求身份。只用 ['notes'] 缓存所有分页,会把不同页面的数据放进同一条记录。

对象字段顺序不等于键不同;数组元素的顺序仍有意义。详情钩子仅在 id > 0 时启用自动查询,小数或 Infinity 也可能满足这个条件,因此它不是完整的 ID 校验,也不能据此认为 TypeScript 已把任意数据或 undefined 都收窄成有效笔记。

下面使用与当前 React Query 相同的 query-core 5.102.8。在新临时目录安装,保存 cache.mjs,用 Node 24+ 运行。这是没有观察者的离线缓存实验,不是 React 页面或 HTTP 请求。为确定性关闭重试、禁用自动回收,和应用默认配置不同。

npm init -y
npm install --ignore-scripts --no-audit --no-fund @tanstack/query-core@5.102.8
import assert from 'node:assert/strict';
import { QueryClient } from '@tanstack/query-core';

const client = new QueryClient({
  defaultOptions: { queries: { staleTime: 30_000, retry: false, gcTime: Infinity } },
});
let reads = 0;
const queryFn = async () => ({ revision: ++reads });
const first = ['notes', { tag: 'mcp', offset: 0, limit: 20 }];
const reordered = ['notes', { limit: 20, offset: 0, tag: 'mcp' }];
const second = ['notes', { tag: 'mcp', offset: 20, limit: 20 }];
try {
  assert.equal((await client.fetchQuery({ queryKey: first, queryFn })).revision, 1);
  assert.equal((await client.fetchQuery({ queryKey: reordered, queryFn })).revision, 1);
  await client.fetchQuery({ queryKey: second, queryFn });
  assert.equal(reads, 2);
  client.setQueryData(['note', 7], { title: 'separate detail' });
  await client.invalidateQueries({ queryKey: ['notes'] });
  assert.equal(reads, 2);
  assert.equal(client.getQueryState(first).isInvalidated, true);
  assert.equal(client.getQueryState(second).isInvalidated, true);
  assert.equal(client.getQueryState(['note', 7]).isInvalidated, false);
  assert.equal((await client.fetchQuery({ queryKey: first, queryFn })).revision, 3);
  console.log('keys, cached reads, and inactive invalidation pass');
} finally {
  client.clear();
}

将 invalidateQueries 的前缀从 notes 改为 note,列表失效断言应失败;恢复后通过。两个不同页码都匹配列表前缀,详情不匹配。这里没有活动观察者,失效没有马上增加读取次数;下一次 fetchQuery 才重新读取。真实页面的活动查询通常还会在失效时后台重取,见官方失效说明。

API 失败需要成为查询失败

当前 api.ts封装请求:按需设置 Content-Type,带上已配置 token 的 Bearer 头,并将 HTTP 错误和业务 success:false 变成异常。HTTP 401 还会清除 token 并通知认证状态变更。泛型返回类型只是类型断言,不是对任意响应 JSON 的运行时验证。

这类封装让查询层看到失败,但页面仍须显示失败。比如 SearchPage处理 isError;笔记列表当前主要取 data 和 isLoading,不能因此把所有没有数据的显示都理解成已确认的空列表。排查时应分别检查首次加载、后台刷新、失败和真正零结果。

useMutation 也不按 HTTP 方法自动分配。SearchPage 使用它触发按钮提交的 GET 搜索,所以“查询都必须用 useQuery,所有 mutation 都写服务器”不符合现有代码。选择应依据自动缓存和触发方式,而不是只看函数名称。

提交成功与任务完成分开处理

当前总结 mutation 在请求成功后使 queue-status、conversation、conversations、notes、status 等查询失效;批量总结和取消有各自的键集合。但后端接受排队请求后,AI 工作仍可能在运行。这时刷新笔记列表,不保证已经能读到新笔记。

use-queue.ts在有活动任务时每 2 秒查询状态,否则每 10 秒;StatusBar显示任务进展与完成提示。这个完成提示没有同时让 notes 查询失效,不能把它描述为“后台结束后所有列表自动同步”。队列自身的边界见任务队列设计。

导入的 SSE 钩子又是一条独立 fetch 路径,在 done 时更新导入状态并使 conversations、status 失效。SSE 完成、队列完成和普通请求成功是不同事件,应逐条确认需要刷新哪些缓存。

把刷新约定写成可验证的行为

如果要补充后台完成后的刷新,可以让明确的完成事件按任务结果使相关键失效,再用延迟任务样本验证列表确实出现新数据。这是改进建议,当前代码并未在此文章中实现。同样,乐观更新需要回滚和并发策略,不能因为有 invalidateQueries 就宣布已有。

验收时至少区分:筛选改变是否使用新键,提交成功时任务是否仍在运行,完成后是否重新请求,失败是否显示错误而不是空状态。staleTime 与轮询间隔应结合这些事件选择;本文的离线调用次数不是网络性能或页面响应时间测量。

分享:

相关文章

五种 AI 对话解析器:字段保留与损失边界

从 Claude Code、Codex、Cursor、Trae 和 Copilot 的实际解析器建立验收矩阵,检查工具标记、思考文本、顺序与快照覆盖范围。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #TypeScript #Node.js

AI SDK Provider 抽象:能力表与协议边界

核对 ChatCrystal 的七项 Provider 注册、AI SDK 7 模型工厂和结构化输出,用无网络 mock 验证 Responses、Chat Completions、Embedding 与 schema 失败。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #Node.js #TypeScript

SourceAdapter 内部链路:身份、内容哈希与重新导入

对照 ChatCrystal 的注册表、导入器和远端载荷,验证会话身份、内容哈希、元数据捷径与 parserVersion 的实际边界。

曾亮懿 曾亮懿
阅读约 4 分钟
#ChatCrystal #TypeScript #Node.js

从文章到代码

到我的 GitHub 看看 ChatCrystal、开发者工具和日常学习记录,欢迎交流实现细节。