npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

eventloop-graft

v0.1.0

Published

Tool for grafting the Node.js event loop onto other (eg. UI toolkit) event loops on a standard Node.js installation

Readme

eventloop-graft

A tool for grafting the Node.js event loop onto other (eg. UI toolkit) event loops on a standard Node.js installation.

This fixes the 'main thread problem' where a UI or graphics toolkit requires running on the main thread to initialize successfully, and then never returns control to Node.js, breaking asynchronous code. But without needing a custom Node.js fork; this should work on any standard Node.js installation since v12.3.0!

Beware; even though this library takes care of some of the complexity, event loop integration still isn't a trivial task. If you're unfamiliar with this kind of thing, you should check if there's an existing integration for your preferred toolkit first. This library is designed to be built on top of, so someone else may have already done the work for you.

Caveat

This approach is a bit of a hack. I have no reason to believe it will ever stop working (the message handling is documented behaviour and the hacks are well-established), but it's very possible that there are edge cases that this doesn't handle well. In that case, please let me know!

Principle and usage

The idea behind this technique is that the main thread is dedicated to the UI or graphics toolkit, and your application code runs inside of a worker thread instead, in its entirety. This library then takes care of setting this up in a way that works entirely synchronously on the UI thread side, and that can be integrated into the toolkit you're using, while still letting you communicate between the UI and application side. It also proxies methods like console.log, which would otherwise cease to work due to the main thread being blocked.

Most UI and graphics toolkits manage their own event loop, but provide you with some mechanism to run custom code on every tick - often this code must be synchronous. Sometimes there is a literal "run on every tick" hook or callback, sometimes you need to implement it using zero-length timers, and so on; but there's usually a way. In other words, as long as there is some way in the toolkit (bindings) you're using to run synchronous code at an event-loop-like frequency, this integration should work.

In the below simplified example, our 'toolkit' is a simple infinite loop that's fully synchronous; ie. it never returns control to the event loop. Ordinary, such an infinite loop would block your code forever, and nothing would happen.

index.js:

"use strict";

const path = require("node:path");
const graft = require("eventloop-graft");

let app = graft(path.join(__dirname, "app.js"));

function tick() {
	app.process(({ type, ... message }) => {
		if (type === "test") {
			console.log("test from UI side", { message });
			app.send({ type: "test", boolean: true, number: NaN });
		} else if (type === "exit") {
			console.log("Exiting...");
			process.exit();
		} else {
			console.error(`INVALID MESSAGE TYPE [UI SIDE]: ${type}`);
		}
	});
}

function uiLoop(callback) {
	// This function is our mock 'toolkit' implementation, specifically its "run on every tick" hook
	while (true) {
		callback();
	}
}

uiLoop(() => tick());

app.js:

"use strict";

const graft = require("eventloop-graft");

let ui = graft();
	
ui.on("message", ({ type, ... message }) => {
	if (type === "test") {
		console.log("test from App side", { message });
		ui.send({ type: "exit" });
	} else {
		console.error(`INVALID MESSAGE TYPE [APP SIDE]: ${message}`);
	}
});

ui.send({ type: "test", foo: "bar", num: 42 });

These two files together will produce the following output, when you run node index.js:

test from UI side { message: { foo: 'bar', num: 42 } }
test from App side { message: { boolean: true, number: NaN } }
Exiting...

In this example, app.js is run inside of a worker process. Note how the main process (index.js) and the worker process (app.js) are receiving a slightly different export from the eventloop-graft import; the main thread version is expected to provide a path to the application code, whereas the worker version (used for the application code) works as-is.

Also note that an absolute path is provided for the 'application path'; this is required, because a relative path would resolve relative to the current working directory, which would open you up to vulnerabilities in some cases; when externally invokved, the exact code that is run could be influenced by changing the working directory. Therefore, eventloop-graft requires that you always specify an absolute path, constructing it from a relative path yourself if necessary (as in the example above).

The two threads communicate through a message-passing API. The serialization works the same as for postMessage, which is used underneath. The second argument (not shown in the example) accepts a transferList just like postMessage would. You are responsible for passing around a reference to this API in your application code yourself, just like you'd otherwise have to pass around a reference to your (initialized) UI or graphics toolkit. However, console output proxying is automatic, and should work from anywhere in your application code.

A typical setup would involve the message-passing API only being used for communicating with the UI or graphics toolkit (eg. submitting component tree changes, receiving inputs, and so on), and the rest of the application being contained entirely within the worker thread. Notably, you do not need to manually pass operations from the UI thread to the application thread like you would in most Electron applications. Instead, it's typically the responsibility of the toolkit integration layer to generically pass around all inputs from the UI thread to the application thread, letting the application thread decide what to do with them.

Crucially, you should do as little work as possible on the UI (main) thread. End users are highly perceptive of latency in UI interactions, and so there should never be anything slowing down the UI thread; your message processing will block the UI thread, so it's important to keep it as fast as possible. Do all work on the application thread (or worker threads instantiated from it) instead, and only send back precomputed UI changes to the UI thread. Conceptually, you should treat your application worker thread like it's your main thread, even if it isn't the case on a technical level.