Skip to content

Commit 47ca629

Browse files
committed
chore: add agnostic "Handler" class
1 parent d5a9b4c commit 47ca629

File tree

6 files changed

+191
-120
lines changed

6 files changed

+191
-120
lines changed

src/core/handlers/Handler.ts

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
export type HandlerOptions = {
2+
once?: boolean
3+
}
4+
5+
export abstract class Handler<Input = unknown> {
6+
public isUsed: boolean
7+
8+
constructor(protected readonly options: HandlerOptions = {}) {
9+
this.isUsed = false
10+
}
11+
12+
abstract parse(args: { input: Input }): unknown
13+
abstract predicate(args: { input: Input; parsedResult: unknown }): boolean
14+
protected abstract handle(args: {
15+
input: Input
16+
parsedResult: unknown
17+
}): Promise<unknown | null>
18+
19+
public async run(input: Input): Promise<unknown | null> {
20+
if (this.options?.once && this.isUsed) {
21+
return null
22+
}
23+
24+
const parsedResult = this.parse({ input })
25+
const shouldHandle = this.predicate({
26+
input,
27+
parsedResult,
28+
})
29+
30+
if (!shouldHandle) {
31+
return null
32+
}
33+
34+
const result = await this.handle({
35+
input,
36+
parsedResult,
37+
})
38+
39+
this.isUsed = true
40+
41+
return result
42+
}
43+
}

src/core/handlers/WebSocketHandler.ts

Lines changed: 95 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,95 @@
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+
import { Handler } from './Handler'
13+
14+
type WebSocketHandlerParsedResult = {
15+
match: Match
16+
}
17+
18+
type WebSocketHandlerEventMap = {
19+
connection: [
20+
args: {
21+
client: WebSocketClientConnection
22+
server: WebSocketServerConnection
23+
params: PathParams
24+
},
25+
]
26+
}
27+
28+
export class WebSocketHandler extends Handler<MessageEvent<any>> {
29+
public on: <K extends keyof WebSocketHandlerEventMap>(
30+
event: K,
31+
listener: (...args: WebSocketHandlerEventMap[K]) => void,
32+
) => void
33+
34+
public off: <K extends keyof WebSocketHandlerEventMap>(
35+
event: K,
36+
listener: (...args: WebSocketHandlerEventMap[K]) => void,
37+
) => void
38+
39+
public removeAllListeners: <K extends keyof WebSocketHandlerEventMap>(
40+
event?: K,
41+
) => void
42+
43+
protected emitter: Emitter<WebSocketHandlerEventMap>
44+
45+
constructor(private readonly url: Path) {
46+
super()
47+
this.emitter = new Emitter()
48+
49+
// Forward some of the emitter API to the public API
50+
// of the event handler.
51+
this.on = this.emitter.on.bind(this.emitter)
52+
this.off = this.emitter.off.bind(this.emitter)
53+
this.removeAllListeners = this.emitter.removeAllListeners.bind(this.emitter)
54+
}
55+
56+
public parse(args: {
57+
input: MessageEvent<any>
58+
}): WebSocketHandlerParsedResult {
59+
const connection = args.input.data
60+
const match = matchRequestUrl(connection.client.url, this.url)
61+
62+
return {
63+
match,
64+
}
65+
}
66+
67+
public predicate(args: {
68+
input: MessageEvent<any>
69+
parsedResult: WebSocketHandlerParsedResult
70+
}): boolean {
71+
const { match } = args.parsedResult
72+
return match.matches
73+
}
74+
75+
protected async handle(args: {
76+
input: MessageEvent<any>
77+
parsedResult: WebSocketHandlerParsedResult
78+
}): Promise<void> {
79+
const connectionEvent = args.input
80+
81+
// At this point, the WebSocket connection URL has matched the handler.
82+
// Prevent the default behavior of establishing the connection as-is.
83+
connectionEvent.preventDefault()
84+
85+
const connection = connectionEvent.data
86+
87+
// Emit the connection event on the handler.
88+
// This is what the developer adds listeners for.
89+
this.emitter.emit('connection', {
90+
client: connection.client,
91+
server: connection.server,
92+
params: args.parsedResult.match.params || {},
93+
})
94+
}
95+
}
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.

src/core/ws/webSocketInterceptor.ts

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)