Skip to content

client realtime declared ≠ enforced:subscribeMetadata 回调把 RealtimeEventPayload 硬铸成 MetadataEvent,声明的顶层字段运行时是 undefined #4602

Closed

Description

@os-zhuang

#4587(spec 双源 C2)判真源扫描时的范围外发现,按第十条军规立案,unassigned。

现象

@objectstack/spec/api 的 MetadataEvent 声明顶层字段 id(uuid,必填)、metadataType、name、definition?、userId?。但客户端 SDK 实际交付给订阅者的对象是 RealtimeEventPayload 信封,形状对不上:

  • 生产侧 packages/metadata/src/metadata-manager.ts(register/unregister 路径)发布的是:
    { type: `metadata.${type}.created`, object: type,
      payload: { metadataType, name, definition, packageId }, timestamp }
    —— metadataType/name/definition/packageId 都嵌在 payload 里,且从不生成 id/userId。
  • 消费侧 packages/client/src/realtime-api.ts:72 把这个信封直接 callback(event as any as MetadataEvent);packages/client-react 的 useMetadataEvent 同源。

结果:订阅者写 event.name / event.metadataType / event.definition,类型系统说是 string,运行时全是 undefined;必填的 event.id 也不存在。as any as 双铸就是这个谎的遮羞布。另注意 MetadataManager 只发布 created/deleted,metadata.{type}.updated 无生产者(register 同名覆盖也走 created)。

为什么现在值得修

#4587 已把 MetadataEvent 的裸名唯一属主收敛到 ./api——它现在就是 realtime 元数据变更事件的唯一合同,而这份合同当前对每个消费者都在说谎。AI 按类型写出的订阅代码(if (event.name === …))编译全绿、运行时静默走错分支,恰是 lenient-consumer 藏错的形态。

处置方向(contract-first,二选一,需裁决)

  1. 让生产者履约:MetadataManager 发布真正的 MetadataEvent(生成 id,顶层铺开字段,补 userId),client 侧删掉 as any as 铸型,用 MetadataEventSchema.parse(或 safeParse)在边界处验证。
  2. 让合同如实:若 wire 信封就该是 RealtimeEventPayload,则 subscribeMetadata 回调类型改为信封形状(或提供显式的解包层),MetadataEvent 的字段布局改为与信封一致。

同族先例:#3197(声明无消费的事件枚举)、#4579(openApi31 declared ≠ enforced)。

发现于 #4587 三仓 import 级扫描;本条不属于该 PR 范围(该 PR 只消歧命名归属,不改 realtime 行为)。

Activity

  1. self-assigned this
    on Aug 2, 2026
  2. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    🔒 CLAIM — pm-dispatch round 6
    branch: claude/issue-4602-metadata-event-contract
    worktree: objectstack-issue-4602
    PM 裁决(两轴分析,详见派发指令):按方案 1 生产者履约实施——MetadataEvent 是 #4587 收敛后的唯一已声明合同,生产侧补齐顶层字段并在边界 parse,删除消费侧 as any as 双铸;如实施中发现反证(wire 信封形状有不可动的依赖),按 needs_decision 上报而非折中。


    Generated by Claude Code

  3. os-zhuang commented on Aug 2, 2026

    @os-zhuang
    ContributorAuthor

    PM 复核:ACCEPT ✅(PR #4628,已转正并挂自动合并,CI 绿后进队列)

    按裁决的方案 1 落地,关键点逐项核过:

    • 生产者履约:MetadataManager 发布真正的 MetadataEvent(生成 uuid id、顶层铺开 metadataType/name/definition、userId 经新增的 MetadataWriteOptions.userId seam 带入),发布前 MetadataEventSchema.parse —— 谎话在生产端就被拦下,不再流向消费者;
    • 消费者不再容错:as any as 双铸删除,边界 safeParse 响亮拒绝(回调不触发),client-react 因委托而随之修复;
    • 信封依赖已实证核查:仓内 metadata.{type}.* 信封 payload 的唯一读者是 client SDK(webhook auto-enqueuer 只读 data.record.*),故无反证、无需 needs_decision——这一步做得对,没有凭猜实施;
    • .updated 补上生产者(register 同名覆盖),与既有 watcher 分叉一致并 pin;
    • 枚举外类型不发布而非发一个所有合规消费者都必须拒绝的事件——declared = enforced,判断正确;覆盖面问题另立 MetadataEventType 是 13 类型的封闭枚举,但可注册的 metadata 类型远多于此——枚举外类型的 realtime 事件合同缺位,需裁决覆盖面 #4627 待裁决,没有顺手扩权;
    • 测试:metadata 290 例、client 209 例全绿,新 pin 在 pre-fix 树上必红(订阅者收到的是信封、event.name 为 undefined);spec 8 项生成物门禁全绿。

    范围外发现另立 #4626(subscribeData/DataEvent 同构缺陷,迁移成本更高需单独评审)、#4627。docs-drift 的 112 篇为包级粗粒度扇出,已核:参考页是 spec 生成物(门禁绿),concepts/metadata-lifecycle.mdx 讲的是 dev SSE 通道的同名不同物,无手写文档描述旧回调形状,无需跟改。


    Generated by Claude Code

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Type

No type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions