Workbench 与配置

查询、写入与页面资源

查询选项、刷新策略、错误处理、数据边界和 UI Provider 的按需参考。

先完成 View 教程中的读取和按钮,再在需要分页、自动刷新或错误恢复时查本页。 scope 把一组查询和写入绑定到某一个页面声明;每次打开页面都有独立的缓存、订阅和操作状态。

先确定谁触发刷新

状态如何变化写法
只由本页按钮修改mutation 声明 workbench: { invalidates: [query] }
后台或其他页面也会修改query 用 workbench.subscribe 订阅服务端通知
通知已经覆盖本次写入mutation 不再重复声明 invalidation
查询参数随用户选择变化使用 queryFamily(),用 target(input)all() 指定刷新范围

查询与写入的声明可放在模块顶层;当前页面的 API、缓存和结果只能通过该页面的 Hook 取得。 关闭页面后旧 controls 失效,再次打开会创建一份新状态。

按参数查询

下面假设页面 API 另外提供 order(id)rename(input) 方法:

需要按输入读取时使用 queryFamily((context, input) => options),并用 query.useQuery(input)。Factory 为每个输入生成具体、 领域可读的 queryKey;Key 会先验证和 canonicalize,结构相等的 portable key 共享同一 query。 query.target(input) 只失效该 key,query.all() 失效当前 family 的已有 keys。Invalidation target 是当前 scope 的 opaque typed value,不是 global cache key;不能交给另一个 renderer scope。

Options factory 是同步、确定且无副作用的声明函数:Hook 解析资源和 mutation 预检 family target 时都可能再次执行它。 不要在 factory 中读取时间、发起 I/O、注册 subscription 或修改外部状态;这些行为分别留给 queryFnworkbench.subscribemutationFn。同一个 canonical queryKey 必须始终描述同一份读取语义。

const orderQuery = overviewScope.queryFamily(({ api }, input: Readonly<{ id: string }>) => ({
	queryKey: ['orders', 'detail', input.id] as const,
	queryFn: ({ signal }) => {
		signal.throwIfAborted()
		return api.order(input.id)
	},
}))

type RenameOrderInput = Readonly<{ id: string; name: string }>

const renameOrder = overviewScope.mutation(({ api }) => ({
	mutationFn: (input: RenameOrderInput) => api.rename(input),
	workbench: {
		invalidates: (input: RenameOrderInput) => [orderQuery.target({ id: input.id })],
	},
}))

Workbench roots(api / provider / consumer)只由外层 factory 捕获。queryFn 接收 query-core 原生的 安全 context:规范化后的 queryKeysignal;不会混入 Pluxel 自定义参数。signal 首先用于本地取消和阻止晚到 结果提交;除非领域 API 明确提供本地 cancellation adapter,不要把 AbortSignal 当 RPC DTO 传输。

无输入的具体 query 本身就是 exact invalidation target;query family 必须显式选择 target(input)all(),避免一个 family 在代码中含糊地代表“某个 key”还是“全部 key”。

数据与订阅如何清理

每次打开 React 页面都会创建独立的 QueryClient,保存此次打开的 API、订阅与关闭状态。即使相同 View 或 route 同时打开多次,也不会跨 open handle、params、 principal、session 或 Plugin generation 共享 data/error/invalidation。这个 client 随 renderer owner 清理,不向 Remote 暴露 raw QueryClient、raw cache 或全局 client。

Query 把 API result 放入 cache 前会验证 portable data、深拷贝、深冻结,并恰好释放一次 top-level transport result。显式 undefined field、class instance、accessor、cycle、binary 或 capability 都不能进入 cache。带 subscription 的 query 先订阅再读取; 同 key 的 observers 共享一个 read/subscription,read 期间多次 invalidation 只触发一次 follow-up read。后台失败保留最近成功 data 并 标记 stale/error。workbench.subscribe 一旦保留 callback,就必须同步返回或异步 resolve 到 Disposable;API 方法通常声明返回 child RpcTarget,其 browser-side RpcPromise / RpcStub 满足该清理契约。

renderer scope 负责 browser-side subscription 的 retain、abort 与 dispose。只有服务端自定义 callback target 才需要自己 dup() observer、释放每次 callback result,并在 open signal abort 时 unsubscribe;普通 latest snapshot 不应复制这段样板。

Query 与 mutation 契约

选项或操作当前语义
enabledTanStack-compatible boolean;Workbench 默认 truefalse 时当前 observer 不自动读取或持有 subscription,但当前 Hook 的显式 refetch() 仍执行一次读取。
staleTimeTanStack-compatible 毫秒数;无 subscription 时默认 0,有 workbench.subscribe 时默认 Infinity。显式 invalidation 始终覆盖 freshness。
retryTanStack-compatible retry;Workbench 默认 false。predicate 签名为 (failureCount, error) => boolean;renderer boundary failure 不 retry。
retryDelayTanStack-compatible delay;接受非负毫秒数或 (failureCount, error) => number,省略时沿用 query-core backoff。
query signal由 query-core 通过 queryFn context 提供;读取被替代、query deactivate 或 renderer close 时 abort。底层若不观察 signal 仍可 settle,Workbench 仍会 detach/dispose 晚到结果并阻止其提交。
mutation signal由外层 mutation factory 捕获,只在 per-open renderer owner close 时 abort。Hook unmount 和 reset() 不会伪装取消已接受的写入。
mutate() / mutateAsync()mutate() 用于 event handler 的 fire-and-observe,失败进入 Hook state,不向外泄漏 rejected Promise;pending 时的重复调用保留当前 pending state。mutateAsync() 用于需要 result 或显式流程编排的调用,并会 reject pending duplicate。两者经过同一 pipeline。
mutation reset()只把 settled success/error state 清回 idle;pending 时不取消,也不重置。

Query result 提供 statusdataerrorisPendingisFetchingisStale 和 instance-bound refetch() / invalidate()refetch() 返回本次显式读取的 Promise;invalidate() 同步标 stale,只为 active observers 调度读取。Controls 只在产生它的 Hook 仍挂载时有效:卸载后 refetch() reject、invalidate() throw;同一个 queryFamily Hook 切换 input 后,旧 input 的 result controls 也立即失效。Module-scoped resource 不提供无法判定 open handle 的命令式刷新。

useMutation() 是 per-hook single-flight:普通 event handler 直接调用 mutation.mutate(input),再从 Hook state 呈现结果;只有 需要返回值或显式 await / catch 的流程才使用 mutateAsync()。Pending 时第二次 mutateAsync() 稳定失败,不自动 queue 或猜测幂等性。Hook 卸载后不能从旧 controls 启动新 mutation:mutate() throw、mutateAsync() reject,reset() no-op;卸载前已接受的 mutation 仍按 owner lifetime settle。若 mutation result 只是下一份 snapshot 的重复副本,领域 API 应返回 void,并用权威 subscription 或 workbench.invalidates 刷新 query;只有 UI 确实消费的 domain result 才返回 portable DTO。

静态 freshness 关系写 workbench: { invalidates: [query] },只有 target 依赖 mutation input 时才使用 callback;callback 复用并显式标注 mutationFn 的 variables type,避免把 freshness mapping 悄悄放宽成 any。若 mutation commit 必然通过同一权威 subscription 通知当前 snapshot,就省略该 mutation 的 workbench.invalidates;若通知可能丢失、延后,或 RPC reject/detach failure 后仍必须刷新,则声明 invalidation。 Framework 会 coalesce 同期 invalidation,但作者仍应只声明真实 freshness authority,避免 subscription 与 invalidation 无条件触发双重刷新。Targets 会在调用远端方法前完成 scope/key 验证;owner 仍 active 时,在 mutation settle 后标 stale,即使 RPC reject 或 result detach 失败也一样。Mutation success 不等待 invalidated query 的读取完成。

Query/mutation options 是公开类型明确列出的受控 allowlist,不承诺透传 TanStack Query 的全部 options。 TanStack 原生 query/mutation 字段保持顶层;Workbench 自有的 subscription 和 typed invalidation 只出现在 workbench namespace。Factory 返回的顶层与 workbench 对象在 TypeScript 中都是 exact;未知字段会在作者 typecheck 时拒绝,非 TypeScript 调用方或绕过类型的值仍会由 Runtime fail-fast 校验。

稳定错误与恢复

code作者应如何处理
WORKBENCH_RENDERER_CLOSED当前 open 已结束;停止更新,后续交给新的 open 重建。
WORKBENCH_RENDERER_HOOK_INACTIVE丢弃已卸载 Hook 或旧 family input 的 controls;从当前 render 重新取得 controls。
WORKBENCH_RENDERER_SCOPE_MISMATCH修正 descriptor、scope、resource 或 invalidation target 的 wiring,不要在 scopes 间复用 resource。
WORKBENCH_RESOURCE_KEY_INVALID修正 queryKey / target(input),只返回有界 portable key。
WORKBENCH_RESOURCE_LIMIT_EXCEEDED缩小 queryKey,或减少 active query keys / mutation targets;不要盲目 retry。
WORKBENCH_MUTATION_PENDING等待当前 Hook 的 mutation settle,再接受下一次写入。
WORKBENCH_NON_PORTABLE_VALUE让 API 返回普通 portable DTO;移除 undefined、class、accessor、cycle、binary 与 capability。
WORKBENCH_PORTABLE_VALUE_TOO_DEEP扁平化 DTO,避免把深层对象图当作 snapshot。
WORKBENCH_PORTABLE_VALUE_TOO_LARGE分页、裁剪字段或按 key 拆分读取。
WORKBENCH_TRANSPORT_DISPOSE_FAILED修复 top-level transport result 的 disposer/ownership,并检查错误的 cause

低层 useWorkbench(exactDescriptor)useRemoteValue() / createRemoteValue()detachWorkbenchPortableValue() 仍是高级 escape hatch:适用于单组件自管 read owner、callback/progress/cancel、lossless event 或 capability handle。手工 await DTO 时,必须用 detach helper 建立 ownership continuation;不要把 transport-owned result、 capability 或已经释放的 proxy 放入 React state。

每个 renderer 由 React Bridge 挂载为独立 React root。若 renderer 使用 Mantine、router、i18n 等依赖 Context 的 UI library,应在自己的 root 内安装 Provider,并由 producer import 所需样式;Shell 的私有 Provider 不会跨 root 继承,也不是 Workbench API。例如 Mantine renderer 的入口可以直接写成:

import { MantineProvider } from '@mantine/core'

export function OrdersPage() {
	const { host } = overviewScope.useWorkbench()
	return (
		<MantineProvider forceColorScheme={host.colorScheme}>
			<OrdersContent />
		</MantineProvider>
	)
}

这里的 Provider 属于 renderer root,不能从 Shell 继承;但 @mantine/core@mantine/hooks 是 Workbench 固定的 MF2 singleton shared,组件和 hooks 在一个 document 内只加载一份。Mantine 基础 CSS 也由 Shell 统一加载,renderer 不应再次导入 @mantine/core/styles.css,producer build 会直接拒绝这种重复。host.colorScheme 只是可移植的宿主外观事实,不暴露 Shell 私有 Provider 或 theme object。其他 UI library 仍由 producer 自己打包并管理 Provider/CSS。

最后更新于

本页目录