关于 query-layer
用于构建 UI 与服务之间响应式查询层的开发指南,基于 TanStack Query。它说明共享查询与变更定义、缓存行为、错误传递及命令式调用方式。
主要功能
- 01TanStack Query 适配模式
- 02共享查询和变更定义
- 03缓存键与失效处理指南
- 04响应式与命令式 API
- 05传递带标签的错误
- 06操作层边界划分
适用场景
- 01为 UI 组件定义共享查询
- 02通过共享缓存标识执行变更
- 03在组件外获取或确保数据
- 04有依据地选择缓存更新方式
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ epicenterhq/ epicenter --skill query-layer安装后在 Claude Code 中输入/query-layer确认已加载。
包含的文件
除 SKILL.md 外,安装时一起下载的参考资料和脚本。
references/advanced-query-patterns.mderror-transformation-patterns.mdruntime-dependency-injection.md详细介绍
查询层模式
参考仓库
- TanStack Query:用于数据获取的异步状态管理
上游依据
当 TanStack Query 的行为、Svelte 适配器类型、缓存失效语义、乐观更新或变更生命周期回调会影响正确性时,请针对 TanStack/query 向 DeepWiki 提出一个范围明确的问题,再以此为依据。用它来梳理方向,然后在修改代码前,通过本地已安装的类型、源码或官方文档核实关键细节。
对于稳定的基础知识,以及下文已记录的仓库本地模式,无需使用 DeepWiki。
查询层是 UI 组件与服务层之间的响应式桥梁。它使用 TanStack Query 和 Wellcrafted 工厂,为服务函数或可观察操作封装缓存、变更生命周期状态、失效处理和直接命令式访问。
相关技能:有关这些查询所使用的服务层,请参阅
services-layer。有关 Svelte 专用的 TanStack Query 模式,请参阅svelte。有关 Result 到达 UI 边界后的 toast 和报告模式,请参阅error-handling。
核心架构
┌─────────────┐ ┌─────────────┐ ┌──────────────┐
│ UI │ --> │ Query │ --> │ Services │
│ Components │ │ Layer │ │ (UI-free) │
└─────────────┘ └─────────────┘ └──────────────┘
↑ │
└────────────────────┘
Reactive Updates
查询层职责:
- 使用注入的设置/配置调用服务
- 保留有类型的服务和操作错误,除非适配器引入了新的本地失败
- 管理 TanStack Query 缓存以支持乐观更新
- 提供可用于 hook 的
.options,并在存在时提供明确的命令式 API - 通过导出的
*Keys映射管理共享缓存标识
Wellcrafted 查询 API 形态
| 范围 | 查询 | 变更 |
|---|---|---|
| Hook 局部 Result 适配器 | resultQueryOptions(input) | resultMutationOptions(input) |
| 可复用定义 | defineQuery(input) | defineMutation(input) |
当返回 Result 的函数需要进入 TanStack 的数据/错误通道,且无需命令式 API 或共享查询标识时,请在单个 hook 调用处使用 resultQueryOptions 和 resultMutationOptions。
适配器会在内部解包(返回 Ok.data,抛出 Err.error)。
传入返回 Result 的函数;不要先调用 unwrap。
在共享的 $lib/queries 模块中使用 defineQuery 和 defineMutation。
查询会公开 .options、.fetch() 和 .ensure()。它们不可调用。
变更会公开 .options,且可调用。它们不会公开 .execute()。
Whispering 查询模块的规范形态
这些示例所源自的 Whispering 查询模块已被移除,因为 Whispering 已成为不带查询层的独立文件夹应用。这种形态仍是包含查询层的应用中 $lib/queries 模块的约定。
对于 Whispering 风格的 $lib/queries 模块,请将事实来源声明放在靠近其描述的工作的位置。工厂会显式接收由会话持有的运行时:
export const audioKeys = defineKeys({
availability: (id: string, blobId: string, uploadedAt: string | null) =>
['audio', 'availability', id, blobId, uploadedAt] as const,
});
export function createAudioQueries({ defineQuery }: WhisperingQueryRuntime) {
return {
availability: (recording: Accessor<Recording>) =>
defineQuery({
queryKey: audioKeys.availability(
recording().id,
recording().audioBlobId,
recording().uploadedAt,
),
queryFn: () => getRecordingAudioAvailability(recording()),
}),
};
}
规则:
- 在拥有该工作的适配器或状态模块旁导出
*Keys = defineKeys({ ... })。 - 静态键无需使用
as const;当键工厂需要保留字面位置时,使用as const。 - 将键保留在所属模块中,除非其他层需要相同的回退标识。
- 将简短且仅使用一次的输入对象内联。仅当输入类型会被复用、需要导出、过大而影响函数可读性,或具有领域含义时,才为其命名。将命名的输入类型放在紧邻其所用适配器命名空间之前的位置。
- 除非其他模块需要命名该确切联合类型,否则将适配器局部的
defineErrors命名空间保留在本地。
适配器边界:查询与操作
使用 $lib/queries 作为共享的 TanStack 观察层。它可以封装直接的服务/状态调用,也可以封装 $lib/operations 入口点,前提是 UI 需要共享变更身份:多个使用方、缓存失效、乐观更新、useIsMutating,或为该操作设置命名变更键。
将编排逻辑放在 $lib/operations 中:投递、声音、分析、剪贴板写入和多步骤工作流。可复用的操作会返回其结果;终端交互控制器也可以报告该结果。不要仅仅为了观察本地待处理状态,就把只在单个组件中使用的操作提升到 $lib/queries。组件应在本地 createMutation 与直接 await 之间做出选择,这属于 svelte 技能的职责。
依赖方向
UI -> operations/* -> services/* + state/* + $lib/tauri
UI -> queries/* -> services/* or operations/*, plus narrow state reads/writes for observed lifecycle
查询模块接收由会话拥有的查询运行时,并导入服务、状态或操作。它们不会仅为了编排工作而导入同级查询模块;跨适配器协调应放在操作中。
错误流转
在 Whispering 中,服务和操作错误已经是带标签的错误。查询适配器会将其原样传递。由 UI/报告边界决定如何呈现这些错误。
Service / Operation -> Query Adapter -> UI / Report
TaggedError<'Name'> same error report.error({ cause: error })
只有当适配器本身发现了无法由下层处理的故障时,才定义查询本地错误,例如在调用操作前未能找到录音。
响应式与命令式用法
查询层适配器同时提供响应式钩子用法和显式命令式用法。
响应式接口:.options
共享查询适配器将 .options 暴露为静态对象。Svelte 钩子会在访问器中读取它:
<script lang="ts">
import { createQuery, createMutation } from '@tanstack/svelte-query';
import { getWhisperingQueries } from '$lib/whispering/context';
const queries = getWhisperingQueries();
const availability = createQuery(() =>
queries.audio.availability(() => recording).options,
);
const transcribeRecording = createMutation(
() => queries.transcription.transcribeRecording.options,
);
</script>
{#if availability.isPending}
<Spinner />
{:else if availability.error !== null}
<Error message={availability.error.message} />
{:else}
<AvailabilityBadge value={availability.data} />
{/if}
命令式接口:查询选择缓存策略,变更可直接调用
在组件上下文之外使用,或在调用方需要直接获取 Result 时使用:
// In an event handler or workflow
async function handleDownload(recording: Recording) {
const { error } = await queries.download.downloadRecording(recording);
if (error !== null) {
report.error({ cause: error });
return;
}
report.success({ title: 'Recording downloaded' });
}
// In a sequential workflow
async function stopAndTranscribe(toastId: string) {
const { data: url, error: playbackUrlError } =
await queries.audio.availability(() => recording).fetch();
if (playbackUrlError !== null) {
report.error({ cause: playbackUrlError });
return;
}
// Continue with transcription...
}
当你希望 TanStack 按照查询的常规过期策略进行判断时,使用 .fetch():即使无需发起请求,也可能直接返回新鲜的缓存数据。当缓存中存在任何数据都可以接受,且仅在缓存为空时才需要获取数据时,使用 .ensure()。
何时使用各接口
| 适配器接口 | 模式 |
|---|---|
| 共享响应式查询 | createQuery(() => queries.thing.options) |
| 共享响应式变更 | createMutation(() => queries.thing.options) |
| 命令式读取查询 | queries.thing(...).fetch() 或 queries.thing(...).ensure() |
| 命令式变更 | queries.thing(input) |
关于本地组件的操作放置和生命周期决策,请参阅
svelte 技能中的变更指南。
关键规则
- 对共享缓存身份使用
defineKeys- 在所属模块旁导出键映射 - 使用
.options(不带括号) - 它是静态对象,在 Svelte 中应将其包装在访问器中 - 默认不要转换带标签的错误 - 将服务/操作错误原样传递到报告边界
- 服务接收显式应用输入 - 由消费端注入设置和设备配置
- 将组件生命周期策略保留在
svelte中 - 此技能负责共享适配器形态和缓存行为 - 审慎更新缓存 - 仅当缓存所属方和回滚路径明确时才使用乐观写入;否则应使缓存失效或重新获取
参考资料
根据当前工作内容,按需阅读以下资料:
-
如果你正在查看错误原样传递示例和反模式,请阅读 references/error-transformation-patterns.md
-
如果你正在处理运行时依赖注入和服务选择,请阅读 references/runtime-dependency-injection.md
-
如果你正在处理缓存管理、查询定义、RPC 命名空间或通知协调,请阅读 references/advanced-query-patterns.md
-
请参阅
services-layer技能,了解服务的实现方式 -
请参阅
error-handling技能,了解 trySync/tryAsync 模式和出错时显示 toast 的约定