@logbrew/react-native 0.1.10 → 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.
- package/LogBrewReactNative.podspec +25 -3
- package/README.md +174 -4
- package/android/src/main/java/co/logbrew/reactnative/EventRecordStore.java +611 -0
- package/android/src/main/java/co/logbrew/reactnative/FatalStoreModuleImpl.java +129 -0
- package/android/src/newarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/android/src/oldarch/java/co/logbrew/reactnative/FatalStoreModule.java +26 -0
- package/apple-native-diagnostics.d.ts +52 -0
- package/apple-native-diagnostics.js +286 -0
- package/expo.cjs +46 -0
- package/expo.d.ts +16 -0
- package/expo.js +5 -0
- package/fatal-replay.cjs +12 -1
- package/global-errors.cjs +3 -1
- package/index.cjs +86 -2
- package/index.d.cts +7 -0
- package/index.d.ts +7 -0
- package/index.js +86 -2
- package/index.native.d.ts +47 -1
- package/index.native.js +81 -1
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.h +4 -0
- package/ios/AppleDiagnostics/LBRNAppleDiagnosticsModule.mm +56 -0
- package/ios/AppleDiagnostics/LBRNAppleNativeDiagnostics.swift +293 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/AutomaticTelemetryContext.swift +72 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngine.swift +270 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineAutomatic.swift +331 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineDurable.swift +148 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryEngineQueue.swift +343 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DeliveryLifecycle.swift +70 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStore.swift +258 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/DurableDeliveryStoreRecovery.swift +330 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/EventEncoding.swift +114 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/IssueDiagnostics.swift +385 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LifecycleTrace.swift +138 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewClient.swift +358 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewLogger.swift +262 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/LogBrewTrace.swift +357 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Metadata.swift +86 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/NativeStackFrame.swift +27 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/ProductTimeline.swift +228 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/PublicTypes.swift +334 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContext.swift +197 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TelemetryContextValidation.swift +337 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/TraceEvidence.swift +79 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Transport.swift +204 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTrace.swift +277 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/URLSessionTracer.swift +151 -0
- package/ios/GeneratedAppleDiagnostics/LogBrew/Validation.swift +165 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashEngine.swift +125 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashReportSanitizer.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/CrashStorageDirectory.swift +124 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeArtifactIdentity.swift +137 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashCapture.swift +229 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashPublic.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeCrashReplay.swift +317 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangIncidentStore.swift +321 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdog.swift +386 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogContracts.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeHangWatchdogSystem.swift +139 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeMainThreadStackCapture.swift +84 -0
- package/ios/GeneratedAppleDiagnostics/LogBrewCrash/NativeStackFrameSanitizer.swift +170 -0
- package/ios/GeneratedAppleDiagnostics/SOURCE-MANIFEST.json +43 -0
- package/ios/LBRNEventRecordStore.h +29 -0
- package/ios/LBRNEventRecordStore.m +746 -0
- package/ios/LBRNFatalStoreModule.mm +159 -14
- package/ios/LBRNPrivateStorage.h +7 -0
- package/ios/LBRNPrivateStorage.m +38 -0
- package/package.json +47 -4
- package/persistent-delivery.native.js +282 -0
- package/promise-rejections.cjs +3 -1
- package/scripts/sync-apple-native-sources.mjs +93 -0
- package/src/NativeLogBrewAppleDiagnostics.ts +12 -0
- package/src/NativeLogBrewFatalStore.ts +9 -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
|
+
}
|