Supabase 推送回执迁移
rxdb_mutations 的推送路径从「整批成功或整批失败」改为逐实体回执:一批推送里的某一条被行级权限拒绝(或目标行已不存在、依赖的父实体被拒),不再拖垮同一批里其它能成功的变更。这是一次破坏性变更:远端合并结果的契约 RemoteMergeResult 改了形状,自定义远端适配器作者必须跟着改;只用 @aiao/rxdb-adapter-supabase 的开发者不用改业务代码,但要先升级参考 SQL 再升级客户端依赖,并了解被拒变更的新语义。
影响范围
| 你是谁 | 要做什么 |
|---|---|
实现了 RxDBAdapterRemoteBase.mergeChanges 的自定义适配器 | 必须改:返回值形状变了(见下节),否则编译不过 |
只用 @aiao/rxdb-adapter-supabase | 不用改业务代码;升级前先跑新版参考 SQL;了解 lastRejections 新语义即可 |
mergeChanges 返回值变了(自定义远端适配器作者)
RemoteMergeResult 的 changeIdMapping(可选)被 results(必填)取代,mergeChanges 的返回类型也从联合类型收紧为单一类型:
变更前
export interface RemoteMergeResult {
maxChangeId?: number;
changeIdMapping?: Array<{ localId: number; remoteId: number }>;
}
abstract mergeChanges(
actions: SwitchVersionActions,
branchId?: string,
changes?: IRxDBChange[]
): Promise<RemoteMergeResult | number | void>;
变更后
export interface RemoteMergeResult {
/** 本次远端最大变更 id */
maxChangeId?: number;
/** 每条源变更的结果;必须恰好覆盖调用时传入的全部 `changes` */
results: RemoteChangeResult[];
}
export type RemoteChangeResult =
| { localId: number; status: 'applied'; remoteId: number }
| { localId: number; status: 'rejected'; rejection: RemoteChangeRejection };
export interface RemoteChangeRejection {
/** 数据库 SQLSTATE,例如 `42501` */
code: string;
/** 归类:权限拒绝 / 目标行已不存在 / 依赖的实体不可用 */
reason: 'denied' | 'gone' | 'dependency';
/** 远端原始消息 */
message: string;
/** 被拒的实体 */
entity: RemoteEntityRef;
/** `reason = 'dependency'` 时指向父实体;无法定位时给出约束名 */
dependsOn?: RemoteEntityRef | { constraint: string };
}
abstract mergeChanges(
actions: SwitchVersionActions,
branchId?: string,
changes?: IRxDBChange[]
): Promise<RemoteMergeResult>;
RemoteEntityRef({ namespace, entity, entityId })形状不变。
迁移要点:
changeIdMapping删除;改成results,且从「可省」变成必填——过去远端什么都不回传时调用方按「无映射」兜底放行,现在空值本身就是契约违反。results必须恰好覆盖调用时传入的changes参数:每条源变更(IRxDBChange)出现恰好一次,要么status: 'applied'带remoteId,要么status: 'rejected'带rejection。不传changes时返回空数组。- 返回类型从
RemoteMergeResult | number | void收紧为单一的Promise<RemoteMergeResult>——旧代码里「返回一个数字当maxChangeId」或「什么都不返回」的写法不再通过类型检查。
推送仓库现在会校验 results
@aiao/rxdb-plugin-sync 的推送仓库不再信任远端「什么都不说」:它对 results 做覆盖检查,三种情况都会让整轮推送失败、水位线不推进:
- 缺失:
changes里某条源变更的localId没出现在results里; - 重复:同一个
localId出现了不止一次; - 未知:
results里出现了调用时没传入的localId。
自定义适配器如果过去依赖「只回传部分映射、其余按远端 id 等于本地 id 处理」之类的隐式兜底,升级后会在推送时直接抛错,而不是静默产生错误的 remoteId。
PushRepositoryResult 新增必填字段 rejected
推送仓库结果新增必填字段 rejected: number(本轮被拒的源变更数),与既有的 pushed 配合读:pushed 现在只数 status: 'applied' 的变更,不再把被拒的也算进去。和 failures 一样设计成必填而非可选——「字段缺失」和「没有被拒」是两件不同的事,可选字段会让调用方不得不写 result.rejected ?? 0 猜测缺省含义。
被拒变更的本地语义
一条源变更被远端拒绝后,本地的处理方式是持久标记为终态,不再重推,而不是像以前的整批失败那样留在待推队列里反复重试:
- 本地
RxDBChange新增两列:rejectedAt(拒绝时刻)与rejection(拒绝原因,形状同RemoteChangeRejection)。两者与remoteId互斥——一条变更至多其一非空。 - 「待推」判据从「
remoteId = null」改为「remoteId = null AND rejectedAt = null」:被拒变更不会再被下一轮推送取到,也不会让待推计数、过期清理之类的查询永远卡住。 - 推送提交时,被拒实体(不是单条变更,是该实体本批全部被拒的源变更一起)会被对齐为远端当前值:远端还有这行就整行覆盖为远端值,远端已经没有就把本地那行删掉;对齐不产生新的本地变更、不进撤销栈。
- 被拒不是永久的:下一次有新的本地修改经过正常推送流程重新判定,远端权限或数据变化后完全可能变为
applied。
框架侧经 SyncState.lastRejections 读取最近一轮的被拒列表(三框架 useSyncState() 对称暴露),完整字段、语义与跨重启查询示例见 @aiao/rxdb-plugin-sync 的「被拒的推送」,这里不重复。
系统模式 6 → 7
RXDB_SYSTEM_SCHEMA_VERSION 从 6 升到 7,对应的正是上一节那两个新列:RxDBChange 补 rejectedAt(可空时间戳)与 rejection(可空 JSON)。两个本地适配器的 migrateSystemSchema() 都按「列不存在才 ADD COLUMN」的幂等写法补列,旧行不回填两列(保持 null)。这一步在 connect() 时自动跑,不需要手工迁移脚本;唯一要注意的是不要跳过小版本连续升级——如果中间直接从更早的系统模式版本升级,migrateSystemSchema() 仍会按顺序把中间的几次结构变更一并补齐。
升级顺序:先 SQL,再客户端
这次改动同时动了参考 SQL 与客户端,必须先执行新版 SQL,再升级客户端依赖:
- 在 Supabase 数据库执行新版
docker/sql/04-rxdb-utils-functions.sql。rxdb_mutations从 5 参签名换成 6 参签名,新增的第 6 个参数p_receipts boolean DEFAULT false带默认值,旧客户端按名匹配到前 5 个参数,行为不变。 - 升级客户端依赖
@aiao/rxdb-adapter-supabase(连带@aiao/rxdb、@aiao/rxdb-plugin-sync,若要读lastRejections还要升级对应框架绑定包)。
反过来做(先升级客户端)会在推送时直接报错,见下节。
新旧版本组合
| 客户端 | 远端 SQL | 结果 |
|---|---|---|
| 旧(US-220 及更早) | 旧(5 参,无 p_receipts) | 变更前的整批语义,不受影响 |
| 旧(US-220 及更早) | 新(6 参) | 不传 p_receipts,取默认值 false:仍按整批语义工作——全有或全无,被拒以 42501 / RX001 / RX002 整批抛出 |
| 新(本迁移之后) | 旧(5 参) | PostgREST 找不到带 p_receipts 的函数签名 → PGRST202 → SupabaseDataError;推送失败,水位线不推进,本地变更仍待推 |
| 新(本迁移之后) | 新(6 参) | 本文描述的逐实体回执语义 |
任何组合都不会「部分成功却不告知客户端」——旧 SQL 配新客户端不会静默丢失被拒信息,它直接让整次推送失败并报出可识别的错误。
新客户端连旧 SQL 的报错特征
推送抛 SupabaseDataError,code 为 PGRST202,消息形如:
Failed to merge changes: Could not find the function public.rxdb_mutations(p_changes, p_deletes, p_receipts, p_skip_sync, p_updates, p_upserts) in the schema cache
对应的 HTTP 响应是 404。推送水位线不推进,本地变更不会丢失;执行新版 SQL 后无需任何客户端操作,下一次推送会把积压的变更一并推上去。
参考
- Supabase 适配器
@aiao/rxdb-plugin-sync· 被拒的推送- Supabase 修改推送语义迁移:上一次改动
rxdb_mutations签名时走的同一套「先 SQL 后客户端」升级顺序