@shamilovtim/react-native-nitro-sse 2.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/LICENSE +21 -0
- package/NitroSse.podspec +37 -0
- package/README.md +144 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +124 -0
- package/android/src/main/AndroidManifest.xml +4 -0
- package/android/src/main/cpp/cpp-adapter.cpp +10 -0
- package/android/src/main/java/com/margelo/nitro/nitrosse/NetworkInspector.kt +94 -0
- package/android/src/main/java/com/margelo/nitro/nitrosse/NitroSse.kt +889 -0
- package/android/src/main/java/com/margelo/nitro/nitrosse/NitroSsePackage.kt +22 -0
- package/ios/NitroSse.swift +760 -0
- package/ios/NitroSseNetworkInspector.h +24 -0
- package/ios/NitroSseNetworkInspector.mm +68 -0
- package/lib/module/NitroSse.nitro.js +4 -0
- package/lib/module/NitroSse.nitro.js.map +1 -0
- package/lib/module/NitroSseClient.js +244 -0
- package/lib/module/NitroSseClient.js.map +1 -0
- package/lib/module/SseInterface.js +4 -0
- package/lib/module/SseInterface.js.map +1 -0
- package/lib/module/index.js +26 -0
- package/lib/module/index.js.map +1 -0
- package/lib/module/package.json +1 -0
- package/lib/typescript/package.json +1 -0
- package/lib/typescript/src/NitroSse.nitro.d.ts +48 -0
- package/lib/typescript/src/NitroSse.nitro.d.ts.map +1 -0
- package/lib/typescript/src/NitroSseClient.d.ts +26 -0
- package/lib/typescript/src/NitroSseClient.d.ts.map +1 -0
- package/lib/typescript/src/SseInterface.d.ts +237 -0
- package/lib/typescript/src/SseInterface.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +11 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +23 -0
- package/nitrogen/generated/android/c++/JFunc_std__shared_ptr_Promise_std__shared_ptr_Promise_std__unordered_map_std__string__std__string_____.hpp +138 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_SseEvent_.hpp +102 -0
- package/nitrogen/generated/android/c++/JHttpMethod.hpp +58 -0
- package/nitrogen/generated/android/c++/JHybridNitroSseSpec.cpp +134 -0
- package/nitrogen/generated/android/c++/JHybridNitroSseSpec.hpp +71 -0
- package/nitrogen/generated/android/c++/JSseConfig.hpp +162 -0
- package/nitrogen/generated/android/c++/JSseEvent.hpp +90 -0
- package/nitrogen/generated/android/c++/JSseEventType.hpp +67 -0
- package/nitrogen/generated/android/c++/JSseMockConfig.hpp +101 -0
- package/nitrogen/generated/android/c++/JSseMockEvent.hpp +94 -0
- package/nitrogen/generated/android/c++/JSseMockMode.hpp +58 -0
- package/nitrogen/generated/android/c++/JSseStats.hpp +70 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__unordered_map_std__string__std__string_____.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/Func_void_std__vector_SseEvent_.kt +80 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/HttpMethod.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/HybridNitroSseSpec.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseConfig.kt +135 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseEvent.kt +86 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseEventType.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseMockConfig.kt +71 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseMockEvent.kt +91 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseMockMode.kt +23 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseStats.kt +66 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/nitrosseOnLoad.kt +35 -0
- package/nitrogen/generated/android/nitrosse+autolinking.cmake +81 -0
- package/nitrogen/generated/android/nitrosse+autolinking.gradle +27 -0
- package/nitrogen/generated/android/nitrosseOnLoad.cpp +58 -0
- package/nitrogen/generated/android/nitrosseOnLoad.hpp +34 -0
- package/nitrogen/generated/ios/NitroSse+autolinking.rb +62 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Bridge.cpp +74 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Bridge.hpp +415 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Umbrella.hpp +74 -0
- package/nitrogen/generated/ios/NitroSseAutolinking.mm +33 -0
- package/nitrogen/generated/ios/NitroSseAutolinking.swift +26 -0
- package/nitrogen/generated/ios/c++/HybridNitroSseSpecSwift.cpp +11 -0
- package/nitrogen/generated/ios/c++/HybridNitroSseSpecSwift.hpp +161 -0
- package/nitrogen/generated/ios/swift/Func_std__shared_ptr_Promise_std__shared_ptr_Promise_std__unordered_map_std__string__std__string_____.swift +67 -0
- package/nitrogen/generated/ios/swift/Func_void_std__exception_ptr.swift +46 -0
- package/nitrogen/generated/ios/swift/Func_void_std__shared_ptr_Promise_std__unordered_map_std__string__std__string___.swift +66 -0
- package/nitrogen/generated/ios/swift/Func_void_std__unordered_map_std__string__std__string_.swift +54 -0
- package/nitrogen/generated/ios/swift/Func_void_std__vector_SseEvent_.swift +46 -0
- package/nitrogen/generated/ios/swift/HttpMethod.swift +40 -0
- package/nitrogen/generated/ios/swift/HybridNitroSseSpec.swift +63 -0
- package/nitrogen/generated/ios/swift/HybridNitroSseSpec_cxx.swift +246 -0
- package/nitrogen/generated/ios/swift/SseConfig.swift +346 -0
- package/nitrogen/generated/ios/swift/SseEvent.swift +155 -0
- package/nitrogen/generated/ios/swift/SseEventType.swift +52 -0
- package/nitrogen/generated/ios/swift/SseMockConfig.swift +94 -0
- package/nitrogen/generated/ios/swift/SseMockEvent.swift +179 -0
- package/nitrogen/generated/ios/swift/SseMockMode.swift +40 -0
- package/nitrogen/generated/ios/swift/SseStats.swift +70 -0
- package/nitrogen/generated/shared/c++/HttpMethod.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridNitroSseSpec.cpp +29 -0
- package/nitrogen/generated/shared/c++/HybridNitroSseSpec.hpp +81 -0
- package/nitrogen/generated/shared/c++/SseConfig.hpp +156 -0
- package/nitrogen/generated/shared/c++/SseEvent.hpp +115 -0
- package/nitrogen/generated/shared/c++/SseEventType.hpp +88 -0
- package/nitrogen/generated/shared/c++/SseMockConfig.hpp +105 -0
- package/nitrogen/generated/shared/c++/SseMockEvent.hpp +119 -0
- package/nitrogen/generated/shared/c++/SseMockMode.hpp +76 -0
- package/nitrogen/generated/shared/c++/SseStats.hpp +96 -0
- package/package.json +186 -0
- package/src/NitroSse.nitro.ts +53 -0
- package/src/NitroSseClient.ts +297 -0
- package/src/SseInterface.ts +248 -0
- package/src/index.ts +31 -0
|
@@ -0,0 +1,760 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import LDSwiftEventSource
|
|
4
|
+
import Network
|
|
5
|
+
#if os(iOS)
|
|
6
|
+
import UIKit
|
|
7
|
+
#endif
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* NitroSse implements a high-performance SSE client for iOS using LDSwiftEventSource.
|
|
11
|
+
*
|
|
12
|
+
* ARCHITECTURE DECISIONS:
|
|
13
|
+
* 1. Threading Serialization: All operations are strictly serialized on a dedicated background queue (sseQueue).
|
|
14
|
+
* This ensures thread-safety for internal states (buffer, backoff) and prevents blocking the JS/Main threads.
|
|
15
|
+
* 2. Mobile Survival Logic: Implements a "Hibernation" pattern. When the app enters the background,
|
|
16
|
+
* we flush remaining events and stop the socket to preserve battery and follow Apple's background policies.
|
|
17
|
+
* The connection is automatically resumed from the last known ID when the app returns to foreground.
|
|
18
|
+
* 3. Batching: Reduces JSI bridge overhead by accumulating events and dispatching them
|
|
19
|
+
* as a single array after a configurable interval.
|
|
20
|
+
*/
|
|
21
|
+
class NitroSse: HybridNitroSseSpec {
|
|
22
|
+
private static let sseQueueKey = DispatchSpecificKey<Void>()
|
|
23
|
+
|
|
24
|
+
deinit {
|
|
25
|
+
if DispatchQueue.getSpecific(key: NitroSse.sseQueueKey) != nil {
|
|
26
|
+
self.stopInternal()
|
|
27
|
+
self.stopNetworkMonitoring()
|
|
28
|
+
} else {
|
|
29
|
+
sseQueue.sync {
|
|
30
|
+
self.stopInternal()
|
|
31
|
+
self.stopNetworkMonitoring()
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
NotificationCenter.default.removeObserver(self)
|
|
35
|
+
}
|
|
36
|
+
private var eventSource: EventSource?
|
|
37
|
+
private var config: SseConfig?
|
|
38
|
+
private var onEventsCallback: ((_ events: [SseEvent]) -> Void)?
|
|
39
|
+
private var isRunning: Bool = false
|
|
40
|
+
private var consecutiveAuthErrors: Int = 0
|
|
41
|
+
private let maxAuthRetries: Int = 3
|
|
42
|
+
private var requestId: String? = nil
|
|
43
|
+
private var eventBuffer: [SseEvent] = []
|
|
44
|
+
private var isFlushPending: Bool = false
|
|
45
|
+
|
|
46
|
+
private var backoffCounter: Int = 0
|
|
47
|
+
private var currentReconnectAttempts: Int = 0
|
|
48
|
+
private var lastProcessedId: String? = nil
|
|
49
|
+
|
|
50
|
+
private var totalBytesReceived: Double = 0
|
|
51
|
+
private var reconnectCount: Double = 0
|
|
52
|
+
private var lastErrorTime: Double? = nil
|
|
53
|
+
private var lastErrorCode: String? = nil
|
|
54
|
+
|
|
55
|
+
private let sseQueue: DispatchQueue = {
|
|
56
|
+
let queue = DispatchQueue(label: "com.margelo.nitro.sse", qos: .utility)
|
|
57
|
+
queue.setSpecific(key: NitroSse.sseQueueKey, value: ())
|
|
58
|
+
return queue
|
|
59
|
+
}()
|
|
60
|
+
private var connectionAttemptVersion: Int = 0
|
|
61
|
+
#if os(iOS)
|
|
62
|
+
private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid
|
|
63
|
+
#endif
|
|
64
|
+
private var wasRunningBeforeHibernation: Bool = false
|
|
65
|
+
private var isAppInBackground: Bool = false
|
|
66
|
+
|
|
67
|
+
private var pathMonitor: NWPathMonitor?
|
|
68
|
+
private var lastPathInterface: NWInterface.InterfaceType?
|
|
69
|
+
private var wasRunningBeforeNetworkLoss: Bool = false
|
|
70
|
+
|
|
71
|
+
/**
|
|
72
|
+
* Set up the NitroSse instance with configuration and an event callback.
|
|
73
|
+
* This prepares the lifecycle observers and internal dispatch queue.
|
|
74
|
+
*
|
|
75
|
+
* - Parameters:
|
|
76
|
+
* - config: The SSE configuration containing URL, headers, and more.
|
|
77
|
+
* - onEvent: Callback function to receive batched SSE events.
|
|
78
|
+
*/
|
|
79
|
+
func setup(config: SseConfig, onEvent: @escaping ((_ events: [SseEvent]) -> Void)) throws {
|
|
80
|
+
sseQueue.async {
|
|
81
|
+
self.config = config
|
|
82
|
+
self.onEventsCallback = onEvent
|
|
83
|
+
|
|
84
|
+
NotificationCenter.default.removeObserver(self)
|
|
85
|
+
#if os(iOS)
|
|
86
|
+
NotificationCenter.default.addObserver(self, selector: #selector(self.handleAppDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
87
|
+
NotificationCenter.default.addObserver(self, selector: #selector(self.handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
88
|
+
#endif
|
|
89
|
+
|
|
90
|
+
if config.monitorNetwork != false {
|
|
91
|
+
self.startNetworkMonitoring()
|
|
92
|
+
} else {
|
|
93
|
+
self.stopNetworkMonitoring()
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
private func stopNetworkMonitoring() {
|
|
99
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
100
|
+
pathMonitor?.cancel()
|
|
101
|
+
pathMonitor = nil
|
|
102
|
+
lastPathInterface = nil
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
private func startNetworkMonitoring() {
|
|
106
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
107
|
+
guard pathMonitor == nil else { return }
|
|
108
|
+
|
|
109
|
+
let monitor = NWPathMonitor()
|
|
110
|
+
monitor.pathUpdateHandler = { [weak self] path in
|
|
111
|
+
self?.sseQueue.async {
|
|
112
|
+
self?.handleNetworkChange(path: path)
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
self.pathMonitor = monitor
|
|
116
|
+
monitor.start(queue: sseQueue)
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
private func handleNetworkChange(path: NWPath) {
|
|
120
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
121
|
+
let isSatisfied = path.status == .satisfied
|
|
122
|
+
|
|
123
|
+
var interfaceType: NWInterface.InterfaceType? = nil
|
|
124
|
+
if path.usesInterfaceType(.wifi) {
|
|
125
|
+
interfaceType = .wifi
|
|
126
|
+
} else if path.usesInterfaceType(.cellular) {
|
|
127
|
+
interfaceType = .cellular
|
|
128
|
+
} else if path.usesInterfaceType(.wiredEthernet) {
|
|
129
|
+
interfaceType = .wiredEthernet
|
|
130
|
+
} else {
|
|
131
|
+
interfaceType = path.availableInterfaces.first?.type
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
print("[NitroSse] Network path changed: status=\(path.status), interface=\(String(describing: interfaceType))")
|
|
135
|
+
|
|
136
|
+
if isSatisfied {
|
|
137
|
+
if wasRunningBeforeNetworkLoss {
|
|
138
|
+
print("[NitroSse] Network restored. Resuming stream.")
|
|
139
|
+
wasRunningBeforeNetworkLoss = false
|
|
140
|
+
if self.isAppInBackground && self.config?.backgroundExecution != true {
|
|
141
|
+
self.wasRunningBeforeHibernation = true
|
|
142
|
+
} else if isRunning {
|
|
143
|
+
self.restart()
|
|
144
|
+
} else {
|
|
145
|
+
try? self.start()
|
|
146
|
+
}
|
|
147
|
+
} else if isRunning {
|
|
148
|
+
if let lastInterface = lastPathInterface, let currentInterface = interfaceType, lastInterface != currentInterface {
|
|
149
|
+
print("[NitroSse] Network interface changed (\(lastInterface) -> \(currentInterface)). Restarting stream.")
|
|
150
|
+
self.restart()
|
|
151
|
+
} else if lastPathInterface == nil {
|
|
152
|
+
print("[NitroSse] Network interface acquired. Restarting stream.")
|
|
153
|
+
self.restart()
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
} else {
|
|
157
|
+
if isRunning {
|
|
158
|
+
print("[NitroSse] Network lost. Hibernating.")
|
|
159
|
+
wasRunningBeforeNetworkLoss = true
|
|
160
|
+
self.hibernateConnection()
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
if isSatisfied {
|
|
165
|
+
self.lastPathInterface = interfaceType
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
@objc private func handleAppDidEnterBackground() {
|
|
170
|
+
sseQueue.async {
|
|
171
|
+
self.isAppInBackground = true
|
|
172
|
+
guard self.isRunning, let config = self.config else { return }
|
|
173
|
+
|
|
174
|
+
if config.backgroundExecution == true {
|
|
175
|
+
print("[NitroSse] App backgrounded. backgroundExecution is true, keeping connection alive.")
|
|
176
|
+
#if os(iOS)
|
|
177
|
+
// Start a background task to tell the OS we want to keep running
|
|
178
|
+
self.cleanupBackgroundTask()
|
|
179
|
+
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "NitroSse-KeepAlive") { [weak self] in
|
|
180
|
+
self?.sseQueue.async {
|
|
181
|
+
print("[NitroSse] Background task expired. Hibernating now.")
|
|
182
|
+
self?.hibernateConnection()
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
#endif
|
|
186
|
+
return
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
self.hibernateConnection()
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
private func hibernateConnection() {
|
|
194
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
195
|
+
guard self.isRunning else { return }
|
|
196
|
+
|
|
197
|
+
self.wasRunningBeforeHibernation = true
|
|
198
|
+
print("[NitroSse] Hibernating NitroSse connection.")
|
|
199
|
+
|
|
200
|
+
self.flushEventsToJs()
|
|
201
|
+
|
|
202
|
+
self.eventSource?.stop()
|
|
203
|
+
self.eventSource = nil
|
|
204
|
+
if let rid = self.requestId {
|
|
205
|
+
NitroSseNetworkInspector.reportResponseEnd(rid, encodedDataLength: Int(self.totalBytesReceived))
|
|
206
|
+
self.requestId = nil
|
|
207
|
+
}
|
|
208
|
+
self.isRunning = false
|
|
209
|
+
|
|
210
|
+
self.cleanupBackgroundTask()
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
@objc private func handleAppWillEnterForeground() {
|
|
214
|
+
sseQueue.async {
|
|
215
|
+
self.isAppInBackground = false
|
|
216
|
+
self.cleanupBackgroundTask()
|
|
217
|
+
if self.wasRunningBeforeHibernation {
|
|
218
|
+
print("[NitroSse] App foregrounded. Resuming stream.")
|
|
219
|
+
self.wasRunningBeforeHibernation = false
|
|
220
|
+
try? self.start()
|
|
221
|
+
}
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
private func cleanupBackgroundTask() {
|
|
226
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
227
|
+
#if os(iOS)
|
|
228
|
+
if self.backgroundTaskIdentifier != .invalid {
|
|
229
|
+
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier)
|
|
230
|
+
self.backgroundTaskIdentifier = .invalid
|
|
231
|
+
}
|
|
232
|
+
#endif
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
private func pushEventToBuffer(_ event: SseEvent) {
|
|
236
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
237
|
+
|
|
238
|
+
let batchIntervalMs = config?.batchingIntervalMs ?? 0
|
|
239
|
+
let bufferCapacity = Int(config?.maxBufferSize ?? 1000)
|
|
240
|
+
|
|
241
|
+
eventBuffer.append(event)
|
|
242
|
+
|
|
243
|
+
// If we reached capacity, flush immediately regardless of interval
|
|
244
|
+
if eventBuffer.count >= bufferCapacity || batchIntervalMs <= 0 {
|
|
245
|
+
flushEventsToJs()
|
|
246
|
+
} else if !isFlushPending {
|
|
247
|
+
isFlushPending = true
|
|
248
|
+
sseQueue.asyncAfter(deadline: .now() + (Double(batchIntervalMs) / 1000.0)) { [weak self] in
|
|
249
|
+
self?.sseQueue.async {
|
|
250
|
+
self?.flushEventsToJs()
|
|
251
|
+
}
|
|
252
|
+
}
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
private func flushEventsToJs() {
|
|
256
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
257
|
+
guard !eventBuffer.isEmpty else { return }
|
|
258
|
+
|
|
259
|
+
let batch = eventBuffer
|
|
260
|
+
eventBuffer.removeAll()
|
|
261
|
+
isFlushPending = false
|
|
262
|
+
|
|
263
|
+
onEventsCallback?(batch)
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
internal func parseJsonToAnyMap(_ data: String) -> AnyMap? {
|
|
267
|
+
guard let jsonData = data.data(using: .utf8) else { return nil }
|
|
268
|
+
do {
|
|
269
|
+
if let dictionary = try JSONSerialization.jsonObject(with: jsonData, options: []) as? [String: Any?] {
|
|
270
|
+
return AnyMap.fromDictionaryIgnoreIncompatible(dictionary)
|
|
271
|
+
}
|
|
272
|
+
} catch {
|
|
273
|
+
// Non-fatal, just log and return nil
|
|
274
|
+
print("[NitroSse] Failed to parse JSON: \(error.localizedDescription)")
|
|
275
|
+
}
|
|
276
|
+
return nil
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
/**
|
|
280
|
+
* Manually set the last seen event ID.
|
|
281
|
+
* This will be used as the 'Last-Event-ID' header on the next connection attempt.
|
|
282
|
+
*/
|
|
283
|
+
func setLastProcessedId(id: String) {
|
|
284
|
+
sseQueue.async {
|
|
285
|
+
self.lastProcessedId = id
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
/// Update the HTTP headers that will be used for subsequent SSE connections.
|
|
290
|
+
/// - Parameters:
|
|
291
|
+
/// - headers: A dictionary of HTTP header names and values to replace the current headers for future connections.
|
|
292
|
+
func updateHeaders(headers: [String: String]) throws {
|
|
293
|
+
sseQueue.async {
|
|
294
|
+
guard let config = self.config else { return }
|
|
295
|
+
self.config = SseConfig(
|
|
296
|
+
url: config.url,
|
|
297
|
+
method: config.method,
|
|
298
|
+
headers: headers,
|
|
299
|
+
body: config.body,
|
|
300
|
+
backgroundExecution: config.backgroundExecution,
|
|
301
|
+
batchingIntervalMs: config.batchingIntervalMs,
|
|
302
|
+
maxBufferSize: config.maxBufferSize,
|
|
303
|
+
connectionTimeoutMs: config.connectionTimeoutMs,
|
|
304
|
+
readTimeoutMs: config.readTimeoutMs,
|
|
305
|
+
retryIntervalMs: config.retryIntervalMs,
|
|
306
|
+
maxRetryIntervalMs: config.maxRetryIntervalMs,
|
|
307
|
+
jitterFactor: config.jitterFactor,
|
|
308
|
+
maxReconnectAttempts: config.maxReconnectAttempts,
|
|
309
|
+
autoParseJSON: config.autoParseJSON,
|
|
310
|
+
monitorNetwork: config.monitorNetwork,
|
|
311
|
+
onBeforeRequest: config.onBeforeRequest,
|
|
312
|
+
mock: config.mock
|
|
313
|
+
)
|
|
314
|
+
print("[NitroSse] Headers updated for subsequent connections.")
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
/**
|
|
319
|
+
* Returns runtime statistics about the SSE connection.
|
|
320
|
+
*/
|
|
321
|
+
func getStats() throws -> SseStats {
|
|
322
|
+
return sseQueue.sync {
|
|
323
|
+
return SseStats(
|
|
324
|
+
totalBytesReceived: totalBytesReceived,
|
|
325
|
+
reconnectCount: reconnectCount,
|
|
326
|
+
lastErrorTime: lastErrorTime,
|
|
327
|
+
lastErrorCode: lastErrorCode
|
|
328
|
+
)
|
|
329
|
+
}
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
/**
|
|
333
|
+
* Start the SSE connection.
|
|
334
|
+
* This triggers the initial request and handles subsequent reconnections.
|
|
335
|
+
*/
|
|
336
|
+
func start() throws {
|
|
337
|
+
let startBody = {
|
|
338
|
+
guard !self.isRunning else { return }
|
|
339
|
+
|
|
340
|
+
// Critical check: Ensure config is available before starting
|
|
341
|
+
guard self.config != nil else {
|
|
342
|
+
throw RuntimeError("NitroSse not configured. Call setup() first.")
|
|
343
|
+
}
|
|
344
|
+
|
|
345
|
+
self.isRunning = true
|
|
346
|
+
self.consecutiveAuthErrors = 0
|
|
347
|
+
self.backoffCounter = 0
|
|
348
|
+
self.connectionAttemptVersion += 1
|
|
349
|
+
let version = self.connectionAttemptVersion
|
|
350
|
+
|
|
351
|
+
self.establishConnection(attemptVersion: version)
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
if DispatchQueue.getSpecific(key: NitroSse.sseQueueKey) != nil {
|
|
355
|
+
try startBody()
|
|
356
|
+
} else {
|
|
357
|
+
try sseQueue.sync(execute: startBody)
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
/// Initiates a new SSE connection attempt using the current configuration, honoring the provided attempt version to ignore stale async work.
|
|
362
|
+
///
|
|
363
|
+
/// If a configured `onBeforeRequest` interceptor is present, this method executes it (with a configurable timeout); when the interceptor resolves with additional headers those headers are merged into the active configuration before proceeding. If the interceptor fails or times out, interceptor error handling is invoked. If no interceptor is configured, the connection is established immediately.
|
|
364
|
+
/// - Parameters:
|
|
365
|
+
/// - attemptVersion: A numeric token representing the connection attempt version; the method aborts if this does not match the current `connectionAttemptVersion`, ensuring stale asynchronous results are ignored.
|
|
366
|
+
private func establishConnection(attemptVersion: Int) {
|
|
367
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
368
|
+
guard isRunning, let config = config, attemptVersion == self.connectionAttemptVersion else { return }
|
|
369
|
+
|
|
370
|
+
if let interceptor = config.onBeforeRequest {
|
|
371
|
+
|
|
372
|
+
let capturedConfig = config
|
|
373
|
+
class CompletionFlag {
|
|
374
|
+
var isCompleted = false
|
|
375
|
+
}
|
|
376
|
+
let flag = CompletionFlag()
|
|
377
|
+
let timeoutMs = capturedConfig.connectionTimeoutMs ?? 15000.0
|
|
378
|
+
|
|
379
|
+
sseQueue.asyncAfter(deadline: .now() + (timeoutMs / 1000.0)) { [weak self] in
|
|
380
|
+
guard let self = self else { return }
|
|
381
|
+
self.sseQueue.async { [weak self] in
|
|
382
|
+
guard let self = self, self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
383
|
+
if !flag.isCompleted {
|
|
384
|
+
flag.isCompleted = true
|
|
385
|
+
let error = NSError(domain: "NitroSse", code: -1, userInfo: [NSLocalizedDescriptionKey: "onBeforeRequest interceptor timed out after \(timeoutMs) ms"])
|
|
386
|
+
self.handleInterceptorError(error, attemptVersion: attemptVersion)
|
|
387
|
+
}
|
|
388
|
+
}
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
interceptor().then { [weak self] promise2 in
|
|
392
|
+
promise2.then { [weak self] newHeaders in
|
|
393
|
+
self?.sseQueue.async { [weak self] in
|
|
394
|
+
guard let self = self, self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
395
|
+
if !flag.isCompleted {
|
|
396
|
+
flag.isCompleted = true
|
|
397
|
+
let currentConfig = self.config ?? capturedConfig
|
|
398
|
+
var mergedHeaders = currentConfig.headers ?? [:]
|
|
399
|
+
for (k, v) in newHeaders {
|
|
400
|
+
mergedHeaders[k] = v
|
|
401
|
+
}
|
|
402
|
+
self.config = SseConfig(
|
|
403
|
+
url: currentConfig.url,
|
|
404
|
+
method: currentConfig.method,
|
|
405
|
+
headers: mergedHeaders,
|
|
406
|
+
body: currentConfig.body,
|
|
407
|
+
backgroundExecution: currentConfig.backgroundExecution,
|
|
408
|
+
batchingIntervalMs: currentConfig.batchingIntervalMs,
|
|
409
|
+
maxBufferSize: currentConfig.maxBufferSize,
|
|
410
|
+
connectionTimeoutMs: currentConfig.connectionTimeoutMs,
|
|
411
|
+
readTimeoutMs: currentConfig.readTimeoutMs,
|
|
412
|
+
retryIntervalMs: currentConfig.retryIntervalMs,
|
|
413
|
+
maxRetryIntervalMs: currentConfig.maxRetryIntervalMs,
|
|
414
|
+
jitterFactor: currentConfig.jitterFactor,
|
|
415
|
+
maxReconnectAttempts: currentConfig.maxReconnectAttempts,
|
|
416
|
+
autoParseJSON: currentConfig.autoParseJSON,
|
|
417
|
+
monitorNetwork: currentConfig.monitorNetwork,
|
|
418
|
+
onBeforeRequest: currentConfig.onBeforeRequest,
|
|
419
|
+
mock: currentConfig.mock
|
|
420
|
+
)
|
|
421
|
+
self.performEstablishConnection(attemptVersion: attemptVersion)
|
|
422
|
+
}
|
|
423
|
+
}
|
|
424
|
+
}.catch { [weak self] error in
|
|
425
|
+
self?.sseQueue.async { [weak self] in
|
|
426
|
+
guard let self = self, self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
427
|
+
if !flag.isCompleted {
|
|
428
|
+
flag.isCompleted = true
|
|
429
|
+
self.handleInterceptorError(error, attemptVersion: attemptVersion)
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
}
|
|
433
|
+
}.catch { [weak self] error in
|
|
434
|
+
self?.sseQueue.async { [weak self] in
|
|
435
|
+
guard let self = self, self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
436
|
+
if !flag.isCompleted {
|
|
437
|
+
flag.isCompleted = true
|
|
438
|
+
self.handleInterceptorError(error, attemptVersion: attemptVersion)
|
|
439
|
+
}
|
|
440
|
+
}
|
|
441
|
+
}
|
|
442
|
+
} else {
|
|
443
|
+
self.performEstablishConnection(attemptVersion: attemptVersion)
|
|
444
|
+
}
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
private func handleInterceptorError(_ error: Error, attemptVersion: Int) {
|
|
448
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
449
|
+
guard self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
450
|
+
print("[NitroSse] Interceptor failed: \(error.localizedDescription)")
|
|
451
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Interceptor Error: \(error.localizedDescription)", statusCode: -1, retry: nil))
|
|
452
|
+
// Reconnect after delay
|
|
453
|
+
self.scheduleAutomaticReconnect(isError: true, attemptVersion: attemptVersion)
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
private func performEstablishConnection(attemptVersion: Int) {
|
|
457
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
458
|
+
guard isRunning, let config = config, let url = URL(string: config.url), attemptVersion == self.connectionAttemptVersion else { return }
|
|
459
|
+
|
|
460
|
+
if let rid = self.requestId {
|
|
461
|
+
NitroSseNetworkInspector.reportResponseEnd(rid, encodedDataLength: Int(self.totalBytesReceived))
|
|
462
|
+
self.requestId = nil
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
let sessionConfig = URLSessionConfiguration.default
|
|
466
|
+
let readTimeout = (config.readTimeoutMs ?? 300000.0) / 1000.0
|
|
467
|
+
sessionConfig.timeoutIntervalForRequest = readTimeout
|
|
468
|
+
sessionConfig.timeoutIntervalForResource = readTimeout
|
|
469
|
+
|
|
470
|
+
let handler = SseHandler(parent: self, attemptVersion: attemptVersion)
|
|
471
|
+
var esConfig = EventSource.Config(handler: handler, url: url)
|
|
472
|
+
esConfig.urlSessionConfiguration = sessionConfig
|
|
473
|
+
esConfig.headers = config.headers ?? [:]
|
|
474
|
+
|
|
475
|
+
if let lastId = self.lastProcessedId, !lastId.isEmpty {
|
|
476
|
+
esConfig.headers["Last-Event-ID"] = lastId
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
esConfig.lastEventId = self.lastProcessedId ?? ""
|
|
480
|
+
esConfig.method = config.method?.stringValue.uppercased() ?? "GET"
|
|
481
|
+
esConfig.body = config.body?.data(using: .utf8)
|
|
482
|
+
|
|
483
|
+
let es = EventSource(config: esConfig)
|
|
484
|
+
self.eventSource = es
|
|
485
|
+
handler.source = es
|
|
486
|
+
|
|
487
|
+
let request = URLRequest(url: url)
|
|
488
|
+
self.requestId = NitroSseNetworkInspector.reportRequestStart(request, encodedDataLength: 0)
|
|
489
|
+
|
|
490
|
+
es.start()
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
/**
|
|
494
|
+
* Stop the SSE connection and clear any pending reconnect timers.
|
|
495
|
+
*/
|
|
496
|
+
func stop() {
|
|
497
|
+
sseQueue.async {
|
|
498
|
+
self.connectionAttemptVersion += 1
|
|
499
|
+
self.stopInternal()
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
private func stopInternal() {
|
|
504
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
505
|
+
self.isRunning = false
|
|
506
|
+
self.wasRunningBeforeNetworkLoss = false
|
|
507
|
+
self.wasRunningBeforeHibernation = false
|
|
508
|
+
self.eventSource?.stop()
|
|
509
|
+
self.eventSource = nil
|
|
510
|
+
if let rid = self.requestId {
|
|
511
|
+
NitroSseNetworkInspector.reportResponseEnd(rid, encodedDataLength: Int(self.totalBytesReceived))
|
|
512
|
+
self.requestId = nil
|
|
513
|
+
}
|
|
514
|
+
self.backoffCounter = 0
|
|
515
|
+
self.isFlushPending = false
|
|
516
|
+
self.cleanupBackgroundTask()
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
/**
|
|
520
|
+
* Immediately emit any pending buffered events to the JS bridge.
|
|
521
|
+
*/
|
|
522
|
+
func flush() {
|
|
523
|
+
sseQueue.async {
|
|
524
|
+
self.flushEventsToJs()
|
|
525
|
+
}
|
|
526
|
+
}
|
|
527
|
+
|
|
528
|
+
/**
|
|
529
|
+
* Restart the SSE connection by stopping and starting again.
|
|
530
|
+
*/
|
|
531
|
+
func restart() {
|
|
532
|
+
sseQueue.async {
|
|
533
|
+
self.stopInternal()
|
|
534
|
+
self.isRunning = true
|
|
535
|
+
self.requestId = nil
|
|
536
|
+
self.connectionAttemptVersion += 1
|
|
537
|
+
self.establishConnection(attemptVersion: self.connectionAttemptVersion)
|
|
538
|
+
}
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
/**
|
|
542
|
+
* Indicates if the SSE connection is currently active or trying to connect.
|
|
543
|
+
*/
|
|
544
|
+
func isConnected() -> Bool {
|
|
545
|
+
return sseQueue.sync {
|
|
546
|
+
return isRunning
|
|
547
|
+
}
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
private func extractRetryAfterSeconds(error: Error) -> TimeInterval? {
|
|
551
|
+
let nsError = error as NSError
|
|
552
|
+
guard let response = nsError.userInfo["response"] as? HTTPURLResponse else { return nil }
|
|
553
|
+
guard let retryAfterHeader = response.allHeaderFields["Retry-After"] as? String else { return nil }
|
|
554
|
+
|
|
555
|
+
if let seconds = Double(retryAfterHeader) {
|
|
556
|
+
return seconds
|
|
557
|
+
}
|
|
558
|
+
|
|
559
|
+
let rfc1123Formatter = DateFormatter()
|
|
560
|
+
rfc1123Formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
561
|
+
rfc1123Formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z"
|
|
562
|
+
if let date = rfc1123Formatter.date(from: retryAfterHeader) {
|
|
563
|
+
let timeUntilDate = date.timeIntervalSinceNow
|
|
564
|
+
return timeUntilDate > 0 ? timeUntilDate : nil
|
|
565
|
+
}
|
|
566
|
+
return nil
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
private func scheduleAutomaticReconnectWithFixedDelay(_ delay: TimeInterval, attemptVersion: Int) {
|
|
570
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
571
|
+
eventSource?.stop()
|
|
572
|
+
eventSource = nil
|
|
573
|
+
sseQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
574
|
+
guard let self = self else { return }
|
|
575
|
+
self.sseQueue.async {
|
|
576
|
+
guard self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
577
|
+
self.establishConnection(attemptVersion: attemptVersion)
|
|
578
|
+
}
|
|
579
|
+
}
|
|
580
|
+
}
|
|
581
|
+
|
|
582
|
+
/// Schedules an automatic reconnection attempt using the configured retry interval, exponential backoff, and jitter.
|
|
583
|
+
///
|
|
584
|
+
/// If the configured maximum reconnect attempts has been reached, this method emits an error event and stops the connection instead of scheduling a reconnect. Otherwise it increments the reconnect attempt counter, computes a delay (exponential backoff when `isError` is true, with jitter applied and a minimum delay of 1 second), clears the current event source, and schedules `establishConnection(attemptVersion:)` to run after the computed delay only if the client is still running and the provided `attemptVersion` matches the current connection attempt version.
|
|
585
|
+
/// - Parameters:
|
|
586
|
+
/// - isError: Whether the reconnect is being scheduled in response to an error (affects use of exponential backoff).
|
|
587
|
+
/// - attemptVersion: The connection attempt version that must match the current `connectionAttemptVersion` when the scheduled reconnect fires; used to ignore stale or superseded reconnect attempts.
|
|
588
|
+
private func scheduleAutomaticReconnect(isError: Bool, attemptVersion: Int) {
|
|
589
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
590
|
+
eventSource?.stop()
|
|
591
|
+
|
|
592
|
+
let jitterFactor = config?.jitterFactor ?? 0.5
|
|
593
|
+
let retryInterval = (config?.retryIntervalMs ?? 1000.0) / 1000.0
|
|
594
|
+
let maxRetryInterval = (config?.maxRetryIntervalMs ?? 30000.0) / 1000.0
|
|
595
|
+
|
|
596
|
+
var delay: TimeInterval = retryInterval
|
|
597
|
+
|
|
598
|
+
let maxAttempts = Int(config?.maxReconnectAttempts ?? -1.0)
|
|
599
|
+
if maxAttempts != -1 && currentReconnectAttempts >= maxAttempts {
|
|
600
|
+
print("[NitroSse] Max reconnection attempts reached (\(maxAttempts)). Stopping.")
|
|
601
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Max reconnection attempts reached (\(maxAttempts)).", statusCode: nil, retry: nil))
|
|
602
|
+
self.stopInternal()
|
|
603
|
+
return
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
currentReconnectAttempts += 1
|
|
607
|
+
|
|
608
|
+
if isError {
|
|
609
|
+
let exponent = Double(backoffCounter)
|
|
610
|
+
let base = min(retryInterval * pow(2.0, exponent), maxRetryInterval)
|
|
611
|
+
backoffCounter += 1
|
|
612
|
+
delay = base * (1.0 - jitterFactor + Double.random(in: 0...(2 * jitterFactor)))
|
|
613
|
+
} else {
|
|
614
|
+
delay = retryInterval * (1.0 - jitterFactor + Double.random(in: 0...(2 * jitterFactor)))
|
|
615
|
+
}
|
|
616
|
+
|
|
617
|
+
let safeDelay = max(delay, 1.0)
|
|
618
|
+
eventSource?.stop()
|
|
619
|
+
eventSource = nil
|
|
620
|
+
sseQueue.asyncAfter(deadline: .now() + safeDelay) { [weak self] in
|
|
621
|
+
guard let self = self else { return }
|
|
622
|
+
self.sseQueue.async {
|
|
623
|
+
guard self.isRunning, attemptVersion == self.connectionAttemptVersion else { return }
|
|
624
|
+
self.establishConnection(attemptVersion: attemptVersion)
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
private class SseHandler: EventHandler {
|
|
630
|
+
weak var parent: NitroSse?
|
|
631
|
+
weak var source: EventSource?
|
|
632
|
+
let attemptVersion: Int
|
|
633
|
+
|
|
634
|
+
init(parent: NitroSse, attemptVersion: Int) {
|
|
635
|
+
self.parent = parent
|
|
636
|
+
self.attemptVersion = attemptVersion
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
/// Handle a successful SSE connection open for this handler.
|
|
640
|
+
///
|
|
641
|
+
/// If this handler corresponds to the active connection and its attempt version matches the parent's current connectionAttemptVersion, resets `backoffCounter`, `currentReconnectAttempts`, and `consecutiveAuthErrors`, and enqueues an `.open` `SseEvent` with status code 200.
|
|
642
|
+
func onOpened() {
|
|
643
|
+
guard let parent = self.parent, source === parent.eventSource else { return }
|
|
644
|
+
parent.sseQueue.async { [weak parent] in
|
|
645
|
+
guard let parent = parent, self.attemptVersion == parent.connectionAttemptVersion else { return }
|
|
646
|
+
parent.backoffCounter = 0
|
|
647
|
+
parent.currentReconnectAttempts = 0
|
|
648
|
+
parent.consecutiveAuthErrors = 0
|
|
649
|
+
parent.pushEventToBuffer(SseEvent(type: .open, data: nil, parsedData: nil, id: nil, event: nil, message: nil, statusCode: 200, retry: nil))
|
|
650
|
+
}
|
|
651
|
+
}
|
|
652
|
+
|
|
653
|
+
func onClosed() {
|
|
654
|
+
guard let parent = self.parent, source === parent.eventSource else { return }
|
|
655
|
+
parent.sseQueue.async { [weak parent] in
|
|
656
|
+
guard let parent = parent, self.attemptVersion == parent.connectionAttemptVersion else { return }
|
|
657
|
+
if let rid = parent.requestId {
|
|
658
|
+
NitroSseNetworkInspector.reportResponseEnd(rid, encodedDataLength: Int(parent.totalBytesReceived))
|
|
659
|
+
parent.requestId = nil
|
|
660
|
+
}
|
|
661
|
+
if parent.isRunning {
|
|
662
|
+
parent.scheduleAutomaticReconnect(isError: false, attemptVersion: self.attemptVersion)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
func onMessage(eventType: String, messageEvent: MessageEvent) {
|
|
668
|
+
guard let parent = self.parent, source === parent.eventSource else { return }
|
|
669
|
+
parent.sseQueue.async { [weak parent] in
|
|
670
|
+
guard let parent = parent, self.attemptVersion == parent.connectionAttemptVersion else { return }
|
|
671
|
+
let encodedDataSize = Double(messageEvent.data.utf8.count)
|
|
672
|
+
let metadataSize = Double(eventType.utf8.count) + Double((messageEvent.lastEventId).utf8.count)
|
|
673
|
+
parent.totalBytesReceived += encodedDataSize + metadataSize
|
|
674
|
+
|
|
675
|
+
if !messageEvent.lastEventId.isEmpty {
|
|
676
|
+
parent.lastProcessedId = messageEvent.lastEventId
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
let parsedData = (parent.config?.autoParseJSON == true) ? parent.parseJsonToAnyMap(messageEvent.data) : nil
|
|
680
|
+
|
|
681
|
+
parent.pushEventToBuffer(SseEvent(type: .message, data: messageEvent.data, parsedData: parsedData, id: messageEvent.lastEventId, event: eventType, message: nil, statusCode: 200, retry: nil))
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
func onComment(comment: String) {
|
|
686
|
+
guard let parent = self.parent, source === parent.eventSource else { return }
|
|
687
|
+
parent.sseQueue.async { [weak parent] in
|
|
688
|
+
guard let parent = parent, self.attemptVersion == parent.connectionAttemptVersion else { return }
|
|
689
|
+
parent.totalBytesReceived += Double(comment.utf8.count)
|
|
690
|
+
parent.pushEventToBuffer(SseEvent(type: .heartbeat, data: nil, parsedData: nil, id: nil, event: nil, message: comment, statusCode: nil, retry: nil))
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
func onError(error: Error) {
|
|
695
|
+
guard let parent = self.parent, source === parent.eventSource else { return }
|
|
696
|
+
parent.sseQueue.async { [weak parent] in
|
|
697
|
+
guard let parent = parent, parent.isRunning, self.attemptVersion == parent.connectionAttemptVersion else { return }
|
|
698
|
+
let nsError = error as NSError
|
|
699
|
+
let statusCode = nsError.code
|
|
700
|
+
|
|
701
|
+
parent.reconnectCount += 1
|
|
702
|
+
parent.lastErrorTime = Date().timeIntervalSince1970 * 1000
|
|
703
|
+
parent.lastErrorCode = "\(nsError.domain)(\(statusCode))"
|
|
704
|
+
|
|
705
|
+
NitroSseNetworkInspector.reportRequestFailed(parent.requestId, cancelled: false)
|
|
706
|
+
parent.requestId = nil
|
|
707
|
+
|
|
708
|
+
if statusCode == 204 {
|
|
709
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "No Content (204). Stopping.", statusCode: 204, retry: nil))
|
|
710
|
+
parent.stopInternal()
|
|
711
|
+
return
|
|
712
|
+
}
|
|
713
|
+
|
|
714
|
+
if statusCode == 401 || statusCode == 403 {
|
|
715
|
+
if parent.config?.onBeforeRequest == nil {
|
|
716
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Auth Error (\(statusCode)) - No interceptor provided. Stopping.", statusCode: Double(statusCode), retry: nil))
|
|
717
|
+
parent.stopInternal()
|
|
718
|
+
return
|
|
719
|
+
}
|
|
720
|
+
|
|
721
|
+
parent.consecutiveAuthErrors += 1
|
|
722
|
+
if parent.consecutiveAuthErrors >= parent.maxAuthRetries {
|
|
723
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Auth Error (\(statusCode)) - Retry limit reached (\(parent.maxAuthRetries)). Stopping.", statusCode: Double(statusCode), retry: nil))
|
|
724
|
+
parent.stopInternal()
|
|
725
|
+
return
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Auth Error (\(statusCode)) - Retry \(parent.consecutiveAuthErrors)/\(parent.maxAuthRetries). Refreshing token...", statusCode: Double(statusCode), retry: nil))
|
|
729
|
+
parent.scheduleAutomaticReconnect(isError: true, attemptVersion: self.attemptVersion)
|
|
730
|
+
return
|
|
731
|
+
}
|
|
732
|
+
|
|
733
|
+
let isFatal = (statusCode == 400)
|
|
734
|
+
if isFatal {
|
|
735
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Fatal Error (\(statusCode)). Stopping.", statusCode: Double(statusCode), retry: nil))
|
|
736
|
+
parent.stopInternal()
|
|
737
|
+
return
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
let retryAfterSeconds = parent.extractRetryAfterSeconds(error: error)
|
|
741
|
+
if (statusCode == 429 || statusCode == 503), let retryAfter = retryAfterSeconds {
|
|
742
|
+
let jitter = Double.random(in: 0.5...1.5)
|
|
743
|
+
let totalDelay = retryAfter + jitter
|
|
744
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Retry-After received: \(Int(totalDelay))s", statusCode: Double(statusCode), retry: totalDelay * 1000.0))
|
|
745
|
+
parent.scheduleAutomaticReconnectWithFixedDelay(totalDelay, attemptVersion: self.attemptVersion)
|
|
746
|
+
return
|
|
747
|
+
}
|
|
748
|
+
|
|
749
|
+
if statusCode == 429 {
|
|
750
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: "Rate Limited (429) without Retry-After. Stopping.", statusCode: 429, retry: nil))
|
|
751
|
+
parent.stopInternal()
|
|
752
|
+
return
|
|
753
|
+
}
|
|
754
|
+
|
|
755
|
+
parent.pushEventToBuffer(SseEvent(type: .error, data: nil, parsedData: nil, id: nil, event: nil, message: error.localizedDescription, statusCode: Double(statusCode), retry: nil))
|
|
756
|
+
parent.scheduleAutomaticReconnect(isError: true, attemptVersion: self.attemptVersion)
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
}
|
|
760
|
+
}
|