@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,318 @@
1
+ import Foundation
2
+ @_spi(Bridge) import TerraSDK
3
+
4
+ /// The hand-written half: one short function per op.
5
+ ///
6
+ /// It cannot be incomplete, because `TerraBridgeOps` is generated from the schema and an op added
7
+ /// there stops this compiling. Conversions live in `Conversions`; what is left is the shape of each
8
+ /// call, and the one place Apple's surface differs from the wire's: `sync`, `disconnect` and plans
9
+ /// hang off a `Connection` here rather than off the client.
10
+ struct TerraBridgeOpsImpl: TerraBridgeOps {
11
+ let terra: Terra
12
+ let host: any BridgeHost
13
+ let broker: TokenBroker
14
+ let audience: TokenAudience
15
+
16
+ // MARK: client
17
+
18
+ func clientAttach(_ request: Empty) async throws -> BootstrapState {
19
+ BootstrapState(
20
+ bootstrapped: true,
21
+ devId: host.devID,
22
+ platform: "ios",
23
+ sdkVersion: TerraSDK.sdkVersion,
24
+ backgroundRegistered: await host.backgroundRegistered())
25
+ }
26
+
27
+ func clientConnect(_ request: ConnectRequest) async throws -> Connection {
28
+ let source = try Conversions.source(request.source)
29
+ let endUser = Conversions.endUser(request.endUser)
30
+ if let token = request.token {
31
+ return Conversions.connection(try await terra.connect(source, for: endUser, using: token))
32
+ }
33
+ // The client's provider is the broker, so it is never nil, and the SDK's answer for a
34
+ // revoked link with no provider - `linkRevoked` - would become the broker's "nobody is
35
+ // listening". So the bridge answers it here, before any link starts.
36
+ if await !broker.canAnswer, await terra.needsTokenForRevokedLink(source, for: endUser) {
37
+ throw TerraError.linkRevoked(source)
38
+ }
39
+ return Conversions.connection(try await terra.connect(source, for: endUser))
40
+ }
41
+
42
+ func clientConnections(_ request: Empty) async throws -> ConnectionList {
43
+ ConnectionList(connections: try await terra.connections.map(Conversions.connection))
44
+ }
45
+
46
+ func clientAvailability(_ request: SourceRequest) async throws -> AvailabilityResult {
47
+ AvailabilityResult(availability: Conversions.availability(
48
+ await terra.availability(for: try Conversions.source(request.source))))
49
+ }
50
+
51
+ func clientDisconnect(_ request: SourceRequest) async throws -> Empty {
52
+ try await connection(request.source).disconnect()
53
+ return Empty()
54
+ }
55
+
56
+ // MARK: token
57
+
58
+ func tokenSetProviderPresent(_ request: TokenProviderRequest) async throws -> Empty {
59
+ await broker.setProviderPresent(request.present, timeoutMs: request.timeoutMs, audience: audience)
60
+ return Empty()
61
+ }
62
+
63
+ func tokenResolve(_ request: TokenResolveRequest) async throws -> Empty {
64
+ await broker.resolve(request.requestId, token: request.token)
65
+ return Empty()
66
+ }
67
+
68
+ func tokenReject(_ request: TokenRejectRequest) async throws -> Empty {
69
+ await broker.reject(request.requestId, message: request.message)
70
+ return Empty()
71
+ }
72
+
73
+ // MARK: permissions
74
+
75
+ /// HealthKit will not say what was granted for reads, so Apple answers nothing.
76
+ func permissionsRequest(_ request: PermissionRequest) async throws -> PermissionGrant {
77
+ _ = try Conversions.source(request.source)
78
+ try await terra.permissions.request(
79
+ read: request.read.map(Conversions.category),
80
+ write: request.write.map(Conversions.category),
81
+ workouts: request.workouts.map(Conversions.scope))
82
+ return PermissionGrant()
83
+ }
84
+
85
+ func permissionsStatus(_ request: CategoryRequest) async throws -> StatusResult {
86
+ _ = try Conversions.source(request.source)
87
+ return StatusResult(status: Conversions.permissionStatus(
88
+ await terra.permissions.status(for: Conversions.category(request.category))))
89
+ }
90
+
91
+ func permissionsWriteStatus(_ request: CategoryRequest) async throws -> StatusResult {
92
+ _ = try Conversions.source(request.source)
93
+ return StatusResult(status: Conversions.permissionStatus(
94
+ await terra.permissions.writeStatus(for: Conversions.category(request.category))))
95
+ }
96
+
97
+ func permissionsRequested(_ request: SourceRequest) async throws -> HealthAccess {
98
+ _ = try Conversions.source(request.source)
99
+ return Conversions.access(await terra.permissions.requested)
100
+ }
101
+
102
+ // MARK: diagnostics
103
+
104
+ func diagnosticsState(_ request: SourceRequest) async throws -> DiagnosticsState {
105
+ Conversions.diagnosticsState(await terra.diagnostics.state(for: try Conversions.source(request.source)))
106
+ }
107
+
108
+ func diagnosticsLastDelivery(_ request: SourceRequest) async throws -> LastDeliveryResult {
109
+ let at = await terra.diagnostics.lastDelivery(for: try Conversions.source(request.source))
110
+ return LastDeliveryResult(at: at.map(BridgeInstant.format))
111
+ }
112
+
113
+ func diagnosticsReport(_ request: SourceRequest) async throws -> SupportReport {
114
+ try Conversions.report(await terra.diagnostics.report(for: try Conversions.source(request.source)))
115
+ }
116
+
117
+ func diagnosticsSend(_ request: SourceRequest) async throws -> Empty {
118
+ try await terra.diagnostics.send(for: try Conversions.source(request.source))
119
+ return Empty()
120
+ }
121
+
122
+ // MARK: background
123
+
124
+ func backgroundIsRegistered(_ request: Empty) async throws -> BoolResult {
125
+ BoolResult(value: await host.backgroundRegistered())
126
+ }
127
+
128
+ // MARK: body
129
+
130
+ func bodyHeartRate(_ request: RangeRequest) async throws -> HeartRateList {
131
+ HeartRateList(samples: try await terra.body.heartRate(in: Conversions.timeRange(request.range, at: "$.range"))
132
+ .map(Conversions.heartRate))
133
+ }
134
+
135
+ func bodyRestingHeartRate(_ request: RangeRequest) async throws -> RestingHeartRateList {
136
+ RestingHeartRateList(samples: try await terra.body.restingHeartRate(in: Conversions.timeRange(request.range, at: "$.range"))
137
+ .map(Conversions.restingHeartRate))
138
+ }
139
+
140
+ func bodyReadings(_ request: RangeRequest) async throws -> BodyReadingList {
141
+ BodyReadingList(samples: try await terra.body.readings(in: Conversions.timeRange(request.range, at: "$.range"))
142
+ .compactMap(Conversions.bodyReading))
143
+ }
144
+
145
+ func bodyLogHeartRate(_ request: HeartRateEntryList) async throws -> RecordIdList {
146
+ ids(try await terra.body.log(entries(request.entries, Conversions.entry)))
147
+ }
148
+
149
+ func bodyLog(_ request: BodyEntryList) async throws -> RecordIdList {
150
+ ids(try await terra.body.log(entries(request.entries, Conversions.entry)))
151
+ }
152
+
153
+ func bodyDelete(_ request: RecordIdRequest) async throws -> Empty {
154
+ try await terra.body.delete(id: Conversions.uuid(request.id, at: "$.id"))
155
+ return Empty()
156
+ }
157
+
158
+ // MARK: daily
159
+
160
+ func dailySteps(_ request: RangeRequest) async throws -> StepCountList {
161
+ StepCountList(samples: try await terra.daily.steps(in: Conversions.timeRange(request.range, at: "$.range"))
162
+ .map(Conversions.steps))
163
+ }
164
+
165
+ func dailyTotals(_ request: RangeRequest) async throws -> DayTotalList {
166
+ DayTotalList(totals: try await terra.daily.totals(in: Conversions.timeRange(request.range, at: "$.range"))
167
+ .compactMap(Conversions.dayTotal))
168
+ }
169
+
170
+ func dailyIntraday(_ request: RangeRequest) async throws -> IntradaySampleList {
171
+ IntradaySampleList(samples: try await terra.daily.intraday(in: Conversions.timeRange(request.range, at: "$.range"))
172
+ .compactMap(Conversions.intraday))
173
+ }
174
+
175
+ func dailyLog(_ request: StepEntryList) async throws -> RecordIdList {
176
+ ids(try await terra.daily.log(entries(request.entries, Conversions.entry)))
177
+ }
178
+
179
+ func dailyDelete(_ request: RecordIdRequest) async throws -> Empty {
180
+ try await terra.daily.delete(id: Conversions.uuid(request.id, at: "$.id"))
181
+ return Empty()
182
+ }
183
+
184
+ // MARK: sleep
185
+
186
+ func sleepSegments(_ request: RangeRequest) async throws -> SleepSegmentList {
187
+ SleepSegmentList(samples: try await terra.sleep.segments(in: Conversions.timeRange(request.range, at: "$.range"))
188
+ .map(Conversions.segment))
189
+ }
190
+
191
+ func sleepLog(_ request: SleepEntryList) async throws -> RecordIdList {
192
+ ids(try await terra.sleep.log(entries(request.entries, Conversions.entry)))
193
+ }
194
+
195
+ func sleepDelete(_ request: RecordIdRequest) async throws -> Empty {
196
+ try await terra.sleep.delete(id: Conversions.uuid(request.id, at: "$.id"))
197
+ return Empty()
198
+ }
199
+
200
+ // MARK: activity
201
+
202
+ func activityWorkouts(_ request: RangeRequest) async throws -> WorkoutList {
203
+ WorkoutList(samples: try await terra.activity.workouts(in: Conversions.timeRange(request.range, at: "$.range"))
204
+ .map(Conversions.workout))
205
+ }
206
+
207
+ func activityRoutes(_ request: RangeRequest) async throws -> WorkoutRouteList {
208
+ WorkoutRouteList(routes: try await terra.activity.routes(in: Conversions.timeRange(request.range, at: "$.range"))
209
+ .map(Conversions.route))
210
+ }
211
+
212
+ func activityIntensity(_ request: RangeRequest) async throws -> WorkoutIntensityList {
213
+ WorkoutIntensityList(samples: try await terra.activity.intensity(in: Conversions.timeRange(request.range, at: "$.range"))
214
+ .compactMap(Conversions.intensity))
215
+ }
216
+
217
+ func activityLog(_ request: WorkoutEntryList) async throws -> RecordIdList {
218
+ ids(try await terra.activity.log(entries(request.entries, Conversions.entry)))
219
+ }
220
+
221
+ func activityDelete(_ request: RecordIdRequest) async throws -> Empty {
222
+ try await terra.activity.delete(id: Conversions.uuid(request.id, at: "$.id"))
223
+ return Empty()
224
+ }
225
+
226
+ // MARK: nutrition
227
+
228
+ func nutritionQuantities(_ request: RangeRequest) async throws -> DietaryQuantityList {
229
+ DietaryQuantityList(samples: try await terra.nutrition.quantities(in: Conversions.timeRange(request.range, at: "$.range"))
230
+ .compactMap(Conversions.quantity))
231
+ }
232
+
233
+ func nutritionLog(_ request: NutritionEntryList) async throws -> RecordIdList {
234
+ ids(try await terra.nutrition.log(entries(request.entries, Conversions.entry)))
235
+ }
236
+
237
+ func nutritionDelete(_ request: RecordIdRequest) async throws -> Empty {
238
+ try await terra.nutrition.delete(id: Conversions.uuid(request.id, at: "$.id"))
239
+ return Empty()
240
+ }
241
+
242
+ func nutritionHydration(_ request: RangeRequest) async throws -> HydrationList {
243
+ HydrationList(samples: try await terra.nutrition.hydration(in: Conversions.timeRange(request.range, at: "$.range"))
244
+ .map(Conversions.hydration))
245
+ }
246
+
247
+ // MARK: menstruation
248
+
249
+ func menstruationFlow(_ request: RangeRequest) async throws -> MenstruationFlowDayList {
250
+ MenstruationFlowDayList(samples: try await terra.menstruation.flow(in: Conversions.timeRange(request.range, at: "$.range"))
251
+ .map(Conversions.flowDay))
252
+ }
253
+
254
+ func menstruationEvents(_ request: RangeRequest) async throws -> MenstrualEventList {
255
+ MenstrualEventList(samples: try await terra.menstruation.events(in: Conversions.timeRange(request.range, at: "$.range"))
256
+ .compactMap(Conversions.menstrualEvent))
257
+ }
258
+
259
+ // MARK: athlete
260
+
261
+ func athleteProfile(_ request: Empty) async throws -> AthleteProfileResult {
262
+ AthleteProfileResult(profile: try await terra.athlete.profile()
263
+ .map { Conversions.athleteProfile($0, zone: .current) })
264
+ }
265
+
266
+ // MARK: planned workouts
267
+
268
+ func plannedWorkoutsPlanned(_ request: Empty) async throws -> PlannedWorkoutList {
269
+ PlannedWorkoutList(workouts: try await connection(nil).workouts.planned().map(Conversions.plannedWorkout))
270
+ }
271
+
272
+ func plannedWorkoutsMarkComplete(_ request: MarkCompleteRequest) async throws -> Empty {
273
+ let plans = try await connection(nil).workouts
274
+ if let at = request.at {
275
+ try await plans.markComplete(id: request.id, at: Conversions.instant(at, at: "$.at"))
276
+ } else {
277
+ try await plans.markComplete(id: request.id)
278
+ }
279
+ return Empty()
280
+ }
281
+
282
+ func plannedWorkoutsDelete(_ request: RecordIdRequest) async throws -> Empty {
283
+ try await connection(nil).workouts.delete(id: request.id)
284
+ return Empty()
285
+ }
286
+
287
+ func plannedWorkoutsSchedulingStatus(_ request: Empty) async throws -> WorkoutScheduling {
288
+ WorkoutScheduling(status: Conversions.schedulingStatus(
289
+ try await connection(nil).workouts.schedulingStatus()))
290
+ }
291
+
292
+ func plannedWorkoutsRequestScheduling(_ request: Empty) async throws -> WorkoutScheduling {
293
+ WorkoutScheduling(status: Conversions.schedulingStatus(
294
+ try await connection(nil).workouts.requestScheduling()))
295
+ }
296
+
297
+ // MARK: -
298
+
299
+ /// Apple hangs per-link behaviour on the `Connection`, so an op naming a source finds it first.
300
+ private func connection(_ wire: Source?) async throws -> TerraSDK.Connection {
301
+ let source = try Conversions.source(wire)
302
+ guard let connection = try await terra.connections.first(where: { $0.source == source }) else {
303
+ throw TerraError.notConnected(source)
304
+ }
305
+ return connection
306
+ }
307
+
308
+ /// Each entry converted with its own path, so a bad one is named by index.
309
+ private func entries<Wire, Entry>(
310
+ _ wire: [Wire], _ convert: (Wire, String) throws -> Entry
311
+ ) throws -> [Entry] {
312
+ try wire.enumerated().map { try convert($1, "$.entries[\($0)]") }
313
+ }
314
+
315
+ private func ids(_ ids: [UUID]) -> RecordIdList {
316
+ RecordIdList(ids: ids.map(\.uuidString))
317
+ }
318
+ }
@@ -0,0 +1,60 @@
1
+ import Foundation
2
+ import TerraSDK
3
+
4
+ /// The hand-written half of streams: one source per stream, a fresh one per call.
5
+ ///
6
+ /// Routing, element validation and framing are generated (`BridgeStreamDispatch`); this only
7
+ /// produces values, so a stream added to the schema stops this compiling until it has a source.
8
+ /// Each source is open when its method returns, so nothing after `subscribe` returns is missed.
9
+ struct TerraBridgeStreamsImpl: TerraBridgeStreams {
10
+ let terra: Terra
11
+ let audience: TokenAudience
12
+ let deviceZone: @Sendable () -> TimeZone
13
+
14
+ /// An event the bridge could not encode is relayed as an empty object, which the generated
15
+ /// envelope check refuses, so the subscription ends with an error instead of skipping it.
16
+ func diagnosticsEvents(_ params: Empty) async throws -> AsyncStream<TerraEvent> {
17
+ Self.relay(terra.diagnostics.events()) { (try? Conversions.event($0)) ?? BridgeJSON([:]) }
18
+ }
19
+
20
+ func bodyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
21
+ changes(await terra.body.changes())
22
+ }
23
+
24
+ func dailyChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
25
+ changes(await terra.daily.changes())
26
+ }
27
+
28
+ func sleepChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
29
+ changes(await terra.sleep.changes())
30
+ }
31
+
32
+ func activityChanges(_ params: Empty) async throws -> AsyncStream<HealthChange> {
33
+ changes(await terra.activity.changes())
34
+ }
35
+
36
+ func bridgeTokenRequest(_ params: Empty) async throws -> AsyncStream<TokenRequest> {
37
+ audience.listen()
38
+ }
39
+
40
+ /// A category the surface excludes has no wire name; a namespace's stream never carries one.
41
+ private func changes(_ source: AsyncStream<TerraSDK.HealthChange>) -> AsyncStream<HealthChange> {
42
+ let zone = deviceZone
43
+ return Self.relay(source) { Conversions.change($0, zone: zone()) }
44
+ }
45
+
46
+ /// Forwards an SDK stream, converting each element, until either side goes away.
47
+ private static func relay<Element: Sendable, Wire: Sendable>(
48
+ _ source: AsyncStream<Element>, _ convert: @escaping @Sendable (Element) -> Wire?
49
+ ) -> AsyncStream<Wire> {
50
+ AsyncStream { continuation in
51
+ let task = Task {
52
+ for await element in source {
53
+ if let wire = convert(element) { continuation.yield(wire) }
54
+ }
55
+ continuation.finish()
56
+ }
57
+ continuation.onTermination = { _ in task.cancel() }
58
+ }
59
+ }
60
+ }
@@ -0,0 +1,147 @@
1
+ import Foundation
2
+ import TerraSDK
3
+
4
+ /// The SDK's token provider, answered by a runtime on the other side of the bridge.
5
+ ///
6
+ /// The client is built at launch, before any JavaScript exists, and captures its provider then. So
7
+ /// the provider is this broker, built first and handed to the client, and the runtime that is
8
+ /// listening now is whichever session last said it has a provider.
9
+ ///
10
+ /// Safe because the SDK asks on one path only, `connect`, which only a live runtime can call. A
11
+ /// background wake never asks: uploads are signed by the device key.
12
+ actor TokenBroker {
13
+ static let defaultTimeoutMs = 30_000
14
+
15
+ private struct Pending {
16
+ let audience: ObjectIdentifier
17
+ let continuation: CheckedContinuation<String, any Error>
18
+ let timer: Task<Void, Never>
19
+ }
20
+
21
+ private weak var owner: TokenAudience?
22
+ private var timeoutMs = TokenBroker.defaultTimeoutMs
23
+ private var pending: [String: Pending] = [:]
24
+
25
+ /// What the client calls. No listener answers `tokenUnavailable` at once, which is what a
26
+ /// native app built without a provider gets.
27
+ func provide() async throws -> String {
28
+ guard let owner else { throw TerraError.tokenUnavailable }
29
+ let id = "tok-\(UUID().uuidString.lowercased())"
30
+ let budget = timeoutMs
31
+ return try await withTaskCancellationHandler {
32
+ try await withCheckedThrowingContinuation { (continuation: CheckedContinuation<String, any Error>) in
33
+ if Task.isCancelled {
34
+ continuation.resume(throwing: CancellationError())
35
+ return
36
+ }
37
+ let timer = Task.detached { [weak self] in
38
+ try? await Task.sleep(nanoseconds: UInt64(min(max(budget, 0), 3_600_000)) * 1_000_000)
39
+ await self?.settle(id, .failure(BridgeError.tokenTimeout(timeoutMs: budget)))
40
+ }
41
+ pending[id] = Pending(audience: ObjectIdentifier(owner), continuation: continuation, timer: timer)
42
+ if !owner.deliver(TokenRequest(requestId: id)) {
43
+ settle(id, .failure(TerraError.tokenUnavailable))
44
+ }
45
+ }
46
+ } onCancel: {
47
+ Task { await self.settle(id, .failure(CancellationError())) }
48
+ }
49
+ }
50
+
51
+ /// Whether an ask made now would reach anyone.
52
+ var canAnswer: Bool { owner?.isListening ?? false }
53
+
54
+ /// The session that answers from now on, or none.
55
+ func setProviderPresent(_ present: Bool, timeoutMs: Int, audience: TokenAudience) {
56
+ guard !audience.isClosed else { return }
57
+ if present {
58
+ owner = audience
59
+ self.timeoutMs = timeoutMs
60
+ } else if owner === audience {
61
+ owner = nil
62
+ }
63
+ }
64
+
65
+ /// An unknown or settled id is ignored: a runtime that reloaded answers asks its predecessor
66
+ /// made, and failing that would turn a harmless race into a failed link.
67
+ func resolve(_ requestId: String, token: String) {
68
+ settle(requestId, .success(token))
69
+ }
70
+
71
+ func reject(_ requestId: String, message: String) {
72
+ settle(requestId, .failure(ProviderRefused(message: message)))
73
+ }
74
+
75
+ /// Fails what this session was asked, and stops routing to it. Only this session's: a stale
76
+ /// teardown arriving after its successor registered must not fail the successor's link.
77
+ func invalidate(_ audience: TokenAudience) {
78
+ if owner === audience { owner = nil }
79
+ let identity = ObjectIdentifier(audience)
80
+ for (id, ask) in pending where ask.audience == identity {
81
+ settle(id, .failure(ProviderRefused(message: "the host runtime went away")))
82
+ }
83
+ }
84
+
85
+ var pendingCount: Int { pending.count }
86
+
87
+ private func settle(_ id: String, _ result: Result<String, any Error>) {
88
+ guard let ask = pending.removeValue(forKey: id) else { return }
89
+ ask.timer.cancel()
90
+ ask.continuation.resume(with: result)
91
+ }
92
+
93
+ /// The provider's own failure. Its text never crosses the bridge; see `Rejections`.
94
+ struct ProviderRefused: Error, Sendable {
95
+ let message: String
96
+ }
97
+ }
98
+
99
+ /// The token-request subscriptions of one session.
100
+ final class TokenAudience: Sendable {
101
+ private struct State {
102
+ var listeners: [UUID: AsyncStream<TokenRequest>.Continuation] = [:]
103
+ var closed = false
104
+ }
105
+
106
+ private let state = Locked(State())
107
+
108
+ var isClosed: Bool { state.withLock { $0.closed } }
109
+
110
+ var isListening: Bool { state.withLock { !$0.closed && !$0.listeners.isEmpty } }
111
+
112
+ func listen() -> AsyncStream<TokenRequest> {
113
+ let (stream, continuation) = AsyncStream<TokenRequest>.makeStream()
114
+ let key = UUID()
115
+ let accepted = state.withLock { state in
116
+ guard !state.closed else { return false }
117
+ state.listeners[key] = continuation
118
+ return true
119
+ }
120
+ guard accepted else {
121
+ continuation.finish()
122
+ return stream
123
+ }
124
+ continuation.onTermination = { [weak self] _ in
125
+ self?.state.withLock { _ = $0.listeners.removeValue(forKey: key) }
126
+ }
127
+ return stream
128
+ }
129
+
130
+ func deliver(_ request: TokenRequest) -> Bool {
131
+ let listeners = state.withLock { $0.closed ? [] : Array($0.listeners.values) }
132
+ var delivered = false
133
+ for listener in listeners {
134
+ if case .enqueued = listener.yield(request) { delivered = true }
135
+ }
136
+ return delivered
137
+ }
138
+
139
+ func close() {
140
+ let listeners = state.withLock { state in
141
+ state.closed = true
142
+ defer { state.listeners = [:] }
143
+ return Array(state.listeners.values)
144
+ }
145
+ for listener in listeners { listener.finish() }
146
+ }
147
+ }
@@ -0,0 +1,79 @@
1
+ #import <React/RCTInvalidating.h>
2
+ #import <TerraReactNativeSpec/TerraReactNativeSpec.h>
3
+
4
+ // Declared here rather than in a header: a public header importing the C++ spec would be read by
5
+ // the Swift half of this module, which cannot import C++.
6
+ @interface TerraBridgeModule : NativeTerraBridgeSpecBase <NativeTerraBridgeSpec, RCTInvalidating>
7
+ @end
8
+
9
+ #if __has_include("TerraReactNative-Swift.h")
10
+ #import "TerraReactNative-Swift.h"
11
+ #else
12
+ #import <TerraReactNative/TerraReactNative-Swift.h>
13
+ #endif
14
+
15
+ static NSString *const TerraBridgeErrorDomain = @"co.tryterra.sdk.bridge";
16
+
17
+ @implementation TerraBridgeModule {
18
+ TerraBridgeModuleImpl *_impl;
19
+ }
20
+
21
+ RCT_EXPORT_MODULE(TerraBridge)
22
+
23
+ - (instancetype)init
24
+ {
25
+ if (self = [super init]) {
26
+ __weak TerraBridgeModule *weakSelf = self;
27
+ _impl = [[TerraBridgeModuleImpl alloc] initWithEmit:^(NSString *subscriptionId, NSString *message) {
28
+ [weakSelf emitOnMessage:@{@"subscriptionId" : subscriptionId, @"message" : message}];
29
+ }];
30
+ }
31
+ return self;
32
+ }
33
+
34
+ static void (^TerraReject(RCTPromiseRejectBlock reject))(NSString *, NSString *, NSString *)
35
+ {
36
+ return ^(NSString *code, NSString *message, NSString *payload) {
37
+ reject(code, message, [NSError errorWithDomain:TerraBridgeErrorDomain code:0 userInfo:@{@"payload" : payload}]);
38
+ };
39
+ }
40
+
41
+ - (void)invoke:(NSString *)op
42
+ payload:(NSString *)payload
43
+ resolve:(RCTPromiseResolveBlock)resolve
44
+ reject:(RCTPromiseRejectBlock)reject
45
+ {
46
+ [_impl invoke:op
47
+ payload:payload
48
+ resolve:^(NSString *response) { resolve(response); }
49
+ reject:TerraReject(reject)];
50
+ }
51
+
52
+ - (void)subscribe:(NSString *)stream
53
+ params:(NSString *)params
54
+ resolve:(RCTPromiseResolveBlock)resolve
55
+ reject:(RCTPromiseRejectBlock)reject
56
+ {
57
+ [_impl subscribe:stream
58
+ params:params
59
+ resolve:^(NSString *subscriptionId) { resolve(subscriptionId); }
60
+ reject:TerraReject(reject)];
61
+ }
62
+
63
+ - (void)unsubscribe:(NSString *)subscriptionId
64
+ {
65
+ [_impl unsubscribe:subscriptionId];
66
+ }
67
+
68
+ - (void)invalidate
69
+ {
70
+ [_impl invalidate];
71
+ }
72
+
73
+ - (std::shared_ptr<facebook::react::TurboModule>)getTurboModule:
74
+ (const facebook::react::ObjCTurboModule::InitParams &)params
75
+ {
76
+ return std::make_shared<facebook::react::NativeTerraBridgeSpecJSI>(params);
77
+ }
78
+
79
+ @end
@@ -0,0 +1,49 @@
1
+ import Foundation
2
+
3
+ @objc(TerraBridgeModuleImpl)
4
+ public final class TerraBridgeModuleImpl: NSObject, @unchecked Sendable {
5
+ public typealias Resolve = @Sendable (String) -> Void
6
+ /// `(code, message, payload JSON)`. The module rejects with the payload under `userInfo.payload`.
7
+ public typealias Reject = @Sendable (String, String, String) -> Void
8
+
9
+ private let attachment: RuntimeAttachment
10
+
11
+ @objc public init(emit: @escaping @Sendable (_ subscriptionId: String, _ message: String) -> Void) {
12
+ attachment = RuntimeAttachment(missingLine: TerraReactNative.missingLine, emit: emit)
13
+ }
14
+
15
+ /// Nothing answers a runtime that has been invalidated: its promises belong to a JS context that is gone.
16
+ @objc public func invoke(_ op: String, payload: String, resolve: @escaping Resolve, reject: @escaping Reject) {
17
+ let attachment = attachment
18
+ Task {
19
+ do throws(BridgeRejection) {
20
+ guard let session = try attachment.session() else { return }
21
+ let answer = try await session.dispatch(op: op, payload: payload)
22
+ if !attachment.isInvalidated { resolve(answer) }
23
+ } catch {
24
+ if !attachment.isInvalidated { reject(error.code, error.payload.message, error.json) }
25
+ }
26
+ }
27
+ }
28
+
29
+ @objc public func subscribe(_ stream: String, params: String, resolve: @escaping Resolve, reject: @escaping Reject) {
30
+ let attachment = attachment
31
+ Task {
32
+ do throws(BridgeRejection) {
33
+ guard let session = try attachment.session() else { return }
34
+ let id = try await session.subscribe(stream: stream, params: params)
35
+ if !attachment.isInvalidated { resolve(id) }
36
+ } catch {
37
+ if !attachment.isInvalidated { reject(error.code, error.payload.message, error.json) }
38
+ }
39
+ }
40
+ }
41
+
42
+ @objc public func unsubscribe(_ subscriptionId: String) {
43
+ attachment.current()?.unsubscribe(id: subscriptionId)
44
+ }
45
+
46
+ @objc public func invalidate() {
47
+ attachment.invalidate()
48
+ }
49
+ }
@@ -0,0 +1,19 @@
1
+ import Foundation
2
+ import os
3
+
4
+ /// iOS raises if a background task is registered after launch finishes.
5
+ /// A HealthKit wake never starts a JavaScript runtime.
6
+ @objc(TerraReactNative)
7
+ public final class TerraReactNative: NSObject {
8
+ static let missingLine = "TerraReactNative.bootstrap() in application(_:didFinishLaunchingWithOptions:), with TerraDevID in Info.plist"
9
+
10
+ private static let log = Logger(subsystem: "co.tryterra.sdk", category: "TerraReactNative")
11
+
12
+ /// Builds the client, registers background sync, and parks both for JavaScript to attach to.
13
+ /// Safe to call more than once; the second call does nothing.
14
+ @objc public static func bootstrap() {
15
+ if BridgeBootstrap.bootstrap(bundle: .main) == nil {
16
+ log.error("Terra is not set up: Info.plist has no \(BridgeBootstrap.devIDKey, privacy: .public)")
17
+ }
18
+ }
19
+ }
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Who a connect is for. Every connect names one: a second account on the same phone is a different
3
+ * person, and must not be handed the first one's link.
4
+ */
5
+ import type { EndUser as WireEndUser } from "./generated/types";
6
+ export type EndUser = WireEndUser;
7
+ export declare const EndUser: {
8
+ /** The person your backend knows as `id`: the same value it mints their token with. */
9
+ readonly reference: (id: string) => EndUser;
10
+ /** Nobody your backend tracks. Every anonymous connect on one device is the same person. */
11
+ readonly anonymous: EndUser;
12
+ };