UNPKG

react-use-comlink

Version:

React hook for seamless usage with Comlink webworker library

194 lines (175 loc) 5.67 kB
'use strict' import { useMemo, useEffect } from 'react' import { wrap, Remote, releaseProxy } from 'comlink' export type WorkerTypes = Blob | string | ReturnWorkerTypes export type ReturnWorkerTypes = () => Exclude<WorkerTypes, ReturnWorkerTypes> | Worker export interface Comlink<T> { /** the Worker itself, exposed for mocking, debugging, etc */ worker: Worker /** Comlink proxy through `Comlink.wrap(worker)` */ proxy: Remote<T> } export function processWorker( worker: WorkerTypes, options?: WorkerOptions, ): Worker; export function processWorker( worker: WorkerTypes | Worker, options: WorkerOptions, acceptsWorker: boolean ): Worker; /** Helper for initializing a new worker from strings, factories or blobs */ export function processWorker( worker: WorkerTypes | Worker, options: WorkerOptions = {}, acceptsWorker: boolean = false ): Worker { let innerWorker: Worker if (acceptsWorker && worker instanceof Worker) { innerWorker = worker } else if (worker instanceof Blob) { innerWorker = new Worker(URL.createObjectURL(worker), options) } else if (typeof worker === 'string') { innerWorker = new Worker(worker, options) } else if (typeof worker === 'function' && !(worker instanceof Worker)) { return processWorker(worker(), options, true) } else { throw new TypeError('CreateUseComlink needs either a Worker factory instance, a Blob or a string containing a path') } return innerWorker } /** * Use the hook directly inside another React Hook or component. Best way to initialize it * is to either use a factory that returns a `new Worker()`, or using an string path (but bundlers * like Parcel won't be able to 'inline' the worker for you). * * The hook does not assumes knowledge of your worker (if it's a class, object, array, etc), * so you must keep the internal state and initialize any classes by yourself * * @example * const MyApp = () => { * const [state, setState] = useState('initialState') * const { proxy } = useComlink(() => new Worker('./some.worker.js')) // initialize worker once * * useEffect(() => { * (async () => { * setState(await proxy.method('some', 'arguments')) * })() * // or plain old * proxy.method().then(setState) * }, [proxy, setState]) * * return (<div>{state}</div>) * } */ export function useComlink< T = unknown >( initWorker: WorkerTypes, deps: React.DependencyList = [], options: WorkerOptions = {}, ): Comlink<T> { const instance = useMemo<Comlink<T>>(() => { const worker = processWorker(initWorker, options) const proxy = wrap<T>(worker) return { worker, proxy, } }, deps) useEffect(() => { const innerProxy = instance.proxy const innerWorker = instance.worker return () => { (innerProxy as any)[releaseProxy]() innerWorker.terminate() } }, [instance]) return instance } /** * Creates a comlink factory from the same Worker for usage multiple times. * * @example * * // returns a reusable hook * const useMyClass = createComlink<typeof MyClass>( * () => new Worker('./some.worker.js'), * ) * * const Component: React.FC = () => { * const { proxy, worker } = useMyClass() * * useEffect(() => { * proxy.someMethod().then((r) => doSomething(r)) * }, [proxy]) * } */ export function createComlink< T = unknown >( initWorker: ReturnWorkerTypes, options: WorkerOptions = {}, ): () => Comlink<T> { const worker = () => processWorker(initWorker, options) const proxy = (w: Worker) => wrap<T>(w) return () => { const instance = useMemo<Comlink<T>>(() => { const innerWorker = worker() const innerProxy = proxy(innerWorker) return { proxy: innerProxy, worker: innerWorker } }, [worker, proxy]) useEffect(() => { const innerProxy = instance.proxy const innerWorker = instance.worker return () => { (innerProxy as any)[releaseProxy]() innerWorker.terminate() } }, [instance]) return instance } } /** * Creates one instance of a worker that can be instantiated inside components and share a global state. * It always loads the worker before mounting any components, so most likely the worker will be * ready to use when called inside a component. * * NOTE: This creates ONE instance of the worker, with a global state, and it should be * used very carefully (and sparsingly). The global state won't show the same value on other * mounted components unless you query it. One of the problems with using this is that sometimes * the browser might FREE the worker during a GC * * @see https://github.com/GoogleChromeLabs/comlink/issues/63 * * @example * const useMyMethods = createComlinkSingleton(new Worker('./worker.js')) // outside the component, it's a factory * * const MyComponent = () => { * const myMethods = useMyMethods() // returns { proxy, worker } * * useEffect(() => { * (async function(){ * await myMethods.proxy.someMethod() * })() * * return () => myMethods.worker.terminate() // bad things may happen! * }, [myMethods]) // myMethods is a memoed object, so it's considered stable * * return (<div />) * } */ export function createComlinkSingleton<T = unknown>(initWorker: Worker, options: WorkerOptions = {}): () => Comlink<T> { const worker = processWorker(initWorker, options, true) const proxy = wrap<T>(worker) return () => { return useMemo(() => ({ worker, proxy }), [worker, proxy]) } } export default useComlink