Skip to content
This repository was archived by the owner on Nov 4, 2026. It is now read-only.
This repository was archived by the owner on Nov 4, 2026. It is now read-only.

ObjectStackDataSource.delete() 读 result.deleted —— 服务端从不返回该键,delete 的 MutationEvent 从未发出、返回值恒 undefined;测试绿是因为 fixture 自己编了这个键 #3412

Closed

Description

@baozhoutao

实现 objectstack#5638(client SDK 的 DeleteDataResult 把 success 声明成了 deleted)时做读取方跨仓测绘撞见。这一条是 objectui 侧真实的运行时缺陷,不是类型问题,故另立。

事实

packages/data-objectstack/src/index.ts:1433-1442:

const result = await this.client.data.delete(
  resource,
  String(id),
  opts?.ifMatch ? { ifMatch: opts.ifMatch } : undefined,
);
if (result.deleted) {
  this.emitMutation({ type: 'delete', resource, id });
}
return result.deleted;

client.data.delete 是 @objectstack/client 里的纯直通(unwrapResponse,无任何运行时改写),服务端 DELETE /api/v1/data/:object/:id 回的是 spec 的 DeleteDataResponse = { object, id, success }。DeleteDataResponseSchema(objectstack packages/spec/src/api/protocol.zod.ts:472)声明的第三个键就是 success;objectstack#5581 / PR #5641 之后,protocol 与 ObjectQL 兜底两条路径都返回 success,deleted 这个键从来没有任何 schema 声明过、任何服务端路径返回过。

所以对真实服务端:

  • result.deleted === undefined → if (result.deleted) 恒假 → 单条删除的 MutationEvent 从来没有发出过;
  • return result.deleted 在声明为 Promise< boolean > 的方法里恒返回 undefined,调用方拿 if (await ds.delete(...)) 判断会一律当成删除失败。

之所以编译通过:@objectstack/client 把这个键声明成了 deleted(objectstack#5638),TS 为错的那一种背书。

用户可见的后果

订阅 onMutation 的界面在单条删除后不刷新 —— 列表仍显示已删的行、计数徽章不减。批量删除面不受影响(bulkDelete 走 deleteMany,index.ts:1525 用 count > 0 判断,与本条无关);index.ts:1549 的兜底逐条删除不读返回值,也不受影响。受影响的只有单条 delete() 这一条路径。

与 #2582(batchTransaction 不发 MutationEvent)同族但不同因:那一条是压根没调 emitMutation,这一条是调了、但守卫读的是一个永远 undefined 的键。

为什么测试是绿的

packages/data-objectstack/src/onMutation.test.ts:80-95,「emits a delete event only when the server confirms deletion」:

const del = vi
  .fn()
  .mockResolvedValueOnce({ deleted: true })
  .mockResolvedValueOnce({ deleted: false });

fixture 自己编造了一个服务端从不返回的键,于是这条用例证明的是「当 deleted 为 true 时发事件」,而 deleted 在生产里永远不是 true。守卫是死的,断言照样绿 —— objectstack#4984 记过的同一种形状(fixture 拼写了被拒绝的别名,于是规则已死而测试仍绿)。

修的时候这条 fixture 属于「整条替换」而不是「改拼写」:把 mock 换成 spec 的 { object, id, success: true } / { …, success: false },让它读的是幸存逻辑真正会读到的东西。

建议修点

  1. index.ts:1438/1441 的 result.deleted → result.success。
  2. onMutation.test.ts 的 delete fixture 换成 spec 形状(见上),另建议补一条「fixture 说 deleted: true 时不发事件」的反向钉,防止再退回。
  3. ⛔ 不要写 result.success ?? result.deleted 之类的容错读:生产方只有一种形状,消费端同时认两种键正是 contract-first 禁止的写法(objectstack#5638 分诊已就同一问题裁定不留双键过渡)。

Blocked-by

objectstack#5638(DeleteDataResult.deleted → success)落地并发版之前,本仓改成 result.success 会因为已发布的 @objectstack/client 类型仍声明 deleted 而 TS 红;绕过它唯一的写法是 as any,而那正是上面第 3 条禁止的形状。运行时上 success 今天就已经是对的(protocol 路径一直返回 success),所以这不是等行为、只是等类型 —— 等 @objectstack/client 带 #5638 的版本进本仓的 pin 之后即可动手。

Blocked-by: objectstack-ai/objectstack#5638

Activity

  1. os-zhuang commented on Aug 6, 2026

    @os-zhuang
    Contributor

    分诊:pm:blocked。⛔ 不打 domain:* —— objectui 是整仓座位,无路由需求(#5468 落地的域表规则)。落点在本仓 packages/data-objectstack,非 feature 级、不涉 packages/spec,不移交 objectstack。

    过时前提检查(objectui origin/main@6195841,实读):

    • packages/data-objectstack/src/index.ts:1438 if (result.deleted) {、:1441 return result.deleted; ✅ 原样仍在;
    • onMutation.test.ts:83-84 的 mockResolvedValueOnce({ deleted: true }) / ({ deleted: false }) ✅ 仍在 —— 「fixture 自己编了这个键」成立。

    ⚠️ 阻塞条件要改写 —— 照正文字面读会误判为已解除。 objectstack#5638 已于 2026-08-05 23:04Z CLOSED(completed)。但本单真正等的不是那个 issue 关闭,而是「带 #5638 的 @objectstack/client 发布版进入本仓 pin」—— 正文自己写清了这一点(「等类型不等行为」),而 issue 状态与 pin 覆盖是两个读数。这正是 SKILL「Pin 滞后 ——『上游已合入』不等于『本仓已看见』」记的盲区。实测:本仓 package.json 仍 pin @objectstack/spec: ^17.0.0-rc.2,而按 #3374 的实测,npm 上最高的已发布版本是 rc.3(2026-08-03 16:42Z),早于 #5638 合入近两天 ⇒ 任何已发布版本都不含它,阻塞仍然成立。

    所以本单的重启条件(请后续巡检按这一条核,不要按 issue 状态核):

    本仓 lockfile 实际解析到的 @objectstack/client 版本包含 objectstack#5638 —— 判据是 lockfile 解析版本,不是 package.json 的 ^ 区间(该坑 #3374 已验证过)。

    这与 #3374 的 rc.4 抬 pin 窗口是同一班车,建议由 objectui 座位在抬 pin 时一并核销;是否正式挂进 #3374 的联动表由该座位定(本单等的是 client 不是 spec,可能不同包同窗口)。

    为什么不是 finding:用户可见后果明确(订阅 onMutation 的界面单条删除后不刷新,列表仍显示已删行)、落点到行、修法已给出且带反向钉建议 —— 是 restore-invariant 形状,不是观察类。阻塞一解除即可直接派发,无需再分诊。

    查重:本仓与 objectstack open issue/PR 各搜一遍(result.deleted / DeleteDataResult / MutationEvent)。objectstack#5638 是上游同源(client 类型半边,已闭合),#2582(batchTransaction 不发 MutationEvent)是同族不同因、已在正文辨析。无影子单,不关重复。

    ⛔ 派发时请把正文第 3 条(不得写 result.success ?? result.deleted 之类双键容错)原样带进派发令 —— 那是 contract-first 口径,不是风格偏好。

    本评论来自分诊座位 Routine(#5474 试点),不构成认领。


    Generated by Claude Code

  2. added a commit that references this issue on Aug 10, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions