@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,15 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// State behind a lock. `Mutex` needs iOS 18, and the floor is 15.
|
|
4
|
+
final class Locked<State>: @unchecked Sendable {
|
|
5
|
+
private let lock = NSLock()
|
|
6
|
+
private var state: State
|
|
7
|
+
|
|
8
|
+
init(_ state: State) { self.state = state }
|
|
9
|
+
|
|
10
|
+
func withLock<Result>(_ body: (inout State) throws -> Result) rethrows -> Result {
|
|
11
|
+
lock.lock()
|
|
12
|
+
defer { lock.unlock() }
|
|
13
|
+
return try body(&state)
|
|
14
|
+
}
|
|
15
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
Copied from hybrid-common by scripts/vendor-bridge-core.js. Do not edit; change hybrid-common.
|
|
@@ -0,0 +1,130 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TerraSDK
|
|
3
|
+
|
|
4
|
+
/// A thrown error as a value another language can switch on.
|
|
5
|
+
///
|
|
6
|
+
/// The code and retryability come from the SDK (`TerraError.code`, the vocabulary in
|
|
7
|
+
/// `docs/sdk-reference.md`), so there is no table here to drift. What this owns is which parts of
|
|
8
|
+
/// an error are safe to send: nothing in `message` or `details` may carry a health value or text
|
|
9
|
+
/// written by code this SDK does not own.
|
|
10
|
+
enum Rejections {
|
|
11
|
+
|
|
12
|
+
static func payload(for error: any Error) -> TerraErrorPayload {
|
|
13
|
+
switch error {
|
|
14
|
+
case let error as TerraError:
|
|
15
|
+
// The broker's own failures reach the SDK through the token provider, which wraps
|
|
16
|
+
// them. Unwrapped here so `bridge.token_timeout` stays distinct from a provider that
|
|
17
|
+
// answered with an error.
|
|
18
|
+
if case .tokenProviderFailed(let underlying) = error, let bridge = underlying as? BridgeError {
|
|
19
|
+
return payload(for: bridge)
|
|
20
|
+
}
|
|
21
|
+
return TerraErrorPayload(
|
|
22
|
+
code: error.code,
|
|
23
|
+
retryable: error.isRetryable,
|
|
24
|
+
message: message(error),
|
|
25
|
+
source: source(error),
|
|
26
|
+
details: details(error))
|
|
27
|
+
case let error as BridgeError:
|
|
28
|
+
return TerraErrorPayload(
|
|
29
|
+
code: error.code,
|
|
30
|
+
retryable: error.isRetryable,
|
|
31
|
+
message: message(error),
|
|
32
|
+
details: details(error))
|
|
33
|
+
case is CancellationError:
|
|
34
|
+
return platform("the call was cancelled")
|
|
35
|
+
default:
|
|
36
|
+
return platform("a platform call failed")
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
private static func platform(_ message: String) -> TerraErrorPayload {
|
|
41
|
+
TerraErrorPayload(code: "platform", retryable: false, message: message)
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Two cases wrap an error this SDK did not raise, and its text is as likely to carry a token
|
|
45
|
+
/// or a reading as to describe the failure, so neither crosses.
|
|
46
|
+
private static func message(_ error: TerraError) -> String {
|
|
47
|
+
switch error {
|
|
48
|
+
case .tokenProviderFailed: return "the host's token provider failed"
|
|
49
|
+
case .platform: return "a platform call failed"
|
|
50
|
+
// The SDK's description embeds its cause's, which can be either of the two above.
|
|
51
|
+
case .rollbackIncomplete(let remaining, let cause):
|
|
52
|
+
return "the log failed (\(message(cause))) and \(remaining.count) sample(s) could not be removed"
|
|
53
|
+
default: return error.errorDescription ?? error.code
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
private static func message(_ error: BridgeError) -> String {
|
|
58
|
+
switch error {
|
|
59
|
+
case .notBootstrapped: return "the native bootstrap did not run"
|
|
60
|
+
case .unsupportedOnPlatform(let op): return "\(op) is not available on iOS"
|
|
61
|
+
case .tokenTimeout(let timeoutMs): return "the token provider did not answer within \(timeoutMs) ms"
|
|
62
|
+
case .invalidPayload(let path): return "the payload is invalid at \(path)"
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
private static func source(_ error: TerraError) -> Source? {
|
|
67
|
+
switch error {
|
|
68
|
+
case .notConnected(let source), .linkRevoked(let source), .devIDMismatch(let source),
|
|
69
|
+
.linkAlreadyRunning(let source):
|
|
70
|
+
return Conversions.source(source)
|
|
71
|
+
default:
|
|
72
|
+
return nil
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
private static func details(_ error: TerraError) -> BridgeJSON? {
|
|
77
|
+
switch error {
|
|
78
|
+
case .routeAwaitingConverter(let type): return bag(["dataType": type.rawValue])
|
|
79
|
+
case .syncAlreadyRunning(let type): return bag(["dataType": type.rawValue])
|
|
80
|
+
case .permissionDenied(let category): return bag(["category": category.rawValue])
|
|
81
|
+
case .sampleNotFound(let id): return bag(["id": id.uuidString])
|
|
82
|
+
// The cause travels as its code: a nested error's description carries its own detail.
|
|
83
|
+
case .rollbackIncomplete(let remaining, let cause):
|
|
84
|
+
return BridgeJSON(["remaining": .number(Double(remaining.count)), "cause": .string(cause.code)])
|
|
85
|
+
case .distanceNotAttributable(let activity):
|
|
86
|
+
return bag(["activity": Conversions.sortKey(Conversions.activity(activity))])
|
|
87
|
+
case .network(let failure): return details(failure)
|
|
88
|
+
case .tokenProviderFailed, .platform:
|
|
89
|
+
return nil
|
|
90
|
+
case .healthDataUnavailable, .notConnected, .tokenUnavailable, .tokenRejected,
|
|
91
|
+
.userLimitReached, .upgradeRequired, .deviceKeyUnavailable, .signatureRejected,
|
|
92
|
+
.clockSkewed, .linkRevoked, .devIDMismatch, .linkConflict, .linkAlreadyRunning,
|
|
93
|
+
.storeLocked, .storeUnreadable, .deadlineExceeded, .authorizationNotDetermined,
|
|
94
|
+
.bloodPressureIncomplete, .plannedWorkoutUnnamed, .authorizationUnavailableInExtension:
|
|
95
|
+
return nil
|
|
96
|
+
@unknown default:
|
|
97
|
+
return nil
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
private static func details(_ failure: TerraError.Network) -> BridgeJSON? {
|
|
102
|
+
switch failure {
|
|
103
|
+
case .serverError(let status): return number("status", status)
|
|
104
|
+
case .throttled(let retryAfter):
|
|
105
|
+
return retryAfter.map { number("retryAfterSeconds", Int($0.rounded(.up))) }
|
|
106
|
+
// A URLError code says nothing a caller can act on beyond the code itself.
|
|
107
|
+
case .transport, .offline, .timedOut, .malformedResponse:
|
|
108
|
+
return nil
|
|
109
|
+
@unknown default:
|
|
110
|
+
return nil
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
private static func details(_ error: BridgeError) -> BridgeJSON? {
|
|
115
|
+
switch error {
|
|
116
|
+
case .notBootstrapped(let line): return bag(["line": line])
|
|
117
|
+
case .unsupportedOnPlatform(let op): return bag(["op": op])
|
|
118
|
+
case .tokenTimeout(let timeoutMs): return number("timeoutMs", timeoutMs)
|
|
119
|
+
case .invalidPayload(let path): return bag(["path": path])
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
private static func bag(_ values: [String: String]) -> BridgeJSON {
|
|
124
|
+
BridgeJSON(values.mapValues { .string($0) })
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
private static func number(_ key: String, _ value: Int) -> BridgeJSON {
|
|
128
|
+
BridgeJSON([key: .number(Double(value))])
|
|
129
|
+
}
|
|
130
|
+
}
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
public final class RuntimeAttachment: Sendable {
|
|
4
|
+
private struct State {
|
|
5
|
+
var attached: BridgeSession?
|
|
6
|
+
var invalidated = false
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
private let state = Locked(State())
|
|
10
|
+
private let attach: @Sendable () throws(BridgeRejection) -> BridgeSession
|
|
11
|
+
|
|
12
|
+
/// Attaches to what `BridgeBootstrap` parked. `missingLine` is what `bridge.not_bootstrapped` names.
|
|
13
|
+
public convenience init(
|
|
14
|
+
missingLine: String,
|
|
15
|
+
emit: @escaping @Sendable (_ subscriptionId: String, _ message: String) -> Void
|
|
16
|
+
) {
|
|
17
|
+
self.init { () throws(BridgeRejection) -> BridgeSession in
|
|
18
|
+
try BridgeBootstrap.attach(missingLine: missingLine, emit: emit)
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
init(attach: @escaping @Sendable () throws(BridgeRejection) -> BridgeSession) {
|
|
23
|
+
self.attach = attach
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
public func session() throws(BridgeRejection) -> BridgeSession? {
|
|
27
|
+
let attach = attach
|
|
28
|
+
let outcome: Result<BridgeSession?, BridgeRejection> = state.withLock { state in
|
|
29
|
+
if state.invalidated { return .success(nil) }
|
|
30
|
+
if let attached = state.attached { return .success(attached) }
|
|
31
|
+
do throws(BridgeRejection) {
|
|
32
|
+
let session = try attach()
|
|
33
|
+
state.attached = session
|
|
34
|
+
return .success(session)
|
|
35
|
+
} catch {
|
|
36
|
+
return .failure(error)
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
return try outcome.get()
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// The attached session, without attaching one.
|
|
43
|
+
public func current() -> BridgeSession? {
|
|
44
|
+
state.withLock { $0.attached }
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
public var isInvalidated: Bool {
|
|
48
|
+
state.withLock { $0.invalidated }
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
@discardableResult
|
|
52
|
+
public func invalidate() -> Task<Void, Never> {
|
|
53
|
+
let session = state.withLock { state -> BridgeSession? in
|
|
54
|
+
state.invalidated = true
|
|
55
|
+
defer { state.attached = nil }
|
|
56
|
+
return state.attached
|
|
57
|
+
}
|
|
58
|
+
return Task { await session?.invalidate() }
|
|
59
|
+
}
|
|
60
|
+
}
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
|
|
3
|
+
/// One native stream per subscriber, started on subscribe and cancelled on unsubscribe.
|
|
4
|
+
///
|
|
5
|
+
/// What arrives is already framed, endings included, by the generated `BridgeStreamDispatch`.
|
|
6
|
+
final class StreamRegistry: Sendable {
|
|
7
|
+
typealias Sink = @Sendable (_ subscriptionId: String, _ message: String) -> Void
|
|
8
|
+
|
|
9
|
+
private struct State {
|
|
10
|
+
var next = 0
|
|
11
|
+
var tasks: [String: Task<Void, Never>] = [:]
|
|
12
|
+
var closed = false
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
private let state = Locked(State())
|
|
16
|
+
|
|
17
|
+
/// Forwards `source`, already framed with its ending by `BridgeStreamDispatch`, until it ends
|
|
18
|
+
/// or is cancelled, and answers the id that cancels it.
|
|
19
|
+
///
|
|
20
|
+
/// The task is created under the lock, so its own removal at the end cannot run before its
|
|
21
|
+
/// insertion - a source that finishes at once would otherwise leave its entry behind forever.
|
|
22
|
+
func start(sink: @escaping Sink, source: AsyncStream<Data>) -> String {
|
|
23
|
+
state.withLock { state in
|
|
24
|
+
state.next += 1
|
|
25
|
+
let id = "sub-\(state.next)"
|
|
26
|
+
guard !state.closed else { return id }
|
|
27
|
+
state.tasks[id] = Task {
|
|
28
|
+
for await message in source {
|
|
29
|
+
// A cancelled source still frames an ending; the subscriber asked for silence.
|
|
30
|
+
guard !Task.isCancelled else { break }
|
|
31
|
+
sink(id, String(decoding: message, as: UTF8.self))
|
|
32
|
+
}
|
|
33
|
+
self.finished(id)
|
|
34
|
+
}
|
|
35
|
+
return id
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/// An unknown id is ignored: a second unsubscribe is not an error.
|
|
40
|
+
func cancel(_ id: String) {
|
|
41
|
+
state.withLock { $0.tasks.removeValue(forKey: id) }?.cancel()
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/// Cancels everything and starts nothing new, for a host runtime that is going away.
|
|
45
|
+
func close() {
|
|
46
|
+
let tasks = state.withLock { state in
|
|
47
|
+
state.closed = true
|
|
48
|
+
defer { state.tasks = [:] }
|
|
49
|
+
return Array(state.tasks.values)
|
|
50
|
+
}
|
|
51
|
+
for task in tasks { task.cancel() }
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
var count: Int { state.withLock { $0.tasks.count } }
|
|
55
|
+
|
|
56
|
+
private func finished(_ id: String) {
|
|
57
|
+
state.withLock { _ = $0.tasks.removeValue(forKey: id) }
|
|
58
|
+
}
|
|
59
|
+
}
|
|
@@ -0,0 +1,122 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import TerraSDK
|
|
3
|
+
|
|
4
|
+
/// The process-wide half: the client, and the broker it was built with.
|
|
5
|
+
///
|
|
6
|
+
/// Built once, at launch, by the native bootstrap - before any JavaScript or Dart exists, because
|
|
7
|
+
/// background sync needs a live client then. A runtime that comes and goes (a JS reload, a Flutter
|
|
8
|
+
/// engine) attaches a `BridgeSession` to it and detaches by invalidating that session.
|
|
9
|
+
///
|
|
10
|
+
/// Nothing here may mention a framework: no promise type, no `NSDictionary`, no `FlutterResult`.
|
|
11
|
+
public final class TerraBridge: Sendable {
|
|
12
|
+
public typealias TokenProvider = @Sendable () async throws -> String
|
|
13
|
+
|
|
14
|
+
/// The client, for the bootstrap that registers it with `BackgroundSync`.
|
|
15
|
+
public let terra: Terra
|
|
16
|
+
|
|
17
|
+
let host: any BridgeHost
|
|
18
|
+
let broker: TokenBroker
|
|
19
|
+
let deviceZone: @Sendable () -> TimeZone
|
|
20
|
+
|
|
21
|
+
/// `client` receives the provider the client must be built with. The broker exists before the
|
|
22
|
+
/// client because the client captures its provider at construction: a broker made afterwards
|
|
23
|
+
/// would be one the SDK never asks.
|
|
24
|
+
public init(
|
|
25
|
+
host: any BridgeHost,
|
|
26
|
+
deviceZone: @escaping @Sendable () -> TimeZone = { .current },
|
|
27
|
+
client: (_ tokenProvider: @escaping TokenProvider) throws -> Terra
|
|
28
|
+
) rethrows {
|
|
29
|
+
let broker = TokenBroker()
|
|
30
|
+
self.broker = broker
|
|
31
|
+
self.host = host
|
|
32
|
+
self.deviceZone = deviceZone
|
|
33
|
+
self.terra = try client { try await broker.provide() }
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/// One session per runtime. `emit` receives `(subscriptionId, StreamMessage JSON)`.
|
|
37
|
+
public func attach(emit: @escaping @Sendable (_ subscriptionId: String, _ message: String) -> Void) -> BridgeSession {
|
|
38
|
+
BridgeSession(bridge: self, emit: emit)
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/// What a framework layer calls: dispatch, subscribe, unsubscribe, invalidate.
|
|
43
|
+
///
|
|
44
|
+
/// A React Native TurboModule forwards `invoke` to `dispatch` and its emitter to `emit`. A Flutter
|
|
45
|
+
/// `MethodChannel` is `dispatch(call.method, call.arguments)` and an `EventChannel` wraps
|
|
46
|
+
/// `subscribe`.
|
|
47
|
+
public final class BridgeSession: Sendable {
|
|
48
|
+
private let bridge: TerraBridge
|
|
49
|
+
private let emit: @Sendable (String, String) -> Void
|
|
50
|
+
private let subscriptions = StreamRegistry()
|
|
51
|
+
private let audience = TokenAudience()
|
|
52
|
+
private let ops: BridgeDispatch
|
|
53
|
+
private let streams: BridgeStreamDispatch
|
|
54
|
+
|
|
55
|
+
init(bridge: TerraBridge, emit: @escaping @Sendable (String, String) -> Void) {
|
|
56
|
+
self.bridge = bridge
|
|
57
|
+
self.emit = emit
|
|
58
|
+
self.ops = BridgeDispatch(ops: TerraBridgeOpsImpl(
|
|
59
|
+
terra: bridge.terra, host: bridge.host, broker: bridge.broker, audience: audience))
|
|
60
|
+
self.streams = BridgeStreamDispatch(
|
|
61
|
+
streams: TerraBridgeStreamsImpl(terra: bridge.terra, audience: audience, deviceZone: bridge.deviceZone),
|
|
62
|
+
errorPayload: { Rejections.payload(for: $0) })
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/// A session dropped without `invalidate` still stops its streams: they would otherwise keep
|
|
66
|
+
/// a source polling for a runtime that is gone.
|
|
67
|
+
deinit {
|
|
68
|
+
audience.close()
|
|
69
|
+
subscriptions.close()
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/// Runs one op and answers its response JSON. Never throws a raw SDK error: the framework layer
|
|
73
|
+
/// rejects with `rejection.payload`, so the error table lives here once rather than in each
|
|
74
|
+
/// wrapper.
|
|
75
|
+
public func dispatch(op: String, payload: String) async throws(BridgeRejection) -> String {
|
|
76
|
+
do {
|
|
77
|
+
let response = try await ops(op: op, payload: Data(payload.utf8))
|
|
78
|
+
return String(decoding: response, as: UTF8.self)
|
|
79
|
+
} catch {
|
|
80
|
+
throw BridgeRejection(payload: Rejections.payload(for: error))
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/// Starts one native stream for this subscriber and answers the id that cancels it. Messages
|
|
85
|
+
/// arrive through `emit` under that id. The source is open before this returns.
|
|
86
|
+
public func subscribe(stream: String, params: String) async throws(BridgeRejection) -> String {
|
|
87
|
+
do {
|
|
88
|
+
let source = try await streams.open(stream: stream, params: Data(params.utf8))
|
|
89
|
+
return subscriptions.start(sink: emit, source: source)
|
|
90
|
+
} catch {
|
|
91
|
+
throw BridgeRejection(payload: Rejections.payload(for: error))
|
|
92
|
+
}
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
public func unsubscribe(id: String) {
|
|
96
|
+
subscriptions.cancel(id)
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/// For a runtime that is going away. The client outlives it, so what is torn down is everything
|
|
100
|
+
/// that pointed at this runtime: its streams, and the token asks it was sent.
|
|
101
|
+
public func invalidate() async {
|
|
102
|
+
audience.close()
|
|
103
|
+
subscriptions.close()
|
|
104
|
+
await bridge.broker.invalidate(audience)
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
/// Visible for tests.
|
|
108
|
+
var liveSubscriptions: Int { subscriptions.count }
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/// What the framework layer turns into a rejected promise.
|
|
112
|
+
public struct BridgeRejection: Error, Sendable {
|
|
113
|
+
public let payload: TerraErrorPayload
|
|
114
|
+
|
|
115
|
+
public var code: String { payload.code }
|
|
116
|
+
|
|
117
|
+
/// The payload as the wire spells it, for a layer that rejects with a JSON string.
|
|
118
|
+
public var json: String {
|
|
119
|
+
(try? String(decoding: BridgeDispatch.makeEncoder().encode(payload), as: UTF8.self))
|
|
120
|
+
?? #"{"code":"\#(payload.code)","message":"","retryable":\#(payload.retryable)}"#
|
|
121
|
+
}
|
|
122
|
+
}
|