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.
- package/NitroFetchWebsockets.podspec +7 -2
- package/android/CMakeLists.txt +5 -0
- package/android/build.gradle +8 -1
- package/android/src/main/cpp/WebSocketConnection.cpp +86 -1
- package/android/src/main/cpp/WebSocketConnection.hpp +5 -0
- package/app.plugin.js +1 -0
- package/cpp/WsTrace.hpp +55 -0
- package/ios/NWWebSocketConnection.hpp +4 -0
- package/ios/NWWebSocketConnection.mm +54 -0
- package/lib/index.d.ts +1 -0
- package/lib/index.js +68 -6
- package/package.json +23 -3
- package/src/index.ts +101 -7
|
@@ -34,7 +34,7 @@ Pod::Spec.new do |s|
|
|
|
34
34
|
]
|
|
35
35
|
|
|
36
36
|
current_xcconfig = s.attributes_hash['pod_target_xcconfig'] || {}
|
|
37
|
-
|
|
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'
|
package/android/CMakeLists.txt
CHANGED
|
@@ -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"
|
package/android/build.gradle
CHANGED
|
@@ -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
|
-
|
|
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->
|
|
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
|
package/cpp/WsTrace.hpp
ADDED
|
@@ -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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
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": "
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|