@tryterra/react-native 0.0.0-stage → 2.1.0

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 (110) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +111 -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 +468 -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/Rejections.kt +150 -0
  11. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
  12. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
  13. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +258 -0
  14. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +52 -0
  15. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
  16. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
  17. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +493 -0
  18. package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +897 -0
  19. package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
  20. package/android/src/main/AndroidManifest.xml +24 -0
  21. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
  22. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
  23. package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
  24. package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
  25. package/docs/availability.md +246 -0
  26. package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
  27. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
  28. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +81513 -0
  29. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2150 -0
  30. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2150 -0
  31. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
  32. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2127 -0
  33. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  34. package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
  35. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  36. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  37. package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6473 -0
  38. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
  39. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +81513 -0
  40. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2150 -0
  41. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2150 -0
  42. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
  43. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2127 -0
  44. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +81513 -0
  45. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2150 -0
  46. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2150 -0
  47. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
  48. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2127 -0
  49. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
  50. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
  51. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
  52. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
  53. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
  54. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6474 -0
  55. package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6392 -0
  56. package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
  57. package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
  58. package/ios/TerraBridgeCore/Conversions.swift +467 -0
  59. package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
  60. package/ios/TerraBridgeCore/Generated/BridgeOps.swift +795 -0
  61. package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +1317 -0
  62. package/ios/TerraBridgeCore/Locked.swift +15 -0
  63. package/ios/TerraBridgeCore/README.md +1 -0
  64. package/ios/TerraBridgeCore/Rejections.swift +130 -0
  65. package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
  66. package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
  67. package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
  68. package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +254 -0
  69. package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
  70. package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
  71. package/ios/TerraBridgeModule.mm +79 -0
  72. package/ios/TerraBridgeModuleImpl.swift +49 -0
  73. package/ios/TerraReactNative.swift +19 -0
  74. package/lib/EndUser.d.ts +12 -0
  75. package/lib/EndUser.js +13 -0
  76. package/lib/NativeTerraBridge.d.ts +23 -0
  77. package/lib/NativeTerraBridge.js +13 -0
  78. package/lib/Terra.d.ts +154 -0
  79. package/lib/Terra.js +280 -0
  80. package/lib/TerraError.d.ts +42 -0
  81. package/lib/TerraError.js +95 -0
  82. package/lib/createTerra.d.ts +8 -0
  83. package/lib/createTerra.js +19 -0
  84. package/lib/generated/client.d.ts +92 -0
  85. package/lib/generated/client.js +196 -0
  86. package/lib/generated/codecs.d.ts +135 -0
  87. package/lib/generated/codecs.js +1358 -0
  88. package/lib/generated/errors.d.ts +13 -0
  89. package/lib/generated/errors.js +68 -0
  90. package/lib/generated/ops.d.ts +13 -0
  91. package/lib/generated/ops.js +137 -0
  92. package/lib/generated/streams.d.ts +23 -0
  93. package/lib/generated/streams.js +58 -0
  94. package/lib/generated/types.d.ts +481 -0
  95. package/lib/generated/types.js +92 -0
  96. package/lib/index.d.ts +29 -0
  97. package/lib/index.js +55 -0
  98. package/package.json +71 -4
  99. package/src/EndUser.ts +15 -0
  100. package/src/NativeTerraBridge.ts +27 -0
  101. package/src/Terra.ts +462 -0
  102. package/src/TerraError.ts +116 -0
  103. package/src/createTerra.ts +18 -0
  104. package/src/generated/client.ts +243 -0
  105. package/src/generated/codecs.ts +1287 -0
  106. package/src/generated/errors.ts +113 -0
  107. package/src/generated/ops.ts +197 -0
  108. package/src/generated/streams.ts +74 -0
  109. package/src/generated/types.ts +667 -0
  110. package/src/index.ts +52 -0
@@ -0,0 +1,795 @@
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 clientDisconnect = "client.disconnect"
16
+ case tokenSetProviderPresent = "token.setProviderPresent"
17
+ case tokenResolve = "token.resolve"
18
+ case tokenReject = "token.reject"
19
+ case permissionsRequest = "permissions.request"
20
+ case permissionsStatus = "permissions.status"
21
+ case permissionsWriteStatus = "permissions.writeStatus"
22
+ case permissionsRequested = "permissions.requested"
23
+ case permissionsStatuses = "permissions.statuses"
24
+ case permissionsBackgroundReadStatus = "permissions.backgroundReadStatus"
25
+ case permissionsHistoryReadStatus = "permissions.historyReadStatus"
26
+ case diagnosticsState = "diagnostics.state"
27
+ case diagnosticsLastDelivery = "diagnostics.lastDelivery"
28
+ case diagnosticsReport = "diagnostics.report"
29
+ case diagnosticsSend = "diagnostics.send"
30
+ case backgroundIsRegistered = "background.isRegistered"
31
+ case backgroundCancel = "background.cancel"
32
+ case bodyHeartRate = "body.heartRate"
33
+ case bodyLogHeartRate = "body.logHeartRate"
34
+ case bodyLog = "body.log"
35
+ case bodyDelete = "body.delete"
36
+ case dailySteps = "daily.steps"
37
+ case dailyLog = "daily.log"
38
+ case dailyDelete = "daily.delete"
39
+ case sleepSegments = "sleep.segments"
40
+ case sleepLog = "sleep.log"
41
+ case sleepDelete = "sleep.delete"
42
+ case activityWorkouts = "activity.workouts"
43
+ case activityLog = "activity.log"
44
+ case activityDelete = "activity.delete"
45
+ case nutritionLog = "nutrition.log"
46
+ case nutritionDelete = "nutrition.delete"
47
+ case plannedWorkoutsPlanned = "plannedWorkouts.planned"
48
+ case plannedWorkoutsMarkComplete = "plannedWorkouts.markComplete"
49
+ case plannedWorkoutsDelete = "plannedWorkouts.delete"
50
+ case plannedWorkoutsSchedulingStatus = "plannedWorkouts.schedulingStatus"
51
+ case plannedWorkoutsRequestScheduling = "plannedWorkouts.requestScheduling"
52
+
53
+ /// Whether this platform implements the op.
54
+ public var isSupported: Bool {
55
+ switch self {
56
+ case .clientAttach: return true
57
+ case .clientConnect: return true
58
+ case .clientConnections: return true
59
+ case .clientDisconnect: return true
60
+ case .tokenSetProviderPresent: return true
61
+ case .tokenResolve: return true
62
+ case .tokenReject: return true
63
+ case .permissionsRequest: return true
64
+ case .permissionsStatus: return true
65
+ case .permissionsWriteStatus: return true
66
+ case .permissionsRequested: return true
67
+ case .permissionsStatuses: return false
68
+ case .permissionsBackgroundReadStatus: return false
69
+ case .permissionsHistoryReadStatus: return false
70
+ case .diagnosticsState: return true
71
+ case .diagnosticsLastDelivery: return true
72
+ case .diagnosticsReport: return true
73
+ case .diagnosticsSend: return true
74
+ case .backgroundIsRegistered: return true
75
+ case .backgroundCancel: return false
76
+ case .bodyHeartRate: return true
77
+ case .bodyLogHeartRate: return true
78
+ case .bodyLog: return true
79
+ case .bodyDelete: return true
80
+ case .dailySteps: return true
81
+ case .dailyLog: return true
82
+ case .dailyDelete: return true
83
+ case .sleepSegments: return true
84
+ case .sleepLog: return true
85
+ case .sleepDelete: return true
86
+ case .activityWorkouts: return true
87
+ case .activityLog: return true
88
+ case .activityDelete: return true
89
+ case .nutritionLog: return true
90
+ case .nutritionDelete: return true
91
+ case .plannedWorkoutsPlanned: return true
92
+ case .plannedWorkoutsMarkComplete: return true
93
+ case .plannedWorkoutsDelete: return true
94
+ case .plannedWorkoutsSchedulingStatus: return true
95
+ case .plannedWorkoutsRequestScheduling: return true
96
+ }
97
+ }
98
+ }
99
+
100
+ /// What P3 implements. One method per op this platform supports, and no `default` anywhere:
101
+ /// an op added to the schema breaks this build rather than reaching a customer as a rejection.
102
+ public protocol TerraBridgeOps: Sendable {
103
+ /// Reports the native client the JS side is attaching to. Idempotent, and safe to call after a
104
+ /// JS reload.
105
+ func clientAttach(_ request: Empty) async throws -> BootstrapState
106
+ func clientConnect(_ request: ConnectRequest) async throws -> Connection
107
+ func clientConnections(_ request: Empty) async throws -> ConnectionList
108
+ func clientDisconnect(_ request: SourceRequest) async throws -> Empty
109
+ /// Tells the broker whether JS can answer at all. Without it a link asks a provider that is not
110
+ /// there and waits out the timeout.
111
+ func tokenSetProviderPresent(_ request: TokenProviderRequest) async throws -> Empty
112
+ /// An unknown or already-settled requestId is ignored rather than rejected, so a reload cannot
113
+ /// fail a link that has already moved on.
114
+ func tokenResolve(_ request: TokenResolveRequest) async throws -> Empty
115
+ func tokenReject(_ request: TokenRejectRequest) async throws -> Empty
116
+ func permissionsRequest(_ request: PermissionRequest) async throws -> PermissionGrant
117
+ func permissionsStatus(_ request: CategoryRequest) async throws -> StatusResult
118
+ func permissionsWriteStatus(_ request: CategoryRequest) async throws -> StatusResult
119
+ func permissionsRequested(_ request: SourceRequest) async throws -> HealthAccess
120
+ func diagnosticsState(_ request: SourceRequest) async throws -> DiagnosticsState
121
+ func diagnosticsLastDelivery(_ request: SourceRequest) async throws -> LastDeliveryResult
122
+ func diagnosticsReport(_ request: SourceRequest) async throws -> SupportReport
123
+ func diagnosticsSend(_ request: SourceRequest) async throws -> Empty
124
+ /// False means bootstrap did not run. The SDK reports the same thing independently as
125
+ /// backgroundRefreshUnscheduled.
126
+ func backgroundIsRegistered(_ request: Empty) async throws -> BoolResult
127
+ func bodyHeartRate(_ request: RangeRequest) async throws -> HeartRateList
128
+ /// Its own op rather than an overload: Swift overloads log by element type and JS cannot.
129
+ func bodyLogHeartRate(_ request: HeartRateEntryList) async throws -> RecordIdList
130
+ func bodyLog(_ request: BodyEntryList) async throws -> RecordIdList
131
+ func bodyDelete(_ request: RecordIdRequest) async throws -> Empty
132
+ func dailySteps(_ request: RangeRequest) async throws -> StepCountList
133
+ func dailyLog(_ request: StepEntryList) async throws -> RecordIdList
134
+ func dailyDelete(_ request: RecordIdRequest) async throws -> Empty
135
+ func sleepSegments(_ request: RangeRequest) async throws -> SleepSegmentList
136
+ func sleepLog(_ request: SleepEntryList) async throws -> RecordIdList
137
+ func sleepDelete(_ request: RecordIdRequest) async throws -> Empty
138
+ func activityWorkouts(_ request: RangeRequest) async throws -> WorkoutList
139
+ func activityLog(_ request: WorkoutEntryList) async throws -> RecordIdList
140
+ func activityDelete(_ request: RecordIdRequest) async throws -> Empty
141
+ func nutritionLog(_ request: NutritionEntryList) async throws -> RecordIdList
142
+ func nutritionDelete(_ request: RecordIdRequest) async throws -> Empty
143
+ /// Available on ios only.
144
+ func plannedWorkoutsPlanned(_ request: Empty) async throws -> PlannedWorkoutList
145
+ /// Available on ios only.
146
+ func plannedWorkoutsMarkComplete(_ request: MarkCompleteRequest) async throws -> Empty
147
+ /// Available on ios only.
148
+ func plannedWorkoutsDelete(_ request: RecordIdRequest) async throws -> Empty
149
+ /// Never prompts.
150
+ ///
151
+ /// Available on ios only.
152
+ func plannedWorkoutsSchedulingStatus(_ request: Empty) async throws -> WorkoutScheduling
153
+ /// Shows WorkoutKit's sheet if the person has not been asked, and reconciles straight away once
154
+ /// authorized.
155
+ ///
156
+ /// Available on ios only.
157
+ func plannedWorkoutsRequestScheduling(_ request: Empty) async throws -> WorkoutScheduling
158
+ }
159
+
160
+ /// Decodes one side of an exchange and re-encodes it, for the bridge-vector runner.
161
+ ///
162
+ /// The runner cannot name the request and response types itself without keeping a second copy
163
+ /// of the op table, and a second copy of the op table is the thing this schema exists to
164
+ /// remove. A round trip is also exactly what a vector asserts: the bytes in are the bytes out,
165
+ /// which pins the DTOs and their coders against the same file the other two runners read.
166
+ public enum BridgeCodecs {
167
+ public static func roundTripRequest(_ op: BridgeOp, _ payload: Data) throws -> Data {
168
+ let decoder = BridgeDispatch.makeDecoder()
169
+ let encoder = BridgeDispatch.makeEncoder()
170
+ switch op {
171
+ case .clientAttach:
172
+ let value = try decoder.decode(Empty.self, from: payload)
173
+ return try encoder.encode(value)
174
+ case .clientConnect:
175
+ let value = try decoder.decode(ConnectRequest.self, from: payload)
176
+ return try encoder.encode(value)
177
+ case .clientConnections:
178
+ let value = try decoder.decode(Empty.self, from: payload)
179
+ return try encoder.encode(value)
180
+ case .clientDisconnect:
181
+ let value = try decoder.decode(SourceRequest.self, from: payload)
182
+ return try encoder.encode(value)
183
+ case .tokenSetProviderPresent:
184
+ let value = try decoder.decode(TokenProviderRequest.self, from: payload)
185
+ return try encoder.encode(value)
186
+ case .tokenResolve:
187
+ let value = try decoder.decode(TokenResolveRequest.self, from: payload)
188
+ return try encoder.encode(value)
189
+ case .tokenReject:
190
+ let value = try decoder.decode(TokenRejectRequest.self, from: payload)
191
+ return try encoder.encode(value)
192
+ case .permissionsRequest:
193
+ let value = try decoder.decode(PermissionRequest.self, from: payload)
194
+ return try encoder.encode(value)
195
+ case .permissionsStatus:
196
+ let value = try decoder.decode(CategoryRequest.self, from: payload)
197
+ return try encoder.encode(value)
198
+ case .permissionsWriteStatus:
199
+ let value = try decoder.decode(CategoryRequest.self, from: payload)
200
+ return try encoder.encode(value)
201
+ case .permissionsRequested:
202
+ let value = try decoder.decode(SourceRequest.self, from: payload)
203
+ return try encoder.encode(value)
204
+ case .permissionsStatuses:
205
+ let value = try decoder.decode(SourceRequest.self, from: payload)
206
+ return try encoder.encode(value)
207
+ case .permissionsBackgroundReadStatus:
208
+ let value = try decoder.decode(SourceRequest.self, from: payload)
209
+ return try encoder.encode(value)
210
+ case .permissionsHistoryReadStatus:
211
+ let value = try decoder.decode(SourceRequest.self, from: payload)
212
+ return try encoder.encode(value)
213
+ case .diagnosticsState:
214
+ let value = try decoder.decode(SourceRequest.self, from: payload)
215
+ return try encoder.encode(value)
216
+ case .diagnosticsLastDelivery:
217
+ let value = try decoder.decode(SourceRequest.self, from: payload)
218
+ return try encoder.encode(value)
219
+ case .diagnosticsReport:
220
+ let value = try decoder.decode(SourceRequest.self, from: payload)
221
+ return try encoder.encode(value)
222
+ case .diagnosticsSend:
223
+ let value = try decoder.decode(SourceRequest.self, from: payload)
224
+ return try encoder.encode(value)
225
+ case .backgroundIsRegistered:
226
+ let value = try decoder.decode(Empty.self, from: payload)
227
+ return try encoder.encode(value)
228
+ case .backgroundCancel:
229
+ let value = try decoder.decode(Empty.self, from: payload)
230
+ return try encoder.encode(value)
231
+ case .bodyHeartRate:
232
+ let value = try decoder.decode(RangeRequest.self, from: payload)
233
+ return try encoder.encode(value)
234
+ case .bodyLogHeartRate:
235
+ let value = try decoder.decode(HeartRateEntryList.self, from: payload)
236
+ return try encoder.encode(value)
237
+ case .bodyLog:
238
+ let value = try decoder.decode(BodyEntryList.self, from: payload)
239
+ return try encoder.encode(value)
240
+ case .bodyDelete:
241
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
242
+ return try encoder.encode(value)
243
+ case .dailySteps:
244
+ let value = try decoder.decode(RangeRequest.self, from: payload)
245
+ return try encoder.encode(value)
246
+ case .dailyLog:
247
+ let value = try decoder.decode(StepEntryList.self, from: payload)
248
+ return try encoder.encode(value)
249
+ case .dailyDelete:
250
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
251
+ return try encoder.encode(value)
252
+ case .sleepSegments:
253
+ let value = try decoder.decode(RangeRequest.self, from: payload)
254
+ return try encoder.encode(value)
255
+ case .sleepLog:
256
+ let value = try decoder.decode(SleepEntryList.self, from: payload)
257
+ return try encoder.encode(value)
258
+ case .sleepDelete:
259
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
260
+ return try encoder.encode(value)
261
+ case .activityWorkouts:
262
+ let value = try decoder.decode(RangeRequest.self, from: payload)
263
+ return try encoder.encode(value)
264
+ case .activityLog:
265
+ let value = try decoder.decode(WorkoutEntryList.self, from: payload)
266
+ return try encoder.encode(value)
267
+ case .activityDelete:
268
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
269
+ return try encoder.encode(value)
270
+ case .nutritionLog:
271
+ let value = try decoder.decode(NutritionEntryList.self, from: payload)
272
+ return try encoder.encode(value)
273
+ case .nutritionDelete:
274
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
275
+ return try encoder.encode(value)
276
+ case .plannedWorkoutsPlanned:
277
+ let value = try decoder.decode(Empty.self, from: payload)
278
+ return try encoder.encode(value)
279
+ case .plannedWorkoutsMarkComplete:
280
+ let value = try decoder.decode(MarkCompleteRequest.self, from: payload)
281
+ return try encoder.encode(value)
282
+ case .plannedWorkoutsDelete:
283
+ let value = try decoder.decode(RecordIdRequest.self, from: payload)
284
+ return try encoder.encode(value)
285
+ case .plannedWorkoutsSchedulingStatus:
286
+ let value = try decoder.decode(Empty.self, from: payload)
287
+ return try encoder.encode(value)
288
+ case .plannedWorkoutsRequestScheduling:
289
+ let value = try decoder.decode(Empty.self, from: payload)
290
+ return try encoder.encode(value)
291
+ }
292
+ }
293
+
294
+ public static func roundTripResponse(_ op: BridgeOp, _ payload: Data) throws -> Data {
295
+ let decoder = BridgeDispatch.makeDecoder()
296
+ let encoder = BridgeDispatch.makeEncoder()
297
+ switch op {
298
+ case .clientAttach:
299
+ let value = try decoder.decode(BootstrapState.self, from: payload)
300
+ return try encoder.encode(value)
301
+ case .clientConnect:
302
+ let value = try decoder.decode(Connection.self, from: payload)
303
+ return try encoder.encode(value)
304
+ case .clientConnections:
305
+ let value = try decoder.decode(ConnectionList.self, from: payload)
306
+ return try encoder.encode(value)
307
+ case .clientDisconnect:
308
+ let value = try decoder.decode(Empty.self, from: payload)
309
+ return try encoder.encode(value)
310
+ case .tokenSetProviderPresent:
311
+ let value = try decoder.decode(Empty.self, from: payload)
312
+ return try encoder.encode(value)
313
+ case .tokenResolve:
314
+ let value = try decoder.decode(Empty.self, from: payload)
315
+ return try encoder.encode(value)
316
+ case .tokenReject:
317
+ let value = try decoder.decode(Empty.self, from: payload)
318
+ return try encoder.encode(value)
319
+ case .permissionsRequest:
320
+ let value = try decoder.decode(PermissionGrant.self, from: payload)
321
+ return try encoder.encode(value)
322
+ case .permissionsStatus:
323
+ let value = try decoder.decode(StatusResult.self, from: payload)
324
+ return try encoder.encode(value)
325
+ case .permissionsWriteStatus:
326
+ let value = try decoder.decode(StatusResult.self, from: payload)
327
+ return try encoder.encode(value)
328
+ case .permissionsRequested:
329
+ let value = try decoder.decode(HealthAccess.self, from: payload)
330
+ return try encoder.encode(value)
331
+ case .permissionsStatuses:
332
+ let value = try decoder.decode(StatusesResult.self, from: payload)
333
+ return try encoder.encode(value)
334
+ case .permissionsBackgroundReadStatus:
335
+ let value = try decoder.decode(StatusResult.self, from: payload)
336
+ return try encoder.encode(value)
337
+ case .permissionsHistoryReadStatus:
338
+ let value = try decoder.decode(StatusResult.self, from: payload)
339
+ return try encoder.encode(value)
340
+ case .diagnosticsState:
341
+ let value = try decoder.decode(DiagnosticsState.self, from: payload)
342
+ return try encoder.encode(value)
343
+ case .diagnosticsLastDelivery:
344
+ let value = try decoder.decode(LastDeliveryResult.self, from: payload)
345
+ return try encoder.encode(value)
346
+ case .diagnosticsReport:
347
+ let value = try decoder.decode(SupportReport.self, from: payload)
348
+ try SupportReportEnvelope.validate(value)
349
+ return try encoder.encode(value)
350
+ case .diagnosticsSend:
351
+ let value = try decoder.decode(Empty.self, from: payload)
352
+ return try encoder.encode(value)
353
+ case .backgroundIsRegistered:
354
+ let value = try decoder.decode(BoolResult.self, from: payload)
355
+ return try encoder.encode(value)
356
+ case .backgroundCancel:
357
+ let value = try decoder.decode(BoolResult.self, from: payload)
358
+ return try encoder.encode(value)
359
+ case .bodyHeartRate:
360
+ let value = try decoder.decode(HeartRateList.self, from: payload)
361
+ return try encoder.encode(value)
362
+ case .bodyLogHeartRate:
363
+ let value = try decoder.decode(RecordIdList.self, from: payload)
364
+ return try encoder.encode(value)
365
+ case .bodyLog:
366
+ let value = try decoder.decode(RecordIdList.self, from: payload)
367
+ return try encoder.encode(value)
368
+ case .bodyDelete:
369
+ let value = try decoder.decode(Empty.self, from: payload)
370
+ return try encoder.encode(value)
371
+ case .dailySteps:
372
+ let value = try decoder.decode(StepCountList.self, from: payload)
373
+ return try encoder.encode(value)
374
+ case .dailyLog:
375
+ let value = try decoder.decode(RecordIdList.self, from: payload)
376
+ return try encoder.encode(value)
377
+ case .dailyDelete:
378
+ let value = try decoder.decode(Empty.self, from: payload)
379
+ return try encoder.encode(value)
380
+ case .sleepSegments:
381
+ let value = try decoder.decode(SleepSegmentList.self, from: payload)
382
+ return try encoder.encode(value)
383
+ case .sleepLog:
384
+ let value = try decoder.decode(RecordIdList.self, from: payload)
385
+ return try encoder.encode(value)
386
+ case .sleepDelete:
387
+ let value = try decoder.decode(Empty.self, from: payload)
388
+ return try encoder.encode(value)
389
+ case .activityWorkouts:
390
+ let value = try decoder.decode(WorkoutList.self, from: payload)
391
+ return try encoder.encode(value)
392
+ case .activityLog:
393
+ let value = try decoder.decode(RecordIdList.self, from: payload)
394
+ return try encoder.encode(value)
395
+ case .activityDelete:
396
+ let value = try decoder.decode(Empty.self, from: payload)
397
+ return try encoder.encode(value)
398
+ case .nutritionLog:
399
+ let value = try decoder.decode(RecordIdList.self, from: payload)
400
+ return try encoder.encode(value)
401
+ case .nutritionDelete:
402
+ let value = try decoder.decode(Empty.self, from: payload)
403
+ return try encoder.encode(value)
404
+ case .plannedWorkoutsPlanned:
405
+ let value = try decoder.decode(PlannedWorkoutList.self, from: payload)
406
+ return try encoder.encode(value)
407
+ case .plannedWorkoutsMarkComplete:
408
+ let value = try decoder.decode(Empty.self, from: payload)
409
+ return try encoder.encode(value)
410
+ case .plannedWorkoutsDelete:
411
+ let value = try decoder.decode(Empty.self, from: payload)
412
+ return try encoder.encode(value)
413
+ case .plannedWorkoutsSchedulingStatus:
414
+ let value = try decoder.decode(WorkoutScheduling.self, from: payload)
415
+ return try encoder.encode(value)
416
+ case .plannedWorkoutsRequestScheduling:
417
+ let value = try decoder.decode(WorkoutScheduling.self, from: payload)
418
+ return try encoder.encode(value)
419
+ }
420
+ }
421
+
422
+ /// One message as a subscription actually delivers it: the envelope, and the element inside.
423
+ ///
424
+ /// Both halves, because checking only the envelope would accept a `data` carrying anything
425
+ /// at all, and checking only the element would accept a stream that never says it failed.
426
+ public static func roundTripStreamMessage(_ stream: String, _ payload: Data) throws -> Data {
427
+ let decoder = BridgeDispatch.makeDecoder()
428
+ let encoder = BridgeDispatch.makeEncoder()
429
+ let message = try decoder.decode(StreamMessage.self, from: payload)
430
+ guard case .data(let element) = message else { return try encoder.encode(message) }
431
+ let inner = try roundTripStreamElement(stream, try encoder.encode(element))
432
+ let checked = try decoder.decode(BridgeJSON.self, from: inner)
433
+ return try encoder.encode(StreamMessage.data(element: checked))
434
+ }
435
+
436
+ /// One element of a stream, by that stream's declared type.
437
+ public static func roundTripStreamElement(_ stream: String, _ payload: Data) throws -> Data {
438
+ let decoder = BridgeDispatch.makeDecoder()
439
+ let encoder = BridgeDispatch.makeEncoder()
440
+ switch stream {
441
+ case "diagnostics.events":
442
+ let value = try decoder.decode(TerraEvent.self, from: payload)
443
+ try TerraEventEnvelope.validate(value)
444
+ return try encoder.encode(value)
445
+ case "body.changes":
446
+ let value = try decoder.decode(HealthChange.self, from: payload)
447
+ return try encoder.encode(value)
448
+ case "daily.changes":
449
+ let value = try decoder.decode(HealthChange.self, from: payload)
450
+ return try encoder.encode(value)
451
+ case "sleep.changes":
452
+ let value = try decoder.decode(HealthChange.self, from: payload)
453
+ return try encoder.encode(value)
454
+ case "activity.changes":
455
+ let value = try decoder.decode(HealthChange.self, from: payload)
456
+ return try encoder.encode(value)
457
+ case "bridge.tokenRequest":
458
+ let value = try decoder.decode(TokenRequest.self, from: payload)
459
+ return try encoder.encode(value)
460
+ default:
461
+ throw BridgeError.unsupportedOnPlatform(op: stream)
462
+ }
463
+ }
464
+ }
465
+
466
+ /// Turns an op string and a JSON payload into a typed call and a JSON answer.
467
+ ///
468
+ /// The encoder and decoder are built once and configured for ISO 8601: an instant spelled two
469
+ /// ways between a request and its response is a bug the vectors would catch and the compiler
470
+ /// would not.
471
+ public struct BridgeDispatch: Sendable {
472
+ private let ops: any TerraBridgeOps
473
+
474
+ public init(ops: any TerraBridgeOps) { self.ops = ops }
475
+
476
+ public static func makeDecoder() -> JSONDecoder { JSONDecoder() }
477
+
478
+ /// Sorted keys, so that two encodings of one value are the same bytes. That is what lets a
479
+ /// vector be compared rather than parsed and walked, and what makes the three runners
480
+ /// compare like for like.
481
+ public static func makeEncoder() -> JSONEncoder {
482
+ let encoder = JSONEncoder()
483
+ encoder.outputFormatting = [.sortedKeys]
484
+ return encoder
485
+ }
486
+
487
+ public static func decodeRequest<Request: Decodable>(
488
+ _ type: Request.Type,
489
+ from payload: Data,
490
+ validate: (Request) throws -> Void = { _ in }
491
+ ) throws -> Request {
492
+ do {
493
+ let request = try makeDecoder().decode(Request.self, from: payload)
494
+ try validate(request)
495
+ return request
496
+ } catch let error as DecodingError {
497
+ throw BridgeError.invalidPayload(path: path(of: error))
498
+ }
499
+ }
500
+
501
+ static func path(of error: DecodingError) -> String {
502
+ let keys: [any CodingKey]
503
+ switch error {
504
+ case .typeMismatch(_, let context), .valueNotFound(_, let context), .dataCorrupted(let context):
505
+ keys = context.codingPath
506
+ case .keyNotFound(let key, let context):
507
+ keys = context.codingPath + [key]
508
+ @unknown default:
509
+ keys = []
510
+ }
511
+ return keys.reduce("$") { path, key in
512
+ if let index = key.intValue { return "\(path)[\(index)]" }
513
+ return "\(path).\(key.stringValue)"
514
+ }
515
+ }
516
+
517
+ public func callAsFunction(op: String, payload: Data) async throws -> Data {
518
+ // An op string no version of the schema declared: a wrapper and a native half that
519
+ // disagree. Reported as the same code an op this platform lacks gets, because from the
520
+ // caller's side the two are indistinguishable and both mean "not available here".
521
+ guard let known = BridgeOp(rawValue: op) else { throw BridgeError.unsupportedOnPlatform(op: op) }
522
+ return try await dispatch(known, payload)
523
+ }
524
+
525
+ private func dispatch(_ op: BridgeOp, _ payload: Data) async throws -> Data {
526
+ let encoder = Self.makeEncoder()
527
+ switch op {
528
+ case .clientAttach:
529
+ let request = try Self.decodeRequest(Empty.self, from: payload)
530
+ let response = try await ops.clientAttach(request)
531
+ return try encoder.encode(response)
532
+ case .clientConnect:
533
+ let request = try Self.decodeRequest(ConnectRequest.self, from: payload)
534
+ let response = try await ops.clientConnect(request)
535
+ return try encoder.encode(response)
536
+ case .clientConnections:
537
+ let request = try Self.decodeRequest(Empty.self, from: payload)
538
+ let response = try await ops.clientConnections(request)
539
+ return try encoder.encode(response)
540
+ case .clientDisconnect:
541
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
542
+ let response = try await ops.clientDisconnect(request)
543
+ return try encoder.encode(response)
544
+ case .tokenSetProviderPresent:
545
+ let request = try Self.decodeRequest(TokenProviderRequest.self, from: payload)
546
+ let response = try await ops.tokenSetProviderPresent(request)
547
+ return try encoder.encode(response)
548
+ case .tokenResolve:
549
+ let request = try Self.decodeRequest(TokenResolveRequest.self, from: payload)
550
+ let response = try await ops.tokenResolve(request)
551
+ return try encoder.encode(response)
552
+ case .tokenReject:
553
+ let request = try Self.decodeRequest(TokenRejectRequest.self, from: payload)
554
+ let response = try await ops.tokenReject(request)
555
+ return try encoder.encode(response)
556
+ case .permissionsRequest:
557
+ let request = try Self.decodeRequest(PermissionRequest.self, from: payload)
558
+ let response = try await ops.permissionsRequest(request)
559
+ return try encoder.encode(response)
560
+ case .permissionsStatus:
561
+ let request = try Self.decodeRequest(CategoryRequest.self, from: payload)
562
+ let response = try await ops.permissionsStatus(request)
563
+ return try encoder.encode(response)
564
+ case .permissionsWriteStatus:
565
+ let request = try Self.decodeRequest(CategoryRequest.self, from: payload)
566
+ let response = try await ops.permissionsWriteStatus(request)
567
+ return try encoder.encode(response)
568
+ case .permissionsRequested:
569
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
570
+ let response = try await ops.permissionsRequested(request)
571
+ return try encoder.encode(response)
572
+ case .permissionsStatuses:
573
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
574
+ case .permissionsBackgroundReadStatus:
575
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
576
+ case .permissionsHistoryReadStatus:
577
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
578
+ case .diagnosticsState:
579
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
580
+ let response = try await ops.diagnosticsState(request)
581
+ return try encoder.encode(response)
582
+ case .diagnosticsLastDelivery:
583
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
584
+ let response = try await ops.diagnosticsLastDelivery(request)
585
+ return try encoder.encode(response)
586
+ case .diagnosticsReport:
587
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
588
+ let response = try await ops.diagnosticsReport(request)
589
+ try SupportReportEnvelope.validate(response)
590
+ return try encoder.encode(response)
591
+ case .diagnosticsSend:
592
+ let request = try Self.decodeRequest(SourceRequest.self, from: payload)
593
+ let response = try await ops.diagnosticsSend(request)
594
+ return try encoder.encode(response)
595
+ case .backgroundIsRegistered:
596
+ let request = try Self.decodeRequest(Empty.self, from: payload)
597
+ let response = try await ops.backgroundIsRegistered(request)
598
+ return try encoder.encode(response)
599
+ case .backgroundCancel:
600
+ throw BridgeError.unsupportedOnPlatform(op: op.rawValue)
601
+ case .bodyHeartRate:
602
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
603
+ let response = try await ops.bodyHeartRate(request)
604
+ return try encoder.encode(response)
605
+ case .bodyLogHeartRate:
606
+ let request = try Self.decodeRequest(HeartRateEntryList.self, from: payload)
607
+ let response = try await ops.bodyLogHeartRate(request)
608
+ return try encoder.encode(response)
609
+ case .bodyLog:
610
+ let request = try Self.decodeRequest(BodyEntryList.self, from: payload)
611
+ let response = try await ops.bodyLog(request)
612
+ return try encoder.encode(response)
613
+ case .bodyDelete:
614
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
615
+ let response = try await ops.bodyDelete(request)
616
+ return try encoder.encode(response)
617
+ case .dailySteps:
618
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
619
+ let response = try await ops.dailySteps(request)
620
+ return try encoder.encode(response)
621
+ case .dailyLog:
622
+ let request = try Self.decodeRequest(StepEntryList.self, from: payload)
623
+ let response = try await ops.dailyLog(request)
624
+ return try encoder.encode(response)
625
+ case .dailyDelete:
626
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
627
+ let response = try await ops.dailyDelete(request)
628
+ return try encoder.encode(response)
629
+ case .sleepSegments:
630
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
631
+ let response = try await ops.sleepSegments(request)
632
+ return try encoder.encode(response)
633
+ case .sleepLog:
634
+ let request = try Self.decodeRequest(SleepEntryList.self, from: payload)
635
+ let response = try await ops.sleepLog(request)
636
+ return try encoder.encode(response)
637
+ case .sleepDelete:
638
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
639
+ let response = try await ops.sleepDelete(request)
640
+ return try encoder.encode(response)
641
+ case .activityWorkouts:
642
+ let request = try Self.decodeRequest(RangeRequest.self, from: payload)
643
+ let response = try await ops.activityWorkouts(request)
644
+ return try encoder.encode(response)
645
+ case .activityLog:
646
+ let request = try Self.decodeRequest(WorkoutEntryList.self, from: payload)
647
+ let response = try await ops.activityLog(request)
648
+ return try encoder.encode(response)
649
+ case .activityDelete:
650
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
651
+ let response = try await ops.activityDelete(request)
652
+ return try encoder.encode(response)
653
+ case .nutritionLog:
654
+ let request = try Self.decodeRequest(NutritionEntryList.self, from: payload)
655
+ let response = try await ops.nutritionLog(request)
656
+ return try encoder.encode(response)
657
+ case .nutritionDelete:
658
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
659
+ let response = try await ops.nutritionDelete(request)
660
+ return try encoder.encode(response)
661
+ case .plannedWorkoutsPlanned:
662
+ let request = try Self.decodeRequest(Empty.self, from: payload)
663
+ let response = try await ops.plannedWorkoutsPlanned(request)
664
+ return try encoder.encode(response)
665
+ case .plannedWorkoutsMarkComplete:
666
+ let request = try Self.decodeRequest(MarkCompleteRequest.self, from: payload)
667
+ let response = try await ops.plannedWorkoutsMarkComplete(request)
668
+ return try encoder.encode(response)
669
+ case .plannedWorkoutsDelete:
670
+ let request = try Self.decodeRequest(RecordIdRequest.self, from: payload)
671
+ let response = try await ops.plannedWorkoutsDelete(request)
672
+ return try encoder.encode(response)
673
+ case .plannedWorkoutsSchedulingStatus:
674
+ let request = try Self.decodeRequest(Empty.self, from: payload)
675
+ let response = try await ops.plannedWorkoutsSchedulingStatus(request)
676
+ return try encoder.encode(response)
677
+ case .plannedWorkoutsRequestScheduling:
678
+ let request = try Self.decodeRequest(Empty.self, from: payload)
679
+ let response = try await ops.plannedWorkoutsRequestScheduling(request)
680
+ return try encoder.encode(response)
681
+ }
682
+ }
683
+ }
684
+
685
+ /// Every stream the schema declares, including the ones this platform does not publish.
686
+ public enum BridgeStream: String, CaseIterable, Sendable {
687
+ case diagnosticsEvents = "diagnostics.events"
688
+ case bodyChanges = "body.changes"
689
+ case dailyChanges = "daily.changes"
690
+ case sleepChanges = "sleep.changes"
691
+ case activityChanges = "activity.changes"
692
+ case bridgeTokenRequest = "bridge.tokenRequest"
693
+
694
+ /// Whether this platform publishes the stream.
695
+ public var isSupported: Bool {
696
+ switch self {
697
+ case .diagnosticsEvents: return true
698
+ case .bodyChanges: return true
699
+ case .dailyChanges: return true
700
+ case .sleepChanges: return true
701
+ case .activityChanges: return true
702
+ case .bridgeTokenRequest: return true
703
+ }
704
+ }
705
+ }
706
+
707
+ /// What P3 publishes. One method per stream this platform supports, each a fresh stream per call:
708
+ /// a subscriber that goes away cancels its own source and nobody else's.
709
+ public protocol TerraBridgeStreams: Sendable {
710
+ /// Every subscriber gets its own native stream, matching the SDK's fresh-stream-per-call
711
+ /// contract.
712
+ func diagnosticsEvents(_ params: Empty) async throws -> AsyncStream<TerraEvent>
713
+ func bodyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
714
+ func dailyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
715
+ func sleepChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
716
+ func activityChanges(_ params: Empty) async throws -> AsyncStream<HealthChange>
717
+ /// Bridge-owned. The broker's ask, not an SDK stream.
718
+ func bridgeTokenRequest(_ params: Empty) async throws -> AsyncStream<TokenRequest>
719
+ }
720
+
721
+ public struct BridgeStreamDispatch: Sendable {
722
+ private let streams: any TerraBridgeStreams
723
+ private let errorPayload: @Sendable (any Error) -> TerraErrorPayload
724
+
725
+ public init(streams: any TerraBridgeStreams, errorPayload: @escaping @Sendable (any Error) -> TerraErrorPayload) {
726
+ self.streams = streams
727
+ self.errorPayload = errorPayload
728
+ }
729
+
730
+ public func open(stream: String, params: Data) async throws -> AsyncStream<Data> {
731
+ guard let known = BridgeStream(rawValue: stream) else { throw BridgeError.unsupportedOnPlatform(op: stream) }
732
+ switch known {
733
+ case .diagnosticsEvents:
734
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
735
+ return await framed({ try await streams.diagnosticsEvents(request) }) { try TerraEventEnvelope.validate($0) }
736
+ case .bodyChanges:
737
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
738
+ return await framed({ try await streams.bodyChanges(request) })
739
+ case .dailyChanges:
740
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
741
+ return await framed({ try await streams.dailyChanges(request) })
742
+ case .sleepChanges:
743
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
744
+ return await framed({ try await streams.sleepChanges(request) })
745
+ case .activityChanges:
746
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
747
+ return await framed({ try await streams.activityChanges(request) })
748
+ case .bridgeTokenRequest:
749
+ let request = try BridgeDispatch.decodeRequest(Empty.self, from: params)
750
+ return await framed({ try await streams.bridgeTokenRequest(request) })
751
+ }
752
+ }
753
+
754
+ private static func frame<Element: Encodable>(_ element: Element) throws -> Data {
755
+ let encoder = BridgeDispatch.makeEncoder()
756
+ let object = try BridgeDispatch.makeDecoder().decode(BridgeJSON.self, from: try encoder.encode(element))
757
+ return try encoder.encode(StreamMessage.data(element: object))
758
+ }
759
+
760
+ private func framed<Element: Encodable & Sendable>(
761
+ _ open: () async throws -> AsyncStream<Element>,
762
+ validate: @escaping @Sendable (Element) throws -> Void = { _ in }
763
+ ) async -> AsyncStream<Data> {
764
+ let errorPayload = self.errorPayload
765
+ let source: AsyncStream<Element>
766
+ do {
767
+ source = try await open()
768
+ } catch {
769
+ let failed = try? BridgeDispatch.makeEncoder().encode(StreamMessage.error(error: errorPayload(error)))
770
+ return AsyncStream { continuation in
771
+ if let failed { continuation.yield(failed) }
772
+ continuation.finish()
773
+ }
774
+ }
775
+ return AsyncStream { continuation in
776
+ let task = Task {
777
+ let encoder = BridgeDispatch.makeEncoder()
778
+ do {
779
+ for await element in source {
780
+ try validate(element)
781
+ continuation.yield(try Self.frame(element))
782
+ }
783
+ continuation.yield(try encoder.encode(StreamMessage.complete))
784
+ } catch {
785
+ if let failed = try? encoder.encode(StreamMessage.error(error: errorPayload(error))) {
786
+ continuation.yield(failed)
787
+ }
788
+ }
789
+ continuation.finish()
790
+ }
791
+ continuation.onTermination = { _ in task.cancel() }
792
+ }
793
+ }
794
+ }
795
+