msw
Version:
300 lines (267 loc) • 8.19 kB
text/typescript
import {
WebSocketClientConnection,
WebSocketServerConnection,
type WebSocketClientHandle,
type WebSocketServerHandle,
type WebSocketConnectionEventData,
type WebSocketData,
} from '@mswjs/interceptors/WebSocket'
import { devUtils } from '#core/utils/internal/dev-utils'
import { getTimestamp } from '#core/utils/logging/get-timestamp'
import { toPublicUrl } from '#core/utils/request/to-public-url'
import { getMessageLength } from './get-message-length'
import { getPublicData } from './get-public-data'
import { colors } from '#core/utils/logging/colors'
export function attachWebSocketLogger(
connection: WebSocketConnectionEventData,
): () => void {
const { client, server } = connection
const controller = new AbortController()
logConnectionOpen(client)
// Log the events sent from the WebSocket client.
// WebSocket client connection object is written from the
// server's perspective so these message events are outgoing.
/**
* @todo Provide the reference to the exact event handler
* that called this `client.send()`.
*/
client.addEventListener(
'message',
(event) => {
logOutgoingClientMessage(event)
},
{ signal: controller.signal },
)
client.addEventListener(
'close',
(event) => {
logConnectionClose(event)
},
{ signal: controller.signal },
)
// Log client errors (connection closures due to errors).
getClientSocket(client)?.addEventListener(
'error',
(event) => {
logClientError(event)
},
{ signal: controller.signal },
)
const { send: originalClientSend } = client
client.send = new Proxy(client.send, {
apply(target, thisArg, args) {
const [data] = args
const messageEvent = new MessageEvent('message', { data })
Object.defineProperties(messageEvent, {
currentTarget: {
enumerable: true,
writable: false,
value: getClientSocket(client),
},
target: {
enumerable: true,
writable: false,
value: getClientSocket(client),
},
})
queueMicrotask(() => {
logIncomingMockedClientMessage(messageEvent)
})
return Reflect.apply(target, thisArg, args)
},
})
server.addEventListener(
'open',
() => {
server.addEventListener('message', (event) => {
logIncomingServerMessage(event)
})
},
{
once: true,
signal: controller.signal,
},
)
// Log outgoing client events initiated by the event handler.
// The actual client never sent these but the handler did.
const { send: originalServerSend } = server
server.send = new Proxy(server.send, {
apply(target, thisArg, args) {
const [data] = args
const messageEvent = new MessageEvent('message', { data })
Object.defineProperties(messageEvent, {
currentTarget: {
enumerable: true,
writable: false,
value: getServerSocket(server),
},
target: {
enumerable: true,
writable: false,
value: getServerSocket(server),
},
})
logOutgoingMockedClientMessage(messageEvent)
return Reflect.apply(target, thisArg, args)
},
})
// Undo method proxies.
controller.signal.addEventListener(
'abort',
() => {
client.send = originalClientSend
server.send = originalServerSend
},
{ once: true },
)
return () => {
controller.abort()
}
}
/**
* Prints the WebSocket connection.
* This is meant to be logged by every WebSocket handler
* that intercepted this connection. This helps you see
* what handlers observe this connection.
*/
function logConnectionOpen(client: WebSocketClientHandle) {
const publicUrl = toPublicUrl(client.url)
console.groupCollapsed(
devUtils.formatMessage(`${getTimestamp()} %c▶%c ${publicUrl}`),
`color:${colors.system}`,
'color:inherit',
)
// eslint-disable-next-line no-console
console.log('Client:', getClientSocket(client) ?? client)
console.groupEnd()
}
/**
* Return the underlying `WebSocket` of the given client, if any.
* Only in-process client connections are backed by a socket;
* a handle to a connection elsewhere (e.g. another runtime) is not.
*/
function getClientSocket(client: WebSocketClientHandle): WebSocket | undefined {
if (client instanceof WebSocketClientConnection) {
return client.socket
}
return undefined
}
/**
* Return the underlying `WebSocket` of the given server, if any.
* Only in-process server connections are backed by a socket.
*/
function getServerSocket(server: WebSocketServerHandle): WebSocket | undefined {
if (server instanceof WebSocketServerConnection) {
return server.socket
}
return undefined
}
function logConnectionClose(event: CloseEvent) {
const target = event.target as WebSocket
const publicUrl = toPublicUrl(target.url)
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c■%c ${publicUrl}`,
),
`color:${colors.system}`,
'color:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}
function logClientError(event: Event) {
const socket = event.target as WebSocket
const publicUrl = toPublicUrl(socket.url)
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c\u00D7%c ${publicUrl}`,
),
`color:${colors.system}`,
'color:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}
/**
* Prints the outgoing client message.
*/
async function logOutgoingClientMessage(event: MessageEvent<WebSocketData>) {
const byteLength = getMessageLength(event.data)
const publicData = await getPublicData(event.data)
const arrow = event.defaultPrevented ? '⇡' : '⬆'
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c${arrow}%c ${publicData} %c${byteLength}%c`,
),
`color:${colors.outgoing}`,
'color:inherit',
'color:gray;font-weight:normal',
'color:inherit;font-weight:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}
/**
* Prints the outgoing client message initiated
* by `server.send()` in the event handler.
*/
async function logOutgoingMockedClientMessage(
event: MessageEvent<WebSocketData>,
) {
const byteLength = getMessageLength(event.data)
const publicData = await getPublicData(event.data)
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c⬆%c ${publicData} %c${byteLength}%c`,
),
`color:${colors.mocked}`,
'color:inherit',
'color:gray;font-weight:normal',
'color:inherit;font-weight:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}
/**
* Prints the outgoing client message initiated
* by `client.send()` in the event handler.
*/
async function logIncomingMockedClientMessage(
event: MessageEvent<WebSocketData>,
) {
const byteLength = getMessageLength(event.data)
const publicData = await getPublicData(event.data)
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c⬇%c ${publicData} %c${byteLength}%c`,
),
`color:${colors.mocked}`,
'color:inherit',
'color:gray;font-weight:normal',
'color:inherit;font-weight:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}
async function logIncomingServerMessage(event: MessageEvent<WebSocketData>) {
const byteLength = getMessageLength(event.data)
const publicData = await getPublicData(event.data)
const arrow = event.defaultPrevented ? '⇣' : '⬇'
console.groupCollapsed(
devUtils.formatMessage(
`${getTimestamp({ milliseconds: true })} %c${arrow}%c ${publicData} %c${byteLength}%c`,
),
`color:${colors.incoming}`,
'color:inherit',
'color:gray;font-weight:normal',
'color:inherit;font-weight:inherit',
)
// eslint-disable-next-line no-console
console.log(event)
console.groupEnd()
}