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!
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
.
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:
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 ๐ฌ