tdlr; check out leni, a low-level tool for communicating with web workers.
Web Workers are one of the most underused features on the web. There's a strong chance that if you're reading this you're an experienced JavaScript developer. There's an equally strong chance that you've never used Web Workers. I barely have, and I go out of my way to look for reasons to use them 😅!
One of the problems with Workers is that they are weird to communicate with; a single event message
is how all communication is done. If you want your Worker to do more than just 1 thing, this means clumsy code like:
app.js
let worker = new Worker('./worker.js');
worker.postMessage({
type: 'ADD_ONE',
value: 3
});
worker.js
self.addEventListener('message', function(ev){
let msg = ev.data;
switch(msg.type) {
case 'ADD_ONE':
addOne(msg.value);
break;
}
});
function addOne(num) {
let value = 1 + num;
postMessage({
type: 'ADD_ONE_RESULT',
value
});
}
These sort of switches are then required on both sides.
What is this really doing, though? These are just events, right? You listen to events and emit events.
Leni
A new library I wrote called leni removes all of this boilerplate, and lets you more easily save state within a worker.
It does this by providing a string tag. You can think of a tag as just an identifier for a particular event emitter that you want to be created on the other side.
This helps you organize a set of functionality around a given tag. It works like this:
app.js
import { connect } from 'https://unpkg.com/leni/leni.js';
let worker = new Worker('./worker.js');
let ee = connect('calculator', worker);
ee.addEventListener('value', val => {
})
ee.post('state', 3);
ee.post('add', 4);
worker.js
importScripts('https://unpkg.com/leni/leni.js');
function makeCalc(ee) {
let state;
ee.addEventListener('state', val => {
state = val;
});
ee.addEventListener('add', function(num){
state += num;
ee.post('value', state);
});
}
leni.subscribe('calculator', makeCalc);
As you can see from the above, leni is very low-level, and only handles creating instances of event emitters. Every time you call connect(tag, worker)
a new emitter will be created both in the main thread and within the worker.
Given this, we can add more abstractions on top. I can imagine using proxies to make these events feel more like direct method calls:
import { connect } from 'https://unpkg.com/leni/leni.js';
function proxyToWorker(emitter) {
return new Proxy({}, {
get(target, key) {
return new Proxy(function(){}, {
apply(target, thisArg, args) {
return new Promise(resolve => {
emitter.addEventListener(`${key}-response`, resolve);
emitter.emit(key, args);
});
}
});
}
})
}
async function startApp() {
let worker = new Worker('./worker.js');
let ee = connect('calculator', worker);
let calc = proxyToWorker(ee);
let value = await calc.addOne(3);
}
startApp();
Please check out Leni and let me know what you think. You can find me on Twitter as matthewcp.