API 设计与使用

契约、数据与资源

区分本地能力、RPC 数据和资源引用,让插件作者只维护必要的公共契约。

设计 API 时,先回答调用方要读取一个事实,还是取得一项可持续使用的能力。前者返回 snapshot 或操作回执,后者返回有明确生命周期的 handle;不必让每个业务实体都变成远端对象。

要决定什么规则位置
本地方法还是浏览器 API本地边界、页面边界
返回数据、回执还是资源方法命名、数据与资源
谁创建、共享、释放引用作用域持有
哪一层校验和授权生产者边界
错误如何让调用方恢复本地 Result、签名之外的契约

本地插件 API 按领域设计

同一 host 中,required dependency 通过 constructor 声明。方法使用领域词汇,按需要返回普通数据或具有明确撤回语义的对象,不为本地调用复制一套 RPC 方法和 DTO。

只读 TypeScript 类型不会隔离共享可变对象。返回快照时,由生产者保证调用方不能通过它意外修改内部状态;返回 handle 时,说明谁拥有资源、停止或 replacement 后还能否调用。需要持续使用的对象不能只靠最初取得它时的一次可用性检查。

跨 Plugin 的可调用入口使用 prototype method;不要用捕获原始实例的 function-valued field 代替。具体 caller 与生命周期规则见插件模型。

页面 API 只公开客户端需要的能力

Workbench 的默认写法把 browser-safe DTO、API interface 和 definition 放在 workbench.ts。服务端 target 实现这个 interface;客户端通过 exact descriptor 获得对应类型,不再手写第二份 client interface。

沿用 View 教程中的 OrdersSnapshot、OrdersApi 和 OrdersWorkbench,查询直接调用 API:

import { createWorkbenchRenderer } from '@pluxel/workbench/react'
import { OrdersWorkbench } from '../workbench.ts'

export const overviewScope = createWorkbenchRenderer(OrdersWorkbench.overview)

export const ordersQuery = overviewScope.query(({ api }) => ({
	queryKey: ['orders', 'snapshot'] as const,
	queryFn: () => api.snapshotDto(),
}))

普通 scope query/mutation 已经处理结果边界,作者不需要在其中再包一次 consumeWorkbenchValue()。只有 UI 需要消费写入后的事实时才返回回执或快照;只需刷新查询时,mutation 可以返回 void 并声明失效范围。

一个公开类型应对应一个真实兼容性承诺。不要直接把数据库 row、SDK 实例或 Plugin implementation 当作浏览器契约。只有出现真实的第二客户端时,才把共用 contract 提取到中立模块或独立 subpath;import type 避免运行时加载,但仍需保证公开声明的依赖适合客户端消费。

本地插件可选 Result 契约

跨 Plugin 公开可预期失败时,可以共同从 @pluxel/core/better-result 导入。该子入口直接再导出 上游的全部命名导出与类型;安装、完整导出范围、同步/异步组合和错误处理见 better-result 共享入口。

Result 实例只用于本地 API。通过 Workbench、Worker 或 JSON 传输时,由生产者投影为经过校验的普通 DTO, 消费者按领域协议恢复;部分成功、已保存未应用等状态继续保留各自语义。 仅将调用方需要按类型恢复的预期业务失败放进错误分支。用户可以修正的输入错误可作为领域失败;上游响应 schema 损坏、未知服务故障和 Plugin 生命周期失败仍按各自契约传播。 具体选择与官方插件示例索引见 Better Result 指南。

共享 RPC 能力按作用域持有

Workbench 已经为每次打开页面取得所需 root。scope.useWorkbench()、query 和 mutation factory 借用同一组引用,不执行获取 RPC,也不调用 dup()。View 使用 api,Attachment 使用 provider 和可选 consumer;这里的 provider 表示 Attachment 的能力提供方,不是需要作者再创建的 React Provider。

组件需要手动使用能力时,直接写 const { api } = overviewScope.useWorkbench();普通读写优先声明 scope query/mutation,统一处理结果、订阅和查询失效。页面组件默认消费 scope/resource,不从 host 或 session 根重新导航,也不创建自己的连接。host 提供宿主交互;本地服务端 Plugin 之间仍通过 dependency 调用领域能力,不绕经浏览器 RPC。

边界持有与共享规则
document sessionShell 持有连接、认证与顶层 capability
单次 View/Attachment openopened handle 持有 root;scope 向本页面所有组件借出同一引用
多组件长期共用的子 capability在最近共同父级设置一个领域 owner,获取一次,用 Context 或 props 借出
单次任务独占的子 capability任务内取得并释放,依赖它的异步调用完成后再退出作用域
DTO 查询resource cache 只保存数据,不承担 capability 生命周期

不要按服务端对象层级机械增加 Provider。只有某个 scope 确实绑定身份、资源范围或独立生命周期时才建立相应 owner;简单查询直接传业务参数。不同 open 的 root 与 cache 保持独立,即使 descriptor 相同也不建立全局 stub 池。

借用者不释放共享引用。dup() 只用于确实需要独立持有的引用,并由新 owner 释放;它不会延长服务端授权或 Plugin generation。子 owner 在 scope 变化或页面关闭时释放自己取得的引用,迟到的获取结果只释放、不发布。共享 capability 不等于调用自动去重;重复 DTO 读取由同一 resource 的 query key 管理。

RPC 方法名表达返回值所有权

纯数据 RPC 方法统一使用 *Dto 后缀,并在 interface 和 target 实现中写出返回类型。名称描述返回值契约,不表示这个方法只读;返回纯数据回执的写操作也使用后缀。

方法边界与返回值命名示例
RPC 返回纯数据,包括 primitive、snapshot、分页结果或操作回执*DtosnapshotDto()、listDto(input)、updateDto(input)
RPC 命令返回 void领域动词refresh()、remove(id)
RPC 返回 capability、订阅 handle 或混合数据与 capability领域动词,不加 Dtowatch(observer)、openEntry(input)
本地 Plugin 领域方法、客户端 facade 或已打开的 handle领域词汇,不随 transport 改名plugin.snapshot()、client.plugins.list()、opened.run()

例如 refreshDto(): Promise<OrdersSnapshot> 返回刷新后的事实,refresh(): Promise<void> 只承诺操作完成。两者按实际消费需求选择,不为统一命名额外制造回执。纯数据 Result union 仍是 DTO;只要其中一个分支带 capability,整体就是资源或混合结果。

Framework raw RPC 同样遵守这项规则:layoutDto() 返回数据,openEntry() 返回含 root 的资源。*Dto 不承诺“无副作用”:Content 的 subscribeDto() 注册 observer,但订阅寿命属于 root,返回值仍是纯数据。Content 的其余 raw 方法和本地 handle 名称见 Content;Management 消费后的本地 facade 继续使用领域名称。

生产者负责 DTO 边界

服务端 target 依次完成输入的领域校验、基于可信 principal 与当前状态的授权、领域操作,以及面向客户端的字段投影。不要直接返回数据库 row、Plugin、Context 或供应商对象。标注返回类型可以检查已知字段;运行时输入和可传输性仍要验证。

RpcTarget 的原型方法和 getter 属于远端可访问表面,TypeScript private 不会在运行时隐藏它们。Target 的内部辅助方法使用 #private 或移到普通领域服务。此规则只针对 RPC target;Plugin 类受 caller facade 约束,不能照搬 #private,遵循插件模型。

已经是明确 DTO 的领域快照可以直接复用;只在需要隐藏字段或转换领域表示时建立投影。返回前调用 @pluxel/workbench/server 的 assertWorkbenchDto(dto),验证完整数据树和大小、深度预算。该函数不复制、不冻结、不释放资源,也不改变对象身份;它不替代领域 schema、授权或隐私字段选择,不会替调用方删除 capability。异步领域结果先 await,再校验实际 DTO。

import { assertWorkbenchDto } from '@pluxel/workbench/server'
import type { OrdersSnapshot } from './workbench.ts'

function projectOrdersDto(snapshot: OrdersSnapshot): OrdersSnapshot {
	const dto: OrdersSnapshot = {
		revision: snapshot.revision,
		openOrders: snapshot.openOrders,
	}
	assertWorkbenchDto(dto, 'Orders snapshot')
	return dto
}

生产者校验负责保证发出的值符合 DTO 承诺;Cap’n Web 负责 transport 编解码;客户端消费边界负责接管解码后的对象并冻结以供缓存。它们分别拥有不同责任,不需要再增加 copyDto() 层。

数据与资源分别持有

结果消费方式清理责任
scope query/mutation 的 portable 数据页面读取本地结果,不缓存 stubrenderer boundary 处理 transport result
View/Attachment root通过 scope 或对应低层 Hook 借用打开页面的 owner 释放,组件不自行释放借来的 root
订阅、child capability、长任务 handle显式持有,按领域契约使用获取者或接管它的 owner 清理
混合数据与 capability 的结果拆分数据和资源,明确 ownership transfer不能整体当作普通查询数据缓存

当前 scope 的数据边界接管返回值所有权,校验完整数据树后原地深冻结,移除并释放 top-level transport result 的元数据;不会再复制 RPC 已解码的数据。它不是任意 Cap’n Web 值的缓存:显式 undefined 字段、非枚举业务字段、class、accessor、cycle、binary 和 capability 都不属于 portable 查询结果。省略 optional 字段与写入 undefined 字段不同。

如果 query/mutation 返回本地对象而非 RPC 结果,也会转移整个数据树的所有权;返回新构造或已不可变的快照,不要返回其他代码仍会修改的借用对象。表单需要独立草稿时,按业务字段建立草稿。

手动读取 RPC、订阅和资源型返回值的操作方式以页面资源为准。consumeWorkbenchValue() 处理纯数据并返回 WorkbenchSnapshot<T>。它接管传入的对象或 PromiseLike 结果,保留数据对象身份,并在成功或失败时清理结果;调用方不再单独 dispose 同一结果。它不把 capability 转成数据。不要把通用的 JSON.stringify() / structuredClone() 当作 RPC ownership 转移机制。

方法签名之外还要说明什么

为一个公开方法写文档时,补充调用方无法从类型推导的内容即可:

  • 返回的是 snapshot、live handle 还是写入回执;是否有版本、新鲜度或部分成功语义。
  • 输入在哪里验证,身份与 scope 从何而来,实际执行时如何授权。
  • 资源由谁创建、借用和释放,stop、replacement、关闭页面后如何失效。
  • 哪些失败可恢复;取消或断线后,写入是否可能已提交。

Cap’n Web 负责远端引用和调用,不替业务决定授权、事务和恢复。不要假设自定义 Error 的字段会自动穿过 transport;需要客户端分支处理的错误必须验证实际接收结果。已有领域 Result 或 receipt 若承载部分成功、冲突或未知结果,也不能为了统一返回风格丢掉这些信息。

最后更新于

本页目录