Skip to content

fix(mobile): 批量合并离线镜像通知,修复多设备集中离线崩溃 - #4259

Merged
MagicLizi merged 1 commit into
mainfrom
fix/mobile-offline-mirror-wave
Sep 10, 2026
Merged

MagicLizi merged 1 commit into
mainfrom
fix/mobile-offline-mirror-wave

Conversation

@guyong-zapo

Copy link
Copy Markdown
Collaborator

这次改了什么

摘要

修复 Android 端 JS 层 Maximum update depth exceeded 致命崩溃(2026-09-10 CindyGlobal 0.1.13(19) + canary OTA,冷启动约 5.5s 后退出)。

根因(隔离复现已定位,剂量依赖模型,非无限循环):presence 整批离线时,每台设备各触发一轮 store 通知(invalidateDeviceMirror + markDeviceOffline 各 emit 一次),首页 effect 在同一条嵌套链里 setScheduleIndex;设备数超过 React 嵌套更新上限(约 50)即致命退出。复现矩阵:40 台逐台通知正常;80 台第 53 次通知崩;80 台同批处理仅 3 次渲染、正常。同 OTA 包减少设备数后稳定运行,证实为剂量依赖。RN 定时器队列把同批到期的 5s 宽限 wipe 定时器在同一 task 内投递,是嵌套链的来源;崩溃时点(连接后 ~5.5s)与之吻合。

本 PR 修复(批量合并通知):

  1. remoteScheduleEventStore.invalidateDeviceMirrors(ids):整批设备一次快照替换、一次 notify
  2. remoteSessionStore.markDevicesOffline(ids):清扫主体抽为 sweepDevicesOffline 共用,整批只 emit 一次。
  3. 新增 offlineMirrorWipeQueue:wipe 定时器回调只入队,同一 task 内到期的整波设备经 queueMicrotask 合并为一次批量清理;跨 task 各自独立 flush,每台设备的离线清理与恢复语义不变
  4. 批量接线:DeviceLinkContext 的 markOfflineDeviceMirrors(ids) 与首页 REST 快照整批 offline 的 soft 失效循环。

配套硬化(重复离线标记幂等、详情页 effect 按代次消费、代次单调)在 #4252 单独评审,与本 PR 相互独立、不互相依赖。

变更类型

  • fix 缺陷修复

范围

UI 变化

  • 引用的设计规范:不涉及。纯状态机/通知合并逻辑修复,无视觉、交互或文案变化(命中 UI 路径的文件为设备列表 screens,改动仅限 store 通知机制与批量失效入口)

怎么验证的

自动验证

pnpm test:unit:related
结果:apps/mobile 426 files / 5356 tests 全部通过(含 40/80/100 台批量通知剂量回归、
wipe 队列合并/去重/跨 task 用例);desktop 与 maker-core 存在与本改动无关的既有本机失败
(已隔离验证,以 CI 为准)。

pnpm --filter mobile run typecheck
结果:通过。

node apps/mobile/scripts/ci-fingerprint.mjs compute(改动前后比对)
结果:iOS 8545caf7c05b591dc81a460f7ed6cd12f3e6f339 / Android 70a051193695c4af3775baa93c0d6db5e2c8bd82
前后完全一致 → 不触发冷更。

手工验证

未执行:80+ 台真实账号在完整 Android 客户端上的验证待修复经 canary OTA 下发后进行(隔离复现环境为 React DOM/JSDOM,53 这一数字不能外推为产品阈值)。

未执行的验证

Android 实机多设备集中离线复现路径未在本地执行:需要真机 + 大量设备账号环境;已以 store 层剂量回归测试(整波单次 notify)与队列合并测试覆盖机制语义。

风险

风险分类

  • 无已知风险
  • 原生层 / fingerprint / OTA(仅指 OTA 下发,不触发冷更)

影响与回滚

  • 影响范围:纯 JS 改动。离线 wipe 的通知时机有微任务级合并(同 task 内的整波合并为一次),清理语义与单设备路径不变;不改变 runtime fingerprint(ci-fingerprint 前后哈希一致),经 OTA 热更即可到达存量装机,不触发冷更、无需冷更把关确认。
  • 回滚 / 降级方式:revert 本 PR 后重新下发上一版 canary OTA 即可。

提交前检查

  • 已 review 完整 diff
  • 每个 commit 都带 DCO 签名(git commit -s,见 DCO)
  • UI 改动已在「UI 变化」注明(不涉及,附理由)
  • 未提交凭证、令牌或授权文件
  • 已确认测试结果或说明未执行原因

隔离复现定位真实机制:presence 整批离线时每台设备各触发一轮 store
通知,首页 effect 在同一条嵌套链里 setScheduleIndex,设备数超过
React 嵌套更新上限(约 50)即致命退出——40 台正常,80 台第 53 次
通知崩,同批处理仅 3 次渲染则正常。同一 OTA 包减少设备数后稳定运
行,证实为剂量依赖而非无限循环。RN 定时器队列把同批到期的 wipe 定
时器在同一 task 内投递,是嵌套链的来源。

- remoteScheduleEventStore 新增 invalidateDeviceMirrors:整批设备
  一次快照替换、一次 notify。
- remoteSessionStore 新增 markDevicesOffline:清扫主体抽为
  sweepDevicesOffline 共用,整批只 emit 一次。
- 新增 offlineMirrorWipeQueue:wipe 定时器回调只入队,同一 task 内
  到期的整波设备经 queueMicrotask 合并为一次批量清理;跨 task 各自
  独立 flush,每台设备的离线清理语义不变。
- 设备详情批量入口 markOfflineDeviceMirrors 与首页 REST 快照整批
  offline 的 soft 失效循环改走批量路径。
- 测试:40/80/100 台批量通知剂量回归(整波单次 notify)、队列合并
  /去重/跨 task、interactionModel 源码不变量锚点随重构更新。

ci-fingerprint 前后一致(iOS 8545caf7 / Android 70a05119),纯 JS
改动不触发冷更。

Signed-off-by: guyong <guyong@xd.com>
@greptile-apps

greptile-apps Bot commented Sep 10, 2026

Copy link
Copy Markdown

Greptile Summary

本 PR 将移动端集中离线时的逐设备 store 通知改为批量通知,并通过微任务队列合并同一 task 内到期的 presence wipe,以避免大量同步通知触发 React 嵌套更新上限。

  • 为远程日程与会话 store 增加批量失效、批量离线接口。
  • 将 REST 快照中的 soft offline 设备集中处理。
  • 新增离线镜像 wipe 合并队列,并保持逐设备缓存清理语义。
  • 补充 40/80/100 台设备的通知剂量回归测试,以及队列合并、去重和跨 task 测试。

Confidence Score: 5/5

未发现由本 PR 引入的具体可达缺陷,当前变更看起来可以安全合并。

批量 API 保留了原有逐设备清理语义,同时把两个 store 的通知压缩为每波一次;新增队列在微任务边界去重并刷新,相关高剂量与幂等行为已有针对性测试覆盖。

Important Files Changed

Filename Overview
apps/mobile/src/device-link/offlineMirrorWipeQueue.ts 新增按微任务边界合并并去重离线镜像清理请求的队列,未发现可达的行为缺陷。
apps/mobile/src/device-link/DeviceLinkContext.tsx 将 presence wipe 接入批量队列,并集中执行会话、日程及关联缓存清理。
apps/mobile/src/session/remoteSessionStore.ts 抽取可复用的离线清扫主体并新增单次通知的批量接口,原单设备语义得到保留。
apps/mobile/src/scheduler/remoteScheduleEvents.ts 新增批量镜像失效接口,在完成整批版本更新后仅发送一次通知。
apps/mobile/app/devices/index.tsx REST 快照中的 soft offline 项改为收集后批量失效,hard cleanup 路径保持逐项处理。
apps/mobile/src/tests/offlineMirrorWipeQueue.test.ts 覆盖同 task 合并、设备 ID 去重、跨 task 独立刷新和空 ID 行为。
apps/mobile/src/tests/remoteScheduleEvents.test.ts 覆盖 40、80、100 台设备批量失效时每波仅通知一次及版本递增。
apps/mobile/src/tests/remoteSessionStore.test.ts 验证批量离线清扫的单次通知、状态清理和重复调用幂等性。

Flowchart

%%{init: {'theme': 'neutral'}}%%
flowchart TD
  A[Presence 宽限定时器到期] --> B[offlineMirrorWipeQueue.enqueue]
  B --> C{同一 task 内继续到期?}
  C -->|是| B
  C -->|task 结束| D[queueMicrotask flush]
  D --> E[去重后的 deviceIds]
  E --> F[remoteSessionStore.markDevicesOffline]
  E --> G[逐设备清理索引与缓存]
  E --> H[remoteScheduleEventStore.invalidateDeviceMirrors]
  F --> I[会话 store 仅 notify 一次]
  H --> J[日程 store 仅 notify 一次]
Loading

Reviews (1): Last reviewed commit: "fix(mobile): 批量合并离线镜像通知,修复多设备集中离线崩溃" | Re-trigger Greptile

@MagicLizi MagicLizi added the touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示) label Sep 10, 2026

@MagicLizi MagicLizi left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

PR #4259 代码审查报告

  • repo: makecindy/cindy
  • head: 8eaafff
  • base: 3ef3ac9 (main)
  • diffHash: e968727b9624e238e886f76ea57dcc5e9a70efa1d13b7e7c01641f71a023b537
  • tier: standard
  • author: guyong-zapo
  • reviewer viewer: magiclizi

Findings

无 P0/P1。

Rule coverage

  • AGENTS.md / docs/dev-rules/development-workflow.md:fix 模板三段齐全;描述与 diff 一致(批量 store 通知 + wipe 微任务合并);未夹带无关功能。DCO Signed-off-by 与 author 一致。
  • docs/dev-rules/mobile-development.md:纯 JS 改动。作者声明 ci-fingerprint 前后一致、不触发冷更;未改 app.json / 原生模块 / mobile package.json 指纹输入。本审查未再跑 fingerprint 脚本(与 CI pr-design-basis/client-ci 全绿交叉核对)。
  • docs/dev-rules/remote-and-mobile-adaptation.md:改的是控制端离线镜像清理通知剂量,不是 relay 重连/拆连接径。故障半径仍是单设备软离线清理;同 task 内只合并通知,跨 task 仍独立 flush。未扩大到整条 relay。
  • 产品门语义 fallback(product):命中 apps/mobile UI 路径,但 diff 只改 store 通知合并与 REST/presence 批量入口,无外观/布局/文案/交互流向变化。用户看不见像素差异。不 hold。
  • DESIGN.md:无视觉改动,不对照界面证据。format.uiEvidenceMissing=true 为路径启发式,非阻断;PR 已写明「不涉及」。未发截图催办。
  • security:hardHits=0 / softHits=0。无新依赖、无凭证。

代码核对

  1. remoteScheduleEventStore.invalidateDeviceMirrors:整批删 snapshot、单调 +1 invalidation version、一次 emit。空 id 跳过。单设备 invalidateDeviceMirror 保留。
  2. remoteSessionStore.sweepDevicesOffline:单/批量共用原 markDeviceOffline 清扫(pending text batch、sessionDeviceIndex、pending host anchors、权威 pending、live 投影)。changed 才 emit 一次。重复离线幂等(测试覆盖)。
  3. freezeUnboundPendingHostAnchorsForOffline 的第三段循环对同一 session 的多个 deviceIds 可能多次调用 freeze;函数按 deviceId 过滤且幂等,不构成语义回归。
  4. offlineMirrorWipeQueue:同 task 首台预约一次 schedule(默认 queueMicrotask),后续只入 Set;跨 task 因 pending 已清空而独立 flush;空 id 忽略;wave 内去重。
  5. DeviceLinkContext:presence wipe 改 enqueue;flush 走 markOfflineDeviceMirrors(session/schedule 各 notify 一次)。hard wipeUnavailableDeviceMirror 仍逐台,PR 明确不包含。
  6. Home REST:soft 先收集再 softInvalidateDeviceMirrors;hard/ghost 仍逐台。首页 crash 链是 scheduleMirrorInvalidations effect 里同步 setScheduleIndex;批量后该 snapshot 引用每波只换一次。
  7. 单设备 hydrate 失败仍走 markDeviceOffline → 批量包装单 id,语义不变。
  8. 未解决 thread:0。Greptile 普通评论无阻断问题。Copilot/Codex 对当前 head 超时未表态,按 botSettle 上限放行。

Verification

  • 定向单测(overlay 本 worktree packages 到临时 node_modules 后):
    pnpm --filter mobile exec vitest run
    src/tests/offlineMirrorWipeQueue.test.ts
    src/tests/remoteScheduleEvents.test.ts
    src/tests/interactionModel.test.ts
    src/tests/remoteSessionStore.test.ts
    结果:Test Files 4 passed / Tests 315 passed;VITEST_FULL_EXIT=0。
    日志:$ROUND/verify-pr-4259-overlay.log
  • 首次借用主仓 node_modules 失败(@cindy/maker-shared 链到无关 worktree,isRemoteTextDelta is not a function)。属审查环境,不是本 PR 回归。overlay 后通过。
  • typecheck:首次环境同样因包解析失败;未在匹配依赖上重跑。作者声称 pnpm --filter mobile run typecheck 通过;GitHub client-ci success。本审查不以本机失败的 typecheck 记 P1。
  • 未执行:Android 实机 80+ 设备集中离线(作者已声明;机制由 store/队列测试覆盖)。
  • CI(live context):client-ci / pr-design-basis / PR #4259 均为 success。

Overall

pass。没有 P0/P1。

@MagicLizi
MagicLizi merged commit 48c94b0 into main Sep 10, 2026
20 checks passed
@MagicLizi

Copy link
Copy Markdown
Contributor

合了。多设备同时掉线时把通知收成一波,避开了 React 嵌套更新上限,冷启动不再被剂量打崩。

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

Labels

touches:product-ui 改动碰到产品 / UI 面(review-pr 自动维护,仅展示)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants