Skip to content

Commit ccb40e0

Browse files
authored
fix(defineNetwork): prevent event forwarding manually (#2740)
1 parent c07e09b commit ccb40e0

2 files changed

Lines changed: 52 additions & 7 deletions

File tree

src/core/experimental/define-network.ts

Lines changed: 30 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ import {
1212
type AnyHandler,
1313
} from './handlers-controller'
1414
import { toReadonlyArray } from '../utils/internal/toReadonlyArray'
15+
import { Disposable } from '../utils/internal/Disposable'
1516

1617
type UnionToIntersection<U> = (U extends any ? (k: U) => void : never) extends (
1718
k: infer I,
@@ -112,6 +113,7 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
112113
): NetworkApi<Sources> {
113114
let readyState: NetworkReadyState = NetworkReadyState.DISABLED
114115
const events = new Emitter<MergeEventMaps<Sources>>()
116+
const disposable = new Disposable()
115117

116118
const deriveHandlersController = (
117119
handlers: DefineNetworkOptions<Sources>['handlers'],
@@ -130,15 +132,17 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
130132
* certain setup APIs, like `setupServer`, don't await `.enable` (`.listen`).
131133
*/
132134
let handlersController = deriveHandlersController(resolvedOptions.handlers)
133-
let listenersController: AbortController
134135

135136
return {
136137
get readyState() {
137138
return readyState
138139
},
139140
events,
140141
configure(options) {
141-
invariant(readyState === NetworkReadyState.DISABLED, '')
142+
invariant(
143+
readyState === NetworkReadyState.DISABLED,
144+
'Failed to call "configure()" on the network: cannot configure an already enabled network.',
145+
)
142146

143147
if (
144148
options.handlers &&
@@ -158,9 +162,18 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
158162
'Failed to call "enable" on the network: already enabled',
159163
)
160164

161-
listenersController = new AbortController()
162165
readyState = NetworkReadyState.ENABLED
163166

167+
/**
168+
* @note Use a session object scoped to the current "enable()"
169+
* to prevent "frame.events" listeners from surviving across enable/disable cycles.
170+
* @see The note about `AbortController` below.
171+
*/
172+
const session = { active: true }
173+
disposable['subscriptions'].push(() => {
174+
session.active = false
175+
})
176+
164177
const result = resolvedOptions.sources.map((source) => {
165178
/**
166179
* @note Preemptively disable the network source before enabling.
@@ -171,8 +184,19 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
171184
NetworkSource.prototype.disable.call(source)
172185

173186
source.on('frame', async ({ frame }) => {
174-
frame.events.on('*', (event) => events.emit(event), {
175-
signal: listenersController.signal,
187+
frame.events.on('*', (event) => {
188+
/**
189+
* @note Prevent event forwarding manually and not via an AbortController
190+
* because certain runtimes, like Cloudflare, throw when referencing an
191+
* AbortController created in a different context. Bear in mind that the frame
192+
* events run in the patched request client context while the AbortController
193+
* is created outside, in the "defineNetwork" closure, which is a test context.
194+
*/
195+
if (!session.active) {
196+
return
197+
}
198+
199+
events.emit(event)
176200
})
177201

178202
const handlers = frame.getHandlers(handlersController)
@@ -197,8 +221,8 @@ export function defineNetwork<Sources extends Array<NetworkSource<any>>>(
197221
'Failed to call "disable" on the network: already disabled',
198222
)
199223

200-
listenersController.abort()
201224
readyState = NetworkReadyState.DISABLED
225+
disposable.dispose()
202226

203227
return colorlessPromiseAll(
204228
resolvedOptions.sources.map((source) => source.disable()),
Lines changed: 22 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,33 @@
1+
import { devUtils } from './devUtils'
2+
13
export type DisposableSubscription = () => void
24

35
export class Disposable {
46
protected subscriptions: Array<DisposableSubscription> = []
57

68
public dispose() {
79
let subscription: DisposableSubscription | undefined
10+
const errors: Array<Error> = []
11+
812
while ((subscription = this.subscriptions.shift())) {
9-
subscription()
13+
try {
14+
subscription()
15+
} catch (error) {
16+
if (error instanceof Error) {
17+
errors.push(error)
18+
}
19+
}
20+
}
21+
22+
if (errors.length > 0) {
23+
console.error(
24+
new AggregateError(
25+
errors,
26+
devUtils.formatMessage(
27+
'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',
28+
),
29+
),
30+
)
1031
}
1132
}
1233
}

0 commit comments

Comments
 (0)