Skip to content

Commit e00e4d6

Browse files
authored
feat: support ws.onUpgrade for handling connection upgrades (#2732)
1 parent 6953307 commit e00e4d6

7 files changed

Lines changed: 314 additions & 22 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: 139 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,68 @@
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+
it('dedupes the shared upgrade sibling across multiple handlers from the same link', () => {
54+
const chat = ws.link('*')
55+
const wsOne = chat.addEventListener('connection', () => {})
56+
const wsTwo = chat.addEventListener('connection', () => {})
57+
const [upgradeHandler] = getSiblingHandlers(wsOne)
58+
59+
const controller = new InMemoryHandlersController([wsOne, wsTwo])
60+
61+
expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
62+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
63+
})
64+
})
65+
666
describe(InMemoryHandlersController.prototype.use, () => {
767
it('prepends a handler to an empty controller', () => {
868
const controller = new InMemoryHandlersController([])
@@ -51,6 +111,44 @@ describe(InMemoryHandlersController.prototype.use, () => {
51111

52112
expect(controller.currentHandlers()).toEqual([graphqlOne, httpTwo, httpOne])
53113
})
114+
115+
it('propagates siblings to their kind buckets at runtime', () => {
116+
const controller = new InMemoryHandlersController([])
117+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
118+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
119+
120+
controller.use([wsHandler])
121+
122+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
123+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
124+
})
125+
126+
it('prepends incoming siblings before existing handlers of the same kind', () => {
127+
const existingHttp = http.get('/existing', () => {})
128+
const controller = new InMemoryHandlersController([existingHttp])
129+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
130+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
131+
132+
controller.use([wsHandler])
133+
134+
expect(controller.getHandlersByKind('request')).toEqual([
135+
upgradeHandler,
136+
existingHttp,
137+
])
138+
})
139+
140+
it('dedupes the shared upgrade sibling when called with multiple handlers from the same link', () => {
141+
const chat = ws.link('*')
142+
const wsOne = chat.addEventListener('connection', () => {})
143+
const wsTwo = chat.addEventListener('connection', () => {})
144+
const [upgradeHandler] = getSiblingHandlers(wsOne)
145+
146+
const controller = new InMemoryHandlersController([])
147+
controller.use([wsOne, wsTwo])
148+
149+
expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
150+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
151+
})
54152
})
55153

56154
describe(InMemoryHandlersController.prototype.reset, () => {
@@ -96,6 +194,42 @@ describe(InMemoryHandlersController.prototype.reset, () => {
96194
*/
97195
expect(controller.currentHandlers()).toEqual([httpTwo])
98196
})
197+
198+
it('places siblings into their kind buckets when resetting to next handlers', () => {
199+
const controller = new InMemoryHandlersController([])
200+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
201+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
202+
203+
controller.reset([wsHandler])
204+
205+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
206+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
207+
})
208+
209+
it('restores siblings when resetting to the initial handlers', () => {
210+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
211+
const [upgradeHandler] = getSiblingHandlers(wsHandler)
212+
const controller = new InMemoryHandlersController([wsHandler])
213+
214+
controller.use([http.get('/runtime', () => {})])
215+
controller.reset([])
216+
217+
expect(controller.getHandlersByKind('websocket')).toEqual([wsHandler])
218+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
219+
})
220+
221+
it('dedupes the shared upgrade sibling when reset with multiple handlers from the same link', () => {
222+
const chat = ws.link('*')
223+
const wsOne = chat.addEventListener('connection', () => {})
224+
const wsTwo = chat.addEventListener('connection', () => {})
225+
const [upgradeHandler] = getSiblingHandlers(wsOne)
226+
227+
const controller = new InMemoryHandlersController([])
228+
controller.reset([wsOne, wsTwo])
229+
230+
expect(controller.getHandlersByKind('websocket')).toEqual([wsOne, wsTwo])
231+
expect(controller.getHandlersByKind('request')).toEqual([upgradeHandler])
232+
})
99233
})
100234

101235
describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
@@ -112,11 +246,10 @@ describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
112246
]).getHandlersByKind('websocket'),
113247
).toEqual([])
114248

249+
const wsHandler = ws.link('*').addEventListener('connection', () => {})
115250
expect(
116-
new InMemoryHandlersController([
117-
ws.link('*').addEventListener('connection', () => {}),
118-
]).getHandlersByKind('request'),
119-
).toEqual([])
251+
new InMemoryHandlersController([wsHandler]).getHandlersByKind('request'),
252+
).toEqual(getSiblingHandlers(wsHandler))
120253
})
121254

122255
it('returns all handlers if they all match', () => {
@@ -142,14 +275,15 @@ describe(InMemoryHandlersController.prototype.getHandlersByKind, () => {
142275
const httpHandler = http.get('/', () => {})
143276
const graphqlHandler = graphql.query('', () => {})
144277
const wsHandler = ws.link('*').addEventListener('connection', () => {})
278+
const wsHandlerSiblings = getSiblingHandlers(wsHandler)
145279

146280
expect(
147281
new InMemoryHandlersController([
148282
httpHandler,
149283
graphqlHandler,
150284
wsHandler,
151285
]).getHandlersByKind('request'),
152-
).toEqual([httpHandler, graphqlHandler])
286+
).toEqual([httpHandler, graphqlHandler, ...wsHandlerSiblings])
153287

154288
expect(
155289
new InMemoryHandlersController([

src/core/experimental/handlers-controller.ts

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -2,18 +2,31 @@ 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>>>
89

910
export function groupHandlersByKind(handlers: Array<AnyHandler>): HandlersMap {
1011
const groups: HandlersMap = {}
1112

13+
const pushUnique = (kind: AnyHandler['kind'], handler: AnyHandler) => {
14+
const bucket = (groups[kind] ||= [])
15+
16+
if (!bucket.includes(handler)) {
17+
bucket.push(handler)
18+
}
19+
}
20+
1221
/**
1322
* @note `Object.groupBy` is not implemented in Node.js v20.
1423
*/
1524
for (const handler of handlers) {
16-
;(groups[handler.kind] ||= []).push(handler)
25+
pushUnique(handler.kind, handler)
26+
27+
for (const sibling of getSiblingHandlers(handler)) {
28+
pushUnique(sibling.kind, sibling)
29+
}
1730
}
1831

1932
return groups
@@ -69,14 +82,16 @@ export abstract class HandlersController {
6982
}
7083

7184
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]
85+
const overrides = groupHandlersByKind(nextHandlers)
86+
87+
// Prepend overrides to their respective kind buckets so they take
88+
// priority over existing handlers while preserving input order.
89+
for (const kind in overrides) {
90+
const overridesForKind = overrides[kind as AnyHandler['kind']]!
91+
const existingForKind = handlers[kind as AnyHandler['kind']]
92+
handlers[kind as AnyHandler['kind']] = existingForKind
93+
? [...overridesForKind, ...existingForKind]
94+
: overridesForKind
8095
}
8196

8297
this.setState({ handlers })

src/core/handlers/WebSocketHandler.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -118,7 +118,7 @@ export class WebSocketHandler {
118118
params: parsedResult.match.params || {},
119119
}
120120

121-
if (resolutionContext?.[kAutoConnect]) {
121+
if (resolutionContext?.[kAutoConnect] ?? true) {
122122
if (this[kConnect](resolvedConnection)) {
123123
return resolvedConnection
124124
}
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+
}

0 commit comments

Comments
 (0)