@encharm/cws 4.8.4 → 4.11.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 (45) hide show
  1. package/CHANGELOG.md +11 -0
  2. package/CLAUDE.md +9 -1
  3. package/README.md +27 -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/client.d.ts +3 -1
  26. package/dist/client.js +10 -2
  27. package/dist/index.d.ts +2 -1
  28. package/dist/index.js +3 -0
  29. package/dist/prepared.d.ts +6 -0
  30. package/dist/prepared.js +17 -0
  31. package/dist/shared.d.ts +1 -0
  32. package/dist/shared.js +2 -1
  33. package/package.json +1 -1
  34. package/src/Addon.cpp +2 -0
  35. package/src/Addon.h +47 -0
  36. package/src/Hub.cpp +4 -2
  37. package/src/MicroDeflate.h +109 -0
  38. package/src/Networking.h +5 -2
  39. package/src/SendWorker.cpp +229 -0
  40. package/src/SendWorker.h +25 -0
  41. package/src/Socket.h +195 -4
  42. package/src/WebSocket.cpp +169 -2
  43. package/src/WebSocket.h +22 -0
  44. package/src/Zlib.cpp +33 -1
  45. package/src/Zlib.h +6 -0
package/dist/client.d.ts CHANGED
@@ -1,6 +1,7 @@
1
1
  /// <reference types="node" />
2
2
  import { WebSocketServer } from './server';
3
3
  import { SocketAddress, ServerConfigs } from './index';
4
+ import { PreparedMessage } from './prepared';
4
5
  export declare class WebSocket {
5
6
  url: string;
6
7
  private options;
@@ -28,9 +29,10 @@ export declare class WebSocket {
28
29
  on(event: 'error', listener: (err: Error) => void): void;
29
30
  on(event: 'message', listener: (message: string | any) => void): void;
30
31
  on(event: 'close', listener: (code?: number, reason?: string) => void): void;
31
- send(message: string | Buffer, options?: {
32
+ send(message: string | Buffer | ArrayBufferView | PreparedMessage, options?: {
32
33
  binary?: boolean;
33
34
  compress?: boolean;
35
+ prefix?: string | Buffer | ArrayBufferView;
34
36
  }, cb?: (err?: Error) => void): void;
35
37
  ping(message?: string | Buffer): void;
36
38
  close(code?: number, reason?: string): void;
package/dist/client.js CHANGED
@@ -3,6 +3,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.WebSocket = void 0;
4
4
  const server_1 = require("./server");
5
5
  const shared_1 = require("./shared");
6
+ const prepared_1 = require("./prepared");
6
7
  const clientGroup = shared_1.native.client.group.create(0, shared_1.DEFAULT_PAYLOAD_LIMIT);
7
8
  function messageByteLength(message) {
8
9
  if (typeof message === 'string') {
@@ -78,6 +79,7 @@ class WebSocket {
78
79
  }
79
80
  send(message, options, cb) {
80
81
  if (this.external) {
82
+ const prepared = message instanceof prepared_1.PreparedMessage;
81
83
  let opCode = typeof message === 'string' ? shared_1.OPCODE_TEXT : shared_1.OPCODE_BINARY;
82
84
  if (options && options.binary === false) {
83
85
  opCode = shared_1.OPCODE_TEXT;
@@ -90,9 +92,15 @@ class WebSocket {
90
92
  compress = !!options.compress;
91
93
  }
92
94
  else if (this.compressThreshold !== undefined) {
93
- compress = messageByteLength(message) >= this.compressThreshold;
95
+ compress = messageByteLength(message) + (options && options.prefix ? messageByteLength(options.prefix) : 0) >= this.compressThreshold;
96
+ }
97
+ const callback = cb ? () => process.nextTick(cb) : null;
98
+ if (prepared) {
99
+ this.nativeApi.sendShared(this.external, options && options.prefix, message.external, opCode, callback, compress);
100
+ }
101
+ else {
102
+ this.nativeApi.send(this.external, message, opCode, callback, compress);
94
103
  }
95
- this.nativeApi.send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
96
104
  }
97
105
  else if (cb) {
98
106
  cb(new Error('Socket not connected'));
package/dist/index.d.ts CHANGED
@@ -30,6 +30,7 @@ export declare type ServerConfigs = {
30
30
  verifyClient?: (info: ConnectionInfo, next: VerifyClientNext) => void;
31
31
  };
32
32
  export { WebSocket } from './client';
33
+ export { PreparedMessage } from './prepared';
33
34
  export { WebSocketServer } from './server';
34
35
  export declare const secureProtocol: string;
35
- export { zlibBackend } from './shared';
36
+ export { zlibBackend, sendThread } from './shared';
package/dist/index.js CHANGED
@@ -3,8 +3,11 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.secureProtocol = void 0;
4
4
  var client_1 = require("./client");
5
5
  Object.defineProperty(exports, "WebSocket", { enumerable: true, get: function () { return client_1.WebSocket; } });
6
+ var prepared_1 = require("./prepared");
7
+ Object.defineProperty(exports, "PreparedMessage", { enumerable: true, get: function () { return prepared_1.PreparedMessage; } });
6
8
  var server_1 = require("./server");
7
9
  Object.defineProperty(exports, "WebSocketServer", { enumerable: true, get: function () { return server_1.WebSocketServer; } });
8
10
  exports.secureProtocol = 'TLSv1_2_method';
9
11
  var shared_1 = require("./shared");
10
12
  Object.defineProperty(exports, "zlibBackend", { enumerable: true, get: function () { return shared_1.zlibBackend; } });
13
+ Object.defineProperty(exports, "sendThread", { enumerable: true, get: function () { return shared_1.sendThread; } });
@@ -0,0 +1,6 @@
1
+ /// <reference types="node" />
2
+ export declare class PreparedMessage {
3
+ readonly byteLength: number;
4
+ readonly external: any;
5
+ constructor(data: Buffer | ArrayBufferView | ArrayBuffer);
6
+ }
@@ -0,0 +1,17 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.PreparedMessage = void 0;
4
+ const shared_1 = require("./shared");
5
+ const preparedRegistry = typeof global.FinalizationRegistry === 'function'
6
+ ? new global.FinalizationRegistry((external) => shared_1.native.server.releaseShared(external))
7
+ : undefined;
8
+ class PreparedMessage {
9
+ constructor(data) {
10
+ this.byteLength = data.byteLength;
11
+ this.external = shared_1.native.server.prepareShared(data);
12
+ if (preparedRegistry) {
13
+ preparedRegistry.register(this, this.external);
14
+ }
15
+ }
16
+ }
17
+ exports.PreparedMessage = PreparedMessage;
package/dist/shared.d.ts CHANGED
@@ -10,4 +10,5 @@ export declare const SLIDING_DEFLATE_WINDOW: number;
10
10
  export declare const DEFAULT_PAYLOAD_LIMIT: number;
11
11
  export declare const native: any;
12
12
  export declare const zlibBackend: string;
13
+ export declare const sendThread: string;
13
14
  export declare function setupNative(group: any, type: string, wsServer?: WebSocketServer): void;
package/dist/shared.js CHANGED
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.setupNative = exports.zlibBackend = exports.native = exports.DEFAULT_PAYLOAD_LIMIT = exports.SLIDING_DEFLATE_WINDOW = exports.PERMESSAGE_DEFLATE = exports.APP_PING_CODE = exports.OPCODE_BINARY = exports.OPCODE_PING = exports.OPCODE_TEXT = exports.noop = void 0;
3
+ exports.setupNative = exports.sendThread = exports.zlibBackend = exports.native = exports.DEFAULT_PAYLOAD_LIMIT = exports.SLIDING_DEFLATE_WINDOW = exports.PERMESSAGE_DEFLATE = exports.APP_PING_CODE = exports.OPCODE_BINARY = exports.OPCODE_PING = exports.OPCODE_TEXT = exports.noop = void 0;
4
4
  const client_1 = require("./client");
5
5
  exports.noop = () => { };
6
6
  exports.OPCODE_TEXT = 1;
@@ -20,6 +20,7 @@ exports.native = (() => {
20
20
  }
21
21
  })();
22
22
  exports.zlibBackend = exports.native.zlibBackend;
23
+ exports.sendThread = exports.native.sendThread;
23
24
  function setupNative(group, type, wsServer) {
24
25
  exports.native.setNoop(exports.noop);
25
26
  exports.native[type].group.onConnection(group, (external) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@encharm/cws",
3
- "version": "4.8.4",
3
+ "version": "4.11.0",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "description": "cWS - fast C++ WebSocket implementation for Node.js",
package/src/Addon.cpp CHANGED
@@ -24,6 +24,8 @@ void Initialize(Local<Object> exports) {
24
24
  String::NewFromUtf8(isolate, cWS::zlib::backend()).ToLocalChecked()).Check();
25
25
  hub.getNodeData()->corkState->enabled = corkEnabledFromEnv();
26
26
  registerCheck(isolate);
27
+ exports->Set(isolate->GetCurrentContext(), String::NewFromUtf8(isolate, "sendThread").ToLocalChecked(),
28
+ String::NewFromUtf8(isolate, cS::SendWorker::status()).ToLocalChecked()).Check();
27
29
  }
28
30
 
29
31
  NODE_MODULE(addon, Initialize)
package/src/Addon.h CHANGED
@@ -6,6 +6,7 @@
6
6
  #include <openssl/ssl.h>
7
7
  #include <uv.h>
8
8
  #include <cstring>
9
+ #include "SendWorker.h"
9
10
 
10
11
  #define NODE_WANT_INTERNALS 1
11
12
 
@@ -97,6 +98,11 @@ void registerCheck(Isolate *isolate) {
97
98
  });
98
99
  uv_unref((uv_handle_t *)&check);
99
100
 
101
+ // Send worker thread: only meaningful with corking, whose end-of-tick flush feeds it.
102
+ if (hub.getNodeData()->corkState->enabled) {
103
+ cS::SendWorker::init((uv_loop_t *)hub.getLoop());
104
+ }
105
+
100
106
  uv_prepare_init((uv_loop_t *)hub.getLoop(), &corkPrepare);
101
107
  uv_prepare_start(&corkPrepare, [](uv_prepare_t *) {
102
108
  cS::Socket::flushCorked(hub.getNodeData());
@@ -638,6 +644,44 @@ void sendPrepared(const FunctionCallbackInfo<Value> &args) {
638
644
  ->Value());
639
645
  }
640
646
 
647
+ template <bool isServer>
648
+ void prepareShared(const FunctionCallbackInfo<Value> &args) {
649
+ NativeString nativeString(args.GetIsolate(), args[0]);
650
+ args.GetReturnValue().Set(External::New(
651
+ args.GetIsolate(),
652
+ cWS::WebSocket<isServer>::prepareShared(nativeString.getData(),
653
+ nativeString.getLength())));
654
+ }
655
+
656
+ template <bool isServer>
657
+ void releaseShared(const FunctionCallbackInfo<Value> &args) {
658
+ cWS::SharedPayload::unref(
659
+ (cWS::SharedPayload *)args[0].As<External>()->Value());
660
+ }
661
+
662
+ // (socket, prefix, payload, opCode, callback, compress)
663
+ template <bool isServer>
664
+ void sendShared(const FunctionCallbackInfo<Value> &args) {
665
+ NativeString prefix(args.GetIsolate(), args[1]);
666
+ cWS::SharedPayload *payload =
667
+ (cWS::SharedPayload *)args[2].As<External>()->Value();
668
+ cWS::OpCode opCode = (cWS::OpCode)args[3].As<Integer>()->Value();
669
+
670
+ SendCallbackData *sc = nullptr;
671
+ void (*callback)(cWS::WebSocket<isServer> *, void *, bool, void *) = nullptr;
672
+ if (args[4]->IsFunction()) {
673
+ callback = sendCallback;
674
+ sc = new SendCallbackData;
675
+ sc->jsCallback.Reset(args.GetIsolate(), Local<Function>::Cast(args[4]));
676
+ sc->isolate = args.GetIsolate();
677
+ }
678
+ bool compress = args[5].As<Boolean>()->Value();
679
+
680
+ unwrapSocket<isServer>(args[0].As<External>())
681
+ ->sendShared(prefix.getData(), prefix.getLength(), payload, opCode,
682
+ compress, callback, sc);
683
+ }
684
+
641
685
  template <bool isServer>
642
686
  void finalizeMessage(const FunctionCallbackInfo<Value> &args) {
643
687
  cWS::WebSocket<isServer>::finalizeMessage(
@@ -717,6 +761,9 @@ struct Namespace {
717
761
  NODE_SET_METHOD(object, "prepareMessage", prepareMessage<isServer>);
718
762
  NODE_SET_METHOD(object, "sendPrepared", sendPrepared<isServer>);
719
763
  NODE_SET_METHOD(object, "finalizeMessage", finalizeMessage<isServer>);
764
+ NODE_SET_METHOD(object, "prepareShared", prepareShared<isServer>);
765
+ NODE_SET_METHOD(object, "sendShared", sendShared<isServer>);
766
+ NODE_SET_METHOD(object, "releaseShared", releaseShared<isServer>);
720
767
 
721
768
  Local<Object> group = Object::New(isolate);
722
769
  NODE_SET_METHOD(group, "onConnection", onConnection<isServer>);
package/src/Hub.cpp CHANGED
@@ -15,8 +15,10 @@ zlib::Stream *Hub::allocateDefaultCompressor(int level, int windowBits, int memL
15
15
  }
16
16
 
17
17
  char *Hub::deflate(char *data, size_t &length, zlib::Stream *slidingDeflateWindow) {
18
- zlib::Stream *compressor = slidingDeflateWindow ? slidingDeflateWindow : deflationStream;
19
- return zlib::deflate(compressor, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer, !slidingDeflateWindow);
18
+ if (!slidingDeflateWindow) {
19
+ return zlib::deflateIndependent(deflationStream, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer);
20
+ }
21
+ return zlib::deflate(slidingDeflateWindow, data, length, zlibBuffer, LARGE_BUFFER_SIZE, dynamicZlibBuffer, false);
20
22
  }
21
23
 
22
24
  char *Hub::inflate(char *data, size_t &length, size_t maxPayload) {
@@ -0,0 +1,109 @@
1
+ #ifndef CWS_MICRODEFLATE_H
2
+ #define CWS_MICRODEFLATE_H
3
+
4
+ // Minimal raw-DEFLATE encoder for independent messages (permessage-deflate without
5
+ // context takeover, i.e. the shared-compressor mode). One fixed-Huffman block, greedy
6
+ // LZ77 over a 4-byte hash, no lazy matching, no stream state, and no per-message table
7
+ // clearing: stale entries are validated by position and content. Output ends the way
8
+ // Z_SYNC_FLUSH ends (empty stored block); the returned length already excludes the
9
+ // trailing 00 00 ff ff, as RFC 7692 requires. Measured on a real BSON RPC stream: same
10
+ // ratio as zlib-ng level 1 at ~1.7x its speed, mostly by skipping the 64 KB hash reset
11
+ // zlib performs per message. Decodes with any inflater.
12
+ //
13
+ // Not for context takeover: matches never reference earlier messages.
14
+
15
+ #include <cstdint>
16
+ #include <cstring>
17
+ #include <cstddef>
18
+
19
+ namespace cWS {
20
+ namespace microdeflate {
21
+
22
+ // Worst case output for `length` input bytes (all literals at 9 bits + block overhead).
23
+ inline size_t bound(size_t length) {
24
+ return length + length / 8 + 32;
25
+ }
26
+
27
+ class Encoder {
28
+ static const int HASH_BITS = 13;
29
+ uint32_t table[1 << HASH_BITS];
30
+
31
+ static const uint16_t *lenBase() { static const uint16_t t[29] = {3,4,5,6,7,8,9,10,11,13,15,17,19,23,27,31,35,43,51,59,67,83,99,115,131,163,195,227,258}; return t; }
32
+ static const uint8_t *lenExtra() { static const uint8_t t[29] = {0,0,0,0,0,0,0,0,1,1,1,1,2,2,2,2,3,3,3,3,4,4,4,4,5,5,5,5,0}; return t; }
33
+ static const uint16_t *distBase() { static const uint16_t t[30] = {1,2,3,4,5,7,9,13,17,25,33,49,65,97,129,193,257,385,513,769,1025,1537,2049,3073,4097,6145,8193,12289,16385,24577}; return t; }
34
+ static const uint8_t *distExtra() { static const uint8_t t[30] = {0,0,0,0,1,1,2,2,3,3,4,4,5,5,6,6,7,7,8,8,9,9,10,10,11,11,12,12,13,13}; return t; }
35
+
36
+ struct Tables {
37
+ uint16_t litCode[288]; uint8_t litBits[288];
38
+ uint8_t lenCode[259];
39
+ uint8_t distCode[32769];
40
+ uint8_t distCodeRev[30];
41
+ static uint32_t rev(uint32_t v, int n) { uint32_t r = 0; for (int i = 0; i < n; i++) { r = (r << 1) | (v & 1); v >>= 1; } return r; }
42
+ Tables() {
43
+ for (int i = 0; i < 288; i++) {
44
+ int bits, code;
45
+ if (i < 144) { bits = 8; code = 0x30 + i; }
46
+ else if (i < 256) { bits = 9; code = 0x190 + (i - 144); }
47
+ else if (i < 280) { bits = 7; code = i - 256; }
48
+ else { bits = 8; code = 0xC0 + (i - 280); }
49
+ litCode[i] = (uint16_t) rev(code, bits); litBits[i] = (uint8_t) bits;
50
+ }
51
+ for (int c = 0; c < 29; c++) for (int l = lenBase()[c]; l < (c < 28 ? lenBase()[c + 1] : 259); l++) lenCode[l] = (uint8_t) c;
52
+ for (int c = 0; c < 30; c++) { for (int d = distBase()[c]; d < (c < 29 ? distBase()[c + 1] : 32769); d++) distCode[d] = (uint8_t) c; distCodeRev[c] = (uint8_t) rev(c, 5); }
53
+ }
54
+ };
55
+ static const Tables &tables() { static const Tables t; return t; }
56
+
57
+ struct BitWriter {
58
+ uint8_t *out; size_t pos = 0; uint64_t acc = 0; int n = 0;
59
+ inline void put(uint32_t v, int bits) { acc |= (uint64_t) v << n; n += bits; while (n >= 8) { out[pos++] = (uint8_t) acc; acc >>= 8; n -= 8; } }
60
+ inline void flushByte() { if (n) { out[pos++] = (uint8_t) acc; acc = 0; n = 0; } }
61
+ };
62
+
63
+ static inline uint32_t hash4(const uint8_t *p) { uint32_t v; memcpy(&v, p, 4); return (v * 2654435761u) >> (32 - HASH_BITS); }
64
+
65
+ public:
66
+ Encoder() { memset(table, 0xff, sizeof(table)); tables(); }
67
+
68
+ // `out` must have room for bound(length) bytes. Returns the compressed length
69
+ // (without the 4-byte sync-flush tail).
70
+ size_t compress(const uint8_t *in, size_t length, uint8_t *out) {
71
+ const Tables &t = tables();
72
+ BitWriter w{out};
73
+ w.put(0, 1); w.put(1, 2); // BFINAL=0, BTYPE=01 (fixed Huffman)
74
+ size_t i = 0;
75
+ const size_t limit = length >= 4 ? length - 4 : 0;
76
+ while (i < limit) {
77
+ uint32_t h = hash4(in + i);
78
+ uint32_t cand = table[h];
79
+ table[h] = (uint32_t) i;
80
+ if (cand < i && i - cand <= 32768 && memcmp(in + cand, in + i, 4) == 0) {
81
+ size_t maxLen = length - i; if (maxLen > 258) maxLen = 258;
82
+ size_t m = 4;
83
+ while (m < maxLen && in[cand + m] == in[i + m]) m++;
84
+ uint32_t dist = (uint32_t) (i - cand);
85
+ int lc = t.lenCode[m];
86
+ w.put(t.litCode[257 + lc], t.litBits[257 + lc]);
87
+ if (lenExtra()[lc]) w.put((uint32_t) (m - lenBase()[lc]), lenExtra()[lc]);
88
+ int dc = t.distCode[dist];
89
+ w.put(t.distCodeRev[dc], 5);
90
+ if (distExtra()[dc]) w.put(dist - distBase()[dc], distExtra()[dc]);
91
+ if (i + 1 < limit) table[hash4(in + i + 1)] = (uint32_t) (i + 1);
92
+ i += m;
93
+ } else {
94
+ w.put(t.litCode[in[i]], t.litBits[in[i]]);
95
+ i++;
96
+ }
97
+ }
98
+ for (; i < length; i++) w.put(t.litCode[in[i]], t.litBits[in[i]]);
99
+ w.put(t.litCode[256], t.litBits[256]); // end of block
100
+ w.put(0, 1); w.put(0, 2); w.flushByte(); // empty stored block: BFINAL=0, BTYPE=00, then byte-align
101
+ out[w.pos++] = 0; out[w.pos++] = 0; out[w.pos++] = 0xff; out[w.pos++] = 0xff;
102
+ return w.pos - 4;
103
+ }
104
+ };
105
+
106
+ }
107
+ }
108
+
109
+ #endif // CWS_MICRODEFLATE_H
package/src/Networking.h CHANGED
@@ -34,8 +34,11 @@
34
34
  #define be64toh(x) __builtin_bswap64(x)
35
35
  #else
36
36
  #define __thread __declspec(thread)
37
- #define htobe64(x) htonll(x)
38
- #define be64toh(x) ntohll(x)
37
+ // Windows is little-endian; the intrinsic avoids depending on WinSock's htonll/ntohll,
38
+ // whose declaration depends on SDK version macros that not every translation unit sets.
39
+ #include <stdlib.h>
40
+ #define htobe64(x) _byteswap_uint64(x)
41
+ #define be64toh(x) _byteswap_uint64(x)
39
42
  #define pthread_t DWORD
40
43
  #define pthread_self GetCurrentThreadId
41
44
  #endif
@@ -0,0 +1,229 @@
1
+ #include "SendWorker.h"
2
+ #include "Socket.h"
3
+ #include "Zlib.h"
4
+ #include "cWS.h"
5
+ #include "readerwriterqueue.h"
6
+ #include <thread>
7
+ #include <cstdlib>
8
+ #include <cstring>
9
+ #include <cerrno>
10
+ #include <cctype>
11
+ #include <string>
12
+
13
+ namespace cS {
14
+
15
+ namespace {
16
+ bool workerActive = false;
17
+ const char *workerStatus = "not initialised";
18
+ moodycamel::BlockingReaderWriterQueue<Socket::SendOp *> *toWorker = nullptr;
19
+ moodycamel::ReaderWriterQueue<Socket::SendOp *> *toMain = nullptr;
20
+ uv_async_t mainWake;
21
+
22
+ void onMainWake(uv_async_t *) {
23
+ Socket::SendOp *op;
24
+ while (toMain->try_dequeue(op)) {
25
+ Socket::sendComplete(op);
26
+ }
27
+ }
28
+
29
+ void workerLoop() {
30
+ Socket::SendOp *op;
31
+ for (;;) {
32
+ toWorker->wait_dequeue(op);
33
+ Socket::performSend(op);
34
+ toMain->enqueue(op);
35
+ uv_async_send(&mainWake);
36
+ }
37
+ }
38
+ }
39
+
40
+ bool SendWorker::init(uv_loop_t *loop) {
41
+ const char *disabled = getenv("CWS_SEND_THREAD");
42
+ std::string value = disabled ? disabled : "";
43
+ for (char &c : value) c = (char) tolower((unsigned char) c);
44
+ if (value == "0" || value == "false" || value == "off" || value == "no") {
45
+ workerStatus = "disabled by CWS_SEND_THREAD";
46
+ return false;
47
+ }
48
+ toWorker = new moodycamel::BlockingReaderWriterQueue<Socket::SendOp *>(4096);
49
+ toMain = new moodycamel::ReaderWriterQueue<Socket::SendOp *>(4096);
50
+ uv_async_init(loop, &mainWake, onMainWake);
51
+ uv_unref((uv_handle_t *) &mainWake); // completions alone must not keep the process alive
52
+ std::thread(workerLoop).detach();
53
+ workerActive = true;
54
+ workerStatus = "active";
55
+ return true;
56
+ }
57
+
58
+ bool SendWorker::active() { return workerActive; }
59
+ const char *SendWorker::status() { return workerStatus; }
60
+
61
+ bool SendWorker::submit(void *op) {
62
+ // try_enqueue never allocates; a full queue means "send it yourself this tick".
63
+ return toWorker->try_enqueue((Socket::SendOp *) op);
64
+ }
65
+
66
+ // ---- Socket side (needs Socket internals) ----
67
+
68
+ // Deflates a pending message with the given stream and replaces its raw payload with a
69
+ // framed compressed message. Shared between the worker (its own compressor) and the
70
+ // main-thread materialize path (the hub's).
71
+ static void deflateAndFrame(Socket::Queue::Message *m, cWS::zlib::Stream *stream, bool resetAfter, char *buffer, size_t bufferSize, std::string &dynamic) {
72
+ size_t compressedLength = m->length;
73
+ // resetAfter == independent message (no context takeover): microdeflate; else the socket's window
74
+ char *deflated = resetAfter ? cWS::zlib::deflateIndependent(stream, (char *) m->data, compressedLength, buffer, bufferSize, dynamic)
75
+ : cWS::zlib::deflate(stream, (char *) m->data, compressedLength, buffer, bufferSize, dynamic, false);
76
+ const size_t HEADER = cWS::WebSocketProtocol<true, cWS::WebSocket<true>>::LONG_MESSAGE_HEADER;
77
+ char *frame = new char[compressedLength + HEADER];
78
+ size_t frameLength = cWS::WebSocketProtocol<true, cWS::WebSocket<true>>::formatMessage(frame, deflated, compressedLength, (cWS::OpCode) m->opCode, compressedLength, true);
79
+ delete [] (char *) m->data;
80
+ m->data = frame;
81
+ m->length = frameLength;
82
+ m->ownsData = true;
83
+ m->compressPending = false;
84
+ }
85
+
86
+ namespace {
87
+ // worker-thread compression state: its own shared compressor and scratch buffers
88
+ cWS::zlib::Stream *workerDeflate = nullptr;
89
+ const size_t WORKER_BUFFER = 300 * 1024;
90
+ char *workerBuffer = nullptr;
91
+ std::string workerDynamic;
92
+ }
93
+
94
+ void Socket::materializeOnMain(Socket *s, Queue::Message *m, cWS::zlib::Stream *stream, bool resetAfter, char *buffer, size_t bufferSize, std::string &dynamic) {
95
+ deflateAndFrame(m, stream, resetAfter, buffer, bufferSize, dynamic);
96
+ }
97
+
98
+ void Socket::performSend(SendOp *op) {
99
+ if (!workerDeflate) {
100
+ workerDeflate = cWS::zlib::createDeflate(1, 15, 8);
101
+ workerBuffer = new char[WORKER_BUFFER];
102
+ }
103
+ // 1. deflate + frame anything still pending, with this socket's window or the shared one
104
+ cWS::zlib::Stream *stream = op->deflateWindow ? (cWS::zlib::Stream *) op->deflateWindow : workerDeflate;
105
+ for (Queue::Message *m = op->head; m; m = m->nextMessage) {
106
+ if (m->compressPending) {
107
+ deflateAndFrame(m, stream, !op->deflateWindow, workerBuffer, WORKER_BUFFER, workerDynamic);
108
+ }
109
+ }
110
+ // 2. gather
111
+ op->count = 0;
112
+ op->bytes = 0;
113
+ for (Queue::Message *m = op->head; m; m = m->nextMessage) {
114
+ #ifdef _WIN32
115
+ op->bufs[op->count].buf = (CHAR *) m->data;
116
+ op->bufs[op->count].len = (ULONG) m->length;
117
+ #else
118
+ op->iov[op->count].iov_base = (void *) m->data;
119
+ op->iov[op->count].iov_len = m->length;
120
+ #endif
121
+ op->bytes += m->length;
122
+ op->count++;
123
+ }
124
+ #ifdef _WIN32
125
+ DWORD sent = 0;
126
+ if (WSASend(op->fd, op->bufs, (DWORD) op->count, &sent, 0, nullptr, nullptr) == SOCKET_ERROR) {
127
+ op->result = -1;
128
+ op->error = WSAGetLastError();
129
+ } else {
130
+ op->result = (ssize_t) sent;
131
+ op->error = 0;
132
+ }
133
+ #else
134
+ msghdr msg = {};
135
+ msg.msg_iov = op->iov;
136
+ msg.msg_iovlen = op->count;
137
+ op->result = ::sendmsg(op->fd, &msg, MSG_NOSIGNAL);
138
+ op->error = op->result < 0 ? errno : 0;
139
+ #endif
140
+ }
141
+
142
+ static bool sendWouldBlock(int error) {
143
+ #ifdef _WIN32
144
+ return error == WSAEWOULDBLOCK;
145
+ #else
146
+ return error == EWOULDBLOCK || error == EAGAIN;
147
+ #endif
148
+ }
149
+
150
+ void Socket::sendComplete(SendOp *op) {
151
+ Socket *s = op->socket;
152
+ if (!s) {
153
+ // socket closed while the op was in flight: release its messages, then the fd we kept open
154
+ for (Queue::Message *m = op->head; m;) {
155
+ Queue::Message *next = m->nextMessage;
156
+ if (m->callback) {
157
+ m->callback(nullptr, m->callbackData, true, m->reserved);
158
+ }
159
+ Queue::release(op->nodeData, m);
160
+ m = next;
161
+ }
162
+ if (op->closeFd) {
163
+ op->nodeData->netContext->closeSocket(op->fd);
164
+ }
165
+ if (op->destroyWindow) {
166
+ cWS::zlib::destroy((cWS::zlib::Stream *) op->destroyWindow);
167
+ }
168
+ delete op;
169
+ return;
170
+ }
171
+ s->sendOp = nullptr;
172
+
173
+ ssize_t res = op->result;
174
+ if (res < 0) {
175
+ if (sendWouldBlock(op->error)) {
176
+ res = 0;
177
+ } else {
178
+ s->requeueFront(op);
179
+ delete op;
180
+ if (s->endCb) {
181
+ s->endCb(s);
182
+ }
183
+ return;
184
+ }
185
+ }
186
+
187
+ std::vector<PendingCallback> callbacks;
188
+ size_t sent = (size_t) res, opBytes = op->bytes;
189
+ while (sent > 0 && op->head) {
190
+ Queue::Message *m = op->head;
191
+ if (sent >= m->length) {
192
+ sent -= m->length;
193
+ op->bytes -= m->length;
194
+ if (m->callback) {
195
+ callbacks.push_back({m->callback, m->callbackData, m->reserved});
196
+ }
197
+ op->head = m->nextMessage;
198
+ Queue::release(op->nodeData, m);
199
+ } else {
200
+ m->length -= sent;
201
+ m->data += sent;
202
+ op->bytes -= sent;
203
+ sent = 0;
204
+ }
205
+ }
206
+ if (!op->head) {
207
+ op->tail = nullptr;
208
+ }
209
+ bool complete = (size_t) res == opBytes;
210
+ s->requeueFront(op);
211
+ delete op;
212
+
213
+ if (!s->messageQueue.empty()) {
214
+ if (complete) {
215
+ s->submitToWorker(); // more arrived while in flight
216
+ } else if ((s->getPoll() & UV_WRITABLE) == 0) {
217
+ s->setPoll(s->getPoll() | UV_WRITABLE); // kernel buffer full: the drain loop resumes when writable
218
+ s->changePoll(s);
219
+ }
220
+ }
221
+ for (PendingCallback &c : callbacks) {
222
+ c.callback(s, c.callbackData, false, c.reserved);
223
+ if (s->isClosed()) {
224
+ break;
225
+ }
226
+ }
227
+ }
228
+
229
+ }
@@ -0,0 +1,25 @@
1
+ #ifndef CWS_SEND_WORKER_H
2
+ #define CWS_SEND_WORKER_H
3
+
4
+ // Send worker thread. The end-of-tick flush hands each socket's queued frames to
5
+ // one worker thread as an op; the worker does the gathered send syscall (the
6
+ // kernel's TCP work is charged to it, not to the JS thread) and posts the op back.
7
+ // Both directions are single-producer/single-consumer lock-free queues
8
+ // (deps/readerwriterqueue); the worker parks on a semaphore when idle and the main
9
+ // thread is woken through a uv_async. Disabled with CWS_SEND_THREAD=0.
10
+
11
+ #include <uv.h>
12
+
13
+ namespace cS {
14
+
15
+ struct SendWorker {
16
+ static bool init(uv_loop_t *loop);
17
+ static bool active();
18
+ static const char *status(); // "active", or why not
19
+ // Main thread only. Returns false if the queue is full; the caller then sends synchronously.
20
+ static bool submit(void *op);
21
+ };
22
+
23
+ }
24
+
25
+ #endif // CWS_SEND_WORKER_H