feat: add resilient native cold-start update checks - #620
Conversation
…Core buildCheckRequestBody, resolveCheckResult, decideDownload, isInRollout, joinUrls and orderEndpointCandidates now live in src/updateFlowCore.ts with a pure import closure — no IO, no react-native, identity and randomness injected as parameters — so the exact same source can later be evaluated by the native orchestrator (NATIVE_CHECKUPDATE_DESIGN §5). client.ts, provider.tsx and endpoint.ts consume it in place; the endpoint candidate ordering (random first pick, configured order as fallback) moves into the pure layer while the hedged-race engine stays IO-side. Behavior is unchanged; isInRollout tests no longer need module mocks. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…xt guardian, no override channel Three decisions land in the design: (1) buildCheckRequest returns a declarative endpoint plan and the native engine is a sequential fallback, not a port of the JS hedged race — the cold-start background path is latency-insensitive; (2) the guardian evaluates as plain-text JS source, never HBC, removing the bytecode-version coupling and narrowing the engine spike; (3) the remote override channel (§5.3-5.5) is cut — the decision layer is the most testable part of the pipeline, the JS-side checkUpdate remains a full fallback and the server can already route around decision bugs by shaping responses, so the premium exceeds the exposure. Phase status in §8 updated: protocol prerequisite lifted, pure-function extraction done (updateFlowCore.ts). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…latform The bundled updateFlowCore (6.4KB plain-text JS, zero deps) produces byte-identical correct output under bare JSC, Hermes (RN 0.73 VM, source eval, no HBC) and V8, and a 40-line ObjC host evaluates it via JSContext and calls decideDownload end to end — the iOS orchestrator shape, linking only system frameworks. Since the source is the shared artifact, the engine is now chosen per platform: iOS system JavaScriptCore (zero risk, proven), Harmony system JSVM-API, Android remains the only open spike (javascriptengine / Hermes linking / QuickJS; worst case Android alone falls back to a C++ decision layer as a hybrid). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Symbol recon on the prebuilt hermes-android 250829098.0.10 (RN 0.85): the stable C ABI header (hermes_abi.h) ships in the prefab but its entry point get_hermes_abi_vtable is not exported from libhermesvm.so; only the C++ makeHermesRuntime is, which couples the caller to the host's exact RuntimeConfig layout and requires compiling a matching jsi.cpp (zero JSI symbols exported). For a prebuilt librnupdate.so shipped across RN versions that is an unmanageable matrix. Android candidates narrow to androidx.javascriptengine (needs on-device verification) and QuickJS; the route revives if upstream ever exports the stable C ABI. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…arity cpp/update_flow_core is a 1:1 port of src/updateFlowCore.ts (the reference implementation): Murmur3_32, IsInRollout, JoinUrls, OrderEndpointCandidates, BuildCheckRequestBody, ResolveCheckResult and DecideDownload, built on a minimal flow_json DOM that mirrors the JS semantics parity depends on — insertion-ordered objects (overwrite keeps position), undefined distinct from null and skipped by stringify, JS truthiness and strict equality. Parity is a mechanically enforced contract, not a convention: scripts/generate-flow-vectors.ts runs the TS reference over a dense input matrix and writes cpp/update_flow_core/tests/flow_vectors.json (69 cases); src/__tests__/flowVectors.test.ts fails when the TS side and the committed vectors disagree, and scripts/test-update-flow-core.sh (wired into the CI cpp-test job under ASan+UBSan) replays the same vectors against the C++ port. Semantic changes land TS-first, regenerate the vectors, then port. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…tforms Records the reasoning chain that closes the runtime-selection question: engines were only ever a means to a single source of truth; with the host Hermes ruled out and javascriptengine only viable as a runtime-conditional optimization, an always-available C++ implementation must exist on Android anyway, at which point uniform C++ beats both the hybrid (two mechanisms) and QuickJS-everywhere (200KB x 4 to save one mechanical, vector-guarded edit). The single-source property is preserved as "one semantics, mechanically enforced" via the golden-vector contract. The guardian chapter is demoted to a rejected-route record; remaining work is §8 step 4 (native orchestration). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
The parser also consumes checkUpdate responses, so malformed input must fail cleanly: recursive descent is now capped at 64 nesting levels (a 100k-bracket bomb previously walked straight toward the stack limit), and the test binary gains a robustness suite — 20 malformed inputs that must set ok=false without crashing (verified under ASan+UBSan in CI), plus tricky-but-valid cases pinning JSON.parse semantics the port relies on (duplicate keys keep first position with last value, surrogate-pair decoding, canonical number reserialization). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
…§10)
HandleCheckResponse composes Parse → ResolveCheckResult → DecideDownload
so platform orchestrators go from raw response text to a download decision
(with the resolved info attached for setLocalHashInfo) in one call,
containing zero decision logic; covered by nine composition cases in the
C++ test suite under ASan+UBSan.
The design doc gains §10: the provisioning gap surfaced by step 4 — JS is
the single config source, syncNativeConfig persists {appKey, endpoints,
queryUrls, afterDownload, rnu, rn} and missing config means the native
check silently does not run (which doubles as the rollout gate); the
cold-start flow, JS result-reuse handoff, bounded failure policy (no
retry storms, no version blacklisting — the local-watchdog lesson), and
the per-platform landing order (iOS first).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
… check
First brick of NATIVE_CHECKUPDATE_DESIGN §10: the native check runs before
any JS, so its config must already be on disk. JS is the single config
source — after every setOptions the client persists {appKey, endpoints,
queryUrls, afterDownload, rnu, rn} through the new syncNativeConfig native
method (feature-detected, fire-and-forget, skipped on web). afterDownload
folds updateStrategy: silent strategies let the native side activate a
downloaded version for the next launch, alert strategies keep activation
with JS. Absent persisted config disables the native check entirely, which
doubles as the rollout gate: old integrations and first launches see zero
behavior change, and shipping this ahead of the orchestrator lets devices
accumulate config in the field.
All three platforms store the raw JSON (parsed on read by the future
orchestrator) and validate it at write time — a corrupt config would
otherwise silently disable the native check with no signal. Verified:
bun test (155) + tsc + biome, harmony tsc against the DevEco SDK, clang
-fsyntax-only for RCTPushy.mm against e2e Pods, javac of main+oldarch
against the RN 0.85 AAR (newarch mirrors setUuid; covered by CI codegen
builds).
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
RCTPushyOrchestrator implements NATIVE_CHECKUPDATE_DESIGN §10 on iOS: once per process, five seconds after +bundleURL (the integration-guaranteed anchor), a utility-QoS thread reads the JS-provisioned config (absent = check disabled), builds the request through cpp/update_flow_core, walks the ordered endpoints sequentially with per-request timeouts (queryUrls discovery merges remote candidates after a failed round — no hedged race on this latency-insensitive path), and hands the raw response to HandleCheckResponse. A download decision drives the existing pipeline through a bare bridge-less module instance (performUpdate; progress events are hasListeners-gated), trying each attempt's candidate URLs in order: diff → pdiff → full. On success the version info persists like setLocalHashInfo, and under the silent strategies (afterDownload: setNeedUpdate) the version activates for the next launch — which is the whole point: a device bricked by a bad hot update pulls the fixed version before any JS runs. The raw response is cached with a timestamp for the JS-side reuse step (§10.3). Failures are silent and bounded: one round per launch, no retry storms, no version blacklisting. getBundleHash's compute is extracted into PushyBundleHashSync, shared with the orchestrator's request body. cpp/update_flow_core joins the podspec. Verified with clang -fsyntax-only against the e2e Pods in both DEBUG=0 and DEBUG=1 shapes. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
NativeCheckOrchestrator mirrors the iOS orchestrator on Android (NATIVE_CHECKUPDATE_DESIGN §10): once per process, five seconds after getBundleUrl on a low-priority daemon thread, gated on the JS-provisioned config (absent = disabled) and on BuildConfig.DEBUG. Decisions come from cpp/update_flow_core through the new NativeUpdateFlow JNI surface (string-in/string-out JSON: buildCheckRequestBody, orderEndpointCandidates, handleCheckResponse), compiled into librnupdate.so — all four prebuilt ABIs rebuilt and re-verified (exported symbols including the three new ones, 16KB page alignment). IO reuses what exists: OkHttp for the sequential endpoint fallback with queryUrls discovery, DownloadTask via UpdateContext.downloadPatchFromPpk/downloadPatchFromApk/downloadFullUpdate for diff → pdiff → full attempts, switchVersion for next-launch activation under the silent strategies, and the raw response cached in SharedPreferences for JS reuse (§10.3). computeBundleHash becomes package-private to feed the request body. Verified: javac of main+oldarch against the RN 0.85 AAR + okhttp classpath. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Completes the third platform of NATIVE_CHECKUPDATE_DESIGN §10. NativeCheckOrchestrator.ts mirrors the iOS/Android orchestrators: once per process, five seconds after getBundleUrl (dev never reaches it — Metro's provider bypasses PushyFileJSBundleProvider, which doubles as the debug gate), reads the JS-provisioned config, and drives the flow through the decision layer now compiled into librnupdate: flow_json + update_flow_core join the CMake build and three NAPI exports (buildCheckRequestBody, orderEndpointCandidates, handleCheckResponse) join pushy.cpp, string-in/ string-out JSON. IO reuses @ohos.net.http for the sequential endpoint fallback with queryUrls discovery and UpdateContext's download/switch pipeline for diff → pdiff → full attempts; the raw response caches via setKv for JS reuse (§10.3). UpdateContext gains hasDownloadedVersion so an already-downloaded, not-yet-activated version is not re-downloaded every launch under alert strategies. The har staging script now stages cpp/update_flow_core alongside patch_core. Verified: DevEco tsc over the ets sources and the OHOS llvm toolchain syntax pass over pushy.cpp. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the double-check window between the native cold-start check and the JS-side checkUpdate: getNativeCheckCache (all three platforms + spec) exposes the response the orchestrator persisted, and the JS client reuses it when fresher than two minutes instead of issuing its own request. The cache read happens inside the shared response promise, so no await lands between the dedup window and the lastRespJson assignment (the JS2-1 double-send lesson); absent, stale, or unreadable caches fall through to a normal network check, and dev builds skip the path entirely. Covered by three client tests (fresh reuse without network, stale fallthrough, unreadable cache); full suite 158 green, harmony tsc, clang syntax pass and javac all re-verified. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
All three orchestrators, the provisioning layer and the §10.3 response cache reuse have landed; what remains is e2e coverage (the bricked-bundle end-to-end scenario), README/CHANGELOG and a release. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Closes the last gap in the brick rescue: under alert-style strategies (the
default) the native check downloaded the fix but never activated it, and a
bricked device's JS can never do so. The server can now mark a version
config.forceBoot ("boot into this version next launch") and the native
orchestrator activates it regardless of the client's strategy.
Activation policy folds into the pure layer: shouldActivateAfterDownload
(afterDownload === 'setNeedUpdate' || info.config.forceBoot, JS truthiness)
lands in updateFlowCore + the C++ port with 8 new golden vectors (77
total), and HandleCheckResponse gains the afterDownload parameter and
emits `activate` on download decisions — all three orchestrators now read
that single boolean instead of comparing strategy strings. Deliberate
semantics, pinned by composition tests: native-only (the JS interactive
flow is not consulted), the device-local rolledBack guard wins over
forceBoot, and first_time crash protection still applies to the forced
version. librnupdate.so rebuilt for all four ABIs with the new JNI
signature (symbols + 16KB alignment verified); JS suite (158), harmony
tsc, OHOS clang, iOS clang and javac all green. Server-side counterpart
(root-version config passthrough + console toggle) tracked in §10.7.
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
versions.config belongs to the retired gray-release design and is being actively scrubbed by the binding transaction; the first draft that revived it through the binding branches was reverted (legacy rollout leakage, dual config sources, working against the scrub). forceBoot is a property of a delivery, so it lives on bindings.config — the per-delivery slot the upsert API already plumbs — which also self-clears on rebinding and scopes the directive to a single packageVersion. Client protocol unchanged: the response config stays synthesized from binding data. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
Note Reviews pausedIt looks like this branch is under active development. To avoid overwhelming you with review comments due to an influx of new commits, CodeRabbit has automatically paused this review. You can configure this behavior by changing the Use the following commands to manage reviews:
Use the checkboxes below for quick actions:
📝 WalkthroughWalkthroughThe PR adds shared TypeScript and C++ update-flow decisions, native cold-start orchestration for iOS, Android, and Harmony, configuration synchronization, response-cache reuse, bounded downloads, completion markers, and cross-language parity tests. ChangesUnified native update flow
Estimated code review effort: 5 (Critical) | ~120 minutes Sequence Diagram(s)sequenceDiagram
participant JavaScriptClient
participant NativeUpdateFlow
participant NativeOrchestrator
participant DownloadTask
participant NativeStorage
JavaScriptClient->>NativeStorage: syncNativeConfig
NativeOrchestrator->>NativeUpdateFlow: build request and handle response
NativeOrchestrator->>DownloadTask: execute ordered download plan
DownloadTask->>NativeStorage: write completion marker
NativeOrchestrator->>NativeStorage: persist response cache
JavaScriptClient->>NativeStorage: getNativeCheckCache
Possibly related PRs
🚥 Pre-merge checks | ✅ 4 | ❌ 1❌ Failed checks (1 warning)
✅ Passed checks (4 passed)
✨ Finishing Touches 💡 1📝 Generate docstrings 💡
🧪 Generate unit tests (beta)
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. Comment |
There was a problem hiding this comment.
Actionable comments posted: 8
🧹 Nitpick comments (15)
ios/RCTPushy/RCTPushy.mm (1)
1470-1511: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueConsider a bound on the total number of endpoint attempts.
Each attempt blocks for up to 15 seconds (10 s request timeout plus the 5 s semaphore backstop). The configured
endpointslist and the discovered remote list are both unbounded, so a long list keeps the utility thread busy for minutes. A simple attempt counter, or a total deadline for the whole round, keeps the cold-start check bounded.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/RCTPushy/RCTPushy.mm` around lines 1470 - 1511, Bound the total endpoint attempts in the update-check flow by adding a shared counter or overall deadline across both loops over config.Get("endpoints") and remote.elements(). Stop processing when the limit is reached, while preserving deduplication via tried and returning immediately for the first valid PushyIsValidCheckResponse result.harmony/pushy/src/main/ets/PushyTurboModule.ts (1)
189-205: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueReuse the orchestrator key constants instead of string literals.
NativeCheckOrchestrator.tsdefinesKEY_CONFIG = 'nativeConfig'andKEY_RESP_CACHE = 'nativeCheckResp'. This module repeats both literals. A rename in one file then silently breaks the read/write pairing. The Android side avoids this by referencingNativeCheckOrchestrator.KEY_RESP_CACHE. Export the two constants fromNativeCheckOrchestrator.tsand import them here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@harmony/pushy/src/main/ets/PushyTurboModule.ts` around lines 189 - 205, Export KEY_CONFIG and KEY_RESP_CACHE from NativeCheckOrchestrator.ts, then import and use them in syncNativeConfig and getNativeCheckCache instead of the 'nativeConfig' and 'nativeCheckResp' string literals. Preserve the existing persistence and fallback behavior.scripts/build-harmony-har.js (1)
242-245: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winAdd an
ensureFileExistsprecondition for the update-flow core.Every other staged native source in
syncHarmonyNativeSourceshas anensureFileExistscheck before the copy block, includingpatch_core.cppat lines 213-218. The newupdate_flow_corecopy has none.If
cpp/update_flow_core/update_flow_core.cppis absent or renamed, this script stages an incomplete directory.harmony/pushy/src/main/cpp/CMakeLists.txtthen falls back to the staged path at lines 20-23 and the build fails later with a compiler or linker error. An explicit guard reports the real cause here.♻️ Proposed change
ensureFileExists( path.join(patchCoreDir, 'patch_core.cpp'), `Missing shared patch core source: ${relativeToProject( path.join(patchCoreDir, 'patch_core.cpp'), )}`, ); + ensureFileExists( + path.join(updateFlowCoreDir, 'update_flow_core.cpp'), + `Missing shared update flow core source: ${relativeToProject( + path.join(updateFlowCoreDir, 'update_flow_core.cpp'), + )}`, + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/build-harmony-har.js` around lines 242 - 245, Add an ensureFileExists precondition for cpp/update_flow_core/update_flow_core.cpp in syncHarmonyNativeSources before the copyPath call targeting updateHarmonyNativeStageDir/update_flow_core, matching the existing patch_core.cpp guard pattern. Keep the existing copy operation unchanged.harmony/pushy/src/main/ets/DownloadTask.ts (1)
15-16: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winThe
.pushy-completemarker filename has no shared definition. The writer and the reader each carry their own copy of the literal. If one copy changes,hasDownloadedVersionreturnsfalsefor every completed version and the native orchestrator re-downloads an already-ready update on every cold start. Android avoids this with the singleUpdateContext.VERSION_COMPLETE_FILEconstant.
harmony/pushy/src/main/ets/DownloadTask.ts#L15-L16: exportVERSION_COMPLETE_FILE_NAMEinstead of keeping it module-private.harmony/pushy/src/main/ets/UpdateContext.ts#L539-L550: import the exported constant and build the marker path from it instead of the inline.pushy-completeliteral.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@harmony/pushy/src/main/ets/DownloadTask.ts` around lines 15 - 16, Export VERSION_COMPLETE_FILE_NAME from DownloadTask.ts, then import and reuse it in UpdateContext.ts when constructing the completed-version marker path, replacing the inline '.pushy-complete' literal in the relevant marker lookup.harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts (1)
180-183: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winValidate
hashas a safe path component here, like Android does.This code only rejects an empty
hash. The Android orchestrator callsUpdateContext.isSafePathComponent(hash)at the same point and returns early.Path safety is still preserved on Harmony:
hasDownloadedVersionwrapsassertSafePathComponentin a try/catch, andcreateTaskParamsthrows before any path is built. So a hostilehashcannot escaperootDir. The observable difference is diagnostics and wasted work: an unsafehashproduces one throw per candidate URL insideperformAttempts, each logged only at debug level, and the round ends with a generic "not downloaded" outcome.Add the explicit check so the reason is logged once and the platforms behave the same.
♻️ Proposed change
const hash = decision.hash ?? ''; - if (!hash) { + if (!hash || !isSafePathComponent(hash)) { + logger.debug(TAG, `rejected hash from decision: ${hash}`); return; }Export a non-throwing
isSafePathComponentpredicate next to the existingassertSafePathComponenthelper and import it here.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts` around lines 180 - 183, Validate the resolved hash in the Harmony orchestrator using a non-throwing path-safety predicate before continuing past the empty-hash guard. Export that predicate alongside assertSafePathComponent, import it into NativeCheckOrchestrator, and return early through the existing diagnostic path when the hash is unsafe, matching Android’s behavior.android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java (2)
218-221: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick winAdd a call timeout to the check client.
connectTimeoutandreadTimeoutbound single socket operations only. A server that sends one byte at a time keeps the connection open indefinitely, so this thread never reaches the next endpoint.DownloadTask.HTTP_CLIENTalready setscallTimeoutfor the same reason.♻️ Proposed change
private static final OkHttpClient httpClient = new OkHttpClient.Builder() .connectTimeout(10, TimeUnit.SECONDS) .readTimeout(10, TimeUnit.SECONDS) + .callTimeout(30, TimeUnit.SECONDS) .build();🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java` around lines 218 - 221, Add a finite callTimeout to the OkHttpClient builder in NativeCheckOrchestrator’s httpClient, matching the timeout configuration used by DownloadTask.HTTP_CLIENT, while preserving the existing connectTimeout and readTimeout settings.
319-387: 🚀 Performance & Scalability | 🔵 Trivial | ⚖️ Poor tradeoffBound the whole attempt loop, not each attempt.
Each URL gets its own 600-second latch. A decision that carries several attempts with several URLs each can keep this thread and the single-threaded download executor busy for a very long time. The executor is shared with JavaScript-triggered downloads, so those queue behind it.
Consider a single deadline for
performAttemptsand pass the remaining time tolatch.await.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java` around lines 319 - 387, Update performAttempts to establish one overall deadline for the entire attempts-and-URLs loop, rather than resetting the 600-second timeout for each URL. Before each latch.await call, calculate the remaining time and await only that duration; return false when the deadline is exhausted, while preserving success, interruption, and existing download behavior.android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java (1)
266-273: 📐 Maintainability & Code Quality | 🔵 Trivial | ⚡ Quick winUse
NativeCheckOrchestrator.KEY_CONFIGinstead of the string literal.
getNativeCheckCacheon line 236 reads throughNativeCheckOrchestrator.KEY_RESP_CACHE. This writer hardcodes"nativeConfig". The reader isNativeCheckOrchestrator.runOnce, which usesKEY_CONFIG. A rename of the constant would silently disable the native check.♻️ Proposed change
public void run() { - updateContext.setKv("nativeConfig", config); + updateContext.setKv(NativeCheckOrchestrator.KEY_CONFIG, config); promise.resolve(true); }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java` around lines 266 - 273, Update the native configuration write inside the StateSerialRunner operation to use NativeCheckOrchestrator.KEY_CONFIG instead of the hardcoded "nativeConfig" key, keeping the existing promise resolution behavior unchanged.android/src/main/java/cn/reactnative/modules/update/UpdateContext.java (1)
566-566: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUse one source for the rolled-back version snapshot.
Line 566 passes
launchState.rolledBackVersion. Lines 582 and 586 passrolledBackVersion(), which re-reads SharedPreferences. Both values agree today, becauseapplyStatepersists the launch state wheneverdidRollbackis true. The two forms still express different assumptions, and a future change to the persist condition would make them diverge silently.Read
launchState.rolledBackVersioninto a local variable once and pass it at all three call sites.Also applies to: 582-587
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java` at line 566, In the update flow surrounding NativeCheckOrchestrator.schedule, capture launchState.rolledBackVersion in a local variable once, then pass that variable at the call sites currently using launchState.rolledBackVersion and rolledBackVersion(). Remove the repeated SharedPreferences read while preserving the existing scheduling behavior.src/client.ts (1)
267-267: 🚀 Performance & Scalability | 🔵 Trivial | 💤 Low valueSkip the native write when the serialized config did not change.
setOptionsruns on every option update, including each re-created client and each provider-driven option change.syncNativeConfigthen issues a bridge call and a native key-value write every time, even when the serialized config is identical. On Android the write goes through a serialized file operation.Cache the last successfully synced JSON string and compare before writing.
♻️ Proposed refactor
+ private lastSyncedConfigJson?: string; + private syncNativeConfig = () => { const configJson = this.getNativeConfigJson(); - if (!configJson) { + if (!configJson || configJson === this.lastSyncedConfigJson) { return; } + this.lastSyncedConfigJson = configJson; Promise.resolve(PushyModule.syncNativeConfig(configJson)).catch( (e: any) => { + this.lastSyncedConfigJson = undefined; log('syncNativeConfig failed:', e?.message || e); } ); };Also applies to: 300-310
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/client.ts` at line 267, Update the config synchronization flow around syncNativeConfig and its call sites in setOptions to cache the last successfully synced serialized JSON string, skip the native bridge/write when the new serialization matches it, and update the cache only after a successful sync.NATIVE_CHECKUPDATE_DESIGN.md (1)
315-318: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAdd a language to the fenced code blocks.
markdownlint reports MD040 for the blocks at Line 315 and Line 332. Both blocks contain pseudo-code. Use ```text to satisfy the rule.
📝 Proposed fix
-``` +```text { appKey, endpoints: server.main, queryUrls, afterDownload: 'none' | 'setNeedUpdate', disabled?: boolean, rnu, rn }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@NATIVE_CHECKUPDATE_DESIGN.md` around lines 315 - 318, Update the fenced pseudo-code blocks in NATIVE_CHECKUPDATE_DESIGN.md to specify the text language, including the blocks containing the appKey configuration and the corresponding block near the later referenced section. Use text fences while preserving their contents.Source: Linters/SAST tools
scripts/test-update-flow-core.sh (1)
11-25: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueSilence the intentional word splitting for shellcheck.
$SANITIZE_FLAGSmust split into separate compiler arguments, so the missing quotes are correct here. POSIXshhas no arrays, so the split is the only option. Add a directive so SC2086 does not stay in the lint output.♻️ Proposed change
+# shellcheck disable=SC2086 # SANITIZE_FLAGS must split into separate args c++ \ -std=c++17 \SC1007 on Line 4 is a false positive.
CDPATH= cd -- ...is the intended idiom for a one-command environment assignment.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/test-update-flow-core.sh` around lines 11 - 25, Add a shellcheck directive adjacent to the intentional $SANITIZE_FLAGS expansion in the compiler invocation to suppress SC2086, while preserving its unquoted word splitting. Also suppress SC1007 for the existing CDPATH= cd -- idiom on the relevant line without changing either command’s behavior.Source: Linters/SAST tools
cpp/update_flow_core/flow_json.cpp (1)
443-469: 🗄️ Data Integrity & Integration | 🔵 Trivial | 💤 Low value
ParseNumberis more lenient thanJSON.parse.The scanner collects any run of digits,
.,e,E,+,-, then delegates tostrtod.strtodaccepts forms that RFC 8259 andJSON.parsereject, for example1.,01, and.5. The header describes the parser as strict. The practical effect is a divergence: a malformed response can produce a native decision while the JavaScript path throws on the same bytes.The risk is small because the server produces the responses. If you want the two paths to agree on rejection, validate the token shape before
strtod.♻️ Optional: token-shape validation
char* end = nullptr; std::string token = text_.substr(start, pos_ - start); + // JSON grammar: -?(0|[1-9][0-9]*)(\.[0-9]+)?([eE][+-]?[0-9]+)? + if (!IsJsonNumberToken(token)) { + ok_ = false; + return Value::Undefined(); + } double n = std::strtod(token.c_str(), &end);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/update_flow_core/flow_json.cpp` around lines 443 - 469, Make ParseNumber enforce strict JSON number syntax before converting the token: require an optional leading minus, a zero or nonzero digit sequence without leading zeros, an optional fractional part containing at least one digit, and an optional exponent with an optional sign and at least one digit. Reject malformed shapes such as 1., 01, and .5 by setting ok_ false and returning Value::Undefined(), while preserving valid-number conversion through strtod.src/updateFlowCore.ts (1)
200-219: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winGuard the
upToDatecomparisons against missing hashes.Line 203 and line 217 compare optional values. If both sides are
undefined, the strict comparison succeeds and the function reportsupToDate. A malformed server payload that omitshashthen suppresses the update instead of falling through. The native orchestrator replays this decision before JavaScript starts, so the suppression also applies to the cold-start rescue path.The generator pins this behavior in a golden vector, so change the vectors together with the code if you accept the guard.
♻️ Proposed guard
- if (expVersion.hash === identity.currentVersion) { + if (expVersion.hash && expVersion.hash === identity.currentVersion) { return { upToDate: true }; } @@ - if (rootResult.update && rootResult.hash === identity.currentVersion) { + if ( + rootResult.update && + rootResult.hash && + rootResult.hash === identity.currentVersion + ) { return { upToDate: true }; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@src/updateFlowCore.ts` around lines 200 - 219, Guard both upToDate comparisons in the update flow around expVersion.hash and rootResult.hash so equality is evaluated only when the relevant hash is present. Ensure missing hashes fall through to normal update handling rather than returning upToDate, and update the generator golden vectors if required by the changed behavior.scripts/generate-flow-vectors.ts (1)
279-290: 🎯 Functional Correctness | 🔵 Trivial | ⚡ Quick winConvert the file URL with
fileURLToPathinstead of usingpathname.
URL.pathnamekeeps percent-encoding for file system paths. If the repository path contains a space or non-ASCII character,Bun.write(...)can target the wrong path. UseBun.fileURLToPath(new URL(...))for this destination.♻️ Proposed fix
- const outPath = new URL( - '../cpp/update_flow_core/tests/flow_vectors.json', - import.meta.url - ).pathname; + const outPath = Bun.fileURLToPath( + new URL('../cpp/update_flow_core/tests/flow_vectors.json', import.meta.url) + );🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@scripts/generate-flow-vectors.ts` around lines 279 - 290, Update the destination path construction in the import.meta.main block to convert the file URL with Bun.fileURLToPath(new URL(...)) instead of reading URL.pathname, then continue passing the decoded filesystem path to Bun.write and the existing log.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 261-317: Normalize endpoint base URLs before constructing
check-update requests to prevent duplicate slashes. Update runCheckRequest’s
ordered and remote endpoint handling so each base is canonicalized before
deduplication and concatenation, or apply the normalization in the client
persistence path; preserve existing endpoint ordering and fallback behavior.
In `@cpp/update_flow_core/tests/update_flow_core_test.cpp`:
- Around line 276-306: Validate that the `cases` value loaded before the vector
loop is an array and non-empty before iterating. If the shape is invalid or
empty, report the failure, increment `failures`, and preserve the existing
nonzero exit path so `RunParserRobustness()` and `RunHandleCheckResponse()`
results are still included.
In `@cpp/update_flow_core/update_flow_core.cpp`:
- Around line 130-134: Update the random index calculation around randomSample
to validate and clamp non-finite or out-of-range values before converting to
size_t. Ensure NaN, infinity, and values producing an index outside the valid
range resolve to the same safe boundary behavior as the existing first
calculation, while preserving normal sampling for finite in-range values.
In `@cpp/update_flow_core/update_flow_jni.cpp`:
- Around line 15-17: Update ToJString to decode the UTF-8 produced by Stringify
into UTF-16 jchars and call env->NewString with the decoded characters and
length; do not pass the raw std::string to NewStringUTF, preserving correct
handling of non-ASCII code points such as emoji.
In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1259-1272: Update the request task flow around the dataTask
completion handler and dispatch_semaphore_wait: retain the created
NSURLSessionDataTask, check that response is an NSHTTPURLResponse before reading
statusCode, and only return result after a successful semaphore wait. When the
wait times out, cancel the task and return without reading the handler-written
result; keep the completion handler’s result local to the successful completion
path to avoid a timeout race.
In `@NATIVE_CHECKUPDATE_DESIGN.md`:
- Line 111: Update both references in NATIVE_CHECKUPDATE_DESIGN.md at lines 111
and 284: replace the stale “69” counts for generated/golden vectors with the
current total of 77 test cases across 8 functions, or remove the numeric claims
and refer to the generated vector set.
In `@src/client.ts`:
- Around line 280-298: Introduce one client accessor for the effective package
version, using overridePackageVersion when present and packageVersion otherwise,
and reuse it wherever package identity is sent or resolved. In
src/client.ts:280-298, include that value in getNativeConfigJson; in
src/provider.tsx:226-230, pass the client’s effective version to
resolveCheckResult instead of the imported packageVersion. Ensure both sites use
the same accessor and no direct raw-version lookup remains there.
In `@src/updateFlowCore.ts`:
- Around line 261-299: Update decideDownload so a non-development release with
no resolved artifact URLs does not return a download plan with attempts: [];
return the appropriate none decision (or otherwise fail before execution) while
preserving devNoop behavior for development builds. Document or enforce the
empty-plan handling in the executors that consume DownloadDecision, using
decideDownload and the relevant execution symbols.
---
Nitpick comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 218-221: Add a finite callTimeout to the OkHttpClient builder in
NativeCheckOrchestrator’s httpClient, matching the timeout configuration used by
DownloadTask.HTTP_CLIENT, while preserving the existing connectTimeout and
readTimeout settings.
- Around line 319-387: Update performAttempts to establish one overall deadline
for the entire attempts-and-URLs loop, rather than resetting the 600-second
timeout for each URL. Before each latch.await call, calculate the remaining time
and await only that duration; return false when the deadline is exhausted, while
preserving success, interruption, and existing download behavior.
In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java`:
- Line 566: In the update flow surrounding NativeCheckOrchestrator.schedule,
capture launchState.rolledBackVersion in a local variable once, then pass that
variable at the call sites currently using launchState.rolledBackVersion and
rolledBackVersion(). Remove the repeated SharedPreferences read while preserving
the existing scheduling behavior.
In `@android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java`:
- Around line 266-273: Update the native configuration write inside the
StateSerialRunner operation to use NativeCheckOrchestrator.KEY_CONFIG instead of
the hardcoded "nativeConfig" key, keeping the existing promise resolution
behavior unchanged.
In `@cpp/update_flow_core/flow_json.cpp`:
- Around line 443-469: Make ParseNumber enforce strict JSON number syntax before
converting the token: require an optional leading minus, a zero or nonzero digit
sequence without leading zeros, an optional fractional part containing at least
one digit, and an optional exponent with an optional sign and at least one
digit. Reject malformed shapes such as 1., 01, and .5 by setting ok_ false and
returning Value::Undefined(), while preserving valid-number conversion through
strtod.
In `@harmony/pushy/src/main/ets/DownloadTask.ts`:
- Around line 15-16: Export VERSION_COMPLETE_FILE_NAME from DownloadTask.ts,
then import and reuse it in UpdateContext.ts when constructing the
completed-version marker path, replacing the inline '.pushy-complete' literal in
the relevant marker lookup.
In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts`:
- Around line 180-183: Validate the resolved hash in the Harmony orchestrator
using a non-throwing path-safety predicate before continuing past the empty-hash
guard. Export that predicate alongside assertSafePathComponent, import it into
NativeCheckOrchestrator, and return early through the existing diagnostic path
when the hash is unsafe, matching Android’s behavior.
In `@harmony/pushy/src/main/ets/PushyTurboModule.ts`:
- Around line 189-205: Export KEY_CONFIG and KEY_RESP_CACHE from
NativeCheckOrchestrator.ts, then import and use them in syncNativeConfig and
getNativeCheckCache instead of the 'nativeConfig' and 'nativeCheckResp' string
literals. Preserve the existing persistence and fallback behavior.
In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1470-1511: Bound the total endpoint attempts in the update-check
flow by adding a shared counter or overall deadline across both loops over
config.Get("endpoints") and remote.elements(). Stop processing when the limit is
reached, while preserving deduplication via tried and returning immediately for
the first valid PushyIsValidCheckResponse result.
In `@NATIVE_CHECKUPDATE_DESIGN.md`:
- Around line 315-318: Update the fenced pseudo-code blocks in
NATIVE_CHECKUPDATE_DESIGN.md to specify the text language, including the blocks
containing the appKey configuration and the corresponding block near the later
referenced section. Use text fences while preserving their contents.
In `@scripts/build-harmony-har.js`:
- Around line 242-245: Add an ensureFileExists precondition for
cpp/update_flow_core/update_flow_core.cpp in syncHarmonyNativeSources before the
copyPath call targeting updateHarmonyNativeStageDir/update_flow_core, matching
the existing patch_core.cpp guard pattern. Keep the existing copy operation
unchanged.
In `@scripts/generate-flow-vectors.ts`:
- Around line 279-290: Update the destination path construction in the
import.meta.main block to convert the file URL with Bun.fileURLToPath(new
URL(...)) instead of reading URL.pathname, then continue passing the decoded
filesystem path to Bun.write and the existing log.
In `@scripts/test-update-flow-core.sh`:
- Around line 11-25: Add a shellcheck directive adjacent to the intentional
$SANITIZE_FLAGS expansion in the compiler invocation to suppress SC2086, while
preserving its unquoted word splitting. Also suppress SC1007 for the existing
CDPATH= cd -- idiom on the relevant line without changing either command’s
behavior.
In `@src/client.ts`:
- Line 267: Update the config synchronization flow around syncNativeConfig and
its call sites in setOptions to cache the last successfully synced serialized
JSON string, skip the native bridge/write when the new serialization matches it,
and update the cache only after a successful sync.
In `@src/updateFlowCore.ts`:
- Around line 200-219: Guard both upToDate comparisons in the update flow around
expVersion.hash and rootResult.hash so equality is evaluated only when the
relevant hash is present. Ensure missing hashes fall through to normal update
handling rather than returning upToDate, and update the generator golden vectors
if required by the changed behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 737dfbd2-4374-4af0-a965-d3447fcc3c1f
⛔ Files ignored due to path filters (4)
android/lib/arm64-v8a/librnupdate.sois excluded by!**/*.soandroid/lib/armeabi-v7a/librnupdate.sois excluded by!**/*.soandroid/lib/x86/librnupdate.sois excluded by!**/*.soandroid/lib/x86_64/librnupdate.sois excluded by!**/*.so
📒 Files selected for processing (46)
.github/workflows/test.ymlNATIVE_CHECKUPDATE_DESIGN.mdandroid/jni/Android.mkandroid/src/main/java/cn/reactnative/modules/update/DownloadTask.javaandroid/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.javaandroid/src/main/java/cn/reactnative/modules/update/NativeUpdateFlow.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateContext.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.javaandroid/src/newarch/cn/reactnative/modules/update/UpdateModule.javaandroid/src/oldarch/cn/reactnative/modules/update/UpdateModule.javacpp/update_flow_core/flow_json.cppcpp/update_flow_core/flow_json.hcpp/update_flow_core/tests/flow_vectors.jsoncpp/update_flow_core/tests/update_flow_core_test.cppcpp/update_flow_core/update_flow_core.cppcpp/update_flow_core/update_flow_core.hcpp/update_flow_core/update_flow_jni.cppharmony/pushy/src/main/cpp/CMakeLists.txtharmony/pushy/src/main/cpp/pushy.cppharmony/pushy/src/main/ets/DownloadTask.tsharmony/pushy/src/main/ets/NativeCheckOrchestrator.tsharmony/pushy/src/main/ets/NativePatchCore.tsharmony/pushy/src/main/ets/PushyTurboModule.tsharmony/pushy/src/main/ets/UpdateContext.tsios/RCTPushy/RCTPushy.mmpackage.jsonreact-native-update.podspecscripts/build-harmony-har.jsscripts/generate-flow-vectors.tsscripts/test-update-flow-core.shscripts/verify-android-so.jssrc/NativePushy.tssrc/__tests__/client.test.tssrc/__tests__/flowVectors.test.tssrc/__tests__/isInRollout.test.tssrc/__tests__/resolveCheckResult.test.tssrc/__tests__/setup.tssrc/__tests__/updateFlowCore.test.tssrc/client.tssrc/endpoint.tssrc/isInRollout.tssrc/provider.tsxsrc/resolveCheckResult.tssrc/type.tssrc/updateFlowCore.tssrc/utils.ts
💤 Files with no reviewable changes (2)
- src/resolveCheckResult.ts
- src/isInRollout.ts
There was a problem hiding this comment.
Actionable comments posted: 1
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (1)
cpp/update_flow_core/flow_json.cpp (1)
482-506: 🎯 Functional Correctness | 🟡 Minor | ⚡ Quick winSerialize non-finite
Numbervalues asnull.
std::strtod("1e309", ...)parses1e309as infinity and the parser accepts it.AppendNumber()then emitsinf, which is not valid JSON. JSON does not supportInfinity; encode parsed non-finite values asnull, or reject range-overflow results during parsing.Proposed fix
void AppendNumber(double n, std::string* out) { + if (!std::isfinite(n)) { + out->append("null"); + return; + } if (std::isfinite(n) && n == std::floor(n) && std::fabs(n) <= 9007199254740992.0) {🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@cpp/update_flow_core/flow_json.cpp` around lines 482 - 506, Update the number parsing return path after std::strtod in the relevant JSON parser method to detect non-finite values and return Value::Null() so inputs such as 1e309 serialize as valid JSON null. Preserve normal finite-number parsing and existing malformed-token handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In
`@android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java`:
- Around line 397-404: The timeout path in performAttempts must cancel and join
the DownloadTask before returning false, ensuring no timed-out task can still
write when runOnce publishes nativeCheckResp. Update the latch timeout handling
around the download round to stop the scheduled task, wait for its termination,
and only then allow the response cache to be stored.
---
Outside diff comments:
In `@cpp/update_flow_core/flow_json.cpp`:
- Around line 482-506: Update the number parsing return path after std::strtod
in the relevant JSON parser method to detect non-finite values and return
Value::Null() so inputs such as 1e309 serialize as valid JSON null. Preserve
normal finite-number parsing and existing malformed-token handling.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e803e595-2013-4c36-bd9b-9db69194ee92
⛔ Files ignored due to path filters (4)
android/lib/arm64-v8a/librnupdate.sois excluded by!**/*.soandroid/lib/armeabi-v7a/librnupdate.sois excluded by!**/*.soandroid/lib/x86/librnupdate.sois excluded by!**/*.soandroid/lib/x86_64/librnupdate.sois excluded by!**/*.so
📒 Files selected for processing (24)
NATIVE_CHECKUPDATE_DESIGN.mdandroid/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.javacpp/update_flow_core/flow_json.cppcpp/update_flow_core/tests/flow_vectors.jsoncpp/update_flow_core/tests/update_flow_core_test.cppcpp/update_flow_core/update_flow_core.cppcpp/update_flow_core/update_flow_jni.cppharmony/pushy/src/main/ets/DownloadTask.tsharmony/pushy/src/main/ets/NativeCheckOrchestrator.tsharmony/pushy/src/main/ets/PathUtils.tsharmony/pushy/src/main/ets/PushyTurboModule.tsharmony/pushy/src/main/ets/UpdateContext.tsios/RCTPushy/RCTPushy.mmscripts/build-harmony-har.jsscripts/generate-flow-vectors.tsscripts/test-update-flow-core.shsrc/__tests__/client.test.tssrc/__tests__/provider.render.test.tsxsrc/__tests__/resolveCheckResult.test.tssrc/__tests__/updateFlowCore.test.tssrc/client.tssrc/provider.tsxsrc/updateFlowCore.ts
🚧 Files skipped from review as they are similar to previous changes (13)
- android/src/main/java/cn/reactnative/modules/update/UpdateModuleImpl.java
- src/provider.tsx
- harmony/pushy/src/main/ets/DownloadTask.ts
- scripts/build-harmony-har.js
- harmony/pushy/src/main/ets/PushyTurboModule.ts
- scripts/test-update-flow-core.sh
- harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
- src/tests/client.test.ts
- cpp/update_flow_core/update_flow_core.cpp
- src/tests/resolveCheckResult.test.ts
- src/updateFlowCore.ts
- ios/RCTPushy/RCTPushy.mm
- NATIVE_CHECKUPDATE_DESIGN.md
Everything still open after two review rounds of the hardening branch, ordered by priority with current state, risk and suggested fixes: the Android/Harmony duplicate-download race (P1, the failure path can delete an installed version), the response-cache ts anchor and the per-platform deadline parity gap, the two semantic costs of iOS download joining, the .pushy-complete migration decision, the schedule-unreachable-on-throw path, two recorded-only P4 items, and the non-code release checklist (e2e, docs, 10.51.0, server-side activation steps). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
🧹 Nitpick comments (1)
NATIVE_CHECK_FOLLOWUPS.md (1)
91-93: 🩺 Stability & Availability | 🔵 Trivial | ⚡ Quick win确保救援检测只调度一次。
如果现有正常出口仍调用
schedule,在finally中无条件调度会为成功解析路径再创建一次检查。该重复检查可能重新触发下载。请将调度集中到一个公共出口,或使用scheduled状态仅在异常且尚未调度时执行兜底。为正常解析、回滚异常和状态解析异常分别增加测试。🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@NATIVE_CHECK_FOLLOWUPS.md` around lines 91 - 93, 将状态解析流程的调度集中到唯一出口,确保正常解析、回滚异常和状态解析异常都只调用一次 schedule;若保留 finally 兜底,则增加 scheduled 状态,仅在异常且尚未调度时以空快照(rolledBackVersion 为 null)执行。三端同步修改,并补充覆盖三种路径的测试。
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 5-7: 将文档中的 P1 发布要求改为硬性门槛:发版前必须修复,若接受风险则明确记录审批与回滚方案。补充方案 3
的原子性要求,围绕 hasCompletedVersion(hash) 使用 get-or-create 锁,避免检查、任务执行、标记写入与
cleanUpAfterFailure 之间的 TOCTOU;采用方案 2 时将检查和清理置于同一临界域内。
---
Nitpick comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 91-93: 将状态解析流程的调度集中到唯一出口,确保正常解析、回滚异常和状态解析异常都只调用一次 schedule;若保留
finally 兜底,则增加 scheduled 状态,仅在异常且尚未调度时以空快照(rolledBackVersion 为
null)执行。三端同步修改,并补充覆盖三种路径的测试。
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: ef59e3f9-d8c3-446e-83b5-fb1ea023442a
📒 Files selected for processing (1)
NATIVE_CHECK_FOLLOWUPS.md
181952a's re-review surfaced ten more confirmed items, six introduced by the fixes themselves; the open ones now live here: the config-sync revert race, the download-round deadline treated as one cross-platform design (Harmony missing it entirely, iOS still per-URL, Android checking after launch and starving the full-download budget), the dead alert for hash-less rollout entries, the silently swallowed noArtifact decline, and two small P4s (slash-variant duplicate requests, missing Harmony whole-call timeout). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
♻️ Duplicate comments (1)
NATIVE_CHECK_FOLLOWUPS.md (1)
7-8:⚠️ Potential issue | 🟠 Major将 P1 改为硬性发版门槛。
当前文字仍允许带着已安装版本被删除的风险发版。发版前必须修复 P1;如果接受风险,请记录明确的审批和回滚方案。
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@NATIVE_CHECK_FOLLOWUPS.md` around lines 7 - 8, Update the release risk guidance in NATIVE_CHECK_FOLLOWUPS.md so P1 is a mandatory release gate: it must be fixed before release, or require documented risk approval and a rollback plan. Keep the existing P2–P4 handling unchanged.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 50-53: Update the slow-CDN condition in the “复评补充(181952a 引入/暴露)”
section to state that the CDN only needs to return one byte within every
60-second interval to bypass the inactivity watchdog, preserving the existing
boundary meaning.
---
Duplicate comments:
In `@NATIVE_CHECK_FOLLOWUPS.md`:
- Around line 7-8: Update the release risk guidance in NATIVE_CHECK_FOLLOWUPS.md
so P1 is a mandatory release gate: it must be fixed before release, or require
documented risk approval and a rollback plan. Keep the existing P2–P4 handling
unchanged.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: 169ec69a-2bff-4a72-a2f1-20da41485854
📒 Files selected for processing (1)
NATIVE_CHECK_FOLLOWUPS.md
There was a problem hiding this comment.
Actionable comments posted: 2
🧹 Nitpick comments (2)
ios/RCTPushy/RCTPushyDownloader.mm (1)
48-54: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueUpdate the comment to describe the caller-supplied resource timeout.
The comment still explains a fixed total-duration cap. Line 54 now takes the value from
timeoutInterval. State that the caller supplies the cap and thatRCTPushy.mmpasses 600 by default or the remaining cold-start deadline budget.📝 Suggested wording
// Avoid hanging forever on a stalled connection (default resource timeout // is 7 days). The 30s idle timeout matches Android's readTimeout and is - // what actually catches a stalled transfer; the total-duration cap matches - // Android's 10min callTimeout — 300s made a 30MB full package on a slow - // (<100KB/s) network fail on iOS while succeeding on Android. + // what actually catches a stalled transfer. The caller supplies the + // total-duration cap: RCTPushy passes 600s by default (matching Android's + // 10min callTimeout — 300s made a 30MB full package on a slow, <100KB/s + // network fail on iOS while succeeding on Android), or the remaining + // cold-start deadline budget for orchestrated downloads. sessionConfig.timeoutIntervalForRequest = 30; sessionConfig.timeoutIntervalForResource = MAX(1, timeoutInterval);🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/RCTPushy/RCTPushyDownloader.mm` around lines 48 - 54, Update the timeout comment above sessionConfig.timeoutIntervalForResource to describe the caller-supplied resource timeout: note that RCTPushy.mm passes 600 seconds by default or the remaining cold-start deadline budget, while retaining the explanation of the 30-second idle timeout.harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts (1)
426-433: 📐 Maintainability & Code Quality | 🔵 Trivial | 💤 Low valueAlign the unknown-attempt-type classification with Android.
Harmony treats any type that is not
difforpdiffas a full attempt. Android treats only"full"as a full attempt, so an unrecognized type there shares the incremental budget and only breaks the inner loop instead of returningfalse. The three platforms currently disagree for any attempt type the server may add later.Match one rule across the platforms. The simplest option is to test for the
fulltype explicitly here.♻️ Optional alignment
- const isFullAttempt = - type !== DOWNLOAD_TYPE_DIFF && type !== DOWNLOAD_TYPE_PDIFF; + const isFullAttempt = type === DOWNLOAD_TYPE_FULL;🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts` around lines 426 - 433, Update the full-attempt classification in the surrounding download-attempt logic to recognize only DOWNLOAD_TYPE_FULL explicitly, matching Android’s behavior. Ensure unrecognized types use incrementalDeadline and follow the existing inner-loop break path rather than being treated as full attempts; retain the existing diff/pdiff handling.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/client.ts`:
- Around line 340-344: Update syncNativeConfig so that when
getNativeConfigJson() returns no config, it also clears the persisted
nativeConfig or stores an explicit disabled state through the existing native
bridge, matching the behavior across Android, Harmony, and iOS. Preserve the
current early-return behavior after ensuring stale native configuration cannot
remain active.
In `@src/provider.tsx`:
- Around line 235-247: Update the malformed-hash handling in the provider
response flow to preserve expired APK results: only rewrite the result to `{
upToDate: true }` for non-expired update responses, while retaining
`info.expired` and `downloadUrl` when `ExpiredCheckResult` indicates expiration
without a hash. Add coverage for an expired response with optional update data
and no hash, verifying the APK download path remains active.
---
Nitpick comments:
In `@harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts`:
- Around line 426-433: Update the full-attempt classification in the surrounding
download-attempt logic to recognize only DOWNLOAD_TYPE_FULL explicitly, matching
Android’s behavior. Ensure unrecognized types use incrementalDeadline and follow
the existing inner-loop break path rather than being treated as full attempts;
retain the existing diff/pdiff handling.
In `@ios/RCTPushy/RCTPushyDownloader.mm`:
- Around line 48-54: Update the timeout comment above
sessionConfig.timeoutIntervalForResource to describe the caller-supplied
resource timeout: note that RCTPushy.mm passes 600 seconds by default or the
remaining cold-start deadline budget, while retaining the explanation of the
30-second idle timeout.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: e7a9556e-68a1-4279-8ff7-bcab4af977b5
📒 Files selected for processing (17)
NATIVE_CHECKUPDATE_DESIGN.mdNATIVE_CHECK_FOLLOWUPS.mdandroid/src/main/java/cn/reactnative/modules/update/DownloadTask.javaandroid/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.javaandroid/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateContext.javaharmony/pushy/src/main/ets/DownloadTask.tsharmony/pushy/src/main/ets/DownloadTaskParams.tsharmony/pushy/src/main/ets/NativeCheckOrchestrator.tsharmony/pushy/src/main/ets/UpdateContext.tsios/RCTPushy/RCTPushy.mmios/RCTPushy/RCTPushyDownloader.hios/RCTPushy/RCTPushyDownloader.mmsrc/__tests__/client.test.tssrc/__tests__/provider.render.test.tsxsrc/client.tssrc/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (1)
- android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
The 13 closures in 283dfd4 all verified; nine second-order items remain, concentrated in the unified deadline model (joiners inheriting the orchestrator's nearly-exhausted budget, Harmony awaits escaping the budget entirely, wall-clock vs monotonic anchors), plus telemetry inflation, the artifact-less dead-alert variant, deferred-download UX, duplicated progress events and a duplicated completion predicate. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 2
Caution
Some comments are outside the diff and can’t be posted inline due to platform limitations.
⚠️ Outside diff range comments (2)
ios/RCTPushy/RCTPushy.mm (1)
1534-1536: 🩺 Stability & Availability | 🟡 Minor | ⚡ Quick winGuard against a nil
bodybefore it reaches the cache dictionary.
[NSString stringWithUTF8String:]returns nil when the C string is not valid UTF-8. Ifbodybecomes nil, the flow continues:PushyHttpRequestsends a POST with no HTTPBody, andpersistResponseCache:request:config:responseAt:then builds the dictionary literal at Line 1607 with a nil value for@"request". That raisesNSInvalidArgumentException.scheduleFromColdStartcatches it, so the whole cold-start check is lost instead of failing on the single bad input.Return early when
bodyis nil.🛠️ Proposed fix
std::string bodyJson = flowjson::Stringify(updateflow::BuildCheckRequestBody(input)); NSString *body = [NSString stringWithUTF8String:bodyJson.c_str()]; + if (body == nil) { + RCTLogWarn(@"RCTPushy -- native check: request body is not valid UTF-8"); + return; + }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@ios/RCTPushy/RCTPushy.mm` around lines 1534 - 1536, In the request-body construction near BuildCheckRequestBody, check whether NSString body is nil immediately after string conversion and return early if so. Prevent the nil value from reaching PushyHttpRequest or persistResponseCache:request:config:responseAt:, while preserving the existing flow for valid UTF-8 bodies.android/src/main/java/cn/reactnative/modules/update/UpdateContext.java (1)
624-631: 🗄️ Data Integrity & Integration | 🟡 Minor | ⚡ Quick winAlign
switchVersionwith the completion predicate.
switchVersion()accepts any existingindex.bundlejspath, whilehasCompletedVersion()requires regularindex.bundlejsand.pushy-complete. This can activate an incomplete version after a failure between bundle creation and marker creation. Preserve pre-marker versions with an explicit compatibility path before tightening this check.🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java` around lines 624 - 631, Update switchVersion to use the same completion requirements as hasCompletedVersion, requiring both regular index.bundlejs and VERSION_COMPLETE_FILE before activation. Before enforcing this for new switches, retain an explicit compatibility path for pre-marker versions so existing versions without the completion marker remain usable.
🧹 Nitpick comments (1)
android/src/main/java/cn/reactnative/modules/update/DownloadTask.java (1)
506-513: 🗄️ Data Integrity & Integration | 🔵 Trivial | ⚡ Quick winApply the same completion guard before cleanup.
The catch block at Line 476 protects a completed directory from deletion by a duplicate task. This handler does not. If
createNewFile()throwsIOExceptionwhile another task has already written the marker and the bundle,cleanUpAfterFailuredeletes the completed install.Reuse
hasCompletedPatchDirectory()here for consistent ownership handoff.♻️ Proposed guard
} catch (Throwable error) { Log.e(UpdateContext.TAG, "failed to mark completed update", error); - cleanUpAfterFailure(taskType); + if (!hasCompletedPatchDirectory()) { + cleanUpAfterFailure(taskType); + } if (params.listener != null) { params.listener.onDownloadFailed(error); } return; }🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the rest with a brief reason, keep changes minimal, and validate. In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java` around lines 506 - 513, In the catch handler for marking a completed update, check hasCompletedPatchDirectory() before calling cleanUpAfterFailure(taskType). Skip failure cleanup when the patch directory is already complete, while preserving the existing failure logging, listener notification, and return behavior.
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java`:
- Around line 112-121: Update the timeout construction in DownloadTask.run() so
a positive remainingNanos value converts to a callTimeout of at least one
millisecond, preventing sub-millisecond values from reaching OkHttp. Preserve
the existing expiration check for non-positive remainingNanos and continue using
deadlineNanos with System.nanoTime().
In `@src/provider.tsx`:
- Around line 247-258: Update the decision handling around decideDownload in the
update flow so every applicable decision.action === 'none' result, including
reason 'rolledBack', marks the update as up to date before it can reach the
alert path. Keep client.reportInvalidUpdateOnce('noArtifact', ...) restricted to
the noArtifact reason, while preserving the existing behavior for other decision
actions.
---
Outside diff comments:
In `@android/src/main/java/cn/reactnative/modules/update/UpdateContext.java`:
- Around line 624-631: Update switchVersion to use the same completion
requirements as hasCompletedVersion, requiring both regular index.bundlejs and
VERSION_COMPLETE_FILE before activation. Before enforcing this for new switches,
retain an explicit compatibility path for pre-marker versions so existing
versions without the completion marker remain usable.
In `@ios/RCTPushy/RCTPushy.mm`:
- Around line 1534-1536: In the request-body construction near
BuildCheckRequestBody, check whether NSString body is nil immediately after
string conversion and return early if so. Prevent the nil value from reaching
PushyHttpRequest or persistResponseCache:request:config:responseAt:, while
preserving the existing flow for valid UTF-8 bodies.
---
Nitpick comments:
In `@android/src/main/java/cn/reactnative/modules/update/DownloadTask.java`:
- Around line 506-513: In the catch handler for marking a completed update,
check hasCompletedPatchDirectory() before calling cleanUpAfterFailure(taskType).
Skip failure cleanup when the patch directory is already complete, while
preserving the existing failure logging, listener notification, and return
behavior.
🪄 Autofix
Fix all unresolved CodeRabbit comments on this PR:
- Push a commit to this branch (recommended)
- Create a new PR with the fixes
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: db1b5735-c1f7-4c43-86af-08a3e050b425
📒 Files selected for processing (18)
NATIVE_CHECKUPDATE_DESIGN.mdNATIVE_CHECK_FOLLOWUPS.mdandroid/src/main/java/cn/reactnative/modules/update/DownloadTask.javaandroid/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.javaandroid/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateContext.javaharmony/pushy/src/main/ets/DownloadTask.tsharmony/pushy/src/main/ets/DownloadTaskParams.tsharmony/pushy/src/main/ets/MonotonicClock.tsharmony/pushy/src/main/ets/NativeCheckOrchestrator.tsharmony/pushy/src/main/ets/UpdateContext.tsios/RCTPushy/RCTPushy.mmios/RCTPushy/RCTPushyDownloader.hios/RCTPushy/RCTPushyDownloader.mmsrc/__tests__/client.test.tssrc/__tests__/provider.render.test.tsxsrc/client.tssrc/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (10)
- ios/RCTPushy/RCTPushyDownloader.mm
- ios/RCTPushy/RCTPushyDownloader.h
- android/src/main/java/cn/reactnative/modules/update/DownloadTaskParams.java
- android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
- src/tests/client.test.ts
- harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
- harmony/pushy/src/main/ets/UpdateContext.ts
- harmony/pushy/src/main/ets/DownloadTask.ts
- src/client.ts
- NATIVE_CHECKUPDATE_DESIGN.md
|
@coderabbitai review |
✅ Action performedReview finished.
|
There was a problem hiding this comment.
Actionable comments posted: 1
🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.
Inline comments:
In `@src/__tests__/provider.render.test.tsx`:
- Around line 185-198: Add an assertion immediately after renderProvider(client)
in the rolled-back update test to verify client.checkUpdate was called, while
preserving the existing assertions that no invalid-update report, download, or
alert occurs.
🪄 Autofix
✅ Autofix completed
ℹ️ Review info
⚙️ Run configuration
Configuration used: defaults
Review profile: CHILL
Plan: Pro Plus
Run ID: cca14af7-2bc6-41aa-82cc-c0a2272c8632
📒 Files selected for processing (6)
android/src/main/java/cn/reactnative/modules/update/DownloadTask.javaandroid/src/main/java/cn/reactnative/modules/update/UpdateContext.javaios/RCTPushy/RCTPushy.mmsrc/__tests__/provider.render.test.tsxsrc/__tests__/setup.tssrc/provider.tsx
🚧 Files skipped from review as they are similar to previous changes (4)
- src/tests/setup.ts
- src/provider.tsx
- android/src/main/java/cn/reactnative/modules/update/DownloadTask.java
- ios/RCTPushy/RCTPushy.mm
Fixes Applied SuccessfullyFixed 1 file(s) based on 1 unresolved review comment. Files modified:
Commit: The changes have been pushed to the Time taken: |
Fixed 1 file(s) based on 1 unresolved review comment. Co-authored-by: CodeRabbit <noreply@coderabbit.ai>
The third round's nine closures all verified. Ten items remain, three of them merge-relevant: the native check ignoring checkStrategy and undoing resetToPackagedBundle (the root cause of the now-red e2e-ios, confirmed across two runs failing different tests — a race signature — while master is green), the strict deadline comparison that makes the iOS same-type join path unreachable, and Harmony's STARTUP clock counting deep sleep where the other two platforms stop. The rest are follow-ups. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
Three merge-gate fixes from the fourth review round. The cold-start check ignored checkStrategy entirely and had no interaction with resetToPackagedBundle, so an app that had switched automatic checks off still got silent version switches, and a reset could be undone by a check that was already in flight — the root cause of the red e2e-ios, where the beforeEach reset lost its BINARY_BASE assertion to a check that re-activated the version it had just removed (two runs failed different tests, the signature of a race; master is green). afterDownload now requires checkStrategy != null, so the check still runs and still downloads (a bricked device stays rescuable) but activation waits for JS or for the server's explicit forceBoot. A reset generation counter, mirrored on all three platforms, is sampled before the round starts and compared before activation and before publishing the response cache, so a reset that lands mid-round always wins; the reset also drops the cached response that still advertised the removed version. The iOS registry compared absolute deadlines with a strict >, and a JS caller always computes now+600 a few seconds after the owner did, so the same-type join path was unreachable and every second caller serialized behind the owner. It now compares remaining budget and defers only a genuinely starved owner (less than half the newcomer's budget left). Harmony's monotonic clock used TimeType.STARTUP, which counts deep sleep, while iOS systemUptime and Android nanoTime both stop — a device sleeping mid-download lost its budget and aborted a rescue the other platforms complete. Switched to TimeType.ACTIVE. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai review 本次推送修复了第四轮评审的三个合并阻断项(详见
请重点复核:reset 代数守卫在三端的采样/比对点是否完备(是否还有第四个应当拦截的落点)、iOS 剩余预算比较的阈值是否会在极端时序下反向误判、以及 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Candidate files =='
fd -a '^(NATIVE_CHECK_FOLLOWUPS\.md|NativeCheckOrchestrator\.java|UpdateContext\.java|RCTPushy\.mm|NativeCheckOrchestrator\.ts|UpdateContext\.ts|NativePatchCore\.ts|DownloadTask\.ts|MonotonicClock\.ts)$' .
printf '%s\n' '== Symbol locations =='
rg -n -i -C 4 \
'reset(ToPackagedBundle)?|generation|nativeCheckResp|KEY_RESP_CACHE|scheduleNativeCheck|afterDownload|checkStrategy|forceBoot|inFlight|defer|deadline|remaining' \
NATIVE_CHECK_FOLLOWUPS.md \
android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
ios/RCTPushy/RCTPushy.mm \
harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts \
harmony/pushy/src/main/ets/UpdateContext.ts \
harmony/pushy/src/main/ets/DownloadTask.ts \
harmony/pushy/src/main/ets/MonotonicClock.ts \
2>/dev/null || true
printf '%s\n' '== Native activation policy definition and callers =='
rg -n -C 4 \
'ShouldActivateAfterDownload|shouldActivateAfterDownload|forceBoot|afterDownload' \
cpp/update_flow_core src android ios harmony \
2>/dev/null || trueLength of output: 147089 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Android: reset, native orchestration, and state writes =='
sed -n '145,225p' android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java
sed -n '485,525p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
rg -n -C 4 'void switchVersion|switchVersion\(' \
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
printf '%s\n' '== Harmony: reset, native orchestration, and state writes =='
sed -n '180,270p' harmony/pushy/src/main/ets/NativeCheckOrchestrator.ts
sed -n '421,475p' harmony/pushy/src/main/ets/UpdateContext.ts
rg -n -C 4 'switchVersion\(' harmony/pushy/src/main/ets/UpdateContext.ts
printf '%s\n' '== iOS: reset, orchestration, cache persistence, and switch operation =='
sed -n '215,235p' ios/RCTPushy/RCTPushy.mm
sed -n '847,902p' ios/RCTPushy/RCTPushy.mm
sed -n '1558,1645p' ios/RCTPushy/RCTPushy.mm
rg -n -C 5 'persistResponseCache|switchVersion:.*error|PushyWithStateLock' \
ios/RCTPushy/RCTPushy.mm
printf '%s\n' '== Generation-related tests =='
rg -n -i -C 3 'reset.*(generation|native|cache)|generation.*reset|resetToPackagedBundle' \
android/src/test android/src/androidTest ios harmony src \
2>/dev/null || trueLength of output: 51409
其余两个重点复核结果如下:
✏️ Learnings added
You are interacting with an AI system. |
The first version of the reset guard was compare-and-act: the generation was checked, then switchVersion and the response-cache write followed as separate steps, and the hash_<hash> version info was written before the check ran at all. A reset landing inside that window could still be overwritten — the window shrank from minutes to microseconds but never closed. Version info, activation and the response cache now land through one commitNativeCheckResult(expectedGeneration, ...) entry point that re-checks the generation and performs every write under the same mutual exclusion resetToPackagedBundle uses, and the reset invalidates the generation before it starts clearing state. iOS reuses the existing state lock (switchVersion grew a lock-free core so the commit can hold the non-reentrant lock across all three writes), Android shares a commit lock, and Harmony documents why ArkTS's single thread plus an await-free commit is already atomic. All three call sites — nothing to do, download failed, download succeeded — go through the same entry point, so no write escapes the guard. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
|
@coderabbitai review P1 已修复( 现在改为一次原子提交:版本元信息 + 激活 + 响应缓存合并为
三个调用点(nothing-to-do、未下载、下载成功)现在全部走同一入口,没有写入能绕过守卫。顺带把 Harmony 的 验证:JS 178 项、Biome/tsc/DevEco strict、77 金标向量 + ASan/UBSan、 请复核:iOS 无锁核的拆分是否在所有调用路径上都保持了原有语义(尤其 |
|
Tip For best results, initiate chat on the files or code changes. 🧩 Analysis chain🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Commit paths =='
git show --stat --oneline ffa606f
git show --format= --name-only ffa606f
printf '%s\n' '== Relevant symbol map =='
rg -n -C 4 \
'PushySwitchVersionLocked|PushyWithStateLock|switchVersion|ignoreRollback|commitNativeCheckResult|commitLock|StateSerialRunner|resetToPackagedBundle' \
ios/RCTPushy/RCTPushy.mm \
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java \
2>/dev/null || true
printf '%s\n' '== Structural outlines =='
ast-grep outline ios/RCTPushy/RCTPushy.mm --items all --match 'Pushy.*(Switch|StateLock)|commitNativeCheckResult|resetToPackagedBundle' 2>/dev/null || true
ast-grep outline android/src/main/java/cn/reactnative/modules/update --items all --type method,class --match 'commitNativeCheckResult|resetToPackagedBundle|StateSerialRunner' 2>/dev/null || true
printf '%s\n' '== Focused change diff =='
git show --format= --unified=80 ffa606f -- \
ios/RCTPushy/RCTPushy.mm \
android/src/main/java/cn/reactnative/modules/update/UpdateContext.java \
android/src/main/java/cn/reactnative/modules/update/NativeCheckOrchestrator.java \
android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.javaLength of output: 109041 🏁 Script executed: #!/bin/bash
set -euo pipefail
printf '%s\n' '== Android StateSerialRunner implementation =='
sed -n '1,180p' android/src/main/java/cn/reactnative/modules/update/StateSerialRunner.java
printf '%s\n' '== Android StateSerialRunner call sites =='
rg -n -C 8 'StateSerialRunner\.run\s*\(' android/src/main/java/cn/reactnative/modules/update
printf '%s\n' '== Android reset and state-method call paths =='
rg -n -C 10 \
'\.resetToPackagedBundle\s*\(|resetToPackagedBundle\s*\(|\.switchVersion\s*\(|\.setKv\s*\(|\.markSuccess\s*\(|\.clearFirstTime\s*\(|\.clearRollbackMark\s*\(' \
android/src/main/java/cn/reactnative/modules/update
printf '%s\n' '== Android commit and reset complete method ranges =='
sed -n '360,430p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
sed -n '470,535p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
sed -n '650,705p' android/src/main/java/cn/reactnative/modules/update/UpdateContext.java
printf '%s\n' '== iOS locked-helper references and calling-method ranges =='
rg -n -C 12 'PushySwitchVersionLocked\s*\(' ios/RCTPushy/RCTPushy.mm
sed -n '1218,1245p' ios/RCTPushy/RCTPushy.mm
sed -n '1650,1700p' ios/RCTPushy/RCTPushy.mmLength of output: 56286
iOS
原有 Android 未发现
✏️ Learnings added
You are interacting with an AI system. |
Summary
forceBoothandlingWhy
Update checking previously depended on the application bundle reaching JavaScript. A broken hot update could therefore prevent the code responsible for fetching its replacement from running.
This change introduces a small native rescue path that runs after cold start, reuses the same decision semantics on every platform, and activates fixes only under the configured strategy. The final hardening pass addresses races found during review: rollback state could be consumed before the delayed check, interrupted extraction could look complete, iOS native and JS downloads could overwrite each other, cached responses could cross request/config boundaries, and malformed HTTP 2xx bodies could suppress endpoint fallback.
Impact
Applications can recover from a bricked OTA bundle on the next launch without changing the existing JS API or normal update strategy. Native work remains delayed and bounded, failures stay silent, and alert-style strategies continue to leave activation to JavaScript.
Validation
bun run lintbun test src/__tests__— 162 passed./scripts/test-update-flow-core.sh— 77 vectors passed./scripts/test-patch-core.sh— 29 tests passed:react-native-update:compileReleaseJavaWithJavacreact-native-updatePods target, Release simulator buildnode scripts/verify-android-so.js— arm64-v8a, armeabi-v7a, x86, and x86_64 passedgit diff --checkNeed help on this PR? Tag
@codesmith-botwith what you need. Autofix is disabled.Summary by CodeRabbit