From 1d67f96b928335bb660b25ae3d70d7ce04ebf8bb Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 18:07:46 +0100 Subject: [PATCH 1/6] fix(WebSocket): rename "WebSocketRawData" to "WebSocketData" --- src/interceptors/WebSocket/WebSocketClassTransport.ts | 4 ++-- .../WebSocket/WebSocketClientConnection.ts | 4 ++-- src/interceptors/WebSocket/WebSocketOverride.ts | 8 ++++---- .../WebSocket/WebSocketServerConnection.ts | 4 ++-- src/interceptors/WebSocket/WebSocketTransport.ts | 10 ++++------ src/interceptors/WebSocket/index.ts | 4 ++-- 6 files changed, 16 insertions(+), 18 deletions(-) diff --git a/src/interceptors/WebSocket/WebSocketClassTransport.ts b/src/interceptors/WebSocket/WebSocketClassTransport.ts index 1797b133..6a2ff388 100644 --- a/src/interceptors/WebSocket/WebSocketClassTransport.ts +++ b/src/interceptors/WebSocket/WebSocketClassTransport.ts @@ -1,6 +1,6 @@ import { bindEvent } from './utils/bindEvent' import { - WebSocketRawData, + WebSocketData, WebSocketTransport, WebSocketTransportOnCloseCallback, WebSocketTransportOnIncomingCallback, @@ -22,7 +22,7 @@ export class WebSocketClassTransport extends WebSocketTransport { this.ws[kOnSend] = (...args) => this.onOutgoing(...args) } - public send(data: WebSocketRawData): void { + public send(data: WebSocketData): void { queueMicrotask(() => { const message = bindEvent( /** diff --git a/src/interceptors/WebSocket/WebSocketClientConnection.ts b/src/interceptors/WebSocket/WebSocketClientConnection.ts index 37487bd9..0a0c13ec 100644 --- a/src/interceptors/WebSocket/WebSocketClientConnection.ts +++ b/src/interceptors/WebSocket/WebSocketClientConnection.ts @@ -5,7 +5,7 @@ * meant to be used over any WebSocket implementation * (not all of them follow the one from WHATWG). */ -import type { WebSocketRawData, WebSocketTransport } from './WebSocketTransport' +import type { WebSocketData, WebSocketTransport } from './WebSocketTransport' import { WebSocketMessageListener } from './WebSocketOverride' import { bindEvent } from './utils/bindEvent' import { CloseEvent } from './utils/events' @@ -76,7 +76,7 @@ export class WebSocketClientConnection { /** * Send data to the connected client. */ - public send(data: WebSocketRawData): void { + public send(data: WebSocketData): void { this.transport.send(data) } diff --git a/src/interceptors/WebSocket/WebSocketOverride.ts b/src/interceptors/WebSocket/WebSocketOverride.ts index 00711dff..0cdb069b 100644 --- a/src/interceptors/WebSocket/WebSocketOverride.ts +++ b/src/interceptors/WebSocket/WebSocketOverride.ts @@ -1,5 +1,5 @@ import { invariant } from 'outvariant' -import type { WebSocketRawData } from './WebSocketTransport' +import type { WebSocketData } from './WebSocketTransport' import { bindEvent } from './utils/bindEvent' import { CloseEvent } from './utils/events' @@ -40,7 +40,7 @@ export class WebSocketOverride extends EventTarget implements WebSocket { private _onerror: WebSocketEventListener | null = null private _onclose: WebSocketCloseListener | null = null - private [kOnSend]?: (data: WebSocketRawData) => void + private [kOnSend]?: (data: WebSocketData) => void constructor(url: string | URL, protocols?: string | Array) { super() @@ -110,7 +110,7 @@ export class WebSocketOverride extends EventTarget implements WebSocket { /** * @see https://websockets.spec.whatwg.org/#ref-for-dom-websocket-send%E2%91%A0 */ - public send(data: WebSocketRawData): void { + public send(data: WebSocketData): void { if (this.readyState === this.CONNECTING) { this.close() throw new DOMException('InvalidStateError') @@ -217,7 +217,7 @@ export class WebSocketOverride extends EventTarget implements WebSocket { } } -function getDataSize(data: WebSocketRawData): number { +function getDataSize(data: WebSocketData): number { if (typeof data === 'string') { return data.length } diff --git a/src/interceptors/WebSocket/WebSocketServerConnection.ts b/src/interceptors/WebSocket/WebSocketServerConnection.ts index ecdaef72..0ef94fbb 100644 --- a/src/interceptors/WebSocket/WebSocketServerConnection.ts +++ b/src/interceptors/WebSocket/WebSocketServerConnection.ts @@ -1,6 +1,6 @@ import { invariant } from 'outvariant' import type { WebSocketOverride } from './WebSocketOverride' -import type { WebSocketRawData } from './WebSocketTransport' +import type { WebSocketData } from './WebSocketTransport' import type { WebSocketClassTransport } from './WebSocketClassTransport' import { bindEvent } from './utils/bindEvent' import { CancelableMessageEvent } from './utils/events' @@ -165,7 +165,7 @@ export class WebSocketServerConnection { * server.send(new Blob(['hello'])) * server.send(new TextEncoder().encode('hello')) */ - public send(data: WebSocketRawData): void { + public send(data: WebSocketData): void { const { realWebSocket } = this invariant( realWebSocket, diff --git a/src/interceptors/WebSocket/WebSocketTransport.ts b/src/interceptors/WebSocket/WebSocketTransport.ts index c8ad6d12..7bf50567 100644 --- a/src/interceptors/WebSocket/WebSocketTransport.ts +++ b/src/interceptors/WebSocket/WebSocketTransport.ts @@ -1,12 +1,10 @@ -export type WebSocketRawData = string | ArrayBufferLike | Blob | ArrayBufferView +export type WebSocketData = string | ArrayBufferLike | Blob | ArrayBufferView export type WebSocketTransportOnIncomingCallback = ( - event: MessageEvent + event: MessageEvent ) => void -export type WebSocketTransportOnOutgoingCallback = ( - data: WebSocketRawData -) => void +export type WebSocketTransportOnOutgoingCallback = (data: WebSocketData) => void export type WebSocketTransportOnCloseCallback = (event: CloseEvent) => void @@ -26,7 +24,7 @@ export abstract class WebSocketTransport { /** * Send the data from the server to this client. */ - abstract send(data: WebSocketRawData): void + abstract send(data: WebSocketData): void /** * Close the client connection. diff --git a/src/interceptors/WebSocket/index.ts b/src/interceptors/WebSocket/index.ts index a12decb5..38d33f5c 100644 --- a/src/interceptors/WebSocket/index.ts +++ b/src/interceptors/WebSocket/index.ts @@ -4,7 +4,7 @@ import { WebSocketServerConnection } from './WebSocketServerConnection' import { WebSocketClassTransport } from './WebSocketClassTransport' import { WebSocketOverride } from './WebSocketOverride' -export type { WebSocketRawData } from './WebSocketTransport' +export type { WebSocketData } from './WebSocketTransport' export { WebSocketClientConnection, WebSocketServerConnection } export type WebSocketEventMap = { @@ -19,7 +19,7 @@ export type WebSocketEventMap = { * The original WebSocket server connection. */ server: WebSocketServerConnection - } + }, ] } From 1dcec8223c67a7a08241ef6c5ae2fdd0c9d327f5 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 18:10:24 +0100 Subject: [PATCH 2/6] fix(WebSocket): rename "ws" to "socket" internally --- .../WebSocket/WebSocketClassTransport.ts | 18 +++++++++++------- .../WebSocket/WebSocketClientConnection.ts | 8 ++++---- .../WebSocket/WebSocketServerConnection.ts | 14 ++++++-------- src/interceptors/WebSocket/index.ts | 10 +++++----- 4 files changed, 26 insertions(+), 24 deletions(-) diff --git a/src/interceptors/WebSocket/WebSocketClassTransport.ts b/src/interceptors/WebSocket/WebSocketClassTransport.ts index 6a2ff388..2962f245 100644 --- a/src/interceptors/WebSocket/WebSocketClassTransport.ts +++ b/src/interceptors/WebSocket/WebSocketClassTransport.ts @@ -8,18 +8,22 @@ import { } from './WebSocketTransport' import { kOnSend, kClose, WebSocketOverride } from './WebSocketOverride' +/** + * Abstraction over the given mock `WebSocket` instance that allows + * for controlling that instance (e.g. sending and receiving messages). + */ export class WebSocketClassTransport extends WebSocketTransport { public onOutgoing: WebSocketTransportOnOutgoingCallback = () => {} public onIncoming: WebSocketTransportOnIncomingCallback = () => {} public onClose: WebSocketTransportOnCloseCallback = () => {} - constructor(protected readonly ws: WebSocketOverride) { + constructor(protected readonly socket: WebSocketOverride) { super() - this.ws.addEventListener('close', (event) => this.onClose(event), { + this.socket.addEventListener('close', (event) => this.onClose(event), { once: true, }) - this.ws[kOnSend] = (...args) => this.onOutgoing(...args) + this.socket[kOnSend] = (...args) => this.onOutgoing(...args) } public send(data: WebSocketData): void { @@ -33,14 +37,14 @@ export class WebSocketClassTransport extends WebSocketTransport { * mocked message events like the one below * (must be dispatched on the client instance). */ - this.ws, + this.socket, new MessageEvent('message', { data, - origin: this.ws.url, + origin: this.socket.url, }) ) - this.ws.dispatchEvent(message) + this.socket.dispatchEvent(message) }) } @@ -50,6 +54,6 @@ export class WebSocketClassTransport extends WebSocketTransport { * to allow closing the connection with the status codes * that are non-configurable by the user (> 1000 <= 1015). */ - this.ws[kClose](code, reason) + this.socket[kClose](code, reason) } } diff --git a/src/interceptors/WebSocket/WebSocketClientConnection.ts b/src/interceptors/WebSocket/WebSocketClientConnection.ts index 0a0c13ec..c0e12578 100644 --- a/src/interceptors/WebSocket/WebSocketClientConnection.ts +++ b/src/interceptors/WebSocket/WebSocketClientConnection.ts @@ -25,24 +25,24 @@ export class WebSocketClientConnection { protected [kEmitter]: EventTarget constructor( - protected readonly ws: WebSocket, + protected readonly socket: WebSocket, protected readonly transport: WebSocketTransport ) { this.id = uuidv4() - this.url = new URL(ws.url) + this.url = new URL(socket.url) this[kEmitter] = new EventTarget() // Emit outgoing client data ("ws.send()") as "message" // events on the client connection. this.transport.onOutgoing = (data) => { this[kEmitter].dispatchEvent( - bindEvent(this.ws, new MessageEvent('message', { data })) + bindEvent(this.socket, new MessageEvent('message', { data })) ) } this.transport.onClose = (event) => { this[kEmitter].dispatchEvent( - bindEvent(this.ws, new CloseEvent('close', event)) + bindEvent(this.socket, new CloseEvent('close', event)) ) } } diff --git a/src/interceptors/WebSocket/WebSocketServerConnection.ts b/src/interceptors/WebSocket/WebSocketServerConnection.ts index 0ef94fbb..a88376fa 100644 --- a/src/interceptors/WebSocket/WebSocketServerConnection.ts +++ b/src/interceptors/WebSocket/WebSocketServerConnection.ts @@ -20,7 +20,7 @@ export class WebSocketServerConnection { private [kEmitter]: EventTarget constructor( - private readonly mockWebSocket: WebSocketOverride, + private readonly socket: WebSocketOverride, private readonly createConnection: () => WebSocket, private readonly transport: WebSocketClassTransport ) { @@ -58,14 +58,14 @@ export class WebSocketServerConnection { * Preventing the default on the message event stops this. */ if (!messageEvent.defaultPrevented) { - this.mockWebSocket.dispatchEvent( + this.socket.dispatchEvent( bindEvent( /** * @note Bind the forwarded original server events * to the mock WebSocket instance so it would * dispatch them straight away. */ - this.mockWebSocket, + this.socket, // Clone the message event again to prevent // the "already being dispatched" exception. new MessageEvent('message', { @@ -105,7 +105,7 @@ export class WebSocketServerConnection { // Close the original connection when the (mock) // client closes, regardless of the reason. - this.mockWebSocket.addEventListener( + this.socket.addEventListener( 'close', (event) => { ws.close(event.code, event.reason) @@ -120,9 +120,7 @@ export class WebSocketServerConnection { // Forward server errors to the WebSocket client as-is. // We may consider exposing them to the interceptor in the future. ws.addEventListener('error', () => { - this.mockWebSocket.dispatchEvent( - bindEvent(this.mockWebSocket, new Event('error')) - ) + this.socket.dispatchEvent(bindEvent(this.socket, new Event('error'))) }) this.realWebSocket = ws @@ -170,7 +168,7 @@ export class WebSocketServerConnection { invariant( realWebSocket, 'Failed to call "server.send()" for "%s": the connection is not open. Did you forget to call "await server.connect()"?', - this.mockWebSocket.url + this.socket.url ) // Delegate the send to when the original connection is open. diff --git a/src/interceptors/WebSocket/index.ts b/src/interceptors/WebSocket/index.ts index 38d33f5c..dc28dfc2 100644 --- a/src/interceptors/WebSocket/index.ts +++ b/src/interceptors/WebSocket/index.ts @@ -56,22 +56,22 @@ export class WebSocketInterceptor extends Interceptor { // All WebSocket instances are mocked and don't forward // any events to the original server (no connection established). // To forward the events, the user must use the "server.send()" API. - const mockWs = new WebSocketOverride(url, protocols) - const transport = new WebSocketClassTransport(mockWs) + const socket = new WebSocketOverride(url, protocols) + const transport = new WebSocketClassTransport(socket) // The "globalThis.WebSocket" class stands for // the client-side connection. Assume it's established // as soon as the WebSocket instance is constructed. this.emitter.emit('connection', { - client: new WebSocketClientConnection(mockWs, transport), + client: new WebSocketClientConnection(socket, transport), server: new WebSocketServerConnection( - mockWs, + socket, createConnection, transport ), }) - return mockWs + return socket }, }) From 3f9fd7fe59564559ad2d5b4762ef5c8ec1613660 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 18:10:57 +0100 Subject: [PATCH 3/6] fix(WebSocketServerConnection): accept "createConnection" as last argument --- src/interceptors/WebSocket/WebSocketServerConnection.ts | 4 ++-- src/interceptors/WebSocket/index.ts | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/interceptors/WebSocket/WebSocketServerConnection.ts b/src/interceptors/WebSocket/WebSocketServerConnection.ts index a88376fa..355dcb00 100644 --- a/src/interceptors/WebSocket/WebSocketServerConnection.ts +++ b/src/interceptors/WebSocket/WebSocketServerConnection.ts @@ -21,8 +21,8 @@ export class WebSocketServerConnection { constructor( private readonly socket: WebSocketOverride, - private readonly createConnection: () => WebSocket, - private readonly transport: WebSocketClassTransport + private readonly transport: WebSocketClassTransport, + private readonly createConnection: () => WebSocket ) { this[kEmitter] = new EventTarget() diff --git a/src/interceptors/WebSocket/index.ts b/src/interceptors/WebSocket/index.ts index dc28dfc2..bdd01595 100644 --- a/src/interceptors/WebSocket/index.ts +++ b/src/interceptors/WebSocket/index.ts @@ -66,8 +66,8 @@ export class WebSocketInterceptor extends Interceptor { client: new WebSocketClientConnection(socket, transport), server: new WebSocketServerConnection( socket, - createConnection, - transport + transport, + createConnection ), }) From e49a287491e401942846c2e50377c7efe7bbeba4 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 18:12:20 +0100 Subject: [PATCH 4/6] feat(WebSocket): export "WebSocketTransport" class --- src/interceptors/WebSocket/index.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/interceptors/WebSocket/index.ts b/src/interceptors/WebSocket/index.ts index bdd01595..1875325c 100644 --- a/src/interceptors/WebSocket/index.ts +++ b/src/interceptors/WebSocket/index.ts @@ -4,7 +4,7 @@ import { WebSocketServerConnection } from './WebSocketServerConnection' import { WebSocketClassTransport } from './WebSocketClassTransport' import { WebSocketOverride } from './WebSocketOverride' -export type { WebSocketData } from './WebSocketTransport' +export { type WebSocketData, WebSocketTransport } from './WebSocketTransport' export { WebSocketClientConnection, WebSocketServerConnection } export type WebSocketEventMap = { From c7e108bf8ada2dff175cf1b5ae4fa8e2436fa532 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 18:14:48 +0100 Subject: [PATCH 5/6] chore(WebSocketTransport): explain callbacks --- .../WebSocket/WebSocketTransport.ts | 19 +++++++++++++------ 1 file changed, 13 insertions(+), 6 deletions(-) diff --git a/src/interceptors/WebSocket/WebSocketTransport.ts b/src/interceptors/WebSocket/WebSocketTransport.ts index 7bf50567..3a4f9218 100644 --- a/src/interceptors/WebSocket/WebSocketTransport.ts +++ b/src/interceptors/WebSocket/WebSocketTransport.ts @@ -10,15 +10,22 @@ export type WebSocketTransportOnCloseCallback = (event: CloseEvent) => void export abstract class WebSocketTransport { /** - * Listener for the incoming server events. - * This is called when the client receives the - * event from the original server connection. - * - * This way, we can trigger the "message" event - * on the mocked connection to let the user know. + * A callback for the incoming server events. + * This is called when the WebSocket client receives + * a message from the server. */ abstract onIncoming: WebSocketTransportOnIncomingCallback + + /** + * A callback for outgoing client events. + * This is called when the WebSocket client sends data. + */ abstract onOutgoing: WebSocketTransportOnOutgoingCallback + + /** + * A callback for the close client event. + * This is called when the WebSocket client is closed. + */ abstract onClose: WebSocketTransportOnCloseCallback /** From 0233b2eabe304cf281e52efee1078ef2294c0ea4 Mon Sep 17 00:00:00 2001 From: Artem Zakharchenko Date: Sun, 18 Feb 2024 19:04:10 +0100 Subject: [PATCH 6/6] fix: export "WebSocketClientConnectionProtocol" interface --- .../WebSocket/WebSocketClientConnection.ts | 17 +++++++++++++---- src/interceptors/WebSocket/index.ts | 11 +++++++++-- 2 files changed, 22 insertions(+), 6 deletions(-) diff --git a/src/interceptors/WebSocket/WebSocketClientConnection.ts b/src/interceptors/WebSocket/WebSocketClientConnection.ts index c0e12578..45b19355 100644 --- a/src/interceptors/WebSocket/WebSocketClientConnection.ts +++ b/src/interceptors/WebSocket/WebSocketClientConnection.ts @@ -13,20 +13,29 @@ import { uuidv4 } from '../../utils/uuid' const kEmitter = Symbol('kEmitter') +export interface WebSocketClientConnectionProtocol { + id: string + url: URL + send(data: WebSocketData): void + close(code?: number, reason?: string): void +} + /** * The WebSocket client instance represents an incoming * client connection. The user can control the connection, * send and receive events. */ -export class WebSocketClientConnection { +export class WebSocketClientConnection + implements WebSocketClientConnectionProtocol +{ public readonly id: string public readonly url: URL - protected [kEmitter]: EventTarget + private [kEmitter]: EventTarget constructor( - protected readonly socket: WebSocket, - protected readonly transport: WebSocketTransport + private readonly socket: WebSocket, + private readonly transport: WebSocketTransport ) { this.id = uuidv4() this.url = new URL(socket.url) diff --git a/src/interceptors/WebSocket/index.ts b/src/interceptors/WebSocket/index.ts index 1875325c..6040d0e1 100644 --- a/src/interceptors/WebSocket/index.ts +++ b/src/interceptors/WebSocket/index.ts @@ -1,11 +1,18 @@ import { Interceptor } from '../../Interceptor' -import { WebSocketClientConnection } from './WebSocketClientConnection' +import { + type WebSocketClientConnectionProtocol, + WebSocketClientConnection, +} from './WebSocketClientConnection' import { WebSocketServerConnection } from './WebSocketServerConnection' import { WebSocketClassTransport } from './WebSocketClassTransport' import { WebSocketOverride } from './WebSocketOverride' export { type WebSocketData, WebSocketTransport } from './WebSocketTransport' -export { WebSocketClientConnection, WebSocketServerConnection } +export { + WebSocketClientConnection, + WebSocketClientConnectionProtocol, + WebSocketServerConnection, +} export type WebSocketEventMap = { connection: [