@mswjs/interceptors 0.15.1 → 0.16.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/LICENSE.md +9 -0
  2. package/README.md +68 -49
  3. package/lib/BatchInterceptor.d.ts +18 -0
  4. package/lib/BatchInterceptor.js +79 -0
  5. package/lib/BatchInterceptor.js.map +1 -0
  6. package/lib/Interceptor.d.ts +49 -0
  7. package/lib/Interceptor.js +197 -0
  8. package/lib/Interceptor.js.map +1 -0
  9. package/lib/RemoteInterceptor.d.ts +24 -0
  10. package/lib/RemoteInterceptor.js +216 -0
  11. package/lib/RemoteInterceptor.js.map +1 -0
  12. package/lib/glossary.d.ts +32 -0
  13. package/lib/glossary.js +3 -0
  14. package/lib/glossary.js.map +1 -0
  15. package/lib/index.d.ts +2 -2
  16. package/lib/index.js +2 -2
  17. package/lib/index.js.map +1 -1
  18. package/lib/interceptors/ClientRequest/NodeClientRequest.d.ts +5 -5
  19. package/lib/interceptors/ClientRequest/NodeClientRequest.js +56 -15
  20. package/lib/interceptors/ClientRequest/NodeClientRequest.js.map +1 -1
  21. package/lib/interceptors/ClientRequest/http.get.d.ts +2 -3
  22. package/lib/interceptors/ClientRequest/http.get.js +2 -5
  23. package/lib/interceptors/ClientRequest/http.get.js.map +1 -1
  24. package/lib/interceptors/ClientRequest/http.request.d.ts +2 -3
  25. package/lib/interceptors/ClientRequest/http.request.js +3 -6
  26. package/lib/interceptors/ClientRequest/http.request.js.map +1 -1
  27. package/lib/interceptors/ClientRequest/index.d.ts +14 -4
  28. package/lib/interceptors/ClientRequest/index.js +59 -46
  29. package/lib/interceptors/ClientRequest/index.js.map +1 -1
  30. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js +7 -2
  31. package/lib/interceptors/ClientRequest/utils/getIncomingMessageBody.js.map +1 -1
  32. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.d.ts +11 -4
  33. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js +110 -58
  34. package/lib/interceptors/XMLHttpRequest/XMLHttpRequestOverride.js.map +1 -1
  35. package/lib/interceptors/XMLHttpRequest/index.d.ts +11 -5
  36. package/lib/interceptors/XMLHttpRequest/index.js +43 -25
  37. package/lib/interceptors/XMLHttpRequest/index.js.map +1 -1
  38. package/lib/interceptors/fetch/index.d.ts +8 -2
  39. package/lib/interceptors/fetch/index.js +120 -68
  40. package/lib/interceptors/fetch/index.js.map +1 -1
  41. package/lib/presets/browser.d.ts +3 -1
  42. package/lib/presets/browser.js +2 -2
  43. package/lib/presets/browser.js.map +1 -1
  44. package/lib/presets/node.d.ts +3 -1
  45. package/lib/presets/node.js +1 -1
  46. package/lib/presets/node.js.map +1 -1
  47. package/lib/utils/AsyncEventEmitter.d.ts +29 -0
  48. package/lib/utils/AsyncEventEmitter.js +241 -0
  49. package/lib/utils/AsyncEventEmitter.js.map +1 -0
  50. package/lib/utils/createLazyCallback.d.ts +11 -0
  51. package/lib/utils/createLazyCallback.js +75 -0
  52. package/lib/utils/createLazyCallback.js.map +1 -0
  53. package/lib/utils/nextTick.d.ts +2 -0
  54. package/lib/utils/nextTick.js +16 -0
  55. package/lib/utils/nextTick.js.map +1 -0
  56. package/lib/utils/toIsoResponse.d.ts +1 -1
  57. package/package.json +6 -6
  58. package/src/BatchInterceptor.test.ts +113 -0
  59. package/src/BatchInterceptor.ts +60 -0
  60. package/src/Interceptor.test.ts +166 -0
  61. package/src/Interceptor.ts +226 -0
  62. package/src/RemoteInterceptor.ts +176 -0
  63. package/src/glossary.ts +42 -0
  64. package/src/index.ts +2 -2
  65. package/src/interceptors/ClientRequest/NodeClientRequest.test.ts +87 -70
  66. package/src/interceptors/ClientRequest/NodeClientRequest.ts +139 -100
  67. package/src/interceptors/ClientRequest/http.get.ts +7 -11
  68. package/src/interceptors/ClientRequest/http.request.ts +8 -12
  69. package/src/interceptors/ClientRequest/index.test.ts +43 -0
  70. package/src/interceptors/ClientRequest/index.ts +46 -46
  71. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.test.ts +9 -0
  72. package/src/interceptors/ClientRequest/utils/getIncomingMessageBody.ts +9 -2
  73. package/src/interceptors/XMLHttpRequest/XMLHttpRequestOverride.ts +215 -159
  74. package/src/interceptors/XMLHttpRequest/index.ts +41 -23
  75. package/src/interceptors/fetch/index.ts +81 -55
  76. package/src/presets/browser.ts +3 -3
  77. package/src/presets/node.ts +3 -3
  78. package/src/utils/AsyncEventEmitter.test.ts +68 -0
  79. package/src/utils/AsyncEventEmitter.ts +171 -0
  80. package/src/utils/createLazyCallback.ts +49 -0
  81. package/src/utils/nextTick.ts +11 -0
  82. package/src/utils/toIsoResponse.ts +1 -1
  83. package/lib/createInterceptor.d.ts +0 -54
  84. package/lib/createInterceptor.js +0 -27
  85. package/lib/createInterceptor.js.map +0 -1
  86. package/lib/remote.d.ts +0 -21
  87. package/lib/remote.js +0 -178
  88. package/lib/remote.js.map +0 -1
  89. package/src/createInterceptor.ts +0 -100
  90. package/src/remote.ts +0 -174
@@ -0,0 +1,166 @@
1
+ import {
2
+ Interceptor,
3
+ getGlobalSymbol,
4
+ deleteGlobalSymbol,
5
+ InterceptorReadyState,
6
+ } from './Interceptor'
7
+ import { nextTickAsync } from './utils/nextTick'
8
+
9
+ const symbol = Symbol('test')
10
+
11
+ afterEach(() => {
12
+ deleteGlobalSymbol(symbol)
13
+ })
14
+
15
+ it('does not set a maximum listeners limit', () => {
16
+ const interceptor = new Interceptor(symbol)
17
+ expect(interceptor['emitter'].getMaxListeners()).toBe(0)
18
+ })
19
+
20
+ describe('persistence', () => {
21
+ it('stores global reference to the applied interceptor', () => {
22
+ const interceptor = new Interceptor(symbol)
23
+ interceptor.apply()
24
+
25
+ expect(getGlobalSymbol(symbol)).toEqual(interceptor)
26
+ })
27
+
28
+ it('deletes global reference when the interceptor is disposed', () => {
29
+ const interceptor = new Interceptor(symbol)
30
+
31
+ interceptor.apply()
32
+ interceptor.dispose()
33
+
34
+ expect(getGlobalSymbol(symbol)).toBeUndefined()
35
+ })
36
+ })
37
+
38
+ describe('readyState', () => {
39
+ it('sets the state to "IDLE" when the interceptor is created', () => {
40
+ const interceptor = new Interceptor(symbol)
41
+ expect(interceptor.readyState).toBe(InterceptorReadyState.IDLE)
42
+ })
43
+
44
+ it('leaves state as "IDLE" if the interceptor failed the environment check', async () => {
45
+ class MyInterceptor extends Interceptor<any> {
46
+ protected checkEnvironment(): boolean {
47
+ return false
48
+ }
49
+ }
50
+ const interceptor = new MyInterceptor(symbol)
51
+ interceptor.apply()
52
+
53
+ expect(interceptor.readyState).toBe(InterceptorReadyState.IDLE)
54
+
55
+ await nextTickAsync(() => {
56
+ expect(interceptor.readyState).toBe(InterceptorReadyState.IDLE)
57
+ })
58
+ })
59
+
60
+ it('perfroms state transition when the interceptor is applying', async () => {
61
+ const interceptor = new Interceptor(symbol)
62
+ interceptor.apply()
63
+
64
+ expect(interceptor.readyState).toBe(InterceptorReadyState.APPLYING)
65
+
66
+ await nextTickAsync(() => {
67
+ expect(interceptor.readyState).toBe(InterceptorReadyState.APPLIED)
68
+ })
69
+ })
70
+
71
+ it('perfroms state transition when disposing of the interceptor', async () => {
72
+ const interceptor = new Interceptor(symbol)
73
+ interceptor.apply()
74
+ interceptor.dispose()
75
+
76
+ expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSING)
77
+
78
+ await nextTickAsync(() => {
79
+ expect(interceptor.readyState).toBe(InterceptorReadyState.DISPOSED)
80
+ })
81
+ })
82
+ })
83
+
84
+ describe('apply', () => {
85
+ it('does not apply the same interceptor multiple times', () => {
86
+ const interceptor = new Interceptor(symbol)
87
+ const setupSpy = jest.spyOn(
88
+ interceptor,
89
+ // @ts-expect-error Protected property spy.
90
+ 'setup'
91
+ )
92
+
93
+ // Intentionally apply the same interceptor multiple times.
94
+ interceptor.apply()
95
+ interceptor.apply()
96
+ interceptor.apply()
97
+
98
+ // The "setup" must not be called repeatedly.
99
+ expect(setupSpy).toHaveBeenCalledTimes(1)
100
+
101
+ expect(getGlobalSymbol(symbol)).toEqual(interceptor)
102
+ })
103
+
104
+ it('does not call "apply" if the interceptor fails environment check', () => {
105
+ class MyInterceptor extends Interceptor<{}> {
106
+ checkEnvironment() {
107
+ return false
108
+ }
109
+ }
110
+
111
+ const interceptor = new MyInterceptor(Symbol('test'))
112
+ const setupSpy = jest.spyOn(
113
+ interceptor,
114
+ // @ts-expect-error Protected property spy.
115
+ 'setup'
116
+ )
117
+ interceptor.apply()
118
+
119
+ expect(setupSpy).not.toHaveBeenCalled()
120
+ })
121
+
122
+ it('proxies listeners from new interceptor to already running interceptor', () => {
123
+ const firstInterceptor = new Interceptor(symbol)
124
+ const secondInterceptor = new Interceptor(symbol)
125
+
126
+ firstInterceptor.apply()
127
+ const firstListener = jest.fn()
128
+ firstInterceptor.on('test', firstListener)
129
+
130
+ secondInterceptor.apply()
131
+ const secondListener = jest.fn()
132
+ secondInterceptor.on('test', secondListener)
133
+
134
+ // Emitting event in the first interceptor will bubble to the second one.
135
+ firstInterceptor['emitter'].emit('test', 'hello world')
136
+
137
+ expect(firstListener).toHaveBeenCalledTimes(1)
138
+ expect(firstListener).toHaveBeenCalledWith('hello world')
139
+
140
+ expect(secondListener).toHaveBeenCalledTimes(1)
141
+ expect(secondListener).toHaveBeenCalledWith('hello world')
142
+
143
+ expect(secondInterceptor['emitter'].listenerCount('test')).toBe(0)
144
+ })
145
+ })
146
+
147
+ describe('dispose', () => {
148
+ it('removes all listeners when the interceptor is disposed', async () => {
149
+ const interceptor = new Interceptor(symbol)
150
+
151
+ interceptor.apply()
152
+ const listener = jest.fn()
153
+ interceptor.on('test', listener)
154
+ interceptor.dispose()
155
+
156
+ // Even after emitting an event, the listener must not get called.
157
+ interceptor['emitter'].emit('test')
158
+ expect(listener).not.toHaveBeenCalled()
159
+
160
+ // The listener must not be called on the next tick either.
161
+ await nextTickAsync(() => {
162
+ interceptor['emitter'].emit('test')
163
+ expect(listener).not.toHaveBeenCalled()
164
+ })
165
+ })
166
+ })
@@ -0,0 +1,226 @@
1
+ import { Debugger, debug } from 'debug'
2
+ import { AsyncEventEmitter } from './utils/AsyncEventEmitter'
3
+ import { nextTick } from './utils/nextTick'
4
+
5
+ export type InterceptorEventMap = Record<string, (...args: any[]) => void>
6
+ export type InterceptorSubscription = () => void
7
+
8
+ export function getGlobalSymbol<V>(symbol: Symbol): V | undefined {
9
+ return (
10
+ // @ts-ignore https://github.com/Microsoft/TypeScript/issues/24587
11
+ globalThis[symbol] || undefined
12
+ )
13
+ }
14
+
15
+ function setGlobalSymbol(symbol: Symbol, value: any): void {
16
+ // @ts-ignore
17
+ globalThis[symbol] = value
18
+ }
19
+
20
+ export function deleteGlobalSymbol(symbol: Symbol): void {
21
+ // @ts-ignore
22
+ delete globalThis[symbol]
23
+ }
24
+
25
+ export enum InterceptorReadyState {
26
+ IDLE = 'IDLE',
27
+ APPLYING = 'APPLYING',
28
+ APPLIED = 'APPLIED',
29
+ DISPOSING = 'DISPOSING',
30
+ DISPOSED = 'DISPOSED',
31
+ }
32
+
33
+ export type ExtractEventNames<EventMap extends Record<string, any>> =
34
+ EventMap extends Record<infer EventName, any> ? EventName : never
35
+
36
+ export class Interceptor<EventMap extends InterceptorEventMap> {
37
+ protected emitter: AsyncEventEmitter<EventMap>
38
+ protected subscriptions: InterceptorSubscription[]
39
+ protected log: Debugger
40
+
41
+ public readyState: InterceptorReadyState
42
+
43
+ constructor(private readonly symbol: Symbol) {
44
+ this.readyState = InterceptorReadyState.IDLE
45
+
46
+ this.emitter = new AsyncEventEmitter()
47
+ this.subscriptions = []
48
+ this.log = debug(symbol.description!)
49
+
50
+ // Do not limit the maximum number of listeners
51
+ // so not to limit the maximum amount of parallel events emitted.
52
+ this.emitter.setMaxListeners(0)
53
+
54
+ this.log('constructing the interceptor...')
55
+ }
56
+
57
+ /**
58
+ * Determine if this interceptor can be applied
59
+ * in the current environment.
60
+ */
61
+ protected checkEnvironment(): boolean {
62
+ return true
63
+ }
64
+
65
+ /**
66
+ * Apply this interceptor to the current process.
67
+ * Returns an already running interceptor instance if it's present.
68
+ */
69
+ public apply(): void {
70
+ const log = this.log.extend('apply')
71
+ log('applying the interceptor...')
72
+
73
+ if (this.readyState === InterceptorReadyState.APPLIED) {
74
+ log('intercepted already applied!')
75
+ return
76
+ }
77
+
78
+ const shouldApply = this.checkEnvironment()
79
+
80
+ if (!shouldApply) {
81
+ log('the interceptor cannot be applied in this environment!')
82
+ return
83
+ }
84
+
85
+ this.readyState = InterceptorReadyState.APPLYING
86
+
87
+ // Always activate the emitter when applying the interceptor.
88
+ // This will ensure the interceptor can process events after it's
89
+ // been disposed and re-applied again (it may be a singleton).
90
+ this.emitter.activate()
91
+ log('activated the emiter!', this.emitter.readyState)
92
+
93
+ // Whenever applying a new interceptor, check if it hasn't been applied already.
94
+ // This enables to apply the same interceptor multiple times, for example from a different
95
+ // interceptor, only proxying events but keeping the stubs in a single place.
96
+ const runningInstance = this.getInstance()
97
+
98
+ if (runningInstance) {
99
+ log('found a running instance, reusing...')
100
+
101
+ // Proxy any listeners you set on this instance to the running instance.
102
+ this.on = (event, listener) => {
103
+ log('proxying the "%s" listener', event)
104
+
105
+ // Add listeners to the running instance so they appear
106
+ // at the top of the event listeners list and are executed first.
107
+ runningInstance.emitter.addListener(event, listener)
108
+
109
+ // Ensure that once this interceptor instance is disposed,
110
+ // it removes all listeners it has appended to the running interceptor instance.
111
+ this.subscriptions.push(() => {
112
+ runningInstance.emitter.removeListener(event, listener)
113
+ log('removed proxied "%s" listener!', event)
114
+ })
115
+ }
116
+
117
+ nextTick(() => {
118
+ this.readyState = InterceptorReadyState.APPLIED
119
+ })
120
+
121
+ return
122
+ }
123
+
124
+ log('no running instance found, setting up a new instance...')
125
+
126
+ // Setup the interceptor.
127
+ this.setup()
128
+
129
+ // Store the newly applied interceptor instance globally.
130
+ this.setInstance()
131
+
132
+ nextTick(() => {
133
+ this.readyState = InterceptorReadyState.APPLIED
134
+ })
135
+ }
136
+
137
+ /**
138
+ * Setup the module augments and stubs necessary for this interceptor.
139
+ * This method is not run if there's a running interceptor instance
140
+ * to prevent instantiating an interceptor multiple times.
141
+ */
142
+ protected setup(): void {}
143
+
144
+ /**
145
+ * Listen to the interceptor's public events.
146
+ */
147
+ public on<Event extends ExtractEventNames<EventMap>>(
148
+ event: Event,
149
+ listener: EventMap[Event]
150
+ ): void {
151
+ const log = this.log.extend('on')
152
+
153
+ if (
154
+ this.readyState === InterceptorReadyState.DISPOSING ||
155
+ this.readyState === InterceptorReadyState.DISPOSED
156
+ ) {
157
+ log('cannot listen to events, already disposed!')
158
+ return
159
+ }
160
+
161
+ log('adding "%s" event listener:', event, listener.name)
162
+
163
+ this.emitter.on(event, listener)
164
+ }
165
+
166
+ /**
167
+ * Disposes of any side-effects this interceptor has introduced.
168
+ */
169
+ public dispose(): void {
170
+ const log = this.log.extend('dispose')
171
+
172
+ if (this.readyState === InterceptorReadyState.DISPOSED) {
173
+ log('cannot dispose, already disposed!')
174
+ return
175
+ }
176
+
177
+ log('disposing the interceptor...')
178
+ this.readyState = InterceptorReadyState.DISPOSING
179
+
180
+ if (!this.getInstance()) {
181
+ log('no interceptors running, skipping dispose...')
182
+ return
183
+ }
184
+
185
+ // Delete the global symbol as soon as possible,
186
+ // indicating that the interceptor is no longer running.
187
+ this.clearInstance()
188
+
189
+ log('global symbol deleted:', getGlobalSymbol(this.symbol))
190
+
191
+ if (this.subscriptions.length > 0) {
192
+ log('disposing of %d subscriptions...', this.subscriptions.length)
193
+
194
+ for (const dispose of this.subscriptions) {
195
+ dispose()
196
+ }
197
+
198
+ this.subscriptions = []
199
+
200
+ log('disposed of all subscriptions!', this.subscriptions.length)
201
+ }
202
+
203
+ this.emitter.deactivate()
204
+ log('destroyed the listener!')
205
+
206
+ nextTick(() => {
207
+ this.readyState = InterceptorReadyState.DISPOSED
208
+ })
209
+ }
210
+
211
+ private getInstance(): this | undefined {
212
+ const instance = getGlobalSymbol<this>(this.symbol)
213
+ this.log('retrieved global instance:', instance?.constructor?.name)
214
+ return instance
215
+ }
216
+
217
+ private setInstance(): void {
218
+ setGlobalSymbol(this.symbol, this)
219
+ this.log('set global instance!', this.symbol.description)
220
+ }
221
+
222
+ private clearInstance(): void {
223
+ deleteGlobalSymbol(this.symbol)
224
+ this.log('cleared global instance!', this.symbol.description)
225
+ }
226
+ }
@@ -0,0 +1,176 @@
1
+ import { ChildProcess } from 'child_process'
2
+ import { Headers } from 'headers-polyfill'
3
+ import type {
4
+ HttpRequestEventMap,
5
+ InteractiveIsomorphicRequest,
6
+ IsomorphicRequest,
7
+ } from './glossary'
8
+ import { Interceptor } from './Interceptor'
9
+ import { BatchInterceptor } from './BatchInterceptor'
10
+ import { ClientRequestInterceptor } from './interceptors/ClientRequest'
11
+ import { XMLHttpRequestInterceptor } from './interceptors/XMLHttpRequest'
12
+ import { createLazyCallback } from './utils/createLazyCallback'
13
+ import { toIsoResponse } from './utils/toIsoResponse'
14
+
15
+ export class RemoteHttpInterceptor extends BatchInterceptor<
16
+ [ClientRequestInterceptor, XMLHttpRequestInterceptor]
17
+ > {
18
+ constructor() {
19
+ super({
20
+ name: 'remote-interceptor',
21
+ interceptors: [
22
+ new ClientRequestInterceptor(),
23
+ new XMLHttpRequestInterceptor(),
24
+ ],
25
+ })
26
+ }
27
+
28
+ protected setup() {
29
+ super.setup()
30
+
31
+ let handleParentMessage: NodeJS.MessageListener
32
+
33
+ this.on('request', async (request) => {
34
+ // Send the stringified intercepted request to
35
+ // the parent process where the remote resolver is established.
36
+ const serializedRequest = JSON.stringify(request)
37
+
38
+ this.log('sent serialized request to the child:', serializedRequest)
39
+ process.send?.(`request:${serializedRequest}`)
40
+
41
+ const responsePromise = new Promise<void>((resolve) => {
42
+ handleParentMessage = (message) => {
43
+ if (typeof message !== 'string') {
44
+ return resolve()
45
+ }
46
+
47
+ if (message.startsWith(`response:${request.id}`)) {
48
+ const [, serializedResponse] =
49
+ message.match(/^response:.+?:(.+)$/) || []
50
+
51
+ if (!serializedResponse) {
52
+ return resolve()
53
+ }
54
+
55
+ const mockedResponse = JSON.parse(serializedResponse)
56
+ request.respondWith(mockedResponse)
57
+ resolve()
58
+ }
59
+ }
60
+ })
61
+
62
+ // Listen for the mocked resopnse message from the parent.
63
+ this.log(
64
+ 'add "message" listener to the parent process',
65
+ handleParentMessage
66
+ )
67
+ process.addListener('message', handleParentMessage)
68
+
69
+ return responsePromise
70
+ })
71
+
72
+ this.subscriptions.push(() => {
73
+ process.removeListener('message', handleParentMessage)
74
+ })
75
+ }
76
+ }
77
+
78
+ export function requestReviver(key: string, value: any) {
79
+ switch (key) {
80
+ case 'url':
81
+ return new URL(value)
82
+
83
+ case 'headers':
84
+ return new Headers(value)
85
+
86
+ default:
87
+ return value
88
+ }
89
+ }
90
+
91
+ export interface RemoveResolverOptions {
92
+ process: ChildProcess
93
+ }
94
+
95
+ export class RemoteHttpResolver extends Interceptor<HttpRequestEventMap> {
96
+ static symbol = Symbol('remote-resolver')
97
+ private process: ChildProcess
98
+
99
+ constructor(options: RemoveResolverOptions) {
100
+ super(RemoteHttpResolver.symbol)
101
+ this.process = options.process
102
+ }
103
+
104
+ protected setup() {
105
+ const log = this.log.extend('setup')
106
+
107
+ const handleChildMessage: NodeJS.MessageListener = async (message) => {
108
+ log('received message from child!', message)
109
+
110
+ if (typeof message !== 'string' || !message.startsWith('request:')) {
111
+ log('unknown message, ignoring...')
112
+ return
113
+ }
114
+
115
+ const [, serializedRequest] = message.match(/^request:(.+)$/) || []
116
+
117
+ if (!serializedRequest) {
118
+ return
119
+ }
120
+
121
+ const isomorphicRequest: IsomorphicRequest = JSON.parse(
122
+ serializedRequest,
123
+ requestReviver
124
+ )
125
+
126
+ log('parsed intercepted request', isomorphicRequest)
127
+
128
+ const interactiveIsomorphicRequest: InteractiveIsomorphicRequest = {
129
+ ...isomorphicRequest,
130
+ respondWith: createLazyCallback(),
131
+ }
132
+
133
+ this.emitter.emit('request', interactiveIsomorphicRequest)
134
+ await this.emitter.untilIdle('request')
135
+ const [mockedResponse] =
136
+ await interactiveIsomorphicRequest.respondWith.invoked()
137
+
138
+ log('event.respondWith called with:', mockedResponse)
139
+
140
+ // Send the mocked response to the child process.
141
+ const serializedResponse = JSON.stringify(mockedResponse)
142
+
143
+ this.process.send(
144
+ `response:${isomorphicRequest.id}:${serializedResponse}`,
145
+ (error) => {
146
+ if (error) {
147
+ return
148
+ }
149
+
150
+ if (mockedResponse) {
151
+ // Emit an optimistinc "response" event at this point,
152
+ // not to rely on the back-and-forth signaling for the sake of the event.
153
+ this.emitter.emit(
154
+ 'response',
155
+ isomorphicRequest,
156
+ toIsoResponse(mockedResponse)
157
+ )
158
+ }
159
+ }
160
+ )
161
+
162
+ log('sent serialized mocked response to the parent:', serializedResponse)
163
+ }
164
+
165
+ this.subscriptions.push(() => {
166
+ this.process.removeListener('message', handleChildMessage)
167
+ log('removed the "message" listener from the child process!')
168
+ })
169
+
170
+ log('adding a "message" listener to the child process')
171
+ this.process.addListener('message', handleChildMessage)
172
+
173
+ this.process.once('error', () => this.dispose())
174
+ this.process.once('exit', () => this.dispose())
175
+ }
176
+ }
@@ -0,0 +1,42 @@
1
+ import type { HeadersObject, Headers } from 'headers-polyfill'
2
+ import type { LazyCallback } from './utils/createLazyCallback'
3
+
4
+ export type RequestCredentials = 'omit' | 'include' | 'same-origin'
5
+
6
+ export interface IsomorphicRequest {
7
+ id: string
8
+ url: URL
9
+ method: string
10
+ headers: Headers
11
+ /**
12
+ * The value of the request client's "credentials" option
13
+ * or a compatible alternative (i.e. `withCredentials` for `XMLHttpRequest`).
14
+ * Always equals to "omit" in Node.js.
15
+ */
16
+ credentials: RequestCredentials
17
+ body?: string
18
+ }
19
+
20
+ export interface InteractiveIsomorphicRequest extends IsomorphicRequest {
21
+ respondWith: LazyCallback<(mockedResponse: MockedResponse) => void>
22
+ }
23
+
24
+ export interface IsomorphicResponse {
25
+ status: number
26
+ statusText: string
27
+ headers: Headers
28
+ body?: string
29
+ }
30
+
31
+ export interface MockedResponse
32
+ extends Omit<Partial<IsomorphicResponse>, 'headers'> {
33
+ headers?: HeadersObject
34
+ }
35
+
36
+ export type HttpRequestEventMap = {
37
+ request(request: InteractiveIsomorphicRequest): Promise<void> | void
38
+ response(
39
+ request: IsomorphicRequest,
40
+ response: IsomorphicResponse
41
+ ): Promise<void> | void
42
+ }
package/src/index.ts CHANGED
@@ -1,5 +1,5 @@
1
- export * from './createInterceptor'
2
- export * from './remote'
1
+ export * from './glossary'
2
+ export * from './RemoteInterceptor'
3
3
 
4
4
  /* Utils */
5
5
  export { getCleanUrl } from './utils/getCleanUrl'