Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Browse files
Browse the repository at this point in the history
feat!: make web-worker implementation more compatible with spec (#2431)
* fix: make web-worker implementation more compatible with spec * chore: cleanup * test: add more tests for web-worker * chore: debug messageerror in web-worker * chore: relax requirements for web worker * chore: update lockfile * feat(web-worker): refactor into small peaces, add SharedWorker support * chore: update lockfile * chore: cleanup * chore: merge with main * chore: fix reference error
- Loading branch information
1 parent
084e929
commit c3a6352
Showing
22 changed files
with
719 additions
and
189 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,8 @@ | ||
declare function defineWebWorker(): void; | ||
type CloneOption = 'native' | 'ponyfill' | 'none'; | ||
interface DefineWorkerOptions { | ||
clone: CloneOption; | ||
} | ||
|
||
export { defineWebWorker }; | ||
declare function defineWebWorkers(options?: DefineWorkerOptions): void; | ||
|
||
export { defineWebWorkers }; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,3 +1,3 @@ | ||
import { defineWebWorker } from './pure' | ||
import { defineWebWorkers } from './pure' | ||
|
||
defineWebWorker() | ||
defineWebWorkers() |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,168 +1,19 @@ | ||
/* eslint-disable no-restricted-imports */ | ||
import { VitestRunner } from 'vitest/node' | ||
import type { WorkerGlobalState } from 'vitest' | ||
import { createWorkerConstructor } from './worker' | ||
import type { DefineWorkerOptions } from './types' | ||
import { assertGlobalExists } from './utils' | ||
import { createSharedWorkerConstructor } from './shared-worker' | ||
|
||
function getWorkerState(): WorkerGlobalState { | ||
// @ts-expect-error untyped global | ||
return globalThis.__vitest_worker__ | ||
} | ||
|
||
type Procedure = (...args: any[]) => void | ||
|
||
class Bridge { | ||
private callbacks: Record<string, Procedure[]> = {} | ||
|
||
public on(event: string, fn: Procedure) { | ||
this.callbacks[event] ??= [] | ||
this.callbacks[event].push(fn) | ||
} | ||
|
||
public off(event: string, fn: Procedure) { | ||
if (this.callbacks[event]) | ||
this.callbacks[event] = this.callbacks[event].filter(f => f !== fn) | ||
} | ||
|
||
public removeEvents(event: string) { | ||
this.callbacks[event] = [] | ||
} | ||
|
||
public clear() { | ||
this.callbacks = {} | ||
} | ||
|
||
public emit(event: string, ...data: any[]) { | ||
return (this.callbacks[event] || []).map(fn => fn(...data)) | ||
} | ||
} | ||
|
||
interface InlineWorkerContext { | ||
onmessage: Procedure | null | ||
dispatchEvent: (e: Event) => void | ||
addEventListener: (e: string, fn: Procedure) => void | ||
removeEventListener: (e: string, fn: Procedure) => void | ||
postMessage: (data: any) => void | ||
self: InlineWorkerContext | ||
global: InlineWorkerContext | ||
importScripts?: any | ||
} | ||
|
||
class InlineWorkerRunner extends VitestRunner { | ||
constructor(options: any, private context: InlineWorkerContext) { | ||
super(options) | ||
} | ||
export function defineWebWorkers(options?: DefineWorkerOptions) { | ||
if (typeof Worker === 'undefined' || !('__VITEST_WEB_WORKER__' in globalThis.Worker)) { | ||
assertGlobalExists('EventTarget') | ||
assertGlobalExists('MessageEvent') | ||
|
||
prepareContext(context: Record<string, any>) { | ||
const ctx = super.prepareContext(context) | ||
// not supported for now | ||
// need to be async | ||
this.context.self.importScripts = () => {} | ||
return Object.assign(ctx, this.context, { | ||
importScripts: () => {}, | ||
}) | ||
globalThis.Worker = createWorkerConstructor(options) | ||
} | ||
} | ||
|
||
export function defineWebWorker() { | ||
if ('Worker' in globalThis) | ||
return | ||
|
||
const { config, rpc, mockMap, moduleCache } = getWorkerState() | ||
|
||
const options = { | ||
fetchModule(id: string) { | ||
return rpc.fetch(id) | ||
}, | ||
resolveId(id: string, importer?: string) { | ||
return rpc.resolveId(id, importer) | ||
}, | ||
moduleCache, | ||
mockMap, | ||
interopDefault: config.deps.interopDefault ?? true, | ||
root: config.root, | ||
base: config.base, | ||
} | ||
|
||
globalThis.Worker = class Worker { | ||
private inside = new Bridge() | ||
private outside = new Bridge() | ||
|
||
private messageQueue: any[] | null = [] | ||
|
||
public onmessage: null | Procedure = null | ||
public onmessageerror: null | Procedure = null | ||
public onerror: null | Procedure = null | ||
|
||
constructor(url: URL | string) { | ||
const context: InlineWorkerContext = { | ||
onmessage: null, | ||
dispatchEvent: (event: Event) => { | ||
this.inside.emit(event.type, event) | ||
return true | ||
}, | ||
addEventListener: this.inside.on.bind(this.inside), | ||
removeEventListener: this.inside.off.bind(this.inside), | ||
postMessage: (data) => { | ||
this.outside.emit('message', { data }) | ||
}, | ||
get self() { | ||
return context | ||
}, | ||
get global() { | ||
return context | ||
}, | ||
} | ||
|
||
this.inside.on('message', (e) => { | ||
context.onmessage?.(e) | ||
}) | ||
|
||
this.outside.on('message', (e) => { | ||
this.onmessage?.(e) | ||
}) | ||
|
||
const runner = new InlineWorkerRunner(options, context) | ||
|
||
const id = (url instanceof URL ? url.toString() : url).replace(/^file:\/+/, '/') | ||
|
||
runner.resolveUrl(id).then(([, fsPath]) => { | ||
runner.executeFile(fsPath).then(() => { | ||
// worker should be new every time, invalidate its sub dependency | ||
moduleCache.invalidateSubDepTree([fsPath, runner.mocker.getMockPath(fsPath)]) | ||
const q = this.messageQueue | ||
this.messageQueue = null | ||
if (q) | ||
q.forEach(this.postMessage, this) | ||
}).catch((e) => { | ||
this.outside.emit('error', e) | ||
this.onerror?.(e) | ||
console.error(e) | ||
}) | ||
}) | ||
} | ||
|
||
dispatchEvent(event: Event) { | ||
this.outside.emit(event.type, event) | ||
return true | ||
} | ||
|
||
addEventListener(event: string, fn: Procedure) { | ||
this.outside.on(event, fn) | ||
} | ||
|
||
removeEventListener(event: string, fn: Procedure) { | ||
this.outside.off(event, fn) | ||
} | ||
|
||
postMessage(data: any) { | ||
if (this.messageQueue != null) | ||
this.messageQueue.push(data) | ||
else | ||
this.inside.emit('message', { data }) | ||
} | ||
if (typeof SharedWorker === 'undefined' || !('__VITEST_WEB_WORKER__' in globalThis.SharedWorker)) { | ||
assertGlobalExists('EventTarget') | ||
|
||
terminate() { | ||
this.outside.clear() | ||
this.inside.clear() | ||
} | ||
globalThis.SharedWorker = createSharedWorkerConstructor() | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { VitestRunner } from 'vitest/node' | ||
|
||
export class InlineWorkerRunner extends VitestRunner { | ||
constructor(options: any, private context: any) { | ||
super(options) | ||
} | ||
|
||
prepareContext(context: Record<string, any>) { | ||
const ctx = super.prepareContext(context) | ||
// not supported for now, we can't synchronously load modules | ||
const importScripts = () => { | ||
throw new Error('[vitest] `importScripts` is not supported in Vite workers. Please, consider using `import` instead.') | ||
} | ||
return Object.assign(ctx, this.context, { | ||
importScripts, | ||
}) | ||
} | ||
} |
Oops, something went wrong.