react-native-nitro-websockets 0.1.0-beta.5 → 1.0.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.
@@ -34,7 +34,7 @@ Pod::Spec.new do |s|
34
34
  ]
35
35
 
36
36
  current_xcconfig = s.attributes_hash['pod_target_xcconfig'] || {}
37
- s.pod_target_xcconfig = current_xcconfig.merge({
37
+ merged_xcconfig = {
38
38
  'HEADER_SEARCH_PATHS' => [
39
39
  '$(inherited)',
40
40
  '"${PODS_TARGET_SRCROOT}/cpp"',
@@ -43,7 +43,12 @@ Pod::Spec.new do |s|
43
43
  '"$(PODS_CONFIGURATION_BUILD_DIR)/NitroFetchWebsockets"',
44
44
  ].join(' '),
45
45
  'OTHER_LDFLAGS' => '-lc++',
46
- })
46
+ }
47
+ if ENV['NITRO_WS_TRACING'] == '1'
48
+ existing_cxx_flags = current_xcconfig['OTHER_CPLUSPLUSFLAGS'] || '$(inherited)'
49
+ merged_xcconfig['OTHER_CPLUSPLUSFLAGS'] = "#{existing_cxx_flags} -DNITRO_WS_TRACING=1"
50
+ end
51
+ s.pod_target_xcconfig = current_xcconfig.merge(merged_xcconfig)
47
52
 
48
53
  s.dependency 'React-jsi'
49
54
  s.dependency 'React-callinvoker'
@@ -77,6 +77,11 @@ add_library(${PACKAGE_NAME} SHARED
77
77
  # Add Nitrogen specs
78
78
  include(${CMAKE_SOURCE_DIR}/../nitrogen/generated/android/NitroFetchWebsockets+autolinking.cmake)
79
79
 
80
+ # ── Tracing (Perfetto / systrace) ─────────────────────────────────────────────
81
+ if(NITRO_WS_TRACING)
82
+ target_compile_definitions(${PACKAGE_NAME} PRIVATE NITRO_WS_TRACING=1)
83
+ endif()
84
+
80
85
  # Include directories
81
86
  include_directories(
82
87
  "src/main/cpp"
@@ -31,6 +31,8 @@ def getExtOrDefault(name) {
31
31
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : project.properties["NitroFetchWebsockets_" + name]
32
32
  }
33
33
 
34
+ def enableTracing = (getExtOrDefault("enableTracing") ?: "false").toString().toBoolean()
35
+
34
36
  def getExtOrIntegerDefault(name) {
35
37
  return rootProject.ext.has(name) ? rootProject.ext.get(name) : (project.properties["NitroFetchWebsockets_" + name]).toInteger()
36
38
  }
@@ -45,11 +47,16 @@ android {
45
47
  minSdkVersion getExtOrIntegerDefault("minSdkVersion")
46
48
  targetSdkVersion getExtOrIntegerDefault("targetSdkVersion")
47
49
  buildConfigField "boolean", "IS_NEW_ARCHITECTURE_ENABLED", isNewArchitectureEnabled().toString()
50
+ buildConfigField "boolean", "NITRO_WS_TRACING", enableTracing.toString()
48
51
 
49
52
  externalNativeBuild {
50
53
  cmake {
51
54
  cppFlags "-frtti -fexceptions -Wall -Wextra -fstack-protector-all"
52
- arguments "-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"
55
+ def cmakeArgs = ["-DANDROID_STL=c++_shared", "-DANDROID_SUPPORT_FLEXIBLE_PAGE_SIZES=ON"]
56
+ if (enableTracing) {
57
+ cmakeArgs.add("-DNITRO_WS_TRACING=ON")
58
+ }
59
+ arguments(*cmakeArgs)
53
60
  abiFilters (*reactNativeArchitectures())
54
61
 
55
62
  buildTypes {
@@ -13,6 +13,10 @@
13
13
  #include <stdexcept>
14
14
  #include <algorithm>
15
15
 
16
+ #if defined(NITRO_WS_TRACING)
17
+ #include <android/trace.h>
18
+ #endif
19
+
16
20
  namespace margelo::nitro::nitrofetchwebsockets {
17
21
 
18
22
 
@@ -66,7 +70,7 @@ int nitroWsCallback(lws* wsi, enum lws_callback_reasons reason,
66
70
  break;
67
71
 
68
72
  case LWS_CALLBACK_CLIENT_RECEIVE:
69
- if (conn) conn->handleReceive(in, len, lws_frame_is_binary(wsi) != 0);
73
+ if (conn) conn->handleReceiveFragment(wsi, in, len);
70
74
  break;
71
75
 
72
76
  case LWS_CALLBACK_CLIENT_WRITEABLE:
@@ -132,6 +136,10 @@ void WebSocketConnection::connect(
132
136
  _url = url;
133
137
  _state = State::CONNECTING;
134
138
 
139
+ #if defined(NITRO_WS_TRACING)
140
+ ATrace_beginSection(("NitroWS connect " + url).c_str());
141
+ #endif
142
+
135
143
  ParsedUrl parsed;
136
144
  try {
137
145
  parsed = parseUrl(url);
@@ -181,6 +189,9 @@ void WebSocketConnection::connect(
181
189
  self->_wsi = wsi;
182
190
  }
183
191
  });
192
+ #if defined(NITRO_WS_TRACING)
193
+ ATrace_endSection();
194
+ #endif
184
195
  }
185
196
 
186
197
 
@@ -203,6 +214,9 @@ void WebSocketConnection::close(int code, const std::string& reason) {
203
214
 
204
215
 
205
216
  void WebSocketConnection::send(const std::string& data) {
217
+ #if defined(NITRO_WS_TRACING)
218
+ ATrace_beginSection("NitroWS send text");
219
+ #endif
206
220
  std::vector<uint8_t> buf(LWS_PRE + data.size());
207
221
  std::memcpy(buf.data() + LWS_PRE, data.c_str(), data.size());
208
222
  {
@@ -211,9 +225,15 @@ void WebSocketConnection::send(const std::string& data) {
211
225
  _bufferedAmount += data.size();
212
226
  }
213
227
  requestWrite();
228
+ #if defined(NITRO_WS_TRACING)
229
+ ATrace_endSection();
230
+ #endif
214
231
  }
215
232
 
216
233
  void WebSocketConnection::sendBinary(const uint8_t* data, size_t len) {
234
+ #if defined(NITRO_WS_TRACING)
235
+ ATrace_beginSection("NitroWS send binary");
236
+ #endif
217
237
  std::vector<uint8_t> buf(LWS_PRE + len);
218
238
  std::memcpy(buf.data() + LWS_PRE, data, len);
219
239
  {
@@ -222,6 +242,9 @@ void WebSocketConnection::sendBinary(const uint8_t* data, size_t len) {
222
242
  _bufferedAmount += len;
223
243
  }
224
244
  requestWrite();
245
+ #if defined(NITRO_WS_TRACING)
246
+ ATrace_endSection();
247
+ #endif
225
248
  }
226
249
 
227
250
  void WebSocketConnection::requestWrite() {
@@ -266,6 +289,9 @@ void WebSocketConnection::setOnError(OnError cb) {
266
289
 
267
290
 
268
291
  void WebSocketConnection::handleEstablished(lws* wsi) {
292
+ #if defined(NITRO_WS_TRACING)
293
+ ATrace_beginSection("NitroWS established");
294
+ #endif
269
295
  _wsi = wsi;
270
296
  _state = State::OPEN;
271
297
  _redirectCount = 0;
@@ -278,6 +304,9 @@ void WebSocketConnection::handleEstablished(lws* wsi) {
278
304
  } else {
279
305
  _openFired = true;
280
306
  }
307
+ #if defined(NITRO_WS_TRACING)
308
+ ATrace_endSection();
309
+ #endif
281
310
  }
282
311
 
283
312
  void WebSocketConnection::handleReceive(const void* in, size_t len, bool isBinary) {
@@ -291,6 +320,50 @@ void WebSocketConnection::handleReceive(const void* in, size_t len, bool isBinar
291
320
  }
292
321
  }
293
322
 
323
+ void WebSocketConnection::handleReceiveFragment(lws* wsi, const void* in, size_t len) {
324
+ #if defined(NITRO_WS_TRACING)
325
+ ATrace_beginSection("NitroWS receive");
326
+ #endif
327
+ bool isBinary = lws_frame_is_binary(wsi) != 0;
328
+ bool isFirst = lws_is_first_fragment(wsi) != 0;
329
+ bool isFinal = lws_is_final_fragment(wsi) != 0;
330
+
331
+ // Fast path: single-frame message (most common case)
332
+ if (isFirst && isFinal) {
333
+ handleReceive(in, len, isBinary);
334
+ #if defined(NITRO_WS_TRACING)
335
+ ATrace_endSection();
336
+ #endif
337
+ return;
338
+ }
339
+
340
+ // Multi-frame: accumulate fragments
341
+ if (isFirst) {
342
+ _rxBuf.clear();
343
+ _rxBinary = isBinary;
344
+ }
345
+
346
+ // Max message size guard — close with 1009 (Message Too Big)
347
+ if (_rxBuf.size() + len > kMaxMessageSize) {
348
+ _rxBuf.clear();
349
+ _rxBuf.shrink_to_fit();
350
+ close(1009, "message too large");
351
+ return;
352
+ }
353
+
354
+ const uint8_t* data = static_cast<const uint8_t*>(in);
355
+ _rxBuf.insert(_rxBuf.end(), data, data + len);
356
+
357
+ if (isFinal) {
358
+ handleReceive(_rxBuf.data(), _rxBuf.size(), _rxBinary);
359
+ _rxBuf.clear();
360
+ _rxBuf.shrink_to_fit();
361
+ }
362
+ #if defined(NITRO_WS_TRACING)
363
+ ATrace_endSection();
364
+ #endif
365
+ }
366
+
294
367
  int WebSocketConnection::handleWriteable(lws* wsi) {
295
368
  OutMessage msg;
296
369
  {
@@ -319,18 +392,30 @@ int WebSocketConnection::handleWriteable(lws* wsi) {
319
392
  }
320
393
 
321
394
  void WebSocketConnection::handleClose(int code, const char* reason, size_t len) {
395
+ #if defined(NITRO_WS_TRACING)
396
+ ATrace_beginSection("NitroWS close");
397
+ #endif
322
398
  _state = State::CLOSED;
323
399
  _wsi = nullptr;
324
400
  if (_onClose) {
325
401
  std::string r = (reason && len > 0) ? std::string(reason, len) : "";
326
402
  _onClose(code > 0 ? code : 1000, r, true);
327
403
  }
404
+ #if defined(NITRO_WS_TRACING)
405
+ ATrace_endSection();
406
+ #endif
328
407
  }
329
408
 
330
409
  void WebSocketConnection::handleError(const char* msg) {
410
+ #if defined(NITRO_WS_TRACING)
411
+ ATrace_beginSection("NitroWS error");
412
+ #endif
331
413
  _state = State::CLOSED;
332
414
  _wsi = nullptr;
333
415
  if (_onError) _onError(msg ? std::string(msg) : "WebSocket error");
416
+ #if defined(NITRO_WS_TRACING)
417
+ ATrace_endSection();
418
+ #endif
334
419
  }
335
420
 
336
421
  void WebSocketConnection::handleRedirect(const std::string& location) {
@@ -47,6 +47,7 @@ public:
47
47
  // lws callback handlers (internal, not part of the base interface)
48
48
  void handleEstablished(lws* wsi);
49
49
  void handleReceive(const void* in, size_t len, bool isBinary);
50
+ void handleReceiveFragment(lws* wsi, const void* in, size_t len);
50
51
  int handleWriteable(lws* wsi);
51
52
  void handleClose(int code, const char* reason, size_t len);
52
53
  void handleError(const char* msg);
@@ -72,6 +73,7 @@ private:
72
73
  std::atomic<bool> _isRedirecting{false};
73
74
  std::atomic<int> _redirectCount{0};
74
75
  static constexpr int kMaxRedirects = 5;
76
+ static constexpr size_t kMaxMessageSize = 16 * 1024 * 1024; // 16 MB
75
77
 
76
78
  struct BufferedMessage { std::vector<uint8_t> data; bool isBinary; };
77
79
  std::deque<BufferedMessage> _msgBuffer;
@@ -93,6 +95,9 @@ private:
93
95
  };
94
96
  std::optional<PendingConnect> _pendingConnect;
95
97
  std::mutex _pendingConnectMu;
98
+
99
+ std::vector<uint8_t> _rxBuf;
100
+ bool _rxBinary = false;
96
101
  };
97
102
 
98
103
  } // namespace margelo::nitro::nitrofetchwebsockets
package/app.plugin.js ADDED
@@ -0,0 +1 @@
1
+ module.exports = require('./expo/plugins/dist/index.js').default
@@ -0,0 +1,55 @@
1
+ //
2
+ // WsTrace.hpp
3
+ // Pods
4
+ //
5
+ // Created by Ritesh Shukla on 03.04.26.
6
+ //
7
+
8
+ #if defined(NITRO_WS_TRACING)
9
+
10
+ #if defined(__ANDROID__)
11
+
12
+ #include <android/trace.h>
13
+ #include <string>
14
+
15
+ #define WS_TRACE_BEGIN(label) ATrace_beginSection(label)
16
+ #define WS_TRACE_END() ATrace_endSection()
17
+
18
+ #define WS_TRACE_ASYNC_BEGIN(label, cookie) ATrace_beginAsyncSection(label, cookie)
19
+ #define WS_TRACE_ASYNC_END(label, cookie) ATrace_endAsyncSection(label, cookie)
20
+
21
+ #define WS_TRACE_INT(label, value) ATrace_setCounter(label, value)
22
+
23
+ #elif defined(__APPLE__)
24
+
25
+ #include <os/log.h>
26
+ #include <os/signpost.h>
27
+
28
+ // Shared log handle — defined in NWWebSocketConnection.mm
29
+ namespace margelo::nitro::nitrofetchwebsockets {
30
+ os_log_t wsTraceLog();
31
+ }
32
+
33
+ #define WS_TRACE_BEGIN(label) \
34
+ os_signpost_interval_begin(margelo::nitro::nitrofetchwebsockets::wsTraceLog(), \
35
+ os_signpost_id_generate(margelo::nitro::nitrofetchwebsockets::wsTraceLog()), \
36
+ "NitroWS", "%s", label)
37
+
38
+ #define WS_TRACE_END() ((void)0)
39
+
40
+ // iOS async tracing uses signpost IDs
41
+ #define WS_TRACE_ASYNC_BEGIN(label, cookie) ((void)0)
42
+ #define WS_TRACE_ASYNC_END(label, cookie) ((void)0)
43
+ #define WS_TRACE_INT(label, value) ((void)0)
44
+
45
+ #endif // __ANDROID__ / __APPLE__
46
+
47
+ #else // !NITRO_WS_TRACING
48
+
49
+ #define WS_TRACE_BEGIN(label) ((void)0)
50
+ #define WS_TRACE_END() ((void)0)
51
+ #define WS_TRACE_ASYNC_BEGIN(label, cookie) ((void)0)
52
+ #define WS_TRACE_ASYNC_END(label, cookie) ((void)0)
53
+ #define WS_TRACE_INT(label, value) ((void)0)
54
+
55
+ #endif
@@ -67,6 +67,10 @@ private:
67
67
  void scheduleReceive();
68
68
  void fireClose(int code, const std::string& reason, bool wasClean);
69
69
  void fireError(const std::string& msg);
70
+
71
+ #if defined(NITRO_WS_TRACING)
72
+ uint64_t _signpostId = 0;
73
+ #endif
70
74
  };
71
75
 
72
76
  std::shared_ptr<WebSocketConnectionBase> createNWConnection();
@@ -10,6 +10,16 @@
10
10
 
11
11
  #include <cstring>
12
12
 
13
+ #if defined(NITRO_WS_TRACING)
14
+ #include <os/log.h>
15
+ #include <os/signpost.h>
16
+
17
+ static os_log_t nitroWsLog() {
18
+ static os_log_t log = os_log_create("com.margelo.nitro.websockets", "NitroWS");
19
+ return log;
20
+ }
21
+ #endif
22
+
13
23
  // ── ObjC delegate bridging NSURLSession events to C++ via blocks ────────
14
24
 
15
25
  @interface NWWSDelegate : NSObject <NSURLSessionWebSocketDelegate, NSURLSessionTaskDelegate>
@@ -93,6 +103,13 @@ void NWWebSocketConnection::connect(
93
103
  _negotiatedProtocol.clear();
94
104
  }
95
105
  _state.store(State::CONNECTING, std::memory_order_release);
106
+
107
+ #if defined(NITRO_WS_TRACING)
108
+ os_signpost_id_t spid = os_signpost_id_generate(nitroWsLog());
109
+ _signpostId = spid;
110
+ os_signpost_interval_begin(nitroWsLog(), spid, "NitroWS",
111
+ "%{public}s", url.c_str());
112
+ #endif
96
113
  _closeFired = false;
97
114
  _openFired = false;
98
115
  _receivedCloseCode = 1005;
@@ -155,6 +172,12 @@ void NWWebSocketConnection::connect(
155
172
 
156
173
  conn->_state.store(State::OPEN, std::memory_order_release);
157
174
 
175
+ #if defined(NITRO_WS_TRACING)
176
+ os_signpost_interval_end(nitroWsLog(),
177
+ static_cast<os_signpost_id_t>(conn->_signpostId),
178
+ "NitroWS", "connected");
179
+ #endif
180
+
158
181
  if (protocol.length > 0) {
159
182
  std::lock_guard<std::mutex> lock(conn->_strMu);
160
183
  conn->_negotiatedProtocol = [protocol UTF8String];
@@ -255,6 +278,12 @@ void NWWebSocketConnection::send(const std::string& data) {
255
278
  if (_state.load(std::memory_order_acquire) != State::OPEN) return;
256
279
  if (!_impl->task) return;
257
280
 
281
+ #if defined(NITRO_WS_TRACING)
282
+ os_signpost_event_emit(nitroWsLog(),
283
+ static_cast<os_signpost_id_t>(_signpostId),
284
+ "NitroWS", "send text %zu bytes", data.size());
285
+ #endif
286
+
258
287
  size_t len = data.size();
259
288
  _bufferedAmount.fetch_add(len, std::memory_order_relaxed);
260
289
 
@@ -284,6 +313,12 @@ void NWWebSocketConnection::sendBinary(const uint8_t* data, size_t len) {
284
313
  if (_state.load(std::memory_order_acquire) != State::OPEN) return;
285
314
  if (!_impl->task) return;
286
315
 
316
+ #if defined(NITRO_WS_TRACING)
317
+ os_signpost_event_emit(nitroWsLog(),
318
+ static_cast<os_signpost_id_t>(_signpostId),
319
+ "NitroWS", "send binary %zu bytes", len);
320
+ #endif
321
+
287
322
  _bufferedAmount.fetch_add(len, std::memory_order_relaxed);
288
323
 
289
324
  NSData* nsData = [NSData dataWithBytes:data length:len];
@@ -336,6 +371,13 @@ void NWWebSocketConnection::scheduleReceive() {
336
371
  onMsg = conn->_onMessage;
337
372
  }
338
373
 
374
+ #if defined(NITRO_WS_TRACING)
375
+ os_signpost_event_emit(nitroWsLog(),
376
+ static_cast<os_signpost_id_t>(conn->_signpostId),
377
+ "NitroWS", "receive %s",
378
+ message.type == NSURLSessionWebSocketMessageTypeString ? "text" : "binary");
379
+ #endif
380
+
339
381
  switch (message.type) {
340
382
  case NSURLSessionWebSocketMessageTypeString: {
341
383
  NSData* utf8 =
@@ -446,6 +488,12 @@ void NWWebSocketConnection::fireClose(
446
488
  if (_closeFired.exchange(true, std::memory_order_acq_rel)) return;
447
489
  _state.store(State::CLOSED, std::memory_order_release);
448
490
 
491
+ #if defined(NITRO_WS_TRACING)
492
+ os_signpost_event_emit(nitroWsLog(),
493
+ static_cast<os_signpost_id_t>(_signpostId),
494
+ "NitroWS", "close code=%d clean=%d", code, wasClean ? 1 : 0);
495
+ #endif
496
+
449
497
  OnClose cb;
450
498
  {
451
499
  std::lock_guard<std::mutex> lock(_cbMu);
@@ -457,6 +505,12 @@ void NWWebSocketConnection::fireClose(
457
505
  void NWWebSocketConnection::fireError(const std::string& msg) {
458
506
  _state.store(State::CLOSED, std::memory_order_release);
459
507
 
508
+ #if defined(NITRO_WS_TRACING)
509
+ os_signpost_event_emit(nitroWsLog(),
510
+ static_cast<os_signpost_id_t>(_signpostId),
511
+ "NitroWS", "error %{public}s", msg.c_str());
512
+ #endif
513
+
460
514
  OnError cb;
461
515
  {
462
516
  std::lock_guard<std::mutex> lock(_cbMu);
package/lib/index.d.ts CHANGED
@@ -13,6 +13,7 @@ export { prewarmOnAppStart, removeFromPrewarmQueue, clearPrewarmQueue, } from '.
13
13
  */
14
14
  export declare class NitroWebSocket {
15
15
  private _ws;
16
+ private _inspectorId;
16
17
  constructor(url: string, protocols?: string | string[], headers?: Record<string, string>);
17
18
  get readyState(): import("./NitroWebSocket.nitro").WebSocketReadyState;
18
19
  get url(): string;
package/lib/index.js CHANGED
@@ -3,12 +3,22 @@ import { TextDecoder } from 'react-native-nitro-text-decoder';
3
3
  export { createWebSocket } from './NitroWebSocket.nitro';
4
4
  export { prewarmOnAppStart, removeFromPrewarmQueue, clearPrewarmQueue, } from './prewarm';
5
5
  const utf8Decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true });
6
+ // Try-import NetworkInspector from fetch package (optional peer dep)
7
+ let _inspector = null;
8
+ try {
9
+ _inspector = require('react-native-nitro-fetch').NetworkInspector;
10
+ }
11
+ catch { }
12
+ function generateWsId() {
13
+ return 'ws-' + String(Date.now()) + '-' + String(Math.random()).slice(2, 8);
14
+ }
6
15
  /**
7
16
  * Browser-compatible WebSocket wrapper backed by a Nitro HybridObject
8
17
  * using libwebsockets + mbedTLS under the hood.
9
18
  */
10
19
  export class NitroWebSocket {
11
20
  _ws;
21
+ _inspectorId;
12
22
  constructor(url, protocols, headers) {
13
23
  this._ws = NitroModules.createHybridObject('WebSocket');
14
24
  const protocolList = protocols
@@ -16,6 +26,14 @@ export class NitroWebSocket {
16
26
  ? protocols
17
27
  : [protocols]
18
28
  : [];
29
+ const headerPairs = headers
30
+ ? Object.entries(headers).map(([key, value]) => ({ key, value }))
31
+ : [];
32
+ // Record WS open in inspector
33
+ if (_inspector?.isEnabled()) {
34
+ this._inspectorId = generateWsId();
35
+ _inspector._recordWsOpen(this._inspectorId, url, protocolList, headerPairs);
36
+ }
19
37
  this._ws.connect(url, protocolList, headers ?? {});
20
38
  }
21
39
  get readyState() {
@@ -34,15 +52,30 @@ export class NitroWebSocket {
34
52
  return this._ws.extensions;
35
53
  }
36
54
  set onopen(fn) {
37
- this._ws.onOpen = fn ?? undefined;
55
+ if (fn == null) {
56
+ this._ws.onOpen = undefined;
57
+ return;
58
+ }
59
+ const inspectorId = this._inspectorId;
60
+ this._ws.onOpen = () => {
61
+ if (inspectorId && _inspector?.isEnabled()) {
62
+ _inspector._recordWsConnected(inspectorId);
63
+ }
64
+ fn();
65
+ };
38
66
  }
39
67
  set onmessage(fn) {
40
68
  if (fn == null) {
41
69
  this._ws.onMessage = undefined;
42
70
  return;
43
71
  }
72
+ const inspectorId = this._inspectorId;
44
73
  this._ws.onMessage = (native) => {
45
74
  if (native.isBinary) {
75
+ const size = native.data.byteLength;
76
+ if (inspectorId && _inspector?.isEnabled()) {
77
+ _inspector._recordWsMessage(inspectorId, 'received', `[binary ${size} bytes]`, size, true);
78
+ }
46
79
  fn({
47
80
  data: '',
48
81
  isBinary: true,
@@ -51,26 +84,55 @@ export class NitroWebSocket {
51
84
  }
52
85
  else {
53
86
  const buf = native.data;
87
+ const text = buf.byteLength === 0 ? '' : utf8Decoder.decode(buf, { stream: false });
88
+ const size = buf.byteLength;
89
+ if (inspectorId && _inspector?.isEnabled()) {
90
+ _inspector._recordWsMessage(inspectorId, 'received', text, size, false);
91
+ }
54
92
  fn({
55
- data: buf.byteLength === 0
56
- ? ''
57
- : utf8Decoder.decode(buf, { stream: false }),
93
+ data: text,
58
94
  isBinary: false,
59
95
  });
60
96
  }
61
97
  };
62
98
  }
63
99
  set onclose(fn) {
64
- this._ws.onClose = fn ?? undefined;
100
+ if (fn == null) {
101
+ this._ws.onClose = undefined;
102
+ return;
103
+ }
104
+ const inspectorId = this._inspectorId;
105
+ this._ws.onClose = (e) => {
106
+ if (inspectorId && _inspector?.isEnabled()) {
107
+ _inspector._recordWsClose(inspectorId, e.code, e.reason);
108
+ }
109
+ fn(e);
110
+ };
65
111
  }
66
112
  set onerror(fn) {
67
- this._ws.onError = fn ?? undefined;
113
+ if (fn == null) {
114
+ this._ws.onError = undefined;
115
+ return;
116
+ }
117
+ const inspectorId = this._inspectorId;
118
+ this._ws.onError = (error) => {
119
+ if (inspectorId && _inspector?.isEnabled()) {
120
+ _inspector._recordWsError(inspectorId, error);
121
+ }
122
+ fn(error);
123
+ };
68
124
  }
69
125
  send(data) {
70
126
  if (typeof data === 'string') {
127
+ if (this._inspectorId && _inspector?.isEnabled()) {
128
+ _inspector._recordWsMessage(this._inspectorId, 'sent', data, data.length, false);
129
+ }
71
130
  this._ws.send(data);
72
131
  }
73
132
  else {
133
+ if (this._inspectorId && _inspector?.isEnabled()) {
134
+ _inspector._recordWsMessage(this._inspectorId, 'sent', `[binary ${data.byteLength} bytes]`, data.byteLength, true);
135
+ }
74
136
  this._ws.sendBinary(data);
75
137
  }
76
138
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "react-native-nitro-websockets",
3
- "version": "0.1.0-beta.5",
3
+ "version": "1.0.0",
4
4
  "description": "react-native-nitro-websockets",
5
5
  "main": "lib/index",
6
6
  "module": "lib/index",
@@ -25,6 +25,7 @@
25
25
  "ios/**/*.cpp",
26
26
  "ios/**/*.swift",
27
27
  "app.plugin.js",
28
+ "expo/plugins/dist",
28
29
  "nitro.json",
29
30
  "*.podspec",
30
31
  "README.md",
@@ -58,12 +59,23 @@
58
59
  "thirdparty/mbedtls/CMakeLists.txt",
59
60
  "thirdparty/mbedtls/LICENSE"
60
61
  ],
62
+ "exports": {
63
+ ".": {
64
+ "source": "./src/index.ts",
65
+ "types": "./lib/index.d.ts",
66
+ "default": "./lib/index.js"
67
+ },
68
+ "./package.json": "./package.json",
69
+ "./app.plugin.js": "./app.plugin.js"
70
+ },
61
71
  "scripts": {
62
72
  "typecheck": "tsc --noEmit",
63
- "clean": "rm -rf android/build node_modules/**/android/build lib",
73
+ "clean": "rm -rf android/build node_modules/**/android/build lib expo/plugins/dist",
64
74
  "lint": "eslint \"**/*.{js,ts,tsx}\" --fix",
65
75
  "lint-ci": "eslint \"**/*.{js,ts,tsx}\" -f @jamesacarr/github-actions",
66
76
  "typescript": "tsc",
77
+ "build:plugin": "cd expo/plugins && npx tsc",
78
+ "release": "npm run build:plugin && release-it --only-version",
67
79
  "specs": "tsc --noEmit false && nitrogen --logLevel=\"debug\""
68
80
  },
69
81
  "keywords": [
@@ -89,7 +101,9 @@
89
101
  "registry": "https://registry.npmjs.org/"
90
102
  },
91
103
  "devDependencies": {
104
+ "@expo/config-plugins": "^9.0.0",
92
105
  "@release-it/conventional-changelog": "^9.0.2",
106
+ "expo-module-scripts": "^5.0.8",
93
107
  "@react-native/eslint-config": "^0.85.0-rc.0",
94
108
  "@types/react": "^19.2.0",
95
109
  "eslint": "^9.22.0",
@@ -108,7 +122,13 @@
108
122
  "react": "*",
109
123
  "react-native": "*",
110
124
  "react-native-nitro-modules": "*",
111
- "react-native-nitro-text-decoder": ">=0.1.0"
125
+ "react-native-nitro-text-decoder": ">=0.1.0",
126
+ "react-native-nitro-fetch": "*"
127
+ },
128
+ "peerDependenciesMeta": {
129
+ "react-native-nitro-fetch": {
130
+ "optional": true
131
+ }
112
132
  },
113
133
  "release-it": {
114
134
  "git": false,
package/src/index.ts CHANGED
@@ -28,12 +28,23 @@ export {
28
28
 
29
29
  const utf8Decoder = new TextDecoder('utf-8', { fatal: false, ignoreBOM: true })
30
30
 
31
+ // Try-import NetworkInspector from fetch package (optional peer dep)
32
+ let _inspector: any = null
33
+ try {
34
+ _inspector = require('react-native-nitro-fetch').NetworkInspector
35
+ } catch {}
36
+
37
+ function generateWsId(): string {
38
+ return 'ws-' + String(Date.now()) + '-' + String(Math.random()).slice(2, 8)
39
+ }
40
+
31
41
  /**
32
42
  * Browser-compatible WebSocket wrapper backed by a Nitro HybridObject
33
43
  * using libwebsockets + mbedTLS under the hood.
34
44
  */
35
45
  export class NitroWebSocket {
36
46
  private _ws: HybridWebSocket
47
+ private _inspectorId: string | undefined
37
48
 
38
49
  constructor(
39
50
  url: string,
@@ -46,6 +57,21 @@ export class NitroWebSocket {
46
57
  ? protocols
47
58
  : [protocols]
48
59
  : []
60
+ const headerPairs = headers
61
+ ? Object.entries(headers).map(([key, value]) => ({ key, value }))
62
+ : []
63
+
64
+ // Record WS open in inspector
65
+ if (_inspector?.isEnabled()) {
66
+ this._inspectorId = generateWsId()
67
+ _inspector._recordWsOpen(
68
+ this._inspectorId,
69
+ url,
70
+ protocolList,
71
+ headerPairs
72
+ )
73
+ }
74
+
49
75
  this._ws.connect(url, protocolList, headers ?? {})
50
76
  }
51
77
 
@@ -66,15 +92,36 @@ export class NitroWebSocket {
66
92
  }
67
93
 
68
94
  set onopen(fn: (() => void) | null) {
69
- this._ws.onOpen = fn ?? undefined
95
+ if (fn == null) {
96
+ this._ws.onOpen = undefined
97
+ return
98
+ }
99
+ const inspectorId = this._inspectorId
100
+ this._ws.onOpen = () => {
101
+ if (inspectorId && _inspector?.isEnabled()) {
102
+ _inspector._recordWsConnected(inspectorId)
103
+ }
104
+ fn()
105
+ }
70
106
  }
71
107
  set onmessage(fn: ((e: WebSocketMessageEvent) => void) | null) {
72
108
  if (fn == null) {
73
109
  this._ws.onMessage = undefined
74
110
  return
75
111
  }
112
+ const inspectorId = this._inspectorId
76
113
  this._ws.onMessage = (native: HybridWebSocketMessageEvent) => {
77
114
  if (native.isBinary) {
115
+ const size = native.data.byteLength
116
+ if (inspectorId && _inspector?.isEnabled()) {
117
+ _inspector._recordWsMessage(
118
+ inspectorId,
119
+ 'received',
120
+ `[binary ${size} bytes]`,
121
+ size,
122
+ true
123
+ )
124
+ }
78
125
  fn({
79
126
  data: '',
80
127
  isBinary: true,
@@ -82,27 +129,74 @@ export class NitroWebSocket {
82
129
  })
83
130
  } else {
84
131
  const buf = native.data
132
+ const text =
133
+ buf.byteLength === 0 ? '' : utf8Decoder.decode(buf, { stream: false })
134
+ const size = buf.byteLength
135
+ if (inspectorId && _inspector?.isEnabled()) {
136
+ _inspector._recordWsMessage(
137
+ inspectorId,
138
+ 'received',
139
+ text,
140
+ size,
141
+ false
142
+ )
143
+ }
85
144
  fn({
86
- data:
87
- buf.byteLength === 0
88
- ? ''
89
- : utf8Decoder.decode(buf, { stream: false }),
145
+ data: text,
90
146
  isBinary: false,
91
147
  })
92
148
  }
93
149
  }
94
150
  }
95
151
  set onclose(fn: ((e: NitroWSCloseEvent) => void) | null) {
96
- this._ws.onClose = fn ?? undefined
152
+ if (fn == null) {
153
+ this._ws.onClose = undefined
154
+ return
155
+ }
156
+ const inspectorId = this._inspectorId
157
+ this._ws.onClose = (e: NitroWSCloseEvent) => {
158
+ if (inspectorId && _inspector?.isEnabled()) {
159
+ _inspector._recordWsClose(inspectorId, e.code, e.reason)
160
+ }
161
+ fn(e)
162
+ }
97
163
  }
98
164
  set onerror(fn: ((error: string) => void) | null) {
99
- this._ws.onError = fn ?? undefined
165
+ if (fn == null) {
166
+ this._ws.onError = undefined
167
+ return
168
+ }
169
+ const inspectorId = this._inspectorId
170
+ this._ws.onError = (error: string) => {
171
+ if (inspectorId && _inspector?.isEnabled()) {
172
+ _inspector._recordWsError(inspectorId, error)
173
+ }
174
+ fn(error)
175
+ }
100
176
  }
101
177
 
102
178
  send(data: string | ArrayBuffer) {
103
179
  if (typeof data === 'string') {
180
+ if (this._inspectorId && _inspector?.isEnabled()) {
181
+ _inspector._recordWsMessage(
182
+ this._inspectorId,
183
+ 'sent',
184
+ data,
185
+ data.length,
186
+ false
187
+ )
188
+ }
104
189
  this._ws.send(data)
105
190
  } else {
191
+ if (this._inspectorId && _inspector?.isEnabled()) {
192
+ _inspector._recordWsMessage(
193
+ this._inspectorId,
194
+ 'sent',
195
+ `[binary ${data.byteLength} bytes]`,
196
+ data.byteLength,
197
+ true
198
+ )
199
+ }
106
200
  this._ws.sendBinary(data)
107
201
  }
108
202
  }