fix(objectql,client): subscribeData delivers a real DataEvent (#4626) - #4655
Conversation
The producer (ObjectQL engine) published a raw RealtimeEventPayload envelope
with `{ recordId, after, changes }` nested under `payload` and never generated
`id`/`userId`, while `@objectstack/client`'s `subscribeData` force-cast that
envelope into the callback (`callback(event as any as DataEvent)`). Subscribers
reading the declared top-level `event.recordId` / `event.changes` compiled green
and got `undefined` at runtime. Data-side twin of #4602.
Producer fulfils the contract: insert/update/delete build a true DataEvent
(uuid `id`, flattened top-level fields, `userId` from the execution context)
and `DataEventSchema.parse` it before publish. A multi-row updateMany/deleteMany
names no single record, so it publishes nothing (warn) instead of the previous
`recordId: ''` fabrication; bulk contract tracked in #4639.
Consumers read the fulfilled shape: the client validates at the boundary and
rejects off-contract payloads loudly; the webhook auto-enqueuer drops its
`recordId ?? id ?? after?.id ?? 'unknown'` tolerance chain; service-knowledge
reads the record from `after` and the delete id from `recordId` instead of
indexing the envelope as if it were the row.
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
|
The latest updates on your projects. Learn more about Vercel for GitHub. 1 Skipped Deployment
|
📓 Docs Drift CheckThis PR changes 4 package(s): 26 hand-written doc(s) reference the affected code and may need an implementation-accuracy re-verification:
|
|
PM:暂缓合并,已撤下自动合并并转回 draft。 两个原因,都不是对实现方向的质疑(方向是 PM 裁定的 Option 1,没有变):
放行条件:① 热路径实测数据贴出(或据此改为 needs_decision);② #4639 有维护者裁决。两条满足后我转正重新挂自动合并。 Generated by Claude Code |
|
记录订正(本 PR 已于 14:43:30 合并): 我在合并前撤下了自动合并并把 PR 转回 draft,但它当时已排在合并队列前端,队列照常完成了合并——扣留没有生效,不是有人推翻了决定。责任在我:PR 14:10 就进了队列,我到 14:35 才去核验证据。 因此以下两点是带着缺口合入的,记在这里以免日后被当成已验证:
Generated by Claude Code |
Fixes #4626
问题
@objectstack/spec/api的DataEvent声明了顶层字段:id(uuid,必填)、type、object、recordId(必填)、changes?、before?、after?、userId?、timestamp。但两端都没有兑现这个契约:
生产者(ObjectQL engine)直接 publish 传输信封
RealtimeEventPayload,把{ recordId, after, changes }塞在payload里,id/userId从来没有生成过。消费者(
@objectstack/client的subscribeData)把这个信封二次强制转型成DataEvent:结果:订阅者按类型声明写
event.recordId/event.changes—— 编译全绿,运行时全是undefined。这是已合并的 #4602(subscribeMetadata/MetadataEvent)在数据侧的孪生缺陷。方案(PM 裁定:Option 1 —— 生产者兑现契约)
没有在消费者侧加
??容错(AGENTS.md Prime Directive #12);在生产者处修,并在边界大声拒绝。生产者
ObjectQL.insert()/update()/delete()现在统一走新的publishDataEvent(),构造真正的DataEvent(生成 uuidid、字段拍平到顶层、从执行上下文取userId),并在 publish 前DataEventSchema.parse。传输信封保持不变(RealtimeEventPayload,payload承载完整的DataEvent),线上格式仍是{ type, object, payload, timestamp }。批量 insert 仍然是每条记录一个事件,各自带独立的事件 id。
一处行为变更:多行写入不再发事件
multi: true的updateMany/deleteMany只返回受影响行数,没有任何一条记录可供必填的recordId指代。引擎现在不发事件并 warn 说明,而不是继续发之前那种recordId: ''/after: <数字>的伪造事件 —— 那种事件任何遵守 schema 的消费者都必须拒绝。后果:批量写入不再触发 webhook 和知识库同步(此前会触发一次,但 body 不可用)。真正的批量事件契约在 #4639 跟踪。
消费者(本仓库内的迁移,同 PR)
@objectstack/client的subscribeData拆信封 +DataEventSchema.safeParse。不合契约的 payload 大声拒绝(抛错,callback 不会被调用),绝不强转或放行。recordId过滤选项现在过滤的是兑现后的事件。@objectstack/client-react的useDataSubscription/useDataSubscriptionCallback/useAutoRefresh都委托给它,一并受益。@objectstack/plugin-webhooks的 auto-enqueuer 直接读必填的recordId,删掉了recordId ?? id ?? after?.id ?? before?.id ?? 'unknown'这条容错链 —— 那个'unknown'兜底会把一条指代不明的记录变成一次真实投递。不合契约的事件现在带 warn 丢弃。@objectstack/service-knowledge的事件同步从after取记录体、从recordId取删除 id。此前它把信封本身当成行去索引(于是同步出去的文档没有记录的任何字段),删除则从来解析不出 id。测试
packages/objectql/src/engine-data-events.test.ts(新):事件是完整的DataEvent、批量 insert 每条一个事件、多行写入不发事件、realtime 故障不回滚写入。packages/client/src/realtime-api-data.test.ts(新):边界校验、off-contract 大声拒绝、recordId过滤走兑现后的字段。packages/services/service-knowledge/src/__tests__/event-sync-data-events.test.ts(新)。packages/plugins/plugin-webhooks/src/auto-enqueuer.test.ts:新增 off-contract 丢弃用例。热路径成本
按 PM 要求实测了 uuid 生成 +
DataEventSchema.parse在写路径上的开销,数据见下方评论。🤖 Generated with Claude Code
https://claude.ai/code/session_012C2cd7tL8QDoZ2QKN3djJ5
Generated by Claude Code