fix(ios): read isProtectedDataAvailable on the main actor; retry a query refused while the store re-opens - #374
Conversation
🦋 Changeset detectedLatest commit: 151fa27 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
a392b16 to
becfeff
Compare
…ery refused while the store re-opens UIApplication is @MainActor-isolated, but both isProtectedDataAvailable accessors evaluated it on the JS thread (sync hybrid method / Promise.resolved argument). The async accessor now hops to the main actor; the sync one cannot be fixed by dispatch without risking a deadlock and is left as-is. HKError.errorDatabaseInaccessible (Code 6) was thrown as an opaque string: handleHKNoDataOrThrow keys on domain/code for errorNoData but not for this one, and the NSError is flattened to String(describing:) at the bridge. HealthKit seals the store the moment the device locks and re-opens it a beat after it unlocks, and iOS can resume the app inside that beat, so a query issued in the gap is refused although protected data is reported available. Add retryingWhileStoreReopens to Helpers.swift: it runs an operation and, on exactly that refusal while protected data is available, gives it one more attempt after a short delay. A refusal while protected data is unavailable is the sealed store itself and is thrown as-is, as is every other error. Applied to the four statistics call sites; the helper is generic so the sample and characteristic queries can adopt it in a follow-up.
becfeff to
f0f8cef
Compare
|
@robertherber when you have a moment, could you take a look at this one? It is a small change: The workflows are waiting on approval since this is my first PR here. Happy to adjust anything, or to extend the helper to the sample and characteristic queries in the same PR if you would like it there too. Thanks! |
robertherber
left a comment
There was a problem hiding this comment.
Thanks for your contribution, overall I think it looks good! 👍
…nc to the default exports
commit: |
fix(ios): read isProtectedDataAvailable on the main actor; retry a query refused while the store re-opens
Two defects, one file each
1.
CoreModule.swift—UIApplication.shared.isProtectedDataAvailableis read off the main thread.UIApplicationis@MainActor-isolated. Nitro sync hybrid methods run on the JS thread, andPromise.resolved(withResult:)evaluates its argument at the call site — so bothisProtectedDataAvailable()andisProtectedDataAvailableAsync()read the property from the JS thread. Every other UIKit/HealthKit callback in this file hops toDispatchQueue.main; these two are the only ones that don't. The async variant is fixed here withMainActor.run. The sync variant cannot be fixed by dispatch (blocking the JS thread on main risks deadlock) and is left as-is; callers that need a correct value should use the async one.2.
HKError.errorDatabaseInaccessibleis thrown as an opaque string.handleHKNoDataOrThrowalready keys onnsError.domain/codeto maperrorNoData;errorDatabaseInaccessible(Code 6) falls through tocontinuation.resume(throwing:), and Nitro flattens theNSErrortoString(describing:)before it reaches JS — domain and code are gone, leaving consumers to string-match"Code=6".Measured on an iPhone XS Max / iOS 18.7 with a 1 s sampler through three lock/unlock cycles:
isProtectedDataAvailable(read on either thread — identical on ~200 paired samples) staystruefor exactly ~10 s (iOS's data-protection grace), then flips.activeto the app before it has — a query issued in that window is refused although protected data is (correctly) reported available.That second case is Apple's documented recovery for this error ("wait for protected data to become available, then retry"). This PR adds one generic helper in
Helpers.swift,retryingWhileStoreReopens { … }: it runs the query and, on exactly that refusal while protected data is available (the store is re-opening rather than sealed), gives it one more attempt after 1 s. A refusal while protected data is unavailable is thrown unchanged, as is every other error. It is applied at the four statistics call sites inQuantityTypeModule.swift(queryStatisticsForQuantity,…SeparateBySource,queryStatisticsCollectionForQuantity,…SeparateBySource); the helper is deliberately generic so the same treatment can be extended to the sample and characteristic queries in a follow-up if you want it there too.Consumers that previously received
"Error Domain=com.apple.healthkit Code=6 …"on the unlock edge now receive the statistics; refusals from a sealed store are unchanged.Files
ios/CoreModule.swift,ios/Helpers.swift(the helper),ios/QuantityTypeModule.swift(four call sites),src/healthkit.ios.ts,src/healthkit.ts, plus a changeset.Related: #35 (asked for
isProtectedDataAvailableand the protected-data notifications), #176 (same Code 6 in the wild).Validation (iPhone XS Max, iOS 18.7.10, 1 s sampler + 25 ms burst on
active)isProtectedDataAvailablestilltrue): every query now takes ~1.05–1.4 s and still throws Code 6 — the retry path ran (sleep + second attempt) and correctly gave up on a sealed store.isProtectedDataAvailablefalse): queries throw in ~10 ms — no retry, thrown unchanged.activeedge on this device; 40 reads in 1.3 s all succeed in ~10 ms — the wrapper adds nothing to healthy reads.🤖 Generated with Claude Code