/*
* This file is part of TREB.
*
* TREB is free software: you can redistribute it and/or modify it under the
* terms of the GNU General Public License as published by the Free Software
* Foundation, either version 3 of the License, or (at your option) any
* later version.
*
* TREB is distributed in the hope that it will be useful, but WITHOUT ANY
* WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS
* FOR A PARTICULAR PURPOSE. See the GNU General Public License for more
* details.
*
* You should have received a copy of the GNU General Public License along
* with TREB. If not, see .
*
* Copyright 2022-2026 trebco, llc.
* info@treb.app
*
*/
/**
* this is a wrapper for workers. we want to support node in addition to the
* browser. bun seems to support web APIs with no modification, but node needs
* special attention. not sure about deno.
*
* switching to specific classes for different environments. also the plan is
* to add a main-thread (i.e. no-worker, inline calc) version.
*
*/
export interface WorkerProxy {
/** terminate worker */
Terminate: () => void;
/** initialize, possibly loading code */
Init: (url: string, browser_init_method: () => Worker) => Promise;
/** wrapper for postMessage */
PostMessage: (message: TX) => void;
/** wrapper for addEventListener */
OnMessage: (fn: (message: MessageEvent) => void | Promise) => void;
/** wrapper for addEventListener */
OnError: (fn: (message: ErrorEvent) => void | Promise) => void;
}
function CheckNodeSemantics() {
return typeof Worker === 'undefined';
}
export class WorkerProxyBrowser implements WorkerProxy {
public worker?: Worker;
public Terminate() {
this.worker?.terminate();
}
/**
* OK this changed to support svelte/vite/I guess it's all rollup under
* the hood. whatever the tool is, it does static analysis and it has
* to see the pattern loaded in order to bundle the worker module. so
* we play ball. this is not a good situation.
*
* the browser init method should look something like this:
*
* () => {
* return new Worker(new URL(url, import.meta.url), {
* type: 'module'
* }) as Worker;
* }
*
* so that the code analyzer will see it.
*
* @param url
* @param browser_init_method
*/
public async Init(url: string, browser_init_method: () => Worker) {
this.worker = browser_init_method();
}
public PostMessage(message: TX) {
this.worker?.postMessage(message);
}
public OnMessage(fn: (message: MessageEvent) => (void|Promise)) {
if (this.worker) {
this.worker.onmessage = fn;
}
}
public OnError(fn: (message: ErrorEvent) => (void|Promise)) {
if (this.worker) {
this.worker.onerror = fn;
}
}
}
export class WorkerProxyNode implements WorkerProxy {
public worker?: {
postMessage: (message: TX) => void|Promise;
terminate: () => void;
on: (type: 'message'|'error', handler: (data: RX) => void|Promise) => void;
};
public Terminate() {
this.worker?.terminate();
}
public async Init(url: string) {
const { Worker: NodeWorker } = await import('node:worker_threads');
this.worker = new NodeWorker(new URL(url, import.meta.url));
}
public PostMessage(message: TX) {
// apparently postMessage works on node workers as well?
this.worker?.postMessage(message);
}
public OnMessage(fn: (message: MessageEvent) => (void|Promise)) {
if (this.worker) {
this.worker.on('message', (data: RX) => fn({data} as MessageEvent));
}
}
public OnError(fn: (message: ErrorEvent) => (void|Promise)) {
if (this.worker) {
this.worker.on('error', (err) => fn({error: err} as ErrorEvent));
}
}
}
interface InProcessContext {
addEventListener: (type: 'message', handler: (event: MessageEvent) => void|Promise) => void;
postMessage: (data: RX) => void;
}
let in_process_worker_context: InProcessContext|undefined = undefined;
type composite = WorkerProxy & InProcessContext;
export class WorkerProxyInProcess implements composite {
protected tx_cache: TX[] = [];
protected main_thread_to_worker_message_handler?: (event: MessageEvent) => void|Promise;
protected rx_cache: RX[] = [];
protected worker_to_main_thread_message_handler?: (message: MessageEvent) => void|Promise;
public Terminate() {
this.tx_cache = [];
this.rx_cache = [];
this.main_thread_to_worker_message_handler = undefined;
this.worker_to_main_thread_message_handler = undefined;
}
public async Init(url: string) {
in_process_worker_context = this as InProcessContext;
/* @vite-ignore */
await import(new URL(url, import.meta.url).toString());
}
/** post message from "worker" to "main thread" */
public PostMessage(message: TX) {
if (this.main_thread_to_worker_message_handler) {
// console.info('calling handler', message);
const handler = this.main_thread_to_worker_message_handler;
Promise.resolve().then(() => handler({data: message} as MessageEvent));
}
else {
// console.info("no handler, caching message");
this.tx_cache.push(message);
}
}
/** callback when "main thread" sends message to "worker" */
public OnMessage(fn: (message: MessageEvent) => (void|Promise)) {
this.worker_to_main_thread_message_handler = fn;
if (this.rx_cache.length) {
const copy = [...this.rx_cache];
this.rx_cache = [];
this.FlushCache(copy, fn);
}
}
// eslint-disable-next-line @typescript-eslint/no-unused-vars
public OnError(fn: (message: ErrorEvent) => (void|Promise)) {
// ??
}
public FlushCache(cache: T[], fn: (message: MessageEvent) => void|Promise) {
// console.info(`flush cache ${cache.length}`);
Promise.resolve().then(async () => {
for (const message of cache) {
const result = fn({data: message} as MessageEvent);
if (result instanceof Promise) {
await result; // serial?
}
}
});
}
public addEventListener(type: 'message', handler: (event: MessageEvent) => void|Promise): void {
this.main_thread_to_worker_message_handler = handler;
if (this.tx_cache.length) {
const copy = [...this.tx_cache];
this.tx_cache = [];
this.FlushCache(copy, handler);
}
}
public postMessage(data: RX): void {
if (this.worker_to_main_thread_message_handler) {
this.worker_to_main_thread_message_handler({data} as MessageEvent);
}
else {
this.rx_cache.push(data);
}
}
}
/**
* factory method for workers. supports web workers (also bun), node workers
* and (via parameter) in-process workers. we're still figuring out how to
* signal that we're in process, currently somewhat clumsy
*
* @param in_process
* @returns
*/
export function CreateWorker(in_process = false): WorkerProxy {
if (in_process) {
if (process.env.NODE_ENV !== 'production') {
if (!(globalThis as {worker_console_message?: boolean}).worker_console_message) {
console.info('using in-process worker');
(globalThis as {worker_console_message?: boolean}).worker_console_message = true;
}
}
return new WorkerProxyInProcess();
}
if (CheckNodeSemantics()) {
if (process.env.NODE_ENV !== 'production') {
if (!(globalThis as {worker_console_message?: boolean}).worker_console_message) {
console.info('using node worker');
(globalThis as {worker_console_message?: boolean}).worker_console_message = true;
}
}
return new WorkerProxyNode();
}
if (process.env.NODE_ENV !== 'production') {
if (!(globalThis as {worker_console_message?: boolean}).worker_console_message) {
console.info('using standard web worker');
(globalThis as {worker_console_message?: boolean}).worker_console_message = true;
}
}
return new WorkerProxyBrowser();
};
/**
* this is for the worker side, to normalize postMessage/onmessage
*/
export const GetWorkerContext = async (): Promise => {
let ctx: Worker;
if (in_process_worker_context) {
ctx = in_process_worker_context as unknown as Worker;
}
else if (typeof self === 'undefined') {
const { parentPort } = await import('node:worker_threads');
ctx = {
postMessage: (data: unknown) => {
parentPort?.postMessage(data);
},
addEventListener: (type: 'message', handler: (event: MessageEvent) => void) => {
parentPort?.on('message', (data) => {
handler({ data } as MessageEvent);
});
},
} as unknown as Worker;
}
else {
ctx = self as unknown as Worker;
}
return ctx;
};