Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
16 changes: 10 additions & 6 deletions src/core/experimental/frames/websocket-frame.ts
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
import { TypedEvent } from 'rettime'
import { type WebSocketConnectionData } from '@mswjs/interceptors/WebSocket'
import {
kConnect,
kAutoConnect,
type WebSocketHandler,
type WebSocketConnectionFoo,
} from '../../handlers/WebSocketHandler'
import {
NetworkFrame,
Expand All @@ -16,8 +16,10 @@ import {
import { devUtils } from '../../utils/internal/devUtils'
import { HandlersController, AnyHandler } from '../handlers-controller'

export interface WebSocketNetworkFrameOptions {
connection: WebSocketConnectionData
export interface WebSocketNetworkFrameOptions<
Copy link
Copy Markdown
Contributor

@christoph-fricke christoph-fricke Apr 13, 2026

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

As far as I understand, the ConnectionProtocols interfaces are the intended abstraction for decoupling MSW internals from WebSocket specifics, right? They are supposed to enable custom frames that extend can extend WebSocketNetworkFrame.
The previous @msw/playwright implementation already proofed that it is possible to implement ConnectionProtocols with something that is not a WebSocketInterceptor. For other alternative sources, I am toying with the idea of a HttpServerSource and WebSocketServerSource, i.e. sources that create an actual server. I have some use cases for that... I don't see blockers creating ConnectionProtocols there either.

The biggest change I would make: I think ConnectionProtocols as the abstraction target is better "highlighted" and "communicated" when the WebSocketNetworkFrame does not accept an generic connection. It feels like overhead and is just one more think to understand. To avoid more confusion, it might help if these interfaces do not need imports from @mswjs/interceptors, but that might be unfeasible. In the end, most people will likely just use existing sources and not implement their own sources and frames... Adjusted frame options:

interface WebSocketNetworkFrameOptions {
  connection: WebSocketConnectionFoo // <-- Type must be package exported
}

// I actually prefer it flattened (avoids having to find a name for "Foo"):
interface WebSocketNetworkFrameOptions {
  client: WebSocketClientConnectionProtocol
  server: WebSocketServerConnectionProtocol
  info: WebSocketConnectionData['info']
}

I quickly verify that removing the generic works. It only requires a small adjustment to the InterceptorWebSocketNetworkFrame:

class InterceptorWebSocketNetworkFrame extends WebSocketNetworkFrame {
  #client: WebSocketConnectionData['client']

  constructor(args: { connection: WebSocketConnectionData }) {
    super({ connection: args.connection })
    this.#client = args.connection.client
  }

  public errorWith(reason?: unknown): void {
     // ... collapsed code
     this.#client.socket.dispatchEvent(errorEvent)
  }

  public passthrough() {
    this.data.connection.server.connect()
  }
}

Connection extends WebSocketConnectionFoo,
> {
connection: Connection
}

export type WebSocketNetworkFrameEventMap = {
Expand Down Expand Up @@ -68,14 +70,16 @@ class UnhandledWebSocketExceptionEvent<
}
}

export abstract class WebSocketNetworkFrame extends NetworkFrame<
export abstract class WebSocketNetworkFrame<
Connection extends WebSocketConnectionFoo,
> extends NetworkFrame<
'ws',
{
connection: WebSocketConnectionData
connection: Connection
},
WebSocketNetworkFrameEventMap
> {
constructor(options: WebSocketNetworkFrameOptions) {
constructor(options: WebSocketNetworkFrameOptions<Connection>) {
super('ws', {
connection: options.connection,
})
Expand Down
2 changes: 1 addition & 1 deletion src/core/experimental/sources/interceptor-source.ts
Original file line number Diff line number Diff line change
Expand Up @@ -154,7 +154,7 @@ class InterceptorHttpNetworkFrame extends HttpNetworkFrame {
}
}

class InterceptorWebSocketNetworkFrame extends WebSocketNetworkFrame {
class InterceptorWebSocketNetworkFrame extends WebSocketNetworkFrame<WebSocketConnectionData> {
constructor(args: { connection: WebSocketConnectionData }) {
super({ connection: args.connection })
}
Expand Down
11 changes: 8 additions & 3 deletions src/core/handlers/WebSocketHandler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -22,10 +22,13 @@ export type WebSocketHandlerEventMap = {
connection: [args: WebSocketHandlerConnection]
}

export interface WebSocketHandlerConnection {
export interface WebSocketConnectionFoo {
client: WebSocketClientConnectionProtocol
server: WebSocketServerConnectionProtocol
info: WebSocketConnectionData['info']
}

export interface WebSocketHandlerConnection extends WebSocketConnectionFoo {
params: PathParams
}

Expand Down Expand Up @@ -97,7 +100,7 @@ export class WebSocketHandler {
}

public async run(
connection: WebSocketConnectionData,
connection: WebSocketConnectionFoo,
resolutionContext?: WebSocketResolutionContext,
): Promise<WebSocketHandlerConnection | null> {
const parsedResult = this.parse({
Expand Down Expand Up @@ -160,7 +163,9 @@ export class WebSocketHandler {
return this[kEmitter].emit('connection', connection)
}

public log(connection: WebSocketConnectionData): () => void {
public log(
connection: WebSocketConnectionData | WebSocketConnectionFoo,
): () => void {
return attachWebSocketLogger(connection)
}

Expand Down
88 changes: 51 additions & 37 deletions src/core/ws/utils/attachWebSocketLogger.ts
Original file line number Diff line number Diff line change
@@ -1,13 +1,15 @@
import type {
WebSocketClientConnection,
WebSocketConnectionData,
WebSocketData,
WebSocketConnectionData,
WebSocketClientConnection,
WebSocketClientConnectionProtocol,
} from '@mswjs/interceptors/WebSocket'
import { devUtils } from '../../utils/internal/devUtils'
import { getTimestamp } from '../../utils/logging/getTimestamp'
import { toPublicUrl } from '../../utils/request/toPublicUrl'
import { getMessageLength } from './getMessageLength'
import { getPublicData } from './getPublicData'
import type { WebSocketConnectionFoo } from '../../handlers/WebSocketHandler'

export const colors = {
system: '#3b82f6',
Expand All @@ -17,7 +19,7 @@ export const colors = {
}

export function attachWebSocketLogger(
connection: WebSocketConnectionData,
connection: WebSocketConnectionData | WebSocketConnectionFoo,
): () => void {
const { client, server } = connection
const controller = new AbortController()
Expand Down Expand Up @@ -48,31 +50,36 @@ export function attachWebSocketLogger(
)

// Log client errors (connection closures due to errors).
client.socket.addEventListener(
'error',
(event) => {
logClientError(event)
},
{ signal: controller.signal },
)
if ('socket' in client) {
client.socket.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: client.socket,
},
target: {
enumerable: true,
writable: false,
value: client.socket,
},
})

if ('socket' in client) {
Object.defineProperties(messageEvent, {
currentTarget: {
enumerable: true,
writable: false,
value: client.socket,
},
target: {
enumerable: true,
writable: false,
value: client.socket,
},
})
}

queueMicrotask(() => {
logIncomingMockedClientMessage(messageEvent)
Expand Down Expand Up @@ -102,18 +109,21 @@ export function attachWebSocketLogger(
apply(target, thisArg, args) {
const [data] = args
const messageEvent = new MessageEvent('message', { data })
Object.defineProperties(messageEvent, {
currentTarget: {
enumerable: true,
writable: false,
value: server.socket,
},
target: {
enumerable: true,
writable: false,
value: server.socket,
},
})

if ('socket' in server) {
Object.defineProperties(messageEvent, {
currentTarget: {
enumerable: true,
writable: false,
value: server.socket,
},
target: {
enumerable: true,
writable: false,
value: server.socket,
},
})
}

logOutgoingMockedClientMessage(messageEvent)

Expand Down Expand Up @@ -142,16 +152,20 @@ export function attachWebSocketLogger(
* that intercepted this connection. This helps you see
* what handlers observe this connection.
*/
export function logConnectionOpen(client: WebSocketClientConnection) {
export function logConnectionOpen(
client: WebSocketClientConnectionProtocol | WebSocketClientConnection,
) {
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:', client.socket)
if ('socket' in client) {
// eslint-disable-next-line no-console
console.log('Client:', client.socket)
}
console.groupEnd()
}

Expand Down
Loading