Leni - A small library for worker communication

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 => {
  // val is 7
})

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.