@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.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/TerraReactNative.podspec +48 -0
- package/android/build.gradle +87 -0
- package/android/src/bridgeCore/kotlin/README.md +1 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +557 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +300 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +50 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +582 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +1569 -0
- package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
- package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
- package/docs/availability.md +396 -0
- package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6594 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6595 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6513 -0
- package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
- package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
- package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
- package/ios/TerraBridgeCore/Conversions.swift +594 -0
- package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
- package/ios/TerraBridgeCore/Generated/BridgeOps.swift +967 -0
- package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +2050 -0
- package/ios/TerraBridgeCore/Locked.swift +15 -0
- package/ios/TerraBridgeCore/README.md +1 -0
- package/ios/TerraBridgeCore/Rejections.swift +130 -0
- package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
- package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
- package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
- package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +318 -0
- package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
- package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
- package/ios/TerraBridgeModule.mm +79 -0
- package/ios/TerraBridgeModuleImpl.swift +49 -0
- package/ios/TerraReactNative.swift +19 -0
- package/lib/EndUser.d.ts +12 -0
- package/lib/EndUser.js +13 -0
- package/lib/NativeTerraBridge.d.ts +23 -0
- package/lib/NativeTerraBridge.js +13 -0
- package/lib/Terra.d.ts +223 -0
- package/lib/Terra.js +336 -0
- package/lib/TerraError.d.ts +42 -0
- package/lib/TerraError.js +95 -0
- package/lib/createTerra.d.ts +8 -0
- package/lib/createTerra.js +19 -0
- package/lib/generated/client.d.ts +128 -0
- package/lib/generated/client.js +256 -0
- package/lib/generated/codecs.d.ts +219 -0
- package/lib/generated/codecs.js +2066 -0
- package/lib/generated/errors.d.ts +13 -0
- package/lib/generated/errors.js +68 -0
- package/lib/generated/ops.d.ts +13 -0
- package/lib/generated/ops.js +161 -0
- package/lib/generated/streams.d.ts +23 -0
- package/lib/generated/streams.js +58 -0
- package/lib/generated/types.d.ts +794 -0
- package/lib/generated/types.js +224 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +55 -0
- package/package.json +71 -4
- package/src/EndUser.ts +15 -0
- package/src/NativeTerraBridge.ts +27 -0
- package/src/Terra.ts +592 -0
- package/src/TerraError.ts +116 -0
- package/src/createTerra.ts +18 -0
- package/src/generated/client.ts +315 -0
- package/src/generated/codecs.ts +1989 -0
- package/src/generated/errors.ts +113 -0
- package/src/generated/ops.ts +233 -0
- package/src/generated/streams.ts +74 -0
- package/src/generated/types.ts +1222 -0
- 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
|
+
}
|