@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,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
|
+
}
|