msw
Version:
185 lines (157 loc) • 6.42 kB
text/typescript
import { invariant } from 'outvariant'
import { isNodeProcess } from 'is-node-process'
import { DeferredPromise } from '@open-draft/deferred-promise'
import type {
SetupWorkerInternalContext,
StartReturnType,
StartOptions,
SetupWorker,
} from './glossary'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler'
import { createStartHandler } from './start/createStartHandler'
import { devUtils } from '~/core/utils/internal/devUtils'
import { SetupApi } from '~/core/SetupApi'
import { mergeRight } from '~/core/utils/internal/mergeRight'
import type { LifeCycleEventsMap } from '~/core/sharedOptions'
import type { WebSocketHandler } from '~/core/handlers/WebSocketHandler'
import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent'
import { attachWebSocketLogger } from '~/core/ws/utils/attachWebSocketLogger'
import { WorkerChannel } from '../utils/workerChannel'
import { createFallbackRequestListener } from './start/createFallbackRequestListener'
import { printStartMessage } from './start/utils/printStartMessage'
import { printStopMessage } from './stop/utils/printStopMessage'
import { supportsServiceWorker } from '../utils/supports'
export class SetupWorkerApi
extends SetupApi<LifeCycleEventsMap>
implements SetupWorker
{
private context: SetupWorkerInternalContext
constructor(...handlers: Array<RequestHandler | WebSocketHandler>) {
super(...handlers)
invariant(
!isNodeProcess(),
devUtils.formatMessage(
'Failed to execute `setupWorker` in a non-browser environment. Consider using `setupServer` for Node.js environment instead.',
),
)
this.context = this.createWorkerContext()
}
private createWorkerContext(): SetupWorkerInternalContext {
const workerPromise = new DeferredPromise<ServiceWorker>()
return {
// Mocking is not considered enabled until the worker
// signals back the successful activation event.
isMockingEnabled: false,
startOptions: null as any,
workerPromise,
registration: undefined,
getRequestHandlers: () => {
return this.handlersController.currentHandlers()
},
emitter: this.emitter,
workerChannel: new WorkerChannel({
worker: workerPromise,
}),
}
}
public async start(options: StartOptions = {}): StartReturnType {
if ('waitUntilReady' in options) {
devUtils.warn(
'The "waitUntilReady" option has been deprecated. Please remove it from this "worker.start()" call. Follow the recommended Browser integration (https://mswjs.io/docs/integrations/browser) to eliminate any race conditions between the Service Worker registration and any requests made by your application on initial render.',
)
}
// Warn the developer on multiple "worker.start()" calls.
// While this will not affect the worker in any way,
// it likely indicates an issue with the developer's code.
if (this.context.isMockingEnabled) {
devUtils.warn(
`Found a redundant "worker.start()" call. Note that starting the worker while mocking is already enabled will have no effect. Consider removing this "worker.start()" call.`,
)
return this.context.registration
}
this.context.workerStoppedAt = undefined
this.context.startOptions = mergeRight(
DEFAULT_START_OPTIONS,
options,
) as SetupWorkerInternalContext['startOptions']
// Enable the WebSocket interception.
handleWebSocketEvent({
getUnhandledRequestStrategy: () => {
return this.context.startOptions.onUnhandledRequest
},
getHandlers: () => {
return this.handlersController.currentHandlers()
},
onMockedConnection: (connection) => {
if (!this.context.startOptions.quiet) {
// Attach the logger for mocked connections since
// those won't be visible in the browser's devtools.
attachWebSocketLogger(connection)
}
},
onPassthroughConnection() {},
})
webSocketInterceptor.apply()
this.subscriptions.push(() => {
webSocketInterceptor.dispose()
})
// Use a fallback interception algorithm in the environments
// where the Service Worker API isn't supported.
if (!supportsServiceWorker()) {
const fallbackInterceptor = createFallbackRequestListener(
this.context,
this.context.startOptions,
)
this.subscriptions.push(() => {
fallbackInterceptor.dispose()
})
this.context.isMockingEnabled = true
printStartMessage({
message: 'Mocking enabled (fallback mode).',
quiet: this.context.startOptions.quiet,
})
return undefined
}
const startHandler = createStartHandler(this.context)
const registration = await startHandler(this.context.startOptions, options)
this.context.isMockingEnabled = true
return registration
}
public stop(): void {
super.dispose()
if (!this.context.isMockingEnabled) {
devUtils.warn(
'Found a redundant "worker.stop()" call. Notice that stopping the worker after it has already been stopped has no effect. Consider removing this "worker.stop()" call.',
)
return
}
this.context.isMockingEnabled = false
this.context.workerStoppedAt = Date.now()
this.context.emitter.removeAllListeners()
if (supportsServiceWorker()) {
this.context.workerChannel.removeAllListeners('RESPONSE')
window.clearInterval(this.context.keepAliveInterval)
}
// Post the internal stop message on the window
// to let any logic know when the worker has stopped.
// E.g. the WebSocket client manager needs this to know
// when to clear its in-memory clients list.
window.postMessage({ type: 'msw/worker:stop' })
printStopMessage({
quiet: this.context.startOptions?.quiet,
})
}
}
/**
* Sets up a requests interception in the browser with the given request handlers.
* @param {RequestHandler[]} handlers List of request handlers.
*
* @see {@link https://mswjs.io/docs/api/setup-worker `setupWorker()` API reference}
*/
export function setupWorker(
...handlers: Array<RequestHandler | WebSocketHandler>
): SetupWorker {
return new SetupWorkerApi(...handlers)
}