msw
Version:
227 lines (203 loc) • 7.48 kB
text/typescript
import { invariant } from 'outvariant'
import { isNodeProcess } from 'is-node-process'
import {
SetupWorkerInternalContext,
ServiceWorkerIncomingEventsMap,
StartReturnType,
StopHandler,
StartHandler,
StartOptions,
} from './glossary'
import { createStartHandler } from './start/createStartHandler'
import { createStop } from './stop/createStop'
import { ServiceWorkerMessage } from './start/utils/createMessageChannel'
import { RequestHandler } from '~/core/handlers/RequestHandler'
import { DEFAULT_START_OPTIONS } from './start/utils/prepareStartHandler'
import { createFallbackStart } from './start/createFallbackStart'
import { createFallbackStop } from './stop/createFallbackStop'
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 { SetupWorker } from './glossary'
import { supportsReadableStreamTransfer } from '../utils/supportsReadableStreamTransfer'
import { webSocketInterceptor } from '~/core/ws/webSocketInterceptor'
import { handleWebSocketEvent } from '~/core/ws/handleWebSocketEvent'
import { attachWebSocketLogger } from '~/core/ws/utils/attachWebSocketLogger'
interface Listener {
target: EventTarget
eventType: string
callback: EventListenerOrEventListenerObject
}
export class SetupWorkerApi
extends SetupApi<LifeCycleEventsMap>
implements SetupWorker
{
private context: SetupWorkerInternalContext
private startHandler: StartHandler = null as any
private stopHandler: StopHandler = null as any
private listeners: Array<Listener>
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.listeners = []
this.context = this.createWorkerContext()
}
private createWorkerContext(): SetupWorkerInternalContext {
const context: SetupWorkerInternalContext = {
// Mocking is not considered enabled until the worker
// signals back the successful activation event.
isMockingEnabled: false,
startOptions: null as any,
worker: null,
getRequestHandlers: () => {
return this.handlersController.currentHandlers()
},
registration: null,
emitter: this.emitter,
workerChannel: {
on: (eventType, callback) => {
this.context.events.addListener<
MessageEvent<ServiceWorkerMessage<typeof eventType, any>>
>(navigator.serviceWorker, 'message', (event) => {
// Avoid messages broadcasted from unrelated workers.
if (event.source !== this.context.worker) {
return
}
const message = event.data
if (!message) {
return
}
if (message.type === eventType) {
callback(event, message)
}
})
},
send: (type) => {
this.context.worker?.postMessage(type)
},
},
events: {
addListener: (target, eventType, callback) => {
target.addEventListener(eventType, callback as EventListener)
this.listeners.push({
eventType,
target,
callback: callback as EventListener,
})
return () => {
target.removeEventListener(eventType, callback as EventListener)
}
},
removeAllListeners: () => {
for (const { target, eventType, callback } of this.listeners) {
target.removeEventListener(eventType, callback)
}
this.listeners = []
},
once: (eventType) => {
const bindings: Array<() => void> = []
return new Promise<
ServiceWorkerMessage<
typeof eventType,
ServiceWorkerIncomingEventsMap[typeof eventType]
>
>((resolve, reject) => {
const handleIncomingMessage = (event: MessageEvent) => {
try {
const message = event.data
if (message.type === eventType) {
resolve(message)
}
} catch (error) {
reject(error)
}
}
bindings.push(
this.context.events.addListener(
navigator.serviceWorker,
'message',
handleIncomingMessage,
),
this.context.events.addListener(
navigator.serviceWorker,
'messageerror',
reject,
),
)
}).finally(() => {
bindings.forEach((unbind) => unbind())
})
},
},
supports: {
serviceWorkerApi:
!('serviceWorker' in navigator) || location.protocol === 'file:',
readableStreamTransfer: supportsReadableStreamTransfer(),
},
}
this.startHandler = context.supports.serviceWorkerApi
? createFallbackStart(context)
: createStartHandler(context)
this.stopHandler = context.supports.serviceWorkerApi
? createFallbackStop(context)
: createStop(context)
return context
}
public async start(options: StartOptions = {}): StartReturnType {
if (options.waitUntilReady === true) {
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.',
)
}
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()
})
return await this.startHandler(this.context.startOptions, options)
}
public stop(): void {
super.dispose()
this.context.events.removeAllListeners()
this.context.emitter.removeAllListeners()
this.stopHandler()
}
}
/**
* 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)
}