Skip to content

feat: native background sync engine v2 + dual-unit hkUnit#344

Open
oakleaf wants to merge 3 commits intokingstinct:masterfrom
appitudeio:feat/native-background-sync-v2
Open

feat: native background sync engine v2 + dual-unit hkUnit#344
oakleaf wants to merge 3 commits intokingstinct:masterfrom
appitudeio:feat/native-background-sync-v2

Conversation

@oakleaf
Copy link
Copy Markdown
Contributor

@oakleaf oakleaf commented Apr 14, 2026

Summary

  • Native sync engine v2: Rewrites the background sync path with four sample-kind handlers (cumulativeQuantity, discreteQuantity, categorySample, workout), configurable lookback window, Swift concurrency with 20s deadline, and deterministic native-first dispatch
  • Dual-unit hkUnit field: SyncTypeConfig.hkUnit lets consumers specify Apple's HKUnit factorization grammar separately from their wire format — e.g. unit: "bpm" for the backend, hkUnit: "count/min" for HealthKit queries. Configure-time validation surfaces invalid strings immediately
  • Device/source/metadata enrichment: Discrete, category, and workout records now include device (HKDevice), source (HKSourceRevision), and metadata fields. Cumulative queries return aggregated stats — no per-sample metadata to expose
  • Breadcrumb channel: Native HTTP failures are persisted in UserDefaults and flushed to the consumer's backend on the next successful push via clientFailuresSince
  • Locale fields: Every record carries timeZone (IANA), timeZoneOffsetMinutes, localDate

Breaking changes

  • SyncTypeConfig.kind replaces the old cumulative: boolean — now a string union (cumulativeQuantity / discreteQuantity / categorySample / workout)
  • BackgroundSyncEndpoint.lookbackDays added (default 1 — today only)

Design decisions

  • Fork stays consumer-agnostic — no product-specific unit names in Swift. Consumers declare both formats via hkUnit / unit; neither is guessed
  • Fail at setup, not at wake — invalid HKUnit → configureBackgroundSync throws synchronously
  • Companion pod isolationBackgroundHKUnitCatcher is a separate ObjC NSException catcher with extern "C" linkage, excluded from the main pod to avoid duplicate symbols. SWIFT_ACTIVE_COMPILATION_CONDITIONS lets NativeSyncEngine.swift compile in both pod targets

Test plan

  • EAS preview build compiles clean (both pod targets)
  • On-device: app launches without NSInvalidArgumentException crash
  • Native sync pushes arrive with syncPath: "native" and correct data types
  • Workout records include device, source, metadata fields
  • Locale fields (timeZone, timeZoneOffsetMinutes, localDate) present on all records
  • Wire unit preserved (e.g. "bpm" not "count/min" on heart_rate records)
  • Pre-push hooks: 9 tests pass, tsc --noEmit clean

oakleaf and others added 2 commits April 14, 2026 09:12
Rewrite the native HealthKit sync engine with:

- Four sample-kind handlers (cumulativeQuantity, discreteQuantity,
  categorySample, workout) replacing the old cumulative-only path
- Configurable lookback window (consumer sets lookbackDays; default 1)
- Swift concurrency with 20s deadline per observer wake
- Sleep category value mapping with @unknown default + raw-value fallback
- iOS 17+ workout.statistics(for:) with graceful pre-17 fallback
- Deterministic native-first dispatch: native sync always runs on wake,
  JS callback fires in parallel for reactive foreground UI
- Native breadcrumb channel persisted in UserDefaults, flushed to the
  consumer's backend on next successful push via clientFailuresSince
- Every record carries timeZone (IANA), timeZoneOffsetMinutes, localDate

BackgroundDeliveryManager registers one observer per distinct kind (not per
type) and stores registrations + kind in UserDefaults for cold-launch restore.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
…chment

SyncTypeConfig gains optional hkUnit field for when a consumer's wire
unit differs from Apple's HKUnit factorization grammar (e.g. "bpm" vs
"count/min"). Set hkUnit for HealthKit queries, unit for the wire.
Omit hkUnit when they match — falls back to unit automatically.

Configure-time validation in configureBackgroundSync surfaces invalid
HKUnit strings synchronously instead of silently dropping records on
background wake hours later.

NativeSyncEngine now emits device (HKDevice), source (HKSourceRevision),
and metadata on all sample-based records (discrete, category, workout).
Cumulative queries return aggregated HKStatistics — no per-sample
metadata to expose.

Companion pod gains SWIFT_ACTIVE_COMPILATION_CONDITIONS so
NativeSyncEngine compiles cleanly in both pod targets: calls
BGSafeHKUnitFromString in the companion pod, falls back to
HKUnitFromStringCatchingExceptions in the main pod.

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
@changeset-bot
Copy link
Copy Markdown

changeset-bot Bot commented Apr 14, 2026

🦋 Changeset detected

Latest commit: 02ecd01

The changes in this PR will be included in the next version bump.

This PR includes changesets to release 1 package
Name Type
@kingstinct/react-native-healthkit Major

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

@pkg-pr-new
Copy link
Copy Markdown

pkg-pr-new Bot commented Apr 14, 2026

Open in StackBlitz

npm i https://pkg.pr.new/kingstinct/react-native-healthkit/@kingstinct/react-native-healthkit@344

commit: 02ecd01

@robertherber
Copy link
Copy Markdown
Member

I don't understand how metadata gets exposed this way? Also it seems like the native build broke with this latest change.

Also there's now two change sets - I think one is enough :)

…ut record types

- Delete duplicate v1 changeset (native-background-sync.md) — keep only v2
- Exclude BackgroundDeliveryManager.swift and NativeSyncEngine.swift from the
  main pod's source_files glob — these @objc singletons were being compiled by
  both the main and companion pods, causing duplicate HKHealthStore instances
  and DispatchQueue lock contention that hung the contract test suite
- Add TypeScript interfaces for output record shapes (SyncRecordDevice,
  SyncRecordSource, SyncRecordCumulative, SyncRecordDiscrete,
  SyncRecordCategory, SyncRecordWorkout) so consumers can type their backend
  request handlers and the metadata flow is explicit in the PR diff

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants