@encharm/cws 4.8.0 → 4.8.4

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/CHANGELOG.md CHANGED
@@ -1,3 +1,16 @@
1
+ ## Released 4.8.4
2
+ * `npm install` no longer rebuilds the binding from source when a prebuilt one matches the platform and Node ABI. Since the node-gyp fallback was repaired in 4.8.0, every install on a machine with a compiler was silently replacing the shipped zlib-ng binding with a Node-zlib build. `CWS_FORCE_BUILD=1` forces the source build.
3
+
4
+ ## Released 4.8.3
5
+ * Corked sends up to 1 KB use the per-loop block pool instead of malloc/free per message; queued messages remember their pool block. Also initialise the `reserved` callback argument, which was passed uninitialised for messages that hit the write queue.
6
+ * JS: resolve the native namespace once per socket instead of on every `send`/`close`/`ping`.
7
+
8
+ ## Released 4.8.2
9
+ * Accept compressed messages that end with a BFINAL=1 DEFLATE block (RFC 7692 section 7.2.3.4). Such clients (libdeflate-based and some non-zlib implementations) were disconnected on their first compressed message.
10
+
11
+ ## Released 4.8.1
12
+ * Skip the kernel-level cork/uncork (two `setsockopt` calls) around every socket read when write corking is active; those writes leave in the end-of-tick gathered write anyway. Saves 1-2 us of server CPU per read.
13
+
1
14
  ## Released 4.8.0
2
15
  * Prebuilt bindings now compress with zlib-ng 2.2.4 (vendored in `deps/zlib-ng`, statically linked, native `zng_` API so it cannot collide with Node's zlib). Measured on a real RPC stream: level 1 costs 2.2-2.5 us/KB vs 5.6-8 us/KB with zlib. The node-gyp fallback build keeps using Node's zlib through the same `src/Zlib.cpp` wrapper.
3
16
  * New `perMessageDeflate.level` option (default 2) and a `zlibBackend` export naming the compiled-in implementation.
package/CLAUDE.md CHANGED
@@ -21,7 +21,7 @@ Tests bind ports 3000 (ws) and 3001 (wss, certs in `tests/certs/`). The test fil
21
21
 
22
22
  ### Building the release binaries (multi-ABI)
23
23
 
24
- `binding.gyp` is only used by `npm install` for a local build against the running Node (Node's zlib, no zlib-ng); it must carry the same include path and defines as the Makefile or it fails silently (the install script discards the exit code). Release binaries are built by the `Makefile` (macOS/Linux) and `make.bat` (Windows), which:
24
+ `binding.gyp` is only used by `npm install` for a local build against the running Node (Node's zlib, no zlib-ng), and only when `scripts/install.js` finds no prebuilt binding for the platform/ABI (`CWS_FORCE_BUILD=1` overrides); it must carry the same include path and defines as the Makefile or it fails silently (the install script discards the exit code). Release binaries are built by the `Makefile` (macOS/Linux) and `make.bat` (Windows), which:
25
25
 
26
26
  1. Download official Node header tarballs for one pinned version per supported major into `targets/` (`VER_115`=Node 20, `VER_127`=Node 22, `VER_137`=Node 24, `VER_147`=Node 26; the number is the Node ABI / `process.versions.modules`).
27
27
  2. Compile `src/*.cpp` once per ABI with `g++`/`cl` directly, with `-I src/headers/$V` for the matching Node major.
package/README.md CHANGED
@@ -14,6 +14,10 @@ This table is true if you run ssl directly with `cws` (`Node.js`). In case if yo
14
14
 
15
15
  | cWS Version | Node 26 | Node 24 | Node 22 | Node 20 |
16
16
  |-------------|---------|----------|---------|----------
17
+ | 4.8.4 | X | X | X | X |
18
+ | 4.8.3 | X | X | X | X |
19
+ | 4.8.2 | X | X | X | X |
20
+ | 4.8.1 | X | X | X | X |
17
21
  | 4.8.0 | X | X | X | X |
18
22
  | 4.7.0 | X | X | X | X |
19
23
  | 4.6.0 | X | X | X | X |
package/dist/client.d.ts CHANGED
@@ -13,6 +13,7 @@ export declare class WebSocket {
13
13
  private external;
14
14
  private socketType;
15
15
  private compressThreshold;
16
+ private nativeApi;
16
17
  constructor(url: string, options?: any);
17
18
  get bufferedAmount(): number;
18
19
  get _socket(): SocketAddress;
package/dist/client.js CHANGED
@@ -26,8 +26,10 @@ class WebSocket {
26
26
  message: shared_1.noop
27
27
  };
28
28
  this.socketType = 'client';
29
+ this.nativeApi = shared_1.native.client;
29
30
  if (!this.url && this.options.external) {
30
31
  this.socketType = 'server';
32
+ this.nativeApi = shared_1.native.server;
31
33
  this.external = this.options.external;
32
34
  this.compressThreshold = this.options.compressThreshold;
33
35
  }
@@ -90,7 +92,7 @@ class WebSocket {
90
92
  else if (this.compressThreshold !== undefined) {
91
93
  compress = messageByteLength(message) >= this.compressThreshold;
92
94
  }
93
- shared_1.native[this.socketType].send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
95
+ this.nativeApi.send(this.external, message, opCode, cb ? () => process.nextTick(cb) : null, compress);
94
96
  }
95
97
  else if (cb) {
96
98
  cb(new Error('Socket not connected'));
@@ -98,18 +100,18 @@ class WebSocket {
98
100
  }
99
101
  ping(message) {
100
102
  if (this.external) {
101
- shared_1.native[this.socketType].send(this.external, message, shared_1.OPCODE_PING);
103
+ this.nativeApi.send(this.external, message, shared_1.OPCODE_PING);
102
104
  }
103
105
  }
104
106
  close(code = 1000, reason) {
105
107
  if (this.external) {
106
- shared_1.native[this.socketType].close(this.external, code, reason);
108
+ this.nativeApi.close(this.external, code, reason);
107
109
  this.external = null;
108
110
  }
109
111
  }
110
112
  terminate() {
111
113
  if (this.external) {
112
- shared_1.native[this.socketType].terminate(this.external);
114
+ this.nativeApi.terminate(this.external);
113
115
  this.external = null;
114
116
  }
115
117
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@encharm/cws",
3
- "version": "4.8.0",
3
+ "version": "4.8.4",
4
4
  "main": "./dist/index.js",
5
5
  "types": "./dist/index.d.ts",
6
6
  "description": "cWS - fast C++ WebSocket implementation for Node.js",
@@ -8,7 +8,7 @@
8
8
  "lint": "tslint -c tslint.json 'lib/**/*.ts'",
9
9
  "test": "nyc mocha './tests/**/*.test.ts' --exit",
10
10
  "clean": "rimraf dist/bindings/*",
11
- "install": "node-gyp rebuild > build_log.txt 2>&1 || exit 0",
11
+ "install": "node scripts/install.js",
12
12
  "build-ts": "rimraf dist/*.js && rimraf dist/*.ts && npm run lint && tsc",
13
13
  "build-cpp": "node-gyp rebuild > build_log.txt 2>&1 || exit 0"
14
14
  },
@@ -0,0 +1,21 @@
1
+ // npm/yarn `install` hook. The package ships prebuilt bindings under dist/bindings; only
2
+ // fall back to a node-gyp source build (against Node's own zlib, no zlib-ng) when none
3
+ // matches this platform and Node ABI. Set CWS_FORCE_BUILD=1 to build regardless.
4
+ // The build is best effort: failures are logged to build_log.txt and never fail the install.
5
+ 'use strict';
6
+ const fs = require('fs');
7
+ const path = require('path');
8
+ const { spawnSync } = require('child_process');
9
+
10
+ const root = path.join(__dirname, '..');
11
+ const binding = path.join(root, 'dist', 'bindings', `cws_${process.platform}_${process.arch}_node${process.versions.modules}.node`);
12
+
13
+ if (fs.existsSync(binding) && !process.env.CWS_FORCE_BUILD) {
14
+ process.exit(0);
15
+ }
16
+
17
+ const log = fs.openSync(path.join(root, 'build_log.txt'), 'w');
18
+ fs.writeSync(log, process.env.CWS_FORCE_BUILD ? `CWS_FORCE_BUILD set, building from source (prebuilt: ${binding})\n` : `no prebuilt binding at ${binding}, building from source\n`);
19
+ spawnSync('node-gyp', ['rebuild'], { cwd: root, stdio: ['ignore', log, log], shell: true });
20
+ fs.closeSync(log);
21
+ process.exit(0);
@@ -289,7 +289,7 @@ void HttpSocket<isServer>::onEnd(cS::Socket *s) {
289
289
  if (message->callback) {
290
290
  message->callback(nullptr, message->callbackData, true, nullptr);
291
291
  }
292
- httpSocket->messageQueue.pop();
292
+ httpSocket->messageQueue.pop(httpSocket->nodeData);
293
293
  }
294
294
 
295
295
  while (httpSocket->outstandingResponsesHead) {
package/src/Socket.h CHANGED
@@ -50,20 +50,32 @@ protected:
50
50
  Message *nextMessage = nullptr;
51
51
  void (*callback)(void *socket, void *data, bool cancelled, void *reserved) = nullptr;
52
52
  void *callbackData = nullptr, *reserved = nullptr;
53
+ // >= 0: block came from NodeData's small-block pool (index for freeSmallMemoryBlock);
54
+ // -1: heap (new char[]). Messages are allocated as raw char arrays, so every
55
+ // allocation site must set this explicitly (the initializers above never run).
56
+ int poolIndex = -1;
53
57
  };
54
58
 
55
59
  Message *head = nullptr, *tail = nullptr;
56
60
  size_t totalLength = 0;
57
61
 
58
- void pop()
62
+ static void release(NodeData *nodeData, Message *message) {
63
+ if (message->poolIndex >= 0) {
64
+ nodeData->freeSmallMemoryBlock((char *) message, message->poolIndex);
65
+ } else {
66
+ delete [] (char *) message;
67
+ }
68
+ }
69
+
70
+ void pop(NodeData *nodeData)
59
71
  {
60
72
  Message *nextMessage;
61
73
  if ((nextMessage = head->nextMessage)) {
62
74
  totalLength -= head->length;
63
- delete [] (char *) head;
75
+ release(nodeData, head);
64
76
  head = nextMessage;
65
77
  } else {
66
- delete [] (char *) head;
78
+ release(nodeData, head);
67
79
  head = tail = nullptr;
68
80
  totalLength = 0;
69
81
  }
@@ -174,7 +186,7 @@ protected:
174
186
  // which frees the queue (use-after-free otherwise).
175
187
  auto callback = messagePtr->callback;
176
188
  void *callbackData = messagePtr->callbackData, *reserved = messagePtr->reserved;
177
- socket->messageQueue.pop();
189
+ socket->messageQueue.pop(socket->nodeData);
178
190
  if (callback) {
179
191
  callback(p, callbackData, false, reserved);
180
192
  if (socket->isClosed()) {
@@ -256,7 +268,7 @@ protected:
256
268
  // which frees the queue (use-after-free otherwise).
257
269
  auto callback = messagePtr->callback;
258
270
  void *callbackData = messagePtr->callbackData, *reserved = messagePtr->reserved;
259
- socket->messageQueue.pop();
271
+ socket->messageQueue.pop(socket->nodeData);
260
272
  if (callback) {
261
273
  callback(p, callbackData, false, reserved);
262
274
  if (socket->isClosed()) {
@@ -318,6 +330,10 @@ protected:
318
330
  messagePtr->length = length;
319
331
  messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
320
332
  messagePtr->nextMessage = nullptr;
333
+ messagePtr->callback = nullptr;
334
+ messagePtr->callbackData = nullptr;
335
+ messagePtr->reserved = nullptr;
336
+ messagePtr->poolIndex = -1;
321
337
 
322
338
  if (data) {
323
339
  memcpy((char *) messagePtr->data, data, messagePtr->length);
@@ -385,10 +401,21 @@ protected:
385
401
  if (corkActive()) {
386
402
  // Frame now, write later: everything sent to this socket during the
387
403
  // current loop iteration goes out in one gathered write (uncork()).
388
- Queue::Message *messagePtr = allocMessage(estimatedLength - sizeof(Queue::Message));
404
+ // Small messages come from the per-loop block pool (pop() returns them).
405
+ Queue::Message *messagePtr;
406
+ if (estimatedLength <= cS::NodeData::preAllocMaxSize) {
407
+ int memoryIndex = nodeData->getMemoryBlockIndex((int) estimatedLength);
408
+ messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
409
+ messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
410
+ messagePtr->poolIndex = memoryIndex;
411
+ } else {
412
+ messagePtr = allocMessage(estimatedLength - sizeof(Queue::Message));
413
+ }
389
414
  messagePtr->length = T::transform(message, (char *) messagePtr->data, length, transformData);
415
+ messagePtr->nextMessage = nullptr;
390
416
  messagePtr->callback = callback;
391
417
  messagePtr->callbackData = callbackData;
418
+ messagePtr->reserved = nullptr;
392
419
  enqueue(messagePtr);
393
420
  if (!corkPending) {
394
421
  corkPending = true;
@@ -405,6 +432,9 @@ protected:
405
432
  Queue::Message *messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
406
433
  messagePtr->data = ((char *) messagePtr) + sizeof(Queue::Message);
407
434
  messagePtr->length = T::transform(message, (char *) messagePtr->data, length, transformData);
435
+ messagePtr->nextMessage = nullptr;
436
+ messagePtr->reserved = nullptr;
437
+ messagePtr->poolIndex = memoryIndex;
408
438
 
409
439
  bool wasTransferred;
410
440
  if (write(messagePtr, wasTransferred)) {
@@ -589,7 +619,7 @@ public:
589
619
  if (callbacks && m->callback) {
590
620
  callbacks->push_back({m->callback, m->callbackData, m->reserved});
591
621
  }
592
- messageQueue.pop();
622
+ messageQueue.pop(nodeData);
593
623
  } else {
594
624
  m->length -= sent;
595
625
  m->data += sent;
package/src/WebSocket.cpp CHANGED
@@ -151,6 +151,9 @@ void WebSocket<isServer>::sendPrepared(typename WebSocket<isServer>::PreparedMes
151
151
  Queue::Message *messagePtr = (Queue::Message *) nodeData->getSmallMemoryBlock(memoryIndex);
152
152
  messagePtr->data = preparedMessage->buffer;
153
153
  messagePtr->length = preparedMessage->length;
154
+ messagePtr->nextMessage = nullptr;
155
+ messagePtr->reserved = nullptr;
156
+ messagePtr->poolIndex = memoryIndex;
154
157
 
155
158
  bool wasTransferred;
156
159
  if (write(messagePtr, wasTransferred)) {
@@ -195,9 +198,15 @@ cS::Socket *WebSocket<isServer>::onData(cS::Socket *s, char *data, size_t length
195
198
 
196
199
  webSocket->hasOutstandingPong = false;
197
200
  if (!webSocket->isShuttingDown()) {
198
- webSocket->cork(true);
201
+ // The kernel-level cork (TCP_CORK/TCP_NOPUSH) around a read only helps when
202
+ // replies are written immediately; with write corking they leave in one gathered
203
+ // write at the end of the tick, so skip the two setsockopt calls per read.
204
+ bool kernelCork = !webSocket->corkActive();
205
+ if (kernelCork) {
206
+ webSocket->cork(true);
207
+ }
199
208
  WebSocketProtocol<isServer, WebSocket<isServer>>::consume(data, (unsigned int) length, webSocket);
200
- if (!webSocket->isClosed()) {
209
+ if (kernelCork && !webSocket->isClosed()) {
201
210
  webSocket->cork(false);
202
211
  }
203
212
  }
@@ -306,7 +315,7 @@ void WebSocket<isServer>::onEnd(cS::Socket *s) {
306
315
  if (message->callback) {
307
316
  message->callback(nullptr, message->callbackData, true, nullptr);
308
317
  }
309
- webSocket->messageQueue.pop();
318
+ webSocket->messageQueue.pop(webSocket->nodeData);
310
319
  }
311
320
 
312
321
  webSocket->nodeData->clearPendingPollChanges(webSocket);
package/src/Zlib.cpp CHANGED
@@ -106,7 +106,10 @@ char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, cha
106
106
  zs.next_out = (OutPtr) buffer;
107
107
  zs.avail_out = (unsigned int) bufferSize;
108
108
  err = ZFN(inflate)(&zs, Z_FINISH);
109
- if (!zs.avail_in) {
109
+ // Z_STREAM_END: the sender ended the message with a BFINAL=1 block, which
110
+ // RFC 7692 section 7.2.3.4 permits (libdeflate and some non-zlib clients do
111
+ // this); anything left in the input is the 4-byte tail and is ignored.
112
+ if (!zs.avail_in || err == Z_STREAM_END) {
110
113
  break;
111
114
  }
112
115
 
@@ -115,7 +118,7 @@ char *inflate(Stream *stream, char *data, size_t &length, size_t maxPayload, cha
115
118
 
116
119
  ZFN(inflateReset)(&zs);
117
120
 
118
- if ((err != Z_BUF_ERROR && err != Z_OK) || dynamic.length() > maxPayload) {
121
+ if ((err != Z_BUF_ERROR && err != Z_OK && err != Z_STREAM_END) || dynamic.length() > maxPayload) {
119
122
  length = 0;
120
123
  return nullptr;
121
124
  }