import type { CloneOption, DefineWorkerOptions, Procedure } from './types' import { startWebWorkerModuleRunner } from './runner' import { createMessageEvent, debug, getFileIdFromUrl } from './utils' export function createWorkerConstructor(options?: DefineWorkerOptions): typeof Worker { const cloneType = () => (options?.clone ?? process.env.VITEST_WEB_WORKER_CLONE ?? 'native') as CloneOption return class Worker extends EventTarget { static __VITEST_WEB_WORKER__ = true private _vw_workerTarget = new EventTarget() private _vw_insideListeners = new Map() private _vw_outsideListeners = new Map() private _vw_name: string private _vw_messageQueue: any[] | null = [] public onmessage: null | Procedure = null public onmessageerror: null | Procedure = null public onerror: null | Procedure = null constructor(url: URL | string, options?: WorkerOptions) { super() let selfProxy: typeof globalThis // should be in sync with DedicatedWorkerGlobalScope, but without globalThis const context = { onmessage: null as null | ((e: MessageEvent) => void), onmessageerror: null, onerror: null, onlanguagechange: null, onoffline: null, ononline: null, onrejectionhandled: null, onrtctransform: null, onunhandledrejection: null, origin: typeof location !== 'undefined' ? location.origin : 'http://localhost:3000', crossOriginIsolated: false, name: options?.name || '', close: () => this.terminate(), dispatchEvent: (event: Event) => { return this._vw_workerTarget.dispatchEvent(event) }, addEventListener: (...args: any[]) => { if (args[1]) { this._vw_insideListeners.set(args[0], args[1]) } return this._vw_workerTarget.addEventListener(...(args as [any, any])) }, removeEventListener: (...args: any[]) => { return this._vw_workerTarget.removeEventListener(...(args as [any, any])) }, postMessage: (...args: any[]) => { if (!args.length) { throw new SyntaxError('"postMessage" requires at least one argument.') } debug('posting message %o from the worker %s to the main thread', args[0], this._vw_name) const event = createMessageEvent(args[0], args[1], cloneType()) this.dispatchEvent(event) }, get self() { return selfProxy }, } selfProxy = new Proxy(context, { get(target, prop, receiver) { if (Reflect.has(target, prop)) { return Reflect.get(target, prop, receiver) } return globalThis[prop as 'crypto'] }, }) as any this._vw_workerTarget.addEventListener('message', (e) => { context.onmessage?.(e as MessageEvent) }) this.addEventListener('message', (e) => { this.onmessage?.(e) }) this.addEventListener('messageerror', (e) => { this.onmessageerror?.(e) }) const fileId = getFileIdFromUrl(url) this._vw_name = fileId const runner = startWebWorkerModuleRunner(context) runner.mocker .resolveId(fileId) .then(({ url, id: resolvedId }) => { this._vw_name = options?.name ?? url debug('initialize worker %s', this._vw_name) return runner.import(url).then(() => { runner._invalidateSubTreeById([resolvedId, runner.mocker.getMockPath(resolvedId)]) const q = this._vw_messageQueue this._vw_messageQueue = null if (q) { q.forEach(([data, transfer]) => this.postMessage(data, transfer), this) } debug('worker %s successfully initialized', this._vw_name) }) }) .catch((e) => { debug('worker %s failed to initialize: %o', this._vw_name, e) const EventConstructor = globalThis.ErrorEvent || globalThis.Event const error = new EventConstructor('error', { error: e, message: e.message, }) this.dispatchEvent(error) this.onerror?.(error) console.error(e) }) } addEventListener( type: string, callback: EventListenerOrEventListenerObject | null, options?: boolean | AddEventListenerOptions, ): void { if (callback) { this._vw_outsideListeners.set(type, callback) } return super.addEventListener(type, callback, options) } postMessage(...args: [any, StructuredSerializeOptions | Transferable[] | undefined]): void { if (!args.length) { throw new SyntaxError('"postMessage" requires at least one argument.') } const [data, transferOrOptions] = args if (this._vw_messageQueue != null) { debug('worker %s is not yet initialized, queue message %s', this._vw_name, data) this._vw_messageQueue.push([data, transferOrOptions]) return } debug('posting message %o from the main thread to the worker %s', data, this._vw_name) const event = createMessageEvent(data, transferOrOptions, cloneType()) if (event.type === 'messageerror') { this.dispatchEvent(event) } else { this._vw_workerTarget.dispatchEvent(event) } } terminate() { debug('terminating worker %s', this._vw_name) this._vw_outsideListeners.forEach((fn, type) => { this.removeEventListener(type, fn) }) this._vw_insideListeners.forEach((fn, type) => { this._vw_workerTarget.removeEventListener(type, fn) }) } } }