Skip to content

Instantly share code, notes, and snippets.

@GrandSchtroumpf
Created July 15, 2024 13:32
Show Gist options
  • Save GrandSchtroumpf/cf06e37f9d8d73988a1e48d187599cc4 to your computer and use it in GitHub Desktop.
Save GrandSchtroumpf/cf06e37f9d8d73988a1e48d187599cc4 to your computer and use it in GitHub Desktop.
Qwik web worker
import { component$, useSignal, $ } from "@builder.io/qwik";
import { useWebWorker } from "./use-web-worker";
export default component$(() => {
const count = useSignal(0);
const { postMessage } = useWebWorker<string>({
track: [count],
// This code run in a web worker
worker$: $(({ postMessage, onmessage }) => {
// Receive message from the main thread
onmessage((content) => {
console.log('Received message', content, 'with counter', count.value);
postMessage('Hello from worker'); // <-- Send message to the main thread
});
// manage running task
const i = setInterval(() => console.log('interval', count.value), 1000);
return () => clearInterval(i);
}),
// React to message from worker
onMessage$: $((message) => {
console.log('message from the worker', message);
}),
});
return <>
{/* Send message to worker */}
<button onClick$={() => postMessage('From main thread')}>Post Message</button>
<button onClick$={() => count.value++}>Counter</button>
</>
})
import { OnVisibleTaskOptions, QRL, Signal, useTask$, useVisibleTask$ } from "@builder.io/qwik";
import { QwikWorker, webWorkerQrl } from "./web-worker";
interface OnWebWorkerOptions<T> extends OnVisibleTaskOptions {
track?: Signal[],
onMessage$?: QRL<(args: T) => any>;
worker$: QRL<(args: QwikWorker<T>) => any>;
}
export const useWebWorker = <T = unknown>(opt: OnWebWorkerOptions<T>) => {
const {
track = [],
onMessage$,
worker$,
...visibleOptions
} = opt;
const worker = webWorkerQrl(worker$);
// Terminate worker when component is closed
useTask$(() => worker.terminate);
useVisibleTask$(async (task) => {
track.forEach(task.track);
worker.close();
worker.create();
if (onMessage$) return worker.onMessage$(onMessage$);
}, visibleOptions);
return worker;
}
import {
$,
implicit$FirstArg,
type QRL,
_getContextElement,
_serializeData,
} from '@builder.io/qwik';
//@ts-ignore
import workerUrl from './web-worker.worker.ts?worker&url';
const qwikWorkers = new Map<string, Worker>();
let workerRequests = 0;
const getWorkerRequest = () => ++workerRequests;
const getWorker = (qrl: QRL) => {
let worker = qwikWorkers.get(qrl.getHash());
if (!worker) {
qwikWorkers.set(
qrl.getHash(),
(worker = new Worker(workerUrl, {
name: `worker$(${qrl.getSymbol()})`,
type: 'module',
}))
);
}
return worker;
};
type Callback<T> = (args: T) => any | QRL<(args: T) => any>;
export interface QwikWorker<Message> {
onmessage: (cb: Callback<Message>) => any;
onclose: (cb: Callback<void>) => any;
postMessage: QRL<(data: Message) => any>;
}
export const webWorkerQrl = <T = unknown>(qrl: QRL<(this: any, props: QwikWorker<T>) => any>) => {
const sendMessage = $((type: 'init' | 'close' | 'message', data?: string) => {
const worker = getWorker(qrl);
const ctxElement = (_getContextElement() as HTMLElement | undefined);
const containerEl = ctxElement?.closest('[q\\:container]') ?? document.documentElement;
const qbase = containerEl.getAttribute('q:base') ?? '/';
const baseURI = document.baseURI;
const requestId = getWorkerRequest();
worker.postMessage([type, requestId, baseURI, qbase, data]);
})
const create = $(async () => {
const data = await _serializeData([qrl], false);
sendMessage('init', data);
});
/** Post message to web worker instance */
const postMessage = $(async (...args: any[]) => {
const data = await _serializeData(args, false);
sendMessage('message', data);
});
/** Close current process be do not use */
const close = $(() => sendMessage('close'))
/** Register callback when worker post message to main thread */
const onMessage$ = $((cb: (...args: any[]) => any) => {
const worker = getWorker(qrl);
const handler = ({ data }: MessageEvent) => {
if (!Array.isArray(data)) return;
const [type, ...args] = data;
if (type !== 'onmessage') return;
cb(...args);
};
worker.addEventListener('message', handler);
return () => worker.removeEventListener('message', handler);
});
/** Terminate the worker process */
const terminate = $(() => {
qwikWorkers.get(qrl.getHash())?.terminate();
qwikWorkers.delete(qrl.getHash());
});
return {
create,
onMessage$,
postMessage,
close,
terminate
};
};
export const webWorker$ = implicit$FirstArg(webWorkerQrl);
import { $, _deserializeData } from '@builder.io/qwik';
import { QwikWorker } from './web-worker';
globalThis.document = {
nodeType: 9,
ownerDocument: null,
dispatchEvent() {
return true;
},
createElement() {
return {
nodeType: 1,
} as any;
},
} as any;
const msgCallbacks = new Set<(...args: any[]) => any>();
const closeCallbacks = new Set<() => any>();
const props: QwikWorker<unknown> = {
// Marking it as QRL rerender the worker for some reason
onmessage: (cb) => msgCallbacks.add(cb),
onclose: (cb) => msgCallbacks.add(cb),
postMessage: $((...args: any[]) => self.postMessage(['onmessage', ...args])),
}
globalThis.onmessage = async ({ data }) => {
const [type, requestId, baseURI, qBase, params] = data;
const containerEl = {
nodeType: 1,
ownerDocument: {
baseURI,
},
closest() {
return containerEl;
},
getAttribute(name: string) {
return name === 'q:base' ? qBase : undefined;
},
};
try {
if (type === 'init') {
const [qrl] = _deserializeData(params, containerEl);
const output = await qrl.apply(self, [props]);
if (typeof output === 'function') closeCallbacks.add(output);
self.postMessage([type, requestId, true]);
}
if (type === 'message') {
const args = _deserializeData(params, containerEl);
msgCallbacks.forEach(cb => cb(args));
self.postMessage([type, requestId, true]);
}
if (type === 'close') {
closeCallbacks.forEach(cb => cb());
msgCallbacks.clear();
closeCallbacks.clear();
}
} catch (err) {
console.error(err);
self.postMessage([requestId, false, err]);
return;
}
};
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment