Skip to content

Commit

Permalink
fix: support stopping event propagation
Browse files Browse the repository at this point in the history
  • Loading branch information
kettanaito committed Oct 7, 2024
1 parent b6c8374 commit 7f89cac
Show file tree
Hide file tree
Showing 5 changed files with 574 additions and 7 deletions.
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
Expand Up @@ -137,7 +137,7 @@
"@bundled-es-modules/statuses": "^1.0.1",
"@bundled-es-modules/tough-cookie": "^0.1.6",
"@inquirer/confirm": "^3.0.0",
"@mswjs/interceptors": "^0.36.1",
"@mswjs/interceptors": "^0.36.4",
"@open-draft/deferred-promise": "^2.2.0",
"@open-draft/until": "^2.1.0",
"@types/cookie": "^0.6.0",
Expand Down
8 changes: 4 additions & 4 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

72 changes: 72 additions & 0 deletions src/core/handlers/WebSocketHandler.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,5 @@
import { Emitter } from 'strict-event-emitter'
import { createRequestId } from '@mswjs/interceptors'
import type { WebSocketConnectionData } from '@mswjs/interceptors/WebSocket'
import {
type Match,
Expand All @@ -23,13 +24,18 @@ interface WebSocketHandlerConnection extends WebSocketConnectionData {
export const kEmitter = Symbol('kEmitter')
export const kDispatchEvent = Symbol('kDispatchEvent')
export const kSender = Symbol('kSender')
const kStopPropagationPatched = Symbol('kStopPropagationPatched')
const KOnStopPropagation = Symbol('KOnStopPropagation')

export class WebSocketHandler {
public id: string
public callFrame?: string

protected [kEmitter]: Emitter<WebSocketHandlerEventMap>

constructor(private readonly url: Path) {
this.id = createRequestId()

this[kEmitter] = new Emitter()
this.callFrame = getCallFrame(new Error())
}
Expand Down Expand Up @@ -63,8 +69,74 @@ export class WebSocketHandler {
params: parsedResult.match.params || {},
}

// Support `event.stopPropagation()` for various client/server events.
connection.client.addEventListener(
'message',
createStopPropagationListener(this),
)
connection.client.addEventListener(
'close',
createStopPropagationListener(this),
)

connection.server.addEventListener(
'open',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'message',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'error',
createStopPropagationListener(this),
)
connection.server.addEventListener(
'close',
createStopPropagationListener(this),
)

// Emit the connection event on the handler.
// This is what the developer adds listeners for.
this[kEmitter].emit('connection', resolvedConnection)
}
}

function createStopPropagationListener(handler: WebSocketHandler) {
return function stopPropagationListener(event: Event) {
const propagationStoppedAt = Reflect.get(event, 'kPropagationStoppedAt') as
| string
| undefined

if (propagationStoppedAt && handler.id !== propagationStoppedAt) {
event.stopImmediatePropagation()
return
}

Object.defineProperty(event, KOnStopPropagation, {
value(this: WebSocketHandler) {
Object.defineProperty(event, 'kPropagationStoppedAt', {
value: handler.id,
})
},
configurable: true,
})

// Since the same event instance is shared between all client/server objects,
// make sure to patch its `stopPropagation` method only once.
if (!Reflect.get(event, kStopPropagationPatched)) {
event.stopPropagation = new Proxy(event.stopPropagation, {
apply: (target, thisArg, args) => {
Reflect.get(event, KOnStopPropagation)?.call(handler)
return Reflect.apply(target, thisArg, args)
},
})

Object.defineProperty(event, kStopPropagationPatched, {
value: true,
// If something else attempts to redefine this, throw.
configurable: false,
})
}
}
}
6 changes: 4 additions & 2 deletions test/node/ws-api/on-unhandled-request/error.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -27,12 +27,14 @@ it(
const socket = new WebSocket('wss://localhost:4321')
const errorListener = vi.fn()

await vi.waitFor(() => {
await vi.waitUntil(() => {
return new Promise((resolve, reject) => {
// These are intentionally swapped. The connection MUST error.
socket.addEventListener('error', errorListener)
socket.addEventListener('error', resolve)
socket.onopen = reject
socket.onopen = () => {
reject(new Error('WebSocket connection opened unexpectedly'))
}
})
})

Expand Down
Loading

0 comments on commit 7f89cac

Please sign in to comment.