@encharm/cws 4.8.3 → 4.10.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.
Files changed (42) hide show
  1. package/CHANGELOG.md +10 -0
  2. package/CLAUDE.md +6 -2
  3. package/README.md +13 -0
  4. package/binding.gyp +4 -2
  5. package/deps/readerwriterqueue/LICENSE.md +28 -0
  6. package/deps/readerwriterqueue/README.md +186 -0
  7. package/deps/readerwriterqueue/atomicops.h +761 -0
  8. package/deps/readerwriterqueue/readerwriterqueue.h +979 -0
  9. package/dist/bindings/cws_darwin_arm64_node115.node +0 -0
  10. package/dist/bindings/cws_darwin_arm64_node127.node +0 -0
  11. package/dist/bindings/cws_darwin_arm64_node137.node +0 -0
  12. package/dist/bindings/cws_darwin_arm64_node147.node +0 -0
  13. package/dist/bindings/cws_linux_arm64_node115.node +0 -0
  14. package/dist/bindings/cws_linux_arm64_node127.node +0 -0
  15. package/dist/bindings/cws_linux_arm64_node137.node +0 -0
  16. package/dist/bindings/cws_linux_arm64_node147.node +0 -0
  17. package/dist/bindings/cws_linux_x64_node115.node +0 -0
  18. package/dist/bindings/cws_linux_x64_node127.node +0 -0
  19. package/dist/bindings/cws_linux_x64_node137.node +0 -0
  20. package/dist/bindings/cws_linux_x64_node147.node +0 -0
  21. package/dist/bindings/cws_win32_x64_node115.node +0 -0
  22. package/dist/bindings/cws_win32_x64_node127.node +0 -0
  23. package/dist/bindings/cws_win32_x64_node137.node +0 -0
  24. package/dist/bindings/cws_win32_x64_node147.node +0 -0
  25. package/dist/index.d.ts +1 -1
  26. package/dist/index.js +1 -0
  27. package/dist/shared.d.ts +1 -0
  28. package/dist/shared.js +2 -1
  29. package/package.json +2 -2
  30. package/scripts/install.js +21 -0
  31. package/src/Addon.cpp +2 -0
  32. package/src/Addon.h +6 -0
  33. package/src/Hub.cpp +4 -2
  34. package/src/MicroDeflate.h +109 -0
  35. package/src/Networking.h +5 -2
  36. package/src/SendWorker.cpp +229 -0
  37. package/src/SendWorker.h +25 -0
  38. package/src/Socket.h +195 -4
  39. package/src/WebSocket.cpp +20 -2
  40. package/src/WebSocket.h +1 -0
  41. package/src/Zlib.cpp +33 -1
  42. package/src/Zlib.h +6 -0
package/src/Socket.h CHANGED
@@ -2,6 +2,9 @@
2
2
  #define SOCKET_CWS_H
3
3
 
4
4
  #include "Networking.h"
5
+ #include "SendWorker.h"
6
+ #include "Zlib.h"
7
+ #include <vector>
5
8
  #ifndef _WIN32
6
9
  #include <sys/uio.h>
7
10
  #endif
@@ -42,6 +45,7 @@ protected:
42
45
  bool corkable = false;
43
46
  bool corkPending = false;
44
47
 
48
+ public:
45
49
  // this is not needed by HttpSocket!
46
50
  struct Queue {
47
51
  struct Message {
@@ -54,12 +58,21 @@ protected:
54
58
  // -1: heap (new char[]). Messages are allocated as raw char arrays, so every
55
59
  // allocation site must set this explicitly (the initializers above never run).
56
60
  int poolIndex = -1;
61
+ // data lives in its own new[] buffer (not inside this block); freed with delete[].
62
+ bool ownsData = false;
63
+ // data is a raw, unframed payload still to be deflated and framed (by the send
64
+ // worker, or by materializeCb if a main-thread write path reaches it first).
65
+ bool compressPending = false;
66
+ unsigned char opCode = 0;
57
67
  };
58
68
 
59
69
  Message *head = nullptr, *tail = nullptr;
60
70
  size_t totalLength = 0;
61
71
 
62
72
  static void release(NodeData *nodeData, Message *message) {
73
+ if (message->ownsData) {
74
+ delete [] (char *) message->data;
75
+ }
63
76
  if (message->poolIndex >= 0) {
64
77
  nodeData->freeSmallMemoryBlock((char *) message, message->poolIndex);
65
78
  } else {
@@ -98,6 +111,55 @@ protected:
98
111
  }
99
112
  } messageQueue;
100
113
 
114
+ // One in-flight worker-thread send per socket (see SendWorker.h). The frames it
115
+ // covers are moved out of messageQueue into the op, so nothing the worker reads
116
+ // can be freed underneath it; the main thread takes them back on completion.
117
+ // `socket` is nulled if the socket closes first, and the fd is then closed by the
118
+ // completion (never while the worker may still be inside a send on it).
119
+ struct SendOp {
120
+ Socket *socket;
121
+ NodeData *nodeData;
122
+ Queue::Message *head, *tail;
123
+ size_t bytes;
124
+ int count;
125
+ uv_os_sock_t fd;
126
+ ssize_t result;
127
+ int error;
128
+ bool closeFd;
129
+ void *deflateWindow; // this socket's sliding window (nullptr: worker's shared compressor)
130
+ void *destroyWindow; // set when the socket closed mid-flight: completion destroys it
131
+ #ifdef _WIN32
132
+ WSABUF bufs[512];
133
+ #else
134
+ struct iovec iov[512];
135
+ #endif
136
+ };
137
+ static void performSend(SendOp *op); // worker thread
138
+ static void sendComplete(SendOp *op); // main thread
139
+ static void materializeOnMain(Socket *s, Queue::Message *m, cWS::zlib::Stream *stream, bool resetAfter, char *buffer, size_t bufferSize, std::string &dynamic);
140
+ protected:
141
+ SendOp *sendOp = nullptr;
142
+ // Set by setState<STATE>(): STATE::onEnd, so the completion path can end a socket on a hard error.
143
+ void (*endCb)(Socket *) = nullptr;
144
+ // Per-socket deflate window handed to worker ops (WebSocket sets it); once an op has
145
+ // been orphaned by close, the op owns the window and the socket must not destroy it.
146
+ void *workerDeflateWindow = nullptr;
147
+ bool workerOwnsWindow = false;
148
+ // Deflates + frames a compressPending message on the main thread (set by WebSocket).
149
+ void (*materializeCb)(Socket *, Queue::Message *) = nullptr;
150
+
151
+ void materializePending() {
152
+ for (Queue::Message *m = messageQueue.front(); m; m = m->nextMessage) {
153
+ if (m->compressPending && materializeCb) {
154
+ materializeCb(this, m);
155
+ }
156
+ }
157
+ }
158
+
159
+ bool sendBusy() {
160
+ return sendOp != nullptr;
161
+ }
162
+
101
163
  int getPoll() {
102
164
  return state.poll;
103
165
  }
@@ -258,10 +320,13 @@ protected:
258
320
  }
259
321
 
260
322
  if (events & UV_WRITABLE) {
261
- if (!socket->messageQueue.empty() && (events & UV_WRITABLE)) {
323
+ if (!socket->messageQueue.empty() && (events & UV_WRITABLE) && !socket->sendBusy()) {
262
324
  socket->cork(true);
263
325
  while (true) {
264
326
  Queue::Message *messagePtr = socket->messageQueue.front();
327
+ if (messagePtr->compressPending && socket->materializeCb) {
328
+ socket->materializeCb(socket, messagePtr);
329
+ }
265
330
  ssize_t sent = ::send(socket->getFd(), messagePtr->data, messagePtr->length, MSG_NOSIGNAL);
266
331
  if (sent == (ssize_t) messagePtr->length) {
267
332
  // Pop before the callback: it may close/terminate this socket,
@@ -310,6 +375,7 @@ protected:
310
375
 
311
376
  template<class STATE>
312
377
  void setState() {
378
+ endCb = STATE::onEnd;
313
379
  if (ssl) {
314
380
  setCb(sslIoHandler<STATE>);
315
381
  } else {
@@ -334,6 +400,9 @@ protected:
334
400
  messagePtr->callbackData = nullptr;
335
401
  messagePtr->reserved = nullptr;
336
402
  messagePtr->poolIndex = -1;
403
+ messagePtr->ownsData = false;
404
+ messagePtr->compressPending = false;
405
+ messagePtr->opCode = 0;
337
406
 
338
407
  if (data) {
339
408
  memcpy((char *) messagePtr->data, data, messagePtr->length);
@@ -348,6 +417,12 @@ protected:
348
417
 
349
418
  bool write(Queue::Message *message, bool &wasTransferred) {
350
419
  ssize_t sent = 0;
420
+ if (sendBusy()) {
421
+ // a worker send is in flight: anything written now must follow it
422
+ messageQueue.push(message);
423
+ wasTransferred = true;
424
+ return true;
425
+ }
351
426
  if (messageQueue.empty()) {
352
427
 
353
428
  if (ssl) {
@@ -408,6 +483,9 @@ protected:
408
483
  messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
409
484
  messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
410
485
  messagePtr->poolIndex = memoryIndex;
486
+ messagePtr->ownsData = false;
487
+ messagePtr->compressPending = false;
488
+ messagePtr->opCode = 0;
411
489
  } else {
412
490
  messagePtr = allocMessage(estimatedLength - sizeof(Queue::Message));
413
491
  }
@@ -435,6 +513,9 @@ protected:
435
513
  messagePtr->nextMessage = nullptr;
436
514
  messagePtr->reserved = nullptr;
437
515
  messagePtr->poolIndex = memoryIndex;
516
+ messagePtr->ownsData = false;
517
+ messagePtr->compressPending = false;
518
+ messagePtr->opCode = 0;
438
519
 
439
520
  bool wasTransferred;
440
521
  if (write(messagePtr, wasTransferred)) {
@@ -486,7 +567,7 @@ protected:
486
567
 
487
568
  public:
488
569
  auto getBufferedAmount() {
489
- return messageQueue.totalLength;
570
+ return messageQueue.totalLength + (sendOp ? sendOp->bytes : 0);
490
571
  }
491
572
 
492
573
  Socket(NodeData *nodeData, Loop *loop, uv_os_sock_t fd, SSL *ssl) : Poll(loop, fd), ssl(ssl), nodeData(nodeData) {
@@ -639,6 +720,20 @@ public:
639
720
  return;
640
721
  }
641
722
 
723
+ if (!ssl && SendWorker::active()) {
724
+ if (sendBusy()) {
725
+ return; // the completion submits what has queued up meanwhile
726
+ }
727
+ if (getPoll() & UV_WRITABLE) {
728
+ change(nodeData->loop, this, setPoll(UV_READABLE));
729
+ }
730
+ if (submitToWorker()) {
731
+ return;
732
+ }
733
+ // worker queue full: fall through and send synchronously this tick
734
+ }
735
+ materializePending();
736
+
642
737
  std::vector<PendingCallback> callbacks;
643
738
  bool stalled = false;
644
739
  while (!messageQueue.empty() && !stalled) {
@@ -675,9 +770,10 @@ public:
675
770
  return;
676
771
  }
677
772
  unregisterCork();
678
- if (isClosed()) {
773
+ if (isClosed() || sendBusy()) {
679
774
  return;
680
775
  }
776
+ materializePending();
681
777
  while (!messageQueue.empty()) {
682
778
  ssize_t sent = writeQueueGathered();
683
779
  if (sent <= 0 || consumeSent(sent, nullptr)) {
@@ -701,13 +797,108 @@ public:
701
797
  }
702
798
  }
703
799
 
800
+ // Moves up to 512 queued frames into an op and hands it to the worker thread.
801
+ bool submitToWorker() {
802
+ if (sendOp || messageQueue.empty() || isClosed()) {
803
+ return true;
804
+ }
805
+ SendOp *op = new SendOp();
806
+ op->socket = this;
807
+ op->nodeData = nodeData;
808
+ op->fd = getFd();
809
+ op->head = messageQueue.head;
810
+ op->bytes = 0;
811
+ op->count = 0;
812
+ op->result = 0;
813
+ op->error = 0;
814
+ op->closeFd = false;
815
+ op->deflateWindow = workerDeflateWindow;
816
+ op->destroyWindow = nullptr;
817
+ Queue::Message *m = messageQueue.head, *last = nullptr;
818
+ while (m && op->count < 512) {
819
+ // iov entries are filled by the worker once pending messages are compressed
820
+ op->bytes += m->length;
821
+ op->count++;
822
+ last = m;
823
+ m = m->nextMessage;
824
+ }
825
+ op->tail = last;
826
+ last->nextMessage = nullptr;
827
+ messageQueue.head = m;
828
+ if (!m) {
829
+ messageQueue.tail = nullptr;
830
+ messageQueue.totalLength = 0;
831
+ } else {
832
+ messageQueue.totalLength -= op->bytes;
833
+ }
834
+ if (!SendWorker::submit(op)) {
835
+ requeueFront(op);
836
+ delete op;
837
+ return false;
838
+ }
839
+ sendOp = op;
840
+ return true;
841
+ }
842
+
843
+ // Queues a raw payload to be deflated and framed on the send worker (WebSocket::send
844
+ // uses this for compressed messages when the worker is active). The payload is copied
845
+ // because callers may reuse their buffer as soon as send() returns.
846
+ void enqueueCompressPending(const char *payload, size_t length, unsigned char opCode,
847
+ void(*callback)(void *socket, void *data, bool cancelled, void *reserved), void *callbackData) {
848
+ int memoryIndex = nodeData->getMemoryBlockIndex(sizeof(Queue::Message));
849
+ Queue::Message *messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
850
+ char *raw = new char[length ? length : 1];
851
+ memcpy(raw, payload, length);
852
+ messagePtr->data = raw;
853
+ messagePtr->length = length;
854
+ messagePtr->nextMessage = nullptr;
855
+ messagePtr->callback = callback;
856
+ messagePtr->callbackData = callbackData;
857
+ messagePtr->reserved = nullptr;
858
+ messagePtr->poolIndex = memoryIndex;
859
+ messagePtr->ownsData = true;
860
+ messagePtr->compressPending = true;
861
+ messagePtr->opCode = opCode;
862
+ enqueue(messagePtr);
863
+ if (!corkPending) {
864
+ corkPending = true;
865
+ nodeData->corkState->pending.push_back(this);
866
+ }
867
+ }
868
+
869
+ // Puts what is left of an op back at the head of the queue, in order.
870
+ void requeueFront(SendOp *op) {
871
+ if (!op->head) {
872
+ return;
873
+ }
874
+ op->tail->nextMessage = messageQueue.head;
875
+ messageQueue.head = op->head;
876
+ if (!messageQueue.tail) {
877
+ messageQueue.tail = op->tail;
878
+ }
879
+ messageQueue.totalLength += op->bytes;
880
+ }
881
+
704
882
  template <class T>
705
883
  void closeSocket() {
706
884
  unregisterCork();
707
885
  uv_os_sock_t fd = getFd();
708
886
  Context *netContext = nodeData->netContext;
709
887
  stop(nodeData->loop);
710
- netContext->closeSocket(fd);
888
+ if (sendOp) {
889
+ // The worker may be inside a send on this fd: closing it now could let the
890
+ // number be reused by a new connection and receive our bytes. Orphan the op
891
+ // and let its completion close the fd.
892
+ sendOp->socket = nullptr;
893
+ sendOp->closeFd = true;
894
+ if (workerDeflateWindow) {
895
+ sendOp->destroyWindow = workerDeflateWindow; // the worker may still be using it
896
+ workerOwnsWindow = true;
897
+ }
898
+ sendOp = nullptr;
899
+ } else {
900
+ netContext->closeSocket(fd);
901
+ }
711
902
 
712
903
  if (ssl) {
713
904
  SSL_free(ssl);
package/src/WebSocket.cpp CHANGED
@@ -14,6 +14,8 @@ WebSocket<isServer>::WebSocket(bool perMessageDeflate, cS::Socket *socket) : cS:
14
14
  Group<isServer> *group = Group<isServer>::from(this);
15
15
  slidingDeflateWindow = Hub::allocateDefaultCompressor(group->deflateLevel, group->deflateWindowBits, group->deflateMemLevel);
16
16
  }
17
+ workerDeflateWindow = slidingDeflateWindow;
18
+ materializeCb = WebSocket<isServer>::materialize;
17
19
  }
18
20
 
19
21
  /*
@@ -61,9 +63,25 @@ void WebSocket<isServer>::send(const char *message, size_t length, OpCode opCode
61
63
  }
62
64
  };
63
65
 
66
+ if (transformData.compress && !ssl && corkActive() && cS::SendWorker::active()) {
67
+ // deflate + framing happen on the send worker, not on the JS thread
68
+ enqueueCompressPending(message, length, (unsigned char) opCode, (void(*)(void *, void *, bool, void *)) callback, callbackData);
69
+ return;
70
+ }
71
+
64
72
  sendTransformed<WebSocketTransformer>((char *) message, length, (void(*)(void *, void *, bool, void *)) callback, callbackData, transformData);
65
73
  }
66
74
 
75
+ // Main-thread fallback for a compressPending message that a synchronous write path reached
76
+ // (same-tick terminate, full worker queue, drain loop after a short write).
77
+ template <bool isServer>
78
+ void WebSocket<isServer>::materialize(cS::Socket *s, cS::Socket::Queue::Message *m) {
79
+ WebSocket<isServer> *webSocket = static_cast<WebSocket<isServer> *>(s);
80
+ Hub *hub = Group<isServer>::from(webSocket)->hub;
81
+ zlib::Stream *stream = webSocket->slidingDeflateWindow ? (zlib::Stream *) webSocket->slidingDeflateWindow : hub->deflationStream;
82
+ cS::Socket::materializeOnMain(s, m, stream, !webSocket->slidingDeflateWindow, hub->zlibBuffer, Hub::LARGE_BUFFER_SIZE, hub->dynamicZlibBuffer);
83
+ }
84
+
67
85
  /*
68
86
  * Prepares a single message for use with sendPrepared.
69
87
  *
@@ -320,8 +338,8 @@ void WebSocket<isServer>::onEnd(cS::Socket *s) {
320
338
 
321
339
  webSocket->nodeData->clearPendingPollChanges(webSocket);
322
340
 
323
- // remove any per-websocket zlib memory
324
- if (webSocket->slidingDeflateWindow) {
341
+ // remove any per-websocket zlib memory (unless an orphaned send op took it over)
342
+ if (webSocket->slidingDeflateWindow && !webSocket->workerOwnsWindow) {
325
343
  // this relates to Hub::allocateDefaultCompressor
326
344
  zlib::destroy((zlib::Stream *) webSocket->slidingDeflateWindow);
327
345
  webSocket->slidingDeflateWindow = nullptr;
package/src/WebSocket.h CHANGED
@@ -24,6 +24,7 @@ protected:
24
24
  unsigned char controlTipLength = 0, hasOutstandingPong = false;
25
25
 
26
26
  void *slidingDeflateWindow = nullptr;
27
+ static void materialize(cS::Socket *s, cS::Socket::Queue::Message *m);
27
28
 
28
29
  WebSocket(bool perMessageDeflate, cS::Socket *socket);
29
30
 
package/src/Zlib.cpp CHANGED
@@ -1,4 +1,7 @@
1
1
  #include "Zlib.h"
2
+ #include "MicroDeflate.h"
3
+ #include <cstdlib>
4
+ #include <cstring>
2
5
 
3
6
  #ifdef CWS_ZLIB_NG
4
7
  #include "zlib-ng.h"
@@ -133,8 +136,37 @@ char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, cha
133
136
  return buffer;
134
137
  }
135
138
 
139
+ bool microDeflateEnabled() {
140
+ static int enabled = -1;
141
+ if (enabled < 0) {
142
+ const char *v = getenv("CWS_MICRO_DEFLATE");
143
+ enabled = !(v && (!strcmp(v, "0") || !strcmp(v, "false") || !strcmp(v, "off") || !strcmp(v, "no")));
144
+ }
145
+ return enabled == 1;
146
+ }
147
+
148
+ char *deflateIndependent(Stream *fallback, char *data, size_t &length, char *buffer, size_t bufferSize, std::string &dynamic) {
149
+ if (!microDeflateEnabled()) {
150
+ return deflate(fallback, data, length, buffer, bufferSize, dynamic, true);
151
+ }
152
+ static thread_local microdeflate::Encoder *encoder = nullptr;
153
+ if (!encoder) {
154
+ encoder = new microdeflate::Encoder();
155
+ }
156
+ size_t need = microdeflate::bound(length);
157
+ uint8_t *out;
158
+ if (need <= bufferSize) {
159
+ out = (uint8_t *) buffer;
160
+ } else {
161
+ dynamic.resize(need);
162
+ out = (uint8_t *) &dynamic[0];
163
+ }
164
+ length = encoder->compress((const uint8_t *) data, length, out);
165
+ return (char *) out;
166
+ }
167
+
136
168
  const char *backend() {
137
- return CWS_ZLIB_BACKEND;
169
+ return microDeflateEnabled() ? CWS_ZLIB_BACKEND " + microdeflate" : CWS_ZLIB_BACKEND;
138
170
  }
139
171
 
140
172
  }
package/src/Zlib.h CHANGED
@@ -29,6 +29,12 @@ char *deflate(Stream *stream, char *data, size_t &length, char *buffer, size_t b
29
29
  // and length 0 on error or when the output would exceed maxPayload. Always resets.
30
30
  char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, char *buffer, size_t bufferSize, std::string &dynamic);
31
31
 
32
+ // Compresses one independent message (no context takeover). Uses the built-in
33
+ // microdeflate encoder unless CWS_MICRO_DEFLATE=0, in which case `fallback` (a
34
+ // shared zlib stream, reset after) is used. Same output contract as deflate().
35
+ char *deflateIndependent(Stream *fallback, char *data, size_t &length, char *buffer, size_t bufferSize, std::string &dynamic);
36
+ bool microDeflateEnabled();
37
+
32
38
  // e.g. "zlib-ng 2.2.4" or "zlib 1.3.1"
33
39
  const char *backend();
34
40