@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 +13 -0
- package/CLAUDE.md +1 -1
- package/README.md +4 -0
- package/dist/bindings/cws_darwin_arm64_node115.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node127.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node137.node +0 -0
- package/dist/bindings/cws_darwin_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_arm64_node115.node +0 -0
- package/dist/bindings/cws_linux_arm64_node127.node +0 -0
- package/dist/bindings/cws_linux_arm64_node137.node +0 -0
- package/dist/bindings/cws_linux_arm64_node147.node +0 -0
- package/dist/bindings/cws_linux_x64_node115.node +0 -0
- package/dist/bindings/cws_linux_x64_node127.node +0 -0
- package/dist/bindings/cws_linux_x64_node137.node +0 -0
- package/dist/bindings/cws_linux_x64_node147.node +0 -0
- package/dist/bindings/cws_win32_x64_node115.node +0 -0
- package/dist/bindings/cws_win32_x64_node127.node +0 -0
- package/dist/bindings/cws_win32_x64_node137.node +0 -0
- package/dist/bindings/cws_win32_x64_node147.node +0 -0
- package/dist/client.d.ts +1 -0
- package/dist/client.js +6 -4
- package/package.json +2 -2
- package/scripts/install.js +21 -0
- package/src/HTTPSocket.cpp +1 -1
- package/src/Socket.h +37 -7
- package/src/WebSocket.cpp +12 -3
- package/src/Zlib.cpp +5 -2
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 |
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
package/dist/client.d.ts
CHANGED
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
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
|
|
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);
|
package/src/HTTPSocket.cpp
CHANGED
|
@@ -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
|
|
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
|
-
|
|
75
|
+
release(nodeData, head);
|
|
64
76
|
head = nextMessage;
|
|
65
77
|
} else {
|
|
66
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
}
|