@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,582 @@
1
+ // Generated from surface/terra-surface.json by surface/gen. Do not edit by hand.
2
+ // Regenerate with: node surface/gen/src/cli.ts generate
3
+
4
+ package co.tryterra.sdk.bridge.generated
5
+
6
+ import kotlinx.coroutines.flow.Flow
7
+ import kotlinx.coroutines.flow.catch
8
+ import kotlinx.coroutines.flow.emitAll
9
+ import kotlinx.coroutines.flow.flow
10
+ import kotlinx.coroutines.flow.map
11
+ import kotlinx.coroutines.flow.onCompletion
12
+ import kotlinx.serialization.KSerializer
13
+ import kotlinx.serialization.json.Json
14
+ import kotlinx.serialization.json.JsonObject
15
+ import kotlinx.serialization.json.jsonObject
16
+ import kotlinx.serialization.serializer
17
+
18
+ /**
19
+ * Every op the schema declares, including the ones this platform does not implement.
20
+ *
21
+ * The unsupported ones are entries here rather than absent so that dispatch can answer
22
+ * bridge.unsupported_on_platform deliberately, instead of failing to parse the op.
23
+ */
24
+ public enum class BridgeOp(public val wire: String, public val isSupported: Boolean) {
25
+ CLIENT_ATTACH("client.attach", true),
26
+ CLIENT_CONNECT("client.connect", true),
27
+ CLIENT_CONNECTIONS("client.connections", true),
28
+ CLIENT_AVAILABILITY("client.availability", true),
29
+ CLIENT_DISCONNECT("client.disconnect", true),
30
+ TOKEN_SET_PROVIDER_PRESENT("token.setProviderPresent", true),
31
+ TOKEN_RESOLVE("token.resolve", true),
32
+ TOKEN_REJECT("token.reject", true),
33
+ PERMISSIONS_REQUEST("permissions.request", true),
34
+ PERMISSIONS_STATUS("permissions.status", true),
35
+ PERMISSIONS_WRITE_STATUS("permissions.writeStatus", true),
36
+ PERMISSIONS_REQUESTED("permissions.requested", true),
37
+ PERMISSIONS_STATUSES("permissions.statuses", true),
38
+ PERMISSIONS_BACKGROUND_READ_STATUS("permissions.backgroundReadStatus", true),
39
+ PERMISSIONS_HISTORY_READ_STATUS("permissions.historyReadStatus", true),
40
+ DIAGNOSTICS_STATE("diagnostics.state", true),
41
+ DIAGNOSTICS_LAST_DELIVERY("diagnostics.lastDelivery", true),
42
+ DIAGNOSTICS_REPORT("diagnostics.report", true),
43
+ DIAGNOSTICS_SEND("diagnostics.send", true),
44
+ BACKGROUND_IS_REGISTERED("background.isRegistered", true),
45
+ BACKGROUND_CANCEL("background.cancel", true),
46
+ BODY_HEART_RATE("body.heartRate", true),
47
+ BODY_RESTING_HEART_RATE("body.restingHeartRate", true),
48
+ BODY_READINGS("body.readings", true),
49
+ BODY_LOG_HEART_RATE("body.logHeartRate", true),
50
+ BODY_LOG("body.log", true),
51
+ BODY_DELETE("body.delete", true),
52
+ DAILY_STEPS("daily.steps", true),
53
+ DAILY_TOTALS("daily.totals", true),
54
+ DAILY_INTRADAY("daily.intraday", true),
55
+ DAILY_LOG("daily.log", true),
56
+ DAILY_DELETE("daily.delete", true),
57
+ SLEEP_SEGMENTS("sleep.segments", true),
58
+ SLEEP_LOG("sleep.log", true),
59
+ SLEEP_DELETE("sleep.delete", true),
60
+ ACTIVITY_WORKOUTS("activity.workouts", true),
61
+ ACTIVITY_ROUTES("activity.routes", true),
62
+ ACTIVITY_INTENSITY("activity.intensity", true),
63
+ ACTIVITY_LOG("activity.log", true),
64
+ ACTIVITY_DELETE("activity.delete", true),
65
+ NUTRITION_QUANTITIES("nutrition.quantities", true),
66
+ NUTRITION_LOG("nutrition.log", true),
67
+ NUTRITION_DELETE("nutrition.delete", true),
68
+ NUTRITION_HYDRATION("nutrition.hydration", true),
69
+ MENSTRUATION_FLOW("menstruation.flow", true),
70
+ MENSTRUATION_EVENTS("menstruation.events", true),
71
+ ATHLETE_PROFILE("athlete.profile", false),
72
+ PLANNED_WORKOUTS_PLANNED("plannedWorkouts.planned", false),
73
+ PLANNED_WORKOUTS_MARK_COMPLETE("plannedWorkouts.markComplete", false),
74
+ PLANNED_WORKOUTS_DELETE("plannedWorkouts.delete", false),
75
+ PLANNED_WORKOUTS_SCHEDULING_STATUS("plannedWorkouts.schedulingStatus", false),
76
+ PLANNED_WORKOUTS_REQUEST_SCHEDULING("plannedWorkouts.requestScheduling", false),
77
+ ;
78
+
79
+ public companion object {
80
+ private val byWire: Map<String, BridgeOp> = entries.associateBy { it.wire }
81
+
82
+ public fun of(wire: String): BridgeOp? = byWire[wire]
83
+ }
84
+ }
85
+
86
+ /**
87
+ * What P2 implements. One suspend function per op this platform supports.
88
+ *
89
+ * An op added to the schema stops this module compiling until the implementation grows a
90
+ * member for it, which is the whole reason the dispatch is generated from the same file.
91
+ */
92
+ public interface TerraBridgeOps {
93
+ /**
94
+ * Reports the native client the JS side is attaching to. Idempotent, and safe to call after a
95
+ * JS reload.
96
+ */
97
+ public suspend fun clientAttach(request: Empty): BootstrapState
98
+ public suspend fun clientConnect(request: ConnectRequest): Connection
99
+ public suspend fun clientConnections(request: Empty): ConnectionList
100
+ /** Whether the device can serve the source at all. Needs no connection and prompts for nothing. */
101
+ public suspend fun clientAvailability(request: SourceRequest): AvailabilityResult
102
+ public suspend fun clientDisconnect(request: SourceRequest): Empty
103
+ /**
104
+ * Tells the broker whether JS can answer at all. Without it a link asks a provider that is not
105
+ * there and waits out the timeout.
106
+ */
107
+ public suspend fun tokenSetProviderPresent(request: TokenProviderRequest): Empty
108
+ /**
109
+ * An unknown or already-settled requestId is ignored rather than rejected, so a reload cannot
110
+ * fail a link that has already moved on.
111
+ */
112
+ public suspend fun tokenResolve(request: TokenResolveRequest): Empty
113
+ public suspend fun tokenReject(request: TokenRejectRequest): Empty
114
+ public suspend fun permissionsRequest(request: PermissionRequest): PermissionGrant
115
+ public suspend fun permissionsStatus(request: CategoryRequest): StatusResult
116
+ public suspend fun permissionsWriteStatus(request: CategoryRequest): StatusResult
117
+ public suspend fun permissionsRequested(request: SourceRequest): HealthAccess
118
+ /** Available on android only. */
119
+ public suspend fun permissionsStatuses(request: SourceRequest): StatusesResult
120
+ /** Available on android only. */
121
+ public suspend fun permissionsBackgroundReadStatus(request: SourceRequest): StatusResult
122
+ /** Available on android only. */
123
+ public suspend fun permissionsHistoryReadStatus(request: SourceRequest): StatusResult
124
+ public suspend fun diagnosticsState(request: SourceRequest): DiagnosticsState
125
+ public suspend fun diagnosticsLastDelivery(request: SourceRequest): LastDeliveryResult
126
+ public suspend fun diagnosticsReport(request: SourceRequest): SupportReport
127
+ public suspend fun diagnosticsSend(request: SourceRequest): Empty
128
+ /**
129
+ * False means bootstrap did not run. The SDK reports the same thing independently as
130
+ * backgroundRefreshUnscheduled.
131
+ */
132
+ public suspend fun backgroundIsRegistered(request: Empty): BoolResult
133
+ /** Available on android only. */
134
+ public suspend fun backgroundCancel(request: Empty): BoolResult
135
+ public suspend fun bodyHeartRate(request: RangeRequest): HeartRateList
136
+ /**
137
+ * The store's own daily figures. Health Connect grants them under daily, so Android answers
138
+ * authorization_not_determined where only body was asked for.
139
+ */
140
+ public suspend fun bodyRestingHeartRate(request: RangeRequest): RestingHeartRateList
141
+ public suspend fun bodyReadings(request: RangeRequest): BodyReadingList
142
+ /** Its own op rather than an overload: Swift overloads log by element type and JS cannot. */
143
+ public suspend fun bodyLogHeartRate(request: HeartRateEntryList): RecordIdList
144
+ public suspend fun bodyLog(request: BodyEntryList): RecordIdList
145
+ public suspend fun bodyDelete(request: RecordIdRequest): Empty
146
+ public suspend fun dailySteps(request: RangeRequest): StepCountList
147
+ /**
148
+ * One total per metric per whole local day the range touches. A day with nothing recorded has
149
+ * no total, not a zero.
150
+ */
151
+ public suspend fun dailyTotals(request: RangeRequest): DayTotalList
152
+ /** The active energy, basal energy, distance and floors records the totals are made of. */
153
+ public suspend fun dailyIntraday(request: RangeRequest): IntradaySampleList
154
+ public suspend fun dailyLog(request: StepEntryList): RecordIdList
155
+ public suspend fun dailyDelete(request: RecordIdRequest): Empty
156
+ public suspend fun sleepSegments(request: RangeRequest): SleepSegmentList
157
+ public suspend fun sleepLog(request: SleepEntryList): RecordIdList
158
+ public suspend fun sleepDelete(request: RecordIdRequest): Empty
159
+ public suspend fun activityWorkouts(request: RangeRequest): WorkoutList
160
+ /** The GPS tracks of the workouts in the range, each naming its workout. */
161
+ public suspend fun activityRoutes(request: RangeRequest): WorkoutRouteList
162
+ /** Speed, power and cycling cadence readings in the range. */
163
+ public suspend fun activityIntensity(request: RangeRequest): WorkoutIntensityList
164
+ public suspend fun activityLog(request: WorkoutEntryList): RecordIdList
165
+ public suspend fun activityDelete(request: RecordIdRequest): Empty
166
+ /** One quantity per nutrient a meal recorded, not grouped into meals. Water is not among them. */
167
+ public suspend fun nutritionQuantities(request: RangeRequest): DietaryQuantityList
168
+ public suspend fun nutritionLog(request: NutritionEntryList): RecordIdList
169
+ public suspend fun nutritionDelete(request: RecordIdRequest): Empty
170
+ /** Water drunk in the range, one sample per drink. */
171
+ public suspend fun nutritionHydration(request: RangeRequest): HydrationList
172
+ /**
173
+ * Logged flow days overlapping the range, one per day. A day logged with no flow is noFlow,
174
+ * not left out.
175
+ */
176
+ public suspend fun menstruationFlow(request: RangeRequest): MenstruationFlowDayList
177
+ /** Ovulation tests, cervical mucus and intermenstrual bleeding logged in the range. */
178
+ public suspend fun menstruationEvents(request: RangeRequest): MenstrualEventList
179
+ }
180
+
181
+ /**
182
+ * Decodes one side of an exchange and re-encodes it, for the bridge-vector runner.
183
+ *
184
+ * The runner cannot name the request and response types itself without keeping a second copy
185
+ * of the op table, and a second copy of the op table is the thing this schema exists to
186
+ * remove. A round trip is also exactly what a vector asserts: the bytes in are the bytes out,
187
+ * which pins the DTOs and their serializer against the same file the other two runners read.
188
+ */
189
+ public object BridgeCodecs {
190
+ public fun roundTripRequest(op: BridgeOp, payload: String, json: Json = BridgeDispatch.defaultJson): String =
191
+ when (op) {
192
+ BridgeOp.CLIENT_ATTACH -> json.encodeToString(json.decodeFromString<Empty>(payload))
193
+ BridgeOp.CLIENT_CONNECT -> json.encodeToString(json.decodeFromString<ConnectRequest>(payload))
194
+ BridgeOp.CLIENT_CONNECTIONS -> json.encodeToString(json.decodeFromString<Empty>(payload))
195
+ BridgeOp.CLIENT_AVAILABILITY -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
196
+ BridgeOp.CLIENT_DISCONNECT -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
197
+ BridgeOp.TOKEN_SET_PROVIDER_PRESENT -> json.encodeToString(json.decodeFromString<TokenProviderRequest>(payload))
198
+ BridgeOp.TOKEN_RESOLVE -> json.encodeToString(json.decodeFromString<TokenResolveRequest>(payload))
199
+ BridgeOp.TOKEN_REJECT -> json.encodeToString(json.decodeFromString<TokenRejectRequest>(payload))
200
+ BridgeOp.PERMISSIONS_REQUEST -> json.encodeToString(json.decodeFromString<PermissionRequest>(payload))
201
+ BridgeOp.PERMISSIONS_STATUS -> json.encodeToString(json.decodeFromString<CategoryRequest>(payload))
202
+ BridgeOp.PERMISSIONS_WRITE_STATUS -> json.encodeToString(json.decodeFromString<CategoryRequest>(payload))
203
+ BridgeOp.PERMISSIONS_REQUESTED -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
204
+ BridgeOp.PERMISSIONS_STATUSES -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
205
+ BridgeOp.PERMISSIONS_BACKGROUND_READ_STATUS -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
206
+ BridgeOp.PERMISSIONS_HISTORY_READ_STATUS -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
207
+ BridgeOp.DIAGNOSTICS_STATE -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
208
+ BridgeOp.DIAGNOSTICS_LAST_DELIVERY -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
209
+ BridgeOp.DIAGNOSTICS_REPORT -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
210
+ BridgeOp.DIAGNOSTICS_SEND -> json.encodeToString(json.decodeFromString<SourceRequest>(payload))
211
+ BridgeOp.BACKGROUND_IS_REGISTERED -> json.encodeToString(json.decodeFromString<Empty>(payload))
212
+ BridgeOp.BACKGROUND_CANCEL -> json.encodeToString(json.decodeFromString<Empty>(payload))
213
+ BridgeOp.BODY_HEART_RATE -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
214
+ BridgeOp.BODY_RESTING_HEART_RATE -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
215
+ BridgeOp.BODY_READINGS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
216
+ BridgeOp.BODY_LOG_HEART_RATE -> json.encodeToString(json.decodeFromString<HeartRateEntryList>(payload))
217
+ BridgeOp.BODY_LOG -> json.encodeToString(json.decodeFromString<BodyEntryList>(payload))
218
+ BridgeOp.BODY_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
219
+ BridgeOp.DAILY_STEPS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
220
+ BridgeOp.DAILY_TOTALS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
221
+ BridgeOp.DAILY_INTRADAY -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
222
+ BridgeOp.DAILY_LOG -> json.encodeToString(json.decodeFromString<StepEntryList>(payload))
223
+ BridgeOp.DAILY_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
224
+ BridgeOp.SLEEP_SEGMENTS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
225
+ BridgeOp.SLEEP_LOG -> json.encodeToString(json.decodeFromString<SleepEntryList>(payload))
226
+ BridgeOp.SLEEP_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
227
+ BridgeOp.ACTIVITY_WORKOUTS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
228
+ BridgeOp.ACTIVITY_ROUTES -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
229
+ BridgeOp.ACTIVITY_INTENSITY -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
230
+ BridgeOp.ACTIVITY_LOG -> json.encodeToString(json.decodeFromString<WorkoutEntryList>(payload))
231
+ BridgeOp.ACTIVITY_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
232
+ BridgeOp.NUTRITION_QUANTITIES -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
233
+ BridgeOp.NUTRITION_LOG -> json.encodeToString(json.decodeFromString<NutritionEntryList>(payload))
234
+ BridgeOp.NUTRITION_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
235
+ BridgeOp.NUTRITION_HYDRATION -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
236
+ BridgeOp.MENSTRUATION_FLOW -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
237
+ BridgeOp.MENSTRUATION_EVENTS -> json.encodeToString(json.decodeFromString<RangeRequest>(payload))
238
+ BridgeOp.ATHLETE_PROFILE -> json.encodeToString(json.decodeFromString<Empty>(payload))
239
+ BridgeOp.PLANNED_WORKOUTS_PLANNED -> json.encodeToString(json.decodeFromString<Empty>(payload))
240
+ BridgeOp.PLANNED_WORKOUTS_MARK_COMPLETE -> json.encodeToString(json.decodeFromString<MarkCompleteRequest>(payload))
241
+ BridgeOp.PLANNED_WORKOUTS_DELETE -> json.encodeToString(json.decodeFromString<RecordIdRequest>(payload))
242
+ BridgeOp.PLANNED_WORKOUTS_SCHEDULING_STATUS -> json.encodeToString(json.decodeFromString<Empty>(payload))
243
+ BridgeOp.PLANNED_WORKOUTS_REQUEST_SCHEDULING -> json.encodeToString(json.decodeFromString<Empty>(payload))
244
+ }
245
+
246
+ public fun roundTripResponse(op: BridgeOp, payload: String, json: Json = BridgeDispatch.defaultJson): String =
247
+ when (op) {
248
+ BridgeOp.CLIENT_ATTACH -> json.encodeToString(json.decodeFromString<BootstrapState>(payload))
249
+ BridgeOp.CLIENT_CONNECT -> json.encodeToString(json.decodeFromString<Connection>(payload))
250
+ BridgeOp.CLIENT_CONNECTIONS -> json.encodeToString(json.decodeFromString<ConnectionList>(payload))
251
+ BridgeOp.CLIENT_AVAILABILITY -> json.encodeToString(json.decodeFromString<AvailabilityResult>(payload))
252
+ BridgeOp.CLIENT_DISCONNECT -> json.encodeToString(json.decodeFromString<Empty>(payload))
253
+ BridgeOp.TOKEN_SET_PROVIDER_PRESENT -> json.encodeToString(json.decodeFromString<Empty>(payload))
254
+ BridgeOp.TOKEN_RESOLVE -> json.encodeToString(json.decodeFromString<Empty>(payload))
255
+ BridgeOp.TOKEN_REJECT -> json.encodeToString(json.decodeFromString<Empty>(payload))
256
+ BridgeOp.PERMISSIONS_REQUEST -> json.encodeToString(json.decodeFromString<PermissionGrant>(payload))
257
+ BridgeOp.PERMISSIONS_STATUS -> json.encodeToString(json.decodeFromString<StatusResult>(payload))
258
+ BridgeOp.PERMISSIONS_WRITE_STATUS -> json.encodeToString(json.decodeFromString<StatusResult>(payload))
259
+ BridgeOp.PERMISSIONS_REQUESTED -> json.encodeToString(json.decodeFromString<HealthAccess>(payload))
260
+ BridgeOp.PERMISSIONS_STATUSES -> json.encodeToString(json.decodeFromString<StatusesResult>(payload))
261
+ BridgeOp.PERMISSIONS_BACKGROUND_READ_STATUS -> json.encodeToString(json.decodeFromString<StatusResult>(payload))
262
+ BridgeOp.PERMISSIONS_HISTORY_READ_STATUS -> json.encodeToString(json.decodeFromString<StatusResult>(payload))
263
+ BridgeOp.DIAGNOSTICS_STATE -> json.encodeToString(json.decodeFromString<DiagnosticsState>(payload))
264
+ BridgeOp.DIAGNOSTICS_LAST_DELIVERY -> json.encodeToString(json.decodeFromString<LastDeliveryResult>(payload))
265
+ BridgeOp.DIAGNOSTICS_REPORT -> {
266
+ val value = json.decodeFromString<SupportReport>(payload)
267
+ SupportReportEnvelope.validate(value)
268
+ json.encodeToString(value)
269
+ }
270
+ BridgeOp.DIAGNOSTICS_SEND -> json.encodeToString(json.decodeFromString<Empty>(payload))
271
+ BridgeOp.BACKGROUND_IS_REGISTERED -> json.encodeToString(json.decodeFromString<BoolResult>(payload))
272
+ BridgeOp.BACKGROUND_CANCEL -> json.encodeToString(json.decodeFromString<BoolResult>(payload))
273
+ BridgeOp.BODY_HEART_RATE -> json.encodeToString(json.decodeFromString<HeartRateList>(payload))
274
+ BridgeOp.BODY_RESTING_HEART_RATE -> json.encodeToString(json.decodeFromString<RestingHeartRateList>(payload))
275
+ BridgeOp.BODY_READINGS -> json.encodeToString(json.decodeFromString<BodyReadingList>(payload))
276
+ BridgeOp.BODY_LOG_HEART_RATE -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
277
+ BridgeOp.BODY_LOG -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
278
+ BridgeOp.BODY_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
279
+ BridgeOp.DAILY_STEPS -> json.encodeToString(json.decodeFromString<StepCountList>(payload))
280
+ BridgeOp.DAILY_TOTALS -> json.encodeToString(json.decodeFromString<DayTotalList>(payload))
281
+ BridgeOp.DAILY_INTRADAY -> json.encodeToString(json.decodeFromString<IntradaySampleList>(payload))
282
+ BridgeOp.DAILY_LOG -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
283
+ BridgeOp.DAILY_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
284
+ BridgeOp.SLEEP_SEGMENTS -> json.encodeToString(json.decodeFromString<SleepSegmentList>(payload))
285
+ BridgeOp.SLEEP_LOG -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
286
+ BridgeOp.SLEEP_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
287
+ BridgeOp.ACTIVITY_WORKOUTS -> json.encodeToString(json.decodeFromString<WorkoutList>(payload))
288
+ BridgeOp.ACTIVITY_ROUTES -> json.encodeToString(json.decodeFromString<WorkoutRouteList>(payload))
289
+ BridgeOp.ACTIVITY_INTENSITY -> json.encodeToString(json.decodeFromString<WorkoutIntensityList>(payload))
290
+ BridgeOp.ACTIVITY_LOG -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
291
+ BridgeOp.ACTIVITY_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
292
+ BridgeOp.NUTRITION_QUANTITIES -> json.encodeToString(json.decodeFromString<DietaryQuantityList>(payload))
293
+ BridgeOp.NUTRITION_LOG -> json.encodeToString(json.decodeFromString<RecordIdList>(payload))
294
+ BridgeOp.NUTRITION_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
295
+ BridgeOp.NUTRITION_HYDRATION -> json.encodeToString(json.decodeFromString<HydrationList>(payload))
296
+ BridgeOp.MENSTRUATION_FLOW -> json.encodeToString(json.decodeFromString<MenstruationFlowDayList>(payload))
297
+ BridgeOp.MENSTRUATION_EVENTS -> json.encodeToString(json.decodeFromString<MenstrualEventList>(payload))
298
+ BridgeOp.ATHLETE_PROFILE -> json.encodeToString(json.decodeFromString<AthleteProfileResult>(payload))
299
+ BridgeOp.PLANNED_WORKOUTS_PLANNED -> json.encodeToString(json.decodeFromString<PlannedWorkoutList>(payload))
300
+ BridgeOp.PLANNED_WORKOUTS_MARK_COMPLETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
301
+ BridgeOp.PLANNED_WORKOUTS_DELETE -> json.encodeToString(json.decodeFromString<Empty>(payload))
302
+ BridgeOp.PLANNED_WORKOUTS_SCHEDULING_STATUS -> json.encodeToString(json.decodeFromString<WorkoutScheduling>(payload))
303
+ BridgeOp.PLANNED_WORKOUTS_REQUEST_SCHEDULING -> json.encodeToString(json.decodeFromString<WorkoutScheduling>(payload))
304
+ }
305
+
306
+ /** One element of a stream, by that stream's declared type. */
307
+ public fun roundTripStreamElement(stream: String, payload: String, json: Json = BridgeDispatch.defaultJson): String =
308
+ when (stream) {
309
+ "diagnostics.events" -> {
310
+ val value = json.decodeFromString<TerraEvent>(payload)
311
+ TerraEventEnvelope.validate(value)
312
+ json.encodeToString(value)
313
+ }
314
+ "body.changes" -> json.encodeToString(json.decodeFromString<HealthChange>(payload))
315
+ "daily.changes" -> json.encodeToString(json.decodeFromString<HealthChange>(payload))
316
+ "sleep.changes" -> json.encodeToString(json.decodeFromString<HealthChange>(payload))
317
+ "activity.changes" -> json.encodeToString(json.decodeFromString<HealthChange>(payload))
318
+ "bridge.tokenRequest" -> json.encodeToString(json.decodeFromString<TokenRequest>(payload))
319
+ else -> throw BridgeError.UnsupportedOnPlatform(stream)
320
+ }
321
+
322
+ /**
323
+ * One message as a subscription actually delivers it: the envelope, and the element inside.
324
+ *
325
+ * Both halves, because checking only the envelope would accept a `data` carrying anything at
326
+ * all, and checking only the element would accept a stream that never says it failed.
327
+ */
328
+ public fun roundTripStreamMessage(stream: String, payload: String, json: Json = BridgeDispatch.defaultJson): String {
329
+ val message = json.decodeFromString<StreamMessage>(payload)
330
+ return when (message) {
331
+ is StreamMessage.Data ->
332
+ json.encodeToString<StreamMessage>(
333
+ StreamMessage.Data(
334
+ element = json.decodeFromString<JsonObject>(
335
+ roundTripStreamElement(stream, json.encodeToString(message.element), json),
336
+ ),
337
+ ),
338
+ )
339
+ else -> json.encodeToString<StreamMessage>(message)
340
+ }
341
+ }
342
+ }
343
+
344
+ /**
345
+ * kotlinx's own message carries the input, which may be a health sample, so only the path is
346
+ * lifted out of it.
347
+ */
348
+ internal inline fun <reified T> Json.decodeRequest(payload: String, validate: (T) -> Unit = {}): T =
349
+ try {
350
+ decodeFromString<T>(payload).also(validate)
351
+ } catch (malformed: IllegalArgumentException) {
352
+ throw BridgeError.InvalidPayload(BridgeDispatch.pathOf(malformed))
353
+ }
354
+
355
+ /**
356
+ * Turns an op string and a JSON payload into a typed call and a JSON answer.
357
+ *
358
+ * `when` as an expression over BridgeOp, with no else: the compiler, not a test, is what
359
+ * notices an op the schema added and this file has not handled.
360
+ */
361
+ public class BridgeDispatch(
362
+ private val ops: TerraBridgeOps,
363
+ private val json: Json = defaultJson,
364
+ ) {
365
+ public suspend fun dispatch(op: String, payload: String): String {
366
+ val known = BridgeOp.of(op) ?: throw BridgeError.UnsupportedOnPlatform(op)
367
+ return when (known) {
368
+ BridgeOp.CLIENT_ATTACH ->
369
+ json.encodeToString(ops.clientAttach(json.decodeRequest<Empty>(payload)))
370
+ BridgeOp.CLIENT_CONNECT ->
371
+ json.encodeToString(ops.clientConnect(json.decodeRequest<ConnectRequest>(payload)))
372
+ BridgeOp.CLIENT_CONNECTIONS ->
373
+ json.encodeToString(ops.clientConnections(json.decodeRequest<Empty>(payload)))
374
+ BridgeOp.CLIENT_AVAILABILITY ->
375
+ json.encodeToString(ops.clientAvailability(json.decodeRequest<SourceRequest>(payload)))
376
+ BridgeOp.CLIENT_DISCONNECT ->
377
+ json.encodeToString(ops.clientDisconnect(json.decodeRequest<SourceRequest>(payload)))
378
+ BridgeOp.TOKEN_SET_PROVIDER_PRESENT ->
379
+ json.encodeToString(ops.tokenSetProviderPresent(json.decodeRequest<TokenProviderRequest>(payload)))
380
+ BridgeOp.TOKEN_RESOLVE ->
381
+ json.encodeToString(ops.tokenResolve(json.decodeRequest<TokenResolveRequest>(payload)))
382
+ BridgeOp.TOKEN_REJECT ->
383
+ json.encodeToString(ops.tokenReject(json.decodeRequest<TokenRejectRequest>(payload)))
384
+ BridgeOp.PERMISSIONS_REQUEST ->
385
+ json.encodeToString(ops.permissionsRequest(json.decodeRequest<PermissionRequest>(payload)))
386
+ BridgeOp.PERMISSIONS_STATUS ->
387
+ json.encodeToString(ops.permissionsStatus(json.decodeRequest<CategoryRequest>(payload)))
388
+ BridgeOp.PERMISSIONS_WRITE_STATUS ->
389
+ json.encodeToString(ops.permissionsWriteStatus(json.decodeRequest<CategoryRequest>(payload)))
390
+ BridgeOp.PERMISSIONS_REQUESTED ->
391
+ json.encodeToString(ops.permissionsRequested(json.decodeRequest<SourceRequest>(payload)))
392
+ BridgeOp.PERMISSIONS_STATUSES ->
393
+ json.encodeToString(ops.permissionsStatuses(json.decodeRequest<SourceRequest>(payload)))
394
+ BridgeOp.PERMISSIONS_BACKGROUND_READ_STATUS ->
395
+ json.encodeToString(ops.permissionsBackgroundReadStatus(json.decodeRequest<SourceRequest>(payload)))
396
+ BridgeOp.PERMISSIONS_HISTORY_READ_STATUS ->
397
+ json.encodeToString(ops.permissionsHistoryReadStatus(json.decodeRequest<SourceRequest>(payload)))
398
+ BridgeOp.DIAGNOSTICS_STATE ->
399
+ json.encodeToString(ops.diagnosticsState(json.decodeRequest<SourceRequest>(payload)))
400
+ BridgeOp.DIAGNOSTICS_LAST_DELIVERY ->
401
+ json.encodeToString(ops.diagnosticsLastDelivery(json.decodeRequest<SourceRequest>(payload)))
402
+ BridgeOp.DIAGNOSTICS_REPORT -> {
403
+ val request = json.decodeRequest<SourceRequest>(payload)
404
+ val response = ops.diagnosticsReport(request)
405
+ SupportReportEnvelope.validate(response)
406
+ json.encodeToString(response)
407
+ }
408
+ BridgeOp.DIAGNOSTICS_SEND ->
409
+ json.encodeToString(ops.diagnosticsSend(json.decodeRequest<SourceRequest>(payload)))
410
+ BridgeOp.BACKGROUND_IS_REGISTERED ->
411
+ json.encodeToString(ops.backgroundIsRegistered(json.decodeRequest<Empty>(payload)))
412
+ BridgeOp.BACKGROUND_CANCEL ->
413
+ json.encodeToString(ops.backgroundCancel(json.decodeRequest<Empty>(payload)))
414
+ BridgeOp.BODY_HEART_RATE ->
415
+ json.encodeToString(ops.bodyHeartRate(json.decodeRequest<RangeRequest>(payload)))
416
+ BridgeOp.BODY_RESTING_HEART_RATE ->
417
+ json.encodeToString(ops.bodyRestingHeartRate(json.decodeRequest<RangeRequest>(payload)))
418
+ BridgeOp.BODY_READINGS ->
419
+ json.encodeToString(ops.bodyReadings(json.decodeRequest<RangeRequest>(payload)))
420
+ BridgeOp.BODY_LOG_HEART_RATE ->
421
+ json.encodeToString(ops.bodyLogHeartRate(json.decodeRequest<HeartRateEntryList>(payload)))
422
+ BridgeOp.BODY_LOG ->
423
+ json.encodeToString(ops.bodyLog(json.decodeRequest<BodyEntryList>(payload)))
424
+ BridgeOp.BODY_DELETE ->
425
+ json.encodeToString(ops.bodyDelete(json.decodeRequest<RecordIdRequest>(payload)))
426
+ BridgeOp.DAILY_STEPS ->
427
+ json.encodeToString(ops.dailySteps(json.decodeRequest<RangeRequest>(payload)))
428
+ BridgeOp.DAILY_TOTALS ->
429
+ json.encodeToString(ops.dailyTotals(json.decodeRequest<RangeRequest>(payload)))
430
+ BridgeOp.DAILY_INTRADAY ->
431
+ json.encodeToString(ops.dailyIntraday(json.decodeRequest<RangeRequest>(payload)))
432
+ BridgeOp.DAILY_LOG ->
433
+ json.encodeToString(ops.dailyLog(json.decodeRequest<StepEntryList>(payload)))
434
+ BridgeOp.DAILY_DELETE ->
435
+ json.encodeToString(ops.dailyDelete(json.decodeRequest<RecordIdRequest>(payload)))
436
+ BridgeOp.SLEEP_SEGMENTS ->
437
+ json.encodeToString(ops.sleepSegments(json.decodeRequest<RangeRequest>(payload)))
438
+ BridgeOp.SLEEP_LOG ->
439
+ json.encodeToString(ops.sleepLog(json.decodeRequest<SleepEntryList>(payload)))
440
+ BridgeOp.SLEEP_DELETE ->
441
+ json.encodeToString(ops.sleepDelete(json.decodeRequest<RecordIdRequest>(payload)))
442
+ BridgeOp.ACTIVITY_WORKOUTS ->
443
+ json.encodeToString(ops.activityWorkouts(json.decodeRequest<RangeRequest>(payload)))
444
+ BridgeOp.ACTIVITY_ROUTES ->
445
+ json.encodeToString(ops.activityRoutes(json.decodeRequest<RangeRequest>(payload)))
446
+ BridgeOp.ACTIVITY_INTENSITY ->
447
+ json.encodeToString(ops.activityIntensity(json.decodeRequest<RangeRequest>(payload)))
448
+ BridgeOp.ACTIVITY_LOG ->
449
+ json.encodeToString(ops.activityLog(json.decodeRequest<WorkoutEntryList>(payload)))
450
+ BridgeOp.ACTIVITY_DELETE ->
451
+ json.encodeToString(ops.activityDelete(json.decodeRequest<RecordIdRequest>(payload)))
452
+ BridgeOp.NUTRITION_QUANTITIES ->
453
+ json.encodeToString(ops.nutritionQuantities(json.decodeRequest<RangeRequest>(payload)))
454
+ BridgeOp.NUTRITION_LOG ->
455
+ json.encodeToString(ops.nutritionLog(json.decodeRequest<NutritionEntryList>(payload)))
456
+ BridgeOp.NUTRITION_DELETE ->
457
+ json.encodeToString(ops.nutritionDelete(json.decodeRequest<RecordIdRequest>(payload)))
458
+ BridgeOp.NUTRITION_HYDRATION ->
459
+ json.encodeToString(ops.nutritionHydration(json.decodeRequest<RangeRequest>(payload)))
460
+ BridgeOp.MENSTRUATION_FLOW ->
461
+ json.encodeToString(ops.menstruationFlow(json.decodeRequest<RangeRequest>(payload)))
462
+ BridgeOp.MENSTRUATION_EVENTS ->
463
+ json.encodeToString(ops.menstruationEvents(json.decodeRequest<RangeRequest>(payload)))
464
+ BridgeOp.ATHLETE_PROFILE -> throw BridgeError.UnsupportedOnPlatform(known.wire)
465
+ BridgeOp.PLANNED_WORKOUTS_PLANNED -> throw BridgeError.UnsupportedOnPlatform(known.wire)
466
+ BridgeOp.PLANNED_WORKOUTS_MARK_COMPLETE -> throw BridgeError.UnsupportedOnPlatform(known.wire)
467
+ BridgeOp.PLANNED_WORKOUTS_DELETE -> throw BridgeError.UnsupportedOnPlatform(known.wire)
468
+ BridgeOp.PLANNED_WORKOUTS_SCHEDULING_STATUS -> throw BridgeError.UnsupportedOnPlatform(known.wire)
469
+ BridgeOp.PLANNED_WORKOUTS_REQUEST_SCHEDULING -> throw BridgeError.UnsupportedOnPlatform(known.wire)
470
+ }
471
+ }
472
+
473
+ public companion object {
474
+ /**
475
+ * encodeDefaults is on because a DTO field left at its default still has to reach the
476
+ * other side: a response that omitted `userEntered: false` would decode as absent there.
477
+ */
478
+ private val PATH: Regex = Regex("""at path: (\$\S*)""")
479
+
480
+ /** `$.entries[2].start`, or `$` when the failure names no path. */
481
+ public fun pathOf(error: Throwable): String = error.message?.let { PATH.find(it)?.groupValues?.get(1) } ?: "$"
482
+
483
+ public val defaultJson: Json = Json {
484
+ encodeDefaults = true
485
+ explicitNulls = false
486
+ ignoreUnknownKeys = true
487
+ }
488
+ }
489
+ }
490
+
491
+ /** Every stream the schema declares, including the ones this platform does not publish. */
492
+ public enum class BridgeStream(public val wire: String, public val isSupported: Boolean) {
493
+ DIAGNOSTICS_EVENTS("diagnostics.events", true),
494
+ BODY_CHANGES("body.changes", true),
495
+ DAILY_CHANGES("daily.changes", true),
496
+ SLEEP_CHANGES("sleep.changes", true),
497
+ ACTIVITY_CHANGES("activity.changes", true),
498
+ BRIDGE_TOKEN_REQUEST("bridge.tokenRequest", true),
499
+ ;
500
+
501
+ public companion object {
502
+ private val byWire: Map<String, BridgeStream> = entries.associateBy { it.wire }
503
+
504
+ public fun of(wire: String): BridgeStream? = byWire[wire]
505
+ }
506
+ }
507
+
508
+ /**
509
+ * What P2 publishes. One function per stream this platform supports, each a fresh flow per
510
+ * call: a subscriber that goes away cancels its own collection and nobody else's.
511
+ */
512
+ public interface TerraBridgeStreams {
513
+ /**
514
+ * Every subscriber gets its own native stream, matching the SDK's fresh-stream-per-call
515
+ * contract.
516
+ */
517
+ public fun diagnosticsEvents(params: Empty): Flow<TerraEvent>
518
+ public fun bodyChanges(params: Empty): Flow<HealthChange>
519
+ public fun dailyChanges(params: Empty): Flow<HealthChange>
520
+ public fun sleepChanges(params: Empty): Flow<HealthChange>
521
+ public fun activityChanges(params: Empty): Flow<HealthChange>
522
+ /** Bridge-owned. The broker's ask, not an SDK stream. */
523
+ public fun bridgeTokenRequest(params: Empty): Flow<TokenRequest>
524
+ }
525
+
526
+ public class BridgeStreamDispatch(
527
+ private val streams: TerraBridgeStreams,
528
+ private val errorPayload: (Throwable) -> TerraErrorPayload,
529
+ private val json: Json = BridgeDispatch.defaultJson,
530
+ ) {
531
+ public fun open(stream: String, params: String): Flow<String> {
532
+ val known = BridgeStream.of(stream) ?: throw BridgeError.UnsupportedOnPlatform(stream)
533
+ val messages = when (known) {
534
+ BridgeStream.DIAGNOSTICS_EVENTS -> {
535
+ val request = json.decodeRequest<Empty>(params)
536
+ flow { emitAll(streams.diagnosticsEvents(request)) }.map { element ->
537
+ TerraEventEnvelope.validate(element)
538
+ frame(serializer<TerraEvent>(), element)
539
+ }
540
+ }
541
+ BridgeStream.BODY_CHANGES -> {
542
+ val request = json.decodeRequest<Empty>(params)
543
+ flow { emitAll(streams.bodyChanges(request)) }.map { element ->
544
+ frame(serializer<HealthChange>(), element)
545
+ }
546
+ }
547
+ BridgeStream.DAILY_CHANGES -> {
548
+ val request = json.decodeRequest<Empty>(params)
549
+ flow { emitAll(streams.dailyChanges(request)) }.map { element ->
550
+ frame(serializer<HealthChange>(), element)
551
+ }
552
+ }
553
+ BridgeStream.SLEEP_CHANGES -> {
554
+ val request = json.decodeRequest<Empty>(params)
555
+ flow { emitAll(streams.sleepChanges(request)) }.map { element ->
556
+ frame(serializer<HealthChange>(), element)
557
+ }
558
+ }
559
+ BridgeStream.ACTIVITY_CHANGES -> {
560
+ val request = json.decodeRequest<Empty>(params)
561
+ flow { emitAll(streams.activityChanges(request)) }.map { element ->
562
+ frame(serializer<HealthChange>(), element)
563
+ }
564
+ }
565
+ BridgeStream.BRIDGE_TOKEN_REQUEST -> {
566
+ val request = json.decodeRequest<Empty>(params)
567
+ flow { emitAll(streams.bridgeTokenRequest(request)) }.map { element ->
568
+ frame(serializer<TokenRequest>(), element)
569
+ }
570
+ }
571
+ }
572
+ return messages
573
+ .onCompletion { cause -> if (cause == null) emit(encode(StreamMessage.Complete)) }
574
+ .catch { failure -> emit(encode(StreamMessage.Error(error = errorPayload(failure)))) }
575
+ }
576
+
577
+ private fun <T> frame(serializer: KSerializer<T>, element: T): String =
578
+ encode(StreamMessage.Data(element = json.encodeToJsonElement(serializer, element).jsonObject))
579
+
580
+ private fun encode(message: StreamMessage): String = json.encodeToString(StreamMessage.serializer(), message)
581
+ }
582
+