@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,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
+ }