Skip to content

Wire background-delivered HealthKit updates through to JS#365

Open
the-woody-kim wants to merge 1 commit into
kingstinct:masterfrom
the-woody-kim:fix/background-delivery-jsi-bridge
Open

Wire background-delivered HealthKit updates through to JS#365
the-woody-kim wants to merge 1 commit into
kingstinct:masterfrom
the-woody-kim:fix/background-delivery-jsi-bridge

Conversation

@the-woody-kim

Copy link
Copy Markdown

Summary

BackgroundDeliveryManager's HKObserverQuery correctly survives app termination and wakes the process (registered in didFinishLaunchingWithOptions per Apple's recommendations, see #51), but nothing ever connected its callback to a JS-facing subscription:

  • Events either dispatched to a global jsCallback that nothing in the library ever registered, or queued into pendingEvents that nothing ever drained.
  • Separately, CoreModule.subscribeToObserverQuery always registered its own independent HKObserverQuery for a type, so even a caller that did want to listen for a background-configured type ended up with a second, disconnected observer.
  • The observer's completion handler was also signaled before asking iOS for any extended execution time, so even a listening JS callback could get suspended before the JS runtime finished booting on a cold, background-triggered launch.

Net effect: the background-delivery machinery wakes the process and detects the data change, then discards that information — functionally equivalent to not having it. This is the gap #51#341 (merged with its "kill app, write data, verify wakes and delivers" test-plan item left unchecked) → #343 (closed) → #344 (open, where the discussion pivots to a native-only, no-JS approach) has been circling.

Changes

  • BackgroundDeliveryManager: add a per-type callback API (setCallback/removeCallback/isBackgroundConfigured) alongside the existing global one, so a specific type can be subscribed to without touching unrelated types' behavior. Wrap the observer dispatch in beginBackgroundTask/endBackgroundTask, opened before the completion handler fires, for a real execution window.
  • CoreModule.subscribeToObserverQuery: when a type is already background-configured, route through BackgroundDeliveryManager's per-type API instead of registering a second, independent HKObserverQuery for the same type. unsubscribeQuery mirrors this for cleanup. Types that aren't background-configured are unaffected — this only changes behavior for types that already opted into background delivery.

This is intentionally scoped to wiring the existing plumbing together rather than the native-only rewrite discussed in #344 — it reuses all the existing JS-side sync logic in consuming apps without duplicating business logic into Swift.

Test plan

Verified on a real device (iOS, Release build, no debugger attached — a debugger changes iOS's suspend/resume behavior):

  • Force-quit a host app with background delivery configured for a type
  • Wrote a sample for that type from a different source (Apple Health app directly, not the host app)
  • Confirmed via device console logs (os_log breadcrumbs + Apple's own HealthKit/RunningBoard logs) that: the observer fired, a named background task opened, and the JS callback ran and completed a sync — all while the host app was fully terminated
  • Regression-checked the foreground path (open the app, confirm immediate sync still fires and live updates still work) since this touches subscribeToObserverQuery's shared implementation

One clean trial confirmed the mechanism works end-to-end. I haven't yet run a larger multi-trial success-rate test across varied device states (charging/locked/network) — happy to gather that data if useful for review.

HKObserverQuery in BackgroundDeliveryManager correctly survives app
termination and wakes the process (registered in
didFinishLaunchingWithOptions per the docs), but nothing ever
connected its callback to a JS-facing subscription — events either
dispatched to a global jsCallback that nothing ever registered, or
queued in pendingEvents that nothing ever drained. Separately,
CoreModule.subscribeToObserverQuery always registered its own
independent HKObserverQuery, so even a caller who *did* want to
listen for a background-configured type would get a second observer
disconnected from BackgroundDeliveryManager's state.

Also, the observer's completion handler was signaled before asking
iOS for any extended execution time, so even a listening JS callback
could get suspended before the JS runtime finished booting on a cold,
background-triggered launch.

Changes:
- BackgroundDeliveryManager: add a per-type callback API
  (setCallback/removeCallback/isBackgroundConfigured) alongside the
  existing global one, so a specific type can be subscribed to
  without touching unrelated types' behavior. Wrap the observer
  dispatch in beginBackgroundTask/endBackgroundTask, opened before
  the completion handler fires, for a real execution window.
- CoreModule.subscribeToObserverQuery: when a type is already
  background-configured, route through BackgroundDeliveryManager's
  per-type API instead of registering a second, independent
  HKObserverQuery for the same type. unsubscribeQuery mirrors this
  for cleanup. Types that aren't background-configured are
  unaffected.

Verified on a real device (iOS, Release build, no debugger attached):
force-quit the host app, wrote a HealthKit sample from a different
source, and confirmed via device console logs that the observer
fired, a background task opened, and the JS callback ran while the
app was fully terminated.

Related: kingstinct#51, kingstinct#341, kingstinct#343, kingstinct#344
@changeset-bot

changeset-bot Bot commented Jul 18, 2026

Copy link
Copy Markdown

⚠️ No Changeset found

Latest commit: 7a5349d

Merging this PR will not cause a version bump for any packages. If these changes should not result in a new version, you're good to go. If these changes should result in a version bump, you need to add a changeset.

This PR includes no changesets

When changesets are added to this PR, you'll see the packages that this PR includes changesets for and the associated semver types

Click here to learn what changesets are, and how to add one.

Click here if you're a maintainer who wants to add a changeset to this PR

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