fix(defineNetwork): prevent event forwarding manually (#2740)

This commit is contained in:
Artem Zakharchenko
2026-05-11 11:34:45 +02:00
committed by GitHub
parent 0fa5c5f088
commit 50b4fd4e75
2 changed files with 52 additions and 7 deletions
+30 -6
View File
@@ -12,6 +12,7 @@ import {
type AnyHandler,
} from './handlers-controller'
import { toReadonlyArray } from '../utils/internal/toReadonlyArray'
import { Disposable } from '../utils/internal/Disposable'
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
k: infer I,
@@ -112,6 +113,7 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
): NetworkApi<Sources> {
let readyState: NetworkReadyState = NetworkReadyState.DISABLED
const events = new Emitter<MergeEventMaps<Sources>>()
const disposable = new Disposable()
const deriveHandlersController = (
handlers: DefineNetworkOptions<Sources>['handlers'],
@@ -130,7 +132,6 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
* certain setup APIs, like `setupServer`, don't await `.enable` (`.listen`).
*/
let handlersController = deriveHandlersController(resolvedOptions.handlers)
let listenersController: AbortController
return {
get readyState() {
@@ -138,7 +139,10 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
},
events,
configure(options) {
invariant(readyState === NetworkReadyState.DISABLED, '')
invariant(
readyState === NetworkReadyState.DISABLED,
'Failed to call "configure()" on the network: cannot configure an already enabled network.',
)
if (
options.handlers &&
@@ -158,9 +162,18 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
'Failed to call "enable" on the network: already enabled',
)
listenersController = new AbortController()
readyState = NetworkReadyState.ENABLED
/**
* @note Use a session object scoped to the current "enable()"
* to prevent "frame.events" listeners from surviving across enable/disable cycles.
* @see The note about `AbortController` below.
*/
const session = { active: true }
disposable['subscriptions'].push(() => {
session.active = false
})
const result = resolvedOptions.sources.map((source) => {
/**
* @note Preemptively disable the network source before enabling.
@@ -171,8 +184,19 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
NetworkSource.prototype.disable.call(source)
source.on('frame', async ({ frame }) => {
frame.events.on('*', (event) => events.emit(event), {
signal: listenersController.signal,
frame.events.on('*', (event) => {
/**
* @note Prevent event forwarding manually and not via an AbortController
* because certain runtimes, like Cloudflare, throw when referencing an
* AbortController created in a different context. Bear in mind that the frame
* events run in the patched request client context while the AbortController
* is created outside, in the "defineNetwork" closure, which is a test context.
*/
if (!session.active) {
return
}
events.emit(event)
})
const handlers = frame.getHandlers(handlersController)
@@ -197,8 +221,8 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
'Failed to call "disable" on the network: already disabled',
)
listenersController.abort()
readyState = NetworkReadyState.DISABLED
disposable.dispose()
return colorlessPromiseAll(
resolvedOptions.sources.map((source) => source.disable()),
+22 -1
View File
@@ -1,3 +1,5 @@
import { devUtils } from './devUtils'
export type DisposableSubscription = () => void
export class Disposable {
@@ -5,8 +7,27 @@ export class Disposable {
public dispose() {
let subscription: DisposableSubscription | undefined
const errors: Array<Error> = []
while ((subscription = this.subscriptions.shift())) {
subscription()
try {
subscription()
} catch (error) {
if (error instanceof Error) {
errors.push(error)
}
}
}
if (errors.length > 0) {
console.error(
new AggregateError(
errors,
devUtils.formatMessage(
'Failed to dispose of some side effects. This is likely an issue with MSW, please report it on GitHub: https://github.com/mswjs/msw/issues',
),
),
)
}
}
}