alewin/useWorker

How to keep global state in a worker ?

mquandalle opened this issue ยท 5 comments

Hello,

I'm trying to convert an existing worker using the postMessage API. This worker has some global state that is initialized with the first message, and used in subsequent calls:

import Fuse from 'fuse.js'

let fuse = null // This is the global state
onmessage = function(event) {
	if (event.data.options) // First call we initiate the Fuse object
		fuse = new Fuse(event.data.options) 

	if (event.data.input) { // Second call we use it to search
		let results = fuse.search(event.data.input)
		postMessage(results)
	}
}

The goal is that the search happen in the worker. I've tried writing a high order useWorker function but it's not clear to me what is run in the worker, and what's the best way to write such a flow (first call with the source data, second call with the search query). Thank you for your help!

zant commented

Hi @mquandalle! At the moment we do not support global variables. This is because when you pass a function to useWorker like this:

const [sumWorker] = useWorker((a, b) => a + b)

The function is directly attached to the onmessage method on the Web Worker. Thus, you cannot set anything outside that method. However, I agree it will be a nice feature to have so expect to have it in future versions ๐Ÿ˜‰

For the time being, this is the suggested way to call external dependencies with useWorker:

const [fuseWorker, { kill: terminateWorker }] = useWorker(fuseSearch, {
   // Pass the dependency your using via cdn
    remoteDependencies: ["https://cdn.jsdelivr.net/npm/fuse.js@5.2.3"],
   // Here we tell useWorker to keep the worker alive until terminateWorker is called
   // this way, we avoid killing the worker and refetching the library from the cdn on every call
    autoTerminate: false 
  });

 // Is important to kill the worker on component un-mount
 useEffect(() => () => terminateWorker(), []);

Here's a working sandbox with fuse.js: https://codesandbox.io/s/fuse-use-worker-dmp7f?file=/src/App.js

Thank you for your answer.

Actually new Fuse() is expensive (it build indexes, etc.) so we don't want to call for every search (we use it on a input field, where search is called on every press). That's why we do a first call to the worker (to build the cache) and then we only call fuse.search which is fast.

I'm under the impression that I can't replicate this model with useWorker.

zant commented

No problem! And you're right, with the current version of the hook we cannot set global state.

However, this is a really good use case so I just opened a PR (#45) which will provide this functionality. Hopefully we can merge it and prepare a release candidate.

Please feel free to suggest improvements to the API proposed there. Thanks for reporting!

The only solution at the moment is the one described by @gonzachr ๐Ÿ‘ or #45 (comment) (more efficient)


I'd like to implement a version that uses higher order functions:

Pseudo-example:

const fuseSearch = (list, options) => {
  const fuse = new Fuse(list, options);

  return function search(input) {
    return fuse.search(input);
  }
}

Usage

const [fuseWorker] = useWorker(fuseSearch, {....})

const fuse = await fuseWorker(['demo', 'demo2'], {})
const res = fuse.search('demo2')

is it currently possible?

Not natively because postMessage uses Structured clone algorithm and Function objects cannot be duplicated by the structured clone algorithm :(

how to convert a string into a function?

var adder = new Function("a", "b", "return a + b"); or eval ( not safe!!!)

can we find a workaround?

the library already uses workarounds to create an inline worker, so we can also think of a clean way to allow the worker to return a function

possibile solution:
worker proto
but we should think better about this aspect ...
Tomorrow evening (CEST) I will try to make a proposal :)

I'm trying to find a workaround to keep a reference to the function scope, but I don't know if it's feasible ๐Ÿ˜ฌ