@tryterra/react-native 0.0.0-stage → 2.2.1

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 (112) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +142 -2
  3. package/TerraReactNative.podspec +48 -0
  4. package/android/build.gradle +87 -0
  5. package/android/src/bridgeCore/kotlin/README.md +1 -0
  6. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
  7. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
  8. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +557 -0
  9. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
  10. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +300 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +50 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +582 -0
  19. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +1569 -0
  20. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  21. package/android/src/main/AndroidManifest.xml +24 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  24. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  25. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  26. package/docs/availability.md +396 -0
  27. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +82672 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2186 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2186 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2163 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6594 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +82672 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2186 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2186 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2163 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +82672 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2186 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2186 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2163 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6595 -0
  56. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6513 -0
  57. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  58. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  59. package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
  60. package/ios/TerraBridgeCore/Conversions.swift +594 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  62. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +967 -0
  63. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +2050 -0
  64. package/ios/TerraBridgeCore/Locked.swift +15 -0
  65. package/ios/TerraBridgeCore/README.md +1 -0
  66. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  67. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  68. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  69. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  70. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +318 -0
  71. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  72. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  73. package/ios/TerraBridgeModule.mm +79 -0
  74. package/ios/TerraBridgeModuleImpl.swift +49 -0
  75. package/ios/TerraReactNative.swift +19 -0
  76. package/lib/EndUser.d.ts +12 -0
  77. package/lib/EndUser.js +13 -0
  78. package/lib/NativeTerraBridge.d.ts +23 -0
  79. package/lib/NativeTerraBridge.js +13 -0
  80. package/lib/Terra.d.ts +223 -0
  81. package/lib/Terra.js +336 -0
  82. package/lib/TerraError.d.ts +42 -0
  83. package/lib/TerraError.js +95 -0
  84. package/lib/createTerra.d.ts +8 -0
  85. package/lib/createTerra.js +19 -0
  86. package/lib/generated/client.d.ts +128 -0
  87. package/lib/generated/client.js +256 -0
  88. package/lib/generated/codecs.d.ts +219 -0
  89. package/lib/generated/codecs.js +2066 -0
  90. package/lib/generated/errors.d.ts +13 -0
  91. package/lib/generated/errors.js +68 -0
  92. package/lib/generated/ops.d.ts +13 -0
  93. package/lib/generated/ops.js +161 -0
  94. package/lib/generated/streams.d.ts +23 -0
  95. package/lib/generated/streams.js +58 -0
  96. package/lib/generated/types.d.ts +794 -0
  97. package/lib/generated/types.js +224 -0
  98. package/lib/index.d.ts +29 -0
  99. package/lib/index.js +55 -0
  100. package/package.json +71 -4
  101. package/src/EndUser.ts +15 -0
  102. package/src/NativeTerraBridge.ts +27 -0
  103. package/src/Terra.ts +592 -0
  104. package/src/TerraError.ts +116 -0
  105. package/src/createTerra.ts +18 -0
  106. package/src/generated/client.ts +315 -0
  107. package/src/generated/codecs.ts +1989 -0
  108. package/src/generated/errors.ts +113 -0
  109. package/src/generated/ops.ts +233 -0
  110. package/src/generated/streams.ts +74 -0
  111. package/src/generated/types.ts +1222 -0
  112. package/src/index.ts +54 -0
@@ -0,0 +1,250 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.BloodGlucoseSample
4
+ import co.tryterra.sdk.BloodGlucoseUnit
5
+ import co.tryterra.sdk.BodyMeasurement
6
+ import co.tryterra.sdk.BodyReading
7
+ import co.tryterra.sdk.BodyTemperatureSample
8
+ import co.tryterra.sdk.DayTotal
9
+ import co.tryterra.sdk.HydrationSample
10
+ import co.tryterra.sdk.IntradaySample
11
+ import co.tryterra.sdk.MenstrualEvent
12
+ import co.tryterra.sdk.MenstruationEventSample
13
+ import co.tryterra.sdk.RestingHeartRateSample
14
+ import co.tryterra.sdk.Vo2MaxSample
15
+ import co.tryterra.sdk.WorkoutIntensitySample
16
+ import co.tryterra.sdk.WorkoutRoute
17
+ import co.tryterra.sdk.bridge.generated.BridgeInstant
18
+ import co.tryterra.sdk.health.StoreAvailability
19
+ import co.tryterra.sdk.bridge.generated.BloodGlucoseUnit as WireGlucoseUnit
20
+ import co.tryterra.sdk.bridge.generated.BodyMetric as WireBodyMetric
21
+ import co.tryterra.sdk.bridge.generated.BodyReading as WireBodyReading
22
+ import co.tryterra.sdk.bridge.generated.BodyReadingKind as WireBodyReadingKind
23
+ import co.tryterra.sdk.bridge.generated.BodyTemperatureMetric as WireTemperatureMetric
24
+ import co.tryterra.sdk.bridge.generated.DayTotal as WireDayTotal
25
+ import co.tryterra.sdk.bridge.generated.DayTotalMetric as WireDayTotalMetric
26
+ import co.tryterra.sdk.bridge.generated.GlucoseMealTime as WireMealTime
27
+ import co.tryterra.sdk.bridge.generated.GlucoseSpecimen as WireSpecimen
28
+ import co.tryterra.sdk.bridge.generated.HeartRateVariabilityMetric as WireVariabilityMetric
29
+ import co.tryterra.sdk.bridge.generated.HydrationSample as WireHydrationSample
30
+ import co.tryterra.sdk.bridge.generated.IntradayMetric as WireIntradayMetric
31
+ import co.tryterra.sdk.bridge.generated.IntradaySample as WireIntradaySample
32
+ import co.tryterra.sdk.bridge.generated.MenstrualEvent as WireMenstrualEvent
33
+ import co.tryterra.sdk.bridge.generated.MenstrualEventKind as WireEventKind
34
+ import co.tryterra.sdk.bridge.generated.RestingHeartRateSample as WireRestingHeartRateSample
35
+ import co.tryterra.sdk.bridge.generated.RoutePoint as WireRoutePoint
36
+ import co.tryterra.sdk.bridge.generated.StoreAvailability as WireStoreAvailability
37
+ import co.tryterra.sdk.bridge.generated.VO2MaxTestType as WireTestType
38
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensityMetric as WireIntensityMetric
39
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensitySample as WireIntensitySample
40
+ import co.tryterra.sdk.bridge.generated.WorkoutRoute as WireWorkoutRoute
41
+
42
+ /**
43
+ * The reads that close the rest of the 1.x local-read gap, between the SDK's types and the wire's.
44
+ *
45
+ * Apart from [Conversions] only because there are many of them, and under its rules: every `when`
46
+ * is exhaustive with no `else`, so a case added on either side stops the build here, and a case the
47
+ * read never returns maps to null and is dropped rather than sent under a neighbour's name.
48
+ */
49
+ internal object ReadConversions {
50
+
51
+ /** Null for the metrics `daily.totals` leaves out, which the read never hands over. */
52
+ fun dayTotalMetric(metric: DayTotal.Metric): WireDayTotalMetric? = when (metric) {
53
+ DayTotal.Metric.DISTANCE_WALKING_RUNNING -> WireDayTotalMetric.DISTANCE_WALKING_RUNNING
54
+ DayTotal.Metric.DISTANCE_CYCLING -> WireDayTotalMetric.DISTANCE_CYCLING
55
+ DayTotal.Metric.DISTANCE_SWIMMING -> WireDayTotalMetric.DISTANCE_SWIMMING
56
+ DayTotal.Metric.DISTANCE_WHEELCHAIR -> WireDayTotalMetric.DISTANCE_WHEELCHAIR
57
+ DayTotal.Metric.ACTIVE_ENERGY_BURNED -> WireDayTotalMetric.ACTIVE_ENERGY_BURNED
58
+ DayTotal.Metric.BASAL_ENERGY_BURNED -> WireDayTotalMetric.BASAL_ENERGY_BURNED
59
+ DayTotal.Metric.FLIGHTS_CLIMBED -> WireDayTotalMetric.FLIGHTS_CLIMBED
60
+ DayTotal.Metric.STEPS, DayTotal.Metric.SWIMMING_STROKE_COUNT -> null
61
+ }
62
+
63
+ fun dayTotal(total: DayTotal): WireDayTotal? = dayTotalMetric(total.metric)?.let {
64
+ WireDayTotal(
65
+ metric = it,
66
+ start = BridgeInstant.format(total.interval.start),
67
+ end = BridgeInstant.format(total.interval.end),
68
+ value = total.value,
69
+ unit = total.unit,
70
+ timeZone = total.timeZone?.id,
71
+ )
72
+ }
73
+
74
+ fun intradayMetric(metric: IntradaySample.Metric): WireIntradayMetric = when (metric) {
75
+ IntradaySample.Metric.ACTIVE_ENERGY -> WireIntradayMetric.ACTIVE_ENERGY
76
+ IntradaySample.Metric.DISTANCE -> WireIntradayMetric.DISTANCE
77
+ IntradaySample.Metric.FLOORS_CLIMBED -> WireIntradayMetric.FLOORS_CLIMBED
78
+ }
79
+
80
+ fun intraday(sample: IntradaySample): WireIntradaySample = WireIntradaySample(
81
+ id = sample.id.value,
82
+ metric = intradayMetric(sample.metric),
83
+ start = BridgeInstant.format(sample.interval.start),
84
+ end = BridgeInstant.format(sample.interval.end),
85
+ value = sample.value,
86
+ source = Conversions.sampleSource(sample.source),
87
+ userEntered = sample.userEntered,
88
+ timeZone = sample.timeZone?.id,
89
+ )
90
+
91
+ fun restingHeartRate(sample: RestingHeartRateSample): WireRestingHeartRateSample = WireRestingHeartRateSample(
92
+ id = sample.id.value,
93
+ start = BridgeInstant.format(sample.interval.start),
94
+ end = BridgeInstant.format(sample.interval.end),
95
+ beatsPerMinute = sample.beatsPerMinute,
96
+ source = Conversions.sampleSource(sample.source),
97
+ userEntered = sample.userEntered,
98
+ timeZone = sample.timeZone?.id,
99
+ )
100
+
101
+ /** Null for the three `body.readings` leaves out. */
102
+ fun bodyMetric(metric: BodyMeasurement.Metric): WireBodyMetric? = when (metric) {
103
+ BodyMeasurement.Metric.WEIGHT -> WireBodyMetric.WEIGHT
104
+ BodyMeasurement.Metric.HEIGHT -> WireBodyMetric.HEIGHT
105
+ BodyMeasurement.Metric.BODY_FAT_PERCENTAGE -> WireBodyMetric.BODY_FAT_PERCENTAGE
106
+ BodyMeasurement.Metric.LEAN_BODY_MASS -> WireBodyMetric.LEAN_BODY_MASS
107
+ BodyMeasurement.Metric.BONE_MASS,
108
+ BodyMeasurement.Metric.BODY_WATER_MASS,
109
+ BodyMeasurement.Metric.BASAL_METABOLIC_RATE,
110
+ -> null
111
+ }
112
+
113
+ fun glucoseUnit(unit: BloodGlucoseUnit): WireGlucoseUnit = when (unit) {
114
+ BloodGlucoseUnit.MILLIGRAMS_PER_DECILITRE -> WireGlucoseUnit.MILLIGRAMS_PER_DECILITRE
115
+ BloodGlucoseUnit.MILLIMOLES_PER_LITRE -> WireGlucoseUnit.MILLIMOLES_PER_LITRE
116
+ }
117
+
118
+ fun mealTime(time: BloodGlucoseSample.MealTime): WireMealTime = when (time) {
119
+ BloodGlucoseSample.MealTime.PREPRANDIAL -> WireMealTime.PREPRANDIAL
120
+ BloodGlucoseSample.MealTime.POSTPRANDIAL -> WireMealTime.POSTPRANDIAL
121
+ BloodGlucoseSample.MealTime.UNKNOWN -> WireMealTime.UNKNOWN
122
+ }
123
+
124
+ fun specimen(specimen: BloodGlucoseSample.Specimen): WireSpecimen = when (specimen) {
125
+ BloodGlucoseSample.Specimen.CAPILLARY -> WireSpecimen.CAPILLARY
126
+ BloodGlucoseSample.Specimen.INTERSTITIAL -> WireSpecimen.INTERSTITIAL
127
+ BloodGlucoseSample.Specimen.PLASMA -> WireSpecimen.PLASMA
128
+ BloodGlucoseSample.Specimen.SERUM -> WireSpecimen.SERUM
129
+ BloodGlucoseSample.Specimen.TEARS -> WireSpecimen.TEARS
130
+ BloodGlucoseSample.Specimen.WHOLE_BLOOD -> WireSpecimen.WHOLE_BLOOD
131
+ BloodGlucoseSample.Specimen.UNKNOWN -> WireSpecimen.UNKNOWN
132
+ }
133
+
134
+ fun temperatureMetric(metric: BodyTemperatureSample.Metric): WireTemperatureMetric = when (metric) {
135
+ BodyTemperatureSample.Metric.BODY -> WireTemperatureMetric.BODY
136
+ BodyTemperatureSample.Metric.BASAL -> WireTemperatureMetric.BASAL
137
+ BodyTemperatureSample.Metric.SKIN -> WireTemperatureMetric.SKIN
138
+ }
139
+
140
+ fun testType(type: Vo2MaxSample.TestType): WireTestType = when (type) {
141
+ Vo2MaxSample.TestType.MAX_EXERCISE -> WireTestType.MAX_EXERCISE
142
+ Vo2MaxSample.TestType.PREDICTED_SUB_MAX_EXERCISE -> WireTestType.PREDICTED_SUB_MAX_EXERCISE
143
+ Vo2MaxSample.TestType.PREDICTED_NON_EXERCISE -> WireTestType.PREDICTED_NON_EXERCISE
144
+ }
145
+
146
+ fun bodyReadingKind(kind: BodyReading.Kind): WireBodyReadingKind? = when (kind) {
147
+ is BodyReading.Kind.Measurement -> bodyMetric(kind.metric)?.let { WireBodyReadingKind.Measurement(it, kind.value) }
148
+ is BodyReading.Kind.BloodPressure ->
149
+ WireBodyReadingKind.BloodPressure(kind.systolicMillimetersOfMercury, kind.diastolicMillimetersOfMercury)
150
+ is BodyReading.Kind.BloodGlucose -> WireBodyReadingKind.BloodGlucose(
151
+ value = kind.value,
152
+ unit = glucoseUnit(kind.unit),
153
+ mealTime = kind.mealTime?.let(::mealTime),
154
+ specimen = kind.specimen?.let(::specimen),
155
+ )
156
+ is BodyReading.Kind.OxygenSaturation -> WireBodyReadingKind.OxygenSaturation(kind.fraction)
157
+ is BodyReading.Kind.Temperature -> WireBodyReadingKind.Temperature(temperatureMetric(kind.metric), kind.celsius)
158
+ // RMSSD is the only statistic Health Connect records.
159
+ is BodyReading.Kind.HeartRateVariability ->
160
+ WireBodyReadingKind.HeartRateVariability(WireVariabilityMetric.RMSSD, kind.rmssdMilliseconds)
161
+ is BodyReading.Kind.RespiratoryRate -> WireBodyReadingKind.RespiratoryRate(kind.breathsPerMinute)
162
+ is BodyReading.Kind.Vo2Max ->
163
+ WireBodyReadingKind.Vo2Max(kind.millilitresPerKilogramPerMinute, kind.testType?.let(::testType))
164
+ }
165
+
166
+ fun bodyReading(reading: BodyReading): WireBodyReading? = bodyReadingKind(reading.kind)?.let {
167
+ WireBodyReading(
168
+ id = reading.id.value,
169
+ kind = it,
170
+ start = BridgeInstant.format(reading.interval.start),
171
+ end = BridgeInstant.format(reading.interval.end),
172
+ source = Conversions.sampleSource(reading.source),
173
+ userEntered = reading.userEntered,
174
+ timeZone = reading.timeZone?.id,
175
+ )
176
+ }
177
+
178
+ fun route(route: WorkoutRoute): WireWorkoutRoute = WireWorkoutRoute(
179
+ id = route.id.value,
180
+ workoutId = route.workoutId.value,
181
+ start = BridgeInstant.format(route.interval.start),
182
+ end = BridgeInstant.format(route.interval.end),
183
+ points = route.points.map { WireRoutePoint(BridgeInstant.format(it.time), it.latitude, it.longitude) },
184
+ source = Conversions.sampleSource(route.source),
185
+ timeZone = route.timeZone?.id,
186
+ )
187
+
188
+ fun intensityMetric(metric: WorkoutIntensitySample.Metric): WireIntensityMetric = when (metric) {
189
+ WorkoutIntensitySample.Metric.SPEED -> WireIntensityMetric.SPEED
190
+ WorkoutIntensitySample.Metric.POWER -> WireIntensityMetric.POWER
191
+ WorkoutIntensitySample.Metric.CADENCE -> WireIntensityMetric.CADENCE
192
+ }
193
+
194
+ /** An instant on Health Connect, so both ends are its time, and no workout it could name. */
195
+ fun intensity(sample: WorkoutIntensitySample): WireIntensitySample = WireIntensitySample(
196
+ id = sample.id.value,
197
+ metric = intensityMetric(sample.metric),
198
+ workoutId = null,
199
+ start = BridgeInstant.format(sample.time),
200
+ end = BridgeInstant.format(sample.time),
201
+ value = sample.value,
202
+ source = Conversions.sampleSource(sample.source),
203
+ userEntered = sample.userEntered,
204
+ timeZone = sample.timeZone?.id,
205
+ )
206
+
207
+ fun hydration(sample: HydrationSample): WireHydrationSample = WireHydrationSample(
208
+ id = sample.id.value,
209
+ start = BridgeInstant.format(sample.interval.start),
210
+ end = BridgeInstant.format(sample.interval.end),
211
+ millilitres = sample.millilitres,
212
+ source = Conversions.sampleSource(sample.source),
213
+ userEntered = sample.userEntered,
214
+ timeZone = sample.timeZone?.id,
215
+ )
216
+
217
+ /**
218
+ * Which types a partner policy refuses, and which feature is missing, stay in the native report:
219
+ * the wire names the reason, and the remedy a wrapper app can offer is the same either way.
220
+ */
221
+ fun availability(availability: StoreAvailability): WireStoreAvailability = when (availability) {
222
+ StoreAvailability.Available -> WireStoreAvailability.Available
223
+ StoreAvailability.ModuleMissing -> WireStoreAvailability.ModuleMissing
224
+ StoreAvailability.ProviderAppMissing -> WireStoreAvailability.ProviderAppMissing
225
+ is StoreAvailability.ProviderAppTooOld -> WireStoreAvailability.ProviderAppTooOld(availability.installed)
226
+ is StoreAvailability.PartnerPolicyMissing -> WireStoreAvailability.PartnerPolicyMissing
227
+ is StoreAvailability.FeatureUnavailable -> WireStoreAvailability.FeatureUnavailable
228
+ }
229
+
230
+ /** Null for sexual activity, which `menstruation.events` leaves out. */
231
+ fun eventKind(event: MenstruationEventSample.Event): WireEventKind? = when (event) {
232
+ MenstruationEventSample.Event.INTERMENSTRUAL_BLEEDING -> WireEventKind.INTERMENSTRUAL_BLEEDING
233
+ MenstruationEventSample.Event.OVULATION_TEST -> WireEventKind.OVULATION_TEST
234
+ MenstruationEventSample.Event.CERVICAL_MUCUS -> WireEventKind.CERVICAL_MUCUS
235
+ MenstruationEventSample.Event.SEXUAL_ACTIVITY -> null
236
+ }
237
+
238
+ fun menstrualEvent(event: MenstrualEvent): WireMenstrualEvent? = eventKind(event.event)?.let {
239
+ WireMenstrualEvent(
240
+ id = event.id.value,
241
+ kind = it,
242
+ time = BridgeInstant.format(event.time),
243
+ value = event.value?.wire,
244
+ storeIdentifier = event.storeIdentifier,
245
+ source = Conversions.sampleSource(event.source),
246
+ userEntered = event.userEntered,
247
+ timeZone = event.timeZone?.id,
248
+ )
249
+ }
250
+ }
@@ -0,0 +1,150 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.NetworkFailure
4
+ import co.tryterra.sdk.TerraError
5
+ import co.tryterra.sdk.bridge.generated.BridgeError
6
+ import co.tryterra.sdk.bridge.generated.Source
7
+ import co.tryterra.sdk.bridge.generated.TerraErrorPayload
8
+ import kotlinx.serialization.json.JsonObject
9
+ import kotlinx.serialization.json.JsonPrimitive
10
+
11
+ /**
12
+ * How a thrown error becomes something a caller in another language can switch on.
13
+ *
14
+ * The code comes from the SDK - [TerraError.code] is the vocabulary in `docs/sdk-reference.md`, and
15
+ * the surface asserts this module speaks exactly it - so there is no mapping table here to drift.
16
+ * What this file owns is the *detail bag*: which pieces of an error's associated values are safe to
17
+ * send, and which are not.
18
+ *
19
+ * Nothing here may put a health value in `details`. An error reaches a customer's crash reporter and
20
+ * their logs, so the bag carries identifiers, counts and reasons, never a sample.
21
+ */
22
+ internal object Rejections {
23
+
24
+ /** The payload for any throwable, including one that is not a [TerraError]. */
25
+ fun of(thrown: Throwable): TerraErrorPayload = when (thrown) {
26
+ is TerraError -> ofTerraError(thrown)
27
+ is BridgeError -> TerraErrorPayload(
28
+ code = thrown.code,
29
+ retryable = thrown.isRetryable,
30
+ message = thrown.message ?: thrown.code,
31
+ details = bridgeDetails(thrown),
32
+ )
33
+ else -> TerraErrorPayload(
34
+ code = "platform",
35
+ retryable = false,
36
+ message = "a platform call failed (${thrown::class.java.name})",
37
+ )
38
+ }
39
+
40
+ private fun ofTerraError(error: TerraError): TerraErrorPayload = TerraErrorPayload(
41
+ code = error.code,
42
+ retryable = error.isRetryable,
43
+ message = message(error),
44
+ source = sourceOf(error),
45
+ details = details(error),
46
+ )
47
+
48
+ /**
49
+ * The text that crosses, which is not always the exception's own.
50
+ *
51
+ * Two cases wrap a throwable this SDK did not raise, and `Exception.message` embeds it: a
52
+ * platform failure's comes from whatever library was reading health records, and a token
53
+ * provider's is as likely to carry the token it was minting as to describe the failure.
54
+ * Dropping the details bag for those was not enough while the message still carried the same
55
+ * text, which is what the test that found this asserts.
56
+ */
57
+ private fun message(error: TerraError): String = when (error) {
58
+ is TerraError.TokenProviderFailed -> "the host's token provider failed"
59
+ is TerraError.Platform -> "a platform call failed"
60
+ else -> error.message ?: error.code
61
+ }
62
+
63
+ /** Only the cases that carry one: a default here would be a claim about where a failure happened. */
64
+ private fun sourceOf(error: TerraError): Source? = when (error) {
65
+ is TerraError.NotConnected -> wire(error.source)
66
+ is TerraError.LinkRevoked -> wire(error.source)
67
+ is TerraError.DevIdMismatch -> wire(error.source)
68
+ is TerraError.LinkAlreadyRunning -> wire(error.source)
69
+ is TerraError.ReadQuotaExceeded -> wire(error.source)
70
+ else -> null
71
+ }
72
+
73
+ private fun wire(source: co.tryterra.sdk.Source): Source = when (source) {
74
+ co.tryterra.sdk.Source.HEALTH -> Source.HEALTH
75
+ co.tryterra.sdk.Source.SAMSUNG_HEALTH -> Source.SAMSUNG_HEALTH
76
+ }
77
+
78
+ /**
79
+ * Exhaustive over the sealed hierarchy with no `else`, which is the point.
80
+ *
81
+ * A case added to [TerraError] stops this module compiling until someone decides what, if
82
+ * anything, of it is safe to send. An `else` here would answer that question with silence.
83
+ */
84
+ private fun details(error: TerraError): JsonObject? = when (error) {
85
+ is TerraError.HealthDataUnavailable -> obj("availability" to JsonPrimitive(error.reason::class.java.simpleName))
86
+ is TerraError.PermissionDenied -> obj("category" to JsonPrimitive(error.category.name.lowercase()))
87
+ is TerraError.RouteAwaitingConverter -> obj("dataType" to JsonPrimitive(error.dataType.wire))
88
+ is TerraError.SyncAlreadyRunning -> obj("dataType" to JsonPrimitive(error.dataType.wire))
89
+ is TerraError.SampleNotFound -> obj("id" to JsonPrimitive(error.id.value))
90
+ is TerraError.DeviceKeyUnavailable -> obj("reason" to JsonPrimitive(error.reason))
91
+ // The cause travels as a code: a nested throwable carries a stack and a message.
92
+ is TerraError.RollbackIncomplete -> obj(
93
+ "remaining" to JsonPrimitive(error.remaining.size),
94
+ "cause" to JsonPrimitive(error.underlying.code),
95
+ )
96
+ is TerraError.DistanceNotAttributable -> obj(
97
+ "activity" to JsonPrimitive(co.tryterra.sdk.WorkoutActivity.reportName(error.activity)),
98
+ )
99
+ // No message. The host's own provider threw, and the text it chose is as likely to contain
100
+ // the token it was minting as it is to describe the failure - `IllegalStateException(
101
+ // "token=...")` is not a hypothetical. The host already knows why its provider failed; what
102
+ // it needs from here is the code.
103
+ is TerraError.TokenProviderFailed -> null
104
+ // No message, for the same reason with a different source. A platform failure's text comes
105
+ // from whatever library raised it, which on this path has been reading health records.
106
+ is TerraError.Platform -> null
107
+ is TerraError.Network -> networkDetails(error.failure)
108
+
109
+ is TerraError.NotConnected,
110
+ is TerraError.LinkRevoked,
111
+ is TerraError.DevIdMismatch,
112
+ is TerraError.LinkAlreadyRunning,
113
+ is TerraError.ReadQuotaExceeded,
114
+ -> null
115
+
116
+ TerraError.TokenRejected,
117
+ TerraError.TokenUnavailable,
118
+ TerraError.SignatureRejected,
119
+ TerraError.ClockSkewed,
120
+ TerraError.UserLimitReached,
121
+ TerraError.LinkConflict,
122
+ TerraError.UpgradeRequired,
123
+ TerraError.StoreLocked,
124
+ TerraError.StoreUnreadable,
125
+ TerraError.DeadlineExceeded,
126
+ TerraError.AuthorizationNotDetermined,
127
+ TerraError.BloodPressureIncomplete,
128
+ TerraError.AuthorizationUnavailableInExtension,
129
+ -> null
130
+ }
131
+
132
+ private fun networkDetails(failure: NetworkFailure): JsonObject? = when (failure) {
133
+ is NetworkFailure.ServerError -> obj("status" to JsonPrimitive(failure.status))
134
+ is NetworkFailure.Throttled -> failure.retryAfterSeconds?.let { obj("retryAfterSeconds" to JsonPrimitive(it)) }
135
+ // The IOException's message names a host and sometimes a path; the code alone is the signal.
136
+ is NetworkFailure.Transport -> null
137
+ NetworkFailure.MalformedResponse -> null
138
+ }
139
+
140
+ private fun bridgeDetails(error: BridgeError): JsonObject? = when (error) {
141
+ is BridgeError.NotBootstrapped -> obj("line" to JsonPrimitive(error.line))
142
+ is BridgeError.UnsupportedOnPlatform -> obj("op" to JsonPrimitive(error.op))
143
+ is BridgeError.TokenTimeout -> obj("timeoutMs" to JsonPrimitive(error.timeoutMs))
144
+ is BridgeError.InvalidPayload -> obj("path" to JsonPrimitive(error.path))
145
+ BridgeError.NoActivity -> null
146
+ BridgeError.PermissionSheetUnavailable -> null
147
+ }
148
+
149
+ private fun obj(vararg pairs: Pair<String, JsonPrimitive>): JsonObject = JsonObject(pairs.toMap())
150
+ }
@@ -0,0 +1,77 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import java.util.concurrent.atomic.AtomicLong
4
+ import kotlinx.coroutines.CoroutineScope
5
+ import kotlinx.coroutines.CoroutineStart
6
+ import kotlinx.coroutines.Job
7
+ import kotlinx.coroutines.flow.Flow
8
+ import kotlinx.coroutines.launch
9
+
10
+ /**
11
+ * One native stream per subscriber, started on subscribe and cancelled on unsubscribe.
12
+ *
13
+ * Sharing one collection would be cheaper and wrong: `changes()` polls while it is collected, so a
14
+ * shared stream keeps polling for a subscriber that has gone away, and that costs the user's
15
+ * battery. [invalidate] is the other half of the same rule, for a host runtime that reloads.
16
+ *
17
+ * Messages arrive fully framed by the generated `BridgeStreamDispatch`, endings included, so what
18
+ * this owns is only the lifetime of each collection.
19
+ */
20
+ internal class StreamRegistry(private val scope: CoroutineScope) {
21
+
22
+ /**
23
+ * Guards [jobs], [closed] and every delivery, so subscribe, teardown and a message in flight
24
+ * are ordered by one lock: nothing is admitted or handed on once [invalidate] returns.
25
+ */
26
+ private val gate = Any()
27
+ private val jobs = HashMap<String, Job>()
28
+ private var closed: Boolean = false
29
+ private val nextId = AtomicLong(0)
30
+
31
+ /**
32
+ * Starts [messages] and sends each one to [sink].
33
+ *
34
+ * Returns the id the host cancels with. The id is minted here rather than supplied, so two
35
+ * subscriptions to the same stream cannot collide on a name the host chose.
36
+ */
37
+ fun subscribe(messages: Flow<String>, sink: (String, String) -> Unit): String {
38
+ val id = "sub-${nextId.incrementAndGet()}"
39
+ val job = scope.launch(start = CoroutineStart.LAZY) {
40
+ messages.collect { message -> deliver(id, message, sink) }
41
+ }
42
+ val admitted = synchronized(gate) {
43
+ if (!closed) jobs[id] = job
44
+ !closed
45
+ }
46
+ if (!admitted) {
47
+ job.cancel()
48
+ return id
49
+ }
50
+ job.invokeOnCompletion { synchronized(gate) { jobs.remove(id) } }
51
+ job.start()
52
+ return id
53
+ }
54
+
55
+ /** Cancels one subscription. An unknown id is ignored: a double unsubscribe is not an error. */
56
+ fun unsubscribe(id: String) {
57
+ synchronized(gate) { jobs.remove(id) }?.cancel()
58
+ }
59
+
60
+ /** Cancels every subscription, for a host runtime that is going away, and admits no later one. */
61
+ fun invalidate() {
62
+ val outstanding = synchronized(gate) {
63
+ closed = true
64
+ jobs.values.toList().also { jobs.clear() }
65
+ }
66
+ outstanding.forEach { it.cancel() }
67
+ }
68
+
69
+ /** Visible for tests: how many subscriptions are live. */
70
+ fun count(): Int = synchronized(gate) { jobs.size }
71
+
72
+ private fun deliver(id: String, message: String, sink: (String, String) -> Unit) {
73
+ synchronized(gate) {
74
+ if (!closed && id in jobs) sink(id, message)
75
+ }
76
+ }
77
+ }
@@ -0,0 +1,86 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.Terra
4
+ import co.tryterra.sdk.bridge.generated.BridgeDispatch
5
+ import co.tryterra.sdk.bridge.generated.BridgeStreamDispatch
6
+ import co.tryterra.sdk.bridge.generated.TerraErrorPayload
7
+ import java.time.ZoneId
8
+ import kotlinx.coroutines.CoroutineScope
9
+
10
+ /**
11
+ * The whole public surface of this module: four calls, and none of them mentions a framework.
12
+ *
13
+ * A React Native TurboModule forwards `invoke` to [dispatch] and its event emitter to [subscribe].
14
+ * A Flutter `MethodChannel` handler is `dispatch(call.method, call.arguments)` and an `EventChannel`
15
+ * wraps [subscribe]. Nothing above this line may leak below it - no `ReadableMap`, no promise type,
16
+ * no `MethodCall` - because the second wrapper is supposed to be an emitter and a shell, not a
17
+ * second bridge.
18
+ *
19
+ * One per host runtime. The client and the [tokens] broker outlive a reload and are shared; this
20
+ * object is what a runtime attaches with, and [invalidate] tears down only what it started.
21
+ *
22
+ * @param tokens the broker whose `provide` the client was built with. A different one would be a
23
+ * broker nobody asks, and every `token.*` op would answer requests that never come.
24
+ */
25
+ public class TerraBridge(
26
+ terra: Terra,
27
+ tokens: TokenBroker,
28
+ host: BridgeHost,
29
+ scope: CoroutineScope,
30
+ deviceZone: ZoneId = ZoneId.systemDefault(),
31
+ /** Receives `(subscriptionId, StreamMessage JSON)`. The framework layer forwards it to its own emitter. */
32
+ private val emit: (String, String) -> Unit,
33
+ ) {
34
+ private val session = tokens.session()
35
+ private val ops = BridgeDispatch(TerraBridgeOpsImpl(terra, host, session))
36
+ private val streams = BridgeStreamDispatch(TerraBridgeStreamsImpl(terra, session, deviceZone), Rejections::of)
37
+ private val registry = StreamRegistry(scope)
38
+
39
+ /**
40
+ * Runs one op and answers its response JSON.
41
+ *
42
+ * Throws [BridgeRejection], never a raw SDK error: the framework layer rejects a promise with a
43
+ * code and a detail bag, and asking it to translate a `TerraError` would put the error table in
44
+ * the RN module and again in the Flutter plugin.
45
+ */
46
+ public suspend fun dispatch(op: String, payload: String): String = try {
47
+ ops.dispatch(op, payload)
48
+ } catch (thrown: Throwable) {
49
+ if (thrown is kotlinx.coroutines.CancellationException) throw thrown
50
+ throw BridgeRejection(Rejections.of(thrown))
51
+ }
52
+
53
+ /**
54
+ * Starts one stream and answers the id that cancels it.
55
+ *
56
+ * Every subscription gets its own native stream. The SDK's `changes()` polls while it is
57
+ * collected, so a shared one would keep polling for a subscriber that had gone away.
58
+ */
59
+ public fun subscribe(stream: String, params: String): String {
60
+ val messages = try {
61
+ streams.open(stream, params)
62
+ } catch (thrown: Throwable) {
63
+ throw BridgeRejection(Rejections.of(thrown))
64
+ }
65
+ return registry.subscribe(messages, emit)
66
+ }
67
+
68
+ public fun unsubscribe(id: String) {
69
+ registry.unsubscribe(id)
70
+ }
71
+
72
+ /**
73
+ * For a host runtime that is going away: a JavaScript reload, or a Flutter engine detaching.
74
+ *
75
+ * The native client outlives it and must not be rebuilt, so what is torn down is everything
76
+ * this runtime started - its subscriptions and its pending token asks - and nothing a
77
+ * replacement runtime has started since.
78
+ */
79
+ public fun invalidate() {
80
+ registry.invalidate()
81
+ session.close()
82
+ }
83
+
84
+ /** What the framework layer turns into a rejected promise. */
85
+ public class BridgeRejection(public val payload: TerraErrorPayload) : Exception(payload.message)
86
+ }