@culpeo/async-ws 1.0.0 → 1.1.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/CHANGELOG.md +8 -0
- package/README.md +62 -1
- package/dist/browser/index.js +58 -0
- package/dist/cjs/index.cjs +70 -0
- package/dist/esm/index.js +70 -0
- package/dist/iife/index.js +58 -0
- package/dist/index.d.ts +20 -0
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,13 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.1.0
|
|
4
|
+
|
|
5
|
+
### Minor Changes
|
|
6
|
+
|
|
7
|
+
- 89a662e: ### Adopt existing WebSocket connections
|
|
8
|
+
|
|
9
|
+
New static method `WebSocketClient.fromSocket(socket, options?)` wraps an already-open WebSocket (e.g. from a `WebSocketServer` connection event) into a ready-to-use `WebSocketClient`. Node.js only.
|
|
10
|
+
|
|
3
11
|
## 1.0.0
|
|
4
12
|
|
|
5
13
|
### Major Changes
|
package/README.md
CHANGED
|
@@ -17,6 +17,7 @@
|
|
|
17
17
|
- Configurable `maxBufferSize` with oldest-message eviction when full
|
|
18
18
|
- Connect timeout and `AbortSignal` support
|
|
19
19
|
- Keep-alive with automatic ping/pong (Node.js)
|
|
20
|
+
- Server-side socket adoption with `fromSocket()` (Node.js)
|
|
20
21
|
- Exposed WebSocket properties (`protocol`, `url`, `bufferedAmount`, `extensions`)
|
|
21
22
|
- Clean close information via `lastCloseInfo`
|
|
22
23
|
- TypeScript-first with bundled type definitions
|
|
@@ -133,7 +134,39 @@ readonly lastCloseInfo: WebSocketCloseInfo | null
|
|
|
133
134
|
|
|
134
135
|
Returns close metadata from the most recent close event, or `null` if the socket has not closed yet.
|
|
135
136
|
|
|
136
|
-
#### Methods
|
|
137
|
+
#### Static Methods
|
|
138
|
+
|
|
139
|
+
#### `fromSocket()`
|
|
140
|
+
|
|
141
|
+
```ts
|
|
142
|
+
static fromSocket(rawSocket: unknown, options?: ClientOptions): WebSocketClient
|
|
143
|
+
```
|
|
144
|
+
|
|
145
|
+
Wraps an already-open WebSocket into a `WebSocketClient` in the `"open"` state, ready to send and receive. Intended for server scenarios where a `WebSocketServer` hands you an established connection.
|
|
146
|
+
|
|
147
|
+
**Node.js only.** Throws in browser builds.
|
|
148
|
+
|
|
149
|
+
```ts
|
|
150
|
+
import { WebSocketServer } from "ws";
|
|
151
|
+
import { WebSocketClient } from "@culpeo/async-ws";
|
|
152
|
+
|
|
153
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
154
|
+
|
|
155
|
+
wss.on("connection", async (socket) => {
|
|
156
|
+
const client = WebSocketClient.fromSocket(socket);
|
|
157
|
+
|
|
158
|
+
for await (const msg of client) {
|
|
159
|
+
console.log("received:", msg.data);
|
|
160
|
+
await client.send("echo: " + msg.data);
|
|
161
|
+
}
|
|
162
|
+
});
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
The client takes ownership of the socket lifecycle — calling `close()` will close the underlying socket. Call `fromSocket()` immediately in the `connection` handler to avoid missing messages.
|
|
166
|
+
|
|
167
|
+
Accepts any WebSocket-compatible object (validated structurally, not via `instanceof`), so it works even when multiple copies of the `ws` package are installed.
|
|
168
|
+
|
|
169
|
+
#### Instance Methods
|
|
137
170
|
|
|
138
171
|
#### `connect()`
|
|
139
172
|
|
|
@@ -405,6 +438,34 @@ await client.connect("wss://example.com/ws", {
|
|
|
405
438
|
});
|
|
406
439
|
```
|
|
407
440
|
|
|
441
|
+
## Server-Side Socket Adoption (Node.js)
|
|
442
|
+
|
|
443
|
+
Use `fromSocket()` to wrap connections from a `WebSocketServer`:
|
|
444
|
+
|
|
445
|
+
```ts
|
|
446
|
+
import { WebSocketServer } from "ws";
|
|
447
|
+
import { WebSocketClient } from "@culpeo/async-ws";
|
|
448
|
+
|
|
449
|
+
const wss = new WebSocketServer({ port: 8080 });
|
|
450
|
+
|
|
451
|
+
wss.on("connection", async (socket) => {
|
|
452
|
+
const client = WebSocketClient.fromSocket(socket);
|
|
453
|
+
|
|
454
|
+
const msg = await client.receive();
|
|
455
|
+
await client.send("got: " + msg.data);
|
|
456
|
+
await client.close();
|
|
457
|
+
});
|
|
458
|
+
```
|
|
459
|
+
|
|
460
|
+
The same `ClientOptions` are supported:
|
|
461
|
+
|
|
462
|
+
```ts
|
|
463
|
+
const client = WebSocketClient.fromSocket(socket, {
|
|
464
|
+
maxBufferSize: 100,
|
|
465
|
+
keepAlive: { interval: 30000 },
|
|
466
|
+
});
|
|
467
|
+
```
|
|
468
|
+
|
|
408
469
|
## Keep-Alive (Node.js)
|
|
409
470
|
|
|
410
471
|
Enable automatic ping/pong to detect dead connections:
|
package/dist/browser/index.js
CHANGED
|
@@ -61,6 +61,10 @@ function socketPing(_socket) {
|
|
|
61
61
|
function attachPongListener(_socket, _onPong) {
|
|
62
62
|
return () => { };
|
|
63
63
|
}
|
|
64
|
+
function adoptSocket(_rawSocket) {
|
|
65
|
+
throw new Error("fromSocket() is not supported in browsers. " +
|
|
66
|
+
"Browsers cannot accept server-side WebSocket connections.");
|
|
67
|
+
}
|
|
64
68
|
|
|
65
69
|
/**
|
|
66
70
|
* Imperative WebSocket client that works in both browser and Node.js.
|
|
@@ -93,6 +97,60 @@ class WebSocketClient {
|
|
|
93
97
|
}
|
|
94
98
|
}
|
|
95
99
|
}
|
|
100
|
+
/**
|
|
101
|
+
* Adopt an already-open WebSocket (e.g. from a `WebSocketServer` connection event).
|
|
102
|
+
*
|
|
103
|
+
* Returns a `WebSocketClient` in the "open" state, ready to send/receive.
|
|
104
|
+
* The client takes ownership of the socket lifecycle: calling `close()`
|
|
105
|
+
* will close the underlying socket.
|
|
106
|
+
*
|
|
107
|
+
* **Node.js only.** Throws in browser builds.
|
|
108
|
+
*
|
|
109
|
+
* Call this immediately in the server's `connection` handler to avoid
|
|
110
|
+
* missing messages:
|
|
111
|
+
*
|
|
112
|
+
* ```ts
|
|
113
|
+
* wss.on("connection", (socket) => {
|
|
114
|
+
* const client = WebSocketClient.fromSocket(socket);
|
|
115
|
+
* const msg = await client.receive();
|
|
116
|
+
* });
|
|
117
|
+
* ```
|
|
118
|
+
*/
|
|
119
|
+
static fromSocket(rawSocket, options) {
|
|
120
|
+
const client = new WebSocketClient(options);
|
|
121
|
+
const socket = adoptSocket();
|
|
122
|
+
client.socket = socket;
|
|
123
|
+
setBinaryType(socket);
|
|
124
|
+
client.state = "open";
|
|
125
|
+
const currentConnectionId = ++client.connectionId;
|
|
126
|
+
client.removeListeners = attachListeners(socket,
|
|
127
|
+
// onOpen — already open, won't fire
|
|
128
|
+
() => { },
|
|
129
|
+
// onMessage
|
|
130
|
+
(data, binary) => {
|
|
131
|
+
client.enqueueMessage({ data, binary });
|
|
132
|
+
},
|
|
133
|
+
// onClose
|
|
134
|
+
(code, reason, wasClean) => {
|
|
135
|
+
if (currentConnectionId !== client.connectionId)
|
|
136
|
+
return;
|
|
137
|
+
client.closeInfo = { code, reason, wasClean };
|
|
138
|
+
client.state = "closed";
|
|
139
|
+
client.cleanup();
|
|
140
|
+
if (client.buffer.length === 0) {
|
|
141
|
+
client.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
142
|
+
}
|
|
143
|
+
},
|
|
144
|
+
// onError
|
|
145
|
+
(error) => {
|
|
146
|
+
if (currentConnectionId !== client.connectionId)
|
|
147
|
+
return;
|
|
148
|
+
client.terminalError = error;
|
|
149
|
+
client.rejectAllWaiters(error);
|
|
150
|
+
});
|
|
151
|
+
client.startKeepAlive();
|
|
152
|
+
return client;
|
|
153
|
+
}
|
|
96
154
|
/** Current connection state. */
|
|
97
155
|
get readyState() {
|
|
98
156
|
return this.state;
|
package/dist/cjs/index.cjs
CHANGED
|
@@ -75,6 +75,22 @@ function attachPongListener(socket, onPong) {
|
|
|
75
75
|
socket.on("pong", handler);
|
|
76
76
|
return () => socket.off("pong", handler);
|
|
77
77
|
}
|
|
78
|
+
function adoptSocket(rawSocket) {
|
|
79
|
+
const s = rawSocket;
|
|
80
|
+
if (!s ||
|
|
81
|
+
typeof s !== "object" ||
|
|
82
|
+
typeof s.send !== "function" ||
|
|
83
|
+
typeof s.close !== "function" ||
|
|
84
|
+
typeof s.addEventListener !== "function" ||
|
|
85
|
+
typeof s.removeEventListener !== "function") {
|
|
86
|
+
throw new Error("Expected a WebSocket-compatible object (must have send, close, addEventListener, removeEventListener).");
|
|
87
|
+
}
|
|
88
|
+
if (s.readyState !== ws.WebSocket.OPEN) {
|
|
89
|
+
throw new Error("Socket must be in the OPEN state to be adopted. " +
|
|
90
|
+
"Call fromSocket() immediately in the server's connection handler.");
|
|
91
|
+
}
|
|
92
|
+
return rawSocket;
|
|
93
|
+
}
|
|
78
94
|
ws.WebSocket.OPEN;
|
|
79
95
|
|
|
80
96
|
/**
|
|
@@ -112,6 +128,60 @@ class WebSocketClient {
|
|
|
112
128
|
this.keepAliveConfig = options.keepAlive;
|
|
113
129
|
}
|
|
114
130
|
}
|
|
131
|
+
/**
|
|
132
|
+
* Adopt an already-open WebSocket (e.g. from a `WebSocketServer` connection event).
|
|
133
|
+
*
|
|
134
|
+
* Returns a `WebSocketClient` in the "open" state, ready to send/receive.
|
|
135
|
+
* The client takes ownership of the socket lifecycle: calling `close()`
|
|
136
|
+
* will close the underlying socket.
|
|
137
|
+
*
|
|
138
|
+
* **Node.js only.** Throws in browser builds.
|
|
139
|
+
*
|
|
140
|
+
* Call this immediately in the server's `connection` handler to avoid
|
|
141
|
+
* missing messages:
|
|
142
|
+
*
|
|
143
|
+
* ```ts
|
|
144
|
+
* wss.on("connection", (socket) => {
|
|
145
|
+
* const client = WebSocketClient.fromSocket(socket);
|
|
146
|
+
* const msg = await client.receive();
|
|
147
|
+
* });
|
|
148
|
+
* ```
|
|
149
|
+
*/
|
|
150
|
+
static fromSocket(rawSocket, options) {
|
|
151
|
+
const client = new WebSocketClient(options);
|
|
152
|
+
const socket = adoptSocket(rawSocket);
|
|
153
|
+
client.socket = socket;
|
|
154
|
+
setBinaryType(socket);
|
|
155
|
+
client.state = "open";
|
|
156
|
+
const currentConnectionId = ++client.connectionId;
|
|
157
|
+
client.removeListeners = attachListeners(socket,
|
|
158
|
+
// onOpen — already open, won't fire
|
|
159
|
+
() => { },
|
|
160
|
+
// onMessage
|
|
161
|
+
(data, binary) => {
|
|
162
|
+
client.enqueueMessage({ data, binary });
|
|
163
|
+
},
|
|
164
|
+
// onClose
|
|
165
|
+
(code, reason, wasClean) => {
|
|
166
|
+
if (currentConnectionId !== client.connectionId)
|
|
167
|
+
return;
|
|
168
|
+
client.closeInfo = { code, reason, wasClean };
|
|
169
|
+
client.state = "closed";
|
|
170
|
+
client.cleanup();
|
|
171
|
+
if (client.buffer.length === 0) {
|
|
172
|
+
client.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
173
|
+
}
|
|
174
|
+
},
|
|
175
|
+
// onError
|
|
176
|
+
(error) => {
|
|
177
|
+
if (currentConnectionId !== client.connectionId)
|
|
178
|
+
return;
|
|
179
|
+
client.terminalError = error;
|
|
180
|
+
client.rejectAllWaiters(error);
|
|
181
|
+
});
|
|
182
|
+
client.startKeepAlive();
|
|
183
|
+
return client;
|
|
184
|
+
}
|
|
115
185
|
/** Current connection state. */
|
|
116
186
|
get readyState() {
|
|
117
187
|
return this.state;
|
package/dist/esm/index.js
CHANGED
|
@@ -73,6 +73,22 @@ function attachPongListener(socket, onPong) {
|
|
|
73
73
|
socket.on("pong", handler);
|
|
74
74
|
return () => socket.off("pong", handler);
|
|
75
75
|
}
|
|
76
|
+
function adoptSocket(rawSocket) {
|
|
77
|
+
const s = rawSocket;
|
|
78
|
+
if (!s ||
|
|
79
|
+
typeof s !== "object" ||
|
|
80
|
+
typeof s.send !== "function" ||
|
|
81
|
+
typeof s.close !== "function" ||
|
|
82
|
+
typeof s.addEventListener !== "function" ||
|
|
83
|
+
typeof s.removeEventListener !== "function") {
|
|
84
|
+
throw new Error("Expected a WebSocket-compatible object (must have send, close, addEventListener, removeEventListener).");
|
|
85
|
+
}
|
|
86
|
+
if (s.readyState !== WebSocket.OPEN) {
|
|
87
|
+
throw new Error("Socket must be in the OPEN state to be adopted. " +
|
|
88
|
+
"Call fromSocket() immediately in the server's connection handler.");
|
|
89
|
+
}
|
|
90
|
+
return rawSocket;
|
|
91
|
+
}
|
|
76
92
|
WebSocket.OPEN;
|
|
77
93
|
|
|
78
94
|
/**
|
|
@@ -110,6 +126,60 @@ class WebSocketClient {
|
|
|
110
126
|
this.keepAliveConfig = options.keepAlive;
|
|
111
127
|
}
|
|
112
128
|
}
|
|
129
|
+
/**
|
|
130
|
+
* Adopt an already-open WebSocket (e.g. from a `WebSocketServer` connection event).
|
|
131
|
+
*
|
|
132
|
+
* Returns a `WebSocketClient` in the "open" state, ready to send/receive.
|
|
133
|
+
* The client takes ownership of the socket lifecycle: calling `close()`
|
|
134
|
+
* will close the underlying socket.
|
|
135
|
+
*
|
|
136
|
+
* **Node.js only.** Throws in browser builds.
|
|
137
|
+
*
|
|
138
|
+
* Call this immediately in the server's `connection` handler to avoid
|
|
139
|
+
* missing messages:
|
|
140
|
+
*
|
|
141
|
+
* ```ts
|
|
142
|
+
* wss.on("connection", (socket) => {
|
|
143
|
+
* const client = WebSocketClient.fromSocket(socket);
|
|
144
|
+
* const msg = await client.receive();
|
|
145
|
+
* });
|
|
146
|
+
* ```
|
|
147
|
+
*/
|
|
148
|
+
static fromSocket(rawSocket, options) {
|
|
149
|
+
const client = new WebSocketClient(options);
|
|
150
|
+
const socket = adoptSocket(rawSocket);
|
|
151
|
+
client.socket = socket;
|
|
152
|
+
setBinaryType(socket);
|
|
153
|
+
client.state = "open";
|
|
154
|
+
const currentConnectionId = ++client.connectionId;
|
|
155
|
+
client.removeListeners = attachListeners(socket,
|
|
156
|
+
// onOpen — already open, won't fire
|
|
157
|
+
() => { },
|
|
158
|
+
// onMessage
|
|
159
|
+
(data, binary) => {
|
|
160
|
+
client.enqueueMessage({ data, binary });
|
|
161
|
+
},
|
|
162
|
+
// onClose
|
|
163
|
+
(code, reason, wasClean) => {
|
|
164
|
+
if (currentConnectionId !== client.connectionId)
|
|
165
|
+
return;
|
|
166
|
+
client.closeInfo = { code, reason, wasClean };
|
|
167
|
+
client.state = "closed";
|
|
168
|
+
client.cleanup();
|
|
169
|
+
if (client.buffer.length === 0) {
|
|
170
|
+
client.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
171
|
+
}
|
|
172
|
+
},
|
|
173
|
+
// onError
|
|
174
|
+
(error) => {
|
|
175
|
+
if (currentConnectionId !== client.connectionId)
|
|
176
|
+
return;
|
|
177
|
+
client.terminalError = error;
|
|
178
|
+
client.rejectAllWaiters(error);
|
|
179
|
+
});
|
|
180
|
+
client.startKeepAlive();
|
|
181
|
+
return client;
|
|
182
|
+
}
|
|
113
183
|
/** Current connection state. */
|
|
114
184
|
get readyState() {
|
|
115
185
|
return this.state;
|
package/dist/iife/index.js
CHANGED
|
@@ -64,6 +64,10 @@ var AsyncWS = (function (exports) {
|
|
|
64
64
|
function attachPongListener(_socket, _onPong) {
|
|
65
65
|
return () => { };
|
|
66
66
|
}
|
|
67
|
+
function adoptSocket(_rawSocket) {
|
|
68
|
+
throw new Error("fromSocket() is not supported in browsers. " +
|
|
69
|
+
"Browsers cannot accept server-side WebSocket connections.");
|
|
70
|
+
}
|
|
67
71
|
|
|
68
72
|
/**
|
|
69
73
|
* Imperative WebSocket client that works in both browser and Node.js.
|
|
@@ -96,6 +100,60 @@ var AsyncWS = (function (exports) {
|
|
|
96
100
|
}
|
|
97
101
|
}
|
|
98
102
|
}
|
|
103
|
+
/**
|
|
104
|
+
* Adopt an already-open WebSocket (e.g. from a `WebSocketServer` connection event).
|
|
105
|
+
*
|
|
106
|
+
* Returns a `WebSocketClient` in the "open" state, ready to send/receive.
|
|
107
|
+
* The client takes ownership of the socket lifecycle: calling `close()`
|
|
108
|
+
* will close the underlying socket.
|
|
109
|
+
*
|
|
110
|
+
* **Node.js only.** Throws in browser builds.
|
|
111
|
+
*
|
|
112
|
+
* Call this immediately in the server's `connection` handler to avoid
|
|
113
|
+
* missing messages:
|
|
114
|
+
*
|
|
115
|
+
* ```ts
|
|
116
|
+
* wss.on("connection", (socket) => {
|
|
117
|
+
* const client = WebSocketClient.fromSocket(socket);
|
|
118
|
+
* const msg = await client.receive();
|
|
119
|
+
* });
|
|
120
|
+
* ```
|
|
121
|
+
*/
|
|
122
|
+
static fromSocket(rawSocket, options) {
|
|
123
|
+
const client = new WebSocketClient(options);
|
|
124
|
+
const socket = adoptSocket();
|
|
125
|
+
client.socket = socket;
|
|
126
|
+
setBinaryType(socket);
|
|
127
|
+
client.state = "open";
|
|
128
|
+
const currentConnectionId = ++client.connectionId;
|
|
129
|
+
client.removeListeners = attachListeners(socket,
|
|
130
|
+
// onOpen — already open, won't fire
|
|
131
|
+
() => { },
|
|
132
|
+
// onMessage
|
|
133
|
+
(data, binary) => {
|
|
134
|
+
client.enqueueMessage({ data, binary });
|
|
135
|
+
},
|
|
136
|
+
// onClose
|
|
137
|
+
(code, reason, wasClean) => {
|
|
138
|
+
if (currentConnectionId !== client.connectionId)
|
|
139
|
+
return;
|
|
140
|
+
client.closeInfo = { code, reason, wasClean };
|
|
141
|
+
client.state = "closed";
|
|
142
|
+
client.cleanup();
|
|
143
|
+
if (client.buffer.length === 0) {
|
|
144
|
+
client.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
145
|
+
}
|
|
146
|
+
},
|
|
147
|
+
// onError
|
|
148
|
+
(error) => {
|
|
149
|
+
if (currentConnectionId !== client.connectionId)
|
|
150
|
+
return;
|
|
151
|
+
client.terminalError = error;
|
|
152
|
+
client.rejectAllWaiters(error);
|
|
153
|
+
});
|
|
154
|
+
client.startKeepAlive();
|
|
155
|
+
return client;
|
|
156
|
+
}
|
|
99
157
|
/** Current connection state. */
|
|
100
158
|
get readyState() {
|
|
101
159
|
return this.state;
|
package/dist/index.d.ts
CHANGED
|
@@ -84,6 +84,26 @@ declare class WebSocketClient {
|
|
|
84
84
|
private readonly maxBufferSize;
|
|
85
85
|
private readonly keepAliveConfig;
|
|
86
86
|
constructor(options?: ClientOptions);
|
|
87
|
+
/**
|
|
88
|
+
* Adopt an already-open WebSocket (e.g. from a `WebSocketServer` connection event).
|
|
89
|
+
*
|
|
90
|
+
* Returns a `WebSocketClient` in the "open" state, ready to send/receive.
|
|
91
|
+
* The client takes ownership of the socket lifecycle: calling `close()`
|
|
92
|
+
* will close the underlying socket.
|
|
93
|
+
*
|
|
94
|
+
* **Node.js only.** Throws in browser builds.
|
|
95
|
+
*
|
|
96
|
+
* Call this immediately in the server's `connection` handler to avoid
|
|
97
|
+
* missing messages:
|
|
98
|
+
*
|
|
99
|
+
* ```ts
|
|
100
|
+
* wss.on("connection", (socket) => {
|
|
101
|
+
* const client = WebSocketClient.fromSocket(socket);
|
|
102
|
+
* const msg = await client.receive();
|
|
103
|
+
* });
|
|
104
|
+
* ```
|
|
105
|
+
*/
|
|
106
|
+
static fromSocket(rawSocket: unknown, options?: ClientOptions): WebSocketClient;
|
|
87
107
|
/** Current connection state. */
|
|
88
108
|
get readyState(): WebSocketState;
|
|
89
109
|
/** Close info from the last close event, if any. */
|