react-use-comlink
Version:
React hook for seamless usage with Comlink webworker library
194 lines (175 loc) • 5.67 kB
text/typescript
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