@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,300 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.RecordId
4
+ import co.tryterra.sdk.Source
5
+ import co.tryterra.sdk.Terra
6
+ import co.tryterra.sdk.bridge.generated.BoolResult
7
+ import co.tryterra.sdk.bridge.generated.BootstrapState
8
+ import co.tryterra.sdk.bridge.generated.BridgeError
9
+ import co.tryterra.sdk.bridge.generated.BridgeInstant
10
+ import co.tryterra.sdk.bridge.generated.CategoryRequest
11
+ import co.tryterra.sdk.bridge.generated.Connection
12
+ import co.tryterra.sdk.bridge.generated.ConnectRequest
13
+ import co.tryterra.sdk.bridge.generated.ConnectionList
14
+ import co.tryterra.sdk.bridge.generated.DiagnosticsState
15
+ import co.tryterra.sdk.bridge.generated.DietaryQuantityList
16
+ import co.tryterra.sdk.bridge.generated.Empty
17
+ import co.tryterra.sdk.bridge.generated.HealthAccess
18
+ import co.tryterra.sdk.bridge.generated.HeartRateEntryList
19
+ import co.tryterra.sdk.bridge.generated.HeartRateList
20
+ import co.tryterra.sdk.bridge.generated.LastDeliveryResult
21
+ import co.tryterra.sdk.bridge.generated.MenstruationFlowDayList
22
+ import co.tryterra.sdk.bridge.generated.NutritionEntryList
23
+ import co.tryterra.sdk.bridge.generated.PermissionGrant
24
+ import co.tryterra.sdk.bridge.generated.PermissionRequest
25
+ import co.tryterra.sdk.bridge.generated.RangeRequest
26
+ import co.tryterra.sdk.bridge.generated.RecordIdList
27
+ import co.tryterra.sdk.bridge.generated.RecordIdRequest
28
+ import co.tryterra.sdk.bridge.generated.SleepEntryList
29
+ import co.tryterra.sdk.bridge.generated.SleepSegmentList
30
+ import co.tryterra.sdk.bridge.generated.SourceRequest
31
+ import co.tryterra.sdk.bridge.generated.StatusResult
32
+ import co.tryterra.sdk.bridge.generated.StatusesResult
33
+ import co.tryterra.sdk.bridge.generated.StepCountList
34
+ import co.tryterra.sdk.bridge.generated.StepEntryList
35
+ import co.tryterra.sdk.bridge.generated.SupportReport
36
+ import co.tryterra.sdk.bridge.generated.TerraBridgeOps
37
+ import co.tryterra.sdk.bridge.generated.TokenProviderRequest
38
+ import co.tryterra.sdk.bridge.generated.TokenRejectRequest
39
+ import co.tryterra.sdk.bridge.generated.TokenResolveRequest
40
+ import co.tryterra.sdk.bridge.generated.WorkoutEntryList
41
+ import co.tryterra.sdk.bridge.generated.WorkoutList
42
+ import co.tryterra.sdk.bridge.generated.AvailabilityResult
43
+ import co.tryterra.sdk.bridge.generated.BodyEntryList
44
+ import co.tryterra.sdk.bridge.generated.BodyReadingList
45
+ import co.tryterra.sdk.bridge.generated.DayTotalList
46
+ import co.tryterra.sdk.bridge.generated.HydrationList
47
+ import co.tryterra.sdk.bridge.generated.IntradaySampleList
48
+ import co.tryterra.sdk.bridge.generated.MenstrualEventList
49
+ import co.tryterra.sdk.bridge.generated.RestingHeartRateList
50
+ import co.tryterra.sdk.bridge.generated.WorkoutIntensityList
51
+ import co.tryterra.sdk.bridge.generated.WorkoutRouteList
52
+ import co.tryterra.sdk.bridge.generated.Source as WireSource
53
+ import kotlinx.coroutines.CancellationException
54
+
55
+ /**
56
+ * The hand-written half: about forty short functions that call the SDK.
57
+ *
58
+ * It cannot be incomplete. `TerraBridgeOps` is generated from the schema, so an op added there stops
59
+ * this file compiling until someone implements it - which is the property the old wrapper's
60
+ * hand-written string switch could not have, because nothing forced the switch to be exhaustive.
61
+ *
62
+ * Everything that is not a direct call lives in `Conversions`. What is left here is the shape of
63
+ * each call and the one place the SDK's Android surface differs from the wrapper's: the activity a
64
+ * sheet needs.
65
+ */
66
+ internal class TerraBridgeOpsImpl(
67
+ private val terra: Terra,
68
+ private val host: BridgeHost,
69
+ private val tokens: TokenBroker.Session,
70
+ ) : TerraBridgeOps {
71
+
72
+ // ---- client -----------------------------------------------------------------------------
73
+
74
+ override suspend fun clientAttach(request: Empty): BootstrapState = BootstrapState(
75
+ bootstrapped = true,
76
+ devId = host.devId,
77
+ platform = host.platform,
78
+ sdkVersion = host.sdkVersion,
79
+ backgroundRegistered = host.backgroundRegistered(),
80
+ )
81
+
82
+ override suspend fun clientConnect(request: ConnectRequest): Connection {
83
+ val source = sourceOf(request.source)
84
+ val endUser = Conversions.endUser(request.endUser)
85
+ val stored = request.token?.let { terra.connect(source, endUser, it) } ?: terra.connect(source, endUser)
86
+ return Conversions.connection(stored)
87
+ }
88
+
89
+ override suspend fun clientConnections(request: Empty): ConnectionList =
90
+ ConnectionList(terra.connections().map(Conversions::connection))
91
+
92
+ override suspend fun clientAvailability(request: SourceRequest): AvailabilityResult =
93
+ AvailabilityResult(ReadConversions.availability(terra.availability(sourceOf(request.source))))
94
+
95
+ override suspend fun clientDisconnect(request: SourceRequest): Empty {
96
+ terra.disconnect(sourceOf(request.source))
97
+ return Empty()
98
+ }
99
+
100
+ // ---- token ------------------------------------------------------------------------------
101
+
102
+ override suspend fun tokenSetProviderPresent(request: TokenProviderRequest): Empty {
103
+ tokens.setProviderPresent(request.present, request.timeoutMs)
104
+ return Empty()
105
+ }
106
+
107
+ override suspend fun tokenResolve(request: TokenResolveRequest): Empty {
108
+ tokens.resolve(request.requestId, request.token)
109
+ return Empty()
110
+ }
111
+
112
+ override suspend fun tokenReject(request: TokenRejectRequest): Empty {
113
+ tokens.reject(request.requestId, request.message)
114
+ return Empty()
115
+ }
116
+
117
+ // ---- permissions ------------------------------------------------------------------------
118
+
119
+ /**
120
+ * The one op that needs an activity, presented through the launcher that activity registered.
121
+ *
122
+ * A host with no registered launcher gets `bridge.no_activity`, and one whose launcher cannot
123
+ * present right now gets `bridge.permission_sheet_unavailable`, rather than a crash in the contract.
124
+ */
125
+ override suspend fun permissionsRequest(request: PermissionRequest): PermissionGrant {
126
+ val source = sourceOf(request.source)
127
+ val permissions = terra.on(source).permissions
128
+ val launcher = host.permissionLauncher(source) ?: throw BridgeError.NoActivity
129
+ val granted = try {
130
+ permissions.request(
131
+ read = request.read.map(Conversions::category).toSet(),
132
+ write = request.write.map(Conversions::category).toSet(),
133
+ workouts = request.workouts.map(Conversions::scope).toSet(),
134
+ launcher = launcher,
135
+ )
136
+ } catch (cancelled: CancellationException) {
137
+ // CancellationException is an IllegalStateException.
138
+ throw cancelled
139
+ } catch (refused: IllegalStateException) {
140
+ // The SDK's refusal of a launcher whose sheet is still up or that was unregistered,
141
+ // raised before anything is recorded.
142
+ throw BridgeError.PermissionSheetUnavailable
143
+ }
144
+ return PermissionGrant(granted = Conversions.access(granted))
145
+ }
146
+
147
+ override suspend fun permissionsStatus(request: CategoryRequest): StatusResult =
148
+ StatusResult(Conversions.permissionStatus(permissionsOf(request.source).status(Conversions.category(request.category))))
149
+
150
+ override suspend fun permissionsWriteStatus(request: CategoryRequest): StatusResult =
151
+ StatusResult(
152
+ Conversions.permissionStatus(permissionsOf(request.source).writeStatus(Conversions.category(request.category))),
153
+ )
154
+
155
+ override suspend fun permissionsRequested(request: SourceRequest): HealthAccess =
156
+ Conversions.access(permissionsOf(request.source).requested())
157
+
158
+ override suspend fun permissionsStatuses(request: SourceRequest): StatusesResult {
159
+ val statuses = permissionsOf(request.source).statuses()
160
+ .map { (category, status) -> Conversions.category(category) to Conversions.permissionStatus(status) }
161
+ .toMap()
162
+ return StatusesResult(statuses)
163
+ }
164
+
165
+ override suspend fun permissionsBackgroundReadStatus(request: SourceRequest): StatusResult =
166
+ StatusResult(Conversions.permissionStatus(permissionsOf(request.source).backgroundReadStatus()))
167
+
168
+ override suspend fun permissionsHistoryReadStatus(request: SourceRequest): StatusResult =
169
+ StatusResult(Conversions.permissionStatus(permissionsOf(request.source).historyReadStatus()))
170
+
171
+ // ---- diagnostics ------------------------------------------------------------------------
172
+
173
+ override suspend fun diagnosticsState(request: SourceRequest): DiagnosticsState =
174
+ Conversions.diagnosticsState(diagnosticsOf(request.source).state())
175
+
176
+ override suspend fun diagnosticsLastDelivery(request: SourceRequest): LastDeliveryResult =
177
+ LastDeliveryResult(diagnosticsOf(request.source).lastDelivery()?.let(BridgeInstant::format))
178
+
179
+ override suspend fun diagnosticsReport(request: SourceRequest): SupportReport =
180
+ Conversions.report(diagnosticsOf(request.source).report())
181
+
182
+ override suspend fun diagnosticsSend(request: SourceRequest): Empty {
183
+ diagnosticsOf(request.source).send()
184
+ return Empty()
185
+ }
186
+
187
+ // ---- background -------------------------------------------------------------------------
188
+
189
+ override suspend fun backgroundIsRegistered(request: Empty): BoolResult =
190
+ BoolResult(host.backgroundRegistered())
191
+
192
+ override suspend fun backgroundCancel(request: Empty): BoolResult =
193
+ BoolResult(host.cancelBackgroundSync())
194
+
195
+ // ---- body -------------------------------------------------------------------------------
196
+
197
+ override suspend fun bodyHeartRate(request: RangeRequest): HeartRateList =
198
+ HeartRateList(terra.body.heartRate(Conversions.timeRange(request.range)).map(Conversions::heartRate))
199
+
200
+ override suspend fun bodyRestingHeartRate(request: RangeRequest): RestingHeartRateList =
201
+ RestingHeartRateList(terra.body.restingHeartRate(Conversions.timeRange(request.range)).map(ReadConversions::restingHeartRate))
202
+
203
+ override suspend fun bodyReadings(request: RangeRequest): BodyReadingList =
204
+ BodyReadingList(terra.body.readings(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::bodyReading))
205
+
206
+ override suspend fun bodyLogHeartRate(request: HeartRateEntryList): RecordIdList =
207
+ RecordIdList(terra.body.log(request.entries.map(Conversions::entry)).map { it.value })
208
+
209
+ override suspend fun bodyLog(request: BodyEntryList): RecordIdList =
210
+ RecordIdList(terra.body.log(request.entries.map(Conversions::entry)).map { it.value })
211
+
212
+ override suspend fun bodyDelete(request: RecordIdRequest): Empty {
213
+ terra.body.delete(RecordId(request.id))
214
+ return Empty()
215
+ }
216
+
217
+ // ---- daily ------------------------------------------------------------------------------
218
+
219
+ override suspend fun dailySteps(request: RangeRequest): StepCountList =
220
+ StepCountList(terra.daily.steps(Conversions.timeRange(request.range)).map(Conversions::steps))
221
+
222
+ override suspend fun dailyTotals(request: RangeRequest): DayTotalList =
223
+ DayTotalList(terra.daily.totals(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::dayTotal))
224
+
225
+ override suspend fun dailyIntraday(request: RangeRequest): IntradaySampleList =
226
+ IntradaySampleList(terra.daily.intraday(Conversions.timeRange(request.range)).map(ReadConversions::intraday))
227
+
228
+ override suspend fun dailyLog(request: StepEntryList): RecordIdList =
229
+ RecordIdList(terra.daily.log(request.entries.map(Conversions::entry)).map { it.value })
230
+
231
+ override suspend fun dailyDelete(request: RecordIdRequest): Empty {
232
+ terra.daily.delete(RecordId(request.id))
233
+ return Empty()
234
+ }
235
+
236
+ // ---- sleep ------------------------------------------------------------------------------
237
+
238
+ override suspend fun sleepSegments(request: RangeRequest): SleepSegmentList =
239
+ SleepSegmentList(terra.sleep.segments(Conversions.timeRange(request.range)).map(Conversions::segment))
240
+
241
+ override suspend fun sleepLog(request: SleepEntryList): RecordIdList =
242
+ RecordIdList(terra.sleep.log(request.entries.map(Conversions::entry)).map { it.value })
243
+
244
+ override suspend fun sleepDelete(request: RecordIdRequest): Empty {
245
+ terra.sleep.delete(RecordId(request.id))
246
+ return Empty()
247
+ }
248
+
249
+ // ---- activity ---------------------------------------------------------------------------
250
+
251
+ override suspend fun activityWorkouts(request: RangeRequest): WorkoutList =
252
+ WorkoutList(terra.activity.workouts(Conversions.timeRange(request.range)).map(Conversions::workout))
253
+
254
+ override suspend fun activityRoutes(request: RangeRequest): WorkoutRouteList =
255
+ WorkoutRouteList(terra.activity.routes(Conversions.timeRange(request.range)).map(ReadConversions::route))
256
+
257
+ override suspend fun activityIntensity(request: RangeRequest): WorkoutIntensityList =
258
+ WorkoutIntensityList(terra.activity.intensity(Conversions.timeRange(request.range)).map(ReadConversions::intensity))
259
+
260
+ override suspend fun activityLog(request: WorkoutEntryList): RecordIdList =
261
+ RecordIdList(terra.activity.log(request.entries.map(Conversions::entry)).map { it.value })
262
+
263
+ override suspend fun activityDelete(request: RecordIdRequest): Empty {
264
+ terra.activity.delete(RecordId(request.id))
265
+ return Empty()
266
+ }
267
+
268
+ // ---- nutrition --------------------------------------------------------------------------
269
+
270
+ override suspend fun nutritionQuantities(request: RangeRequest): DietaryQuantityList =
271
+ DietaryQuantityList(terra.nutrition.quantities(Conversions.timeRange(request.range)).map(Conversions::quantity))
272
+
273
+ override suspend fun nutritionLog(request: NutritionEntryList): RecordIdList =
274
+ RecordIdList(terra.nutrition.log(request.entries.map(Conversions::entry)).map { it.value })
275
+
276
+ override suspend fun nutritionDelete(request: RecordIdRequest): Empty {
277
+ terra.nutrition.delete(RecordId(request.id))
278
+ return Empty()
279
+ }
280
+
281
+ override suspend fun nutritionHydration(request: RangeRequest): HydrationList =
282
+ HydrationList(terra.nutrition.hydration(Conversions.timeRange(request.range)).map(ReadConversions::hydration))
283
+
284
+ // ---- menstruation -----------------------------------------------------------------------
285
+
286
+ override suspend fun menstruationFlow(request: RangeRequest): MenstruationFlowDayList =
287
+ MenstruationFlowDayList(terra.menstruation.flow(Conversions.timeRange(request.range)).map(Conversions::flowDay))
288
+
289
+ override suspend fun menstruationEvents(request: RangeRequest): MenstrualEventList =
290
+ MenstrualEventList(terra.menstruation.events(Conversions.timeRange(request.range)).mapNotNull(ReadConversions::menstrualEvent))
291
+
292
+ /** Absent means the default source, which is what every other entry point on the SDK assumes. */
293
+ private fun sourceOf(wire: WireSource?): Source = wire?.let(Conversions::source) ?: Source.HEALTH
294
+
295
+ private fun permissionsOf(wire: WireSource?) = terra.on(sourceOf(wire)).permissions
296
+
297
+ /** [wire]'s own diagnostics, which read that store's permission record rather than Health Connect's. */
298
+ private fun diagnosticsOf(wire: WireSource?) = terra.on(sourceOf(wire)).diagnostics
299
+
300
+ }
@@ -0,0 +1,50 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.DataCategory
4
+ import co.tryterra.sdk.Terra
5
+ import co.tryterra.sdk.bridge.generated.Empty
6
+ import co.tryterra.sdk.bridge.generated.HealthChange
7
+ import co.tryterra.sdk.bridge.generated.TerraBridgeStreams
8
+ import co.tryterra.sdk.bridge.generated.TerraEvent
9
+ import co.tryterra.sdk.bridge.generated.TokenRequest
10
+ import java.time.ZoneId
11
+ import kotlinx.coroutines.flow.Flow
12
+ import kotlinx.coroutines.flow.map
13
+
14
+ /**
15
+ * The hand-written half of the streams: which SDK flow answers each stream, as typed values.
16
+ *
17
+ * Routing, validation and the `StreamMessage` framing are generated in `BridgeStreamDispatch`, so a
18
+ * stream added to the schema stops this file compiling until it is published here.
19
+ */
20
+ internal class TerraBridgeStreamsImpl(
21
+ private val terra: Terra,
22
+ private val tokens: TokenBroker.Session,
23
+ private val deviceZone: ZoneId,
24
+ ) : TerraBridgeStreams {
25
+
26
+ override fun diagnosticsEvents(params: Empty): Flow<TerraEvent> =
27
+ terra.diagnostics.events().map(Conversions::event)
28
+
29
+ override fun bodyChanges(params: Empty): Flow<HealthChange> = changes(DataCategory.BODY)
30
+
31
+ override fun dailyChanges(params: Empty): Flow<HealthChange> = changes(DataCategory.DAILY)
32
+
33
+ override fun sleepChanges(params: Empty): Flow<HealthChange> = changes(DataCategory.SLEEP)
34
+
35
+ override fun activityChanges(params: Empty): Flow<HealthChange> = changes(DataCategory.ACTIVITY)
36
+
37
+ override fun bridgeTokenRequest(params: Empty): Flow<TokenRequest> = tokens.requests
38
+
39
+ private fun changes(category: DataCategory): Flow<HealthChange> {
40
+ val source = when (category) {
41
+ DataCategory.BODY -> terra.body.changes()
42
+ DataCategory.DAILY -> terra.daily.changes()
43
+ DataCategory.SLEEP -> terra.sleep.changes()
44
+ DataCategory.ACTIVITY -> terra.activity.changes()
45
+ DataCategory.NUTRITION, DataCategory.MENSTRUATION ->
46
+ error("no change stream for $category")
47
+ }
48
+ return source.map { change -> Conversions.change(change, deviceZone) }
49
+ }
50
+ }
@@ -0,0 +1,198 @@
1
+ package co.tryterra.sdk.bridge
2
+
3
+ import co.tryterra.sdk.TerraError
4
+ import co.tryterra.sdk.bridge.generated.BridgeError
5
+ import co.tryterra.sdk.bridge.generated.TokenRequest
6
+ import java.util.concurrent.ConcurrentHashMap
7
+ import java.util.concurrent.atomic.AtomicLong
8
+ import java.util.concurrent.atomic.AtomicReference
9
+ import kotlinx.coroutines.CompletableDeferred
10
+ import kotlinx.coroutines.TimeoutCancellationException
11
+ import kotlinx.coroutines.flow.Flow
12
+ import kotlinx.coroutines.flow.MutableSharedFlow
13
+ import kotlinx.coroutines.flow.MutableStateFlow
14
+ import kotlinx.coroutines.flow.merge
15
+ import kotlinx.coroutines.flow.emitAll
16
+ import kotlinx.coroutines.flow.filter
17
+ import kotlinx.coroutines.flow.filterNotNull
18
+ import kotlinx.coroutines.flow.flow
19
+ import kotlinx.coroutines.flow.map
20
+ import kotlinx.coroutines.flow.onSubscription
21
+ import kotlinx.coroutines.flow.takeWhile
22
+ import kotlinx.coroutines.withTimeout
23
+
24
+ /**
25
+ * The SDK's token provider, answered by a runtime that is not in the same process boundary.
26
+ *
27
+ * The native client is built before any JavaScript exists, because `BackgroundSync.register` needs
28
+ * a live `Terra` at process start. So the closure the SDK holds cannot be the host's - it is this
29
+ * broker's [provide], which asks whichever runtime is listening and waits. Bootstrap creates the
30
+ * broker first, builds the client with `tokenProvider = broker::provide`, and hands the same broker
31
+ * to every [TerraBridge] it attaches; a broker the client was not built with is one nobody asks.
32
+ *
33
+ * The whole reason this is safe is in the SDK: the provider is referenced on exactly one path,
34
+ * `Linker.connect`, and `connect` can only be reached from the host's own code. A background wake
35
+ * never calls it - uploads are signed by the device key, not by a token - so the broker is never
36
+ * asked a question while nobody is there to answer it.
37
+ */
38
+ public class TokenBroker {
39
+
40
+ private val current = AtomicReference<Session?>(null)
41
+ private val nextId = AtomicLong(0)
42
+
43
+ /**
44
+ * The closure handed to `Terra`.
45
+ *
46
+ * Throws [TerraError.TokenUnavailable] rather than a bridge code when nothing is listening: a
47
+ * host should not be able to tell itself apart from a native app built without a provider.
48
+ */
49
+ public suspend fun provide(): String {
50
+ val session = current.get() ?: throw TerraError.TokenUnavailable
51
+ return session.ask("tok-${nextId.incrementAndGet()}")
52
+ }
53
+
54
+ /** A runtime's own view of the broker. One per [TerraBridge], so one per runtime. */
55
+ internal fun session(): Session = Session()
56
+
57
+ /**
58
+ * Everything one runtime asked, answers and tears down.
59
+ *
60
+ * Per runtime rather than per broker, because a reload is a new runtime answering through the
61
+ * same broker: the old one's teardown has to fail its own asks and leave the new one's alone, and
62
+ * without an owner on each ask a teardown racing a replacement rejects a link the new runtime
63
+ * could have answered.
64
+ */
65
+ internal inner class Session {
66
+ private val pending = ConcurrentHashMap<String, CompletableDeferred<String>>()
67
+
68
+ /** Each new ask's id. */
69
+ private val announced = MutableSharedFlow<String>(extraBufferCapacity = ANNOUNCE_BUFFER)
70
+
71
+ /** A state rather than a message on [announced], so a full buffer cannot drop the ending. */
72
+ private val closing = MutableStateFlow(false)
73
+
74
+ @Volatile
75
+ private var open: Boolean = true
76
+
77
+ @Volatile
78
+ private var timeoutMs: Int = DEFAULT_TIMEOUT_MS
79
+
80
+ /**
81
+ * What the `bridge.tokenRequest` stream publishes for this runtime: every subscription sees
82
+ * every ask, and the first answer settles it.
83
+ *
84
+ * Broadcast rather than handed to one collector, because a runtime can hold two
85
+ * subscriptions - a reload of the screen that made the first, a second `createTerra` - and
86
+ * an ask taken by one that never answers would time the link out while the other could
87
+ * have answered. A subscription starting late is first handed the asks still waiting, so
88
+ * an ask made between setting the provider and subscribing is not lost; the replay and the
89
+ * live feed can overlap, so each subscription skips an id it has already seen. Another
90
+ * runtime's asks never reach it: they live in that runtime's own session.
91
+ */
92
+ val requests: Flow<TokenRequest> = flow {
93
+ val seen = HashSet<String>()
94
+ val asks = announced.onSubscription { for (id in pending.keys.sorted()) emit(id) }
95
+ emitAll(
96
+ merge(asks, closing.filter { it }.map { null })
97
+ .takeWhile { it != null }
98
+ .filterNotNull()
99
+ .filter { id -> seen.add(id) && pending.containsKey(id) }
100
+ .map { id -> TokenRequest(requestId = id) },
101
+ )
102
+ }
103
+
104
+ /**
105
+ * Whether this runtime answers, and how long it may take.
106
+ *
107
+ * Without this the broker cannot tell "no host is listening" from "the host is slow", and
108
+ * would spend the whole timeout on a link that could have failed immediately with the same
109
+ * code a native app with no provider gets.
110
+ */
111
+ fun setProviderPresent(present: Boolean, timeoutMs: Int) {
112
+ this.timeoutMs = timeoutMs
113
+ if (present) {
114
+ if (open) current.set(this)
115
+ } else {
116
+ current.compareAndSet(this, null)
117
+ }
118
+ }
119
+
120
+ suspend fun ask(id: String): String {
121
+ val answer = CompletableDeferred<String>()
122
+ pending[id] = answer
123
+ // Re-read after the insert. `close` between the caller's check and the insert would
124
+ // otherwise leave this ask in the map with nobody left to answer it, and the link would
125
+ // wait out its whole timeout instead of failing at once.
126
+ if (!open) {
127
+ pending.remove(id)
128
+ throw TerraError.TokenUnavailable
129
+ }
130
+ val limit = timeoutMs
131
+ return try {
132
+ // Inside the timeout: a subscription that stopped collecting can fill the feed, and
133
+ // an ask waiting for room is still an ask the host has not answered.
134
+ withTimeout(limit.toLong()) {
135
+ announced.emit(id)
136
+ answer.await()
137
+ }
138
+ } catch (timedOut: TimeoutCancellationException) {
139
+ throw TerraError.TokenProviderFailed(BridgeError.TokenTimeout(limit))
140
+ } finally {
141
+ pending.remove(id)
142
+ }
143
+ }
144
+
145
+ /**
146
+ * An unknown or already-settled id is ignored rather than refused.
147
+ *
148
+ * A host that reloads answers a request the previous runtime made, and failing that answer
149
+ * would turn a harmless race into a failed link.
150
+ */
151
+ fun resolve(requestId: String, token: String) {
152
+ pending[requestId]?.complete(token)
153
+ }
154
+
155
+ fun reject(requestId: String, message: String) {
156
+ pending[requestId]?.completeExceptionally(TerraError.TokenProviderFailed(ProviderRefused(message)))
157
+ }
158
+
159
+ /**
160
+ * Fails this runtime's outstanding asks, for a runtime that is going away.
161
+ *
162
+ * Otherwise a pending ask holds the link open for its whole timeout and the host sees a
163
+ * connect that hangs rather than one that failed. Marked closed before draining, so an ask
164
+ * inserted after the drain finds `open` false on its re-read and removes itself.
165
+ */
166
+ fun close() {
167
+ open = false
168
+ current.compareAndSet(this, null)
169
+ closing.value = true
170
+ while (pending.isNotEmpty()) {
171
+ for (id in pending.keys.toList()) {
172
+ pending.remove(id)?.completeExceptionally(
173
+ TerraError.TokenProviderFailed(ProviderRefused("the host runtime went away")),
174
+ )
175
+ }
176
+ }
177
+ }
178
+
179
+ /** Visible for tests: how many asks are waiting for an answer. */
180
+ fun pendingCount(): Int = pending.size
181
+ }
182
+
183
+ /** The provider's own failure, carried so that its message reaches `details.message`. */
184
+ public class ProviderRefused(message: String) : Exception(message)
185
+
186
+ public companion object {
187
+ /**
188
+ * Thirty seconds, matching the documented default.
189
+ *
190
+ * Long enough for a host to call its own backend on a poor connection, short enough that a
191
+ * host which never answers fails the link rather than appearing to hang.
192
+ */
193
+ public const val DEFAULT_TIMEOUT_MS: Int = 30_000
194
+
195
+ /** Asks a slow subscription can fall behind by; a link makes at most two. */
196
+ private const val ANNOUNCE_BUFFER = 64
197
+ }
198
+ }