fix(mobile): 批量合并离线镜像通知,修复多设备集中离线崩溃 - #4259
Merged
Merged
Conversation
隔离复现定位真实机制: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>
8 tasks
|
| 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 一次]
Reviews (1): Last reviewed commit: "fix(mobile): 批量合并离线镜像通知,修复多设备集中离线崩溃" | Re-trigger Greptile
MagicLizi
approved these changes
Sep 10, 2026
MagicLizi
left a comment
Contributor
There was a problem hiding this comment.
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。无新依赖、无凭证。
代码核对
- remoteScheduleEventStore.invalidateDeviceMirrors:整批删 snapshot、单调 +1 invalidation version、一次 emit。空 id 跳过。单设备 invalidateDeviceMirror 保留。
- remoteSessionStore.sweepDevicesOffline:单/批量共用原 markDeviceOffline 清扫(pending text batch、sessionDeviceIndex、pending host anchors、权威 pending、live 投影)。changed 才 emit 一次。重复离线幂等(测试覆盖)。
- freezeUnboundPendingHostAnchorsForOffline 的第三段循环对同一 session 的多个 deviceIds 可能多次调用 freeze;函数按 deviceId 过滤且幂等,不构成语义回归。
- offlineMirrorWipeQueue:同 task 首台预约一次 schedule(默认 queueMicrotask),后续只入 Set;跨 task 因 pending 已清空而独立 flush;空 id 忽略;wave 内去重。
- DeviceLinkContext:presence wipe 改 enqueue;flush 走 markOfflineDeviceMirrors(session/schedule 各 notify 一次)。hard wipeUnavailableDeviceMirror 仍逐台,PR 明确不包含。
- Home REST:soft 先收集再 softInvalidateDeviceMirrors;hard/ghost 仍逐台。首页 crash 链是 scheduleMirrorInvalidations effect 里同步 setScheduleIndex;批量后该 snapshot 引用每波只换一次。
- 单设备 hydrate 失败仍走 markDeviceOffline → 批量包装单 id,语义不变。
- 未解决 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。
Contributor
|
合了。多设备同时掉线时把通知收成一波,避开了 React 嵌套更新上限,冷启动不再被剂量打崩。 |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
这次改了什么
摘要
修复 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 修复(批量合并通知):
remoteScheduleEventStore.invalidateDeviceMirrors(ids):整批设备一次快照替换、一次 notify。remoteSessionStore.markDevicesOffline(ids):清扫主体抽为sweepDevicesOffline共用,整批只 emit 一次。offlineMirrorWipeQueue:wipe 定时器回调只入队,同一 task 内到期的整波设备经queueMicrotask合并为一次批量清理;跨 task 各自独立 flush,每台设备的离线清理与恢复语义不变。markOfflineDeviceMirrors(ids)与首页 REST 快照整批 offline 的 soft 失效循环。配套硬化(重复离线标记幂等、详情页 effect 按代次消费、代次单调)在 #4252 单独评审,与本 PR 相互独立、不互相依赖。
变更类型
fix缺陷修复范围
wipeUnavailableDeviceMirror(硬清理,显式关闭远控路径)的逐台通知合并——低频路径,后续单独评估UI 变化
怎么验证的
自动验证
手工验证
未执行:80+ 台真实账号在完整 Android 客户端上的验证待修复经 canary OTA 下发后进行(隔离复现环境为 React DOM/JSDOM,53 这一数字不能外推为产品阈值)。
未执行的验证
Android 实机多设备集中离线复现路径未在本地执行:需要真机 + 大量设备账号环境;已以 store 层剂量回归测试(整波单次 notify)与队列合并测试覆盖机制语义。
风险
风险分类
影响与回滚
提交前检查
git commit -s,见 DCO)