react-native-nitro-sse 0.1.0-beta.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 +30 -0
- package/README.md +121 -0
- package/android/CMakeLists.txt +24 -0
- package/android/build.gradle +120 -0
- package/android/src/main/AndroidManifest.xml +2 -0
- package/android/src/main/cpp/cpp-adapter.cpp +6 -0
- package/android/src/main/java/com/margelo/nitro/nitrosse/NitroSse.kt +279 -0
- package/android/src/main/java/com/margelo/nitro/nitrosse/NitroSsePackage.kt +22 -0
- package/ios/NitroSse.swift +311 -0
- package/lib/module/NitroSse.nitro.js +4 -0
- package/lib/module/NitroSse.nitro.js.map +1 -0
- package/lib/module/SseInterface.js +2 -0
- package/lib/module/SseInterface.js.map +1 -0
- package/lib/module/index.js +22 -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 +34 -0
- package/lib/typescript/src/NitroSse.nitro.d.ts.map +1 -0
- package/lib/typescript/src/SseInterface.d.ts +25 -0
- package/lib/typescript/src/SseInterface.d.ts.map +1 -0
- package/lib/typescript/src/index.d.ts +5 -0
- package/lib/typescript/src/index.d.ts.map +1 -0
- package/nitro.json +17 -0
- package/nitrogen/generated/android/c++/JFunc_void_std__vector_SseEvent_.hpp +100 -0
- package/nitrogen/generated/android/c++/JHttpMethod.hpp +58 -0
- package/nitrogen/generated/android/c++/JHybridNitroSseSpec.cpp +110 -0
- package/nitrogen/generated/android/c++/JHybridNitroSseSpec.hpp +71 -0
- package/nitrogen/generated/android/c++/JSseConfig.hpp +98 -0
- package/nitrogen/generated/android/c++/JSseEvent.hpp +76 -0
- package/nitrogen/generated/android/c++/JSseEventType.hpp +67 -0
- package/nitrogen/generated/android/c++/JSseStats.hpp +70 -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 +82 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseConfig.kt +56 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseEvent.kt +50 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseEventType.kt +26 -0
- package/nitrogen/generated/android/kotlin/com/margelo/nitro/nitrosse/SseStats.kt +47 -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 +46 -0
- package/nitrogen/generated/android/nitrosseOnLoad.hpp +25 -0
- package/nitrogen/generated/ios/NitroSse+autolinking.rb +60 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Bridge.cpp +41 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Bridge.hpp +210 -0
- package/nitrogen/generated/ios/NitroSse-Swift-Cxx-Umbrella.hpp +63 -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 +130 -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 +60 -0
- package/nitrogen/generated/ios/swift/HybridNitroSseSpec_cxx.swift +212 -0
- package/nitrogen/generated/ios/swift/SseConfig.swift +130 -0
- package/nitrogen/generated/ios/swift/SseEvent.swift +101 -0
- package/nitrogen/generated/ios/swift/SseEventType.swift +52 -0
- package/nitrogen/generated/ios/swift/SseStats.swift +63 -0
- package/nitrogen/generated/shared/c++/HttpMethod.hpp +76 -0
- package/nitrogen/generated/shared/c++/HybridNitroSseSpec.cpp +26 -0
- package/nitrogen/generated/shared/c++/HybridNitroSseSpec.hpp +78 -0
- package/nitrogen/generated/shared/c++/SseConfig.hpp +111 -0
- package/nitrogen/generated/shared/c++/SseEvent.hpp +102 -0
- package/nitrogen/generated/shared/c++/SseEventType.hpp +88 -0
- package/nitrogen/generated/shared/c++/SseStats.hpp +96 -0
- package/package.json +179 -0
- package/src/NitroSse.nitro.ts +37 -0
- package/src/SseInterface.ts +28 -0
- package/src/index.tsx +26 -0
|
@@ -0,0 +1,311 @@
|
|
|
1
|
+
import Foundation
|
|
2
|
+
import NitroModules
|
|
3
|
+
import LDSwiftEventSource
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* NitroSse implements a high-performance SSE client for iOS using LDSwiftEventSource.
|
|
7
|
+
*
|
|
8
|
+
* ARCHITECTURE DECISIONS:
|
|
9
|
+
* 1. Threading Serialization: All operations are strictly serialized on a dedicated background queue (sseQueue).
|
|
10
|
+
* This ensures thread-safety for internal states (buffer, backoff) and prevents blocking the JS/Main threads.
|
|
11
|
+
* 2. Mobile Survival Logic: Implements a "Hibernation" pattern. When the app enters the background,
|
|
12
|
+
* we flush remaining events and stop the socket to preserve battery and follow Apple's background policies.
|
|
13
|
+
* The connection is automatically resumed from the last known ID when the app returns to foreground.
|
|
14
|
+
* 3. Batching: Reduces JSI bridge overhead by accumulating events and dispatching them
|
|
15
|
+
* as a single array after a configurable interval.
|
|
16
|
+
*/
|
|
17
|
+
class NitroSse: HybridNitroSseSpec, EventHandler {
|
|
18
|
+
private var eventSource: EventSource?
|
|
19
|
+
private var config: SseConfig?
|
|
20
|
+
private var onEventsCallback: ((_ events: [SseEvent]) -> Void)?
|
|
21
|
+
private var isRunning: Bool = false
|
|
22
|
+
|
|
23
|
+
private var eventBuffer: [SseEvent] = []
|
|
24
|
+
private var isFlushPending: Bool = false
|
|
25
|
+
|
|
26
|
+
private var backoffCounter: Int = 0
|
|
27
|
+
private var lastProcessedId: String? = nil
|
|
28
|
+
|
|
29
|
+
private var totalBytesReceived: Double = 0
|
|
30
|
+
private var reconnectCount: Double = 0
|
|
31
|
+
private var lastErrorTime: Double? = nil
|
|
32
|
+
private var lastErrorCode: String? = nil
|
|
33
|
+
|
|
34
|
+
private let defaultRetryDelay: TimeInterval = 3.0
|
|
35
|
+
private let baseBackoffDelay: TimeInterval = 2.0
|
|
36
|
+
private let maxBackoffDelay: TimeInterval = 30.0
|
|
37
|
+
|
|
38
|
+
private let sseQueue = DispatchQueue(label: "com.margelo.nitro.sse", qos: .utility)
|
|
39
|
+
private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid
|
|
40
|
+
private var wasRunningBeforeHibernation: Bool = false
|
|
41
|
+
|
|
42
|
+
func setup(config: SseConfig, onEvent: @escaping ((_ events: [SseEvent]) -> Void)) throws {
|
|
43
|
+
self.config = config
|
|
44
|
+
self.onEventsCallback = onEvent
|
|
45
|
+
|
|
46
|
+
NotificationCenter.default.addObserver(self, selector: #selector(handleAppDidEnterBackground), name: UIApplication.didEnterBackgroundNotification, object: nil)
|
|
47
|
+
NotificationCenter.default.addObserver(self, selector: #selector(handleAppWillEnterForeground), name: UIApplication.willEnterForegroundNotification, object: nil)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
@objc private func handleAppDidEnterBackground() {
|
|
51
|
+
sseQueue.async {
|
|
52
|
+
guard self.isRunning else { return }
|
|
53
|
+
|
|
54
|
+
self.wasRunningBeforeHibernation = true
|
|
55
|
+
print("[NitroSse] App backgrounded. Hibernating connection.")
|
|
56
|
+
|
|
57
|
+
self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "NitroSse-GracefulHibernate") {
|
|
58
|
+
self.cleanupBackgroundTask()
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
self.flushEventsToJs()
|
|
62
|
+
|
|
63
|
+
self.eventSource?.stop()
|
|
64
|
+
self.eventSource = nil
|
|
65
|
+
self.isRunning = false
|
|
66
|
+
|
|
67
|
+
self.cleanupBackgroundTask()
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
@objc private func handleAppWillEnterForeground() {
|
|
72
|
+
sseQueue.async {
|
|
73
|
+
self.cleanupBackgroundTask()
|
|
74
|
+
if self.wasRunningBeforeHibernation {
|
|
75
|
+
print("[NitroSse] App foregrounded. Resuming stream.")
|
|
76
|
+
self.wasRunningBeforeHibernation = false
|
|
77
|
+
try? self.start()
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
private func cleanupBackgroundTask() {
|
|
83
|
+
if self.backgroundTaskIdentifier != .invalid {
|
|
84
|
+
UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier)
|
|
85
|
+
self.backgroundTaskIdentifier = .invalid
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
private func pushEventToBuffer(_ event: SseEvent) {
|
|
90
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
91
|
+
|
|
92
|
+
let batchIntervalMs = config?.batchingIntervalMs ?? 0
|
|
93
|
+
let bufferCapacity = Int(config?.maxBufferSize ?? 1000)
|
|
94
|
+
|
|
95
|
+
while eventBuffer.count >= bufferCapacity {
|
|
96
|
+
eventBuffer.removeFirst()
|
|
97
|
+
}
|
|
98
|
+
eventBuffer.append(event)
|
|
99
|
+
|
|
100
|
+
if batchIntervalMs <= 0 {
|
|
101
|
+
flushEventsToJs()
|
|
102
|
+
} else if !isFlushPending {
|
|
103
|
+
isFlushPending = true
|
|
104
|
+
sseQueue.asyncAfter(deadline: .now() + (Double(batchIntervalMs) / 1000.0)) { [weak self] in
|
|
105
|
+
self?.flushEventsToJs()
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
private func flushEventsToJs() {
|
|
111
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
112
|
+
guard !eventBuffer.isEmpty else {
|
|
113
|
+
isFlushPending = false
|
|
114
|
+
return
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
let batch = eventBuffer
|
|
118
|
+
eventBuffer.removeAll()
|
|
119
|
+
isFlushPending = false
|
|
120
|
+
onEventsCallback?(batch)
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
func setLastProcessedId(id: String) {
|
|
124
|
+
sseQueue.async {
|
|
125
|
+
self.lastProcessedId = id
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
func updateHeaders(headers: [String: String]) throws {
|
|
130
|
+
sseQueue.async {
|
|
131
|
+
if let currentConfig = self.config {
|
|
132
|
+
self.config = SseConfig(
|
|
133
|
+
url: currentConfig.url,
|
|
134
|
+
method: currentConfig.method,
|
|
135
|
+
headers: headers,
|
|
136
|
+
body: currentConfig.body,
|
|
137
|
+
backgroundExecution: currentConfig.backgroundExecution,
|
|
138
|
+
batchingIntervalMs: currentConfig.batchingIntervalMs,
|
|
139
|
+
maxBufferSize: currentConfig.maxBufferSize
|
|
140
|
+
)
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
func getStats() throws -> SseStats {
|
|
146
|
+
return sseQueue.sync {
|
|
147
|
+
return SseStats(
|
|
148
|
+
totalBytesReceived: totalBytesReceived,
|
|
149
|
+
reconnectCount: reconnectCount,
|
|
150
|
+
lastErrorTime: lastErrorTime,
|
|
151
|
+
lastErrorCode: lastErrorCode
|
|
152
|
+
)
|
|
153
|
+
}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
func start() throws {
|
|
157
|
+
sseQueue.async {
|
|
158
|
+
guard !self.isRunning else { return }
|
|
159
|
+
self.isRunning = true
|
|
160
|
+
self.backoffCounter = 0
|
|
161
|
+
self.establishConnection()
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
private func establishConnection() {
|
|
166
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
167
|
+
guard isRunning, let config = config, let url = URL(string: config.url) else { return }
|
|
168
|
+
|
|
169
|
+
var esConfig = EventSource.Config(handler: self, url: url)
|
|
170
|
+
esConfig.headers = config.headers ?? [:]
|
|
171
|
+
|
|
172
|
+
if let lastId = self.lastProcessedId, !lastId.isEmpty {
|
|
173
|
+
esConfig.headers["Last-Event-ID"] = lastId
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
esConfig.idleTimeout = 35.0
|
|
177
|
+
esConfig.method = config.method?.stringValue.uppercased() ?? "GET"
|
|
178
|
+
esConfig.body = config.body?.data(using: .utf8)
|
|
179
|
+
|
|
180
|
+
eventSource = EventSource(config: esConfig)
|
|
181
|
+
eventSource?.start()
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
func stop() {
|
|
185
|
+
sseQueue.async {
|
|
186
|
+
self.isRunning = false
|
|
187
|
+
self.eventSource?.stop()
|
|
188
|
+
self.eventSource = nil
|
|
189
|
+
self.backoffCounter = 0
|
|
190
|
+
self.eventBuffer.removeAll()
|
|
191
|
+
self.isFlushPending = false
|
|
192
|
+
self.cleanupBackgroundTask()
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
|
|
196
|
+
func onOpened() {
|
|
197
|
+
sseQueue.async {
|
|
198
|
+
self.backoffCounter = 0
|
|
199
|
+
self.pushEventToBuffer(SseEvent(type: .open, data: nil, id: nil, event: nil, message: nil))
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
func onClosed() {
|
|
204
|
+
sseQueue.async {
|
|
205
|
+
if self.isRunning {
|
|
206
|
+
self.scheduleAutomaticReconnect(isError: false)
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
func onMessage(eventType: String, messageEvent: MessageEvent) {
|
|
212
|
+
sseQueue.async {
|
|
213
|
+
let encodedDataSize = Double(messageEvent.data.utf8.count)
|
|
214
|
+
let metadataSize = Double(eventType.utf8.count) + Double((messageEvent.lastEventId).utf8.count)
|
|
215
|
+
self.totalBytesReceived += encodedDataSize + metadataSize
|
|
216
|
+
|
|
217
|
+
self.pushEventToBuffer(SseEvent(type: .message, data: messageEvent.data, id: messageEvent.lastEventId, event: eventType, message: nil))
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
func onComment(comment: String) {
|
|
222
|
+
sseQueue.async {
|
|
223
|
+
self.totalBytesReceived += Double(comment.utf8.count)
|
|
224
|
+
self.pushEventToBuffer(SseEvent(type: .heartbeat, data: nil, id: nil, event: nil, message: comment))
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
private func extractRetryAfterSeconds(error: Error) -> TimeInterval? {
|
|
229
|
+
let nsError = error as NSError
|
|
230
|
+
guard let response = nsError.userInfo["response"] as? HTTPURLResponse else { return nil }
|
|
231
|
+
guard let retryAfterHeader = response.allHeaderFields["Retry-After"] as? String else { return nil }
|
|
232
|
+
|
|
233
|
+
if let seconds = Double(retryAfterHeader) {
|
|
234
|
+
return seconds
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
let rfc1123Formatter = DateFormatter()
|
|
238
|
+
rfc1123Formatter.locale = Locale(identifier: "en_US_POSIX")
|
|
239
|
+
rfc1123Formatter.dateFormat = "EEE, dd MMM yyyy HH:mm:ss z"
|
|
240
|
+
if let date = rfc1123Formatter.date(from: retryAfterHeader) {
|
|
241
|
+
let timeUntilDate = date.timeIntervalSinceNow
|
|
242
|
+
return timeUntilDate > 0 ? timeUntilDate : nil
|
|
243
|
+
}
|
|
244
|
+
return nil
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
func onError(error: Error) {
|
|
248
|
+
sseQueue.async {
|
|
249
|
+
guard self.isRunning else { return }
|
|
250
|
+
let nsError = error as NSError
|
|
251
|
+
let statusCode = nsError.code
|
|
252
|
+
|
|
253
|
+
self.reconnectCount += 1
|
|
254
|
+
self.lastErrorTime = Date().timeIntervalSince1970 * 1000
|
|
255
|
+
self.lastErrorCode = "\(nsError.domain)(\(statusCode))"
|
|
256
|
+
|
|
257
|
+
let isFatalError = (statusCode == 401 || statusCode == 403 || statusCode == 400)
|
|
258
|
+
if isFatalError {
|
|
259
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, id: nil, event: nil, message: "Fatal Error (\(statusCode)). Stopping."))
|
|
260
|
+
self.stop()
|
|
261
|
+
return
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
let retryAfterSeconds = self.extractRetryAfterSeconds(error: error)
|
|
265
|
+
if (statusCode == 429 || statusCode == 503), let delay = retryAfterSeconds {
|
|
266
|
+
let jitter = Double.random(in: 0.5...2.0)
|
|
267
|
+
let totalDelay = delay + jitter
|
|
268
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, id: nil, event: nil, message: "Retry-After received (\(statusCode))"))
|
|
269
|
+
self.scheduleAutomaticReconnectWithFixedDelay(totalDelay)
|
|
270
|
+
return
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
if statusCode == 429 {
|
|
274
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, id: nil, event: nil, message: "Rate Limited (429) without Retry-After. Stopping."))
|
|
275
|
+
self.stop()
|
|
276
|
+
return
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
self.pushEventToBuffer(SseEvent(type: .error, data: nil, id: nil, event: nil, message: error.localizedDescription))
|
|
280
|
+
self.scheduleAutomaticReconnect(isError: true)
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
|
|
284
|
+
private func scheduleAutomaticReconnectWithFixedDelay(_ delay: TimeInterval) {
|
|
285
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
286
|
+
eventSource?.stop()
|
|
287
|
+
sseQueue.asyncAfter(deadline: .now() + delay) { [weak self] in
|
|
288
|
+
guard let self = self, self.isRunning else { return }
|
|
289
|
+
self.establishConnection()
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
private func scheduleAutomaticReconnect(isError: Bool) {
|
|
294
|
+
dispatchPrecondition(condition: .onQueue(sseQueue))
|
|
295
|
+
eventSource?.stop()
|
|
296
|
+
var delay: TimeInterval = defaultRetryDelay
|
|
297
|
+
if isError {
|
|
298
|
+
let exponent = Double(backoffCounter)
|
|
299
|
+
let base = min(baseBackoffDelay * pow(2.0, exponent), maxBackoffDelay)
|
|
300
|
+
backoffCounter += 1
|
|
301
|
+
delay = base * (0.5 + Double.random(in: 0...1))
|
|
302
|
+
} else {
|
|
303
|
+
delay = defaultRetryDelay * (0.8 + Double.random(in: 0...0.4))
|
|
304
|
+
}
|
|
305
|
+
let safeDelay = max(delay, 2.0)
|
|
306
|
+
sseQueue.asyncAfter(deadline: .now() + safeDelay) { [weak self] in
|
|
307
|
+
guard let self = self, self.isRunning else { return }
|
|
308
|
+
self.establishConnection()
|
|
309
|
+
}
|
|
310
|
+
}
|
|
311
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["NitroSse.nitro.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":[],"sourceRoot":"../../src","sources":["SseInterface.ts"],"mappings":"","ignoreList":[]}
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
import { NitroModules } from 'react-native-nitro-modules';
|
|
4
|
+
export * from "./SseInterface.js";
|
|
5
|
+
export * from "./NitroSse.nitro.js";
|
|
6
|
+
|
|
7
|
+
// Load the Hybrid Object from Native
|
|
8
|
+
let realNitroSse;
|
|
9
|
+
try {
|
|
10
|
+
realNitroSse = NitroModules.createHybridObject('NitroSse');
|
|
11
|
+
} catch {
|
|
12
|
+
console.debug('Native NitroSse not found. This might be a test environment or web.');
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Public interface to use SSE.
|
|
17
|
+
*/
|
|
18
|
+
if (!realNitroSse || !realNitroSse.setup) {
|
|
19
|
+
throw new Error('NitroSse: Native module not found. Ensure you have linked the library and built the app for iOS/Android.');
|
|
20
|
+
}
|
|
21
|
+
export const NitroSseModule = realNitroSse;
|
|
22
|
+
//# sourceMappingURL=index.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"names":["NitroModules","realNitroSse","createHybridObject","console","debug","setup","Error","NitroSseModule"],"sourceRoot":"../../src","sources":["index.tsx"],"mappings":";;AAAA,SAASA,YAAY,QAAQ,4BAA4B;AAGzD,cAAc,mBAAgB;AAC9B,cAAc,qBAAkB;;AAEhC;AACA,IAAIC,YAAkC;AACtC,IAAI;EACFA,YAAY,GAAGD,YAAY,CAACE,kBAAkB,CAAW,UAAU,CAAC;AACtE,CAAC,CAAC,MAAM;EACNC,OAAO,CAACC,KAAK,CACX,qEACF,CAAC;AACH;;AAEA;AACA;AACA;AACA,IAAI,CAACH,YAAY,IAAI,CAAEA,YAAY,CAASI,KAAK,EAAE;EACjD,MAAM,IAAIC,KAAK,CACb,0GACF,CAAC;AACH;AAEA,OAAO,MAAMC,cAAwB,GAAGN,YAAY","ignoreList":[]}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"type":"module"}
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import type { HybridObject } from 'react-native-nitro-modules';
|
|
2
|
+
import type { SseConfig, SseEvent, SseStats } from './SseInterface';
|
|
3
|
+
export interface NitroSse extends HybridObject<{
|
|
4
|
+
ios: 'swift';
|
|
5
|
+
android: 'kotlin';
|
|
6
|
+
}> {
|
|
7
|
+
/**
|
|
8
|
+
* Configure SSE and setup event callback.
|
|
9
|
+
*/
|
|
10
|
+
setup(config: SseConfig, onEvent: (events: SseEvent[]) => void): void;
|
|
11
|
+
/**
|
|
12
|
+
* Start the SSE connection.
|
|
13
|
+
*/
|
|
14
|
+
start(): void;
|
|
15
|
+
/**
|
|
16
|
+
* Stop the SSE connection.
|
|
17
|
+
*/
|
|
18
|
+
stop(): void;
|
|
19
|
+
/**
|
|
20
|
+
* Set the last processed event ID.
|
|
21
|
+
* Native will use this ID to resume connection if interrupted.
|
|
22
|
+
*/
|
|
23
|
+
setLastProcessedId(id: string): void;
|
|
24
|
+
/**
|
|
25
|
+
* Update HTTP headers dynamically (e.g., when token expires).
|
|
26
|
+
* These headers will be used for subsequent connection/reconnection attempts.
|
|
27
|
+
*/
|
|
28
|
+
updateHeaders(headers: Record<string, string>): void;
|
|
29
|
+
/**
|
|
30
|
+
* Get connection statistics.
|
|
31
|
+
*/
|
|
32
|
+
getStats(): SseStats;
|
|
33
|
+
}
|
|
34
|
+
//# sourceMappingURL=NitroSse.nitro.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"NitroSse.nitro.d.ts","sourceRoot":"","sources":["../../../src/NitroSse.nitro.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,4BAA4B,CAAC;AAC/D,OAAO,KAAK,EAAE,SAAS,EAAE,QAAQ,EAAE,QAAQ,EAAE,MAAM,gBAAgB,CAAC;AAEpE,MAAM,WAAW,QACf,SAAQ,YAAY,CAAC;IAAE,GAAG,EAAE,OAAO,CAAC;IAAC,OAAO,EAAE,QAAQ,CAAA;CAAE,CAAC;IACzD;;OAEG;IACH,KAAK,CAAC,MAAM,EAAE,SAAS,EAAE,OAAO,EAAE,CAAC,MAAM,EAAE,QAAQ,EAAE,KAAK,IAAI,GAAG,IAAI,CAAC;IAEtE;;OAEG;IACH,KAAK,IAAI,IAAI,CAAC;IAEd;;OAEG;IACH,IAAI,IAAI,IAAI,CAAC;IAEb;;;OAGG;IACH,kBAAkB,CAAC,EAAE,EAAE,MAAM,GAAG,IAAI,CAAC;IAErC;;;OAGG;IACH,aAAa,CAAC,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,GAAG,IAAI,CAAC;IAErD;;OAEG;IACH,QAAQ,IAAI,QAAQ,CAAC;CACtB"}
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
export type HttpMethod = 'get' | 'post';
|
|
2
|
+
export type SseEventType = 'open' | 'message' | 'error' | 'close' | 'heartbeat';
|
|
3
|
+
export interface SseConfig {
|
|
4
|
+
url: string;
|
|
5
|
+
method?: HttpMethod;
|
|
6
|
+
headers?: Record<string, string>;
|
|
7
|
+
body?: string;
|
|
8
|
+
backgroundExecution?: boolean;
|
|
9
|
+
batchingIntervalMs?: number;
|
|
10
|
+
maxBufferSize?: number;
|
|
11
|
+
}
|
|
12
|
+
export interface SseEvent {
|
|
13
|
+
type: SseEventType;
|
|
14
|
+
data?: string;
|
|
15
|
+
id?: string;
|
|
16
|
+
event?: string;
|
|
17
|
+
message?: string;
|
|
18
|
+
}
|
|
19
|
+
export interface SseStats {
|
|
20
|
+
totalBytesReceived: number;
|
|
21
|
+
reconnectCount: number;
|
|
22
|
+
lastErrorTime?: number;
|
|
23
|
+
lastErrorCode?: string;
|
|
24
|
+
}
|
|
25
|
+
//# sourceMappingURL=SseInterface.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"SseInterface.d.ts","sourceRoot":"","sources":["../../../src/SseInterface.ts"],"names":[],"mappings":"AAAA,MAAM,MAAM,UAAU,GAAG,KAAK,GAAG,MAAM,CAAC;AAExC,MAAM,MAAM,YAAY,GAAG,MAAM,GAAG,SAAS,GAAG,OAAO,GAAG,OAAO,GAAG,WAAW,CAAC;AAEhF,MAAM,WAAW,SAAS;IACxB,GAAG,EAAE,MAAM,CAAC;IACZ,MAAM,CAAC,EAAE,UAAU,CAAC;IACpB,OAAO,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,CAAC,CAAC;IACjC,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,mBAAmB,CAAC,EAAE,OAAO,CAAC;IAC9B,kBAAkB,CAAC,EAAE,MAAM,CAAC;IAC5B,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,YAAY,CAAC;IACnB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,EAAE,CAAC,EAAE,MAAM,CAAC;IACZ,KAAK,CAAC,EAAE,MAAM,CAAC;IACf,OAAO,CAAC,EAAE,MAAM,CAAC;CAClB;AAED,MAAM,WAAW,QAAQ;IACvB,kBAAkB,EAAE,MAAM,CAAC;IAC3B,cAAc,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;IACvB,aAAa,CAAC,EAAE,MAAM,CAAC;CACxB"}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../../src/index.tsx"],"names":[],"mappings":"AACA,OAAO,KAAK,EAAE,QAAQ,EAAE,MAAM,kBAAkB,CAAC;AAEjD,cAAc,gBAAgB,CAAC;AAC/B,cAAc,kBAAkB,CAAC;AAqBjC,eAAO,MAAM,cAAc,EAAE,QAAuB,CAAC"}
|
package/nitro.json
ADDED
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
{
|
|
2
|
+
"cxxNamespace": ["nitrosse"],
|
|
3
|
+
"ios": {
|
|
4
|
+
"iosModuleName": "NitroSse"
|
|
5
|
+
},
|
|
6
|
+
"android": {
|
|
7
|
+
"androidNamespace": ["nitrosse"],
|
|
8
|
+
"androidCxxLibName": "nitrosse"
|
|
9
|
+
},
|
|
10
|
+
"autolinking": {
|
|
11
|
+
"NitroSse": {
|
|
12
|
+
"swift": "NitroSse",
|
|
13
|
+
"kotlin": "NitroSse"
|
|
14
|
+
}
|
|
15
|
+
},
|
|
16
|
+
"ignorePaths": ["node_modules"]
|
|
17
|
+
}
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JFunc_void_std__vector_SseEvent_.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include <functional>
|
|
12
|
+
|
|
13
|
+
#include "SseEvent.hpp"
|
|
14
|
+
#include <vector>
|
|
15
|
+
#include <functional>
|
|
16
|
+
#include <NitroModules/JNICallable.hpp>
|
|
17
|
+
#include "JSseEvent.hpp"
|
|
18
|
+
#include "SseEventType.hpp"
|
|
19
|
+
#include "JSseEventType.hpp"
|
|
20
|
+
#include <string>
|
|
21
|
+
#include <optional>
|
|
22
|
+
|
|
23
|
+
namespace margelo::nitro::nitrosse {
|
|
24
|
+
|
|
25
|
+
using namespace facebook;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Represents the Java/Kotlin callback `(events: Array<SseEvent>) -> Unit`.
|
|
29
|
+
* This can be passed around between C++ and Java/Kotlin.
|
|
30
|
+
*/
|
|
31
|
+
struct JFunc_void_std__vector_SseEvent_: public jni::JavaClass<JFunc_void_std__vector_SseEvent_> {
|
|
32
|
+
public:
|
|
33
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitrosse/Func_void_std__vector_SseEvent_;";
|
|
34
|
+
|
|
35
|
+
public:
|
|
36
|
+
/**
|
|
37
|
+
* Invokes the function this `JFunc_void_std__vector_SseEvent_` instance holds through JNI.
|
|
38
|
+
*/
|
|
39
|
+
void invoke(const std::vector<SseEvent>& events) const {
|
|
40
|
+
static const auto method = javaClassStatic()->getMethod<void(jni::alias_ref<jni::JArrayClass<JSseEvent>> /* events */)>("invoke");
|
|
41
|
+
method(self(), [&]() {
|
|
42
|
+
size_t __size = events.size();
|
|
43
|
+
jni::local_ref<jni::JArrayClass<JSseEvent>> __array = jni::JArrayClass<JSseEvent>::newArray(__size);
|
|
44
|
+
for (size_t __i = 0; __i < __size; __i++) {
|
|
45
|
+
const auto& __element = events[__i];
|
|
46
|
+
auto __elementJni = JSseEvent::fromCpp(__element);
|
|
47
|
+
__array->setElement(__i, *__elementJni);
|
|
48
|
+
}
|
|
49
|
+
return __array;
|
|
50
|
+
}());
|
|
51
|
+
}
|
|
52
|
+
};
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* An implementation of Func_void_std__vector_SseEvent_ that is backed by a C++ implementation (using `std::function<...>`)
|
|
56
|
+
*/
|
|
57
|
+
class JFunc_void_std__vector_SseEvent__cxx final: public jni::HybridClass<JFunc_void_std__vector_SseEvent__cxx, JFunc_void_std__vector_SseEvent_> {
|
|
58
|
+
public:
|
|
59
|
+
static jni::local_ref<JFunc_void_std__vector_SseEvent_::javaobject> fromCpp(const std::function<void(const std::vector<SseEvent>& /* events */)>& func) {
|
|
60
|
+
return JFunc_void_std__vector_SseEvent__cxx::newObjectCxxArgs(func);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
public:
|
|
64
|
+
/**
|
|
65
|
+
* Invokes the C++ `std::function<...>` this `JFunc_void_std__vector_SseEvent__cxx` instance holds.
|
|
66
|
+
*/
|
|
67
|
+
void invoke_cxx(jni::alias_ref<jni::JArrayClass<JSseEvent>> events) {
|
|
68
|
+
_func([&]() {
|
|
69
|
+
size_t __size = events->size();
|
|
70
|
+
std::vector<SseEvent> __vector;
|
|
71
|
+
__vector.reserve(__size);
|
|
72
|
+
for (size_t __i = 0; __i < __size; __i++) {
|
|
73
|
+
auto __element = events->getElement(__i);
|
|
74
|
+
__vector.push_back(__element->toCpp());
|
|
75
|
+
}
|
|
76
|
+
return __vector;
|
|
77
|
+
}());
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
public:
|
|
81
|
+
[[nodiscard]]
|
|
82
|
+
inline const std::function<void(const std::vector<SseEvent>& /* events */)>& getFunction() const {
|
|
83
|
+
return _func;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
public:
|
|
87
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitrosse/Func_void_std__vector_SseEvent__cxx;";
|
|
88
|
+
static void registerNatives() {
|
|
89
|
+
registerHybrid({makeNativeMethod("invoke_cxx", JFunc_void_std__vector_SseEvent__cxx::invoke_cxx)});
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
private:
|
|
93
|
+
explicit JFunc_void_std__vector_SseEvent__cxx(const std::function<void(const std::vector<SseEvent>& /* events */)>& func): _func(func) { }
|
|
94
|
+
|
|
95
|
+
private:
|
|
96
|
+
friend HybridBase;
|
|
97
|
+
std::function<void(const std::vector<SseEvent>& /* events */)> _func;
|
|
98
|
+
};
|
|
99
|
+
|
|
100
|
+
} // namespace margelo::nitro::nitrosse
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
///
|
|
2
|
+
/// JHttpMethod.hpp
|
|
3
|
+
/// This file was generated by nitrogen. DO NOT MODIFY THIS FILE.
|
|
4
|
+
/// https://github.com/mrousavy/nitro
|
|
5
|
+
/// Copyright © Marc Rousavy @ Margelo
|
|
6
|
+
///
|
|
7
|
+
|
|
8
|
+
#pragma once
|
|
9
|
+
|
|
10
|
+
#include <fbjni/fbjni.h>
|
|
11
|
+
#include "HttpMethod.hpp"
|
|
12
|
+
|
|
13
|
+
namespace margelo::nitro::nitrosse {
|
|
14
|
+
|
|
15
|
+
using namespace facebook;
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* The C++ JNI bridge between the C++ enum "HttpMethod" and the the Kotlin enum "HttpMethod".
|
|
19
|
+
*/
|
|
20
|
+
struct JHttpMethod final: public jni::JavaClass<JHttpMethod> {
|
|
21
|
+
public:
|
|
22
|
+
static auto constexpr kJavaDescriptor = "Lcom/margelo/nitro/nitrosse/HttpMethod;";
|
|
23
|
+
|
|
24
|
+
public:
|
|
25
|
+
/**
|
|
26
|
+
* Convert this Java/Kotlin-based enum to the C++ enum HttpMethod.
|
|
27
|
+
*/
|
|
28
|
+
[[maybe_unused]]
|
|
29
|
+
[[nodiscard]]
|
|
30
|
+
HttpMethod toCpp() const {
|
|
31
|
+
static const auto clazz = javaClassStatic();
|
|
32
|
+
static const auto fieldOrdinal = clazz->getField<int>("value");
|
|
33
|
+
int ordinal = this->getFieldValue(fieldOrdinal);
|
|
34
|
+
return static_cast<HttpMethod>(ordinal);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
public:
|
|
38
|
+
/**
|
|
39
|
+
* Create a Java/Kotlin-based enum with the given C++ enum's value.
|
|
40
|
+
*/
|
|
41
|
+
[[maybe_unused]]
|
|
42
|
+
static jni::alias_ref<JHttpMethod> fromCpp(HttpMethod value) {
|
|
43
|
+
static const auto clazz = javaClassStatic();
|
|
44
|
+
switch (value) {
|
|
45
|
+
case HttpMethod::GET:
|
|
46
|
+
static const auto fieldGET = clazz->getStaticField<JHttpMethod>("GET");
|
|
47
|
+
return clazz->getStaticFieldValue(fieldGET);
|
|
48
|
+
case HttpMethod::POST:
|
|
49
|
+
static const auto fieldPOST = clazz->getStaticField<JHttpMethod>("POST");
|
|
50
|
+
return clazz->getStaticFieldValue(fieldPOST);
|
|
51
|
+
default:
|
|
52
|
+
std::string stringValue = std::to_string(static_cast<int>(value));
|
|
53
|
+
throw std::invalid_argument("Invalid enum value (" + stringValue + "!");
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
} // namespace margelo::nitro::nitrosse
|