@kuralle-syrinx/ws 4.1.0 → 4.3.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/package.json +23 -5
- package/src/index.ts +12 -1
- package/src/index.test.ts +0 -350
- package/src/node.test.ts +0 -68
- package/src/replay.test.ts +0 -99
- package/src/web-socket.test.ts +0 -132
- package/src/workers.test.ts +0 -84
- package/tsconfig.json +0 -21
package/package.json
CHANGED
|
@@ -1,9 +1,27 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@kuralle-syrinx/ws",
|
|
3
|
-
"version": "4.
|
|
3
|
+
"version": "4.3.0",
|
|
4
4
|
"private": false,
|
|
5
|
-
"
|
|
5
|
+
"description": "Reconnecting WebSocket connection manager for Syrinx provider sockets — verify-before-trust reconnects, ordered replay, keepalive; Node and Workers",
|
|
6
|
+
"keywords": [
|
|
7
|
+
"voice",
|
|
8
|
+
"voice-agent",
|
|
9
|
+
"speech",
|
|
10
|
+
"syrinx",
|
|
11
|
+
"websocket",
|
|
12
|
+
"reconnect"
|
|
13
|
+
],
|
|
6
14
|
"license": "MIT",
|
|
15
|
+
"homepage": "https://github.com/kuralle/syrinx#readme",
|
|
16
|
+
"repository": {
|
|
17
|
+
"type": "git",
|
|
18
|
+
"url": "git+https://github.com/kuralle/syrinx.git",
|
|
19
|
+
"directory": "packages/ws"
|
|
20
|
+
},
|
|
21
|
+
"bugs": {
|
|
22
|
+
"url": "https://github.com/kuralle/syrinx/issues"
|
|
23
|
+
},
|
|
24
|
+
"type": "module",
|
|
7
25
|
"main": "./src/index.ts",
|
|
8
26
|
"types": "./src/index.ts",
|
|
9
27
|
"exports": {
|
|
@@ -14,13 +32,13 @@
|
|
|
14
32
|
"./workers": "./src/workers.ts"
|
|
15
33
|
},
|
|
16
34
|
"dependencies": {
|
|
17
|
-
"ws": "^8.
|
|
18
|
-
"@kuralle-syrinx/core": "4.
|
|
35
|
+
"ws": "^8.21.0",
|
|
36
|
+
"@kuralle-syrinx/core": "4.3.0"
|
|
19
37
|
},
|
|
20
38
|
"devDependencies": {
|
|
21
39
|
"@types/ws": "^8.5.0",
|
|
22
40
|
"typescript": "^5.7.0",
|
|
23
|
-
"vitest": "^2.
|
|
41
|
+
"vitest": "^3.2.6"
|
|
24
42
|
},
|
|
25
43
|
"scripts": {
|
|
26
44
|
"typecheck": "tsc --noEmit",
|
package/src/index.ts
CHANGED
|
@@ -106,6 +106,10 @@ let connectionSequence = 0;
|
|
|
106
106
|
export class WebSocketConnection {
|
|
107
107
|
private socket: ManagedSocket | null = null;
|
|
108
108
|
private ready = false;
|
|
109
|
+
/** True only once the connection is verified and onReadyBeforeReplay has been
|
|
110
|
+
* given the chance to run — send() must not reach an open-but-unconfigured
|
|
111
|
+
* socket, or caller frames beat the provider config frame on reconnect. */
|
|
112
|
+
private established = false;
|
|
109
113
|
private closed = false;
|
|
110
114
|
private reconnecting = false;
|
|
111
115
|
private connResolver: (() => void) | null = null;
|
|
@@ -135,6 +139,7 @@ export class WebSocketConnection {
|
|
|
135
139
|
async connect(): Promise<void> {
|
|
136
140
|
this.closed = false;
|
|
137
141
|
await this.openSocket();
|
|
142
|
+
this.established = true;
|
|
138
143
|
this.opts.onReadyBeforeReplay?.();
|
|
139
144
|
this.flushReplay();
|
|
140
145
|
}
|
|
@@ -149,7 +154,7 @@ export class WebSocketConnection {
|
|
|
149
154
|
* throws and the caller decides how to retry/report.
|
|
150
155
|
*/
|
|
151
156
|
send(payload: SocketData): void {
|
|
152
|
-
if (!this.socket || !this.socket.isOpen) {
|
|
157
|
+
if (!this.socket || !this.socket.isOpen || !this.established) {
|
|
153
158
|
if (this.replayBufferSize > 0 && !this.closed) {
|
|
154
159
|
this.bufferForReplay(payload);
|
|
155
160
|
return;
|
|
@@ -208,6 +213,7 @@ export class WebSocketConnection {
|
|
|
208
213
|
this.connResolver = null;
|
|
209
214
|
this.connRejecter = null;
|
|
210
215
|
this.ready = false;
|
|
216
|
+
this.established = false;
|
|
211
217
|
this.socket?.dispose();
|
|
212
218
|
this.socket = null;
|
|
213
219
|
}
|
|
@@ -223,6 +229,7 @@ export class WebSocketConnection {
|
|
|
223
229
|
this.socket?.dispose();
|
|
224
230
|
this.socket = null;
|
|
225
231
|
this.ready = false;
|
|
232
|
+
this.established = false;
|
|
226
233
|
this.stopKeepAlive();
|
|
227
234
|
void this.tryReconnect();
|
|
228
235
|
}
|
|
@@ -235,6 +242,7 @@ export class WebSocketConnection {
|
|
|
235
242
|
this.abortPendingOpen(new Error("WebSocket connection replaced"));
|
|
236
243
|
this.socket?.dispose();
|
|
237
244
|
this.ready = false;
|
|
245
|
+
this.established = false;
|
|
238
246
|
|
|
239
247
|
let socket: ManagedSocket | null = null;
|
|
240
248
|
let deadlineTimer: ReturnType<typeof setTimeout> | undefined;
|
|
@@ -312,6 +320,7 @@ export class WebSocketConnection {
|
|
|
312
320
|
settle(() => reject(err));
|
|
313
321
|
if (!bindSocket(created)) return;
|
|
314
322
|
this.ready = false;
|
|
323
|
+
this.established = false;
|
|
315
324
|
this.connRejecter?.(err);
|
|
316
325
|
this.connResolver = null;
|
|
317
326
|
this.connRejecter = null;
|
|
@@ -322,6 +331,7 @@ export class WebSocketConnection {
|
|
|
322
331
|
settle(() => reject(closeErr));
|
|
323
332
|
if (!bindSocket(created)) return;
|
|
324
333
|
this.ready = false;
|
|
334
|
+
this.established = false;
|
|
325
335
|
this.stopKeepAlive();
|
|
326
336
|
this.connRejecter?.(closeErr);
|
|
327
337
|
this.connResolver = null;
|
|
@@ -416,6 +426,7 @@ export class WebSocketConnection {
|
|
|
416
426
|
await this.openSocket();
|
|
417
427
|
if (this.socket && (await this.verifyConnection(VERIFY_TIMEOUT_MS))) {
|
|
418
428
|
this.scheduleReconnectBurstReset();
|
|
429
|
+
this.established = true;
|
|
419
430
|
this.opts.onReadyBeforeReplay?.();
|
|
420
431
|
this.flushReplay();
|
|
421
432
|
this.opts.onReconnected?.();
|
package/src/index.test.ts
DELETED
|
@@ -1,350 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
|
|
3
|
-
import { afterEach, describe, expect, it } from "vitest";
|
|
4
|
-
import { WebSocketServer, type WebSocket } from "ws";
|
|
5
|
-
import type { RetryConfig } from "@kuralle-syrinx/core";
|
|
6
|
-
|
|
7
|
-
import { WebSocketConnection, type ManagedSocket, type SocketFactory } from "./index.js";
|
|
8
|
-
import { createNodeWsSocket } from "./node.js";
|
|
9
|
-
|
|
10
|
-
let servers: WebSocketServer[] = [];
|
|
11
|
-
|
|
12
|
-
afterEach(async () => {
|
|
13
|
-
await Promise.all(
|
|
14
|
-
servers.splice(0).map(
|
|
15
|
-
(server) =>
|
|
16
|
-
new Promise<void>((resolve) => {
|
|
17
|
-
for (const client of server.clients) client.terminate();
|
|
18
|
-
server.close(() => resolve());
|
|
19
|
-
}),
|
|
20
|
-
),
|
|
21
|
-
);
|
|
22
|
-
});
|
|
23
|
-
|
|
24
|
-
async function createServer(onConnection: (socket: WebSocket) => void): Promise<{ url: string; port: number; server: WebSocketServer }> {
|
|
25
|
-
const server = await new Promise<WebSocketServer>((resolve) => {
|
|
26
|
-
let next: WebSocketServer;
|
|
27
|
-
next = new WebSocketServer({ port: 0 }, () => resolve(next));
|
|
28
|
-
});
|
|
29
|
-
servers.push(server);
|
|
30
|
-
server.on("connection", onConnection);
|
|
31
|
-
const address = server.address();
|
|
32
|
-
if (!address || typeof address === "string") throw new Error("Expected TCP server address");
|
|
33
|
-
return { url: `ws://127.0.0.1:${address.port}/`, port: address.port, server };
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const FAST_RETRY: RetryConfig = { maxAttempts: 3, baseDelayMs: 10, maxDelayMs: 40 };
|
|
37
|
-
|
|
38
|
-
async function waitFor(predicate: () => boolean, timeoutMs = 2000): Promise<void> {
|
|
39
|
-
const startedAt = Date.now();
|
|
40
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
41
|
-
if (predicate()) return;
|
|
42
|
-
await new Promise((resolve) => setTimeout(resolve, 5));
|
|
43
|
-
}
|
|
44
|
-
throw new Error("Timed out waiting for condition");
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("WebSocketConnection", () => {
|
|
48
|
-
it("delivers messages and holds the socket open with a KeepAlive", async () => {
|
|
49
|
-
const serverReceived: string[] = [];
|
|
50
|
-
const { url } = await createServer((socket) => {
|
|
51
|
-
socket.on("message", (data, isBinary) => {
|
|
52
|
-
if (!isBinary) serverReceived.push(data.toString());
|
|
53
|
-
});
|
|
54
|
-
socket.send("hello-from-server");
|
|
55
|
-
});
|
|
56
|
-
|
|
57
|
-
const messages: string[] = [];
|
|
58
|
-
const conn = new WebSocketConnection({
|
|
59
|
-
url: () => url,
|
|
60
|
-
socketFactory: createNodeWsSocket,
|
|
61
|
-
retry: FAST_RETRY,
|
|
62
|
-
keepAliveIntervalMs: 30,
|
|
63
|
-
keepAliveMessage: () => JSON.stringify({ type: "KeepAlive" }),
|
|
64
|
-
onMessage: (data, isBinary) => {
|
|
65
|
-
if (!isBinary) messages.push(data.toString());
|
|
66
|
-
},
|
|
67
|
-
});
|
|
68
|
-
await conn.connect();
|
|
69
|
-
await waitFor(() => messages.includes("hello-from-server"));
|
|
70
|
-
// Several keepalive intervals should have elapsed.
|
|
71
|
-
await waitFor(() => serverReceived.filter((m) => m.includes("KeepAlive")).length >= 2);
|
|
72
|
-
|
|
73
|
-
expect(messages).toContain("hello-from-server");
|
|
74
|
-
expect(serverReceived.filter((m) => m.includes("KeepAlive")).length).toBeGreaterThanOrEqual(2);
|
|
75
|
-
|
|
76
|
-
await conn.close();
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("reconnects with verification after the server drops the socket", async () => {
|
|
80
|
-
let connections = 0;
|
|
81
|
-
const { url } = await createServer((socket) => {
|
|
82
|
-
connections += 1;
|
|
83
|
-
// ws auto-replies to ping with pong, so verify() passes.
|
|
84
|
-
if (connections === 1) {
|
|
85
|
-
// Drop the first connection shortly after it opens.
|
|
86
|
-
setTimeout(() => socket.close(1011, "transient"), 20);
|
|
87
|
-
}
|
|
88
|
-
});
|
|
89
|
-
|
|
90
|
-
let reconnecting = 0;
|
|
91
|
-
let reconnected = 0;
|
|
92
|
-
const conn = new WebSocketConnection({
|
|
93
|
-
url: () => url,
|
|
94
|
-
socketFactory: createNodeWsSocket,
|
|
95
|
-
retry: FAST_RETRY,
|
|
96
|
-
minStableMs: 0, // don't treat the deliberate drop as a quick failure
|
|
97
|
-
onMessage: () => undefined,
|
|
98
|
-
onReconnecting: () => {
|
|
99
|
-
reconnecting += 1;
|
|
100
|
-
},
|
|
101
|
-
onReconnected: () => {
|
|
102
|
-
reconnected += 1;
|
|
103
|
-
},
|
|
104
|
-
});
|
|
105
|
-
await conn.connect();
|
|
106
|
-
await waitFor(() => reconnected >= 1);
|
|
107
|
-
|
|
108
|
-
expect(reconnecting).toBeGreaterThanOrEqual(1);
|
|
109
|
-
expect(reconnected).toBe(1);
|
|
110
|
-
expect(connections).toBeGreaterThanOrEqual(2);
|
|
111
|
-
// The reconnected socket is usable.
|
|
112
|
-
expect(conn.isReady).toBe(true);
|
|
113
|
-
conn.send("after-reconnect");
|
|
114
|
-
|
|
115
|
-
await conn.close();
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
it("gives up and reports unrecoverable when the server stays down", async () => {
|
|
119
|
-
const { url, server } = await createServer(() => undefined);
|
|
120
|
-
let unrecoverable: Error | null = null;
|
|
121
|
-
const conn = new WebSocketConnection({
|
|
122
|
-
url: () => url,
|
|
123
|
-
socketFactory: createNodeWsSocket,
|
|
124
|
-
retry: FAST_RETRY,
|
|
125
|
-
maxReconnectAttempts: 2,
|
|
126
|
-
minStableMs: 0,
|
|
127
|
-
onMessage: () => undefined,
|
|
128
|
-
onUnrecoverable: (err) => {
|
|
129
|
-
unrecoverable = err;
|
|
130
|
-
},
|
|
131
|
-
});
|
|
132
|
-
await conn.connect();
|
|
133
|
-
expect(conn.isReady).toBe(true);
|
|
134
|
-
|
|
135
|
-
// Kill the server: the live socket drops and every reconnect attempt fails.
|
|
136
|
-
await new Promise<void>((resolve) => {
|
|
137
|
-
for (const client of server.clients) client.terminate();
|
|
138
|
-
server.close(() => resolve());
|
|
139
|
-
});
|
|
140
|
-
|
|
141
|
-
await waitFor(() => unrecoverable !== null, 3000);
|
|
142
|
-
expect(unrecoverable).toBeInstanceOf(Error);
|
|
143
|
-
expect((unrecoverable! as Error).message).toContain("failed to reconnect");
|
|
144
|
-
|
|
145
|
-
await conn.close();
|
|
146
|
-
});
|
|
147
|
-
|
|
148
|
-
it("rejects connect when disposed before the socket opens", async () => {
|
|
149
|
-
const hangingSocket: ManagedSocket = {
|
|
150
|
-
get isOpen() {
|
|
151
|
-
return false;
|
|
152
|
-
},
|
|
153
|
-
send: () => undefined,
|
|
154
|
-
keepAlivePing: () => undefined,
|
|
155
|
-
verify: async () => false,
|
|
156
|
-
dispose: () => undefined,
|
|
157
|
-
onOpen: () => undefined,
|
|
158
|
-
onMessage: () => undefined,
|
|
159
|
-
onClose: () => undefined,
|
|
160
|
-
onError: () => undefined,
|
|
161
|
-
};
|
|
162
|
-
const socketFactory: SocketFactory = () => hangingSocket;
|
|
163
|
-
const conn = new WebSocketConnection({
|
|
164
|
-
url: () => "ws://127.0.0.1:1/",
|
|
165
|
-
socketFactory,
|
|
166
|
-
retry: FAST_RETRY,
|
|
167
|
-
onMessage: () => undefined,
|
|
168
|
-
});
|
|
169
|
-
|
|
170
|
-
const connectPromise = conn.connect();
|
|
171
|
-
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
172
|
-
await conn.close();
|
|
173
|
-
|
|
174
|
-
await expect(connectPromise).rejects.toThrow(/disposed/i);
|
|
175
|
-
});
|
|
176
|
-
|
|
177
|
-
it("stops reconnecting when the socket keeps dying right after connecting", async () => {
|
|
178
|
-
// Server accepts + answers ping (verify passes) but closes ~15ms after open,
|
|
179
|
-
// every time — backoff can't fix this, so the quick-failure guard must give up.
|
|
180
|
-
const { url } = await createServer((socket) => {
|
|
181
|
-
setTimeout(() => socket.close(1011, "flap"), 15);
|
|
182
|
-
});
|
|
183
|
-
|
|
184
|
-
let unrecoverable: Error | null = null;
|
|
185
|
-
let reconnects = 0;
|
|
186
|
-
const conn = new WebSocketConnection({
|
|
187
|
-
url: () => url,
|
|
188
|
-
socketFactory: createNodeWsSocket,
|
|
189
|
-
retry: FAST_RETRY,
|
|
190
|
-
minStableMs: 200,
|
|
191
|
-
maxQuickFailures: 2,
|
|
192
|
-
onMessage: () => undefined,
|
|
193
|
-
onReconnected: () => {
|
|
194
|
-
reconnects += 1;
|
|
195
|
-
},
|
|
196
|
-
onUnrecoverable: (err) => {
|
|
197
|
-
unrecoverable = err;
|
|
198
|
-
},
|
|
199
|
-
});
|
|
200
|
-
await conn.connect();
|
|
201
|
-
await waitFor(() => unrecoverable !== null, 3000);
|
|
202
|
-
|
|
203
|
-
expect((unrecoverable! as Error).message).toContain("check credentials or provider policy");
|
|
204
|
-
// It should bail out quickly, not loop forever.
|
|
205
|
-
expect(reconnects).toBeLessThanOrEqual(3);
|
|
206
|
-
|
|
207
|
-
await conn.close();
|
|
208
|
-
});
|
|
209
|
-
|
|
210
|
-
it("rejects openSocket when the socket factory never resolves", async () => {
|
|
211
|
-
const connectTimeoutMs = 50;
|
|
212
|
-
const hangingFactory: SocketFactory = () =>
|
|
213
|
-
new Promise<ManagedSocket>(() => {
|
|
214
|
-
/* never resolves */
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
const conn = new WebSocketConnection({
|
|
218
|
-
url: () => "ws://127.0.0.1:1/",
|
|
219
|
-
socketFactory: hangingFactory,
|
|
220
|
-
connectTimeoutMs,
|
|
221
|
-
retry: FAST_RETRY,
|
|
222
|
-
onMessage: () => undefined,
|
|
223
|
-
});
|
|
224
|
-
|
|
225
|
-
const startedAt = Date.now();
|
|
226
|
-
await expect(conn.connect()).rejects.toThrow(/connect timeout/i);
|
|
227
|
-
expect(Date.now() - startedAt).toBeLessThan(connectTimeoutMs + 100);
|
|
228
|
-
expect(Date.now() - startedAt).toBeGreaterThanOrEqual(connectTimeoutMs - 10);
|
|
229
|
-
|
|
230
|
-
await conn.close();
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
it("rejects openSocket when the socket fires no event after construction", async () => {
|
|
234
|
-
const connectTimeoutMs = 50;
|
|
235
|
-
const silentSocket: ManagedSocket = {
|
|
236
|
-
get isOpen() {
|
|
237
|
-
return false;
|
|
238
|
-
},
|
|
239
|
-
send: () => undefined,
|
|
240
|
-
keepAlivePing: () => undefined,
|
|
241
|
-
verify: async () => false,
|
|
242
|
-
dispose: () => undefined,
|
|
243
|
-
onOpen: () => undefined,
|
|
244
|
-
onMessage: () => undefined,
|
|
245
|
-
onClose: () => undefined,
|
|
246
|
-
onError: () => undefined,
|
|
247
|
-
};
|
|
248
|
-
const socketFactory: SocketFactory = () => silentSocket;
|
|
249
|
-
|
|
250
|
-
const conn = new WebSocketConnection({
|
|
251
|
-
url: () => "ws://127.0.0.1:1/",
|
|
252
|
-
socketFactory,
|
|
253
|
-
connectTimeoutMs,
|
|
254
|
-
retry: { maxAttempts: 2, baseDelayMs: 5, maxDelayMs: 10 },
|
|
255
|
-
maxReconnectAttempts: 1,
|
|
256
|
-
minStableMs: 0,
|
|
257
|
-
onMessage: () => undefined,
|
|
258
|
-
});
|
|
259
|
-
|
|
260
|
-
const startedAt = Date.now();
|
|
261
|
-
await expect(conn.connect()).rejects.toThrow(/connect timeout/i);
|
|
262
|
-
expect(Date.now() - startedAt).toBeLessThan(connectTimeoutMs + 100);
|
|
263
|
-
|
|
264
|
-
await conn.close();
|
|
265
|
-
});
|
|
266
|
-
|
|
267
|
-
it("reconnects after openSocket times out on a silent socket during tryReconnect", async () => {
|
|
268
|
-
const connectTimeoutMs = 40;
|
|
269
|
-
let factoryCalls = 0;
|
|
270
|
-
const { url, server } = await createServer((socket) => {
|
|
271
|
-
socket.on("message", () => undefined);
|
|
272
|
-
});
|
|
273
|
-
|
|
274
|
-
const silentSocket: ManagedSocket = {
|
|
275
|
-
get isOpen() {
|
|
276
|
-
return false;
|
|
277
|
-
},
|
|
278
|
-
send: () => undefined,
|
|
279
|
-
keepAlivePing: () => undefined,
|
|
280
|
-
verify: async () => false,
|
|
281
|
-
dispose: () => undefined,
|
|
282
|
-
onOpen: () => undefined,
|
|
283
|
-
onMessage: () => undefined,
|
|
284
|
-
onClose: () => undefined,
|
|
285
|
-
onError: () => undefined,
|
|
286
|
-
};
|
|
287
|
-
|
|
288
|
-
const socketFactory: SocketFactory = (targetUrl, headers) => {
|
|
289
|
-
factoryCalls += 1;
|
|
290
|
-
if (factoryCalls === 2) return silentSocket;
|
|
291
|
-
return createNodeWsSocket(targetUrl, headers);
|
|
292
|
-
};
|
|
293
|
-
|
|
294
|
-
let reconnected = 0;
|
|
295
|
-
const conn = new WebSocketConnection({
|
|
296
|
-
url: () => url,
|
|
297
|
-
socketFactory,
|
|
298
|
-
connectTimeoutMs,
|
|
299
|
-
retry: { maxAttempts: 3, baseDelayMs: 5, maxDelayMs: 10 },
|
|
300
|
-
minStableMs: 0,
|
|
301
|
-
onMessage: () => undefined,
|
|
302
|
-
onReconnected: () => {
|
|
303
|
-
reconnected += 1;
|
|
304
|
-
},
|
|
305
|
-
});
|
|
306
|
-
|
|
307
|
-
await conn.connect();
|
|
308
|
-
expect(factoryCalls).toBe(1);
|
|
309
|
-
|
|
310
|
-
for (const client of server.clients) client.terminate();
|
|
311
|
-
await waitFor(() => reconnected >= 1, 5000);
|
|
312
|
-
expect(factoryCalls).toBeGreaterThanOrEqual(3);
|
|
313
|
-
|
|
314
|
-
await conn.close();
|
|
315
|
-
});
|
|
316
|
-
|
|
317
|
-
it("gives up after maxReconnectDurationMs when the link keeps flapping", async () => {
|
|
318
|
-
const minStableMs = 30;
|
|
319
|
-
const maxReconnectDurationMs = 200;
|
|
320
|
-
const { url } = await createServer((socket) => {
|
|
321
|
-
setTimeout(() => socket.close(1011, "flap"), minStableMs + 1);
|
|
322
|
-
});
|
|
323
|
-
|
|
324
|
-
let unrecoverable: Error | null = null;
|
|
325
|
-
let reconnects = 0;
|
|
326
|
-
const conn = new WebSocketConnection({
|
|
327
|
-
url: () => url,
|
|
328
|
-
socketFactory: createNodeWsSocket,
|
|
329
|
-
retry: { maxAttempts: 50, baseDelayMs: 5, maxDelayMs: 10 },
|
|
330
|
-
minStableMs,
|
|
331
|
-
maxQuickFailures: 100,
|
|
332
|
-
maxReconnectDurationMs,
|
|
333
|
-
maxReconnectAttempts: 50,
|
|
334
|
-
onMessage: () => undefined,
|
|
335
|
-
onReconnected: () => {
|
|
336
|
-
reconnects += 1;
|
|
337
|
-
},
|
|
338
|
-
onUnrecoverable: (err) => {
|
|
339
|
-
unrecoverable = err;
|
|
340
|
-
},
|
|
341
|
-
});
|
|
342
|
-
await conn.connect();
|
|
343
|
-
await waitFor(() => unrecoverable !== null, 5000);
|
|
344
|
-
|
|
345
|
-
expect((unrecoverable! as Error).message).toContain(String(maxReconnectDurationMs));
|
|
346
|
-
expect(reconnects).toBeGreaterThanOrEqual(1);
|
|
347
|
-
|
|
348
|
-
await conn.close();
|
|
349
|
-
});
|
|
350
|
-
});
|
package/src/node.test.ts
DELETED
|
@@ -1,68 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// Regression: disposing a Node `ws` socket that is still CONNECTING must not crash
|
|
4
|
-
// the process. `ws.close()` on a connecting socket emits an asynchronous 'error'
|
|
5
|
-
// event ("WebSocket was closed before the connection was established"); dispose()
|
|
6
|
-
// removeAllListeners()'d the error sink, so without re-attaching one the event
|
|
7
|
-
// becomes an uncaught exception. This happens whenever a caller hangs up during a
|
|
8
|
-
// slow provider connect (surfaced live via the Cartesia TTS plugin teardown).
|
|
9
|
-
|
|
10
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
11
|
-
import { WebSocketServer, type WebSocket } from "ws";
|
|
12
|
-
import { createNodeWsSocket } from "./node.js";
|
|
13
|
-
|
|
14
|
-
describe("createNodeWsSocket dispose while connecting", () => {
|
|
15
|
-
afterEach(() => {
|
|
16
|
-
vi.restoreAllMocks();
|
|
17
|
-
});
|
|
18
|
-
|
|
19
|
-
it("does not raise an unhandled 'error' when disposed mid-handshake", async () => {
|
|
20
|
-
// A socket to a non-routable address stays in CONNECTING. If dispose() leaked an
|
|
21
|
-
// unhandled 'error', the vitest process would record an uncaughtException and fail
|
|
22
|
-
// this file — reaching the assertion is the proof.
|
|
23
|
-
const uncaught: unknown[] = [];
|
|
24
|
-
const onUncaught = (err: unknown): void => {
|
|
25
|
-
uncaught.push(err);
|
|
26
|
-
};
|
|
27
|
-
process.on("uncaughtException", onUncaught);
|
|
28
|
-
try {
|
|
29
|
-
// createNodeWsSocket is synchronous; await narrows the SocketFactory union.
|
|
30
|
-
const socket = await createNodeWsSocket("ws://10.255.255.1:9999", {});
|
|
31
|
-
socket.dispose(); // synchronous, while readyState === CONNECTING
|
|
32
|
-
await new Promise((resolve) => setTimeout(resolve, 150));
|
|
33
|
-
} finally {
|
|
34
|
-
process.off("uncaughtException", onUncaught);
|
|
35
|
-
}
|
|
36
|
-
expect(uncaught).toEqual([]);
|
|
37
|
-
});
|
|
38
|
-
|
|
39
|
-
it("clears the verify timeout when disposed during verify", async () => {
|
|
40
|
-
vi.useFakeTimers();
|
|
41
|
-
const server = await new Promise<WebSocketServer>((resolve) => {
|
|
42
|
-
let next: WebSocketServer;
|
|
43
|
-
next = new WebSocketServer({ port: 0 }, () => resolve(next));
|
|
44
|
-
});
|
|
45
|
-
server.on("connection", (ws) => {
|
|
46
|
-
ws.on("ping", () => undefined);
|
|
47
|
-
});
|
|
48
|
-
const address = server.address();
|
|
49
|
-
if (!address || typeof address === "string") throw new Error("Expected TCP server address");
|
|
50
|
-
|
|
51
|
-
const socket = await createNodeWsSocket(`ws://127.0.0.1:${String(address.port)}/`, {});
|
|
52
|
-
await new Promise<void>((resolve, reject) => {
|
|
53
|
-
const timer = setTimeout(() => reject(new Error("Timed out waiting for websocket open")), 2000);
|
|
54
|
-
socket.onOpen(() => {
|
|
55
|
-
clearTimeout(timer);
|
|
56
|
-
resolve();
|
|
57
|
-
});
|
|
58
|
-
});
|
|
59
|
-
|
|
60
|
-
const verifyPromise = socket.verify(5000);
|
|
61
|
-
socket.dispose();
|
|
62
|
-
await vi.advanceTimersByTimeAsync(6000);
|
|
63
|
-
await expect(verifyPromise).resolves.toBe(false);
|
|
64
|
-
|
|
65
|
-
await new Promise<void>((resolve) => server.close(() => resolve()));
|
|
66
|
-
vi.useRealTimers();
|
|
67
|
-
});
|
|
68
|
-
});
|
package/src/replay.test.ts
DELETED
|
@@ -1,99 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// VE-06.2: reconnect replay. A frame whose send() fails because the socket is not open
|
|
4
|
-
// (provably never reached the wire) is buffered and re-sent in order on reconnect; frames
|
|
5
|
-
// sent on an open socket are never buffered, so received frames are never duplicated.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
-
import type { RetryConfig } from "@kuralle-syrinx/core";
|
|
9
|
-
|
|
10
|
-
import { WebSocketConnection, type SocketData } from "./index.js";
|
|
11
|
-
import { wrapWebSocket, type WebSocketEventLike, type WebSocketLike } from "./web-socket.js";
|
|
12
|
-
|
|
13
|
-
const FAST_RETRY: RetryConfig = { maxAttempts: 5, baseDelayMs: 5, maxDelayMs: 20 };
|
|
14
|
-
|
|
15
|
-
class FakeWebSocket implements WebSocketLike {
|
|
16
|
-
readyState = 0;
|
|
17
|
-
binaryType = "blob";
|
|
18
|
-
readonly sent: SocketData[] = [];
|
|
19
|
-
private readonly listeners = new Map<string, Array<(event: WebSocketEventLike) => void>>();
|
|
20
|
-
send(data: string | ArrayBufferView | ArrayBuffer): void {
|
|
21
|
-
this.sent.push(typeof data === "string" ? data : new Uint8Array(data as ArrayBuffer));
|
|
22
|
-
}
|
|
23
|
-
close(): void { this.readyState = 3; this.emit("close", { code: 1006, reason: "drop" }); }
|
|
24
|
-
addEventListener(type: string, l: (e: WebSocketEventLike) => void): void {
|
|
25
|
-
const list = this.listeners.get(type) ?? []; list.push(l); this.listeners.set(type, list);
|
|
26
|
-
}
|
|
27
|
-
private emit(type: string, event: WebSocketEventLike): void { for (const l of this.listeners.get(type) ?? []) l(event); }
|
|
28
|
-
fireOpen(): void { this.readyState = 1; this.emit("open", {}); }
|
|
29
|
-
}
|
|
30
|
-
|
|
31
|
-
const wait = (ms: number) => new Promise((r) => setTimeout(r, ms));
|
|
32
|
-
|
|
33
|
-
describe("WebSocketConnection reconnect replay (VE-06.2)", () => {
|
|
34
|
-
it("send() to a closed socket throws when replay is disabled (default)", async () => {
|
|
35
|
-
const fake = new FakeWebSocket();
|
|
36
|
-
const conn = new WebSocketConnection({
|
|
37
|
-
url: () => "wss://x", socketFactory: () => wrapWebSocket(fake), retry: FAST_RETRY,
|
|
38
|
-
onMessage: () => undefined,
|
|
39
|
-
});
|
|
40
|
-
const connected = conn.connect(); fake.fireOpen(); await connected;
|
|
41
|
-
fake.readyState = 3; // simulate closed
|
|
42
|
-
expect(() => conn.send("frame")).toThrow(/not open/);
|
|
43
|
-
await conn.close();
|
|
44
|
-
});
|
|
45
|
-
|
|
46
|
-
it("buffers gap frames and replays them in order on reconnect; on-wire frames are not replayed", async () => {
|
|
47
|
-
const fakes: FakeWebSocket[] = [];
|
|
48
|
-
const replayEvents: Array<{ event: string; count: number }> = [];
|
|
49
|
-
const conn = new WebSocketConnection({
|
|
50
|
-
url: () => "wss://x",
|
|
51
|
-
socketFactory: () => { const f = new FakeWebSocket(); fakes.push(f); return wrapWebSocket(f); },
|
|
52
|
-
retry: FAST_RETRY,
|
|
53
|
-
replayBufferSize: 10,
|
|
54
|
-
onReplay: (event, count) => replayEvents.push({ event, count }),
|
|
55
|
-
onMessage: () => undefined,
|
|
56
|
-
});
|
|
57
|
-
|
|
58
|
-
const connected = conn.connect();
|
|
59
|
-
fakes[0]!.fireOpen();
|
|
60
|
-
await connected;
|
|
61
|
-
|
|
62
|
-
conn.send("on-wire-1"); // sent on the open socket — must NOT be replayed
|
|
63
|
-
expect(fakes[0]!.sent).toEqual(["on-wire-1"]);
|
|
64
|
-
|
|
65
|
-
// Connection drops → reconnect begins.
|
|
66
|
-
fakes[0]!.close();
|
|
67
|
-
// During the gap (socket not open) the caller keeps sending — these buffer for replay.
|
|
68
|
-
conn.send("gap-1");
|
|
69
|
-
conn.send("gap-2");
|
|
70
|
-
|
|
71
|
-
// Wait for the reconnect to create a new socket, then open it.
|
|
72
|
-
await wait(40);
|
|
73
|
-
expect(fakes.length).toBeGreaterThanOrEqual(2);
|
|
74
|
-
fakes[fakes.length - 1]!.fireOpen();
|
|
75
|
-
await wait(10);
|
|
76
|
-
|
|
77
|
-
const reconnected = fakes[fakes.length - 1]!;
|
|
78
|
-
// The two gap frames are replayed in order on the new socket; the on-wire frame is NOT.
|
|
79
|
-
expect(reconnected.sent).toEqual(["gap-1", "gap-2"]);
|
|
80
|
-
expect(replayEvents.filter((e) => e.event === "replayed")).toHaveLength(1);
|
|
81
|
-
expect(replayEvents.find((e) => e.event === "replayed")?.count).toBe(2);
|
|
82
|
-
|
|
83
|
-
await conn.close();
|
|
84
|
-
});
|
|
85
|
-
|
|
86
|
-
it("bounds the replay buffer, dropping oldest with an overflow signal", async () => {
|
|
87
|
-
const fake = new FakeWebSocket();
|
|
88
|
-
const events: string[] = [];
|
|
89
|
-
const conn = new WebSocketConnection({
|
|
90
|
-
url: () => "wss://x", socketFactory: () => wrapWebSocket(fake), retry: FAST_RETRY,
|
|
91
|
-
replayBufferSize: 2, onReplay: (e) => events.push(e), onMessage: () => undefined,
|
|
92
|
-
});
|
|
93
|
-
const connected = conn.connect(); fake.fireOpen(); await connected;
|
|
94
|
-
fake.readyState = 3; // closed
|
|
95
|
-
conn.send("a"); conn.send("b"); conn.send("c"); // buffer holds last 2 → "a" overflows
|
|
96
|
-
expect(events.filter((e) => e === "overflow")).toHaveLength(1);
|
|
97
|
-
await conn.close();
|
|
98
|
-
});
|
|
99
|
-
});
|
package/src/web-socket.test.ts
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// Proves the connection manager runs over the standard built-in WebSocket
|
|
4
|
-
// (Cloudflare Workers / browser / Bun) with no `ws` and no ping frames — keepalive uses an
|
|
5
|
-
// app message and verify falls back to readyState.
|
|
6
|
-
|
|
7
|
-
import { describe, expect, it } from "vitest";
|
|
8
|
-
import type { RetryConfig } from "@kuralle-syrinx/core";
|
|
9
|
-
|
|
10
|
-
import { WebSocketConnection, type SocketData } from "./index.js";
|
|
11
|
-
import { wrapWebSocket, type WebSocketEventLike, type WebSocketLike } from "./web-socket.js";
|
|
12
|
-
|
|
13
|
-
const FAST_RETRY: RetryConfig = { maxAttempts: 3, baseDelayMs: 10, maxDelayMs: 40 };
|
|
14
|
-
|
|
15
|
-
/** A controllable standard-WebSocket-shaped socket — no ping, addEventListener-based. */
|
|
16
|
-
class FakeWebSocket implements WebSocketLike {
|
|
17
|
-
readyState = 0;
|
|
18
|
-
binaryType = "blob";
|
|
19
|
-
readonly sent: SocketData[] = [];
|
|
20
|
-
private readonly listeners = new Map<string, Array<(event: WebSocketEventLike) => void>>();
|
|
21
|
-
|
|
22
|
-
send(data: string | ArrayBufferView | ArrayBuffer): void {
|
|
23
|
-
this.sent.push(typeof data === "string" ? data : new Uint8Array(data as ArrayBuffer));
|
|
24
|
-
}
|
|
25
|
-
close(): void {
|
|
26
|
-
this.readyState = 3;
|
|
27
|
-
this.emit("close", { code: 1000, reason: "" });
|
|
28
|
-
}
|
|
29
|
-
addEventListener(type: string, listener: (event: WebSocketEventLike) => void): void {
|
|
30
|
-
const list = this.listeners.get(type) ?? [];
|
|
31
|
-
list.push(listener);
|
|
32
|
-
this.listeners.set(type, list);
|
|
33
|
-
}
|
|
34
|
-
private emit(type: string, event: WebSocketEventLike): void {
|
|
35
|
-
for (const l of this.listeners.get(type) ?? []) l(event);
|
|
36
|
-
}
|
|
37
|
-
fireOpen(): void {
|
|
38
|
-
this.readyState = 1;
|
|
39
|
-
this.emit("open", {});
|
|
40
|
-
}
|
|
41
|
-
fireMessage(data: string | ArrayBuffer): void {
|
|
42
|
-
this.emit("message", { data });
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
describe("WebSocketConnection over the standard WebSocket (Workers/browser)", () => {
|
|
47
|
-
it("connects, delivers text + binary, and keepalives with an app message (no ping)", async () => {
|
|
48
|
-
const fake = new FakeWebSocket();
|
|
49
|
-
const received: Array<{ data: SocketData; isBinary: boolean }> = [];
|
|
50
|
-
const conn = new WebSocketConnection({
|
|
51
|
-
url: () => "wss://example/v1",
|
|
52
|
-
socketFactory: () => wrapWebSocket(fake),
|
|
53
|
-
retry: FAST_RETRY,
|
|
54
|
-
keepAliveIntervalMs: 20,
|
|
55
|
-
keepAliveMessage: () => JSON.stringify({ type: "KeepAlive" }),
|
|
56
|
-
onMessage: (data, isBinary) => received.push({ data, isBinary }),
|
|
57
|
-
});
|
|
58
|
-
|
|
59
|
-
const connected = conn.connect();
|
|
60
|
-
fake.fireOpen();
|
|
61
|
-
await connected;
|
|
62
|
-
expect(conn.isReady).toBe(true);
|
|
63
|
-
|
|
64
|
-
fake.fireMessage("hello-text");
|
|
65
|
-
fake.fireMessage(new Uint8Array([1, 2, 3]).buffer);
|
|
66
|
-
|
|
67
|
-
await new Promise((resolve) => setTimeout(resolve, 60));
|
|
68
|
-
|
|
69
|
-
expect(received[0]).toEqual({ data: "hello-text", isBinary: false });
|
|
70
|
-
const binary = received.find((r) => r.isBinary);
|
|
71
|
-
expect(binary?.data).toEqual(new Uint8Array([1, 2, 3]));
|
|
72
|
-
// KeepAlive went out as an app message, since the built-in WebSocket has no ping frame.
|
|
73
|
-
expect(fake.sent.filter((m) => typeof m === "string" && m.includes("KeepAlive")).length).toBeGreaterThanOrEqual(1);
|
|
74
|
-
|
|
75
|
-
await conn.close();
|
|
76
|
-
expect(fake.readyState).toBe(3);
|
|
77
|
-
});
|
|
78
|
-
|
|
79
|
-
it("verify fails when livenessProbe gets no reply and reconnect continues", async () => {
|
|
80
|
-
let activeFake: FakeWebSocket | null = null;
|
|
81
|
-
let probeCalls = 0;
|
|
82
|
-
let reconnecting = 0;
|
|
83
|
-
|
|
84
|
-
const conn = new WebSocketConnection({
|
|
85
|
-
url: () => "wss://example/v1",
|
|
86
|
-
socketFactory: () => {
|
|
87
|
-
activeFake = new FakeWebSocket();
|
|
88
|
-
queueMicrotask(() => activeFake!.fireOpen());
|
|
89
|
-
return wrapWebSocket(activeFake);
|
|
90
|
-
},
|
|
91
|
-
retry: FAST_RETRY,
|
|
92
|
-
minStableMs: 0,
|
|
93
|
-
connectTimeoutMs: 500,
|
|
94
|
-
livenessProbe: async () => {
|
|
95
|
-
probeCalls += 1;
|
|
96
|
-
await new Promise((resolve) => setTimeout(resolve, 20));
|
|
97
|
-
return false;
|
|
98
|
-
},
|
|
99
|
-
onMessage: () => undefined,
|
|
100
|
-
onReconnecting: () => {
|
|
101
|
-
reconnecting += 1;
|
|
102
|
-
},
|
|
103
|
-
});
|
|
104
|
-
|
|
105
|
-
await conn.connect();
|
|
106
|
-
expect(conn.isReady).toBe(true);
|
|
107
|
-
|
|
108
|
-
activeFake!.close();
|
|
109
|
-
await new Promise((resolve) => setTimeout(resolve, 500));
|
|
110
|
-
|
|
111
|
-
expect(probeCalls).toBeGreaterThanOrEqual(1);
|
|
112
|
-
expect(reconnecting).toBeGreaterThanOrEqual(1);
|
|
113
|
-
|
|
114
|
-
await conn.close();
|
|
115
|
-
});
|
|
116
|
-
});
|
|
117
|
-
|
|
118
|
-
describe("wrapWebSocket skip-open (workerd fetch-upgrade)", () => {
|
|
119
|
-
it("invokes onOpen via microtask when the socket is already open", async () => {
|
|
120
|
-
const fake = new FakeWebSocket();
|
|
121
|
-
fake.readyState = 1;
|
|
122
|
-
|
|
123
|
-
let opened = false;
|
|
124
|
-
wrapWebSocket(fake).onOpen(() => {
|
|
125
|
-
opened = true;
|
|
126
|
-
});
|
|
127
|
-
|
|
128
|
-
expect(opened).toBe(false);
|
|
129
|
-
await new Promise<void>((resolve) => queueMicrotask(resolve));
|
|
130
|
-
expect(opened).toBe(true);
|
|
131
|
-
});
|
|
132
|
-
});
|
package/src/workers.test.ts
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
//
|
|
3
|
-
// Proves the Workers fetch-upgrade adapter drives the full manager: the async
|
|
4
|
-
// socket factory is awaited, the auth headers + Upgrade go out on the fetch, and
|
|
5
|
-
// the accepted (already-open) socket connects without an "open" event.
|
|
6
|
-
|
|
7
|
-
import { afterEach, describe, expect, it, vi } from "vitest";
|
|
8
|
-
import type { RetryConfig } from "@kuralle-syrinx/core";
|
|
9
|
-
|
|
10
|
-
import { WebSocketConnection } from "./index.js";
|
|
11
|
-
import { createWorkersSocket } from "./workers.js";
|
|
12
|
-
import type { WebSocketEventLike } from "./web-socket.js";
|
|
13
|
-
|
|
14
|
-
const FAST_RETRY: RetryConfig = { maxAttempts: 3, baseDelayMs: 10, maxDelayMs: 40 };
|
|
15
|
-
|
|
16
|
-
const realFetch = globalThis.fetch;
|
|
17
|
-
afterEach(() => {
|
|
18
|
-
globalThis.fetch = realFetch;
|
|
19
|
-
});
|
|
20
|
-
|
|
21
|
-
/** A Workers-style already-open socket returned by a fetch upgrade. */
|
|
22
|
-
class FakeWorkersSocket {
|
|
23
|
-
readyState = 1; // already OPEN, as after a fetch upgrade
|
|
24
|
-
binaryType = "blob";
|
|
25
|
-
accepted = false;
|
|
26
|
-
readonly sent: Array<string | ArrayBufferView | ArrayBuffer> = [];
|
|
27
|
-
private readonly listeners = new Map<string, Array<(event: WebSocketEventLike) => void>>();
|
|
28
|
-
accept(): void {
|
|
29
|
-
this.accepted = true;
|
|
30
|
-
}
|
|
31
|
-
send(data: string | ArrayBufferView | ArrayBuffer): void {
|
|
32
|
-
this.sent.push(data);
|
|
33
|
-
}
|
|
34
|
-
close(): void {
|
|
35
|
-
this.readyState = 3;
|
|
36
|
-
}
|
|
37
|
-
addEventListener(type: string, listener: (event: WebSocketEventLike) => void): void {
|
|
38
|
-
const list = this.listeners.get(type) ?? [];
|
|
39
|
-
list.push(listener);
|
|
40
|
-
this.listeners.set(type, list);
|
|
41
|
-
}
|
|
42
|
-
fire(type: string, event: WebSocketEventLike): void {
|
|
43
|
-
for (const l of this.listeners.get(type) ?? []) l(event);
|
|
44
|
-
}
|
|
45
|
-
}
|
|
46
|
-
|
|
47
|
-
describe("createWorkersSocket (Cloudflare fetch-upgrade)", () => {
|
|
48
|
-
it("upgrades with auth headers and connects an already-open socket", async () => {
|
|
49
|
-
const fake = new FakeWorkersSocket();
|
|
50
|
-
const fetchMock = vi.fn(async (_url: string, _init: { headers: Record<string, string> }) => ({
|
|
51
|
-
status: 101,
|
|
52
|
-
webSocket: fake,
|
|
53
|
-
}));
|
|
54
|
-
globalThis.fetch = fetchMock as unknown as typeof fetch;
|
|
55
|
-
|
|
56
|
-
const received: string[] = [];
|
|
57
|
-
const conn = new WebSocketConnection({
|
|
58
|
-
url: () => "wss://api.deepgram.com/v1/listen",
|
|
59
|
-
headers: { Authorization: "Token secret-key" },
|
|
60
|
-
socketFactory: createWorkersSocket,
|
|
61
|
-
retry: FAST_RETRY,
|
|
62
|
-
onMessage: (data) => {
|
|
63
|
-
if (typeof data === "string") received.push(data);
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
|
|
67
|
-
await conn.connect();
|
|
68
|
-
expect(conn.isReady).toBe(true);
|
|
69
|
-
expect(fake.accepted).toBe(true);
|
|
70
|
-
|
|
71
|
-
// The fetch carried the Upgrade + the provider auth header.
|
|
72
|
-
const init = fetchMock.mock.calls[0]![1];
|
|
73
|
-
expect(init.headers.Upgrade).toBe("websocket");
|
|
74
|
-
expect(init.headers.Authorization).toBe("Token secret-key");
|
|
75
|
-
|
|
76
|
-
fake.fire("message", { data: "{\"type\":\"Results\"}" });
|
|
77
|
-
expect(received).toEqual(["{\"type\":\"Results\"}"]);
|
|
78
|
-
|
|
79
|
-
conn.send("audio-frame");
|
|
80
|
-
expect(fake.sent).toContain("audio-frame");
|
|
81
|
-
|
|
82
|
-
await conn.close();
|
|
83
|
-
});
|
|
84
|
-
});
|
package/tsconfig.json
DELETED
|
@@ -1,21 +0,0 @@
|
|
|
1
|
-
{
|
|
2
|
-
"compilerOptions": {
|
|
3
|
-
"target": "ES2022",
|
|
4
|
-
"module": "NodeNext",
|
|
5
|
-
"moduleResolution": "NodeNext",
|
|
6
|
-
"lib": ["ES2022"],
|
|
7
|
-
"strict": true,
|
|
8
|
-
"noUncheckedIndexedAccess": true,
|
|
9
|
-
"noImplicitReturns": true,
|
|
10
|
-
"declaration": true,
|
|
11
|
-
"declarationMap": true,
|
|
12
|
-
"sourceMap": true,
|
|
13
|
-
"outDir": "./dist",
|
|
14
|
-
"rootDir": "./src",
|
|
15
|
-
"esModuleInterop": true,
|
|
16
|
-
"skipLibCheck": true,
|
|
17
|
-
"forceConsistentCasingInFileNames": true
|
|
18
|
-
},
|
|
19
|
-
"include": ["src/**/*.ts"],
|
|
20
|
-
"exclude": ["node_modules", "dist"]
|
|
21
|
-
}
|