@culpeo/async-ws 0.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 +13 -0
- package/LICENSE +21 -0
- package/README.md +358 -0
- package/dist/browser/index.js +270 -0
- package/dist/cjs/index.cjs +293 -0
- package/dist/esm/index.js +291 -0
- package/dist/iife/index.js +277 -0
- package/dist/index.d.ts +97 -0
- package/package.json +84 -0
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
var ws = require('ws');
|
|
4
|
+
|
|
5
|
+
function createWebSocket(url, options) {
|
|
6
|
+
return new ws.WebSocket(url, options?.protocols, {
|
|
7
|
+
headers: options?.headers,
|
|
8
|
+
});
|
|
9
|
+
}
|
|
10
|
+
function socketSend(socket, data) {
|
|
11
|
+
return new Promise((resolve, reject) => {
|
|
12
|
+
socket.send(data, (error) => {
|
|
13
|
+
if (error) {
|
|
14
|
+
reject(error);
|
|
15
|
+
}
|
|
16
|
+
else {
|
|
17
|
+
resolve();
|
|
18
|
+
}
|
|
19
|
+
});
|
|
20
|
+
});
|
|
21
|
+
}
|
|
22
|
+
function setBinaryType(socket) {
|
|
23
|
+
socket.binaryType = "arraybuffer";
|
|
24
|
+
}
|
|
25
|
+
function attachListeners(socket, onOpen, onMessage, onClose, onError) {
|
|
26
|
+
const handleOpen = () => onOpen();
|
|
27
|
+
const handleMessage = (event) => {
|
|
28
|
+
const isBinary = !(typeof event.data === "string");
|
|
29
|
+
let data;
|
|
30
|
+
if (isBinary) {
|
|
31
|
+
if (event.data instanceof ArrayBuffer) {
|
|
32
|
+
data = event.data;
|
|
33
|
+
}
|
|
34
|
+
else if (Buffer.isBuffer(event.data)) {
|
|
35
|
+
const buf = event.data;
|
|
36
|
+
data = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
37
|
+
}
|
|
38
|
+
else if (Array.isArray(event.data)) {
|
|
39
|
+
const buf = Buffer.concat(event.data);
|
|
40
|
+
data = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
41
|
+
}
|
|
42
|
+
else {
|
|
43
|
+
data = event.data;
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
else {
|
|
47
|
+
data = event.data;
|
|
48
|
+
}
|
|
49
|
+
onMessage(data, isBinary);
|
|
50
|
+
};
|
|
51
|
+
const handleClose = (event) => onClose(event.code, event.reason, event.wasClean);
|
|
52
|
+
const handleError = (event) => onError(new Error(event.message));
|
|
53
|
+
socket.addEventListener("open", handleOpen);
|
|
54
|
+
socket.addEventListener("message", handleMessage);
|
|
55
|
+
socket.addEventListener("close", handleClose);
|
|
56
|
+
socket.addEventListener("error", handleError);
|
|
57
|
+
return () => {
|
|
58
|
+
socket.removeEventListener("open", handleOpen);
|
|
59
|
+
socket.removeEventListener("message", handleMessage);
|
|
60
|
+
socket.removeEventListener("close", handleClose);
|
|
61
|
+
socket.removeEventListener("error", handleError);
|
|
62
|
+
};
|
|
63
|
+
}
|
|
64
|
+
function socketClose(socket, code, reason) {
|
|
65
|
+
socket.close(code, reason);
|
|
66
|
+
}
|
|
67
|
+
ws.WebSocket.OPEN;
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Imperative WebSocket client that works in both browser and Node.js.
|
|
71
|
+
*
|
|
72
|
+
* Turns the event-driven WebSocket API into a promise-based one:
|
|
73
|
+
* - `connect(url)` returns a promise that resolves when the connection opens.
|
|
74
|
+
* - `send(data)` returns a promise that resolves when the data is accepted.
|
|
75
|
+
* - `receive()` returns a promise that resolves with the next message.
|
|
76
|
+
* - `close()` returns a promise that resolves when the connection closes.
|
|
77
|
+
* - Supports `for await...of` iteration over incoming messages.
|
|
78
|
+
*/
|
|
79
|
+
class WebSocketClient {
|
|
80
|
+
constructor(options) {
|
|
81
|
+
this.socket = null;
|
|
82
|
+
this.state = "idle";
|
|
83
|
+
this.buffer = [];
|
|
84
|
+
this.waiters = [];
|
|
85
|
+
this.terminalError = null;
|
|
86
|
+
this.closeInfo = null;
|
|
87
|
+
this.removeListeners = null;
|
|
88
|
+
this.maxBufferSize = options?.maxBufferSize ?? 0;
|
|
89
|
+
}
|
|
90
|
+
/** Current connection state. */
|
|
91
|
+
get readyState() {
|
|
92
|
+
return this.state;
|
|
93
|
+
}
|
|
94
|
+
/** Close info from the last close event, if any. */
|
|
95
|
+
get lastCloseInfo() {
|
|
96
|
+
return this.closeInfo;
|
|
97
|
+
}
|
|
98
|
+
/**
|
|
99
|
+
* Connect to a WebSocket server.
|
|
100
|
+
* Resolves when the connection is open. Rejects on error.
|
|
101
|
+
*/
|
|
102
|
+
connect(url, options) {
|
|
103
|
+
if (this.state !== "idle" && this.state !== "closed" && this.state !== "errored") {
|
|
104
|
+
return Promise.reject(new Error(`Cannot connect: client is in "${this.state}" state`));
|
|
105
|
+
}
|
|
106
|
+
this.reset();
|
|
107
|
+
this.state = "connecting";
|
|
108
|
+
return new Promise((resolve, reject) => {
|
|
109
|
+
try {
|
|
110
|
+
this.socket = createWebSocket(url, options);
|
|
111
|
+
setBinaryType(this.socket);
|
|
112
|
+
}
|
|
113
|
+
catch (err) {
|
|
114
|
+
this.state = "errored";
|
|
115
|
+
this.terminalError = err instanceof Error ? err : new Error(String(err));
|
|
116
|
+
reject(this.terminalError);
|
|
117
|
+
return;
|
|
118
|
+
}
|
|
119
|
+
let settled = false;
|
|
120
|
+
this.removeListeners = attachListeners(this.socket,
|
|
121
|
+
// onOpen
|
|
122
|
+
() => {
|
|
123
|
+
if (!settled) {
|
|
124
|
+
settled = true;
|
|
125
|
+
this.state = "open";
|
|
126
|
+
resolve();
|
|
127
|
+
}
|
|
128
|
+
},
|
|
129
|
+
// onMessage
|
|
130
|
+
(data, binary) => {
|
|
131
|
+
this.enqueueMessage({ data, binary });
|
|
132
|
+
},
|
|
133
|
+
// onClose
|
|
134
|
+
(code, reason, wasClean) => {
|
|
135
|
+
this.closeInfo = { code, reason, wasClean };
|
|
136
|
+
this.state;
|
|
137
|
+
this.state = "closed";
|
|
138
|
+
this.cleanup();
|
|
139
|
+
if (!settled) {
|
|
140
|
+
settled = true;
|
|
141
|
+
reject(new Error(`WebSocket closed before opening (code: ${code}, reason: ${reason})`));
|
|
142
|
+
}
|
|
143
|
+
// Only reject pending waiters once buffer is drained
|
|
144
|
+
if (this.buffer.length === 0) {
|
|
145
|
+
this.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
146
|
+
}
|
|
147
|
+
},
|
|
148
|
+
// onError
|
|
149
|
+
(error) => {
|
|
150
|
+
this.terminalError = error;
|
|
151
|
+
if (!settled) {
|
|
152
|
+
settled = true;
|
|
153
|
+
reject(error);
|
|
154
|
+
}
|
|
155
|
+
// Reject any pending receive() waiters immediately
|
|
156
|
+
this.rejectAllWaiters(error);
|
|
157
|
+
// Don't call cleanup() here — per spec, a close event always
|
|
158
|
+
// follows an error event. Let onClose handle state transition
|
|
159
|
+
// and listener removal.
|
|
160
|
+
});
|
|
161
|
+
});
|
|
162
|
+
}
|
|
163
|
+
/**
|
|
164
|
+
* Send data over the WebSocket.
|
|
165
|
+
* Resolves when the data has been accepted by the socket.
|
|
166
|
+
*/
|
|
167
|
+
send(data) {
|
|
168
|
+
if (this.state !== "open" || !this.socket) {
|
|
169
|
+
return Promise.reject(new Error(`Cannot send: client is in "${this.state}" state`));
|
|
170
|
+
}
|
|
171
|
+
return socketSend(this.socket, data);
|
|
172
|
+
}
|
|
173
|
+
/**
|
|
174
|
+
* Wait for and return the next incoming message.
|
|
175
|
+
*
|
|
176
|
+
* If messages have been buffered, returns the oldest one immediately.
|
|
177
|
+
* If the socket has closed cleanly and the buffer is empty, rejects.
|
|
178
|
+
*/
|
|
179
|
+
receive() {
|
|
180
|
+
// Drain buffer first, even if the socket is closed
|
|
181
|
+
if (this.buffer.length > 0) {
|
|
182
|
+
return Promise.resolve(this.buffer.shift());
|
|
183
|
+
}
|
|
184
|
+
if (this.state === "errored" && this.terminalError) {
|
|
185
|
+
return Promise.reject(this.terminalError);
|
|
186
|
+
}
|
|
187
|
+
if (this.state === "closed") {
|
|
188
|
+
return Promise.reject(new Error("WebSocket is closed"));
|
|
189
|
+
}
|
|
190
|
+
if (this.state !== "open") {
|
|
191
|
+
return Promise.reject(new Error(`Cannot receive: client is in "${this.state}" state`));
|
|
192
|
+
}
|
|
193
|
+
return new Promise((resolve, reject) => {
|
|
194
|
+
this.waiters.push({ resolve, reject });
|
|
195
|
+
});
|
|
196
|
+
}
|
|
197
|
+
/**
|
|
198
|
+
* Close the WebSocket connection.
|
|
199
|
+
* Resolves when the close handshake completes.
|
|
200
|
+
*/
|
|
201
|
+
close(code, reason) {
|
|
202
|
+
if (this.state === "closed" || this.state === "idle" || this.state === "errored") {
|
|
203
|
+
return Promise.resolve();
|
|
204
|
+
}
|
|
205
|
+
if (!this.socket) {
|
|
206
|
+
return Promise.resolve();
|
|
207
|
+
}
|
|
208
|
+
if (this.state === "closing") {
|
|
209
|
+
// Already closing — wait for the close event via a one-shot listener
|
|
210
|
+
return new Promise((resolve) => {
|
|
211
|
+
if (this.socket) {
|
|
212
|
+
this.socket.addEventListener("close", () => resolve(), { once: true });
|
|
213
|
+
}
|
|
214
|
+
else {
|
|
215
|
+
resolve();
|
|
216
|
+
}
|
|
217
|
+
});
|
|
218
|
+
}
|
|
219
|
+
this.state = "closing";
|
|
220
|
+
// Validate close code before calling socketClose to avoid leaving the
|
|
221
|
+
// socket in a corrupt state (ws library sets readyState to CLOSING
|
|
222
|
+
// before throwing on invalid codes, making the socket unrecoverable).
|
|
223
|
+
if (code !== undefined) {
|
|
224
|
+
if (code !== 1000 && (code < 3000 || code > 4999)) {
|
|
225
|
+
this.state = "open";
|
|
226
|
+
return Promise.reject(new Error(`Invalid close code: ${code}. Must be 1000 or in range 3000-4999.`));
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return new Promise((resolve) => {
|
|
230
|
+
if (this.socket) {
|
|
231
|
+
this.socket.addEventListener("close", () => resolve(), { once: true });
|
|
232
|
+
}
|
|
233
|
+
socketClose(this.socket, code, reason);
|
|
234
|
+
});
|
|
235
|
+
}
|
|
236
|
+
/**
|
|
237
|
+
* Async iterator over incoming messages.
|
|
238
|
+
*
|
|
239
|
+
* - Yields messages as they arrive.
|
|
240
|
+
* - Returns (ends iteration) on normal close.
|
|
241
|
+
* - Throws on error/abnormal close.
|
|
242
|
+
* - If the consumer `break`s, the socket is NOT closed automatically.
|
|
243
|
+
*/
|
|
244
|
+
async *[Symbol.asyncIterator]() {
|
|
245
|
+
while (true) {
|
|
246
|
+
try {
|
|
247
|
+
yield await this.receive();
|
|
248
|
+
}
|
|
249
|
+
catch {
|
|
250
|
+
// If closed cleanly, end iteration
|
|
251
|
+
if (this.state === "closed" && this.closeInfo?.wasClean) {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
// Otherwise, propagate the error
|
|
255
|
+
throw this.terminalError ?? new Error("WebSocket closed unexpectedly");
|
|
256
|
+
}
|
|
257
|
+
}
|
|
258
|
+
}
|
|
259
|
+
enqueueMessage(msg) {
|
|
260
|
+
if (this.waiters.length > 0) {
|
|
261
|
+
const waiter = this.waiters.shift();
|
|
262
|
+
waiter.resolve(msg);
|
|
263
|
+
}
|
|
264
|
+
else {
|
|
265
|
+
if (this.maxBufferSize > 0 && this.buffer.length >= this.maxBufferSize) {
|
|
266
|
+
this.buffer.shift(); // drop oldest
|
|
267
|
+
}
|
|
268
|
+
this.buffer.push(msg);
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
rejectAllWaiters(error) {
|
|
272
|
+
const pending = this.waiters.splice(0);
|
|
273
|
+
for (const waiter of pending) {
|
|
274
|
+
waiter.reject(error);
|
|
275
|
+
}
|
|
276
|
+
}
|
|
277
|
+
cleanup() {
|
|
278
|
+
if (this.removeListeners) {
|
|
279
|
+
this.removeListeners();
|
|
280
|
+
this.removeListeners = null;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
reset() {
|
|
284
|
+
this.socket = null;
|
|
285
|
+
this.buffer = [];
|
|
286
|
+
this.waiters = [];
|
|
287
|
+
this.terminalError = null;
|
|
288
|
+
this.closeInfo = null;
|
|
289
|
+
this.removeListeners = null;
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
exports.WebSocketClient = WebSocketClient;
|
|
@@ -0,0 +1,291 @@
|
|
|
1
|
+
import { WebSocket } from 'ws';
|
|
2
|
+
|
|
3
|
+
function createWebSocket(url, options) {
|
|
4
|
+
return new WebSocket(url, options?.protocols, {
|
|
5
|
+
headers: options?.headers,
|
|
6
|
+
});
|
|
7
|
+
}
|
|
8
|
+
function socketSend(socket, data) {
|
|
9
|
+
return new Promise((resolve, reject) => {
|
|
10
|
+
socket.send(data, (error) => {
|
|
11
|
+
if (error) {
|
|
12
|
+
reject(error);
|
|
13
|
+
}
|
|
14
|
+
else {
|
|
15
|
+
resolve();
|
|
16
|
+
}
|
|
17
|
+
});
|
|
18
|
+
});
|
|
19
|
+
}
|
|
20
|
+
function setBinaryType(socket) {
|
|
21
|
+
socket.binaryType = "arraybuffer";
|
|
22
|
+
}
|
|
23
|
+
function attachListeners(socket, onOpen, onMessage, onClose, onError) {
|
|
24
|
+
const handleOpen = () => onOpen();
|
|
25
|
+
const handleMessage = (event) => {
|
|
26
|
+
const isBinary = !(typeof event.data === "string");
|
|
27
|
+
let data;
|
|
28
|
+
if (isBinary) {
|
|
29
|
+
if (event.data instanceof ArrayBuffer) {
|
|
30
|
+
data = event.data;
|
|
31
|
+
}
|
|
32
|
+
else if (Buffer.isBuffer(event.data)) {
|
|
33
|
+
const buf = event.data;
|
|
34
|
+
data = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
35
|
+
}
|
|
36
|
+
else if (Array.isArray(event.data)) {
|
|
37
|
+
const buf = Buffer.concat(event.data);
|
|
38
|
+
data = buf.buffer.slice(buf.byteOffset, buf.byteOffset + buf.byteLength);
|
|
39
|
+
}
|
|
40
|
+
else {
|
|
41
|
+
data = event.data;
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
else {
|
|
45
|
+
data = event.data;
|
|
46
|
+
}
|
|
47
|
+
onMessage(data, isBinary);
|
|
48
|
+
};
|
|
49
|
+
const handleClose = (event) => onClose(event.code, event.reason, event.wasClean);
|
|
50
|
+
const handleError = (event) => onError(new Error(event.message));
|
|
51
|
+
socket.addEventListener("open", handleOpen);
|
|
52
|
+
socket.addEventListener("message", handleMessage);
|
|
53
|
+
socket.addEventListener("close", handleClose);
|
|
54
|
+
socket.addEventListener("error", handleError);
|
|
55
|
+
return () => {
|
|
56
|
+
socket.removeEventListener("open", handleOpen);
|
|
57
|
+
socket.removeEventListener("message", handleMessage);
|
|
58
|
+
socket.removeEventListener("close", handleClose);
|
|
59
|
+
socket.removeEventListener("error", handleError);
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
function socketClose(socket, code, reason) {
|
|
63
|
+
socket.close(code, reason);
|
|
64
|
+
}
|
|
65
|
+
WebSocket.OPEN;
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Imperative WebSocket client that works in both browser and Node.js.
|
|
69
|
+
*
|
|
70
|
+
* Turns the event-driven WebSocket API into a promise-based one:
|
|
71
|
+
* - `connect(url)` returns a promise that resolves when the connection opens.
|
|
72
|
+
* - `send(data)` returns a promise that resolves when the data is accepted.
|
|
73
|
+
* - `receive()` returns a promise that resolves with the next message.
|
|
74
|
+
* - `close()` returns a promise that resolves when the connection closes.
|
|
75
|
+
* - Supports `for await...of` iteration over incoming messages.
|
|
76
|
+
*/
|
|
77
|
+
class WebSocketClient {
|
|
78
|
+
constructor(options) {
|
|
79
|
+
this.socket = null;
|
|
80
|
+
this.state = "idle";
|
|
81
|
+
this.buffer = [];
|
|
82
|
+
this.waiters = [];
|
|
83
|
+
this.terminalError = null;
|
|
84
|
+
this.closeInfo = null;
|
|
85
|
+
this.removeListeners = null;
|
|
86
|
+
this.maxBufferSize = options?.maxBufferSize ?? 0;
|
|
87
|
+
}
|
|
88
|
+
/** Current connection state. */
|
|
89
|
+
get readyState() {
|
|
90
|
+
return this.state;
|
|
91
|
+
}
|
|
92
|
+
/** Close info from the last close event, if any. */
|
|
93
|
+
get lastCloseInfo() {
|
|
94
|
+
return this.closeInfo;
|
|
95
|
+
}
|
|
96
|
+
/**
|
|
97
|
+
* Connect to a WebSocket server.
|
|
98
|
+
* Resolves when the connection is open. Rejects on error.
|
|
99
|
+
*/
|
|
100
|
+
connect(url, options) {
|
|
101
|
+
if (this.state !== "idle" && this.state !== "closed" && this.state !== "errored") {
|
|
102
|
+
return Promise.reject(new Error(`Cannot connect: client is in "${this.state}" state`));
|
|
103
|
+
}
|
|
104
|
+
this.reset();
|
|
105
|
+
this.state = "connecting";
|
|
106
|
+
return new Promise((resolve, reject) => {
|
|
107
|
+
try {
|
|
108
|
+
this.socket = createWebSocket(url, options);
|
|
109
|
+
setBinaryType(this.socket);
|
|
110
|
+
}
|
|
111
|
+
catch (err) {
|
|
112
|
+
this.state = "errored";
|
|
113
|
+
this.terminalError = err instanceof Error ? err : new Error(String(err));
|
|
114
|
+
reject(this.terminalError);
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
let settled = false;
|
|
118
|
+
this.removeListeners = attachListeners(this.socket,
|
|
119
|
+
// onOpen
|
|
120
|
+
() => {
|
|
121
|
+
if (!settled) {
|
|
122
|
+
settled = true;
|
|
123
|
+
this.state = "open";
|
|
124
|
+
resolve();
|
|
125
|
+
}
|
|
126
|
+
},
|
|
127
|
+
// onMessage
|
|
128
|
+
(data, binary) => {
|
|
129
|
+
this.enqueueMessage({ data, binary });
|
|
130
|
+
},
|
|
131
|
+
// onClose
|
|
132
|
+
(code, reason, wasClean) => {
|
|
133
|
+
this.closeInfo = { code, reason, wasClean };
|
|
134
|
+
this.state;
|
|
135
|
+
this.state = "closed";
|
|
136
|
+
this.cleanup();
|
|
137
|
+
if (!settled) {
|
|
138
|
+
settled = true;
|
|
139
|
+
reject(new Error(`WebSocket closed before opening (code: ${code}, reason: ${reason})`));
|
|
140
|
+
}
|
|
141
|
+
// Only reject pending waiters once buffer is drained
|
|
142
|
+
if (this.buffer.length === 0) {
|
|
143
|
+
this.rejectAllWaiters(new Error(`WebSocket closed (code: ${code}, reason: ${reason})`));
|
|
144
|
+
}
|
|
145
|
+
},
|
|
146
|
+
// onError
|
|
147
|
+
(error) => {
|
|
148
|
+
this.terminalError = error;
|
|
149
|
+
if (!settled) {
|
|
150
|
+
settled = true;
|
|
151
|
+
reject(error);
|
|
152
|
+
}
|
|
153
|
+
// Reject any pending receive() waiters immediately
|
|
154
|
+
this.rejectAllWaiters(error);
|
|
155
|
+
// Don't call cleanup() here — per spec, a close event always
|
|
156
|
+
// follows an error event. Let onClose handle state transition
|
|
157
|
+
// and listener removal.
|
|
158
|
+
});
|
|
159
|
+
});
|
|
160
|
+
}
|
|
161
|
+
/**
|
|
162
|
+
* Send data over the WebSocket.
|
|
163
|
+
* Resolves when the data has been accepted by the socket.
|
|
164
|
+
*/
|
|
165
|
+
send(data) {
|
|
166
|
+
if (this.state !== "open" || !this.socket) {
|
|
167
|
+
return Promise.reject(new Error(`Cannot send: client is in "${this.state}" state`));
|
|
168
|
+
}
|
|
169
|
+
return socketSend(this.socket, data);
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Wait for and return the next incoming message.
|
|
173
|
+
*
|
|
174
|
+
* If messages have been buffered, returns the oldest one immediately.
|
|
175
|
+
* If the socket has closed cleanly and the buffer is empty, rejects.
|
|
176
|
+
*/
|
|
177
|
+
receive() {
|
|
178
|
+
// Drain buffer first, even if the socket is closed
|
|
179
|
+
if (this.buffer.length > 0) {
|
|
180
|
+
return Promise.resolve(this.buffer.shift());
|
|
181
|
+
}
|
|
182
|
+
if (this.state === "errored" && this.terminalError) {
|
|
183
|
+
return Promise.reject(this.terminalError);
|
|
184
|
+
}
|
|
185
|
+
if (this.state === "closed") {
|
|
186
|
+
return Promise.reject(new Error("WebSocket is closed"));
|
|
187
|
+
}
|
|
188
|
+
if (this.state !== "open") {
|
|
189
|
+
return Promise.reject(new Error(`Cannot receive: client is in "${this.state}" state`));
|
|
190
|
+
}
|
|
191
|
+
return new Promise((resolve, reject) => {
|
|
192
|
+
this.waiters.push({ resolve, reject });
|
|
193
|
+
});
|
|
194
|
+
}
|
|
195
|
+
/**
|
|
196
|
+
* Close the WebSocket connection.
|
|
197
|
+
* Resolves when the close handshake completes.
|
|
198
|
+
*/
|
|
199
|
+
close(code, reason) {
|
|
200
|
+
if (this.state === "closed" || this.state === "idle" || this.state === "errored") {
|
|
201
|
+
return Promise.resolve();
|
|
202
|
+
}
|
|
203
|
+
if (!this.socket) {
|
|
204
|
+
return Promise.resolve();
|
|
205
|
+
}
|
|
206
|
+
if (this.state === "closing") {
|
|
207
|
+
// Already closing — wait for the close event via a one-shot listener
|
|
208
|
+
return new Promise((resolve) => {
|
|
209
|
+
if (this.socket) {
|
|
210
|
+
this.socket.addEventListener("close", () => resolve(), { once: true });
|
|
211
|
+
}
|
|
212
|
+
else {
|
|
213
|
+
resolve();
|
|
214
|
+
}
|
|
215
|
+
});
|
|
216
|
+
}
|
|
217
|
+
this.state = "closing";
|
|
218
|
+
// Validate close code before calling socketClose to avoid leaving the
|
|
219
|
+
// socket in a corrupt state (ws library sets readyState to CLOSING
|
|
220
|
+
// before throwing on invalid codes, making the socket unrecoverable).
|
|
221
|
+
if (code !== undefined) {
|
|
222
|
+
if (code !== 1000 && (code < 3000 || code > 4999)) {
|
|
223
|
+
this.state = "open";
|
|
224
|
+
return Promise.reject(new Error(`Invalid close code: ${code}. Must be 1000 or in range 3000-4999.`));
|
|
225
|
+
}
|
|
226
|
+
}
|
|
227
|
+
return new Promise((resolve) => {
|
|
228
|
+
if (this.socket) {
|
|
229
|
+
this.socket.addEventListener("close", () => resolve(), { once: true });
|
|
230
|
+
}
|
|
231
|
+
socketClose(this.socket, code, reason);
|
|
232
|
+
});
|
|
233
|
+
}
|
|
234
|
+
/**
|
|
235
|
+
* Async iterator over incoming messages.
|
|
236
|
+
*
|
|
237
|
+
* - Yields messages as they arrive.
|
|
238
|
+
* - Returns (ends iteration) on normal close.
|
|
239
|
+
* - Throws on error/abnormal close.
|
|
240
|
+
* - If the consumer `break`s, the socket is NOT closed automatically.
|
|
241
|
+
*/
|
|
242
|
+
async *[Symbol.asyncIterator]() {
|
|
243
|
+
while (true) {
|
|
244
|
+
try {
|
|
245
|
+
yield await this.receive();
|
|
246
|
+
}
|
|
247
|
+
catch {
|
|
248
|
+
// If closed cleanly, end iteration
|
|
249
|
+
if (this.state === "closed" && this.closeInfo?.wasClean) {
|
|
250
|
+
return;
|
|
251
|
+
}
|
|
252
|
+
// Otherwise, propagate the error
|
|
253
|
+
throw this.terminalError ?? new Error("WebSocket closed unexpectedly");
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
enqueueMessage(msg) {
|
|
258
|
+
if (this.waiters.length > 0) {
|
|
259
|
+
const waiter = this.waiters.shift();
|
|
260
|
+
waiter.resolve(msg);
|
|
261
|
+
}
|
|
262
|
+
else {
|
|
263
|
+
if (this.maxBufferSize > 0 && this.buffer.length >= this.maxBufferSize) {
|
|
264
|
+
this.buffer.shift(); // drop oldest
|
|
265
|
+
}
|
|
266
|
+
this.buffer.push(msg);
|
|
267
|
+
}
|
|
268
|
+
}
|
|
269
|
+
rejectAllWaiters(error) {
|
|
270
|
+
const pending = this.waiters.splice(0);
|
|
271
|
+
for (const waiter of pending) {
|
|
272
|
+
waiter.reject(error);
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
cleanup() {
|
|
276
|
+
if (this.removeListeners) {
|
|
277
|
+
this.removeListeners();
|
|
278
|
+
this.removeListeners = null;
|
|
279
|
+
}
|
|
280
|
+
}
|
|
281
|
+
reset() {
|
|
282
|
+
this.socket = null;
|
|
283
|
+
this.buffer = [];
|
|
284
|
+
this.waiters = [];
|
|
285
|
+
this.terminalError = null;
|
|
286
|
+
this.closeInfo = null;
|
|
287
|
+
this.removeListeners = null;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
export { WebSocketClient };
|