@logbrew/react-native 0.1.11 → 0.1.12

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 (61) hide show
  1. package/LogBrewReactNative.podspec +25 -3
  2. package/README.md +106 -3
  3. package/apple-native-diagnostics.d.ts +52 -0
  4. package/apple-native-diagnostics.js +286 -0
  5. package/expo.cjs +46 -0
  6. package/expo.d.ts +16 -0
  7. package/expo.js +5 -0
  8. package/fatal-replay.cjs +12 -1
  9. package/global-errors.cjs +3 -1
  10. package/index.cjs +84 -2
  11. package/index.d.cts +4 -0
  12. package/index.d.ts +4 -0
  13. package/index.js +84 -2
  14. package/index.native.d.ts +4 -0
  15. package/index.native.js +14 -1
  16. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
  17. package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
  18. package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
  19. package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
  20. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
  21. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
  22. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
  23. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
  24. package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
  25. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
  26. package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
  27. package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
  28. package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
  29. package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
  30. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
  31. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
  32. package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
  33. package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
  34. package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
  35. package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
  36. package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
  37. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
  38. package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
  39. package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
  40. package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
  41. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
  42. package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
  43. package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
  44. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
  45. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
  46. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
  47. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
  48. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
  49. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
  50. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
  51. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
  52. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
  53. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
  54. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
  55. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
  56. package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
  57. package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
  58. package/package.json +42 -4
  59. package/promise-rejections.cjs +3 -1
  60. package/scripts/sync-apple-native-sources.mjs +93 -0
  61. package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
@@ -0,0 +1,293 @@
1
+ import Foundation
2
+
3
+ @objc(LBRNAppleNativeDiagnostics)
4
+ public final class LBRNAppleNativeDiagnostics: NSObject, @unchecked Sendable {
5
+ private static let shared = LBRNAppleNativeDiagnostics()
6
+ private static let sdkVersion = "0.1.12"
7
+
8
+ private let lock = NSLock()
9
+ private let replayQueue = DispatchQueue(label: "co.logbrew.react-native.apple-diagnostics-replay")
10
+ private var capture: NativeCrashCapture?
11
+ private var client: LogBrewClient?
12
+ private var transport: HTTPTransport?
13
+ private var installedConfiguration: InstalledConfiguration?
14
+
15
+ @objc(installWithConfiguration:)
16
+ public static func install(with configuration: NSDictionary) -> NSDictionary {
17
+ if Thread.isMainThread {
18
+ return shared.installOnMainThread(configuration)
19
+ }
20
+ var result: NSDictionary = ["status": "native_diagnostics_failed"]
21
+ DispatchQueue.main.sync {
22
+ result = shared.installOnMainThread(configuration)
23
+ }
24
+ return result
25
+ }
26
+
27
+ @objc(replayWithCompletion:)
28
+ public static func replay(withCompletion completion: @escaping (NSDictionary) -> Void) {
29
+ shared.replayQueue.async {
30
+ let result = shared.replayPendingReports()
31
+ DispatchQueue.main.async {
32
+ completion(result)
33
+ }
34
+ }
35
+ }
36
+
37
+ @objc(status)
38
+ public static func status() -> NSDictionary {
39
+ shared.currentStatus()
40
+ }
41
+
42
+ private func installOnMainThread(_ rawConfiguration: NSDictionary) -> NSDictionary {
43
+ guard let configuration = InstalledConfiguration(rawConfiguration) else {
44
+ return failure("native_diagnostics_invalid_configuration")
45
+ }
46
+
47
+ lock.lock()
48
+ defer { lock.unlock() }
49
+
50
+ if let installedConfiguration {
51
+ guard installedConfiguration == configuration else {
52
+ return failure("native_diagnostics_already_installed")
53
+ }
54
+ return captureStatus(status: "already_installed")
55
+ }
56
+
57
+ do {
58
+ let storageDirectory = try Self.prepareStorageDirectory(
59
+ projectId: configuration.projectId,
60
+ )
61
+ let identity = try NativeArtifactIdentity(
62
+ projectId: configuration.projectId,
63
+ release: configuration.release,
64
+ environment: configuration.environment,
65
+ service: configuration.service,
66
+ )
67
+ let watchdog = try configuration.hangThresholdSeconds.map {
68
+ try NativeHangWatchdogConfiguration(threshold: $0)
69
+ }
70
+ let nativeConfiguration = try NativeCrashConfiguration(
71
+ storageDirectory: storageDirectory,
72
+ artifactIdentity: identity,
73
+ hangWatchdog: watchdog,
74
+ )
75
+ let capture = NativeCrashCapture(configuration: nativeConfiguration)
76
+ let client = try LogBrewClient.create(
77
+ apiKey: configuration.apiKey,
78
+ sdkName: "logbrew-react-native-apple",
79
+ sdkVersion: Self.sdkVersion,
80
+ )
81
+ let transport = try HTTPTransport(endpoint: configuration.endpoint)
82
+
83
+ try capture.install()
84
+ self.capture = capture
85
+ self.client = client
86
+ self.transport = transport
87
+ installedConfiguration = configuration
88
+ return captureStatus(status: "installed")
89
+ } catch let error as NativeCrashError {
90
+ return failure(error.code.rawValue)
91
+ } catch {
92
+ return failure("native_diagnostics_failed")
93
+ }
94
+ }
95
+
96
+ private func replayPendingReports() -> NSDictionary {
97
+ lock.lock()
98
+ guard let capture, let client, let transport else {
99
+ lock.unlock()
100
+ return failure("native_diagnostics_not_installed")
101
+ }
102
+ lock.unlock()
103
+
104
+ do {
105
+ let result = try capture.replayPendingReports(in: client, transport: transport)
106
+ return [
107
+ "acknowledged": result.acknowledged,
108
+ "attempted": result.attempted,
109
+ "discarded": result.discarded,
110
+ "pending": result.pending,
111
+ "status": "replayed",
112
+ ]
113
+ } catch let error as NativeCrashError {
114
+ return failure(error.code.rawValue)
115
+ } catch {
116
+ return failure("native_diagnostics_failed")
117
+ }
118
+ }
119
+
120
+ private func currentStatus() -> NSDictionary {
121
+ lock.lock()
122
+ defer { lock.unlock() }
123
+ guard capture != nil else {
124
+ return [
125
+ "acknowledged": 0,
126
+ "discarded": 0,
127
+ "lifecycle": "idle",
128
+ "pending": 0,
129
+ "status": "not_installed",
130
+ ]
131
+ }
132
+ return captureStatus(status: "ready")
133
+ }
134
+
135
+ private func captureStatus(status: String) -> NSDictionary {
136
+ guard let capture else {
137
+ return failure("native_diagnostics_not_installed")
138
+ }
139
+ do {
140
+ let value = try capture.status()
141
+ return [
142
+ "acknowledged": value.acknowledged,
143
+ "discarded": value.discarded,
144
+ "lifecycle": value.lifecycle.logBrewName,
145
+ "pending": value.pending,
146
+ "status": status,
147
+ ]
148
+ } catch let error as NativeCrashError {
149
+ return failure(error.code.rawValue)
150
+ } catch {
151
+ return failure("native_diagnostics_failed")
152
+ }
153
+ }
154
+
155
+ private func failure(_ code: String) -> NSDictionary {
156
+ ["code": code, "status": "error"]
157
+ }
158
+
159
+ private static func prepareStorageDirectory(projectId: String) throws -> URL {
160
+ let fileManager = FileManager.default
161
+ guard let applicationSupport = fileManager.urls(
162
+ for: .applicationSupportDirectory,
163
+ in: .userDomainMask,
164
+ ).first else {
165
+ throw NativeCrashError(.storageUnsupported)
166
+ }
167
+ do {
168
+ try fileManager.createDirectory(
169
+ at: applicationSupport,
170
+ withIntermediateDirectories: true,
171
+ attributes: nil,
172
+ )
173
+ } catch {
174
+ throw NativeCrashError(.storageUnsupported)
175
+ }
176
+ return applicationSupport.appendingPathComponent(
177
+ "LogBrewAppleDiagnostics-\(projectId)",
178
+ isDirectory: true,
179
+ )
180
+ }
181
+ }
182
+
183
+ private struct InstalledConfiguration: Equatable {
184
+ private static let allowedKeys = Set([
185
+ "apiKey",
186
+ "endpoint",
187
+ "environment",
188
+ "fatalHandlerOwnership",
189
+ "hangThresholdSeconds",
190
+ "projectId",
191
+ "release",
192
+ "service",
193
+ ])
194
+
195
+ let apiKey: String
196
+ let endpoint: URL
197
+ let environment: String
198
+ let hangThresholdSeconds: TimeInterval?
199
+ let projectId: String
200
+ let release: String
201
+ let service: String
202
+
203
+ init?(_ values: NSDictionary) {
204
+ let keys = Set(values.allKeys.compactMap { $0 as? String })
205
+ guard keys.count == values.count,
206
+ keys.isSubset(of: Self.allowedKeys),
207
+ values["fatalHandlerOwnership"] as? String == "logbrew",
208
+ let apiKey = Self.exactString(values["apiKey"], maxBytes: 4096),
209
+ let projectId = Self.exactString(values["projectId"], maxBytes: 64),
210
+ let release = Self.exactString(values["release"]),
211
+ let environment = Self.exactString(values["environment"]),
212
+ let service = Self.exactString(values["service"])
213
+ else {
214
+ return nil
215
+ }
216
+
217
+ let endpoint: URL
218
+ if let endpointValue = values["endpoint"] {
219
+ guard let parsedEndpoint = Self.exactDeliveryEndpoint(endpointValue)
220
+ else {
221
+ return nil
222
+ }
223
+ endpoint = parsedEndpoint
224
+ } else {
225
+ endpoint = HTTPTransport.defaultEndpoint
226
+ }
227
+
228
+ let hangThresholdSeconds: TimeInterval?
229
+ if let thresholdValue = values["hangThresholdSeconds"] {
230
+ guard let number = thresholdValue as? NSNumber,
231
+ CFGetTypeID(number) != CFBooleanGetTypeID(),
232
+ number.doubleValue.isFinite,
233
+ (1 ... 30).contains(number.doubleValue)
234
+ else {
235
+ return nil
236
+ }
237
+ hangThresholdSeconds = number.doubleValue
238
+ } else {
239
+ hangThresholdSeconds = nil
240
+ }
241
+
242
+ self.apiKey = apiKey
243
+ self.endpoint = endpoint
244
+ self.environment = environment
245
+ self.hangThresholdSeconds = hangThresholdSeconds
246
+ self.projectId = projectId
247
+ self.release = release
248
+ self.service = service
249
+ }
250
+
251
+ private static func exactString(_ value: Any?, maxBytes: Int = 256) -> String? {
252
+ guard let value = value as? String,
253
+ (1 ... maxBytes).contains(value.utf8.count),
254
+ value == value.trimmingCharacters(in: .whitespacesAndNewlines),
255
+ !value.unicodeScalars.contains(where: CharacterSet.controlCharacters.contains)
256
+ else {
257
+ return nil
258
+ }
259
+ return value
260
+ }
261
+
262
+ private static func exactDeliveryEndpoint(_ value: Any?) -> URL? {
263
+ guard let endpoint = exactString(value, maxBytes: 2_048),
264
+ let components = URLComponents(string: endpoint),
265
+ components.scheme == "https",
266
+ components.host?.isEmpty == false,
267
+ components.path.starts(with: "/"),
268
+ components.user == nil,
269
+ components.password == nil,
270
+ components.query == nil,
271
+ components.fragment == nil,
272
+ let url = components.url,
273
+ url.absoluteString == endpoint,
274
+ url.standardized.absoluteString == endpoint
275
+ else {
276
+ return nil
277
+ }
278
+ return url
279
+ }
280
+ }
281
+
282
+ private extension NativeCrashLifecycleState {
283
+ var logBrewName: String {
284
+ switch self {
285
+ case .idle: "idle"
286
+ case .installed: "installed"
287
+ case .replaying: "replaying"
288
+ case .failed: "failed"
289
+ case .stopped: "stopped"
290
+ @unknown default: "unknown"
291
+ }
292
+ }
293
+ }
@@ -0,0 +1,72 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Foundation
4
+
5
+ func automaticTelemetryContext() -> TelemetryContext {
6
+ let operatingSystem = ProcessInfo.processInfo.operatingSystemVersion
7
+ let version = "\(operatingSystem.majorVersion).\(operatingSystem.minorVersion).\(operatingSystem.patchVersion)"
8
+ let application = automaticApplicationContext()
9
+ let architecture = automaticArchitecture()
10
+ return TelemetryContext(
11
+ resource: TelemetryResource(
12
+ runtime: TelemetryNamedVersion(name: "swift"),
13
+ operatingSystem: TelemetryOperatingSystem(name: automaticOperatingSystemName(), version: version),
14
+ device: architecture.map { TelemetryDevice(architecture: $0) },
15
+ application: application,
16
+ ),
17
+ )
18
+ }
19
+
20
+ private func automaticOperatingSystemName() -> String {
21
+ #if os(iOS)
22
+ "iOS"
23
+ #elseif os(macOS)
24
+ "macOS"
25
+ #elseif os(tvOS)
26
+ "tvOS"
27
+ #elseif os(watchOS)
28
+ "watchOS"
29
+ #elseif os(visionOS)
30
+ "visionOS"
31
+ #else
32
+ "unknown"
33
+ #endif
34
+ }
35
+
36
+ private func automaticArchitecture() -> String? {
37
+ #if arch(arm64)
38
+ "arm64"
39
+ #elseif arch(x86_64)
40
+ "x86_64"
41
+ #elseif arch(arm)
42
+ "arm"
43
+ #elseif arch(i386)
44
+ "i386"
45
+ #else
46
+ nil
47
+ #endif
48
+ }
49
+
50
+ private func automaticApplicationContext() -> TelemetryApplication? {
51
+ let bundle = Bundle.main
52
+ let name = automaticBundleValue(bundle, keys: ["CFBundleDisplayName", "CFBundleName"])
53
+ let version = automaticBundleValue(bundle, keys: ["CFBundleShortVersionString"])
54
+ let build = automaticBundleValue(bundle, keys: ["CFBundleVersion"])
55
+ guard name != nil || version != nil || build != nil else {
56
+ return nil
57
+ }
58
+ return TelemetryApplication(name: name, version: version, build: build)
59
+ }
60
+
61
+ private func automaticBundleValue(_ bundle: Bundle, keys: [String]) -> String? {
62
+ for key in keys {
63
+ guard let candidate = bundle.object(forInfoDictionaryKey: key) as? String else {
64
+ continue
65
+ }
66
+ let normalized = candidate.trimmingCharacters(in: .whitespacesAndNewlines)
67
+ if !normalized.isEmpty, normalized.count <= 256, !containsForbiddenControl(normalized) {
68
+ return normalized
69
+ }
70
+ }
71
+ return nil
72
+ }
@@ -0,0 +1,270 @@
1
+ // Generated by scripts/sync-apple-native-sources.mjs.
2
+ // Edit the canonical Swift source, then regenerate this package boundary.
3
+ import Foundation
4
+
5
+ final class DeliveryEngine: @unchecked Sendable {
6
+ static let maxQueuedEvents = 1000
7
+ static let maxQueuedBytes = 4 * 1024 * 1024
8
+ static let maxRequestEvents = 100
9
+ static let maxRequestBytes = 256 * 1024
10
+ static let maxScheduleDelay: TimeInterval = 24 * 60 * 60
11
+
12
+ struct QueuedEvent {
13
+ let event: Event
14
+ let encodedBytes: Int
15
+ let durableRecordName: String?
16
+ }
17
+
18
+ struct FrozenPrefix {
19
+ let count: Int
20
+ let bytes: Int
21
+ let body: Data
22
+ let eventIDs: [String]
23
+ let durableRecordNames: [String]
24
+ }
25
+
26
+ enum AttemptResult {
27
+ case accepted(statusCode: Int, attempts: Int)
28
+ case retryable(attempts: Int, error: SdkError)
29
+ case terminal(reason: DeliveryPauseReason, attempts: Int, error: SdkError)
30
+
31
+ var attempts: Int {
32
+ switch self {
33
+ case let .accepted(_, attempts), let .retryable(attempts, _), let .terminal(_, attempts, _):
34
+ attempts
35
+ }
36
+ }
37
+ }
38
+
39
+ let apiKey: String
40
+ let sdk: SDKInfo
41
+ let maxRetries: Int
42
+ let stateLock = NSLock()
43
+ let flushLock = NSLock()
44
+ let storageLock = NSLock()
45
+ var queue: [QueuedEvent] = []
46
+ var queuedBytes = 0
47
+ var frozenPrefix: FrozenPrefix?
48
+ var durableStore: DurableDeliveryStore?
49
+ var durableParent: URL?
50
+ var closed = false
51
+
52
+ var state: DeliveryState = .manual
53
+ var lastOutcome: DeliveryOutcome = .none
54
+ var pauseReason: DeliveryPauseReason = .none
55
+ var inFlight = false
56
+ var acceptedEvents = 0
57
+ var droppedEvents = 0
58
+ var deliveryAttempts = 0
59
+ var consecutiveFailures = 0
60
+
61
+ var automaticTransport: (any Transport)?
62
+ var automaticOptions: AutomaticDeliveryOptions?
63
+ var schedulerQueue: DispatchQueue?
64
+ var schedulerTimer: DispatchSourceTimer?
65
+ var nextWakeNanoseconds: UInt64?
66
+ var generation: UInt64 = 0
67
+ var retryAttempt = 0
68
+ let schedulerKey = DispatchSpecificKey<UInt8>()
69
+
70
+ init(apiKey: String, sdk: SDKInfo, maxRetries: Int) {
71
+ self.apiKey = apiKey
72
+ self.sdk = sdk
73
+ self.maxRetries = max(0, maxRetries)
74
+ }
75
+
76
+ deinit {
77
+ schedulerTimer?.setEventHandler {}
78
+ schedulerTimer?.cancel()
79
+ }
80
+
81
+ func pendingEvents() -> Int {
82
+ withStateLock { queue.count }
83
+ }
84
+
85
+ func previewJSON() throws -> String {
86
+ let events = withStateLock { queue.map(\.event) }
87
+ let data = try encodeBatch(events)
88
+ guard let json = String(data: data, encoding: .utf8) else {
89
+ throw SdkError(code: "encoding_error", message: "event batch was not valid UTF-8")
90
+ }
91
+ return json
92
+ }
93
+
94
+ func health() -> DeliveryHealth {
95
+ withStateLock {
96
+ DeliveryHealth(
97
+ state: state,
98
+ queuedEvents: queue.count,
99
+ queuedBytes: queuedBytes,
100
+ inFlight: inFlight,
101
+ acceptedEvents: acceptedEvents,
102
+ droppedEvents: droppedEvents,
103
+ deliveryAttempts: deliveryAttempts,
104
+ consecutiveFailures: consecutiveFailures,
105
+ lastOutcome: lastOutcome,
106
+ pauseReason: pauseReason,
107
+ )
108
+ }
109
+ }
110
+
111
+ func startAutomaticDelivery(transport: any Transport, options: AutomaticDeliveryOptions) throws {
112
+ try validate(options)
113
+
114
+ storageLock.lock()
115
+ stateLock.lock()
116
+ if closed || state == .closed || state == .shuttingDown {
117
+ stateLock.unlock()
118
+ storageLock.unlock()
119
+ throw SdkError(code: "shutdown_error", message: "client is already shut down")
120
+ }
121
+ if automaticTransport != nil {
122
+ stateLock.unlock()
123
+ storageLock.unlock()
124
+ throw SdkError(code: "configuration_error", message: "automatic delivery is already running")
125
+ }
126
+ if state == .paused, pauseReason == .storage {
127
+ stateLock.unlock()
128
+ storageLock.unlock()
129
+ throw SdkError(code: "storage_corrupt", message: "durable delivery data requires explicit recovery")
130
+ }
131
+ generation &+= 1
132
+ let ownerGeneration = generation
133
+ let scheduler = DispatchQueue(label: "co.logbrew.swift.delivery", qos: .utility)
134
+ scheduler.setSpecific(key: schedulerKey, value: 1)
135
+ let timer = DispatchSource.makeTimerSource(queue: scheduler)
136
+ timer.setEventHandler { [weak self] in
137
+ self?.timerFired(generation: ownerGeneration)
138
+ }
139
+ automaticTransport = transport
140
+ automaticOptions = options
141
+ schedulerQueue = scheduler
142
+ schedulerTimer = timer
143
+ state = .running
144
+ pauseReason = .none
145
+ retryAttempt = 0
146
+ let shouldSchedule = scheduleLiveQueueLocked()
147
+ stateLock.unlock()
148
+ storageLock.unlock()
149
+
150
+ timer.resume()
151
+ if shouldSchedule {
152
+ scheduleTimerUpdate()
153
+ }
154
+ }
155
+
156
+ func recoverAutomaticDelivery() throws {
157
+ stateLock.lock()
158
+ guard automaticTransport != nil, automaticOptions != nil else {
159
+ stateLock.unlock()
160
+ throw SdkError(code: "configuration_error", message: "automatic delivery is not running")
161
+ }
162
+ guard state == .paused else {
163
+ stateLock.unlock()
164
+ return
165
+ }
166
+ if pauseReason == .storage {
167
+ stateLock.unlock()
168
+ throw SdkError(code: "storage_corrupt", message: "durable delivery data requires explicit recovery")
169
+ }
170
+ state = .running
171
+ pauseReason = .none
172
+ retryAttempt = 0
173
+ consecutiveFailures = 0
174
+ nextWakeNanoseconds = nil
175
+ let shouldSchedule = scheduleLiveQueueLocked()
176
+ stateLock.unlock()
177
+ if shouldSchedule {
178
+ scheduleTimerUpdate()
179
+ }
180
+ }
181
+
182
+ func stopAutomaticDelivery() {
183
+ let timer: DispatchSourceTimer?
184
+ let scheduler: DispatchQueue?
185
+ storageLock.lock()
186
+ stateLock.lock()
187
+ generation &+= 1
188
+ timer = schedulerTimer
189
+ scheduler = schedulerQueue
190
+ schedulerTimer = nil
191
+ schedulerQueue = nil
192
+ nextWakeNanoseconds = nil
193
+ automaticTransport = nil
194
+ automaticOptions = nil
195
+ retryAttempt = 0
196
+ pauseReason = .none
197
+ if !closed, state != .shuttingDown {
198
+ state = .manual
199
+ }
200
+ stateLock.unlock()
201
+ storageLock.unlock()
202
+
203
+ timer?.setEventHandler {}
204
+ timer?.cancel()
205
+ if DispatchQueue.getSpecific(key: schedulerKey) == nil {
206
+ scheduler?.sync {}
207
+ flushLock.lock()
208
+ flushLock.unlock()
209
+ }
210
+ }
211
+
212
+ func flush(transport: any Transport) throws -> TransportResponse {
213
+ try withManualDelivery {
214
+ try flushAll(transport: transport)
215
+ }
216
+ }
217
+
218
+ func flushEvent(_ eventID: String, transport: any Transport) throws -> Bool {
219
+ try withManualDelivery {
220
+ try flushThroughEvent(eventID, transport: transport)
221
+ }
222
+ }
223
+
224
+ private func withManualDelivery<Result>(
225
+ _ operation: () throws -> Result,
226
+ ) throws -> Result {
227
+ try prepareManualOperation()
228
+ flushLock.lock()
229
+ defer {
230
+ flushLock.unlock()
231
+ rescheduleAfterManualOperation()
232
+ }
233
+ return try operation()
234
+ }
235
+
236
+ func flushOwnedTransport() throws -> TransportResponse {
237
+ let transport = try ownedTransport()
238
+ return try flush(transport: transport)
239
+ }
240
+
241
+ func shutdown(transport: any Transport) throws -> TransportResponse {
242
+ let priorAutomatic = try beginShutdown()
243
+ flushLock.lock()
244
+ do {
245
+ let response = try flushAll(transport: transport)
246
+ stateLock.lock()
247
+ closed = true
248
+ state = .closed
249
+ inFlight = false
250
+ stateLock.unlock()
251
+ flushLock.unlock()
252
+ finishStopping(timer: priorAutomatic.timer, scheduler: priorAutomatic.scheduler)
253
+ return response
254
+ } catch {
255
+ stateLock.lock()
256
+ state = pauseReason == .storage ? .paused : .manual
257
+ closed = false
258
+ inFlight = false
259
+ stateLock.unlock()
260
+ flushLock.unlock()
261
+ finishStopping(timer: priorAutomatic.timer, scheduler: priorAutomatic.scheduler)
262
+ throw error
263
+ }
264
+ }
265
+
266
+ func shutdownOwnedTransport() throws -> TransportResponse {
267
+ let transport = try ownedTransport()
268
+ return try shutdown(transport: transport)
269
+ }
270
+ }