Skip to content

feat(native): 三端统一持久化落盘格式并一次性迁移 - #691

Merged
sunnylqm merged 2 commits into
masterfrom
claude/stored-state-layout
Oct 17, 2026
Merged

sunnylqm merged 2 commits into
masterfrom
claude/stored-state-layout

Conversation

@sunnylqm

@sunnylqm sunnylqm commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

概要

三端(Android、iOS、鸿蒙)改用同一份持久化布局(schema 1),并在首次以新格式启动时一次性迁移旧布局。设计与降级说明见 docs/STORED_STATE_LAYOUT.md。

  • 统一 schema:三端各自两个同名存储 pushy_state / pushy_cache(Android SharedPreferences 文件、iOS NSUserDefaults suite、鸿蒙 preferences)。键名三端一致;值一律是字符串,布尔 "1"/"0",标记置位时存 "1"、清除即删键,空串不落盘;状态存储带 schemaVersion。响应缓存单独放在 pushy_cache(三端存储都是整文件重写,Android 原本就这样,现在三端对齐)。
  • 单一来源 + 生成:scripts/contract.ts 新增 storeSchema / stateFields / legacyLayouts,storedItems 改为三端同名的物理键;生成器产出 Java StoreSchema、ArkTS StoreSchema.ts、iOS RCTPushyState.mm 的键表与 State ↔ 存储读写映射、各端 reset/升级清理清单,以及 C++ 的旧布局表(带 update 字样的旧键自动编码)。syncStorageKeys 并入 storedItems。
  • 共享迁移内核:cpp/patch_core/stored_items.h 的 MigrateStoredItems(layout, legacy, current) -> {puts, removes, schemaVersion, migrated},纯函数,三端只做 KV IO(Android 走新 JNI StoreMigration.migrateStoredItems,鸿蒙走新 NAPI migrateStoredItems,iOS 直接调用)。按目录自动收录进三端构建。

迁移策略

  • 只搬升级后本来就保留的项:uuid、nativeConfig、nativeCheckIncomplete、bundleHashCache,外加 packageVersion/buildTime(让升级照常由 SyncBinaryVersion 判定)。已核实:三端 release 构建每次都会写入新的 buildTime(Android gradle resValue、iOS podspec 脚本阶段、鸿蒙 hvigor 插件),带新 SDK 的二进制第一次启动必然被判为升级、清空热更状态,所以热更状态、版本元信息、启动标记、响应缓存直接删除,不做翻译。例外是 debug 构建(三端 buildTime 都固定为 "0"):迁移后回到内置包,只影响开发期。
  • 幂等、任一步被杀可重入:puts 以 schemaVersion 结尾;新存储里已有的值不覆盖(新存储优先);旧键只在 puts 落盘后删除(Android 一次 commit、鸿蒙 flush 成功后、iOS 写入返回即已交给 cfprefsd);看到当前 schema 时只清理残留旧键;看到更高的 schema 两个存储都不碰。C++ 测试对每个向量的每个前缀都验证了重入结果一致。
  • 失败不阻塞启动:内核异常 / JNI、NAPI 不可用 / 计划不可解析时,按全新安装启动,但宿主单独把旧 uuid 搬过去,不写 schema,下次启动重做完整迁移。
  • 只删自己的键:只删旧布局列出的键(含历史 SDK 写过的 blockUntil、REACTNATIVECN_PUSHY_BLOCKUPDATE 等)与 hash_ 前缀;iOS 旧存储与宿主共用,早期无前缀的 packageVersion/buildTime 可能属于宿主,不读不删。

降级行为

装回旧二进制时旧键已删除,旧二进制表现为全新安装:无热更版本、无原生配置(JS 运行后会重新同步),uuid 由 JS 重新生成(降级期间灰度分桶会变)。再升级回来时新存储的 schema 仍在,不再迁移,降级期间旧二进制写的键被清掉,uuid 恢复为降级前的值。App Store / 应用市场不允许降级,只出现在侧载、adb install -d 等场景;为此保留旧 uuid 键会违背“迁移后删除旧键”,权衡后不保留。

iOS 改用独立 suite 的取舍

换成 initWithSuiteName:@"pushy_state" / @"pushy_cache":避免与宿主键冲突(无前缀 packageVersion/buildTime 曾撞键导致每次启动清掉热更);宿主常见的“退出登录清 defaults 域”不再连带抹掉 uuid 与热更状态;hash_* 前缀清理不再遍历宿主的全部 defaults;三端可用同一组无前缀键名。代价是多一个 plist(同在 Library/Preferences,备份行为不变,隐私清单 CA92.1 照样覆盖)以及本次迁移。

不变的部分

热更行为、JS API、服务端协议都不变;状态转移仍在 state_core.cpp。

验证(本地)

  • C++:test-patch-core.sh(含 ASan/UBSan)新增 stored_items_test:12 个向量的计划、应用后的两个存储、再规划为空、任一前缀被杀后重入;test-update-flow-core.sh、test-core-platform-flags.sh 通过。
  • Android:NDK 28.2 重编 4 个 ABI 的 .so(verify-android-so 通过);JVM 单测全绿,新增 StoreMigrationTest(按向量回放迁移 IO、提交失败保留旧键、内核失败只搬 uuid、State 映射往返)。
  • iOS:test-ios-purge-restore.sh(VERIFY_REGRESSIONS=1)全绿,新增 stored_items_vectors(真实内核 + 真实 PushyMigrateStores)、legacy_migration_upgrade / _same_binary / _write_failure / _read_failure,负向对照 skip-carry、remove-first 均被抓到;全部 RCTPushy 源文件对 RN 头文件 -fsyntax-only 通过。
  • 鸿蒙:harmonyStateLifecycle.test.ts 按向量回放 migrateStore 并覆盖落盘失败、内核失败;DevEco clang 编译 pushy.cpp 通过;check-harmony-types 通过。
  • bun run lint(含生成器 --check)、bun test(473 个)、check-packlist 通过。
  • e2e 的 bundle-hash.test.ts 改为读新的 pushy_state 存储。

🤖 Generated with Claude Code


View with [code]smith Autofix with [code]smith
Need help on this PR? Tag @codesmith-bot with what you need.

Summary by CodeRabbit

  • Improvements
    • Existing update state is migrated to a shared storage format across Android, iOS, and Harmony, preserving recognized settings and identifiers.
    • Migration can be retried after an interruption, and existing data is retained if state writes fail.
    • Update state and response-cache data are stored separately; cached bundle hashes and update-round progress continue to be tracked.
  • Documentation
    • Added details about stored-state formats, migration behavior, and platform-specific legacy data.

Every platform now keeps the same keys with the same value encoding in two
stores of its own, pushy_state and pushy_cache (Android SharedPreferences
files, iOS NSUserDefaults suites, Harmony preferences): every value is a
string, booleans are "1"/"0", markers are "1" while set, and the state store
carries schemaVersion. scripts/contract.ts (storeSchema, stateFields,
storedItems, legacyLayouts) is the single source; the generator emits each
platform's key table and State <-> store mapping and the C++ legacy tables.

The one-time move out of the layouts SDKs up to 10.61 wrote is a shared pure
function, cpp/patch_core/stored_items.h MigrateStoredItems, pinned by
stored-items-vectors.json. It carries only what a binary upgrade keeps (uuid,
native config, round breadcrumb, bundle hash cache) plus the binary identity:
the new layout ships in a new binary, whose release buildTime always changes,
so its first launch resets the update state anyway. Puts end with the
schema, legacy keys are deleted only after the puts are durable, the store
always wins over the legacy value, and a store a later SDK wrote is left
alone. A migration that cannot run falls back to a fresh store that still
carries the uuid and retries on the next launch.

iOS moves off standardUserDefaults into its own suites: no collisions with
host keys, no wipe when the host clears its defaults domain.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 17, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Important

Review skipped

Review was skipped as selected files did not have any reviewable changes.

⛔ Files ignored due to path filters: 4
  • android/lib/arm64-v8a/librnpushy.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnpushy.so is excluded by !**/*.so
  • android/lib/x86/librnpushy.so is excluded by !**/*.so
  • android/lib/x86_64/librnpushy.so is excluded by !**/*.so
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: c42b9a67-df64-41ef-af4d-de687d0e09f0
📥 Commits

Reviewing files that changed from the base of the PR and between 983834b and 4354137.

⛔ Files ignored due to path filters: 4
  • android/lib/arm64-v8a/librnpushy.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnpushy.so is excluded by !**/*.so
  • android/lib/x86/librnpushy.so is excluded by !**/*.so
  • android/lib/x86_64/librnpushy.so is excluded by !**/*.so

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: defaults
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 5cdb8673-4b24-401b-9e84-daad1e0d40c2
📥 Commits

Reviewing files that changed from the base of the PR and between f3853ec and 983834b.

⛔ Files ignored due to path filters: 4
  • android/lib/arm64-v8a/librnpushy.so is excluded by !**/*.so
  • android/lib/armeabi-v7a/librnpushy.so is excluded by !**/*.so
  • android/lib/x86/librnpushy.so is excluded by !**/*.so
  • android/lib/x86_64/librnpushy.so is excluded by !**/*.so
📒 Files selected for processing: 43
  • Example/e2etest/e2e/bundle-hash.test.ts
  • android/src/main/java/cn/reactnative/modules/update/StoreMigration.java
  • android/src/main/java/cn/reactnative/modules/update/StoreSchema.java
  • android/src/main/java/cn/reactnative/modules/update/SyncCoordinator.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
  • android/src/main/java/cn/reactnative/modules/update/UpdateModule.java
  • android/src/main/java/cn/reactnative/modules/update/VersionScopedKeys.java
  • android/src/test/java/cn/reactnative/modules/update/StoreMigrationTest.java
  • android/src/test/java/cn/reactnative/modules/update/SyncRoundReplayTest.java
  • android/src/test/java/cn/reactnative/modules/update/VersionScopedKeysTest.java
  • cpp/android_jni/jni_natives.h
  • cpp/android_jni/jni_registration.cpp
  • cpp/android_jni/stored_items_jni.cpp
  • cpp/patch_core/native_texts.h
  • cpp/patch_core/stored_items.cpp
  • cpp/patch_core/stored_items.h
  • cpp/patch_core/tests/stored-items-vectors.json
  • cpp/patch_core/tests/stored_items_test.cpp
  • docs/STORED_STATE_LAYOUT.md
  • harmony/pushy/src/main/cpp/pushy.cpp
  • harmony/pushy/src/main/ets/NativePatchCore.ts
  • harmony/pushy/src/main/ets/PushyTurboModule.ts
  • harmony/pushy/src/main/ets/StoreSchema.ts
  • harmony/pushy/src/main/ets/SyncCoordinator.ts
  • harmony/pushy/src/main/ets/UpdateContext.ts
  • ios/RCTPushy/RCTPushy+Launch.mm
  • ios/RCTPushy/RCTPushy.mm
  • ios/RCTPushy/RCTPushyInternal.h
  • ios/RCTPushy/RCTPushyOrchestrator.mm
  • ios/RCTPushy/RCTPushyState.mm
  • scripts/contract.ts
  • scripts/generate-contract.ts
  • scripts/test-ios-purge-restore.sh
  • scripts/test-patch-core.sh
  • scripts/tests/ios-purge-restore/extract.py
  • scripts/tests/ios-purge-restore/purge_restore_test.mm
  • scripts/tests/ios-purge-restore/round_replay_test.mm
  • scripts/tests/ios-purge-restore/test_extract.py
  • src/__tests__/harmonyStateLifecycle.test.ts
  • src/__tests__/harmonySyncRound.test.ts
  • src/__tests__/nativeConfiguration.test.ts
  • src/__tests__/nativeHostApi.test.ts
  • src/__tests__/nativeSources.ts
💤 Files with no reviewable changes: 1
  • cpp/patch_core/native_texts.h

Included review availability: This review used your included allowance. Your plan provides up to 4 included reviews per hour; 3 remain after this review.

📝 Walkthrough

The pull request defines a shared persisted-store schema and migration planner for Android, iOS, and Harmony. It adds separate state and cache stores, migrates selected legacy values, updates platform code to use schema keys, and adds migration tests and documentation.

Changes: Shared schema and migration planner

Layer / File(s) Summary
Define the schema and migration planner
scripts/contract.ts, scripts/generate-contract.ts, cpp/patch_core/*, cpp/android_jni/*, harmony/pushy/src/main/cpp/pushy.cpp, harmony/pushy/src/main/ets/NativePatchCore.ts, cpp/patch_core/tests/*, scripts/test-patch-core.sh, docs/STORED_STATE_LAYOUT.md, Example/e2etest/e2e/bundle-hash.test.ts
The contract defines shared store keys, state fields, lifecycle cleanup, and legacy layouts. The C++ planner creates migration plans and exposes them through JNI and N-API. Tests and documentation cover the schema and migration behavior. The bundle-hash test reads bundleHashCache from pushy_state.
Android store integration
android/src/main/java/cn/reactnative/modules/update/*, android/src/test/java/cn/reactnative/modules/update/*
Android opens separate state and cache preferences and runs migration during construction. State access and update flows use schema-defined keys. Tests cover migration vectors, state mapping, retry, and fallback behavior.
iOS store integration
ios/RCTPushy/*, scripts/tests/ios-purge-restore/*, scripts/test-ios-purge-restore.sh
iOS uses separate pushy_state and pushy_cache suites, schema-based state helpers, and migration from prefixed legacy defaults. Tests cover migration vectors, launch behavior, and migration failures.
Harmony store integration
harmony/pushy/src/main/ets/*, src/__tests__/*
Harmony opens separate state and cache preferences and calls the native planner during migration. State and sync flows use schema-defined keys. Tests cover migration vectors, flush failures, fallback, and cache separation.

Priority: ➖ Normal

Estimated code review effort: 4 (Complex) | ~60 minutes

Change: Feature

Sequence Diagram(s)

sequenceDiagram
  participant UpdateContext
  participant StoreSchema
  participant NativePatchCore
  participant StatePreferences
  participant LegacyPreferences
  UpdateContext->>StoreSchema: migrateStore snapshots the stores
  StoreSchema->>NativePatchCore: send layout and flattened store entries
  NativePatchCore-->>StoreSchema: return the migration plan
  StoreSchema->>StatePreferences: apply planned writes and flush
  StoreSchema->>LegacyPreferences: remove planned keys and flush
  StoreSchema-->>UpdateContext: return migration status
Loading
Merge Risk: ⚪ Minimal · up to 98383

This change unifies persisted storage across Android, iOS and Harmony and migrates legacy values once. The supplied evidence shows no outstanding merge-blocking issue.

Pre-merge checks: Passed 4 · Failed 1

Failed Failed checks: 1 warning

Check name Status Explanation Resolution
Docstring Coverage Warning Docstring coverage is 26.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 140 functions across 34 files. (8 skipped… Write docstrings for the functions missing them to satisfy the coverage threshold.
Passed Passed checks: 4 passed
Check name Status Explanation
Description Check Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check Passed The title clearly and concisely describes the main change: a unified persistent storage format across Android, iOS, and Harmony with one-time migration.
Linked Issues check Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check Passed Check skipped because no linked issues were found for this pull request.
Full details: Docstring Coverage

Explanation

Docstring coverage is 26.43% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 140 functions across 34 files. (8 skipped: 8 unsupported.)

Resolution

Write docstrings for the functions missing them to satisfy the coverage threshold.

✨ Finishing Touches: 💡 1
📝 Generate docstrings: 💡
  • Commit to this branch
  • Create a new PR
🧪 Generate unit tests (beta)
  • Commit to this branch
  • Create a new PR
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@sunnylqm
sunnylqm merged commit 08dc697 into master Oct 17, 2026
16 checks passed
@sunnylqm
sunnylqm deleted the claude/stored-state-layout branch October 17, 2026 16:10
@sunnylqm sunnylqm mentioned this pull request Oct 18, 2026
sunnylqm added a commit that referenced this pull request Oct 18, 2026
Bump package.json and harmony/pushy/oh-package.json5 together, with the
bilingual release notes. Ships everything since 10.61.0: one persisted
layout on every platform with a one-time migration (#691), the UIScene life
cycle in the iOS examples (#692) and the CI actions bump (#690).

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant