@tryterra/react-native 0.0.0-stage → 2.2.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/README.md +142 -2
- package/TerraReactNative.podspec +48 -0
- package/android/build.gradle +87 -0
- package/android/src/bridgeCore/kotlin/README.md +1 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeBootstrap.kt +133 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/BridgeHost.kt +32 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Conversions.kt +557 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/LauncherSlots.kt +41 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/ReadConversions.kt +250 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/Rejections.kt +150 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/StreamRegistry.kt +77 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridge.kt +86 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeOpsImpl.kt +300 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TerraBridgeStreamsImpl.kt +50 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/TokenBroker.kt +198 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeErrors.kt +217 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeOps.kt +582 -0
- package/android/src/bridgeCore/kotlin/co/tryterra/sdk/bridge/generated/BridgeTypes.kt +1569 -0
- package/android/src/healthConnectOnly/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +11 -0
- package/android/src/main/AndroidManifest.xml +24 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraBridgeModule.kt +96 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraReactNativePackage.kt +26 -0
- package/android/src/main/java/co/tryterra/sdk/reactnative/TerraStartup.kt +46 -0
- package/android/src/samsung/kotlin/co/tryterra/sdk/reactnative/TerraStores.kt +13 -0
- package/docs/availability.md +396 -0
- package/frameworks/TerraSDK.xcframework/Info.plist +48 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6594 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Info.plist +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/arm64-apple-ios-simulator.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.abi.json +82672 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.package.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.private.swiftinterface +2186 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftdoc +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/Modules/TerraSDK.swiftmodule/x86_64-apple-ios-simulator.swiftinterface +2163 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/PrivacyInfo.xcprivacy +96 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/TerraSDK.framework/_CodeSignature/CodeResources +223 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Info.plist +20 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/DWARF/TerraSDK +0 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/aarch64/TerraSDK.yml +6595 -0
- package/frameworks/TerraSDK.xcframework/ios-arm64_x86_64-simulator/dSYMs/TerraSDK.framework.dSYM/Contents/Resources/Relocations/x86_64/TerraSDK.yml +6513 -0
- package/ios/TerraBridgeCore/BridgeBootstrap.swift +89 -0
- package/ios/TerraBridgeCore/BridgeHost.swift +13 -0
- package/ios/TerraBridgeCore/Conversions+Reads.swift +259 -0
- package/ios/TerraBridgeCore/Conversions.swift +594 -0
- package/ios/TerraBridgeCore/Generated/BridgeErrors.swift +179 -0
- package/ios/TerraBridgeCore/Generated/BridgeOps.swift +967 -0
- package/ios/TerraBridgeCore/Generated/BridgeTypes.swift +2050 -0
- package/ios/TerraBridgeCore/Locked.swift +15 -0
- package/ios/TerraBridgeCore/README.md +1 -0
- package/ios/TerraBridgeCore/Rejections.swift +130 -0
- package/ios/TerraBridgeCore/RuntimeAttachment.swift +60 -0
- package/ios/TerraBridgeCore/StreamRegistry.swift +59 -0
- package/ios/TerraBridgeCore/TerraBridge.swift +122 -0
- package/ios/TerraBridgeCore/TerraBridgeOpsImpl.swift +318 -0
- package/ios/TerraBridgeCore/TerraBridgeStreamsImpl.swift +60 -0
- package/ios/TerraBridgeCore/TokenBroker.swift +147 -0
- package/ios/TerraBridgeModule.mm +79 -0
- package/ios/TerraBridgeModuleImpl.swift +49 -0
- package/ios/TerraReactNative.swift +19 -0
- package/lib/EndUser.d.ts +12 -0
- package/lib/EndUser.js +13 -0
- package/lib/NativeTerraBridge.d.ts +23 -0
- package/lib/NativeTerraBridge.js +13 -0
- package/lib/Terra.d.ts +223 -0
- package/lib/Terra.js +336 -0
- package/lib/TerraError.d.ts +42 -0
- package/lib/TerraError.js +95 -0
- package/lib/createTerra.d.ts +8 -0
- package/lib/createTerra.js +19 -0
- package/lib/generated/client.d.ts +128 -0
- package/lib/generated/client.js +256 -0
- package/lib/generated/codecs.d.ts +219 -0
- package/lib/generated/codecs.js +2066 -0
- package/lib/generated/errors.d.ts +13 -0
- package/lib/generated/errors.js +68 -0
- package/lib/generated/ops.d.ts +13 -0
- package/lib/generated/ops.js +161 -0
- package/lib/generated/streams.d.ts +23 -0
- package/lib/generated/streams.js +58 -0
- package/lib/generated/types.d.ts +794 -0
- package/lib/generated/types.js +224 -0
- package/lib/index.d.ts +29 -0
- package/lib/index.js +55 -0
- package/package.json +71 -4
- package/src/EndUser.ts +15 -0
- package/src/NativeTerraBridge.ts +27 -0
- package/src/Terra.ts +592 -0
- package/src/TerraError.ts +116 -0
- package/src/createTerra.ts +18 -0
- package/src/generated/client.ts +315 -0
- package/src/generated/codecs.ts +1989 -0
- package/src/generated/errors.ts +113 -0
- package/src/generated/ops.ts +233 -0
- package/src/generated/streams.ts +74 -0
- package/src/generated/types.ts +1222 -0
- package/src/index.ts +54 -0
|
@@ -0,0 +1,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
|
+
}
|
package/lib/EndUser.d.ts
ADDED
|
@@ -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
|
+
};
|