|
1 |
| -import { |
2 |
| - BatchInterceptor, |
3 |
| - HttpRequestEventMap, |
4 |
| - Interceptor, |
5 |
| - InterceptorReadyState, |
6 |
| -} from '@mswjs/interceptors' |
7 |
| -import { invariant } from 'outvariant' |
8 |
| -import { SetupApi } from '~/core/SetupApi' |
9 |
| -import { RequestHandler } from '~/core/handlers/RequestHandler' |
10 |
| -import { LifeCycleEventsMap, SharedOptions } from '~/core/sharedOptions' |
11 |
| -import { RequiredDeep } from '~/core/typeUtils' |
12 |
| -import { handleRequest } from '~/core/utils/handleRequest' |
13 |
| -import { devUtils } from '~/core/utils/internal/devUtils' |
14 |
| -import { mergeRight } from '~/core/utils/internal/mergeRight' |
15 |
| -import { SetupServer } from './glossary' |
| 1 | +import { AsyncLocalStorage } from 'node:async_hooks' |
| 2 | +import { ClientRequestInterceptor } from '@mswjs/interceptors/ClientRequest' |
| 3 | +import { XMLHttpRequestInterceptor } from '@mswjs/interceptors/XMLHttpRequest' |
| 4 | +import { FetchInterceptor } from '@mswjs/interceptors/fetch' |
| 5 | +import { HandlersController } from '~/core/SetupApi' |
| 6 | +import type { RequestHandler } from '~/core/handlers/RequestHandler' |
| 7 | +import type { SetupServer } from './glossary' |
| 8 | +import { SetupServerCommonApi } from './SetupServerCommonApi' |
16 | 9 |
|
17 |
| -const DEFAULT_LISTEN_OPTIONS: RequiredDeep<SharedOptions> = { |
18 |
| - onUnhandledRequest: 'warn', |
19 |
| -} |
20 |
| - |
21 |
| -export class SetupServerApi |
22 |
| - extends SetupApi<LifeCycleEventsMap> |
23 |
| - implements SetupServer |
24 |
| -{ |
25 |
| - protected readonly interceptor: BatchInterceptor< |
26 |
| - Array<Interceptor<HttpRequestEventMap>>, |
27 |
| - HttpRequestEventMap |
28 |
| - > |
29 |
| - private resolvedOptions: RequiredDeep<SharedOptions> |
| 10 | +const store = new AsyncLocalStorage<RequestHandlersContext>() |
30 | 11 |
|
31 |
| - constructor( |
32 |
| - interceptors: Array<{ |
33 |
| - new (): Interceptor<HttpRequestEventMap> |
34 |
| - }>, |
35 |
| - ...handlers: Array<RequestHandler> |
36 |
| - ) { |
37 |
| - super(...handlers) |
| 12 | +type RequestHandlersContext = { |
| 13 | + initialHandlers: Array<RequestHandler> |
| 14 | + handlers: Array<RequestHandler> |
| 15 | +} |
38 | 16 |
|
39 |
| - this.interceptor = new BatchInterceptor({ |
40 |
| - name: 'setup-server', |
41 |
| - interceptors: interceptors.map((Interceptor) => new Interceptor()), |
42 |
| - }) |
43 |
| - this.resolvedOptions = {} as RequiredDeep<SharedOptions> |
| 17 | +/** |
| 18 | + * A handlers controller that utilizes `AsyncLocalStorage` in Node.js |
| 19 | + * to prevent the request handlers list from being a shared state |
| 20 | + * across mutliple tests. |
| 21 | + */ |
| 22 | +class AsyncHandlersController implements HandlersController { |
| 23 | + private rootContext: RequestHandlersContext |
44 | 24 |
|
45 |
| - this.init() |
| 25 | + constructor(initialHandlers: Array<RequestHandler>) { |
| 26 | + this.rootContext = { initialHandlers, handlers: [] } |
46 | 27 | }
|
47 | 28 |
|
48 |
| - /** |
49 |
| - * Subscribe to all requests that are using the interceptor object |
50 |
| - */ |
51 |
| - private init(): void { |
52 |
| - this.interceptor.on('request', async ({ request, requestId }) => { |
53 |
| - const response = await handleRequest( |
54 |
| - request, |
55 |
| - requestId, |
56 |
| - this.currentHandlers, |
57 |
| - this.resolvedOptions, |
58 |
| - this.emitter, |
59 |
| - ) |
60 |
| - |
61 |
| - if (response) { |
62 |
| - request.respondWith(response) |
63 |
| - } |
| 29 | + get context(): RequestHandlersContext { |
| 30 | + return store.getStore() || this.rootContext |
| 31 | + } |
64 | 32 |
|
65 |
| - return |
66 |
| - }) |
| 33 | + public prepend(runtimeHandlers: Array<RequestHandler>) { |
| 34 | + this.context.handlers.unshift(...runtimeHandlers) |
| 35 | + } |
67 | 36 |
|
68 |
| - this.interceptor.on( |
69 |
| - 'response', |
70 |
| - ({ response, isMockedResponse, request, requestId }) => { |
71 |
| - this.emitter.emit( |
72 |
| - isMockedResponse ? 'response:mocked' : 'response:bypass', |
73 |
| - { |
74 |
| - response, |
75 |
| - request, |
76 |
| - requestId, |
77 |
| - }, |
78 |
| - ) |
79 |
| - }, |
80 |
| - ) |
| 37 | + public reset(nextHandlers: Array<RequestHandler>) { |
| 38 | + const context = this.context |
| 39 | + context.handlers = [] |
| 40 | + context.initialHandlers = |
| 41 | + nextHandlers.length > 0 ? nextHandlers : context.initialHandlers |
81 | 42 | }
|
82 | 43 |
|
83 |
| - public listen(options: Partial<SharedOptions> = {}): void { |
84 |
| - this.resolvedOptions = mergeRight( |
85 |
| - DEFAULT_LISTEN_OPTIONS, |
86 |
| - options, |
87 |
| - ) as RequiredDeep<SharedOptions> |
| 44 | + public currentHandlers(): Array<RequestHandler> { |
| 45 | + const { initialHandlers, handlers } = this.context |
| 46 | + return handlers.concat(initialHandlers) |
| 47 | + } |
| 48 | +} |
88 | 49 |
|
89 |
| - // Apply the interceptor when starting the server. |
90 |
| - this.interceptor.apply() |
| 50 | +export class SetupServerApi |
| 51 | + extends SetupServerCommonApi |
| 52 | + implements SetupServer |
| 53 | +{ |
| 54 | + constructor(handlers: Array<RequestHandler>) { |
| 55 | + super( |
| 56 | + [ClientRequestInterceptor, XMLHttpRequestInterceptor, FetchInterceptor], |
| 57 | + handlers, |
| 58 | + ) |
91 | 59 |
|
92 |
| - this.subscriptions.push(() => { |
93 |
| - this.interceptor.dispose() |
94 |
| - }) |
| 60 | + this.handlersController = new AsyncHandlersController(handlers) |
| 61 | + } |
95 | 62 |
|
96 |
| - // Assert that the interceptor has been applied successfully. |
97 |
| - // Also guards us from forgetting to call "interceptor.apply()" |
98 |
| - // as a part of the "listen" method. |
99 |
| - invariant( |
100 |
| - [InterceptorReadyState.APPLYING, InterceptorReadyState.APPLIED].includes( |
101 |
| - this.interceptor.readyState, |
102 |
| - ), |
103 |
| - devUtils.formatMessage( |
104 |
| - 'Failed to start "setupServer": the interceptor failed to apply. This is likely an issue with the library and you should report it at "%s".', |
105 |
| - ), |
106 |
| - 'https://github.com/mswjs/msw/issues/new/choose', |
107 |
| - ) |
| 63 | + public boundary<Fn extends (...args: Array<any>) => unknown>( |
| 64 | + callback: Fn, |
| 65 | + ): (...args: Parameters<Fn>) => ReturnType<Fn> { |
| 66 | + return (...args: Parameters<Fn>): ReturnType<Fn> => { |
| 67 | + return store.run<any, any>( |
| 68 | + { |
| 69 | + initialHandlers: this.handlersController.currentHandlers(), |
| 70 | + handlers: [], |
| 71 | + }, |
| 72 | + callback, |
| 73 | + ...args, |
| 74 | + ) |
| 75 | + } |
108 | 76 | }
|
109 | 77 |
|
110 | 78 | public close(): void {
|
111 |
| - this.dispose() |
| 79 | + super.close() |
| 80 | + store.disable() |
112 | 81 | }
|
113 | 82 | }
|
0 commit comments