Skip to content

Commit d5a9b4c

Browse files
committed
chore: design "ws.link" api
1 parent ae70c43 commit d5a9b4c

1 file changed

Lines changed: 102 additions & 53 deletions

File tree

src/core/ws.ts

Lines changed: 102 additions & 53 deletions
Original file line numberDiff line numberDiff line change
@@ -1,71 +1,120 @@
1-
import { WebSocketInterceptor } from '@mswjs/interceptors/WebSocket'
2-
import { matchRequestUrl, type Path } from './utils/matching/matchRequestUrl'
1+
import { Emitter } from 'strict-event-emitter'
2+
import {
3+
WebSocketInterceptor,
4+
type WebSocketEventMap,
5+
type WebSocketClientConnection,
6+
type WebSocketServerConnection,
7+
} from '@mswjs/interceptors/WebSocket'
8+
import {
9+
type Path,
10+
matchRequestUrl,
11+
PathParams,
12+
} from './utils/matching/matchRequestUrl'
313

414
/**
5-
* Intercept outgoing WebSocket connections to the given URL.
6-
* @param url WebSocket server URL
15+
* @fixme Will each "ws" import create a NEW intereptor?
16+
* Consider moving this away and reusing.
717
*/
8-
function createWebSocketHandler(url: Path) {
9-
/**
10-
* @note I think the handler should initialize the interceptor.
11-
* This way, no WebSocket class stubs will be applied unless
12-
* you use a single "ws" handler. Interceptors are deduped.
13-
*/
14-
const interceptor = new WebSocketInterceptor()
15-
interceptor.apply()
18+
const interceptor = new WebSocketInterceptor()
19+
const emitter = new EventTarget()
1620

17-
/**
18-
* @todo Should this maybe live in the "setup" function
19-
* and then emit ONE intercepted connection to MANY "ws"
20-
* handlers? That way:
21-
* - The order of listeners still matters (consistency).
22-
* - The `.use()` makes sense.
23-
*
24-
* The challenge: only apply the interceptor when at least
25-
* ONE "ws.link()" has been created.
26-
*/
27-
interceptor.on('connection', (connection) => {
28-
const match = matchRequestUrl(connection.client.url, url)
29-
30-
// For WebSocket connections that don't match the given
31-
// URL predicate, open them as-is and forward all messages.
32-
if (!match.matches) {
33-
connection.server.connect()
34-
connection.client.on('message', (event) => {
35-
connection.server.send(event.data)
36-
})
37-
return
38-
}
39-
40-
/** @todo Those that match, route to the public API */
21+
interceptor.on('connection', (connection) => {
22+
const connectionMessage = new MessageEvent('connection', {
23+
data: connection,
24+
cancelable: true,
4125
})
4226

43-
/** @fixme Dispose of the interceptor. */
27+
emitter.dispatchEvent(connectionMessage)
28+
29+
// If none of the "ws" handlers matched,
30+
// establish the WebSocket connection as-is.
31+
if (!connectionMessage.defaultPrevented) {
32+
connection.server.connect()
33+
connection.client.on('message', (event) => {
34+
connection.server.send(event.data)
35+
})
36+
}
37+
})
38+
39+
/**
40+
* Disposes of the WebSocket interceptor.
41+
* The interceptor is a singleton instantiated in the
42+
* "ws" API.
43+
*/
44+
export function disposeWebSocketInterceptor() {
45+
interceptor.dispose()
46+
}
47+
48+
type WebSocketLinkHandlerEventMap = {
49+
connection: [
50+
args: {
51+
client: WebSocketClientConnection
52+
server: WebSocketServerConnection
53+
params: PathParams
54+
},
55+
]
56+
}
57+
58+
/**
59+
* Intercepts outgoing WebSocket connections to the given URL.
60+
*
61+
* @example
62+
* const chat = ws.link('wss://chat.example.com')
63+
* chat.on('connection', (connection) => {})
64+
*/
65+
function createWebSocketLinkHandler(url: Path) {
66+
const publicEmitter = new Emitter<WebSocketLinkHandlerEventMap>()
67+
68+
// Apply the WebSocket interceptor.
69+
// This defers the WebSocket class patching to the first
70+
// "ws" event handler call. Repetitive calls to the "apply"
71+
// method have no effect.
72+
interceptor.apply()
73+
74+
emitter.addEventListener(
75+
'connection',
76+
(event: MessageEvent<WebSocketEventMap['connection'][0]>) => {
77+
const { client, server } = event.data
78+
const match = matchRequestUrl(client.url, url)
79+
80+
if (match.matches) {
81+
// Preventing the default on the connection event
82+
// will prevent the WebSocket connection from being
83+
// established.
84+
event.preventDefault()
85+
publicEmitter.emit('connection', {
86+
client,
87+
server,
88+
params: match.params || {},
89+
})
90+
}
91+
},
92+
)
93+
94+
const { on, off, removeAllListeners } = publicEmitter
4495

4596
return {
46-
/**
47-
* @fixme Don't expose these directly. The exposed interface
48-
* must be decoupled from the interceptor to support
49-
* "removeAllEvents" and such.
50-
*/
51-
on: interceptor.on.bind(interceptor),
52-
off: interceptor.off.bind(interceptor),
53-
removeAllListeners: interceptor.removeAllListeners.bind(interceptor),
97+
on,
98+
off,
99+
removeAllListeners,
54100
}
55101
}
56102

57103
export const ws = {
58-
link: createWebSocketHandler,
104+
link: createWebSocketLinkHandler,
59105
}
60106

61107
//
108+
// Public usage.
62109
//
63110

64-
const chat = ws.link('wss://*.service.com')
111+
const chat = ws.link('wss://:roomId.service.com')
65112

66-
chat.on('connection', ({ client }) => {
67-
client.on('message', (event) => {
68-
console.log(event.data)
69-
client.send('Hello from server!')
70-
})
71-
})
113+
export const handlers = [
114+
chat.on('connection', ({ client }) => {
115+
client.on('message', (event) => {
116+
console.log(event.data)
117+
client.send('Hello from server!')
118+
})
119+
}),
120+
]

0 commit comments

Comments
 (0)