@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,343 @@
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
+ extension DeliveryEngine {
6
+ func enqueue(_ event: Event) throws {
7
+ let eventBytes = try encodeEvent(event).count
8
+ let singleRequestBytes = try encodeBatch([event]).count
9
+ var shouldSchedule = false
10
+ var admissionError: SdkError?
11
+ var durableRecordName: String?
12
+
13
+ storageLock.lock()
14
+ defer { storageLock.unlock() }
15
+ stateLock.lock()
16
+ if closed || state == .shuttingDown || state == .closed {
17
+ admissionError = SdkError(code: "shutdown_error", message: "client is already shut down")
18
+ } else if state == .paused, pauseReason == .storage {
19
+ admissionError = SdkError(
20
+ code: "storage_corrupt",
21
+ message: "durable delivery data requires explicit recovery",
22
+ )
23
+ } else if singleRequestBytes > Self.maxRequestBytes {
24
+ droppedEvents += 1
25
+ lastOutcome = .dropped
26
+ admissionError = SdkError(code: "event_too_large", message: "event exceeds the delivery request byte limit")
27
+ } else if queue.count >= Self.maxQueuedEvents || queuedBytes > Self.maxQueuedBytes - eventBytes {
28
+ droppedEvents += 1
29
+ lastOutcome = .dropped
30
+ admissionError = SdkError(code: "queue_full", message: "delivery queue capacity exceeded")
31
+ } else {
32
+ let store = durableStore
33
+ stateLock.unlock()
34
+ do {
35
+ durableRecordName = try store?.append(event, encodedBytes: eventBytes)
36
+ } catch {
37
+ recordStorageFailure()
38
+ throw storageError(error)
39
+ }
40
+ stateLock.lock()
41
+ queue.append(
42
+ QueuedEvent(
43
+ event: event,
44
+ encodedBytes: eventBytes,
45
+ durableRecordName: durableRecordName,
46
+ ),
47
+ )
48
+ queuedBytes += eventBytes
49
+ shouldSchedule = scheduleCaptureLocked()
50
+ }
51
+ stateLock.unlock()
52
+
53
+ if shouldSchedule {
54
+ scheduleTimerUpdate()
55
+ }
56
+ if let admissionError {
57
+ throw admissionError
58
+ }
59
+ }
60
+
61
+ func flushAll(transport: any Transport) throws -> TransportResponse {
62
+ var totalAttempts = 0
63
+ var statusCode = 204
64
+
65
+ while let prefix = try freezePrefix() {
66
+ let response = try deliver(prefix, transport: transport)
67
+ totalAttempts += response.attempts
68
+ statusCode = response.statusCode
69
+ }
70
+
71
+ return TransportResponse(statusCode: statusCode, attempts: totalAttempts)
72
+ }
73
+
74
+ func flushThroughEvent(
75
+ _ eventID: String,
76
+ transport: any Transport,
77
+ ) throws -> Bool {
78
+ guard withStateLock({ queue.contains { $0.event.id == eventID } }) else {
79
+ return false
80
+ }
81
+ while let prefix = try freezePrefix() {
82
+ let containsEvent = prefix.eventIDs.contains(eventID)
83
+ _ = try deliver(prefix, transport: transport)
84
+ if containsEvent {
85
+ return true
86
+ }
87
+ }
88
+ return false
89
+ }
90
+
91
+ private func deliver(
92
+ _ prefix: FrozenPrefix,
93
+ transport: any Transport,
94
+ ) throws -> TransportResponse {
95
+ var totalAttempts = 0
96
+ for attemptIndex in 0 ... maxRetries {
97
+ let result = attempt(transport: transport, prefix: prefix)
98
+ totalAttempts += result.attempts
99
+ recordManualAttempts(result.attempts)
100
+ switch result {
101
+ case let .accepted(status, _):
102
+ try acknowledge(prefix)
103
+ return TransportResponse(statusCode: status, attempts: totalAttempts)
104
+ case .retryable where attemptIndex < maxRetries:
105
+ continue
106
+ case let .retryable(_, error):
107
+ recordManualFailure(outcome: .retryableFailure, pauseReason: .retryExhausted)
108
+ throw error
109
+ case let .terminal(reason, _, error):
110
+ recordManualFailure(outcome: .terminalFailure, pauseReason: reason)
111
+ throw error
112
+ }
113
+ }
114
+ throw SdkError(code: "transport_error", message: "exhausted retries")
115
+ }
116
+
117
+ func freezePrefix() throws -> FrozenPrefix? {
118
+ storageLock.lock()
119
+ defer { storageLock.unlock() }
120
+
121
+ stateLock.lock()
122
+ if let frozenPrefix {
123
+ inFlight = true
124
+ stateLock.unlock()
125
+ return frozenPrefix
126
+ }
127
+ guard !queue.isEmpty else {
128
+ stateLock.unlock()
129
+ return nil
130
+ }
131
+ let queueSnapshot = queue
132
+ let store = durableStore
133
+ stateLock.unlock()
134
+
135
+ let prefix = try makePrefix(queueSnapshot: queueSnapshot, store: store)
136
+ stateLock.lock()
137
+ frozenPrefix = prefix
138
+ inFlight = true
139
+ stateLock.unlock()
140
+ return prefix
141
+ }
142
+
143
+ private func makePrefix(
144
+ queueSnapshot: [QueuedEvent],
145
+ store: DurableDeliveryStore?,
146
+ ) throws -> FrozenPrefix {
147
+ var selected: [Event] = []
148
+ var body = Data()
149
+ for queuedEvent in queueSnapshot.prefix(Self.maxRequestEvents) {
150
+ let candidate = selected + [queuedEvent.event]
151
+ let candidateBody = try encodeBatch(candidate)
152
+ if candidateBody.count > Self.maxRequestBytes {
153
+ break
154
+ }
155
+ selected = candidate
156
+ body = candidateBody
157
+ }
158
+ guard !selected.isEmpty else {
159
+ throw SdkError(code: "event_too_large", message: "event exceeds the delivery request byte limit")
160
+ }
161
+ let selectedQueue = Array(queueSnapshot.prefix(selected.count))
162
+ let prefixBytes = selectedQueue.reduce(0) { $0 + $1.encodedBytes }
163
+ let recordNames = selectedQueue.compactMap(\.durableRecordName)
164
+ if let store {
165
+ guard recordNames.count == selected.count else {
166
+ recordStorageFailure()
167
+ throw storageError(DurableStoreFailure.corrupt)
168
+ }
169
+ do {
170
+ try store.persistPrefix(body: body, eventRecordNames: recordNames, encodedBytes: prefixBytes)
171
+ } catch {
172
+ recordStorageFailure()
173
+ throw storageError(error)
174
+ }
175
+ }
176
+ return FrozenPrefix(
177
+ count: selected.count,
178
+ bytes: prefixBytes,
179
+ body: body,
180
+ eventIDs: selected.map(\.id),
181
+ durableRecordNames: recordNames,
182
+ )
183
+ }
184
+
185
+ func acknowledge(_ prefix: FrozenPrefix) throws {
186
+ do {
187
+ try acknowledgeDurableFiles(prefix)
188
+ } catch {
189
+ recordStorageFailure()
190
+ throw error
191
+ }
192
+ stateLock.lock()
193
+ acknowledgeLocked(prefix)
194
+ acceptedEvents += prefix.count
195
+ consecutiveFailures = 0
196
+ lastOutcome = .accepted
197
+ stateLock.unlock()
198
+ }
199
+
200
+ func acknowledgeDurableFiles(_ prefix: FrozenPrefix) throws {
201
+ storageLock.lock()
202
+ defer { storageLock.unlock() }
203
+ let store = withStateLock { durableStore }
204
+ guard let store else {
205
+ return
206
+ }
207
+ do {
208
+ try store.acknowledge(body: prefix.body, eventRecordNames: prefix.durableRecordNames)
209
+ } catch {
210
+ throw storageError(error)
211
+ }
212
+ }
213
+
214
+ func acknowledgeLocked(_ prefix: FrozenPrefix) {
215
+ guard frozenPrefix?.body == prefix.body, queue.count >= prefix.count else {
216
+ return
217
+ }
218
+ queue.removeFirst(prefix.count)
219
+ queuedBytes -= prefix.bytes
220
+ frozenPrefix = nil
221
+ inFlight = false
222
+ retryAttempt = 0
223
+ }
224
+
225
+ func attempt(transport: any Transport, prefix: FrozenPrefix) -> AttemptResult {
226
+ do {
227
+ let response = try transport.send(apiKey: apiKey, body: prefix.body)
228
+ return classify(response)
229
+ } catch let transportError as TransportError {
230
+ let error = SdkError(code: transportError.code, message: transportError.message)
231
+ if transportError.retryable {
232
+ return .retryable(attempts: 1, error: error)
233
+ }
234
+ return .terminal(reason: .nonRetryable, attempts: 1, error: error)
235
+ } catch {
236
+ return .terminal(
237
+ reason: .nonRetryable,
238
+ attempts: 1,
239
+ error: SdkError(code: "transport_error", message: "unexpected transport response"),
240
+ )
241
+ }
242
+ }
243
+
244
+ private func classify(_ response: TransportResponse) -> AttemptResult {
245
+ guard response.attempts > 0 else {
246
+ return .terminal(
247
+ reason: .nonRetryable,
248
+ attempts: 1,
249
+ error: SdkError(code: "transport_error", message: "unexpected transport response"),
250
+ )
251
+ }
252
+ let attempts = max(1, response.attempts)
253
+ if (200 ..< 300).contains(response.statusCode) {
254
+ return .accepted(statusCode: response.statusCode, attempts: attempts)
255
+ }
256
+ let error = transportError(statusCode: response.statusCode)
257
+ if response.statusCode == 408 || response.statusCode >= 500 {
258
+ return .retryable(attempts: attempts, error: error)
259
+ }
260
+ return .terminal(reason: pauseReason(for: response.statusCode), attempts: attempts, error: error)
261
+ }
262
+
263
+ private func transportError(statusCode: Int) -> SdkError {
264
+ if statusCode == 401 {
265
+ return SdkError(code: "unauthenticated", message: "transport rejected the API key")
266
+ }
267
+ return SdkError(code: "transport_error", message: "unexpected transport status \(statusCode)")
268
+ }
269
+
270
+ private func pauseReason(for statusCode: Int) -> DeliveryPauseReason {
271
+ switch statusCode {
272
+ case 401, 403:
273
+ .authentication
274
+ case 429:
275
+ .quota
276
+ case 400, 404, 409, 413, 422:
277
+ .validation
278
+ default:
279
+ .nonRetryable
280
+ }
281
+ }
282
+
283
+ func recordManualFailure(outcome: DeliveryOutcome, pauseReason reason: DeliveryPauseReason) {
284
+ stateLock.lock()
285
+ inFlight = false
286
+ consecutiveFailures += 1
287
+ lastOutcome = outcome
288
+ if automaticTransport != nil, state != .shuttingDown, state != .closed {
289
+ state = .paused
290
+ pauseReason = reason
291
+ nextWakeNanoseconds = nil
292
+ }
293
+ stateLock.unlock()
294
+ }
295
+
296
+ private func recordManualAttempts(_ attempts: Int) {
297
+ stateLock.lock()
298
+ deliveryAttempts += attempts
299
+ stateLock.unlock()
300
+ }
301
+
302
+ func encodeEvent(_ event: Event) throws -> Data {
303
+ let encoder = JSONEncoder()
304
+ encoder.outputFormatting = [.sortedKeys]
305
+ return try encoder.encode(event)
306
+ }
307
+
308
+ func encodeBatch(_ events: [Event]) throws -> Data {
309
+ let encoder = JSONEncoder()
310
+ encoder.outputFormatting = [.prettyPrinted, .sortedKeys]
311
+ return try encoder.encode(EventBatch(sdk: sdk, events: events))
312
+ }
313
+
314
+ func withStateLock<T>(_ operation: () -> T) -> T {
315
+ stateLock.lock()
316
+ defer { stateLock.unlock() }
317
+ return operation()
318
+ }
319
+
320
+ func recordStorageFailure() {
321
+ stateLock.lock()
322
+ inFlight = false
323
+ state = .paused
324
+ pauseReason = .storage
325
+ lastOutcome = .terminalFailure
326
+ consecutiveFailures += 1
327
+ nextWakeNanoseconds = nil
328
+ stateLock.unlock()
329
+ }
330
+
331
+ func storageError(_ error: Error) -> SdkError {
332
+ if let failure = error as? DurableStoreFailure, failure == .corrupt {
333
+ return SdkError(code: "storage_corrupt", message: "durable delivery data requires explicit recovery")
334
+ }
335
+ if let failure = error as? DurableStoreFailure, failure == .capacity {
336
+ return SdkError(code: "queue_full", message: "durable delivery capacity exceeded")
337
+ }
338
+ if let failure = error as? DurableStoreFailure, failure == .owned {
339
+ return SdkError(code: "storage_error", message: "durable delivery storage is already in use")
340
+ }
341
+ return SdkError(code: "storage_error", message: "durable delivery storage is unavailable")
342
+ }
343
+ }
@@ -0,0 +1,70 @@
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
+ public enum DeliveryState: String, Codable, Sendable {
6
+ case manual
7
+ case running
8
+ case retrying
9
+ case paused
10
+ case shuttingDown = "shutting_down"
11
+ case closed
12
+ }
13
+
14
+ public enum DeliveryOutcome: String, Codable, Sendable {
15
+ case none
16
+ case accepted
17
+ case retryableFailure = "retryable_failure"
18
+ case terminalFailure = "terminal_failure"
19
+ case dropped
20
+ }
21
+
22
+ public enum DeliveryPauseReason: String, Codable, Sendable {
23
+ case none
24
+ case authentication
25
+ case quota
26
+ case validation
27
+ case nonRetryable = "non_retryable"
28
+ case retryExhausted = "retry_exhausted"
29
+ case storage
30
+ }
31
+
32
+ public struct DurableDeliveryOptions: Equatable, Sendable {
33
+ public let directory: URL
34
+
35
+ public init(directory: URL) {
36
+ self.directory = directory
37
+ }
38
+ }
39
+
40
+ public struct AutomaticDeliveryOptions: Equatable, Sendable {
41
+ public let interval: TimeInterval
42
+ public let threshold: Int
43
+ public let retryBaseDelay: TimeInterval
44
+ public let maxRetryDelay: TimeInterval
45
+
46
+ public init(
47
+ interval: TimeInterval = 5,
48
+ threshold: Int = 100,
49
+ retryBaseDelay: TimeInterval = 0.25,
50
+ maxRetryDelay: TimeInterval = 30,
51
+ ) {
52
+ self.interval = interval
53
+ self.threshold = threshold
54
+ self.retryBaseDelay = retryBaseDelay
55
+ self.maxRetryDelay = maxRetryDelay
56
+ }
57
+ }
58
+
59
+ public struct DeliveryHealth: Codable, Equatable, Sendable {
60
+ public let state: DeliveryState
61
+ public let queuedEvents: Int
62
+ public let queuedBytes: Int
63
+ public let inFlight: Bool
64
+ public let acceptedEvents: Int
65
+ public let droppedEvents: Int
66
+ public let deliveryAttempts: Int
67
+ public let consecutiveFailures: Int
68
+ public let lastOutcome: DeliveryOutcome
69
+ public let pauseReason: DeliveryPauseReason
70
+ }
@@ -0,0 +1,258 @@
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
+ enum DurableStoreFailure: Equatable, Error {
6
+ case capacity
7
+ case corrupt
8
+ case invalidLocation
9
+ case inputOutput
10
+ case owned
11
+ }
12
+
13
+ final class DurableDeliveryStore: @unchecked Sendable {
14
+ static let directoryName = "logbrew-delivery-v1"
15
+
16
+ struct RecoveredEvent {
17
+ let event: Event
18
+ let encodedBytes: Int
19
+ let recordName: String
20
+ }
21
+
22
+ struct RecoveredPrefix {
23
+ let body: Data
24
+ let eventRecordNames: [String]
25
+ let encodedBytes: Int
26
+ }
27
+
28
+ struct Recovery {
29
+ let events: [RecoveredEvent]
30
+ let prefix: RecoveredPrefix?
31
+ }
32
+
33
+ struct EventRecord: Codable {
34
+ let version: Int
35
+ let sequence: UInt64
36
+ let sdk: SDKInfo
37
+ let encodedBytes: Int
38
+ let checksum: String
39
+ let event: Event
40
+ }
41
+
42
+ struct PrefixRecord: Codable {
43
+ let version: Int
44
+ let sdk: SDKInfo
45
+ let eventRecordNames: [String]
46
+ let encodedBytes: Int
47
+ let body: Data
48
+ }
49
+
50
+ static let version = 1
51
+ static let prefixName = "frozen-prefix.json"
52
+ static let lockName = ".lock"
53
+ static let maxEventRecordBytes = DeliveryEngine.maxRequestBytes * 2
54
+ static let maxPrefixRecordBytes = DeliveryEngine.maxRequestBytes * 2
55
+
56
+ private let fileManager: FileManager
57
+ private let directory: URL
58
+ private let sdk: SDKInfo
59
+ private let lockHandle: FileHandle
60
+ private var recoveredEvents: [RecoveredEvent]
61
+ private var recoveredPrefix: RecoveredPrefix?
62
+ private var nextSequence: UInt64
63
+ private var failed = false
64
+
65
+ init(parent: URL, sdk: SDKInfo, fileManager: FileManager = .default) throws {
66
+ self.fileManager = fileManager
67
+ self.sdk = sdk
68
+ directory = parent.appendingPathComponent(Self.directoryName, isDirectory: true)
69
+
70
+ try Self.validateParent(parent, fileManager: fileManager)
71
+ try Self.prepareOwnedDirectory(directory, fileManager: fileManager)
72
+ lockHandle = try Self.acquireLock(in: directory, fileManager: fileManager)
73
+
74
+ do {
75
+ let recovery = try Self.loadRecovery(from: directory, sdk: sdk, fileManager: fileManager)
76
+ recoveredEvents = recovery.events
77
+ recoveredPrefix = recovery.prefix
78
+ if let last = recovery.events.last {
79
+ let lastSequence = try Self.sequence(from: last)
80
+ guard lastSequence < UInt64.max else {
81
+ throw DurableStoreFailure.corrupt
82
+ }
83
+ nextSequence = lastSequence + 1
84
+ } else {
85
+ nextSequence = 1
86
+ }
87
+ } catch {
88
+ Self.releaseLock(lockHandle)
89
+ throw error
90
+ }
91
+ }
92
+
93
+ deinit {
94
+ Self.releaseLock(lockHandle)
95
+ }
96
+
97
+ func recovery() -> Recovery {
98
+ Recovery(events: recoveredEvents, prefix: recoveredPrefix)
99
+ }
100
+
101
+ func append(_ event: Event, encodedBytes: Int) throws -> String {
102
+ try requireHealthy()
103
+ guard recoveredEvents.count < DeliveryEngine.maxQueuedEvents,
104
+ recoveredEvents.reduce(0, { $0 + $1.encodedBytes }) <= DeliveryEngine.maxQueuedBytes - encodedBytes
105
+ else {
106
+ throw DurableStoreFailure.capacity
107
+ }
108
+ guard nextSequence < UInt64.max else {
109
+ throw DurableStoreFailure.corrupt
110
+ }
111
+ let name = Self.eventName(sequence: nextSequence)
112
+ let eventData = try Self.encodeEvent(event)
113
+ guard eventData.count == encodedBytes else {
114
+ throw DurableStoreFailure.corrupt
115
+ }
116
+ let record = EventRecord(
117
+ version: Self.version,
118
+ sequence: nextSequence,
119
+ sdk: sdk,
120
+ encodedBytes: encodedBytes,
121
+ checksum: Self.checksum(eventData),
122
+ event: event,
123
+ )
124
+ let data = try Self.encode(record)
125
+ guard data.count <= Self.maxEventRecordBytes else {
126
+ throw DurableStoreFailure.capacity
127
+ }
128
+ do {
129
+ try write(data, named: name)
130
+ } catch {
131
+ failed = true
132
+ throw error
133
+ }
134
+ recoveredEvents.append(RecoveredEvent(event: event, encodedBytes: encodedBytes, recordName: name))
135
+ nextSequence += 1
136
+ return name
137
+ }
138
+
139
+ func appendExisting(_ events: [(event: Event, encodedBytes: Int)]) throws -> [String] {
140
+ guard recoveredEvents.isEmpty, recoveredPrefix == nil else {
141
+ throw DurableStoreFailure.owned
142
+ }
143
+ var names: [String] = []
144
+ do {
145
+ for item in events {
146
+ try names.append(append(item.event, encodedBytes: item.encodedBytes))
147
+ }
148
+ return names
149
+ } catch {
150
+ for name in names {
151
+ try? fileManager.removeItem(at: directory.appendingPathComponent(name, isDirectory: false))
152
+ }
153
+ recoveredEvents.removeAll()
154
+ recoveredPrefix = nil
155
+ nextSequence = 1
156
+ throw error
157
+ }
158
+ }
159
+
160
+ func persistPrefix(body: Data, eventRecordNames: [String], encodedBytes: Int) throws {
161
+ try requireHealthy()
162
+ guard !eventRecordNames.isEmpty,
163
+ eventRecordNames.count <= DeliveryEngine.maxRequestEvents,
164
+ body.count <= DeliveryEngine.maxRequestBytes,
165
+ recoveredEvents.prefix(eventRecordNames.count).map(\.recordName) == eventRecordNames
166
+ else {
167
+ throw DurableStoreFailure.corrupt
168
+ }
169
+ if let recoveredPrefix {
170
+ guard recoveredPrefix.body == body,
171
+ recoveredPrefix.eventRecordNames == eventRecordNames,
172
+ recoveredPrefix.encodedBytes == encodedBytes
173
+ else {
174
+ throw DurableStoreFailure.corrupt
175
+ }
176
+ return
177
+ }
178
+ let record = PrefixRecord(
179
+ version: Self.version,
180
+ sdk: sdk,
181
+ eventRecordNames: eventRecordNames,
182
+ encodedBytes: encodedBytes,
183
+ body: body,
184
+ )
185
+ let data = try Self.encode(record)
186
+ guard data.count <= Self.maxPrefixRecordBytes else {
187
+ throw DurableStoreFailure.capacity
188
+ }
189
+ do {
190
+ try write(data, named: Self.prefixName)
191
+ } catch {
192
+ failed = true
193
+ throw error
194
+ }
195
+ recoveredPrefix = RecoveredPrefix(
196
+ body: body,
197
+ eventRecordNames: eventRecordNames,
198
+ encodedBytes: encodedBytes,
199
+ )
200
+ }
201
+
202
+ func acknowledge(body: Data, eventRecordNames: [String]) throws {
203
+ try requireHealthy()
204
+ guard let prefix = recoveredPrefix,
205
+ prefix.body == body,
206
+ prefix.eventRecordNames == eventRecordNames,
207
+ recoveredEvents.prefix(eventRecordNames.count).map(\.recordName) == eventRecordNames
208
+ else {
209
+ throw DurableStoreFailure.corrupt
210
+ }
211
+ do {
212
+ try fileManager.removeItem(at: directory.appendingPathComponent(Self.prefixName, isDirectory: false))
213
+ for name in eventRecordNames {
214
+ try fileManager.removeItem(at: directory.appendingPathComponent(name, isDirectory: false))
215
+ }
216
+ } catch {
217
+ failed = true
218
+ throw DurableStoreFailure.inputOutput
219
+ }
220
+ recoveredEvents.removeFirst(eventRecordNames.count)
221
+ recoveredPrefix = nil
222
+ }
223
+
224
+ static func purge(parent: URL, fileManager: FileManager = .default) throws {
225
+ try validateParent(parent, fileManager: fileManager)
226
+ let directory = parent.appendingPathComponent(directoryName, isDirectory: true)
227
+ guard fileManager.fileExists(atPath: directory.path) else {
228
+ return
229
+ }
230
+ try validateDirectory(directory, fileManager: fileManager)
231
+ let handle = try acquireLock(in: directory, fileManager: fileManager)
232
+ defer { releaseLock(handle) }
233
+ do {
234
+ try fileManager.removeItem(at: directory)
235
+ } catch {
236
+ throw DurableStoreFailure.inputOutput
237
+ }
238
+ }
239
+
240
+ private func requireHealthy() throws {
241
+ if failed {
242
+ throw DurableStoreFailure.corrupt
243
+ }
244
+ }
245
+
246
+ private func write(_ data: Data, named name: String) throws {
247
+ guard Self.isOwnedName(name) else {
248
+ throw DurableStoreFailure.corrupt
249
+ }
250
+ let destination = directory.appendingPathComponent(name, isDirectory: false)
251
+ do {
252
+ try data.write(to: destination, options: .atomic)
253
+ try Self.hardenOwnedFile(destination, fileManager: fileManager)
254
+ } catch {
255
+ throw DurableStoreFailure.inputOutput
256
+ }
257
+ }
258
+ }