@kingstinct/react-native-healthkit 14.1.0 → 16.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (123) hide show
  1. package/README.md +37 -3
  2. package/ReactNativeHealthkit.podspec +11 -7
  3. package/app.plugin.js +33 -94
  4. package/app.plugin.ts +29 -104
  5. package/ios/CategoryTypeModule.swift +1 -0
  6. package/ios/CharacteristicTypeModule.swift +7 -6
  7. package/ios/Constants.swift +9 -10
  8. package/ios/CoreModule.swift +77 -42
  9. package/ios/CorrelationTypeModule.swift +3 -2
  10. package/ios/ElectrocardiogramModule.swift +4 -3
  11. package/ios/HeartbeatSeriesModule.swift +4 -3
  12. package/ios/Helpers.swift +11 -216
  13. package/ios/MedicationModule.swift +9 -8
  14. package/ios/MemoryEstimation.swift +84 -0
  15. package/ios/PredicateHelpers.swift +16 -86
  16. package/ios/QuantityTypeModule.swift +37 -28
  17. package/ios/Serializers.swift +34 -70
  18. package/ios/SourceProxy.swift +15 -5
  19. package/ios/StateOfMindModule.swift +3 -2
  20. package/ios/WorkoutProxy.swift +31 -13
  21. package/ios/WorkoutsModule.swift +4 -3
  22. package/lib/commonjs/generated/healthkit.generated.js +15 -3
  23. package/lib/commonjs/healthkit.ios.js +6 -2
  24. package/lib/commonjs/healthkit.js +24 -79
  25. package/lib/commonjs/types/Auth.js +3 -18
  26. package/lib/commonjs/types/Background.js +2 -10
  27. package/lib/commonjs/types/QueryOptions.js +2 -17
  28. package/lib/module/generated/healthkit.generated.js +14 -2
  29. package/lib/module/healthkit.ios.js +4 -0
  30. package/lib/module/healthkit.js +22 -77
  31. package/lib/module/types/Auth.js +1 -18
  32. package/lib/module/types/Background.js +1 -10
  33. package/lib/module/types/QueryOptions.js +1 -17
  34. package/lib/typescript/generated/healthkit.generated.d.ts +20 -7
  35. package/lib/typescript/healthkit.d.ts +4 -2
  36. package/lib/typescript/healthkit.ios.d.ts +13 -9
  37. package/lib/typescript/specs/CoreModule.nitro.d.ts +22 -4
  38. package/lib/typescript/specs/SourceProxy.nitro.d.ts +2 -5
  39. package/lib/typescript/types/Auth.d.ts +1 -16
  40. package/lib/typescript/types/Background.d.ts +1 -9
  41. package/lib/typescript/types/Device.d.ts +1 -13
  42. package/lib/typescript/types/QuantityType.d.ts +3 -3
  43. package/lib/typescript/types/QueryOptions.d.ts +3 -37
  44. package/lib/typescript/types/Shared.d.ts +2 -25
  45. package/lib/typescript/types/Source.d.ts +1 -10
  46. package/lib/typescript/types/Subscriptions.d.ts +2 -3
  47. package/lib/typescript/types/Units.d.ts +1 -1
  48. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.cpp +24 -16
  49. package/nitrogen/generated/ios/ReactNativeHealthkit-Swift-Cxx-Bridge.hpp +91 -12
  50. package/nitrogen/generated/ios/c++/HybridCategoryTypeModuleSpecSwift.hpp +8 -5
  51. package/nitrogen/generated/ios/c++/HybridCoreModuleSpecSwift.hpp +8 -0
  52. package/nitrogen/generated/ios/c++/HybridCorrelationTypeModuleSpecSwift.hpp +8 -5
  53. package/nitrogen/generated/ios/c++/HybridElectrocardiogramModuleSpecSwift.hpp +8 -5
  54. package/nitrogen/generated/ios/c++/HybridHeartbeatSeriesModuleSpecSwift.hpp +8 -5
  55. package/nitrogen/generated/ios/c++/HybridMedicationModuleSpecSwift.hpp +8 -5
  56. package/nitrogen/generated/ios/c++/HybridQuantityTypeModuleSpecSwift.hpp +8 -5
  57. package/nitrogen/generated/ios/c++/HybridStateOfMindModuleSpecSwift.hpp +8 -5
  58. package/nitrogen/generated/ios/c++/HybridWorkoutProxySpecSwift.hpp +3 -4
  59. package/nitrogen/generated/ios/c++/HybridWorkoutsModuleSpecSwift.hpp +6 -3
  60. package/nitrogen/generated/ios/swift/CategorySample.swift +8 -8
  61. package/nitrogen/generated/ios/swift/CategoryTypeIdentifier.swift +8 -0
  62. package/nitrogen/generated/ios/swift/CategoryTypeIdentifierWriteable.swift +8 -0
  63. package/nitrogen/generated/ios/swift/CorrelationSample.swift +8 -8
  64. package/nitrogen/generated/ios/swift/ElectrocardiogramSample.swift +8 -8
  65. package/nitrogen/generated/ios/swift/Func_void_std__unordered_map_std__string__std__chrono__system_clock__time_point_.swift +54 -0
  66. package/nitrogen/generated/ios/swift/HeartbeatSeriesSample.swift +8 -8
  67. package/nitrogen/generated/ios/swift/HybridCoreModuleSpec.swift +1 -0
  68. package/nitrogen/generated/ios/swift/HybridCoreModuleSpec_cxx.swift +25 -0
  69. package/nitrogen/generated/ios/swift/MedicationDoseEvent.swift +8 -8
  70. package/nitrogen/generated/ios/swift/ObjectTypeIdentifier.swift +12 -4
  71. package/nitrogen/generated/ios/swift/QuantitySample.swift +8 -8
  72. package/nitrogen/generated/ios/swift/QuantityTypeIdentifier.swift +4 -4
  73. package/nitrogen/generated/ios/swift/QuantityTypeIdentifierWriteable.swift +4 -4
  74. package/nitrogen/generated/ios/swift/QueryStatisticsResponse.swift +6 -13
  75. package/nitrogen/generated/ios/swift/QueryStatisticsResponseFromSingleSource.swift +4 -11
  76. package/nitrogen/generated/ios/swift/SampleTypeIdentifier.swift +12 -4
  77. package/nitrogen/generated/ios/swift/SampleTypeIdentifierWriteable.swift +12 -4
  78. package/nitrogen/generated/ios/swift/SourceRevision.swift +4 -11
  79. package/nitrogen/generated/ios/swift/StateOfMindSample.swift +8 -8
  80. package/nitrogen/generated/ios/swift/WorkoutActivityType.swift +8 -0
  81. package/nitrogen/generated/shared/c++/CategorySample.hpp +7 -7
  82. package/nitrogen/generated/shared/c++/CategoryTypeIdentifier.hpp +67 -59
  83. package/nitrogen/generated/shared/c++/CategoryTypeIdentifierWriteable.hpp +67 -59
  84. package/nitrogen/generated/shared/c++/CorrelationSample.hpp +7 -7
  85. package/nitrogen/generated/shared/c++/ElectrocardiogramSample.hpp +7 -7
  86. package/nitrogen/generated/shared/c++/HeartbeatSeriesSample.hpp +7 -7
  87. package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.cpp +1 -0
  88. package/nitrogen/generated/shared/c++/HybridCoreModuleSpec.hpp +3 -1
  89. package/nitrogen/generated/shared/c++/MedicationDoseEvent.hpp +7 -7
  90. package/nitrogen/generated/shared/c++/ObjectTypeIdentifier.hpp +206 -198
  91. package/nitrogen/generated/shared/c++/QuantitySample.hpp +7 -7
  92. package/nitrogen/generated/shared/c++/QuantityTypeIdentifier.hpp +73 -73
  93. package/nitrogen/generated/shared/c++/QuantityTypeIdentifierWriteable.hpp +73 -73
  94. package/nitrogen/generated/shared/c++/QueryStatisticsResponse.hpp +8 -9
  95. package/nitrogen/generated/shared/c++/QueryStatisticsResponseFromSingleSource.hpp +8 -9
  96. package/nitrogen/generated/shared/c++/SampleTypeIdentifier.hpp +199 -191
  97. package/nitrogen/generated/shared/c++/SampleTypeIdentifierWriteable.hpp +193 -185
  98. package/nitrogen/generated/shared/c++/SourceRevision.hpp +8 -9
  99. package/nitrogen/generated/shared/c++/StateOfMindSample.hpp +7 -7
  100. package/nitrogen/generated/shared/c++/WorkoutActivityType.hpp +4 -0
  101. package/package.json +8 -3
  102. package/src/generated/healthkit-schema.json +53 -8
  103. package/src/generated/healthkit.generated.ts +22 -6
  104. package/src/healthkit.ios.ts +6 -0
  105. package/src/healthkit.ts +32 -84
  106. package/src/specs/CoreModule.nitro.ts +25 -4
  107. package/src/specs/SourceProxy.nitro.ts +2 -4
  108. package/src/test-setup.ts +1 -0
  109. package/src/type-tests/generated-typing.ts +30 -8
  110. package/src/types/Auth.ts +4 -17
  111. package/src/types/Background.ts +1 -9
  112. package/src/types/Device.ts +1 -13
  113. package/src/types/QuantityType.ts +3 -3
  114. package/src/types/QueryOptions.ts +7 -65
  115. package/src/types/Shared.ts +7 -28
  116. package/src/types/Source.ts +1 -11
  117. package/src/types/Subscriptions.ts +2 -3
  118. package/src/types/Units.ts +1 -1
  119. package/ios/BackgroundDeliveryManager.swift +0 -277
  120. package/ios/Bridge.h +0 -10
  121. package/ios/ExceptionCatcher.h +0 -5
  122. package/ios/ExceptionCatcher.mm +0 -32
  123. package/ios/module.modulemap +0 -5
@@ -0,0 +1,84 @@
1
+ //
2
+ // MemoryEstimation.swift
3
+ // Pods
4
+ //
5
+ // Rough estimates of the native heap kept alive by HybridObject proxies,
6
+ // reported to Nitro via `memorySize` so the JS garbage collector can account
7
+ // for memory it cannot see. Precision is not the goal; the right order of
8
+ // magnitude per object is.
9
+ //
10
+
11
+ import HealthKit
12
+ import ReactNativeHealthkitCore
13
+
14
+ /// Approximate per-object bridging overhead of a Nitro HybridObject that Nitro
15
+ /// does not account for itself. The generated `*Spec_cxx.swift` adds the Swift
16
+ /// implementation instance via `MemoryHelper.getSizeOf`; this covers the rest:
17
+ /// the Swift `_cxx` bridge class, the C++ `HybridObject` with its per-runtime
18
+ /// cache map, the `shared_ptr` control block and the reference-state records
19
+ /// behind the JS weak reference. Add it once, in each proxy's `memorySize`.
20
+ let nitroHybridObjectOverheadBytes = 512
21
+
22
+ /// Rough size of an `NSString` on the heap, including its object header.
23
+ func estimateStringMemorySize(_ value: String?) -> Int {
24
+ guard let value = value else {
25
+ return 0
26
+ }
27
+ return 32 + value.utf16.count * 2
28
+ }
29
+
30
+ /// Rough size of a `metadata` dictionary as HealthKit keeps it in memory.
31
+ func estimateMetadataMemorySize(_ metadata: [String: Any]?) -> Int {
32
+ guard let metadata = metadata, !metadata.isEmpty else {
33
+ return 0
34
+ }
35
+ // NSDictionary header plus one bucket per entry.
36
+ var total = 64 + metadata.count * 48
37
+ for (key, value) in metadata {
38
+ total += estimateStringMemorySize(key)
39
+ if let string = value as? String {
40
+ total += estimateStringMemorySize(string)
41
+ } else if value is HKQuantity {
42
+ // HKQuantity: object header, unit object, double.
43
+ total += 128
44
+ } else {
45
+ // NSNumber, NSDate and friends are small tagged or boxed objects.
46
+ total += 32
47
+ }
48
+ }
49
+ return total
50
+ }
51
+
52
+ /// Rough size of one `HKStatistics` entry: the object itself plus the
53
+ /// sum/min/max/average/most-recent quantities and their date interval.
54
+ let estimatedStatisticsEntryMemorySize = 512
55
+
56
+ /// Estimate of the `HKWorkout` object graph only; the proxy adds Nitro's
57
+ /// bridging overhead on top.
58
+ func estimateWorkoutMemorySize(_ workout: HKWorkout) -> Int {
59
+ // HKWorkout object, uuid, dates, HKSourceRevision (+HKSource), HKDevice,
60
+ // and the four total quantities.
61
+ var total = 1_024
62
+ total += estimateMetadataMemorySize(workout.metadata)
63
+
64
+ if let events = workout.workoutEvents {
65
+ for event in events {
66
+ // HKWorkoutEvent: object header, type, NSDateInterval.
67
+ total += 128 + estimateMetadataMemorySize(event.metadata)
68
+ }
69
+ }
70
+
71
+ if #available(iOS 16.0, *) {
72
+ total += workout.allStatistics.count * estimatedStatisticsEntryMemorySize
73
+
74
+ for activity in workout.workoutActivities {
75
+ // HKWorkoutActivity: object header, uuid, dates, HKWorkoutConfiguration.
76
+ total += 512
77
+ total += estimateMetadataMemorySize(activity.metadata)
78
+ total += activity.workoutEvents.count * 128
79
+ total += activity.allStatistics.count * estimatedStatisticsEntryMemorySize
80
+ }
81
+ }
82
+
83
+ return total
84
+ }
@@ -6,6 +6,7 @@
6
6
  //
7
7
 
8
8
  import HealthKit
9
+ import ReactNativeHealthkitCore
9
10
 
10
11
  func createSourcePredicate(_ sources: [HybridSourceProxySpec]?) -> NSPredicate? {
11
12
  if let sources = sources {
@@ -147,103 +148,32 @@ func createPredicateForWorkout(_ filter: FilterForWorkouts?) -> NSPredicate? {
147
148
  return nil
148
149
  }
149
150
 
150
- func createDatePredicate(_ dateFilter: DateFilter?) -> NSPredicate? {
151
- if let dateFilter = dateFilter {
152
- let strictStartDate = dateFilter.strictStartDate ?? false
153
- let strictEndDate = dateFilter.strictEndDate ?? false
154
-
155
- let options: HKQueryOptions =
156
- strictStartDate && strictEndDate
157
- ? [.strictStartDate, .strictEndDate]
158
- : strictEndDate
159
- ? .strictEndDate
160
- : strictStartDate
161
- ? .strictStartDate
162
- : []
163
-
164
- return HKQuery.predicateForSamples(
165
- withStart: dateFilter.startDate,
166
- end: dateFilter.endDate,
167
- options: options
168
- )
169
- }
170
- return nil
171
- }
172
-
173
- func createUUIDsPredicate(uuids: [String]?) -> NSPredicate? {
174
- if let uuids = uuids {
175
- let uuids = uuids.compactMap { uuidStr -> UUID? in
176
- do {
177
- let uuid = try initializeUUID(uuidStr)
178
- return uuid
179
- } catch {
180
- warnWithPrefix(error.localizedDescription)
181
- return nil
182
- }
183
- }
184
- return HKQuery.predicateForObjects(with: Set(uuids))
185
- }
186
- return nil
187
- }
151
+ // The generated filter structs adopt the core protocols so the shared
152
+ // predicate builders in ReactNativeHealthkitCore can read them.
153
+ extension DateFilter: DateFilterConvertible {}
188
154
 
189
- func createUUIDPredicate(_ uuid: String?) -> NSPredicate? {
190
- if let uuidStr = uuid {
191
- do {
192
- let uuid = try initializeUUID(uuidStr)
193
- return HKQuery.predicateForObject(with: uuid)
194
- } catch {
195
- warnWithPrefix("createUUIDPredicate: \(error.localizedDescription)")
155
+ extension PredicateWithMetadataKey: MetadataPredicateConvertible {
156
+ public var operatorRawValue: Int? {
157
+ guard let operatorType = operatorType else {
196
158
  return nil
197
159
  }
160
+ return Int(operatorType.rawValue)
198
161
  }
199
- return nil
200
- }
201
-
202
- func getComparisonPredicateOperator(_ op: ComparisonPredicateOperator?) -> NSComparisonPredicate
203
- .Operator? {
204
- if let rawValue = op?.rawValue {
205
- if let op = NSComparisonPredicate.Operator.init(rawValue: UInt(rawValue)) {
206
- return op
207
- } else {
208
- warnWithPrefix(
209
- "getComparisonPredicateOperator: Unsupported operator in metadata filter: \(rawValue)")
210
- }
211
- }
212
- return nil
213
- }
214
-
215
- func createMetadataPredicate(_ metadata: PredicateWithMetadataKey?) -> NSPredicate? {
216
- if let metadata = metadata {
217
-
218
- guard let valueVariant = metadata.value else {
219
- return HKQuery.predicateForObjects(withMetadataKey: metadata.withMetadataKey)
220
- }
221
-
222
- let actualValue: Any
223
162
 
224
- switch valueVariant {
163
+ public var metadataValue: Any? {
164
+ switch value {
225
165
  case .first(let boolValue):
226
- actualValue = NSNumber(value: boolValue ? 1 : 0)
166
+ return NSNumber(value: boolValue ? 1 : 0)
227
167
  case .second(let stringValue):
228
- actualValue = stringValue
168
+ return stringValue
229
169
  case .third(let doubleValue):
230
- actualValue = NSNumber(value: doubleValue)
170
+ return NSNumber(value: doubleValue)
231
171
  case .fourth(let dateValue):
232
- actualValue = dateValue
233
- }
234
-
235
- if let operatorType = metadata.operatorType != nil
236
- ? getComparisonPredicateOperator(metadata.operatorType)
237
- : .equalTo {
238
- return HKQuery.predicateForObjects(
239
- withMetadataKey: metadata.withMetadataKey,
240
- operatorType: operatorType,
241
- value: actualValue
242
- )
172
+ return dateValue
173
+ case nil:
174
+ return nil
243
175
  }
244
-
245
176
  }
246
- return nil
247
177
  }
248
178
 
249
179
  func createPredicateForSamplesBase(_ filter: FilterForSamplesBase?) -> NSPredicate? {
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  func emptyStatisticsResponse(from: Date?, to: Date?) -> QueryStatisticsResponse {
5
6
  return QueryStatisticsResponse(
@@ -47,13 +48,13 @@ func queryStatisticsForQuantityInternal(
47
48
  }
48
49
  }
49
50
 
50
- store.execute(query)
51
+ healthStore.execute(query)
51
52
  }
52
53
  }
53
54
 
54
55
  func serializeStatistics(gottenStats: HKStatistics, unit: HKUnit) -> QueryStatisticsResponse {
55
56
  let sources = gottenStats.sources?.map { source in
56
- return serializeSource(source)
57
+ return serializeSourceStruct(source)
57
58
  } ?? []
58
59
 
59
60
  var averageQuantity: Quantity?
@@ -178,7 +179,7 @@ func queryStatisticsCollectionForQuantityInternal(
178
179
  }
179
180
  }
180
181
 
181
- store.execute(query)
182
+ healthStore.execute(query)
182
183
  }
183
184
  }
184
185
 
@@ -244,7 +245,7 @@ func serializeStatisticsPerSource(gottenStats: HKStatistics, unit: HKUnit)
244
245
  }
245
246
 
246
247
  return QueryStatisticsResponseFromSingleSource(
247
- source: serializeSource(source),
248
+ source: serializeSourceStruct(source),
248
249
  startDate: gottenStats.startDate,
249
250
  endDate: gottenStats.endDate,
250
251
  duration: duration,
@@ -313,11 +314,13 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
313
314
  return Promise.async {
314
315
  let quantityType = try initializeQuantityType(identifier.stringValue)
315
316
 
316
- if let gottenStats = try await queryStatisticsForQuantityInternal(
317
- quantityType: quantityType,
318
- statistics: statistics,
319
- options: options
320
- ) {
317
+ if let gottenStats = try await retryingWhileStoreReopens({
318
+ try await queryStatisticsForQuantityInternal(
319
+ quantityType: quantityType,
320
+ statistics: statistics,
321
+ options: options
322
+ )
323
+ }) {
321
324
  let unit = try await getUnitToUse(
322
325
  unitOverride: options?.unit,
323
326
  quantityType: quantityType
@@ -336,13 +339,15 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
336
339
  return Promise.async {
337
340
  let quantityType = try initializeQuantityType(identifier.stringValue)
338
341
 
339
- if let statistics = try await queryStatisticsCollectionForQuantityInternal(
340
- quantityType: quantityType,
341
- statistics: statistics,
342
- anchorDate: anchorDate,
343
- intervalComponents: intervalComponents,
344
- options: options
345
- ) {
342
+ if let statistics = try await retryingWhileStoreReopens({
343
+ try await queryStatisticsCollectionForQuantityInternal(
344
+ quantityType: quantityType,
345
+ statistics: statistics,
346
+ anchorDate: anchorDate,
347
+ intervalComponents: intervalComponents,
348
+ options: options
349
+ )
350
+ }) {
346
351
 
347
352
  let unit = try await getUnitToUse(
348
353
  unitOverride: options?.unit,
@@ -385,11 +390,13 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
385
390
  return Promise.async {
386
391
  let quantityType = try initializeQuantityType(identifier.stringValue)
387
392
 
388
- if let gottenStats = try await queryStatisticsForQuantityInternal(
389
- quantityType: quantityType,
390
- statistics: statistics,
391
- options: options
392
- ) {
393
+ if let gottenStats = try await retryingWhileStoreReopens({
394
+ try await queryStatisticsForQuantityInternal(
395
+ quantityType: quantityType,
396
+ statistics: statistics,
397
+ options: options
398
+ )
399
+ }) {
393
400
  let unit = try await getUnitToUse(
394
401
  unitOverride: options?.unit,
395
402
  quantityType: quantityType
@@ -412,13 +419,15 @@ class QuantityTypeModule: HybridQuantityTypeModuleSpec {
412
419
  return Promise.async {
413
420
  let quantityType = try initializeQuantityType(identifier.stringValue)
414
421
 
415
- if let statistics = try await queryStatisticsCollectionForQuantityInternal(
416
- quantityType: quantityType,
417
- statistics: statistics,
418
- anchorDate: anchorDate,
419
- intervalComponents: intervalComponents,
420
- options: options
421
- ) {
422
+ if let statistics = try await retryingWhileStoreReopens({
423
+ try await queryStatisticsCollectionForQuantityInternal(
424
+ quantityType: quantityType,
425
+ statistics: statistics,
426
+ anchorDate: anchorDate,
427
+ intervalComponents: intervalComponents,
428
+ options: options
429
+ )
430
+ }) {
422
431
  let unit = try await getUnitToUse(
423
432
  unitOverride: options?.unit,
424
433
  quantityType: quantityType
@@ -8,12 +8,7 @@
8
8
  import Foundation
9
9
  import HealthKit
10
10
  import NitroModules
11
-
12
- private let metadataDateFormatter: ISO8601DateFormatter = {
13
- let formatter = ISO8601DateFormatter()
14
- formatter.formatOptions = [.withInternetDateTime, .withFractionalSeconds]
15
- return formatter
16
- }()
11
+ import ReactNativeHealthkitCore
17
12
 
18
13
  func serializeQuantityTyped(unit: HKUnit, quantityNullable: HKQuantity?) -> Quantity? {
19
14
  guard let q = quantityNullable else {
@@ -43,10 +38,10 @@ func serializeQuantitySample(sample: HKQuantitySample, unit: HKUnit) throws -> Q
43
38
  startDate: sample.startDate,
44
39
  endDate: sample.endDate,
45
40
  hasUndeterminedDuration: sample.hasUndeterminedDuration,
46
- metadata: serializeMetadata(sample.metadata),
47
41
  uuid: sample.uuid.uuidString,
48
42
  sourceRevision: serializeSourceRevision(sample.sourceRevision),
49
- device: serializeDevice(hkDevice: sample.device)
43
+ device: serializeDevice(hkDevice: sample.device),
44
+ metadata: serializeMetadata(sample.metadata)
50
45
  )
51
46
  }
52
47
  throw runtimeErrorWithPrefix(
@@ -62,25 +57,38 @@ func serializeDeletedSample(sample: HKDeletedObject) -> DeletedSample {
62
57
 
63
58
  func serializeCategorySample(sample: HKCategorySample) -> CategorySample {
64
59
  return CategorySample(
65
- categoryType: CategoryTypeIdentifier(fromString: sample.categoryType.identifier)!,
60
+ categoryType: CategoryTypeIdentifier(
61
+ fromString: categoryTypeIdentifierName(
62
+ fromHealthKitIdentifier: sample.categoryType.identifier))!,
66
63
  value: Double(sample.value),
67
64
  sampleType: serializeSampleType(sample.sampleType),
68
65
  startDate: sample.startDate,
69
66
  endDate: sample.endDate,
70
67
  hasUndeterminedDuration: sample.hasUndeterminedDuration,
71
- metadata: serializeMetadata(sample.metadata),
72
68
  uuid: sample.uuid.uuidString,
73
69
  sourceRevision: serializeSourceRevision(sample.sourceRevision),
74
- device: serializeDevice(hkDevice: sample.device)
70
+ device: serializeDevice(hkDevice: sample.device),
71
+ metadata: serializeMetadata(sample.metadata)
75
72
  )
76
73
  }
77
74
 
75
+ /// Wraps an `HKSource` in a proxy HybridObject. Only use this where the
76
+ /// caller explicitly asks for a source handle (`querySources`,
77
+ /// `currentAppSource`); everything embedded in serialized values should use
78
+ /// `serializeSourceStruct` so that no per-sample native object is allocated.
78
79
  func serializeSource(_ source: HKSource) -> SourceProxy {
79
80
  return SourceProxy(
80
81
  source: source
81
82
  )
82
83
  }
83
84
 
85
+ func serializeSourceStruct(_ source: HKSource) -> Source {
86
+ return Source(
87
+ name: source.name,
88
+ bundleIdentifier: source.bundleIdentifier
89
+ )
90
+ }
91
+
84
92
  func serializeUnknownQuantityTyped(quantity: HKQuantity?) -> Quantity? {
85
93
  guard let quantity = quantity else {
86
94
  return nil
@@ -210,59 +218,19 @@ func serializeUnknownQuantity(quantity: HKQuantity) -> [String: AnyValue]? {
210
218
  return nil
211
219
  }
212
220
 
213
- func serializeMetadata(_ metadata: [String: Any]?) -> AnyMap {
214
- let serialized = AnyMap()
215
- if let m = metadata {
216
- for item in m {
217
- if let number = item.value as? NSNumber {
218
- if isKnownBooleanMetadataKey(item.key) {
219
- serialized.setBoolean(key: item.key, value: number.boolValue)
220
- } else if isKnownNumericMetadataKey(item.key) {
221
- serialized.setDouble(key: item.key, value: number.doubleValue)
222
- } else if CFGetTypeID(number) == CFBooleanGetTypeID() {
223
- serialized.setBoolean(key: item.key, value: number.boolValue)
224
- } else {
225
- serialized.setDouble(key: item.key, value: number.doubleValue)
226
- }
227
- continue
228
- }
229
-
230
- if let bool = item.value as? Bool {
231
- serialized.setBoolean(key: item.key, value: bool)
232
- continue
233
- }
234
-
235
- if let str = item.value as? String {
236
- serialized.setString(key: item.key, value: str)
237
- continue
238
- }
239
-
240
- if let double = item.value as? Double {
241
- serialized.setDouble(key: item.key, value: double)
242
- continue
243
- }
244
-
245
- if let date = item.value as? Date {
246
- serialized.setString(
247
- key: item.key,
248
- value: metadataDateFormatter.string(from: date)
249
- )
250
- continue
251
- }
252
-
253
- if let quantity = item.value as? HKQuantity {
254
- if let s = serializeUnknownQuantity(quantity: quantity) {
255
- serialized.setObject(key: item.key, value: s)
256
- }
257
- continue
258
- }
259
-
260
- if let dict = item.value as? [String: AnyValue] {
261
- serialized.setObject(key: item.key, value: dict)
262
- }
221
+ private let metadataSerializationOptions = MetadataSerializationOptions(
222
+ isBooleanKey: isKnownBooleanMetadataKey,
223
+ isNumericKey: isKnownNumericMetadataKey,
224
+ serializeQuantity: { quantity in
225
+ if let serialized = serializeUnknownQuantity(quantity: quantity) {
226
+ return .object(serialized)
263
227
  }
228
+ return nil
264
229
  }
265
- return serialized
230
+ )
231
+
232
+ func serializeMetadata(_ metadata: [String: Any]?) -> AnyMap {
233
+ return ReactNativeHealthkitCore.serializeMetadata(metadata, options: metadataSerializationOptions)
266
234
  }
267
235
 
268
236
  func serializeDevice(hkDevice: HKDevice?) -> Device? {
@@ -282,15 +250,9 @@ func serializeDevice(hkDevice: HKDevice?) -> Device? {
282
250
  )
283
251
  }
284
252
 
285
- func serializeOperatingSystemVersion(_ version: OperatingSystemVersion) -> String {
286
- let versionString = "\(version.majorVersion).\(version.minorVersion).\(version.patchVersion)"
287
-
288
- return versionString
289
- }
290
-
291
253
  func serializeSourceRevision(_ hkSourceRevision: HKSourceRevision) -> SourceRevision {
292
254
  return SourceRevision(
293
- source: serializeSource(hkSourceRevision.source),
255
+ source: serializeSourceStruct(hkSourceRevision.source),
294
256
  version: hkSourceRevision.version,
295
257
  operatingSystemVersion: serializeOperatingSystemVersion(
296
258
  hkSourceRevision.operatingSystemVersion),
@@ -300,7 +262,9 @@ func serializeSourceRevision(_ hkSourceRevision: HKSourceRevision) -> SourceRevi
300
262
 
301
263
  func serializeSampleType(_ sampleType: HKSampleType) -> SampleType {
302
264
  return SampleType(
303
- identifier: sampleType.identifier,
265
+ identifier: sampleType is HKCategoryType
266
+ ? categoryTypeIdentifierName(fromHealthKitIdentifier: sampleType.identifier)
267
+ : sampleType.identifier,
304
268
  allowsRecalibrationForEstimates: sampleType.allowsRecalibrationForEstimates,
305
269
  isMinimumDurationRestricted: sampleType.isMinimumDurationRestricted,
306
270
  isMaximumDurationRestricted: sampleType.isMaximumDurationRestricted
@@ -7,6 +7,7 @@
7
7
 
8
8
  import HealthKit
9
9
  import NitroModules
10
+ import ReactNativeHealthkitCore
10
11
 
11
12
  class SourceProxy: HybridSourceProxySpec {
12
13
  func toJSON(key: String?) throws -> Source {
@@ -14,11 +15,7 @@ class SourceProxy: HybridSourceProxySpec {
14
15
  warnWithPrefix("SourceProxy does not support toJSON with key: \(key!)")
15
16
  }
16
17
 
17
- return Source(
18
- name: self.name,
19
- bundleIdentifier: self.bundleIdentifier
20
- )
21
-
18
+ return serializeSourceStruct(source)
22
19
  }
23
20
 
24
21
  let source: HKSource
@@ -31,5 +28,18 @@ class SourceProxy: HybridSourceProxySpec {
31
28
  self.source = source
32
29
  self.name = source.name
33
30
  self.bundleIdentifier = source.bundleIdentifier
31
+ // HKSource object with its own name and bundle identifier strings, plus
32
+ // the Swift copies of those strings. Computed once; Nitro reads it on
33
+ // every conversion to JS.
34
+ self.memorySize =
35
+ 128
36
+ + 2 * estimateStringMemorySize(source.name)
37
+ + 2 * estimateStringMemorySize(source.bundleIdentifier)
38
+ + nitroHybridObjectOverheadBytes
34
39
  }
40
+
41
+ /// Estimated heap footprint of the wrapped `HKSource`, reported to Nitro so
42
+ /// the JS garbage collector accounts for the native memory held by each
43
+ /// proxy. Nitro adds the Swift instance's own size on top.
44
+ let memorySize: Int
35
45
  }
@@ -1,5 +1,6 @@
1
1
  import HealthKit
2
2
  import NitroModules
3
+ import ReactNativeHealthkitCore
3
4
 
4
5
  #if compiler(>=6)
5
6
 
@@ -40,10 +41,10 @@ import NitroModules
40
41
  startDate: sample.startDate,
41
42
  endDate: sample.endDate,
42
43
  hasUndeterminedDuration: sample.hasUndeterminedDuration,
43
- metadata: serializeMetadata(sample.metadata),
44
44
  uuid: sample.uuid.uuidString,
45
45
  sourceRevision: serializeSourceRevision(sample.sourceRevision),
46
- device: serializeDevice(hkDevice: sample.device)
46
+ device: serializeDevice(hkDevice: sample.device),
47
+ metadata: serializeMetadata(sample.metadata)
47
48
  )
48
49
  }
49
50
 
@@ -8,6 +8,7 @@
8
8
  import CoreLocation
9
9
  import HealthKit
10
10
  import NitroModules
11
+ import ReactNativeHealthkitCore
11
12
 
12
13
  @available(iOS 17.0.0, *)
13
14
  func getWorkoutPlanInternal(workout: HKWorkout) async throws -> WorkoutPlan? {
@@ -49,38 +50,43 @@ func getWorkoutRoutesInternal(
49
50
 
50
51
  func getRouteLocations(
51
52
  route: HKWorkoutRoute
52
- ) async -> [CLLocation] {
53
- let locations = try! await withCheckedThrowingContinuation {
53
+ ) async throws -> [CLLocation] {
54
+ return try await withCheckedThrowingContinuation {
54
55
  (continuation: CheckedContinuation<[CLLocation], Error>) in
55
56
  var allLocations: [CLLocation] = []
57
+ // HealthKit streams locations in batches through the same handler; make
58
+ // sure the continuation is resumed exactly once whatever the sequence.
59
+ var hasFinished = false
60
+ let finish = { (result: Result<[CLLocation], Error>) in
61
+ guard !hasFinished else {
62
+ return
63
+ }
64
+ hasFinished = true
65
+ continuation.resume(with: result)
66
+ }
56
67
 
57
68
  let query = HKWorkoutRouteQuery(route: route) {
58
69
  (_, locationsOrNil, done, errorOrNil) in
59
70
 
60
71
  DispatchQueue.main.async {
61
72
  if let error = errorOrNil {
62
- continuation.resume(throwing: error)
63
- return
73
+ return finish(.failure(error))
64
74
  }
65
75
 
66
76
  guard let currentLocationBatch = locationsOrNil else {
67
- return continuation.resume(
68
- throwing: runtimeErrorWithPrefix("Unexpected empty response")
69
- )
77
+ return finish(.failure(runtimeErrorWithPrefix("Unexpected empty response")))
70
78
  }
71
79
 
72
80
  allLocations.append(contentsOf: currentLocationBatch)
73
81
 
74
82
  if done {
75
- continuation.resume(returning: allLocations)
83
+ finish(.success(allLocations))
76
84
  }
77
85
  }
78
86
  }
79
87
 
80
- store.execute(query)
88
+ healthStore.execute(query)
81
89
  }
82
-
83
- return locations
84
90
  }
85
91
 
86
92
  func serializeLocation(location: CLLocation, previousLocation: CLLocation?)
@@ -122,7 +128,7 @@ func getSerializedWorkoutLocations(
122
128
  route.metadata
123
129
  )
124
130
 
125
- let routeCLLocations = await getRouteLocations(
131
+ let routeCLLocations = try await getRouteLocations(
126
132
  route: route
127
133
  )
128
134
 
@@ -161,7 +167,7 @@ func saveWorkoutRouteInternal(
161
167
  // create CLLocations and return if locations are empty
162
168
  let clLocations = mapLocations(from: locations)
163
169
  let routeBuilder = HKWorkoutRouteBuilder(
164
- healthStore: store,
170
+ healthStore: healthStore,
165
171
  device: nil
166
172
  )
167
173
  try await routeBuilder.insertRouteData(clLocations)
@@ -397,8 +403,20 @@ class WorkoutProxy: HybridWorkoutProxySpec {
397
403
 
398
404
  init(workout: HKWorkout) {
399
405
  self.workout = workout
406
+ // Computed here rather than lazily: proxies are created inside
407
+ // `Promise.async` on a background executor, while Nitro reads `memorySize`
408
+ // on the JS thread every time the proxy is converted to a JS object.
409
+ self.memorySize = estimateWorkoutMemorySize(workout) + nitroHybridObjectOverheadBytes
400
410
  }
401
411
 
412
+ /// Estimated heap footprint of the wrapped `HKWorkout` graph (events,
413
+ /// activities, statistics, metadata), reported to Nitro so the JS garbage
414
+ /// collector sees the native memory kept alive by each proxy. Nitro adds
415
+ /// the Swift instance's own size on top. Rough by design: the goal is the
416
+ /// right order of magnitude per workout, so hundreds of proxies from a query
417
+ /// do not look like hundreds of empty objects.
418
+ let memorySize: Int
419
+
402
420
  func getWorkoutPlan() throws -> Promise<WorkoutPlan?> {
403
421
  return Promise.async {
404
422
  if #available(iOS 17.0.0, *) {