@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,967 @@
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
+ import Foundation
5
+
6
+ /// Every op the schema declares, including the ones this platform does not implement.
7
+ ///
8
+ /// The unsupported ones are cases here rather than absent so that `dispatch` can answer
9
+ /// `bridge.unsupported_on_platform` deliberately, instead of failing to parse the op and
10
+ /// answering something that reads like a bug in the caller.
11
+ public enum BridgeOp: String, CaseIterable, Sendable {
12
+ case clientAttach = "client.attach"
13
+ case clientConnect = "client.connect"
14
+ case clientConnections = "client.connections"
15
+ case clientAvailability = "client.availability"
16
+ case clientDisconnect = "client.disconnect"
17
+ case tokenSetProviderPresent = "token.setProviderPresent"
18
+ case tokenResolve = "token.resolve"
19
+ case tokenReject = "token.reject"
20
+ case permissionsRequest = "permissions.request"
21
+ case permissionsStatus = "permissions.status"
22
+ case permissionsWriteStatus = "permissions.writeStatus"
23
+ case permissionsRequested = "permissions.requested"
24
+ case permissionsStatuses = "permissions.statuses"
25
+ case permissionsBackgroundReadStatus = "permissions.backgroundReadStatus"
26
+ case permissionsHistoryReadStatus = "permissions.historyReadStatus"
27
+ case diagnosticsState = "diagnostics.state"
28
+ case diagnosticsLastDelivery = "diagnostics.lastDelivery"
29
+ case diagnosticsReport = "diagnostics.report"
30
+ case diagnosticsSend = "diagnostics.send"
31
+ case backgroundIsRegistered = "background.isRegistered"
32
+ case backgroundCancel = "background.cancel"
33
+ case bodyHeartRate = "body.heartRate"
34
+ case bodyRestingHeartRate = "body.restingHeartRate"
35
+ case bodyReadings = "body.readings"
36
+ case bodyLogHeartRate = "body.logHeartRate"
37
+ case bodyLog = "body.log"
38
+ case bodyDelete = "body.delete"
39
+ case dailySteps = "daily.steps"
40
+ case dailyTotals = "daily.totals"
41
+ case dailyIntraday = "daily.intraday"
42
+ case dailyLog = "daily.log"
43
+ case dailyDelete = "daily.delete"
44
+ case sleepSegments = "sleep.segments"
45
+ case sleepLog = "sleep.log"
46
+ case sleepDelete = "sleep.delete"
47
+ case activityWorkouts = "activity.workouts"
48
+ case activityRoutes = "activity.routes"
49
+ case activityIntensity = "activity.intensity"
50
+ case activityLog = "activity.log"
51
+ case activityDelete = "activity.delete"
52
+ case nutritionQuantities = "nutrition.quantities"
53
+ case nutritionLog = "nutrition.log"
54
+ case nutritionDelete = "nutrition.delete"
55
+ case nutritionHydration = "nutrition.hydration"
56
+ case menstruationFlow = "menstruation.flow"
57
+ case menstruationEvents = "menstruation.events"
58
+ case athleteProfile = "athlete.profile"
59
+ case plannedWorkoutsPlanned = "plannedWorkouts.planned"
60
+ case plannedWorkoutsMarkComplete = "plannedWorkouts.markComplete"
61
+ case plannedWorkoutsDelete = "plannedWorkouts.delete"
62
+ case plannedWorkoutsSchedulingStatus = "plannedWorkouts.schedulingStatus"
63
+ case plannedWorkoutsRequestScheduling = "plannedWorkouts.requestScheduling"
64
+
65
+ /// Whether this platform implements the op.
66
+ public var isSupported: Bool {
67
+ switch self {
68
+ case .clientAttach: return true
69
+ case .clientConnect: return true
70
+ case .clientConnections: return true
71
+ case .clientAvailability: return true
72
+ case .clientDisconnect: return true
73
+ case .tokenSetProviderPresent: return true
74
+ case .tokenResolve: return true
75
+ case .tokenReject: return true
76
+ case .permissionsRequest: return true
77
+ case .permissionsStatus: return true
78
+ case .permissionsWriteStatus: return true
79
+ case .permissionsRequested: return true
80
+ case .permissionsStatuses: return false
81
+ case .permissionsBackgroundReadStatus: return false
82
+ case .permissionsHistoryReadStatus: return false
83
+ case .diagnosticsState: return true
84
+ case .diagnosticsLastDelivery: return true
85
+ case .diagnosticsReport: return true
86
+ case .diagnosticsSend: return true
87
+ case .backgroundIsRegistered: return true
88
+ case .backgroundCancel: return false
89
+ case .bodyHeartRate: return true
90
+ case .bodyRestingHeartRate: return true
91
+ case .bodyReadings: return true
92
+ case .bodyLogHeartRate: return true
93
+ case .bodyLog: return true
94
+ case .bodyDelete: return true
95
+ case .dailySteps: return true
96
+ case .dailyTotals: return true
97
+ case .dailyIntraday: return true
98
+ case .dailyLog: return true
99
+ case .dailyDelete: return true
100
+ case .sleepSegments: return true
101
+ case .sleepLog: return true
102
+ case .sleepDelete: return true
103
+ case .activityWorkouts: return true
104
+ case .activityRoutes: return true
105
+ case .activityIntensity: return true
106
+ case .activityLog: return true
107
+ case .activityDelete: return true
108
+ case .nutritionQuantities: return true
109
+ case .nutritionLog: return true
110
+ case .nutritionDelete: return true
111
+ case .nutritionHydration: return true
112
+ case .menstruationFlow: return true
113
+ case .menstruationEvents: return true
114
+ case .athleteProfile: return true
115
+ case .plannedWorkoutsPlanned: return true
116
+ case .plannedWorkoutsMarkComplete: return true
117
+ case .plannedWorkoutsDelete: return true
118
+ case .plannedWorkoutsSchedulingStatus: return true
119
+ case .plannedWorkoutsRequestScheduling: return true
120
+ }
121
+ }
122
+ }
123
+
124
+ /// What P3 implements. One method per op this platform supports, and no `default` anywhere:
125
+ /// an op added to the schema breaks this build rather than reaching a customer as a rejection.
126
+ public protocol TerraBridgeOps: Sendable {
127
+ /// Reports the native client the JS side is attaching to. Idempotent, and safe to call after a
128
+ /// JS reload.
129
+ func clientAttach(_ request: Empty) async throws -> BootstrapState
130
+ func clientConnect(_ request: ConnectRequest) async throws -> Connection
131
+ func clientConnections(_ request: Empty) async throws -> ConnectionList
132
+ /// Whether the device can serve the source at all. Needs no connection and prompts for nothing.
133
+ func clientAvailability(_ request: SourceRequest) async throws -> AvailabilityResult
134
+ func clientDisconnect(_ request: SourceRequest) async throws -> Empty
135
+ /// Tells the broker whether JS can answer at all. Without it a link asks a provider that is not
136
+ /// there and waits out the timeout.
137
+ func tokenSetProviderPresent(_ request: TokenProviderRequest) async throws -> Empty
138
+ /// An unknown or already-settled requestId is ignored rather than rejected, so a reload cannot
139
+ /// fail a link that has already moved on.
140
+ func tokenResolve(_ request: TokenResolveRequest) async throws -> Empty
141
+ func tokenReject(_ request: TokenRejectRequest) async throws -> Empty
142
+ func permissionsRequest(_ request: PermissionRequest) async throws -> PermissionGrant
143
+ func permissionsStatus(_ request: CategoryRequest) async throws -> StatusResult
144
+ func permissionsWriteStatus(_ request: CategoryRequest) async throws -> StatusResult
145
+ func permissionsRequested(_ request: SourceRequest) async throws -> HealthAccess
146
+ func diagnosticsState(_ request: SourceRequest) async throws -> DiagnosticsState
147
+ func diagnosticsLastDelivery(_ request: SourceRequest) async throws -> LastDeliveryResult
148
+ func diagnosticsReport(_ request: SourceRequest) async throws -> SupportReport
149
+ func diagnosticsSend(_ request: SourceRequest) async throws -> Empty
150
+ /// False means bootstrap did not run. The SDK reports the same thing independently as
151
+ /// backgroundRefreshUnscheduled.
152
+ func backgroundIsRegistered(_ request: Empty) async throws -> BoolResult
153
+ func bodyHeartRate(_ request: RangeRequest) async throws -> HeartRateList
154
+ /// The store's own daily figures. Health Connect grants them under daily, so Android answers
155
+ /// authorization_not_determined where only body was asked for.
156
+ func bodyRestingHeartRate(_ request: RangeRequest) async throws -> RestingHeartRateList
157
+ func bodyReadings(_ request: RangeRequest) async throws -> BodyReadingList
158
+ /// Its own op rather than an overload: Swift overloads log by element type and JS cannot.
159
+ func bodyLogHeartRate(_ request: HeartRateEntryList) async throws -> RecordIdList
160
+ func bodyLog(_ request: BodyEntryList) async throws -> RecordIdList
161
+ func bodyDelete(_ request: RecordIdRequest) async throws -> Empty
162
+ func dailySteps(_ request: RangeRequest) async throws -> StepCountList
163
+ /// One total per metric per whole local day the range touches. A day with nothing recorded has
164
+ /// no total, not a zero.
165
+ func dailyTotals(_ request: RangeRequest) async throws -> DayTotalList
166
+ /// The active energy, basal energy, distance and floors records the totals are made of.
167
+ func dailyIntraday(_ request: RangeRequest) async throws -> IntradaySampleList
168
+ func dailyLog(_ request: StepEntryList) async throws -> RecordIdList
169
+ func dailyDelete(_ request: RecordIdRequest) async throws -> Empty
170
+ func sleepSegments(_ request: RangeRequest) async throws -> SleepSegmentList
171
+ func sleepLog(_ request: SleepEntryList) async throws -> RecordIdList
172
+ func sleepDelete(_ request: RecordIdRequest) async throws -> Empty
173
+ func activityWorkouts(_ request: RangeRequest) async throws -> WorkoutList
174
+ /// The GPS tracks of the workouts in the range, each naming its workout.
175
+ func activityRoutes(_ request: RangeRequest) async throws -> WorkoutRouteList
176
+ /// Speed, power and cycling cadence readings in the range.
177
+ func activityIntensity(_ request: RangeRequest) async throws -> WorkoutIntensityList
178
+ func activityLog(_ request: WorkoutEntryList) async throws -> RecordIdList
179
+ func activityDelete(_ request: RecordIdRequest) async throws -> Empty
180
+ /// One quantity per nutrient a meal recorded, not grouped into meals. Water is not among them.
181
+ func nutritionQuantities(_ request: RangeRequest) async throws -> DietaryQuantityList
182
+ func nutritionLog(_ request: NutritionEntryList) async throws -> RecordIdList
183
+ func nutritionDelete(_ request: RecordIdRequest) async throws -> Empty
184
+ /// Water drunk in the range, one sample per drink.
185
+ func nutritionHydration(_ request: RangeRequest) async throws -> HydrationList
186
+ /// Logged flow days overlapping the range, one per day. A day logged with no flow is noFlow,
187
+ /// not left out.
188
+ func menstruationFlow(_ request: RangeRequest) async throws -> MenstruationFlowDayList
189
+ /// Ovulation tests, cervical mucus and intermenstrual bleeding logged in the range.
190
+ func menstruationEvents(_ request: RangeRequest) async throws -> MenstrualEventList
191
+ /// Characteristics have no history.
192
+ ///
193
+ /// Available on ios only.
194
+ func athleteProfile(_ request: Empty) async throws -> AthleteProfileResult
195
+ /// Available on ios only.
196
+ func plannedWorkoutsPlanned(_ request: Empty) async throws -> PlannedWorkoutList
197
+ /// Available on ios only.
198
+ func plannedWorkoutsMarkComplete(_ request: MarkCompleteRequest) async throws -> Empty
199
+ /// Available on ios only.
200
+ func plannedWorkoutsDelete(_ request: RecordIdRequest) async throws -> Empty
201
+ /// Never prompts.
202
+ ///
203
+ /// Available on ios only.
204
+ func plannedWorkoutsSchedulingStatus(_ request: Empty) async throws -> WorkoutScheduling
205
+ /// Shows WorkoutKit's sheet if the person has not been asked, and reconciles straight away once
206
+ /// authorized.
207
+ ///
208
+ /// Available on ios only.
209
+ func plannedWorkoutsRequestScheduling(_ request: Empty) async throws -> WorkoutScheduling
210
+ }
211
+
212
+ /// Decodes one side of an exchange and re-encodes it, for the bridge-vector runner.
213
+ ///
214
+ /// The runner cannot name the request and response types itself without keeping a second copy
215
+ /// of the op table, and a second copy of the op table is the thing this schema exists to
216
+ /// remove. A round trip is also exactly what a vector asserts: the bytes in are the bytes out,
217
+ /// which pins the DTOs and their coders against the same file the other two runners read.
218
+ public enum BridgeCodecs {
219
+ public static func roundTripRequest(_ op: BridgeOp, _ payload: Data) throws -> Data {
220
+ let decoder = BridgeDispatch.makeDecoder()
221
+ let encoder = BridgeDispatch.makeEncoder()
222
+ switch op {
223
+ case .clientAttach:
224
+ let value = try decoder.decode(Empty.self, from: payload)
225
+ return try encoder.encode(value)
226
+ case .clientConnect:
227
+ let value = try decoder.decode(ConnectRequest.self, from: payload)
228
+ return try encoder.encode(value)
229
+ case .clientConnections:
230
+ let value = try decoder.decode(Empty.self, from: payload)
231
+ return try encoder.encode(value)
232
+ case .clientAvailability:
233
+ let value = try decoder.decode(SourceRequest.self, from: payload)
234
+ return try encoder.encode(value)
235
+ case .clientDisconnect:
236
+ let value = try decoder.decode(SourceRequest.self, from: payload)
237
+ return try encoder.encode(value)
238
+ case .tokenSetProviderPresent:
239
+ let value = try decoder.decode(TokenProviderRequest.self, from: payload)
240
+ return try encoder.encode(value)
241
+ case .tokenResolve:
242
+ let value = try decoder.decode(TokenResolveRequest.self, from: payload)
243
+ return try encoder.encode(value)
244
+ case .tokenReject:
245
+ let value = try decoder.decode(TokenRejectRequest.self, from: payload)
246
+ return try encoder.encode(value)
247
+ case .permissionsRequest:
248
+ let value = try decoder.decode(PermissionRequest.self, from: payload)
249
+ return try encoder.encode(value)
250
+ case .permissionsStatus:
251
+ let value = try decoder.decode(CategoryRequest.self, from: payload)
252
+ return try encoder.encode(value)
253
+ case .permissionsWriteStatus:
254
+ let value = try decoder.decode(CategoryRequest.self, from: payload)
255
+ return try encoder.encode(value)
256
+ case .permissionsRequested:
257
+ let value = try decoder.decode(SourceRequest.self, from: payload)
258
+ return try encoder.encode(value)
259
+ case .permissionsStatuses:
260
+ let value = try decoder.decode(SourceRequest.self, from: payload)
261
+ return try encoder.encode(value)
262
+ case .permissionsBackgroundReadStatus:
263
+ let value = try decoder.decode(SourceRequest.self, from: payload)
264
+ return try encoder.encode(value)
265
+ case .permissionsHistoryReadStatus:
266
+ let value = try decoder.decode(SourceRequest.self, from: payload)
267
+ return try encoder.encode(value)
268
+ case .diagnosticsState:
269
+ let value = try decoder.decode(SourceRequest.self, from: payload)
270
+ return try encoder.encode(value)
271
+ case .diagnosticsLastDelivery:
272
+ let value = try decoder.decode(SourceRequest.self, from: payload)
273
+ return try encoder.encode(value)
274
+ case .diagnosticsReport:
275
+ let value = try decoder.decode(SourceRequest.self, from: payload)
276
+ return try encoder.encode(value)
277
+ case .diagnosticsSend:
278
+ let value = try decoder.decode(SourceRequest.self, from: payload)
279
+ return try encoder.encode(value)
280
+ case .backgroundIsRegistered:
281
+ let value = try decoder.decode(Empty.self, from: payload)
282
+ return try encoder.encode(value)
283
+ case .backgroundCancel:
284
+ let value = try decoder.decode(Empty.self, from: payload)
285
+ return try encoder.encode(value)
286
+ case .bodyHeartRate:
287
+ let value = try decoder.decode(RangeRequest.self, from: payload)
288
+ return try encoder.encode(value)
289
+ case .bodyRestingHeartRate:
290
+ let value = try decoder.decode(RangeRequest.self, from: payload)
291
+ return try encoder.encode(value)
292
+ case .bodyReadings:
293
+ let value = try decoder.decode(RangeRequest.self, from: payload)
294
+ return try encoder.encode(value)
295
+ case .bodyLogHeartRate:
296
+ let value = try decoder.decode(HeartRateEntryList.self, from: payload)
297
+ return try encoder.encode(value)
298
+ case .bodyLog:
299
+ let value = try decoder.decode(BodyEntryList.self, from: payload)
300
+ return try encoder.encode(value)
301
+ case .bodyDelete:
302
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
303
+ return try encoder.encode(value)
304
+ case .dailySteps:
305
+ let value = try decoder.decode(RangeRequest.self, from: payload)
306
+ return try encoder.encode(value)
307
+ case .dailyTotals:
308
+ let value = try decoder.decode(RangeRequest.self, from: payload)
309
+ return try encoder.encode(value)
310
+ case .dailyIntraday:
311
+ let value = try decoder.decode(RangeRequest.self, from: payload)
312
+ return try encoder.encode(value)
313
+ case .dailyLog:
314
+ let value = try decoder.decode(StepEntryList.self, from: payload)
315
+ return try encoder.encode(value)
316
+ case .dailyDelete:
317
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
318
+ return try encoder.encode(value)
319
+ case .sleepSegments:
320
+ let value = try decoder.decode(RangeRequest.self, from: payload)
321
+ return try encoder.encode(value)
322
+ case .sleepLog:
323
+ let value = try decoder.decode(SleepEntryList.self, from: payload)
324
+ return try encoder.encode(value)
325
+ case .sleepDelete:
326
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
327
+ return try encoder.encode(value)
328
+ case .activityWorkouts:
329
+ let value = try decoder.decode(RangeRequest.self, from: payload)
330
+ return try encoder.encode(value)
331
+ case .activityRoutes:
332
+ let value = try decoder.decode(RangeRequest.self, from: payload)
333
+ return try encoder.encode(value)
334
+ case .activityIntensity:
335
+ let value = try decoder.decode(RangeRequest.self, from: payload)
336
+ return try encoder.encode(value)
337
+ case .activityLog:
338
+ let value = try decoder.decode(WorkoutEntryList.self, from: payload)
339
+ return try encoder.encode(value)
340
+ case .activityDelete:
341
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
342
+ return try encoder.encode(value)
343
+ case .nutritionQuantities:
344
+ let value = try decoder.decode(RangeRequest.self, from: payload)
345
+ return try encoder.encode(value)
346
+ case .nutritionLog:
347
+ let value = try decoder.decode(NutritionEntryList.self, from: payload)
348
+ return try encoder.encode(value)
349
+ case .nutritionDelete:
350
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
351
+ return try encoder.encode(value)
352
+ case .nutritionHydration:
353
+ let value = try decoder.decode(RangeRequest.self, from: payload)
354
+ return try encoder.encode(value)
355
+ case .menstruationFlow:
356
+ let value = try decoder.decode(RangeRequest.self, from: payload)
357
+ return try encoder.encode(value)
358
+ case .menstruationEvents:
359
+ let value = try decoder.decode(RangeRequest.self, from: payload)
360
+ return try encoder.encode(value)
361
+ case .athleteProfile:
362
+ let value = try decoder.decode(Empty.self, from: payload)
363
+ return try encoder.encode(value)
364
+ case .plannedWorkoutsPlanned:
365
+ let value = try decoder.decode(Empty.self, from: payload)
366
+ return try encoder.encode(value)
367
+ case .plannedWorkoutsMarkComplete:
368
+ let value = try decoder.decode(MarkCompleteRequest.self, from: payload)
369
+ return try encoder.encode(value)
370
+ case .plannedWorkoutsDelete:
371
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
372
+ return try encoder.encode(value)
373
+ case .plannedWorkoutsSchedulingStatus:
374
+ let value = try decoder.decode(Empty.self, from: payload)
375
+ return try encoder.encode(value)
376
+ case .plannedWorkoutsRequestScheduling:
377
+ let value = try decoder.decode(Empty.self, from: payload)
378
+ return try encoder.encode(value)
379
+ }
380
+ }
381
+
382
+ public static func roundTripResponse(_ op: BridgeOp, _ payload: Data) throws -> Data {
383
+ let decoder = BridgeDispatch.makeDecoder()
384
+ let encoder = BridgeDispatch.makeEncoder()
385
+ switch op {
386
+ case .clientAttach:
387
+ let value = try decoder.decode(BootstrapState.self, from: payload)
388
+ return try encoder.encode(value)
389
+ case .clientConnect:
390
+ let value = try decoder.decode(Connection.self, from: payload)
391
+ return try encoder.encode(value)
392
+ case .clientConnections:
393
+ let value = try decoder.decode(ConnectionList.self, from: payload)
394
+ return try encoder.encode(value)
395
+ case .clientAvailability:
396
+ let value = try decoder.decode(AvailabilityResult.self, from: payload)
397
+ return try encoder.encode(value)
398
+ case .clientDisconnect:
399
+ let value = try decoder.decode(Empty.self, from: payload)
400
+ return try encoder.encode(value)
401
+ case .tokenSetProviderPresent:
402
+ let value = try decoder.decode(Empty.self, from: payload)
403
+ return try encoder.encode(value)
404
+ case .tokenResolve:
405
+ let value = try decoder.decode(Empty.self, from: payload)
406
+ return try encoder.encode(value)
407
+ case .tokenReject:
408
+ let value = try decoder.decode(Empty.self, from: payload)
409
+ return try encoder.encode(value)
410
+ case .permissionsRequest:
411
+ let value = try decoder.decode(PermissionGrant.self, from: payload)
412
+ return try encoder.encode(value)
413
+ case .permissionsStatus:
414
+ let value = try decoder.decode(StatusResult.self, from: payload)
415
+ return try encoder.encode(value)
416
+ case .permissionsWriteStatus:
417
+ let value = try decoder.decode(StatusResult.self, from: payload)
418
+ return try encoder.encode(value)
419
+ case .permissionsRequested:
420
+ let value = try decoder.decode(HealthAccess.self, from: payload)
421
+ return try encoder.encode(value)
422
+ case .permissionsStatuses:
423
+ let value = try decoder.decode(StatusesResult.self, from: payload)
424
+ return try encoder.encode(value)
425
+ case .permissionsBackgroundReadStatus:
426
+ let value = try decoder.decode(StatusResult.self, from: payload)
427
+ return try encoder.encode(value)
428
+ case .permissionsHistoryReadStatus:
429
+ let value = try decoder.decode(StatusResult.self, from: payload)
430
+ return try encoder.encode(value)
431
+ case .diagnosticsState:
432
+ let value = try decoder.decode(DiagnosticsState.self, from: payload)
433
+ return try encoder.encode(value)
434
+ case .diagnosticsLastDelivery:
435
+ let value = try decoder.decode(LastDeliveryResult.self, from: payload)
436
+ return try encoder.encode(value)
437
+ case .diagnosticsReport:
438
+ let value = try decoder.decode(SupportReport.self, from: payload)
439
+ try SupportReportEnvelope.validate(value)
440
+ return try encoder.encode(value)
441
+ case .diagnosticsSend:
442
+ let value = try decoder.decode(Empty.self, from: payload)
443
+ return try encoder.encode(value)
444
+ case .backgroundIsRegistered:
445
+ let value = try decoder.decode(BoolResult.self, from: payload)
446
+ return try encoder.encode(value)
447
+ case .backgroundCancel:
448
+ let value = try decoder.decode(BoolResult.self, from: payload)
449
+ return try encoder.encode(value)
450
+ case .bodyHeartRate:
451
+ let value = try decoder.decode(HeartRateList.self, from: payload)
452
+ return try encoder.encode(value)
453
+ case .bodyRestingHeartRate:
454
+ let value = try decoder.decode(RestingHeartRateList.self, from: payload)
455
+ return try encoder.encode(value)
456
+ case .bodyReadings:
457
+ let value = try decoder.decode(BodyReadingList.self, from: payload)
458
+ return try encoder.encode(value)
459
+ case .bodyLogHeartRate:
460
+ let value = try decoder.decode(RecordIdList.self, from: payload)
461
+ return try encoder.encode(value)
462
+ case .bodyLog:
463
+ let value = try decoder.decode(RecordIdList.self, from: payload)
464
+ return try encoder.encode(value)
465
+ case .bodyDelete:
466
+ let value = try decoder.decode(Empty.self, from: payload)
467
+ return try encoder.encode(value)
468
+ case .dailySteps:
469
+ let value = try decoder.decode(StepCountList.self, from: payload)
470
+ return try encoder.encode(value)
471
+ case .dailyTotals:
472
+ let value = try decoder.decode(DayTotalList.self, from: payload)
473
+ return try encoder.encode(value)
474
+ case .dailyIntraday:
475
+ let value = try decoder.decode(IntradaySampleList.self, from: payload)
476
+ return try encoder.encode(value)
477
+ case .dailyLog:
478
+ let value = try decoder.decode(RecordIdList.self, from: payload)
479
+ return try encoder.encode(value)
480
+ case .dailyDelete:
481
+ let value = try decoder.decode(Empty.self, from: payload)
482
+ return try encoder.encode(value)
483
+ case .sleepSegments:
484
+ let value = try decoder.decode(SleepSegmentList.self, from: payload)
485
+ return try encoder.encode(value)
486
+ case .sleepLog:
487
+ let value = try decoder.decode(RecordIdList.self, from: payload)
488
+ return try encoder.encode(value)
489
+ case .sleepDelete:
490
+ let value = try decoder.decode(Empty.self, from: payload)
491
+ return try encoder.encode(value)
492
+ case .activityWorkouts:
493
+ let value = try decoder.decode(WorkoutList.self, from: payload)
494
+ return try encoder.encode(value)
495
+ case .activityRoutes:
496
+ let value = try decoder.decode(WorkoutRouteList.self, from: payload)
497
+ return try encoder.encode(value)
498
+ case .activityIntensity:
499
+ let value = try decoder.decode(WorkoutIntensityList.self, from: payload)
500
+ return try encoder.encode(value)
501
+ case .activityLog:
502
+ let value = try decoder.decode(RecordIdList.self, from: payload)
503
+ return try encoder.encode(value)
504
+ case .activityDelete:
505
+ let value = try decoder.decode(Empty.self, from: payload)
506
+ return try encoder.encode(value)
507
+ case .nutritionQuantities:
508
+ let value = try decoder.decode(DietaryQuantityList.self, from: payload)
509
+ return try encoder.encode(value)
510
+ case .nutritionLog:
511
+ let value = try decoder.decode(RecordIdList.self, from: payload)
512
+ return try encoder.encode(value)
513
+ case .nutritionDelete:
514
+ let value = try decoder.decode(Empty.self, from: payload)
515
+ return try encoder.encode(value)
516
+ case .nutritionHydration:
517
+ let value = try decoder.decode(HydrationList.self, from: payload)
518
+ return try encoder.encode(value)
519
+ case .menstruationFlow:
520
+ let value = try decoder.decode(MenstruationFlowDayList.self, from: payload)
521
+ return try encoder.encode(value)
522
+ case .menstruationEvents:
523
+ let value = try decoder.decode(MenstrualEventList.self, from: payload)
524
+ return try encoder.encode(value)
525
+ case .athleteProfile:
526
+ let value = try decoder.decode(AthleteProfileResult.self, from: payload)
527
+ return try encoder.encode(value)
528
+ case .plannedWorkoutsPlanned:
529
+ let value = try decoder.decode(PlannedWorkoutList.self, from: payload)
530
+ return try encoder.encode(value)
531
+ case .plannedWorkoutsMarkComplete:
532
+ let value = try decoder.decode(Empty.self, from: payload)
533
+ return try encoder.encode(value)
534
+ case .plannedWorkoutsDelete:
535
+ let value = try decoder.decode(Empty.self, from: payload)
536
+ return try encoder.encode(value)
537
+ case .plannedWorkoutsSchedulingStatus:
538
+ let value = try decoder.decode(WorkoutScheduling.self, from: payload)
539
+ return try encoder.encode(value)
540
+ case .plannedWorkoutsRequestScheduling:
541
+ let value = try decoder.decode(WorkoutScheduling.self, from: payload)
542
+ return try encoder.encode(value)
543
+ }
544
+ }
545
+
546
+ /// One message as a subscription actually delivers it: the envelope, and the element inside.
547
+ ///
548
+ /// Both halves, because checking only the envelope would accept a `data` carrying anything
549
+ /// at all, and checking only the element would accept a stream that never says it failed.
550
+ public static func roundTripStreamMessage(_ stream: String, _ payload: Data) throws -> Data {
551
+ let decoder = BridgeDispatch.makeDecoder()
552
+ let encoder = BridgeDispatch.makeEncoder()
553
+ let message = try decoder.decode(StreamMessage.self, from: payload)
554
+ guard case .data(let element) = message else { return try encoder.encode(message) }
555
+ let inner = try roundTripStreamElement(stream, try encoder.encode(element))
556
+ let checked = try decoder.decode(BridgeJSON.self, from: inner)
557
+ return try encoder.encode(StreamMessage.data(element: checked))
558
+ }
559
+
560
+ /// One element of a stream, by that stream's declared type.
561
+ public static func roundTripStreamElement(_ stream: String, _ payload: Data) throws -> Data {
562
+ let decoder = BridgeDispatch.makeDecoder()
563
+ let encoder = BridgeDispatch.makeEncoder()
564
+ switch stream {
565
+ case "diagnostics.events":
566
+ let value = try decoder.decode(TerraEvent.self, from: payload)
567
+ try TerraEventEnvelope.validate(value)
568
+ return try encoder.encode(value)
569
+ case "body.changes":
570
+ let value = try decoder.decode(HealthChange.self, from: payload)
571
+ return try encoder.encode(value)
572
+ case "daily.changes":
573
+ let value = try decoder.decode(HealthChange.self, from: payload)
574
+ return try encoder.encode(value)
575
+ case "sleep.changes":
576
+ let value = try decoder.decode(HealthChange.self, from: payload)
577
+ return try encoder.encode(value)
578
+ case "activity.changes":
579
+ let value = try decoder.decode(HealthChange.self, from: payload)
580
+ return try encoder.encode(value)
581
+ case "bridge.tokenRequest":
582
+ let value = try decoder.decode(TokenRequest.self, from: payload)
583
+ return try encoder.encode(value)
584
+ default:
585
+ throw BridgeError.unsupportedOnPlatform(op: stream)
586
+ }
587
+ }
588
+ }
589
+
590
+ /// Turns an op string and a JSON payload into a typed call and a JSON answer.
591
+ ///
592
+ /// The encoder and decoder are built once and configured for ISO 8601: an instant spelled two
593
+ /// ways between a request and its response is a bug the vectors would catch and the compiler
594
+ /// would not.
595
+ public struct BridgeDispatch: Sendable {
596
+ private let ops: any TerraBridgeOps
597
+
598
+ public init(ops: any TerraBridgeOps) { self.ops = ops }
599
+
600
+ public static func makeDecoder() -> JSONDecoder { JSONDecoder() }
601
+
602
+ /// Sorted keys, so that two encodings of one value are the same bytes. That is what lets a
603
+ /// vector be compared rather than parsed and walked, and what makes the three runners
604
+ /// compare like for like.
605
+ public static func makeEncoder() -> JSONEncoder {
606
+ let encoder = JSONEncoder()
607
+ encoder.outputFormatting = [.sortedKeys]
608
+ return encoder
609
+ }
610
+
611
+ public static func decodeRequest<Request: Decodable>(
612
+ _ type: Request.Type,
613
+ from payload: Data,
614
+ validate: (Request) throws -> Void = { _ in }
615
+ ) throws -> Request {
616
+ do {
617
+ let request = try makeDecoder().decode(Request.self, from: payload)
618
+ try validate(request)
619
+ return request
620
+ } catch let error as DecodingError {
621
+ throw BridgeError.invalidPayload(path: path(of: error))
622
+ }
623
+ }
624
+
625
+ static func path(of error: DecodingError) -> String {
626
+ let keys: [any CodingKey]
627
+ switch error {
628
+ case .typeMismatch(_, let context), .valueNotFound(_, let context), .dataCorrupted(let context):
629
+ keys = context.codingPath
630
+ case .keyNotFound(let key, let context):
631
+ keys = context.codingPath + [key]
632
+ @unknown default:
633
+ keys = []
634
+ }
635
+ return keys.reduce("$") { path, key in
636
+ if let index = key.intValue { return "\(path)[\(index)]" }
637
+ return "\(path).\(key.stringValue)"
638
+ }
639
+ }
640
+
641
+ public func callAsFunction(op: String, payload: Data) async throws -> Data {
642
+ // An op string no version of the schema declared: a wrapper and a native half that
643
+ // disagree. Reported as the same code an op this platform lacks gets, because from the
644
+ // caller's side the two are indistinguishable and both mean "not available here".
645
+ guard let known = BridgeOp(rawValue: op) else { throw BridgeError.unsupportedOnPlatform(op: op) }
646
+ return try await dispatch(known, payload)
647
+ }
648
+
649
+ private func dispatch(_ op: BridgeOp, _ payload: Data) async throws -> Data {
650
+ let encoder = Self.makeEncoder()
651
+ switch op {
652
+ case .clientAttach:
653
+ let request = try Self.decodeRequest(Empty.self, from: payload)
654
+ let response = try await ops.clientAttach(request)
655
+ return try encoder.encode(response)
656
+ case .clientConnect:
657
+ let request = try Self.decodeRequest(ConnectRequest.self, from: payload)
658
+ let response = try await ops.clientConnect(request)
659
+ return try encoder.encode(response)
660
+ case .clientConnections:
661
+ let request = try Self.decodeRequest(Empty.self, from: payload)
662
+ let response = try await ops.clientConnections(request)
663
+ return try encoder.encode(response)
664
+ case .clientAvailability:
665
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
666
+ let response = try await ops.clientAvailability(request)
667
+ return try encoder.encode(response)
668
+ case .clientDisconnect:
669
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
670
+ let response = try await ops.clientDisconnect(request)
671
+ return try encoder.encode(response)
672
+ case .tokenSetProviderPresent:
673
+ let request = try Self.decodeRequest(TokenProviderRequest.self, from: payload)
674
+ let response = try await ops.tokenSetProviderPresent(request)
675
+ return try encoder.encode(response)
676
+ case .tokenResolve:
677
+ let request = try Self.decodeRequest(TokenResolveRequest.self, from: payload)
678
+ let response = try await ops.tokenResolve(request)
679
+ return try encoder.encode(response)
680
+ case .tokenReject:
681
+ let request = try Self.decodeRequest(TokenRejectRequest.self, from: payload)
682
+ let response = try await ops.tokenReject(request)
683
+ return try encoder.encode(response)
684
+ case .permissionsRequest:
685
+ let request = try Self.decodeRequest(PermissionRequest.self, from: payload)
686
+ let response = try await ops.permissionsRequest(request)
687
+ return try encoder.encode(response)
688
+ case .permissionsStatus:
689
+ let request = try Self.decodeRequest(CategoryRequest.self, from: payload)
690
+ let response = try await ops.permissionsStatus(request)
691
+ return try encoder.encode(response)
692
+ case .permissionsWriteStatus:
693
+ let request = try Self.decodeRequest(CategoryRequest.self, from: payload)
694
+ let response = try await ops.permissionsWriteStatus(request)
695
+ return try encoder.encode(response)
696
+ case .permissionsRequested:
697
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
698
+ let response = try await ops.permissionsRequested(request)
699
+ return try encoder.encode(response)
700
+ case .permissionsStatuses:
701
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
702
+ case .permissionsBackgroundReadStatus:
703
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
704
+ case .permissionsHistoryReadStatus:
705
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
706
+ case .diagnosticsState:
707
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
708
+ let response = try await ops.diagnosticsState(request)
709
+ return try encoder.encode(response)
710
+ case .diagnosticsLastDelivery:
711
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
712
+ let response = try await ops.diagnosticsLastDelivery(request)
713
+ return try encoder.encode(response)
714
+ case .diagnosticsReport:
715
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
716
+ let response = try await ops.diagnosticsReport(request)
717
+ try SupportReportEnvelope.validate(response)
718
+ return try encoder.encode(response)
719
+ case .diagnosticsSend:
720
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
721
+ let response = try await ops.diagnosticsSend(request)
722
+ return try encoder.encode(response)
723
+ case .backgroundIsRegistered:
724
+ let request = try Self.decodeRequest(Empty.self, from: payload)
725
+ let response = try await ops.backgroundIsRegistered(request)
726
+ return try encoder.encode(response)
727
+ case .backgroundCancel:
728
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
729
+ case .bodyHeartRate:
730
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
731
+ let response = try await ops.bodyHeartRate(request)
732
+ return try encoder.encode(response)
733
+ case .bodyRestingHeartRate:
734
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
735
+ let response = try await ops.bodyRestingHeartRate(request)
736
+ return try encoder.encode(response)
737
+ case .bodyReadings:
738
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
739
+ let response = try await ops.bodyReadings(request)
740
+ return try encoder.encode(response)
741
+ case .bodyLogHeartRate:
742
+ let request = try Self.decodeRequest(HeartRateEntryList.self, from: payload)
743
+ let response = try await ops.bodyLogHeartRate(request)
744
+ return try encoder.encode(response)
745
+ case .bodyLog:
746
+ let request = try Self.decodeRequest(BodyEntryList.self, from: payload)
747
+ let response = try await ops.bodyLog(request)
748
+ return try encoder.encode(response)
749
+ case .bodyDelete:
750
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
751
+ let response = try await ops.bodyDelete(request)
752
+ return try encoder.encode(response)
753
+ case .dailySteps:
754
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
755
+ let response = try await ops.dailySteps(request)
756
+ return try encoder.encode(response)
757
+ case .dailyTotals:
758
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
759
+ let response = try await ops.dailyTotals(request)
760
+ return try encoder.encode(response)
761
+ case .dailyIntraday:
762
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
763
+ let response = try await ops.dailyIntraday(request)
764
+ return try encoder.encode(response)
765
+ case .dailyLog:
766
+ let request = try Self.decodeRequest(StepEntryList.self, from: payload)
767
+ let response = try await ops.dailyLog(request)
768
+ return try encoder.encode(response)
769
+ case .dailyDelete:
770
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
771
+ let response = try await ops.dailyDelete(request)
772
+ return try encoder.encode(response)
773
+ case .sleepSegments:
774
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
775
+ let response = try await ops.sleepSegments(request)
776
+ return try encoder.encode(response)
777
+ case .sleepLog:
778
+ let request = try Self.decodeRequest(SleepEntryList.self, from: payload)
779
+ let response = try await ops.sleepLog(request)
780
+ return try encoder.encode(response)
781
+ case .sleepDelete:
782
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
783
+ let response = try await ops.sleepDelete(request)
784
+ return try encoder.encode(response)
785
+ case .activityWorkouts:
786
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
787
+ let response = try await ops.activityWorkouts(request)
788
+ return try encoder.encode(response)
789
+ case .activityRoutes:
790
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
791
+ let response = try await ops.activityRoutes(request)
792
+ return try encoder.encode(response)
793
+ case .activityIntensity:
794
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
795
+ let response = try await ops.activityIntensity(request)
796
+ return try encoder.encode(response)
797
+ case .activityLog:
798
+ let request = try Self.decodeRequest(WorkoutEntryList.self, from: payload)
799
+ let response = try await ops.activityLog(request)
800
+ return try encoder.encode(response)
801
+ case .activityDelete:
802
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
803
+ let response = try await ops.activityDelete(request)
804
+ return try encoder.encode(response)
805
+ case .nutritionQuantities:
806
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
807
+ let response = try await ops.nutritionQuantities(request)
808
+ return try encoder.encode(response)
809
+ case .nutritionLog:
810
+ let request = try Self.decodeRequest(NutritionEntryList.self, from: payload)
811
+ let response = try await ops.nutritionLog(request)
812
+ return try encoder.encode(response)
813
+ case .nutritionDelete:
814
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
815
+ let response = try await ops.nutritionDelete(request)
816
+ return try encoder.encode(response)
817
+ case .nutritionHydration:
818
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
819
+ let response = try await ops.nutritionHydration(request)
820
+ return try encoder.encode(response)
821
+ case .menstruationFlow:
822
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
823
+ let response = try await ops.menstruationFlow(request)
824
+ return try encoder.encode(response)
825
+ case .menstruationEvents:
826
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
827
+ let response = try await ops.menstruationEvents(request)
828
+ return try encoder.encode(response)
829
+ case .athleteProfile:
830
+ let request = try Self.decodeRequest(Empty.self, from: payload)
831
+ let response = try await ops.athleteProfile(request)
832
+ return try encoder.encode(response)
833
+ case .plannedWorkoutsPlanned:
834
+ let request = try Self.decodeRequest(Empty.self, from: payload)
835
+ let response = try await ops.plannedWorkoutsPlanned(request)
836
+ return try encoder.encode(response)
837
+ case .plannedWorkoutsMarkComplete:
838
+ let request = try Self.decodeRequest(MarkCompleteRequest.self, from: payload)
839
+ let response = try await ops.plannedWorkoutsMarkComplete(request)
840
+ return try encoder.encode(response)
841
+ case .plannedWorkoutsDelete:
842
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
843
+ let response = try await ops.plannedWorkoutsDelete(request)
844
+ return try encoder.encode(response)
845
+ case .plannedWorkoutsSchedulingStatus:
846
+ let request = try Self.decodeRequest(Empty.self, from: payload)
847
+ let response = try await ops.plannedWorkoutsSchedulingStatus(request)
848
+ return try encoder.encode(response)
849
+ case .plannedWorkoutsRequestScheduling:
850
+ let request = try Self.decodeRequest(Empty.self, from: payload)
851
+ let response = try await ops.plannedWorkoutsRequestScheduling(request)
852
+ return try encoder.encode(response)
853
+ }
854
+ }
855
+ }
856
+
857
+ /// Every stream the schema declares, including the ones this platform does not publish.
858
+ public enum BridgeStream: String, CaseIterable, Sendable {
859
+ case diagnosticsEvents = "diagnostics.events"
860
+ case bodyChanges = "body.changes"
861
+ case dailyChanges = "daily.changes"
862
+ case sleepChanges = "sleep.changes"
863
+ case activityChanges = "activity.changes"
864
+ case bridgeTokenRequest = "bridge.tokenRequest"
865
+
866
+ /// Whether this platform publishes the stream.
867
+ public var isSupported: Bool {
868
+ switch self {
869
+ case .diagnosticsEvents: return true
870
+ case .bodyChanges: return true
871
+ case .dailyChanges: return true
872
+ case .sleepChanges: return true
873
+ case .activityChanges: return true
874
+ case .bridgeTokenRequest: return true
875
+ }
876
+ }
877
+ }
878
+
879
+ /// What P3 publishes. One method per stream this platform supports, each a fresh stream per call:
880
+ /// a subscriber that goes away cancels its own source and nobody else's.
881
+ public protocol TerraBridgeStreams: Sendable {
882
+ /// Every subscriber gets its own native stream, matching the SDK's fresh-stream-per-call
883
+ /// contract.
884
+ func diagnosticsEvents(_ params: Empty) async throws -> AsyncStream<TerraEvent>
885
+ func bodyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
886
+ func dailyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
887
+ func sleepChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
888
+ func activityChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
889
+ /// Bridge-owned. The broker's ask, not an SDK stream.
890
+ func bridgeTokenRequest(_ params: Empty) async throws -> AsyncStream<TokenRequest>
891
+ }
892
+
893
+ public struct BridgeStreamDispatch: Sendable {
894
+ private let streams: any TerraBridgeStreams
895
+ private let errorPayload: @Sendable (any Error) -> TerraErrorPayload
896
+
897
+ public init(streams: any TerraBridgeStreams, errorPayload: @escaping @Sendable (any Error) -> TerraErrorPayload) {
898
+ self.streams = streams
899
+ self.errorPayload = errorPayload
900
+ }
901
+
902
+ public func open(stream: String, params: Data) async throws -> AsyncStream<Data> {
903
+ guard let known = BridgeStream(rawValue: stream) else { throw BridgeError.unsupportedOnPlatform(op: stream) }
904
+ switch known {
905
+ case .diagnosticsEvents:
906
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
907
+ return await framed({ try await streams.diagnosticsEvents(request) }) { try TerraEventEnvelope.validate($0) }
908
+ case .bodyChanges:
909
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
910
+ return await framed({ try await streams.bodyChanges(request) })
911
+ case .dailyChanges:
912
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
913
+ return await framed({ try await streams.dailyChanges(request) })
914
+ case .sleepChanges:
915
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
916
+ return await framed({ try await streams.sleepChanges(request) })
917
+ case .activityChanges:
918
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
919
+ return await framed({ try await streams.activityChanges(request) })
920
+ case .bridgeTokenRequest:
921
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
922
+ return await framed({ try await streams.bridgeTokenRequest(request) })
923
+ }
924
+ }
925
+
926
+ private static func frame<Element: Encodable>(_ element: Element) throws -> Data {
927
+ let encoder = BridgeDispatch.makeEncoder()
928
+ let object = try BridgeDispatch.makeDecoder().decode(BridgeJSON.self, from: try encoder.encode(element))
929
+ return try encoder.encode(StreamMessage.data(element: object))
930
+ }
931
+
932
+ private func framed<Element: Encodable & Sendable>(
933
+ _ open: () async throws -> AsyncStream<Element>,
934
+ validate: @escaping @Sendable (Element) throws -> Void = { _ in }
935
+ ) async -> AsyncStream<Data> {
936
+ let errorPayload = self.errorPayload
937
+ let source: AsyncStream<Element>
938
+ do {
939
+ source = try await open()
940
+ } catch {
941
+ let failed = try? BridgeDispatch.makeEncoder().encode(StreamMessage.error(error: errorPayload(error)))
942
+ return AsyncStream { continuation in
943
+ if let failed { continuation.yield(failed) }
944
+ continuation.finish()
945
+ }
946
+ }
947
+ return AsyncStream { continuation in
948
+ let task = Task {
949
+ let encoder = BridgeDispatch.makeEncoder()
950
+ do {
951
+ for await element in source {
952
+ try validate(element)
953
+ continuation.yield(try Self.frame(element))
954
+ }
955
+ continuation.yield(try encoder.encode(StreamMessage.complete))
956
+ } catch {
957
+ if let failed = try? encoder.encode(StreamMessage.error(error: errorPayload(error))) {
958
+ continuation.yield(failed)
959
+ }
960
+ }
961
+ continuation.finish()
962
+ }
963
+ continuation.onTermination = { _ in task.cancel() }
964
+ }
965
+ }
966
+ }
967
+