@shamilovtim/react-native-nitro-sse 2.3.0 → 2.3.1

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/README.md CHANGED
@@ -1,23 +1,24 @@
1
- # 🚀 react-native-nitro-sse
1
+ # react-native-nitro-sse
2
2
 
3
- High-performance Server-Sent Events (SSE) client for React Native, built on top of **Nitro Modules (JSI)**. Designed for mission-critical systems requiring extreme stability, high-throughput data streaming, and absolute battery optimization.
3
+ High-performance Server-Sent Events (SSE) client for React Native, built on top of **Nitro Modules (JSI)**. Designed for mission-critical apps requiring high-throughput streaming, battery optimization, and extreme stability.
4
4
 
5
- ## 🌟 Why NitroSSE?
5
+ ---
6
+
7
+ ## Why NitroSSE?
6
8
 
7
- Unlike traditional EventSource libraries that run on the JS thread or use the legacy Bridge, NitroSSE moves the entire control logic down to the deepest Native layer:
9
+ Traditional EventSource libraries run on the JS thread or use the legacy React Native Bridge. NitroSSE moves the entire control logic down to the native layer:
8
10
 
9
- - **🚀 Zero-Latency JSI**: Communication between JS and Native is instantaneous, bypassing the asynchronous bridge.
10
- - **🧠 Smart Reconnect**: Automatic reconnection strategy using **Exponential Backoff** and **Jitter** to prevent thundering herd problems.
11
- - **🛡️ DoS Protection**: Respects RFC `Retry-After` headers and enforces strict connection frequency limits.
12
- - **🌊 Backpressure Handling**: Advanced **Batching** mechanism aggregates messages and employs **Tail Drop** strategies to protect the UI thread from freezing during data surges.
13
- - **🔋 Mobile-First Architecture**: Automatically hibernates when the app enters the background and seamlessly reconnects upon foregrounding to conserve battery.
14
- - **💓 Heartbeat Detection**: Native-side detection of keep-alive signals (comments) to maintain a reliable connection watchdog.
15
- - **🔍 DevTools Integration**: Plug-and-play support for **React Native 0.83+ DevTools Network Tab**. Monitor connection status, request/response headers, and timing directly in the official desktop debugger.
16
- - **🛠️ Full Protocol Support**: Comprehensive support for GET/POST methods and dynamic header updates.
11
+ * **Zero-Latency JSI**: Instantaneous JS-Native communication bypassing the async bridge.
12
+ * **Advanced Backpressure**: Batches high-frequency events and employs tail-drop strategies to keep UI thread fluid under extreme load.
13
+ * **Intelligent Reconnect**: Automatic recovery using **Exponential Backoff** and **Jitter** to prevent server stampedes.
14
+ * **Lifecycles**: Auto-hibernates connections in background and resumes on foreground to preserve battery.
15
+ * **Heartbeat Watchdog**: Native detection of keep-alive/ping comments to auto-reconnect dead sockets.
16
+ * **RN DevTools Integration**: Plug-and-play network tracing in **React Native 0.83+ DevTools** (monitor streams directly).
17
+ * **Local Mocking Engine**: Simulates streams, connection drops, and delays entirely in JS without backend setup.
17
18
 
18
19
  ---
19
20
 
20
- ## 📦 Installation
21
+ ## Installation
21
22
 
22
23
  ```sh
23
24
  yarn add @shamilovtim/react-native-nitro-sse react-native-nitro-modules
@@ -25,15 +26,18 @@ yarn add @shamilovtim/react-native-nitro-sse react-native-nitro-modules
25
26
  npm install @shamilovtim/react-native-nitro-sse react-native-nitro-modules
26
27
  ```
27
28
 
28
- > **Note**: `react-native-nitro-modules` is required as the core foundation for JSI performance.
29
+ > [!NOTE]
30
+ > `react-native-nitro-modules` is a peer dependency and must be installed in the host project.
29
31
 
30
32
  ---
31
33
 
32
- ## 📊 Compatibility
34
+ ## Compatibility
33
35
 
34
36
  | react-native-nitro-sse | react-native-nitro-modules |
35
37
  | :--------------------- | :------------------------- |
36
- | **2.2.0 - latest** | **0.35.4** |
38
+ | **2.3.1 - latest** | **0.35.9** |
39
+ | **2.3.0** | **0.35.6** |
40
+ | **2.2.0 - 2.2.3** | **0.35.4** |
37
41
  | **2.0.0 - 2.1.1** | **0.35.2** |
38
42
  | **1.6.2** | **0.35.2** |
39
43
  | **1.4.0 - 1.6.1** | **0.35.0** |
@@ -42,103 +46,107 @@ npm install @shamilovtim/react-native-nitro-sse react-native-nitro-modules
42
46
  | **1.1.0** | **0.33.9** |
43
47
  | **1.0.0** | **0.33.8** |
44
48
 
45
-
46
49
  ---
47
50
 
48
- ## 🚀 Usage
49
-
50
- ### 1. Basic Initialization
51
-
52
- Initialize the module with your endpoint configuration and an event listener.
51
+ ## Usage
53
52
 
54
53
  ```tsx
55
54
  import { createNitroSse } from '@shamilovtim/react-native-nitro-sse';
56
55
 
57
- const nitroSse = createNitroSse();
58
-
59
- nitroSse.setup(
60
- {
61
- url: 'https://api.yourserver.com/stream',
62
- method: 'get',
63
- headers: {
64
- 'Authorization': 'Bearer active-token',
65
- },
66
- // Batch messages every 100ms to optimize UI rendering
67
- batchingIntervalMs: 100,
68
- // Maximum of 1000 messages in the native queue before forced flush
69
- maxBufferSize: 1000,
70
- // Optional: Auto-refresh tokens or dynamic headers
71
- onBeforeRequest: async () => {
72
- const token = await fetchNewToken();
73
- return { 'Authorization': `Bearer ${token}` };
74
- }
75
- },
76
- (events) => {
77
- // events is an array of SseEvent objects
78
- events.forEach((event) => {
79
- console.log('Received:', event);
80
- });
81
- }
82
- );
56
+ const sse = createNitroSse();
83
57
 
84
- // Start the connection
85
- nitroSse.start();
86
- ```
58
+ sse.setup({
59
+ url: 'https://api.example.com/stream',
60
+ headers: { 'Authorization': 'Bearer TOKEN' }
61
+ });
87
62
 
88
- ### 2. Monitoring & Statistics
63
+ // Listen to events
64
+ sse.addEventListener('message', (e) => console.log('Received:', e.data));
65
+ sse.addEventListener('error', (e) => console.error('Error:', e.message));
89
66
 
90
- NitroSSE provides synchronous access to connection health and throughput data.
67
+ // Start streaming
68
+ sse.start();
91
69
 
92
- ```tsx
93
- const stats = nitroSse.getStats();
94
- console.log(`Downloaded: ${stats.totalBytesReceived / 1024} KB`);
95
- console.log(`Reconnections: ${stats.reconnectCount}`);
70
+ // Cleanup when done
71
+ // sse.stop();
96
72
  ```
97
73
 
98
74
  ---
99
75
 
100
- ## ⚙️ Configuration (SseConfig)
76
+ ## Configuration Reference (`SseConfig`)
101
77
 
102
78
  | Parameter | Type | Default | Description |
103
79
  | :--- | :--- | :--- | :--- |
104
- | `url` | `string` | - | **Required**. The URL of the SSE endpoint. |
105
- | `method` | `'get' \| 'post'` | `get` | HTTP method. |
106
- | `headers` | `Record<string, string>` | `{}` | Custom HTTP headers. |
107
- | `body` | `string` | - | Request body (payload) for POST requests. |
108
- | `batchingIntervalMs` | `number` | `0` | Time window (ms) to group events. `0` = real-time. |
109
- | `maxBufferSize` | `number` | `1000` | Native queue limit to prevent memory overflow. |
110
- | `connectionTimeoutMs`| `number` | `15000` | Max time for initial handshake / connect. |
111
- | `readTimeoutMs` | `number` | `300000`| Max idle time between data packets before reconnecting. |
112
- | `backgroundExecution` | `boolean` | `false` | (iOS) Attempt to maintain connection via background tasks. |
113
- | `autoParseJSON` | `boolean` | `false` | Automatically parse message data as JSON in a background native thread. Result is in `parsedData`. |
114
- | `monitorNetwork` | `boolean` | `true` | Automatically hibernate connection on system network loss to save battery. |
115
- | `onBeforeRequest` | `function`| - | Async hook to refresh headers/tokens before every attempt. |
80
+ | `url` | `string` | | **Required**. SSE endpoint target. |
81
+ | `method` | `'get' \| 'post'` | `'get'` | HTTP method. |
82
+ | `headers` | `Record<string, string>` | `{}` | Custom request headers. |
83
+ | `body` | `string` | | Payload sent during POST requests. |
84
+ | `backgroundExecution` | `boolean` | `false` | (iOS) Keep background execution active when app is minimized. |
85
+ | `batchingIntervalMs` | `number` | `0` | Batch window (ms). `0` sends events instantly. |
86
+ | `maxBufferSize` | `number` | `1000` | Native memory safety threshold. |
87
+ | `connectionTimeoutMs`| `number` | `15000` | Handshake connect timeout threshold. |
88
+ | `readTimeoutMs` | `number` | `300000`| Socket inactivity limit triggers reconnect. |
89
+ | `retryIntervalMs` | `number` | `1000` | Backoff base delay (ms) for reconnect. |
90
+ | `maxRetryIntervalMs` | `number` | `30000` | Cap on backoff reconnect delay (ms). |
91
+ | `jitterFactor` | `number` | `0.5` | Randomization spread ratio (`0.0` to `1.0`). |
92
+ | `maxReconnectAttempts`| `number` | `-1` | Limit reconnections. `-1` = infinite. `0` = disabled. |
93
+ | `autoParseJSON` | `boolean` | `false` | Parses event `data` on native thread; outputs to `parsedData`. |
94
+ | `monitorNetwork` | `boolean` | `true` | Pauses/resumes connection on WiFi <-> Cell or link changes. |
95
+ | `onBeforeRequest` | `() => Promise<Record<string, string>>` | — | Async hook running immediately before every request. |
96
+ | `mock` | `SseMockConfig` | — | Injected mock stream settings (Dev environment only). |
116
97
 
117
98
  ---
118
99
 
119
- ## 🛡️ Reliability Features (v1.5.1+)
100
+ ## Mocking & Testing System (`v2.3.0+`)
120
101
 
121
- - **Atomic State Management**: Calling `.start()` or `.stop()` updates the instance status immediately on the JSI thread, ensuring the UI (e.g., `isConnected()`) never feels laggy or stuck.
122
- - **Promise Safety**: The `onBeforeRequest` interceptor is wrapped in a native fallback timeout. Even if your JS token refresh hangs indefinitely, the native state machine will recover and retry safely.
123
- - **Zero-Ghost Reloads**: Seamlessly handles React Native Hot Reloads. The native layer detects when the JS environment is being destroyed and synchronously kills active sockets to prevent duplicate connections.
124
- - **Cumulative Logic**: Statistics (`totalBytesReceived`) are preserved across reconnections, giving you a true 360-degree view of the session's data usage.
102
+ NitroSSE includes a built-in mock streaming engine to test application responses without running live endpoints. Mocks are automatically disabled in production configurations.
125
103
 
126
- ---
104
+ ### Configuration (`SseMockConfig`)
127
105
 
128
- ## 🏗️ System Architecture
106
+ ```tsx
107
+ sse.setup({
108
+ url: 'https://api.mocked-endpoint.com/stream',
109
+ mock: {
110
+ mode: 'replace', // 'replace' = Pure JS local simulation; 'inject' = Server + injected events
111
+ eventsPerSecond: 2,
112
+ loop: true,
113
+ errorRate: 0.1, // 10% chance to simulate connection drops and trigger reconnects
114
+ data: [
115
+ { type: 'open', statusCode: 200 },
116
+ { type: 'message', data: 'Initial greeting' },
117
+ { event: 'user-updated', data: '{"id":123,"status":"online"}', delayMs: 1500 },
118
+ { type: 'message', data: 'Periodic heartbeat comments' },
119
+ ]
120
+ }
121
+ });
122
+ ```
123
+
124
+ ### Manual Injector
125
+ Inject custom testing events programmatically at runtime to mock specific app situations:
126
+
127
+ ```tsx
128
+ sse.injectMockEvent({
129
+ type: 'message',
130
+ event: 'alert',
131
+ data: 'System maintenance warning!'
132
+ });
133
+ ```
134
+
135
+ ---
129
136
 
130
- This project employs a robust **Producer-Consumer** model:
137
+ ## Advanced Operations
131
138
 
132
- 1. **Native (Producer)**: Collects data from the socket on a dedicated Background Thread, handling all backpressure logic.
133
- 2. **Nitro (Bridge)**: Snapshots data and securely transports it via the JSI CallInvoker.
134
- 3. **JavaScript (Consumer)**: Consumes data in batches, ensuring the UI Loop remains buttery smooth even under heavy load.
139
+ * **`updateHeaders(headers)`**: Change request headers (e.g., updating expired authentication tokens) without closing and opening the connection manually.
140
+ * **`setLastProcessedId(id)`**: Update the native parser's last processed event ID. Native uses this ID as the `Last-Event-ID` header on next reconnection.
141
+ * **`restart()`**: Immediately tears down the current network socket and initializes fresh handshake sequence.
142
+ * **`flush()`**: Explicitly forces dispatch of currently buffered events, ignoring `batchingIntervalMs`.
135
143
 
136
144
  ---
137
145
 
138
- ## 📄 License
146
+ ## License
139
147
 
140
148
  MIT
141
149
 
142
150
  ---
143
151
 
144
- Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)
152
+ *Made with [create-react-native-library](https://github.com/callstack/react-native-builder-bob)*
@@ -60,6 +60,7 @@ class NitroSse: HybridNitroSseSpec {
60
60
  private var connectionAttemptVersion: Int = 0
61
61
  #if os(iOS)
62
62
  private var backgroundTaskIdentifier: UIBackgroundTaskIdentifier = .invalid
63
+ private var isBackgroundTaskActive: Bool = false
63
64
  #endif
64
65
  private var wasRunningBeforeHibernation: Bool = false
65
66
  private var isAppInBackground: Bool = false
@@ -174,12 +175,29 @@ class NitroSse: HybridNitroSseSpec {
174
175
  if config.backgroundExecution == true {
175
176
  print("[NitroSse] App backgrounded. backgroundExecution is true, keeping connection alive.")
176
177
  #if os(iOS)
177
- // Start a background task to tell the OS we want to keep running
178
178
  self.cleanupBackgroundTask()
179
- self.backgroundTaskIdentifier = UIApplication.shared.beginBackgroundTask(withName: "NitroSse-KeepAlive") { [weak self] in
179
+ self.isBackgroundTaskActive = true
180
+
181
+ DispatchQueue.main.async { [weak self] in
182
+ let taskId = UIApplication.shared.beginBackgroundTask(withName: "NitroSse-KeepAlive") {
183
+ self?.sseQueue.async {
184
+ print("[NitroSse] Background task expired. Hibernating now.")
185
+ self?.hibernateConnection()
186
+ }
187
+ }
180
188
  self?.sseQueue.async {
181
- print("[NitroSse] Background task expired. Hibernating now.")
182
- self?.hibernateConnection()
189
+ if self?.isBackgroundTaskActive == true {
190
+ if let oldTaskId = self?.backgroundTaskIdentifier, oldTaskId != .invalid {
191
+ DispatchQueue.main.async {
192
+ UIApplication.shared.endBackgroundTask(oldTaskId)
193
+ }
194
+ }
195
+ self?.backgroundTaskIdentifier = taskId
196
+ } else {
197
+ DispatchQueue.main.async {
198
+ UIApplication.shared.endBackgroundTask(taskId)
199
+ }
200
+ }
183
201
  }
184
202
  }
185
203
  #endif
@@ -225,9 +243,13 @@ class NitroSse: HybridNitroSseSpec {
225
243
  private func cleanupBackgroundTask() {
226
244
  dispatchPrecondition(condition: .onQueue(sseQueue))
227
245
  #if os(iOS)
228
- if self.backgroundTaskIdentifier != .invalid {
229
- UIApplication.shared.endBackgroundTask(self.backgroundTaskIdentifier)
246
+ self.isBackgroundTaskActive = false
247
+ let taskId = self.backgroundTaskIdentifier
248
+ if taskId != .invalid {
230
249
  self.backgroundTaskIdentifier = .invalid
250
+ DispatchQueue.main.async {
251
+ UIApplication.shared.endBackgroundTask(taskId)
252
+ }
231
253
  }
232
254
  #endif
233
255
  }
@@ -646,6 +668,15 @@ class NitroSse: HybridNitroSseSpec {
646
668
  parent.backoffCounter = 0
647
669
  parent.currentReconnectAttempts = 0
648
670
  parent.consecutiveAuthErrors = 0
671
+
672
+ NitroSseNetworkInspector.reportResponseStart(
673
+ parent.requestId,
674
+ url: parent.config?.url,
675
+ response: nil,
676
+ statusCode: 200,
677
+ headers: parent.config?.headers ?? [:]
678
+ )
679
+
649
680
  parent.pushEventToBuffer(SseEvent(type: .open, data: nil, parsedData: nil, id: nil, event: nil, message: nil, statusCode: 200, retry: nil))
650
681
  }
651
682
  }
@@ -695,13 +726,26 @@ class NitroSse: HybridNitroSseSpec {
695
726
  guard let parent = self.parent, source === parent.eventSource else { return }
696
727
  parent.sseQueue.async { [weak parent] in
697
728
  guard let parent = parent, parent.isRunning, self.attemptVersion == parent.connectionAttemptVersion else { return }
729
+
698
730
  let nsError = error as NSError
699
- let statusCode = nsError.code
731
+ var statusCode = nsError.code
732
+ if let responseError = error as? UnsuccessfulResponseError {
733
+ statusCode = responseError.responseCode
734
+ }
700
735
 
701
736
  parent.reconnectCount += 1
702
737
  parent.lastErrorTime = Date().timeIntervalSince1970 * 1000
703
738
  parent.lastErrorCode = "\(nsError.domain)(\(statusCode))"
704
739
 
740
+ if statusCode >= 100 && statusCode < 600 {
741
+ NitroSseNetworkInspector.reportResponseStart(
742
+ parent.requestId,
743
+ url: parent.config?.url,
744
+ response: nil,
745
+ statusCode: statusCode,
746
+ headers: [:]
747
+ )
748
+ }
705
749
  NitroSseNetworkInspector.reportRequestFailed(parent.requestId, cancelled: false)
706
750
  parent.requestId = nil
707
751
 
@@ -99,7 +99,7 @@ data class SseConfig(
99
99
  }
100
100
 
101
101
  override fun hashCode(): Int {
102
- return arrayOf(
102
+ return arrayOf<Any?>(
103
103
  url,
104
104
  method,
105
105
  headers,
@@ -59,7 +59,7 @@ data class SseEvent(
59
59
  }
60
60
 
61
61
  override fun hashCode(): Int {
62
- return arrayOf(
62
+ return arrayOf<Any?>(
63
63
  type,
64
64
  data,
65
65
  parsedData,
@@ -47,7 +47,7 @@ data class SseMockConfig(
47
47
  }
48
48
 
49
49
  override fun hashCode(): Int {
50
- return arrayOf(
50
+ return arrayOf<Any?>(
51
51
  mode,
52
52
  data,
53
53
  eventsPerSecond,
@@ -63,7 +63,7 @@ data class SseMockEvent(
63
63
  }
64
64
 
65
65
  override fun hashCode(): Int {
66
- return arrayOf(
66
+ return arrayOf<Any?>(
67
67
  type,
68
68
  data,
69
69
  parsedData,
@@ -43,7 +43,7 @@ data class SseStats(
43
43
  }
44
44
 
45
45
  override fun hashCode(): Int {
46
- return arrayOf(
46
+ return arrayOf<Any?>(
47
47
  totalBytesReceived,
48
48
  reconnectCount,
49
49
  lastErrorTime,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shamilovtim/react-native-nitro-sse",
3
- "version": "2.3.0",
3
+ "version": "2.3.1",
4
4
  "description": "High-performance Server-Sent Events (SSE) for React Native, powered by Nitro Modules.",
5
5
  "main": "./lib/module/index.js",
6
6
  "types": "./lib/typescript/src/index.d.ts",
@@ -43,8 +43,9 @@
43
43
  "typecheck": "tsc",
44
44
  "test": "jest",
45
45
  "test:android": "cd example/android && ./gradlew :react-native-nitro-sse:testDebugUnitTest",
46
- "test:ios": "cd example/ios && xcodebuild test -workspace NitroSseExample.xcworkspace -scheme NitroSse -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16'",
46
+ "test:ios": "cd example/ios && xcodebuild test -workspace NitroSseExample.xcworkspace -scheme NitroSse -sdk iphonesimulator -destination 'platform=iOS Simulator,name=iPhone 16e'",
47
47
  "test:native": "yarn test:android && yarn test:ios",
48
+ "ci": "yarn lint && yarn typecheck && yarn test && yarn prepare && yarn test:native && yarn turbo run build:android build:ios",
48
49
  "release": "release-it --only-version",
49
50
  "server": "node example/sse-server.js",
50
51
  "lint": "eslint \"**/*.{js,ts,tsx}\""
@@ -78,8 +79,9 @@
78
79
  "@eslint/compat": "^1.3.2",
79
80
  "@eslint/eslintrc": "^3.3.1",
80
81
  "@eslint/js": "^9.35.0",
81
- "@react-native/babel-preset": "0.83.4",
82
- "@react-native/eslint-config": "0.83.4",
82
+ "@react-native/babel-preset": "0.85.3",
83
+ "@react-native/eslint-config": "0.85.3",
84
+ "@react-native/jest-preset": "0.85.3",
83
85
  "@release-it/conventional-changelog": "^10.0.1",
84
86
  "@types/jest": "^29.5.14",
85
87
  "@types/react": "^19.2.0",
@@ -90,12 +92,12 @@
90
92
  "eslint-plugin-prettier": "^5.5.4",
91
93
  "jest": "^29.7.0",
92
94
  "lefthook": "^2.0.3",
93
- "nitrogen": "0.35.6",
95
+ "nitrogen": "0.35.9",
94
96
  "prettier": "^2.8.8",
95
- "react": "19.2.0",
96
- "react-native": "0.83.4",
97
+ "react": "19.2.3",
98
+ "react-native": "0.85.3",
97
99
  "react-native-builder-bob": "^0.40.17",
98
- "react-native-nitro-modules": "0.35.6",
100
+ "react-native-nitro-modules": "0.35.9",
99
101
  "release-it": "^19.0.4",
100
102
  "turbo": "^2.5.6",
101
103
  "typescript": "^5.9.2"
@@ -103,7 +105,7 @@
103
105
  "peerDependencies": {
104
106
  "react": "*",
105
107
  "react-native": "*",
106
- "react-native-nitro-modules": ">= 0.35.6"
108
+ "react-native-nitro-modules": ">= 0.35.9"
107
109
  },
108
110
  "workspaces": [
109
111
  "example"
@@ -135,7 +137,7 @@
135
137
  ]
136
138
  },
137
139
  "jest": {
138
- "preset": "react-native",
140
+ "preset": "@react-native/jest-preset",
139
141
  "modulePathIgnorePatterns": [
140
142
  "<rootDir>/example/node_modules",
141
143
  "<rootDir>/lib/"