Skip to content

Commit 40a0a8d

Browse files
committed
chore: add agnostic "Handler" class
1 parent d5a9b4c commit 40a0a8d

5 files changed

Lines changed: 191 additions & 120 deletions

File tree

Lines changed: 138 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,138 @@
1+
import { Emitter } from 'strict-event-emitter'
2+
import type {
3+
WebSocketClientConnection,
4+
WebSocketServerConnection,
5+
} from '@mswjs/interceptors/WebSocket'
6+
import {
7+
type Match,
8+
type Path,
9+
type PathParams,
10+
matchRequestUrl,
11+
} from '../utils/matching/matchRequestUrl'
12+
13+
export type HandlerOptions = {
14+
once?: boolean
15+
}
16+
17+
export abstract class Handler<Input = unknown> {
18+
public isUsed: boolean
19+
20+
constructor(protected readonly options: HandlerOptions = {}) {
21+
this.isUsed = false
22+
}
23+
24+
abstract parse(args: { input: Input }): unknown
25+
abstract predicate(args: { input: Input; parsedResult: unknown }): boolean
26+
protected abstract handle(args: {
27+
input: Input
28+
parsedResult: unknown
29+
}): Promise<unknown | null>
30+
31+
public async run(input: Input): Promise<unknown | null> {
32+
if (this.options?.once && this.isUsed) {
33+
return null
34+
}
35+
36+
const parsedResult = this.parse({ input })
37+
const shouldHandle = this.predicate({
38+
input,
39+
parsedResult,
40+
})
41+
42+
if (!shouldHandle) {
43+
return null
44+
}
45+
46+
const result = await this.handle({
47+
input,
48+
parsedResult,
49+
})
50+
51+
this.isUsed = true
52+
53+
return result
54+
}
55+
}
56+
57+
type WebSocketHandlerParsedResult = {
58+
match: Match
59+
}
60+
61+
type WebSocketHandlerEventMap = {
62+
connection: [
63+
args: {
64+
client: WebSocketClientConnection
65+
server: WebSocketServerConnection
66+
params: PathParams
67+
},
68+
]
69+
}
70+
71+
export class WebSocketHandler extends Handler<MessageEvent<any>> {
72+
public on: <K extends keyof WebSocketHandlerEventMap>(
73+
event: K,
74+
listener: (...args: WebSocketHandlerEventMap[K]) => void,
75+
) => void
76+
77+
public off: <K extends keyof WebSocketHandlerEventMap>(
78+
event: K,
79+
listener: (...args: WebSocketHandlerEventMap[K]) => void,
80+
) => void
81+
82+
public removeAllListeners: <K extends keyof WebSocketHandlerEventMap>(
83+
event?: K,
84+
) => void
85+
86+
protected emitter: Emitter<WebSocketHandlerEventMap>
87+
88+
constructor(private readonly url: Path) {
89+
super()
90+
this.emitter = new Emitter()
91+
92+
// Forward some of the emitter API to the public API
93+
// of the event handler.
94+
this.on = this.emitter.on.bind(this.emitter)
95+
this.off = this.emitter.off.bind(this.emitter)
96+
this.removeAllListeners = this.emitter.removeAllListeners.bind(this.emitter)
97+
}
98+
99+
public parse(args: {
100+
input: MessageEvent<any>
101+
}): WebSocketHandlerParsedResult {
102+
const connection = args.input.data
103+
const match = matchRequestUrl(connection.client.url, this.url)
104+
105+
return {
106+
match,
107+
}
108+
}
109+
110+
public predicate(args: {
111+
input: MessageEvent<any>
112+
parsedResult: WebSocketHandlerParsedResult
113+
}): boolean {
114+
const { match } = args.parsedResult
115+
return match.matches
116+
}
117+
118+
protected async handle(args: {
119+
input: MessageEvent<any>
120+
parsedResult: WebSocketHandlerParsedResult
121+
}): Promise<void> {
122+
const connectionEvent = args.input
123+
124+
// At this point, the WebSocket connection URL has matched the handler.
125+
// Prevent the default behavior of establishing the connection as-is.
126+
connectionEvent.preventDefault()
127+
128+
const connection = connectionEvent.data
129+
130+
// Emit the connection event on the handler.
131+
// This is what the developer adds listeners for.
132+
this.emitter.emit('connection', {
133+
client: connection.client,
134+
server: connection.server,
135+
params: args.parsedResult.match.params || {},
136+
})
137+
}
138+
}
Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
import { type Handler, WebSocketHandler } from '../handlers/WebSocketHandler'
2+
import { webSocketInterceptor } from '../ws/webSocketInterceptor'
3+
4+
export function handleWebSocketEvent(handlers: Array<Handler>) {
5+
webSocketInterceptor.on('connection', (connection) => {
6+
const connectionEvent = new MessageEvent('connection', {
7+
data: connection,
8+
cancelable: true,
9+
})
10+
11+
// Iterate over the handlers and forward the connection
12+
// event to WebSocket event handlers. This is equivalent
13+
// to dispatching that event onto multiple listeners.
14+
for (const handler of handlers) {
15+
if (handler instanceof WebSocketHandler) {
16+
// Never await the run function because event handlers
17+
// are side-effectful and don't block the event loop.
18+
handler.run(connectionEvent)
19+
}
20+
}
21+
22+
// If none of the "ws" handlers matched,
23+
// establish the WebSocket connection as-is.
24+
if (!connectionEvent.defaultPrevented) {
25+
connection.server.connect()
26+
connection.client.on('message', (event) => {
27+
connection.server.send(event.data)
28+
})
29+
}
30+
})
31+
}

src/core/ws.ts

Lines changed: 0 additions & 120 deletions
This file was deleted.
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
import { WebSocketInterceptor } from '@mswjs/interceptors/WebSocket'
2+
3+
export const webSocketInterceptor = new WebSocketInterceptor()

src/core/ws/ws.ts

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import { WebSocketHandler } from '../handlers/WebSocketHandler'
2+
import type { Path } from '../utils/matching/matchRequestUrl'
3+
import { webSocketInterceptor } from './webSocketInterceptor'
4+
5+
/**
6+
* Intercepts outgoing WebSocket connections to the given URL.
7+
*
8+
* @example
9+
* const chat = ws.link('wss://chat.example.com')
10+
* chat.on('connection', (connection) => {})
11+
*/
12+
function createWebSocketLinkHandler(url: Path) {
13+
webSocketInterceptor.apply()
14+
return new WebSocketHandler(url)
15+
}
16+
17+
export const ws = {
18+
link: createWebSocketLinkHandler,
19+
}

0 commit comments

Comments
 (0)