@kuralle-syrinx/server-websocket 4.2.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 +6 -5
- package/src/carrier-commands.ts +332 -0
- package/src/edge-telnyx.ts +389 -0
- package/src/edge.ts +7 -2
- package/src/index.ts +25 -2
- package/src/telnyx-codec.ts +163 -0
- package/src/telnyx.ts +54 -59
- package/src/twilio.ts +23 -0
- package/src/wire-carrier-control.ts +179 -0
- package/src/admission-control.test.ts +0 -270
- package/src/background-audio.test.ts +0 -224
- package/src/browser-opus.test.ts +0 -41
- package/src/browser-pacing.test.ts +0 -440
- package/src/edge-twilio.test.ts +0 -293
- package/src/edge.test.ts +0 -591
- package/src/graceful-drain.test.ts +0 -306
- package/src/inbound-audio.test.ts +0 -58
- package/src/index.test.ts +0 -2074
- package/src/outbound-playout-pipeline.test.ts +0 -314
- package/src/paced-playout.test.ts +0 -247
- package/src/playout-progress.test.ts +0 -56
- package/src/session-store.test.ts +0 -274
- package/src/smartpbx.test.ts +0 -967
- package/src/telnyx.test.ts +0 -1457
- package/src/transport-host.test.ts +0 -51
- package/src/turn-metrics.test.ts +0 -327
- package/src/twilio-auth.test.ts +0 -36
- package/src/twilio.test.ts +0 -1275
- package/src/websocket-close.test.ts +0 -63
- package/src/websocket-lifecycle.test.ts +0 -49
- package/tsconfig.json +0 -22
- package/vitest.config.ts +0 -7
|
@@ -1,440 +0,0 @@
|
|
|
1
|
-
// SPDX-License-Identifier: MIT
|
|
2
|
-
// WT-03: Browser outbound pacing + playout clock + client jitter buffer
|
|
3
|
-
|
|
4
|
-
import { createServer } from "node:http";
|
|
5
|
-
import { afterEach, beforeEach, describe, expect, it, vi } from "vitest";
|
|
6
|
-
import WebSocket from "ws";
|
|
7
|
-
import { Route, VoiceAgentSession } from "@kuralle-syrinx/core";
|
|
8
|
-
import { pcm16SamplesToBytes } from "@kuralle-syrinx/core/audio";
|
|
9
|
-
import { createVoiceWebSocketServer, type VoiceWebSocketServer } from "./index.js";
|
|
10
|
-
|
|
11
|
-
function browserUrl(port: number): string {
|
|
12
|
-
return `ws://127.0.0.1:${port}/ws`;
|
|
13
|
-
}
|
|
14
|
-
|
|
15
|
-
async function openBrowserSocketReady(url: string): Promise<WebSocket> {
|
|
16
|
-
const socket = new WebSocket(url);
|
|
17
|
-
await new Promise<void>((resolve, reject) => {
|
|
18
|
-
const onMessage = (data: WebSocket.RawData, isBinary: boolean): void => {
|
|
19
|
-
if (isBinary) return;
|
|
20
|
-
try {
|
|
21
|
-
const parsed = JSON.parse(data.toString()) as { type?: string };
|
|
22
|
-
if (parsed.type === "ready") {
|
|
23
|
-
socket.off("message", onMessage);
|
|
24
|
-
resolve();
|
|
25
|
-
}
|
|
26
|
-
} catch {
|
|
27
|
-
// Ignore parsing errors
|
|
28
|
-
}
|
|
29
|
-
};
|
|
30
|
-
socket.on("message", onMessage);
|
|
31
|
-
socket.once("error", reject);
|
|
32
|
-
});
|
|
33
|
-
return socket;
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
function waitMs(ms: number): Promise<void> {
|
|
37
|
-
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
38
|
-
}
|
|
39
|
-
|
|
40
|
-
async function waitForCondition(predicate: () => boolean, timeoutMs = 1000): Promise<void> {
|
|
41
|
-
const startedAt = Date.now();
|
|
42
|
-
while (Date.now() - startedAt < timeoutMs) {
|
|
43
|
-
if (predicate()) return;
|
|
44
|
-
await waitMs(10);
|
|
45
|
-
}
|
|
46
|
-
throw new Error("Timed out waiting for browser pacing condition");
|
|
47
|
-
}
|
|
48
|
-
|
|
49
|
-
async function readJsonMatching(socket: WebSocket, predicate: (m: unknown) => boolean): Promise<unknown> {
|
|
50
|
-
return new Promise((resolve) => {
|
|
51
|
-
const onMessage = (data: WebSocket.RawData, isBinary: boolean) => {
|
|
52
|
-
if (isBinary) return;
|
|
53
|
-
const message = JSON.parse(data.toString()) as unknown;
|
|
54
|
-
if (!predicate(message)) return;
|
|
55
|
-
socket.off("message", onMessage);
|
|
56
|
-
resolve(message);
|
|
57
|
-
};
|
|
58
|
-
socket.on("message", onMessage);
|
|
59
|
-
});
|
|
60
|
-
}
|
|
61
|
-
|
|
62
|
-
function collectMessagesMatching(
|
|
63
|
-
socket: WebSocket,
|
|
64
|
-
predicate: (m: unknown) => boolean,
|
|
65
|
-
timeoutMs: number
|
|
66
|
-
): Promise<unknown[]> {
|
|
67
|
-
return new Promise((resolve) => {
|
|
68
|
-
const messages: unknown[] = [];
|
|
69
|
-
const timeout = setTimeout(() => {
|
|
70
|
-
socket.off("message", onMessage);
|
|
71
|
-
resolve(messages);
|
|
72
|
-
}, timeoutMs);
|
|
73
|
-
|
|
74
|
-
const onMessage = (data: WebSocket.RawData, isBinary: boolean) => {
|
|
75
|
-
if (isBinary) return;
|
|
76
|
-
try {
|
|
77
|
-
const message = JSON.parse(data.toString()) as unknown;
|
|
78
|
-
if (predicate(message)) {
|
|
79
|
-
messages.push(message);
|
|
80
|
-
}
|
|
81
|
-
} catch {
|
|
82
|
-
// Ignore parsing errors
|
|
83
|
-
}
|
|
84
|
-
};
|
|
85
|
-
socket.on("message", onMessage);
|
|
86
|
-
});
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
// Track servers so we can force-close them in afterEach even if test fails
|
|
90
|
-
let activeServers: VoiceWebSocketServer[] = [];
|
|
91
|
-
let activeHttpServers: ReturnType<typeof createServer>[] = [];
|
|
92
|
-
|
|
93
|
-
beforeEach(() => {
|
|
94
|
-
activeServers = [];
|
|
95
|
-
activeHttpServers = [];
|
|
96
|
-
});
|
|
97
|
-
|
|
98
|
-
afterEach(async () => {
|
|
99
|
-
for (const server of activeServers) {
|
|
100
|
-
await server.close().catch(() => undefined);
|
|
101
|
-
}
|
|
102
|
-
for (const httpServer of activeHttpServers) {
|
|
103
|
-
httpServer.close();
|
|
104
|
-
}
|
|
105
|
-
activeServers = [];
|
|
106
|
-
activeHttpServers = [];
|
|
107
|
-
});
|
|
108
|
-
|
|
109
|
-
describe("WT-03 Browser outbound pacing", () => {
|
|
110
|
-
it("browser adapter enqueues paced frames with consistent timing", async () => {
|
|
111
|
-
const httpServer = createServer();
|
|
112
|
-
activeHttpServers.push(httpServer);
|
|
113
|
-
|
|
114
|
-
let session: VoiceAgentSession | null = null;
|
|
115
|
-
const server = await createVoiceWebSocketServer({
|
|
116
|
-
server: httpServer,
|
|
117
|
-
port: 0, // Let the system assign a port
|
|
118
|
-
createSession: () => {
|
|
119
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
120
|
-
return session;
|
|
121
|
-
},
|
|
122
|
-
outboundFrameDurationMs: 20,
|
|
123
|
-
});
|
|
124
|
-
activeServers.push(server);
|
|
125
|
-
|
|
126
|
-
const port = (server.address() as any).port;
|
|
127
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
128
|
-
|
|
129
|
-
// Session is already started by the websocket server
|
|
130
|
-
|
|
131
|
-
// Send TTS audio - 160ms of 16kHz PCM16 (should be split into ~8 frames of 20ms each)
|
|
132
|
-
const sampleCount = Math.floor(0.16 * 16000); // 160ms at 16kHz
|
|
133
|
-
const audioSamples = new Int16Array(sampleCount).fill(1000); // Simple sine-like values
|
|
134
|
-
const audioBytes = pcm16SamplesToBytes(audioSamples);
|
|
135
|
-
|
|
136
|
-
// Collect tts_chunk messages over 200ms to ensure all frames arrive
|
|
137
|
-
const ttsChunksPromise = collectMessagesMatching(
|
|
138
|
-
socket,
|
|
139
|
-
(m: any) => m.type === "tts_chunk",
|
|
140
|
-
300
|
|
141
|
-
);
|
|
142
|
-
|
|
143
|
-
// Send TTS audio
|
|
144
|
-
session!.bus.push(Route.Main, {
|
|
145
|
-
kind: "tts.audio",
|
|
146
|
-
contextId: "test-turn",
|
|
147
|
-
timestampMs: Date.now(),
|
|
148
|
-
audio: audioBytes,
|
|
149
|
-
sampleRateHz: 16000,
|
|
150
|
-
});
|
|
151
|
-
|
|
152
|
-
// End TTS
|
|
153
|
-
session!.bus.push(Route.Main, {
|
|
154
|
-
kind: "tts.end",
|
|
155
|
-
contextId: "test-turn",
|
|
156
|
-
timestampMs: Date.now(),
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
const ttsChunks = await ttsChunksPromise;
|
|
160
|
-
socket.close();
|
|
161
|
-
|
|
162
|
-
// Verify we got multiple frames (pacing is working)
|
|
163
|
-
expect(ttsChunks.length).toBeGreaterThan(1);
|
|
164
|
-
|
|
165
|
-
// Verify each frame has expected structure
|
|
166
|
-
for (const chunk of ttsChunks) {
|
|
167
|
-
const c = chunk as any;
|
|
168
|
-
expect(c.type).toBe("tts_chunk");
|
|
169
|
-
expect(c.turnId).toBe("test-turn");
|
|
170
|
-
expect(c.sequence).toBeGreaterThan(0);
|
|
171
|
-
expect(c.sampleRateHz).toBe(48000); // opus frames carry the 48 kHz codec rate
|
|
172
|
-
expect(c.encoding).toBe("opus");
|
|
173
|
-
expect(c.channels).toBe(1);
|
|
174
|
-
expect(c.byteLength).toBeGreaterThan(0);
|
|
175
|
-
expect(c.durationMs).toBeGreaterThan(0);
|
|
176
|
-
}
|
|
177
|
-
|
|
178
|
-
// Verify sequence numbers increment
|
|
179
|
-
const sequences = ttsChunks.map((c: any) => c.sequence);
|
|
180
|
-
for (let i = 1; i < sequences.length; i++) {
|
|
181
|
-
expect(sequences[i]).toBe(sequences[i - 1] + 1);
|
|
182
|
-
}
|
|
183
|
-
});
|
|
184
|
-
|
|
185
|
-
it("emits tts.playout_progress events during paced playback", async () => {
|
|
186
|
-
const httpServer = createServer();
|
|
187
|
-
activeHttpServers.push(httpServer);
|
|
188
|
-
|
|
189
|
-
let session: VoiceAgentSession | null = null;
|
|
190
|
-
const server = await createVoiceWebSocketServer({
|
|
191
|
-
server: httpServer,
|
|
192
|
-
port: 0,
|
|
193
|
-
createSession: () => {
|
|
194
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
195
|
-
return session;
|
|
196
|
-
},
|
|
197
|
-
outboundFrameDurationMs: 20,
|
|
198
|
-
});
|
|
199
|
-
activeServers.push(server);
|
|
200
|
-
|
|
201
|
-
const port = (server.address() as any).port;
|
|
202
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
203
|
-
|
|
204
|
-
// Session is already started by the websocket server
|
|
205
|
-
|
|
206
|
-
// Track playout progress events
|
|
207
|
-
const progressEvents: any[] = [];
|
|
208
|
-
const removeProgressHandler = session!.bus.on("tts.playout_progress", (pkt: any) => {
|
|
209
|
-
if (pkt.contextId === "test-turn") {
|
|
210
|
-
progressEvents.push(pkt);
|
|
211
|
-
}
|
|
212
|
-
});
|
|
213
|
-
|
|
214
|
-
// Send TTS audio - 80ms worth
|
|
215
|
-
const sampleCount = Math.floor(0.08 * 16000);
|
|
216
|
-
const audioSamples = new Int16Array(sampleCount).fill(1000);
|
|
217
|
-
const audioBytes = pcm16SamplesToBytes(audioSamples);
|
|
218
|
-
|
|
219
|
-
session!.bus.push(Route.Main, {
|
|
220
|
-
kind: "tts.audio",
|
|
221
|
-
contextId: "test-turn",
|
|
222
|
-
timestampMs: Date.now(),
|
|
223
|
-
audio: audioBytes,
|
|
224
|
-
sampleRateHz: 16000,
|
|
225
|
-
});
|
|
226
|
-
|
|
227
|
-
session!.bus.push(Route.Main, {
|
|
228
|
-
kind: "tts.end",
|
|
229
|
-
contextId: "test-turn",
|
|
230
|
-
timestampMs: Date.now(),
|
|
231
|
-
});
|
|
232
|
-
|
|
233
|
-
// Wait for playout to complete
|
|
234
|
-
await waitMs(150);
|
|
235
|
-
|
|
236
|
-
socket.close();
|
|
237
|
-
removeProgressHandler();
|
|
238
|
-
|
|
239
|
-
// Should have received playout progress events
|
|
240
|
-
expect(progressEvents.length).toBeGreaterThan(0);
|
|
241
|
-
|
|
242
|
-
// Last event should be completion
|
|
243
|
-
const lastEvent = progressEvents[progressEvents.length - 1];
|
|
244
|
-
expect(lastEvent.complete).toBe(true);
|
|
245
|
-
});
|
|
246
|
-
|
|
247
|
-
it("handles interrupt.tts by clearing playout queue and sending audio_clear", async () => {
|
|
248
|
-
const httpServer = createServer();
|
|
249
|
-
activeHttpServers.push(httpServer);
|
|
250
|
-
|
|
251
|
-
let session: VoiceAgentSession | null = null;
|
|
252
|
-
const server = await createVoiceWebSocketServer({
|
|
253
|
-
server: httpServer,
|
|
254
|
-
port: 0,
|
|
255
|
-
createSession: () => {
|
|
256
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
257
|
-
return session;
|
|
258
|
-
},
|
|
259
|
-
outboundFrameDurationMs: 20,
|
|
260
|
-
});
|
|
261
|
-
activeServers.push(server);
|
|
262
|
-
|
|
263
|
-
const port = (server.address() as any).port;
|
|
264
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
265
|
-
|
|
266
|
-
// Session is already started by the websocket server
|
|
267
|
-
|
|
268
|
-
// Send a longer TTS audio burst
|
|
269
|
-
const sampleCount = Math.floor(0.2 * 16000); // 200ms
|
|
270
|
-
const audioSamples = new Int16Array(sampleCount).fill(1000);
|
|
271
|
-
const audioBytes = pcm16SamplesToBytes(audioSamples);
|
|
272
|
-
|
|
273
|
-
session!.bus.push(Route.Main, {
|
|
274
|
-
kind: "tts.audio",
|
|
275
|
-
contextId: "test-turn",
|
|
276
|
-
timestampMs: Date.now(),
|
|
277
|
-
audio: audioBytes,
|
|
278
|
-
sampleRateHz: 16000,
|
|
279
|
-
});
|
|
280
|
-
|
|
281
|
-
// Wait a bit for some frames to be queued
|
|
282
|
-
await waitMs(10);
|
|
283
|
-
|
|
284
|
-
// Listen for audio_clear message
|
|
285
|
-
const audioClearPromise = readJsonMatching(socket, (m: any) => m.type === "audio_clear");
|
|
286
|
-
|
|
287
|
-
// Send interrupt
|
|
288
|
-
session!.bus.push(Route.Main, {
|
|
289
|
-
kind: "interrupt.tts",
|
|
290
|
-
contextId: "test-turn",
|
|
291
|
-
timestampMs: Date.now(),
|
|
292
|
-
});
|
|
293
|
-
|
|
294
|
-
const audioClear = await audioClearPromise;
|
|
295
|
-
socket.close();
|
|
296
|
-
|
|
297
|
-
expect((audioClear as any).type).toBe("audio_clear");
|
|
298
|
-
expect((audioClear as any).turnId).toBe("test-turn");
|
|
299
|
-
expect((audioClear as any).reason).toBe("barge_in");
|
|
300
|
-
});
|
|
301
|
-
|
|
302
|
-
it("respects custom outbound frame duration and queue limits", async () => {
|
|
303
|
-
const httpServer = createServer();
|
|
304
|
-
activeHttpServers.push(httpServer);
|
|
305
|
-
|
|
306
|
-
let session: VoiceAgentSession | null = null;
|
|
307
|
-
const server = await createVoiceWebSocketServer({
|
|
308
|
-
server: httpServer,
|
|
309
|
-
port: 0,
|
|
310
|
-
createSession: () => {
|
|
311
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
312
|
-
return session;
|
|
313
|
-
},
|
|
314
|
-
outboundFrameDurationMs: 40, // Larger frames
|
|
315
|
-
maxQueuedOutputAudioMs: 100, // Small queue to trigger overflow
|
|
316
|
-
});
|
|
317
|
-
activeServers.push(server);
|
|
318
|
-
|
|
319
|
-
const port = (server.address() as any).port;
|
|
320
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
321
|
-
|
|
322
|
-
// Session is already started by the websocket server
|
|
323
|
-
|
|
324
|
-
// Send large audio burst to trigger overflow
|
|
325
|
-
const sampleCount = Math.floor(1.0 * 16000); // 1 second
|
|
326
|
-
const audioSamples = new Int16Array(sampleCount).fill(1000);
|
|
327
|
-
const audioBytes = pcm16SamplesToBytes(audioSamples);
|
|
328
|
-
|
|
329
|
-
const ttsChunksPromise = collectMessagesMatching(
|
|
330
|
-
socket,
|
|
331
|
-
(m: any) => m.type === "tts_chunk",
|
|
332
|
-
200
|
|
333
|
-
);
|
|
334
|
-
|
|
335
|
-
session!.bus.push(Route.Main, {
|
|
336
|
-
kind: "tts.audio",
|
|
337
|
-
contextId: "test-turn",
|
|
338
|
-
timestampMs: Date.now(),
|
|
339
|
-
audio: audioBytes,
|
|
340
|
-
sampleRateHz: 16000,
|
|
341
|
-
});
|
|
342
|
-
|
|
343
|
-
const ttsChunks = await ttsChunksPromise;
|
|
344
|
-
socket.close();
|
|
345
|
-
|
|
346
|
-
// Tail-drop overflow: the head of the burst (up to the 100ms cap) still
|
|
347
|
-
// plays out — a tiny cap must never silence the stream entirely.
|
|
348
|
-
expect(ttsChunks.length).toBeGreaterThan(0);
|
|
349
|
-
expect(ttsChunks.length).toBeLessThan(20);
|
|
350
|
-
const firstChunk = ttsChunks[0] as any;
|
|
351
|
-
expect(firstChunk.durationMs).toBeGreaterThan(0);
|
|
352
|
-
});
|
|
353
|
-
|
|
354
|
-
it("delivers a long reply in full under the default playout bound (no overflow)", async () => {
|
|
355
|
-
const httpServer = createServer();
|
|
356
|
-
activeHttpServers.push(httpServer);
|
|
357
|
-
|
|
358
|
-
let session: VoiceAgentSession | null = null;
|
|
359
|
-
const metrics: Array<{ name: string; value: string }> = [];
|
|
360
|
-
const server = await createVoiceWebSocketServer({
|
|
361
|
-
server: httpServer,
|
|
362
|
-
port: 0,
|
|
363
|
-
createSession: () => {
|
|
364
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
365
|
-
return session;
|
|
366
|
-
},
|
|
367
|
-
});
|
|
368
|
-
activeServers.push(server);
|
|
369
|
-
|
|
370
|
-
const port = (server.address() as any).port;
|
|
371
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
372
|
-
session!.bus.on("metric.conversation", (pkt) => {
|
|
373
|
-
const metric = pkt as unknown as { name: string; value: string };
|
|
374
|
-
metrics.push(metric);
|
|
375
|
-
});
|
|
376
|
-
|
|
377
|
-
const ttsChunksPromise = collectMessagesMatching(
|
|
378
|
-
socket,
|
|
379
|
-
(m: any) => m.type === "tts_chunk",
|
|
380
|
-
1_200, // collect for the full paced second
|
|
381
|
-
);
|
|
382
|
-
// A 1s burst — far beyond the old 200ms cap that used to clear-and-stop.
|
|
383
|
-
session!.bus.push(Route.Main, {
|
|
384
|
-
kind: "tts.audio",
|
|
385
|
-
contextId: "default-bound-turn",
|
|
386
|
-
timestampMs: Date.now(),
|
|
387
|
-
audio: pcm16SamplesToBytes(new Int16Array(16000)),
|
|
388
|
-
sampleRateHz: 16000,
|
|
389
|
-
});
|
|
390
|
-
|
|
391
|
-
const ttsChunks = await ttsChunksPromise;
|
|
392
|
-
socket.close();
|
|
393
|
-
|
|
394
|
-
expect(ttsChunks.length).toBeGreaterThanOrEqual(40); // ≥800ms of the reply paced out
|
|
395
|
-
expect(metrics.some((m) => m.name.includes("overflow"))).toBe(false);
|
|
396
|
-
});
|
|
397
|
-
|
|
398
|
-
it("allows long playout queues only through an explicit override", async () => {
|
|
399
|
-
const httpServer = createServer();
|
|
400
|
-
activeHttpServers.push(httpServer);
|
|
401
|
-
|
|
402
|
-
let session: VoiceAgentSession | null = null;
|
|
403
|
-
const server = await createVoiceWebSocketServer({
|
|
404
|
-
server: httpServer,
|
|
405
|
-
port: 0,
|
|
406
|
-
createSession: () => {
|
|
407
|
-
session = new VoiceAgentSession({ plugins: {} });
|
|
408
|
-
return session;
|
|
409
|
-
},
|
|
410
|
-
maxQueuedOutputAudioMs: 1000,
|
|
411
|
-
});
|
|
412
|
-
activeServers.push(server);
|
|
413
|
-
|
|
414
|
-
const port = (server.address() as any).port;
|
|
415
|
-
const socket = await openBrowserSocketReady(browserUrl(port));
|
|
416
|
-
let closed = false;
|
|
417
|
-
socket.once("close", () => {
|
|
418
|
-
closed = true;
|
|
419
|
-
});
|
|
420
|
-
|
|
421
|
-
const ttsChunksPromise = collectMessagesMatching(
|
|
422
|
-
socket,
|
|
423
|
-
(m: any) => m.type === "tts_chunk",
|
|
424
|
-
150,
|
|
425
|
-
);
|
|
426
|
-
session!.bus.push(Route.Main, {
|
|
427
|
-
kind: "tts.audio",
|
|
428
|
-
contextId: "explicit-long-queue-turn",
|
|
429
|
-
timestampMs: Date.now(),
|
|
430
|
-
audio: pcm16SamplesToBytes(new Int16Array(6400)),
|
|
431
|
-
sampleRateHz: 16000,
|
|
432
|
-
});
|
|
433
|
-
|
|
434
|
-
const chunks = await ttsChunksPromise;
|
|
435
|
-
socket.close();
|
|
436
|
-
|
|
437
|
-
expect(chunks.length).toBeGreaterThan(0);
|
|
438
|
-
expect(closed).toBe(false);
|
|
439
|
-
});
|
|
440
|
-
});
|