Skip to content

Commit 48689ed

Browse files
committed
feat: support ws.onUpgrade for handling connection upgrades
1 parent 6953307 commit 48689ed

5 files changed

Lines changed: 198 additions & 20 deletions

File tree

src/core/experimental/frames/http-frame.test.ts

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,8 @@ import { ws } from '../../ws'
44
import { bypass } from '../../bypass'
55
import type { HttpNetworkFrameEventMap } from './http-frame'
66
import { HttpNetworkFrame } from './http-frame'
7-
import { InMemoryHandlersController } from '#core/experimental/handlers-controller'
7+
import { InMemoryHandlersController } from '../../experimental/handlers-controller'
8+
import { getSiblingHandlers } from '../../utils/internal/attachSiblingHandlers'
89

910
beforeAll(() => {
1011
vi.spyOn(console, 'error').mockImplementation(() => {})
@@ -46,6 +47,9 @@ it('filters only request type handlers', async () => {
4647
const webSocketHandlers = [
4748
ws.link('ws://localhost').addEventListener('connection', () => {}),
4849
]
50+
const webSocketSiblingHandlers = webSocketHandlers.flatMap((handler) =>
51+
getSiblingHandlers(handler),
52+
)
4953

5054
const controller = new InMemoryHandlersController([
5155
...httpHandlers,
@@ -55,6 +59,7 @@ it('filters only request type handlers', async () => {
5559

5660
expect(frame.getHandlers(controller)).toEqual([
5761
...httpHandlers,
62+
...webSocketSiblingHandlers,
5863
...graphqlHandlers,
5964
])
6065
expect(frame.getHandlers(new InMemoryHandlersController([]))).toEqual([])

src/core/experimental/handlers-controller.test.ts

Lines changed: 101 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,56 @@
11
import { http } from '../http'
22
import { graphql } from '../graphql'
33
import { ws } from '../ws'
4+
import { getSiblingHandlers } from '../utils/internal/attachSiblingHandlers'
45
import { InMemoryHandlersController } from './handlers-controller'
56

7+
describe('constructor', () => {
8+
it('places the sibling in its own kind bucket', () => {
9+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
10+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
11+
12+
const controller = new InMemoryHandlersController([wsHandler])
13+
14+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
15+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
16+
})
17+
18+
it('interleaves the sibling at the owner position when grouping by kind', () => {
19+
const httpOne = http.get('/', () => {})
20+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
21+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
22+
const httpTwo = http.get('/', () => {})
23+
24+
const controller = new InMemoryHandlersController([
25+
httpOne,
26+
wsHandler,
27+
httpTwo,
28+
])
29+
30+
expect(controller.getHandlersByKind('request')).toEqual([
31+
httpOne,
32+
upgradeHandler,
33+
httpTwo,
34+
])
35+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
36+
})
37+
38+
it('extracts siblings from every owner in the input list', () => {
39+
const wsOne = ws.link('*').addEventListener('connection', () => {})
40+
const wsTwo = ws.link('*').addEventListener('connection', () => {})
41+
const [upgradeOne] = getSiblingHandlers(wsOne)
42+
const [upgradeTwo] = getSiblingHandlers(wsTwo)
43+
44+
const controller = new InMemoryHandlersController([wsOne, wsTwo])
45+
46+
expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
47+
expect(controller.getHandlersByKind('request')).toEqual([
48+
upgradeOne,
49+
upgradeTwo,
50+
])
51+
})
52+
})
53+
654
describe(InMemoryHandlersController.prototype.use, () => {
755
it('prepends a handler to an empty controller', () => {
856
const controller = new InMemoryHandlersController([])
@@ -51,6 +99,31 @@ describe(InMemoryHandlersController.prototype.use, () => {
5199

52100
expect(controller.currentHandlers()).toEqual([graphqlOne, httpTwo, httpOne])
53101
})
102+
103+
it('propagates siblings to their kind buckets at runtime', () => {
104+
const controller = new InMemoryHandlersController([])
105+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
106+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
107+
108+
controller.use([wsHandler])
109+
110+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
111+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
112+
})
113+
114+
it('prepends incoming siblings before existing handlers of the same kind', () => {
115+
const existingHttp = http.get('/existing', () => {})
116+
const controller = new InMemoryHandlersController([existingHttp])
117+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
118+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
119+
120+
controller.use([wsHandler])
121+
122+
expect(controller.getHandlersByKind('request')).toEqual([
123+
upgradeHandler,
124+
existingHttp,
125+
])
126+
})
54127
})
55128

56129
describe(InMemoryHandlersController.prototype.reset, () => {
@@ -96,6 +169,29 @@ describe(InMemoryHandlersController.prototype.reset, () => {
96169
*/
97170
expect(controller.currentHandlers()).toEqual([httpTwo])
98171
})
172+
173+
it('places siblings into their kind buckets when resetting to next handlers', () => {
174+
const controller = new InMemoryHandlersController([])
175+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
176+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
177+
178+
controller.reset([wsHandler])
179+
180+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
181+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
182+
})
183+
184+
it('restores siblings when resetting to the initial handlers', () => {
185+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
186+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
187+
const controller = new InMemoryHandlersController([wsHandler])
188+
189+
controller.use([http.get('/runtime', () => {})])
190+
controller.reset([])
191+
192+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
193+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
194+
})
99195
})
100196

101197
describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
@@ -112,11 +208,10 @@ describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
112208
]).getHandlersByKind('websocket'),
113209
).toEqual([])
114210

211+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
115212
expect(
116-
new InMemoryHandlersController([
117-
ws.link('*').addEventListener('connection', () => {}),
118-
]).getHandlersByKind('request'),
119-
).toEqual([])
213+
new InMemoryHandlersController([wsHandler]).getHandlersByKind('request'),
214+
).toEqual(getSiblingHandlers(wsHandler))
120215
})
121216

122217
it('returns all handlers if they all match', () => {
@@ -142,14 +237,15 @@ describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
142237
const httpHandler = http.get('/', () => {})
143238
const graphqlHandler = graphql.query('', () => {})
144239
const wsHandler = ws.link('*').addEventListener('connection', () => {})
240+
const wsHandlerSiblings = getSiblingHandlers(wsHandler)
145241

146242
expect(
147243
new InMemoryHandlersController([
148244
httpHandler,
149245
graphqlHandler,
150246
wsHandler,
151247
]).getHandlersByKind('request'),
152-
).toEqual([httpHandler, graphqlHandler])
248+
).toEqual([httpHandler, graphqlHandler, ...wsHandlerSiblings])
153249

154250
expect(
155251
new InMemoryHandlersController([

src/core/experimental/handlers-controller.ts

Lines changed: 15 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@ import { invariant } from 'outvariant'
22
import { type RequestHandler } from '../handlers/RequestHandler'
33
import { type WebSocketHandler } from '../handlers/WebSocketHandler'
44
import { devUtils } from '../utils/internal/devUtils'
5+
import { getSiblingHandlers } from '../utils/internal/attachSiblingHandlers'
56

67
export type AnyHandler = RequestHandler | WebSocketHandler
78
export type HandlersMap = Partial<Record<AnyHandler['kind'], Array<AnyHandler>>>
@@ -14,6 +15,10 @@ export function groupHandlersByKind(handlers: Array<AnyHandler>): HandlersMap {
1415
*/
1516
for (const handler of handlers) {
1617
;(groups[handler.kind] ||= []).push(handler)
18+
19+
for (const sibling of getSiblingHandlers(handler)) {
20+
;(groups[sibling.kind] ||= []).push(sibling)
21+
}
1722
}
1823

1924
return groups
@@ -69,14 +74,16 @@ export abstract class HandlersController {
6974
}
7075

7176
const { handlers } = this.getState()
72-
73-
// Iterate over next handlers and prepend them to their respective lists.
74-
// Iterate in a reverse order to the keep the order of the runtime handlers as provided.
75-
for (let i = nextHandlers.length - 1; i >= 0; i--) {
76-
const handler = nextHandlers[i]
77-
handlers[handler.kind] = handlers[handler.kind]
78-
? [handler, ...handlers[handler.kind]!]
79-
: [handler]
77+
const overrides = groupHandlersByKind(nextHandlers)
78+
79+
// Prepend overrides to their respective kind buckets so they take
80+
// priority over existing handlers while preserving input order.
81+
for (const kind in overrides) {
82+
const overridesForKind = overrides[kind as AnyHandler['kind']]!
83+
const existingForKind = handlers[kind as AnyHandler['kind']]
84+
handlers[kind as AnyHandler['kind']] = existingForKind
85+
? [...overridesForKind, ...existingForKind]
86+
: overridesForKind
8087
}
8188

8289
this.setState({ handlers })
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
import { invariant } from 'outvariant'
2+
import type { AnyHandler } from '../../experimental/handlers-controller'
3+
4+
const kSiblingHandlers = Symbol('kSiblingHandlers')
5+
6+
export function attachSiblingHandlers<T extends AnyHandler>(
7+
owner: T,
8+
siblings: Array<AnyHandler>,
9+
): T {
10+
invariant(
11+
getSiblingHandlers(owner).length === 0,
12+
'Failed to merge handlers: the owner "%s" handler is already merged',
13+
owner.kind,
14+
)
15+
16+
Object.defineProperty(owner, kSiblingHandlers, {
17+
value: siblings,
18+
enumerable: false,
19+
writable: false,
20+
configurable: false,
21+
})
22+
23+
return owner
24+
}
25+
26+
export function getSiblingHandlers(owner: AnyHandler): Array<AnyHandler> {
27+
return Reflect.get(owner, kSiblingHandlers) || []
28+
}

src/core/ws.ts

Lines changed: 48 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,24 @@
11
import { invariant } from 'outvariant'
2+
import { FetchResponse } from '@mswjs/interceptors'
23
import type {
34
WebSocketData,
45
WebSocketClientConnectionProtocol,
56
} from '@mswjs/interceptors/WebSocket'
7+
import type { ResponseResolver } from './handlers/RequestHandler'
68
import {
79
WebSocketHandler,
810
kEmitter,
911
type WebSocketHandlerEventMap,
1012
} from './handlers/WebSocketHandler'
1113
import { hasRefCounted } from './utils/internal/hasRefCounted'
12-
import { type Path, isPath } from './utils/matching/matchRequestUrl'
14+
import {
15+
type Path,
16+
isPath,
17+
matchRequestUrl,
18+
} from './utils/matching/matchRequestUrl'
1319
import { WebSocketClientManager } from './ws/WebSocketClientManager'
20+
import { http } from './http'
21+
import { attachSiblingHandlers } from './utils/internal/attachSiblingHandlers'
1422

1523
const webSocketChannel = new BroadcastChannel('msw:websocket-client-manager')
1624

@@ -105,23 +113,30 @@ function createWebSocketLinkHandler(url: Path): WebSocketLink {
105113
return clientManager.clients
106114
},
107115
addEventListener(event, listener) {
108-
const handler = new WebSocketHandler(url)
116+
const webSocketHandler = new WebSocketHandler(url)
109117

110118
// Add the connection event listener for when the
111119
// handler matches and emits a connection event.
112120
// When that happens, store that connection in the
113121
// set of all connections for reference.
114-
handler[kEmitter].on('connection', async ({ client }) => {
122+
webSocketHandler[kEmitter].on('connection', async ({ client }) => {
115123
await clientManager.addConnection(client)
116124
})
117125

118126
// The "handleWebSocketEvent" function will invoke
119127
// the "run()" method on the WebSocketHandler.
120128
// If the handler matches, it will emit the "connection"
121129
// event. Attach the user-defined listener to that event.
122-
handler[kEmitter].on(event, listener)
130+
webSocketHandler[kEmitter].on(event, listener)
131+
132+
const upgradeHandler = http.get(({ request }) => {
133+
return (
134+
request.headers.get('upgrade') === 'websocket' &&
135+
matchRequestUrl(new URL(request.url), url).matches
136+
)
137+
}, ws.onUpgrade)
123138

124-
return handler
139+
return attachSiblingHandlers(webSocketHandler, [upgradeHandler])
125140
},
126141

127142
broadcast(data) {
@@ -145,6 +160,13 @@ function createWebSocketLinkHandler(url: Path): WebSocketLink {
145160
}
146161
}
147162

163+
const WEBSOCKET_GUID = '258EAFA5-E914-47DA-95CA-C5AB0DC85B11'
164+
165+
interface WebSocketNamespace {
166+
link: typeof createWebSocketLinkHandler
167+
onUpgrade: ResponseResolver
168+
}
169+
148170
/**
149171
* A namespace to intercept and mock WebSocket connections.
150172
*
@@ -154,8 +176,28 @@ function createWebSocketLinkHandler(url: Path): WebSocketLink {
154176
* @see {@link https://mswjs.io/docs/api/ws `ws` API reference}
155177
* @see {@link https://mswjs.io/docs/basics/handling-websocket-events Handling WebSocket events}
156178
*/
157-
export const ws = {
179+
export const ws: WebSocketNamespace = {
158180
link: createWebSocketLinkHandler,
181+
async onUpgrade({ request }) {
182+
const key = request.headers.get('sec-websocket-key')
183+
184+
if (!key) {
185+
return
186+
}
187+
188+
const keyBytes = new TextEncoder().encode(key + WEBSOCKET_GUID)
189+
const digest = await crypto.subtle.digest('SHA-1', keyBytes)
190+
const acceptValue = btoa(String.fromCharCode(...new Uint8Array(digest)))
191+
192+
return new FetchResponse(null, {
193+
status: 101,
194+
headers: {
195+
upgrade: 'websocket',
196+
connection: 'upgrade',
197+
'sec-websocket-accept': acceptValue,
198+
},
199+
})
200+
},
159201
}
160202

161203
export { type WebSocketData }

0 commit comments

Comments
 (0)