livekit-client 2.15.7 → 2.15.9
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/dist/livekit-client.e2ee.worker.js +1 -1
- package/dist/livekit-client.e2ee.worker.js.map +1 -1
- package/dist/livekit-client.e2ee.worker.mjs +253 -118
- package/dist/livekit-client.e2ee.worker.mjs.map +1 -1
- package/dist/livekit-client.esm.mjs +2442 -323
- package/dist/livekit-client.esm.mjs.map +1 -1
- package/dist/livekit-client.umd.js +1 -1
- package/dist/livekit-client.umd.js.map +1 -1
- package/dist/src/api/SignalClient.d.ts +31 -2
- package/dist/src/api/SignalClient.d.ts.map +1 -1
- package/dist/src/api/WebSocketStream.d.ts +29 -0
- package/dist/src/api/WebSocketStream.d.ts.map +1 -0
- package/dist/src/api/utils.d.ts +2 -0
- package/dist/src/api/utils.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/publishVideo.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/turn.d.ts.map +1 -1
- package/dist/src/connectionHelper/checks/websocket.d.ts.map +1 -1
- package/dist/src/e2ee/E2eeManager.d.ts +16 -2
- package/dist/src/e2ee/E2eeManager.d.ts.map +1 -1
- package/dist/src/e2ee/types.d.ts +35 -1
- package/dist/src/e2ee/types.d.ts.map +1 -1
- package/dist/src/e2ee/utils.d.ts +2 -0
- package/dist/src/e2ee/utils.d.ts.map +1 -1
- package/dist/src/e2ee/worker/DataCryptor.d.ts +15 -0
- package/dist/src/e2ee/worker/DataCryptor.d.ts.map +1 -0
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts +3 -2
- package/dist/src/e2ee/worker/ParticipantKeyHandler.d.ts.map +1 -1
- package/dist/src/e2ee/worker/sifPayload.d.ts +6 -6
- package/dist/src/e2ee/worker/sifPayload.d.ts.map +1 -1
- package/dist/src/index.d.ts +5 -3
- package/dist/src/index.d.ts.map +1 -1
- package/dist/src/logger.d.ts +1 -0
- package/dist/src/logger.d.ts.map +1 -1
- package/dist/src/options.d.ts +10 -2
- package/dist/src/options.d.ts.map +1 -1
- package/dist/src/room/PCTransport.d.ts +1 -0
- package/dist/src/room/PCTransport.d.ts.map +1 -1
- package/dist/src/room/PCTransportManager.d.ts +6 -4
- package/dist/src/room/PCTransportManager.d.ts.map +1 -1
- package/dist/src/room/RTCEngine.d.ts +6 -3
- package/dist/src/room/RTCEngine.d.ts.map +1 -1
- package/dist/src/room/Room.d.ts +3 -2
- package/dist/src/room/Room.d.ts.map +1 -1
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -2
- package/dist/src/room/data-stream/incoming/IncomingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/data-stream/outgoing/OutgoingDataStreamManager.d.ts.map +1 -1
- package/dist/src/room/defaults.d.ts.map +1 -1
- package/dist/src/room/errors.d.ts +2 -1
- package/dist/src/room/errors.d.ts.map +1 -1
- package/dist/src/room/participant/LocalParticipant.d.ts.map +1 -1
- package/dist/src/room/participant/Participant.d.ts +2 -2
- package/dist/src/room/participant/Participant.d.ts.map +1 -1
- package/dist/src/room/token-source/TokenSource.d.ts +70 -0
- package/dist/src/room/token-source/TokenSource.d.ts.map +1 -0
- package/dist/src/room/token-source/types.d.ts +68 -0
- package/dist/src/room/token-source/types.d.ts.map +1 -0
- package/dist/src/room/token-source/utils.d.ts +5 -0
- package/dist/src/room/token-source/utils.d.ts.map +1 -0
- package/dist/src/room/track/LocalTrack.d.ts +1 -1
- package/dist/src/room/track/LocalTrack.d.ts.map +1 -1
- package/dist/src/room/track/options.d.ts +7 -3
- package/dist/src/room/track/options.d.ts.map +1 -1
- package/dist/src/room/track/utils.d.ts.map +1 -1
- package/dist/src/room/types.d.ts +1 -0
- package/dist/src/room/types.d.ts.map +1 -1
- package/dist/src/room/utils.d.ts +8 -1
- package/dist/src/room/utils.d.ts.map +1 -1
- package/dist/src/utils/camelToSnakeCase.d.ts +8 -0
- package/dist/src/utils/camelToSnakeCase.d.ts.map +1 -0
- package/dist/ts4.2/{src/api → api}/SignalClient.d.ts +31 -2
- package/dist/ts4.2/api/WebSocketStream.d.ts +29 -0
- package/dist/ts4.2/{src/api → api}/utils.d.ts +2 -0
- package/dist/ts4.2/{src/e2ee → e2ee}/E2eeManager.d.ts +16 -2
- package/dist/ts4.2/{src/e2ee → e2ee}/types.d.ts +35 -1
- package/dist/ts4.2/{src/e2ee → e2ee}/utils.d.ts +3 -0
- package/dist/ts4.2/e2ee/worker/DataCryptor.d.ts +15 -0
- package/dist/ts4.2/{src/e2ee → e2ee}/worker/ParticipantKeyHandler.d.ts +3 -2
- package/dist/ts4.2/{src/e2ee → e2ee}/worker/sifPayload.d.ts +6 -6
- package/dist/ts4.2/{src/index.d.ts → index.d.ts} +5 -3
- package/dist/ts4.2/{src/logger.d.ts → logger.d.ts} +1 -0
- package/dist/ts4.2/{src/options.d.ts → options.d.ts} +10 -2
- package/dist/ts4.2/{src/room → room}/PCTransport.d.ts +1 -0
- package/dist/ts4.2/{src/room → room}/PCTransportManager.d.ts +6 -4
- package/dist/ts4.2/{src/room → room}/RTCEngine.d.ts +6 -3
- package/dist/ts4.2/{src/room → room}/Room.d.ts +3 -2
- package/dist/ts4.2/{src/room → room}/data-stream/incoming/IncomingDataStreamManager.d.ts +2 -1
- package/dist/ts4.2/{src/room → room}/errors.d.ts +2 -1
- package/dist/ts4.2/{src/room → room}/participant/Participant.d.ts +2 -2
- package/dist/ts4.2/room/token-source/TokenSource.d.ts +71 -0
- package/dist/ts4.2/room/token-source/types.d.ts +68 -0
- package/dist/ts4.2/room/token-source/utils.d.ts +5 -0
- package/dist/ts4.2/{src/room → room}/track/LocalTrack.d.ts +1 -1
- package/dist/ts4.2/{src/room → room}/track/options.d.ts +10 -3
- package/dist/ts4.2/{src/room → room}/types.d.ts +1 -0
- package/dist/ts4.2/{src/room → room}/utils.d.ts +8 -1
- package/dist/ts4.2/utils/camelToSnakeCase.d.ts +8 -0
- package/package.json +11 -10
- package/src/api/SignalClient.test.ts +688 -0
- package/src/api/SignalClient.ts +308 -161
- package/src/api/WebSocketStream.test.ts +625 -0
- package/src/api/WebSocketStream.ts +118 -0
- package/src/api/utils.ts +10 -0
- package/src/connectionHelper/checks/publishVideo.ts +5 -0
- package/src/connectionHelper/checks/turn.ts +1 -0
- package/src/connectionHelper/checks/webrtc.ts +1 -1
- package/src/connectionHelper/checks/websocket.ts +1 -0
- package/src/e2ee/E2eeManager.ts +94 -2
- package/src/e2ee/types.ts +44 -1
- package/src/e2ee/utils.ts +16 -0
- package/src/e2ee/worker/DataCryptor.test.ts +271 -0
- package/src/e2ee/worker/DataCryptor.ts +147 -0
- package/src/e2ee/worker/ParticipantKeyHandler.ts +4 -3
- package/src/e2ee/worker/e2ee.worker.ts +47 -0
- package/src/e2ee/worker/sifPayload.ts +10 -6
- package/src/index.ts +16 -1
- package/src/logger.ts +1 -0
- package/src/options.ts +15 -2
- package/src/room/PCTransport.ts +7 -3
- package/src/room/PCTransportManager.ts +39 -35
- package/src/room/RTCEngine.ts +109 -22
- package/src/room/Room.ts +43 -18
- package/src/room/data-stream/incoming/IncomingDataStreamManager.ts +64 -17
- package/src/room/data-stream/outgoing/OutgoingDataStreamManager.ts +7 -0
- package/src/room/defaults.ts +1 -0
- package/src/room/errors.ts +3 -0
- package/src/room/participant/LocalParticipant.ts +8 -6
- package/src/room/participant/Participant.ts +6 -1
- package/src/room/token-source/TokenSource.ts +285 -0
- package/src/room/token-source/types.ts +84 -0
- package/src/room/token-source/utils.test.ts +63 -0
- package/src/room/token-source/utils.ts +40 -0
- package/src/room/track/LocalAudioTrack.ts +1 -1
- package/src/room/track/LocalTrack.ts +1 -1
- package/src/room/track/options.ts +12 -4
- package/src/room/track/utils.ts +10 -2
- package/src/room/types.ts +1 -0
- package/src/room/utils.ts +37 -4
- package/src/utils/camelToSnakeCase.ts +16 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/ConnectionCheck.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/Checker.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/cloudRegion.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/connectionProtocol.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishAudio.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/publishVideo.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/reconnect.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/turn.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/webrtc.d.ts +0 -0
- /package/dist/ts4.2/{src/connectionHelper → connectionHelper}/checks/websocket.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/KeyProvider.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/constants.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/errors.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/events.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/index.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/FrameCryptor.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/e2ee.worker.d.ts +0 -0
- /package/dist/ts4.2/{src/e2ee → e2ee}/worker/naluUtils.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/DefaultReconnectPolicy.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/DeviceManager.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/ReconnectPolicy.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/RegionUrlProvider.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/attribute-typings.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/incoming/StreamReader.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/OutgoingDataStreamManager.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/data-stream/outgoing/StreamWriter.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/defaults.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/events.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/LocalParticipant.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/ParticipantTrackPermission.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/RemoteParticipant.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/participant/publishUtils.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/rpc.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/stats.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/timers.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalAudioTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalTrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/LocalVideoTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteAudioTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteTrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/RemoteVideoTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/Track.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/TrackPublication.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/create.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/facingMode.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/processor/types.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/record.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/types.d.ts +0 -0
- /package/dist/ts4.2/{src/room → room}/track/utils.d.ts +0 -0
- /package/dist/ts4.2/{src/test → test}/MockMediaStreamTrack.d.ts +0 -0
- /package/dist/ts4.2/{src/test → test}/mocks.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/AsyncQueue.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/browserParser.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/cloneDeep.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/dataPacketBuffer.d.ts +0 -0
- /package/dist/ts4.2/{src/utils → utils}/ttlmap.d.ts +0 -0
- /package/dist/ts4.2/{src/version.d.ts → version.d.ts} +0 -0
|
@@ -0,0 +1,625 @@
|
|
|
1
|
+
/* eslint-disable @typescript-eslint/no-unused-vars */
|
|
2
|
+
import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest';
|
|
3
|
+
import { WebSocketStream } from './WebSocketStream';
|
|
4
|
+
|
|
5
|
+
// Mock WebSocket
|
|
6
|
+
class MockWebSocket {
|
|
7
|
+
static CONNECTING = 0;
|
|
8
|
+
|
|
9
|
+
static OPEN = 1;
|
|
10
|
+
|
|
11
|
+
static CLOSING = 2;
|
|
12
|
+
|
|
13
|
+
static CLOSED = 3;
|
|
14
|
+
|
|
15
|
+
url: string;
|
|
16
|
+
|
|
17
|
+
protocol: string;
|
|
18
|
+
|
|
19
|
+
extensions: string;
|
|
20
|
+
|
|
21
|
+
readyState: number;
|
|
22
|
+
|
|
23
|
+
binaryType: BinaryType;
|
|
24
|
+
|
|
25
|
+
onopen: ((event: Event) => void) | null = null;
|
|
26
|
+
|
|
27
|
+
onclose: ((event: CloseEvent) => void) | null = null;
|
|
28
|
+
|
|
29
|
+
onerror: ((event: Event) => void) | null = null;
|
|
30
|
+
|
|
31
|
+
onmessage: ((event: MessageEvent) => void) | null = null;
|
|
32
|
+
|
|
33
|
+
private eventListeners: Map<string, Set<EventListener>> = new Map();
|
|
34
|
+
|
|
35
|
+
constructor(url: string, protocols?: string | string[]) {
|
|
36
|
+
this.url = url;
|
|
37
|
+
this.protocol = Array.isArray(protocols) && protocols.length > 0 ? protocols[0] : '';
|
|
38
|
+
this.extensions = '';
|
|
39
|
+
this.readyState = MockWebSocket.CONNECTING;
|
|
40
|
+
this.binaryType = 'arraybuffer';
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
send(data: string | ArrayBuffer | Blob | ArrayBufferView) {
|
|
44
|
+
if (this.readyState !== MockWebSocket.OPEN) {
|
|
45
|
+
throw new DOMException('WebSocket is not open', 'InvalidStateError');
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
close(code?: number, reason?: string) {
|
|
50
|
+
if (this.readyState === MockWebSocket.CLOSING || this.readyState === MockWebSocket.CLOSED) {
|
|
51
|
+
return;
|
|
52
|
+
}
|
|
53
|
+
this.readyState = MockWebSocket.CLOSING;
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
addEventListener(type: string, listener: EventListener) {
|
|
57
|
+
if (!this.eventListeners.has(type)) {
|
|
58
|
+
this.eventListeners.set(type, new Set());
|
|
59
|
+
}
|
|
60
|
+
this.eventListeners.get(type)!.add(listener);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
removeEventListener(type: string, listener: EventListener) {
|
|
64
|
+
const listeners = this.eventListeners.get(type);
|
|
65
|
+
if (listeners) {
|
|
66
|
+
listeners.delete(listener);
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
dispatchEvent(event: Event): boolean {
|
|
71
|
+
const listeners = this.eventListeners.get(event.type);
|
|
72
|
+
if (listeners) {
|
|
73
|
+
listeners.forEach((listener) => listener(event));
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Also call on* handlers
|
|
77
|
+
if (event.type === 'open' && this.onopen) {
|
|
78
|
+
this.onopen(event);
|
|
79
|
+
} else if (event.type === 'close' && this.onclose) {
|
|
80
|
+
this.onclose(event as CloseEvent);
|
|
81
|
+
} else if (event.type === 'error' && this.onerror) {
|
|
82
|
+
this.onerror(event);
|
|
83
|
+
} else if (event.type === 'message' && this.onmessage) {
|
|
84
|
+
this.onmessage(event as MessageEvent);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
return true;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// Test helpers
|
|
91
|
+
triggerOpen() {
|
|
92
|
+
this.readyState = MockWebSocket.OPEN;
|
|
93
|
+
this.dispatchEvent(new Event('open'));
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
triggerClose(code: number = 1000, reason: string = '') {
|
|
97
|
+
this.readyState = MockWebSocket.CLOSED;
|
|
98
|
+
const closeEvent = Object.assign(new Event('close'), {
|
|
99
|
+
code,
|
|
100
|
+
reason,
|
|
101
|
+
wasClean: code === 1000,
|
|
102
|
+
}) as CloseEvent;
|
|
103
|
+
this.dispatchEvent(closeEvent);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
triggerError() {
|
|
107
|
+
const errorEvent = new Event('error');
|
|
108
|
+
this.dispatchEvent(errorEvent);
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
triggerMessage(data: any) {
|
|
112
|
+
if (this.readyState !== MockWebSocket.OPEN) {
|
|
113
|
+
throw new Error('Cannot send message when WebSocket is not open');
|
|
114
|
+
}
|
|
115
|
+
const messageEvent = new MessageEvent('message', { data });
|
|
116
|
+
this.dispatchEvent(messageEvent);
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// Mock sleep function
|
|
121
|
+
vi.mock('../room/utils', () => ({
|
|
122
|
+
sleep: vi.fn((duration: number) => new Promise((resolve) => setTimeout(resolve, duration))),
|
|
123
|
+
}));
|
|
124
|
+
|
|
125
|
+
describe('WebSocketStream', () => {
|
|
126
|
+
let mockWebSocket: MockWebSocket;
|
|
127
|
+
let originalWebSocket: typeof WebSocket;
|
|
128
|
+
|
|
129
|
+
beforeEach(() => {
|
|
130
|
+
vi.clearAllMocks();
|
|
131
|
+
|
|
132
|
+
// Store original WebSocket
|
|
133
|
+
originalWebSocket = global.WebSocket;
|
|
134
|
+
|
|
135
|
+
// Mock WebSocket globally
|
|
136
|
+
global.WebSocket = vi.fn((url: string, protocols?: string | string[]) => {
|
|
137
|
+
mockWebSocket = new MockWebSocket(url, protocols);
|
|
138
|
+
return mockWebSocket as any;
|
|
139
|
+
}) as any;
|
|
140
|
+
|
|
141
|
+
// Add constants to the mocked WebSocket
|
|
142
|
+
(global.WebSocket as any).CONNECTING = MockWebSocket.CONNECTING;
|
|
143
|
+
(global.WebSocket as any).OPEN = MockWebSocket.OPEN;
|
|
144
|
+
(global.WebSocket as any).CLOSING = MockWebSocket.CLOSING;
|
|
145
|
+
(global.WebSocket as any).CLOSED = MockWebSocket.CLOSED;
|
|
146
|
+
});
|
|
147
|
+
|
|
148
|
+
afterEach(() => {
|
|
149
|
+
// Restore original WebSocket
|
|
150
|
+
global.WebSocket = originalWebSocket;
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
describe('Constructor and Initialization', () => {
|
|
154
|
+
it('should create WebSocketStream with URL and protocols', () => {
|
|
155
|
+
const wsStream = new WebSocketStream('wss://test.example.com', {
|
|
156
|
+
protocols: ['protocol1', 'protocol2'],
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
expect(wsStream.url).toBe('wss://test.example.com');
|
|
160
|
+
expect(mockWebSocket.url).toBe('wss://test.example.com');
|
|
161
|
+
expect(mockWebSocket.binaryType).toBe('arraybuffer');
|
|
162
|
+
expect(mockWebSocket.protocol).toBe('protocol1');
|
|
163
|
+
expect(wsStream.readyState).toBe(MockWebSocket.CONNECTING);
|
|
164
|
+
|
|
165
|
+
mockWebSocket.triggerOpen();
|
|
166
|
+
expect(wsStream.readyState).toBe(MockWebSocket.OPEN);
|
|
167
|
+
});
|
|
168
|
+
|
|
169
|
+
it('should throw when signal is already aborted', () => {
|
|
170
|
+
const abortController = new AbortController();
|
|
171
|
+
abortController.abort();
|
|
172
|
+
|
|
173
|
+
expect(() => {
|
|
174
|
+
new WebSocketStream('wss://test.example.com', {
|
|
175
|
+
signal: abortController.signal,
|
|
176
|
+
});
|
|
177
|
+
}).toThrow('This operation was aborted');
|
|
178
|
+
});
|
|
179
|
+
|
|
180
|
+
it('should close when abort signal is triggered', () => {
|
|
181
|
+
const abortController = new AbortController();
|
|
182
|
+
const wsStream = new WebSocketStream('wss://test.example.com', {
|
|
183
|
+
signal: abortController.signal,
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
187
|
+
abortController.abort();
|
|
188
|
+
|
|
189
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('opened Promise', () => {
|
|
194
|
+
it('should resolve with readable/writable streams and remove error listener', async () => {
|
|
195
|
+
const wsStream = new WebSocketStream('wss://test.example.com', {
|
|
196
|
+
protocols: ['test-protocol'],
|
|
197
|
+
});
|
|
198
|
+
|
|
199
|
+
mockWebSocket.protocol = 'test-protocol';
|
|
200
|
+
mockWebSocket.extensions = 'test-extension';
|
|
201
|
+
const removeEventListenerSpy = vi.spyOn(mockWebSocket, 'removeEventListener');
|
|
202
|
+
|
|
203
|
+
mockWebSocket.triggerOpen();
|
|
204
|
+
const connection = await wsStream.opened;
|
|
205
|
+
|
|
206
|
+
expect(connection.readable).toBeInstanceOf(ReadableStream);
|
|
207
|
+
expect(connection.writable).toBeInstanceOf(WritableStream);
|
|
208
|
+
expect(connection.protocol).toBe('test-protocol');
|
|
209
|
+
expect(connection.extensions).toBe('test-extension');
|
|
210
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('error', expect.any(Function));
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
it('should reject when WebSocket errors before opening', async () => {
|
|
214
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
215
|
+
|
|
216
|
+
mockWebSocket.triggerError();
|
|
217
|
+
|
|
218
|
+
await expect(wsStream.opened).rejects.toThrow();
|
|
219
|
+
});
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
describe('closed Promise', () => {
|
|
223
|
+
it('should resolve with close code and reason, removing error listener', async () => {
|
|
224
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
225
|
+
const removeEventListenerSpy = vi.spyOn(mockWebSocket, 'removeEventListener');
|
|
226
|
+
|
|
227
|
+
mockWebSocket.triggerOpen();
|
|
228
|
+
mockWebSocket.triggerClose(1001, 'Going away');
|
|
229
|
+
|
|
230
|
+
const closeInfo = await wsStream.closed;
|
|
231
|
+
|
|
232
|
+
expect(closeInfo.closeCode).toBe(1001);
|
|
233
|
+
expect(closeInfo.reason).toBe('Going away');
|
|
234
|
+
expect(removeEventListenerSpy).toHaveBeenCalledWith('error', expect.any(Function));
|
|
235
|
+
});
|
|
236
|
+
|
|
237
|
+
it('should handle error followed by close event', async () => {
|
|
238
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
239
|
+
|
|
240
|
+
mockWebSocket.triggerOpen();
|
|
241
|
+
mockWebSocket.triggerError();
|
|
242
|
+
mockWebSocket.triggerClose(1006, 'Connection failed');
|
|
243
|
+
|
|
244
|
+
const closeInfo = await wsStream.closed;
|
|
245
|
+
|
|
246
|
+
expect(closeInfo.closeCode).toBe(1006);
|
|
247
|
+
expect(closeInfo.reason).toBe('Connection failed');
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('should reject when error occurs without timely close event', async () => {
|
|
251
|
+
const { sleep } = await import('../room/utils');
|
|
252
|
+
vi.mocked(sleep).mockResolvedValue(undefined);
|
|
253
|
+
|
|
254
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
255
|
+
|
|
256
|
+
mockWebSocket.triggerOpen();
|
|
257
|
+
mockWebSocket.triggerError();
|
|
258
|
+
|
|
259
|
+
await expect(wsStream.closed).rejects.toThrow(
|
|
260
|
+
'Encountered unspecified websocket error without a timely close event',
|
|
261
|
+
);
|
|
262
|
+
});
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
describe('ReadableStream behavior', () => {
|
|
266
|
+
it('should enqueue and read multiple messages (ArrayBuffer and string)', async () => {
|
|
267
|
+
const wsStream = new WebSocketStream<ArrayBuffer | string>('wss://test.example.com');
|
|
268
|
+
|
|
269
|
+
mockWebSocket.triggerOpen();
|
|
270
|
+
const connection = await wsStream.opened;
|
|
271
|
+
|
|
272
|
+
const reader = connection.readable.getReader();
|
|
273
|
+
|
|
274
|
+
const message1 = new ArrayBuffer(8);
|
|
275
|
+
const message2 = 'Hello, World!';
|
|
276
|
+
|
|
277
|
+
mockWebSocket.triggerMessage(message1);
|
|
278
|
+
mockWebSocket.triggerMessage(message2);
|
|
279
|
+
|
|
280
|
+
const result1 = await reader.read();
|
|
281
|
+
expect(result1.done).toBe(false);
|
|
282
|
+
expect(result1.value).toBe(message1);
|
|
283
|
+
|
|
284
|
+
const result2 = await reader.read();
|
|
285
|
+
expect(result2.done).toBe(false);
|
|
286
|
+
expect(result2.value).toBe(message2);
|
|
287
|
+
|
|
288
|
+
reader.releaseLock();
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('should error readable stream when WebSocket errors', async () => {
|
|
292
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
293
|
+
|
|
294
|
+
mockWebSocket.triggerOpen();
|
|
295
|
+
const connection = await wsStream.opened;
|
|
296
|
+
|
|
297
|
+
const reader = connection.readable.getReader();
|
|
298
|
+
|
|
299
|
+
mockWebSocket.triggerError();
|
|
300
|
+
|
|
301
|
+
await Promise.all([
|
|
302
|
+
expect(reader.read()).rejects.toBeDefined(),
|
|
303
|
+
expect(wsStream.closed).rejects.toBeDefined(),
|
|
304
|
+
]);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it('should close WebSocket with custom close info when cancelled', async () => {
|
|
308
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
309
|
+
|
|
310
|
+
mockWebSocket.triggerOpen();
|
|
311
|
+
const connection = await wsStream.opened;
|
|
312
|
+
|
|
313
|
+
const reader = connection.readable.getReader();
|
|
314
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
315
|
+
|
|
316
|
+
await reader.cancel({ closeCode: 1001, reason: 'Client is leaving' });
|
|
317
|
+
|
|
318
|
+
expect(closeSpy).toHaveBeenCalledWith(1001, 'Client is leaving');
|
|
319
|
+
});
|
|
320
|
+
|
|
321
|
+
it('should throw when getting a second reader (locked stream)', async () => {
|
|
322
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
323
|
+
|
|
324
|
+
mockWebSocket.triggerOpen();
|
|
325
|
+
const connection = await wsStream.opened;
|
|
326
|
+
|
|
327
|
+
const reader1 = connection.readable.getReader();
|
|
328
|
+
|
|
329
|
+
expect(() => connection.readable.getReader()).toThrow();
|
|
330
|
+
|
|
331
|
+
reader1.releaseLock();
|
|
332
|
+
});
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
describe('WritableStream behavior', () => {
|
|
336
|
+
it('should send multiple chunks through WebSocket', async () => {
|
|
337
|
+
const wsStream = new WebSocketStream<ArrayBuffer | string>('wss://test.example.com');
|
|
338
|
+
|
|
339
|
+
mockWebSocket.triggerOpen();
|
|
340
|
+
const connection = await wsStream.opened;
|
|
341
|
+
|
|
342
|
+
const writer = connection.writable.getWriter();
|
|
343
|
+
const sendSpy = vi.spyOn(mockWebSocket, 'send');
|
|
344
|
+
|
|
345
|
+
const chunk1 = new ArrayBuffer(8);
|
|
346
|
+
const chunk2 = 'Hello, WebSocket!';
|
|
347
|
+
const chunk3 = new ArrayBuffer(16);
|
|
348
|
+
|
|
349
|
+
await writer.write(chunk1);
|
|
350
|
+
await writer.write(chunk2);
|
|
351
|
+
await writer.write(chunk3);
|
|
352
|
+
|
|
353
|
+
expect(sendSpy).toHaveBeenCalledTimes(3);
|
|
354
|
+
expect(sendSpy).toHaveBeenNthCalledWith(1, chunk1);
|
|
355
|
+
expect(sendSpy).toHaveBeenNthCalledWith(2, chunk2);
|
|
356
|
+
expect(sendSpy).toHaveBeenNthCalledWith(3, chunk3);
|
|
357
|
+
|
|
358
|
+
await writer.close();
|
|
359
|
+
});
|
|
360
|
+
|
|
361
|
+
it('should close WebSocket when writable stream is closed or aborted', async () => {
|
|
362
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
363
|
+
|
|
364
|
+
mockWebSocket.triggerOpen();
|
|
365
|
+
const connection = await wsStream.opened;
|
|
366
|
+
|
|
367
|
+
const writer = connection.writable.getWriter();
|
|
368
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
369
|
+
|
|
370
|
+
await writer.abort();
|
|
371
|
+
|
|
372
|
+
expect(closeSpy).toHaveBeenCalledWith();
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
it('should throw error when writing to closed WebSocket', async () => {
|
|
376
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
377
|
+
|
|
378
|
+
mockWebSocket.triggerOpen();
|
|
379
|
+
const connection = await wsStream.opened;
|
|
380
|
+
|
|
381
|
+
const writer = connection.writable.getWriter();
|
|
382
|
+
|
|
383
|
+
mockWebSocket.readyState = MockWebSocket.CLOSED;
|
|
384
|
+
|
|
385
|
+
await expect(writer.write(new ArrayBuffer(8))).rejects.toThrow('WebSocket is not open');
|
|
386
|
+
});
|
|
387
|
+
});
|
|
388
|
+
|
|
389
|
+
describe('close() method', () => {
|
|
390
|
+
it('should close WebSocket with custom close code and reason', () => {
|
|
391
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
392
|
+
|
|
393
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
394
|
+
|
|
395
|
+
wsStream.close({ closeCode: 1001, reason: 'Going away' });
|
|
396
|
+
|
|
397
|
+
expect(closeSpy).toHaveBeenCalledWith(1001, 'Going away');
|
|
398
|
+
});
|
|
399
|
+
|
|
400
|
+
it('should handle close with partial or no arguments', () => {
|
|
401
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
402
|
+
|
|
403
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
404
|
+
|
|
405
|
+
wsStream.close({ closeCode: 1000 });
|
|
406
|
+
expect(closeSpy).toHaveBeenCalledWith(1000, undefined);
|
|
407
|
+
|
|
408
|
+
wsStream.close();
|
|
409
|
+
expect(closeSpy).toHaveBeenCalledWith(undefined, undefined);
|
|
410
|
+
});
|
|
411
|
+
});
|
|
412
|
+
|
|
413
|
+
describe('AbortSignal integration', () => {
|
|
414
|
+
it('should close WebSocket when AbortSignal is triggered at any stage', async () => {
|
|
415
|
+
const abortController = new AbortController();
|
|
416
|
+
const wsStream = new WebSocketStream('wss://test.example.com', {
|
|
417
|
+
signal: abortController.signal,
|
|
418
|
+
});
|
|
419
|
+
|
|
420
|
+
mockWebSocket.triggerOpen();
|
|
421
|
+
await wsStream.opened;
|
|
422
|
+
|
|
423
|
+
const closeSpy = vi.spyOn(mockWebSocket, 'close');
|
|
424
|
+
|
|
425
|
+
abortController.abort();
|
|
426
|
+
|
|
427
|
+
expect(closeSpy).toHaveBeenCalled();
|
|
428
|
+
});
|
|
429
|
+
});
|
|
430
|
+
|
|
431
|
+
describe('Edge cases', () => {
|
|
432
|
+
it('should handle reading from stream after WebSocket closes', async () => {
|
|
433
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
434
|
+
|
|
435
|
+
mockWebSocket.triggerOpen();
|
|
436
|
+
const connection = await wsStream.opened;
|
|
437
|
+
|
|
438
|
+
const reader = connection.readable.getReader();
|
|
439
|
+
|
|
440
|
+
// Send a message then close
|
|
441
|
+
mockWebSocket.triggerMessage(new ArrayBuffer(8));
|
|
442
|
+
mockWebSocket.triggerClose(1000);
|
|
443
|
+
|
|
444
|
+
// Should still be able to read the buffered message
|
|
445
|
+
const result = await reader.read();
|
|
446
|
+
expect(result.done).toBe(false);
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
it('should have proper readyState transitions', () => {
|
|
450
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
451
|
+
|
|
452
|
+
expect(wsStream.readyState).toBe(MockWebSocket.CONNECTING);
|
|
453
|
+
|
|
454
|
+
mockWebSocket.triggerOpen();
|
|
455
|
+
expect(wsStream.readyState).toBe(MockWebSocket.OPEN);
|
|
456
|
+
|
|
457
|
+
mockWebSocket.readyState = MockWebSocket.CLOSING;
|
|
458
|
+
expect(wsStream.readyState).toBe(MockWebSocket.CLOSING);
|
|
459
|
+
|
|
460
|
+
mockWebSocket.triggerClose(1000);
|
|
461
|
+
expect(wsStream.readyState).toBe(MockWebSocket.CLOSED);
|
|
462
|
+
});
|
|
463
|
+
});
|
|
464
|
+
|
|
465
|
+
describe('Stream integration', () => {
|
|
466
|
+
it('should support simultaneous reading and writing', async () => {
|
|
467
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
468
|
+
|
|
469
|
+
mockWebSocket.triggerOpen();
|
|
470
|
+
const connection = await wsStream.opened;
|
|
471
|
+
|
|
472
|
+
const reader = connection.readable.getReader();
|
|
473
|
+
const writer = connection.writable.getWriter();
|
|
474
|
+
|
|
475
|
+
// Write data
|
|
476
|
+
const outgoingData = new ArrayBuffer(8);
|
|
477
|
+
const writePromise = writer.write(outgoingData);
|
|
478
|
+
|
|
479
|
+
// Receive data
|
|
480
|
+
const incomingData = new ArrayBuffer(16);
|
|
481
|
+
mockWebSocket.triggerMessage(incomingData);
|
|
482
|
+
|
|
483
|
+
await writePromise;
|
|
484
|
+
const readResult = await reader.read();
|
|
485
|
+
|
|
486
|
+
expect(readResult.value).toBe(incomingData);
|
|
487
|
+
|
|
488
|
+
reader.releaseLock();
|
|
489
|
+
await writer.close();
|
|
490
|
+
});
|
|
491
|
+
|
|
492
|
+
it('should handle piping streams to WebSocket writable', async () => {
|
|
493
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
494
|
+
|
|
495
|
+
mockWebSocket.triggerOpen();
|
|
496
|
+
const connection = await wsStream.opened;
|
|
497
|
+
|
|
498
|
+
const sourceData = [new ArrayBuffer(8), new ArrayBuffer(16), new ArrayBuffer(32)];
|
|
499
|
+
let dataIndex = 0;
|
|
500
|
+
|
|
501
|
+
const sourceStream = new ReadableStream({
|
|
502
|
+
pull(controller) {
|
|
503
|
+
if (dataIndex < sourceData.length) {
|
|
504
|
+
controller.enqueue(sourceData[dataIndex++]);
|
|
505
|
+
} else {
|
|
506
|
+
controller.close();
|
|
507
|
+
}
|
|
508
|
+
},
|
|
509
|
+
});
|
|
510
|
+
|
|
511
|
+
const sendSpy = vi.spyOn(mockWebSocket, 'send');
|
|
512
|
+
|
|
513
|
+
await sourceStream.pipeTo(connection.writable);
|
|
514
|
+
|
|
515
|
+
expect(sendSpy).toHaveBeenCalledTimes(3);
|
|
516
|
+
expect(sendSpy).toHaveBeenNthCalledWith(1, sourceData[0]);
|
|
517
|
+
expect(sendSpy).toHaveBeenNthCalledWith(2, sourceData[1]);
|
|
518
|
+
expect(sendSpy).toHaveBeenNthCalledWith(3, sourceData[2]);
|
|
519
|
+
});
|
|
520
|
+
});
|
|
521
|
+
|
|
522
|
+
describe('Complex scenarios', () => {
|
|
523
|
+
it('should handle multiple messages queued before first read', async () => {
|
|
524
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
525
|
+
|
|
526
|
+
mockWebSocket.triggerOpen();
|
|
527
|
+
const connection = await wsStream.opened;
|
|
528
|
+
|
|
529
|
+
const msg1 = new ArrayBuffer(8);
|
|
530
|
+
const msg2 = new ArrayBuffer(16);
|
|
531
|
+
const msg3 = new ArrayBuffer(32);
|
|
532
|
+
|
|
533
|
+
mockWebSocket.triggerMessage(msg1);
|
|
534
|
+
mockWebSocket.triggerMessage(msg2);
|
|
535
|
+
mockWebSocket.triggerMessage(msg3);
|
|
536
|
+
|
|
537
|
+
const reader = connection.readable.getReader();
|
|
538
|
+
|
|
539
|
+
const result1 = await reader.read();
|
|
540
|
+
expect(result1.value).toBe(msg1);
|
|
541
|
+
|
|
542
|
+
const result2 = await reader.read();
|
|
543
|
+
expect(result2.value).toBe(msg2);
|
|
544
|
+
|
|
545
|
+
const result3 = await reader.read();
|
|
546
|
+
expect(result3.value).toBe(msg3);
|
|
547
|
+
|
|
548
|
+
reader.releaseLock();
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
it('should handle error during active read operation', async () => {
|
|
552
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
553
|
+
|
|
554
|
+
mockWebSocket.triggerOpen();
|
|
555
|
+
const connection = await wsStream.opened;
|
|
556
|
+
|
|
557
|
+
const reader = connection.readable.getReader();
|
|
558
|
+
|
|
559
|
+
// Start a read that will be interrupted by an error
|
|
560
|
+
const readPromise = reader.read();
|
|
561
|
+
|
|
562
|
+
// Trigger error while read is pending
|
|
563
|
+
mockWebSocket.triggerError();
|
|
564
|
+
|
|
565
|
+
await Promise.all([
|
|
566
|
+
expect(readPromise).rejects.toBeDefined(),
|
|
567
|
+
expect(wsStream.closed).rejects.toBeDefined(),
|
|
568
|
+
]);
|
|
569
|
+
});
|
|
570
|
+
|
|
571
|
+
it('should support zero-length and empty messages', async () => {
|
|
572
|
+
const wsStream = new WebSocketStream<ArrayBuffer | string>('wss://test.example.com');
|
|
573
|
+
|
|
574
|
+
mockWebSocket.triggerOpen();
|
|
575
|
+
const connection = await wsStream.opened;
|
|
576
|
+
|
|
577
|
+
const reader = connection.readable.getReader();
|
|
578
|
+
const writer = connection.writable.getWriter();
|
|
579
|
+
|
|
580
|
+
const emptyBuffer = new ArrayBuffer(0);
|
|
581
|
+
await writer.write(emptyBuffer);
|
|
582
|
+
await writer.write('');
|
|
583
|
+
|
|
584
|
+
mockWebSocket.triggerMessage(emptyBuffer);
|
|
585
|
+
mockWebSocket.triggerMessage('');
|
|
586
|
+
|
|
587
|
+
const result1 = await reader.read();
|
|
588
|
+
expect(result1.value).toBe(emptyBuffer);
|
|
589
|
+
expect((result1.value as ArrayBuffer).byteLength).toBe(0);
|
|
590
|
+
|
|
591
|
+
const result2 = await reader.read();
|
|
592
|
+
expect(result2.value).toBe('');
|
|
593
|
+
|
|
594
|
+
reader.releaseLock();
|
|
595
|
+
await writer.close();
|
|
596
|
+
});
|
|
597
|
+
|
|
598
|
+
it('should preserve message order under load', async () => {
|
|
599
|
+
const wsStream = new WebSocketStream('wss://test.example.com');
|
|
600
|
+
|
|
601
|
+
mockWebSocket.triggerOpen();
|
|
602
|
+
const connection = await wsStream.opened;
|
|
603
|
+
|
|
604
|
+
const reader = connection.readable.getReader();
|
|
605
|
+
|
|
606
|
+
// Send 100 messages rapidly
|
|
607
|
+
const messageCount = 100;
|
|
608
|
+
for (let i = 0; i < messageCount; i++) {
|
|
609
|
+
const buffer = new ArrayBuffer(4);
|
|
610
|
+
const view = new Uint32Array(buffer);
|
|
611
|
+
view[0] = i;
|
|
612
|
+
mockWebSocket.triggerMessage(buffer);
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// Read all messages and verify order
|
|
616
|
+
for (let i = 0; i < messageCount; i++) {
|
|
617
|
+
const result = await reader.read();
|
|
618
|
+
const view = new Uint32Array(result.value as ArrayBuffer);
|
|
619
|
+
expect(view[0]).toBe(i);
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
reader.releaseLock();
|
|
623
|
+
});
|
|
624
|
+
});
|
|
625
|
+
});
|