@xylabs/threads
Version:
Web workers & worker threads as simple as a function call
154 lines (130 loc) • 4.91 kB
text/typescript
/// <reference lib="webworker" />
/* eslint-disable import-x/no-internal-modules */
/* eslint-disable @typescript-eslint/no-explicit-any */
/*
* This source file contains the code for proxying calls in the master thread to calls in the workers
* by `.postMessage()`-ing.
*
* Keep in mind that this code can make or break the program's performance! Need to optimize more…
*/
import DebugLogger from 'debug'
import { multicast, Observable } from 'observable-fns'
import { deserialize, serialize } from '../common.ts'
import { ObservablePromise } from '../observable-promise.ts'
import { isTransferDescriptor } from '../transferable.ts'
import type {
ModuleMethods, ModuleProxy, ProxyableFunction, Worker as WorkerType,
} from '../types/master.ts'
import type {
MasterJobCancelMessage,
MasterJobRunMessage,
WorkerJobErrorMessage,
WorkerJobResultMessage,
WorkerJobStartMessage,
} from '../types/messages.ts'
import {
MasterMessageType,
WorkerMessageType,
} from '../types/messages.ts'
const debugMessages = DebugLogger('threads:master:messages')
let nextJobUID = 1
const dedupe = <T>(array: T[]): T[] => [...new Set(array)]
const isJobErrorMessage = (data: any): data is WorkerJobErrorMessage => data && data.type === WorkerMessageType.error
const isJobResultMessage = (data: any): data is WorkerJobResultMessage => data && data.type === WorkerMessageType.result
const isJobStartMessage = (data: any): data is WorkerJobStartMessage => data && data.type === WorkerMessageType.running
function createObservableForJob<ResultType>(worker: WorkerType, jobUID: number): Observable<ResultType> {
return new Observable((observer) => {
let asyncType: 'observable' | 'promise' | undefined
const messageHandler = ((event: MessageEvent) => {
debugMessages('Message from worker:', event.data)
if (!event.data || event.data.uid !== jobUID) return
if (isJobStartMessage(event.data)) {
asyncType = event.data.resultType
} else if (isJobResultMessage(event.data)) {
if (asyncType === 'promise') {
if (event.data.payload !== undefined) {
observer.next(deserialize(event.data.payload))
}
observer.complete()
worker.removeEventListener('message', messageHandler)
} else {
if (event.data.payload) {
observer.next(deserialize(event.data.payload))
}
if (event.data.complete) {
observer.complete()
worker.removeEventListener('message', messageHandler)
}
}
} else if (isJobErrorMessage(event.data)) {
const error = deserialize(event.data.error as any)
if (asyncType === 'promise' || !asyncType) {
observer.error(error)
} else {
observer.error(error)
}
worker.removeEventListener('message', messageHandler)
}
}) as EventListener
worker.addEventListener('message', messageHandler)
return () => {
if (asyncType === 'observable' || !asyncType) {
const cancelMessage: MasterJobCancelMessage = {
type: MasterMessageType.cancel,
uid: jobUID,
}
worker.postMessage(cancelMessage)
}
worker.removeEventListener('message', messageHandler)
}
})
}
function prepareArguments(rawArgs: any[]): { args: any[]; transferables: Transferable[] } {
if (rawArgs.length === 0) {
// Exit early if possible
return {
args: [],
transferables: [],
}
}
const args: any[] = []
const transferables: Transferable[] = []
for (const arg of rawArgs) {
if (isTransferDescriptor(arg)) {
args.push(serialize(arg.send))
transferables.push(...arg.transferables)
} else {
args.push(serialize(arg))
}
}
return {
args,
transferables: transferables.length === 0 ? transferables : dedupe(transferables),
}
}
export function createProxyFunction<Args extends any[], ReturnType>(worker: WorkerType, method?: string) {
return ((...rawArgs: Args) => {
const uid = nextJobUID++
const { args, transferables } = prepareArguments(rawArgs)
const runMessage: MasterJobRunMessage = {
args,
method,
type: MasterMessageType.run,
uid,
}
debugMessages('Sending command to run function to worker:', runMessage)
try {
worker.postMessage(runMessage, transferables)
} catch (error) {
return ObservablePromise.from(Promise.reject(error))
}
return ObservablePromise.from(multicast(createObservableForJob<ReturnType>(worker, uid)))
}) as any as ProxyableFunction<Args, ReturnType>
}
export function createProxyModule<Methods extends ModuleMethods>(worker: WorkerType, methodNames: string[]): ModuleProxy<Methods> {
const proxy: any = {}
for (const methodName of methodNames) {
proxy[methodName] = createProxyFunction(worker, methodName)
}
return proxy
}