Skip to content

Inspired by Go's Goroutines, this package adds an easy ability to trivially multithread (and potentially multiprocess) your code (supports NodeJS and Bun)

License

Notifications You must be signed in to change notification settings

exoRift/goroutines.js

Repository files navigation

Goroutines.js

Inspired by Go's Goroutines, this package adds an easy ability to trivially multithread (and potentially multiprocess) your code (supports NodeJS and Bun)

Installation

npm install goroutines

Getting Started

Take a costly function,

function fib (n: number): number {
  if (n <= 1) return n
  return fib(n - 1) + fib(n - 2)
}

const result = fib(40)
console.log(resut)

This is a heavy function that blocks the event loop. It can be made asynchronous with the async, but still runs on the same thread (JS is single-threaded) which can be taxing.

Instead, wrap it into a coroutine

import { go } from 'goroutines'

const result = await go(fib)(40)
console.log(result)

The function will now run on a separate thread, leaving your main process unblocked

Important

Technically, we're still blocking the main thread in this example with a top-level await. Make sure to read into how promises work

Data Streaming

Another useful capability of Goroutines is their ability to stream data. You can accomplish something similar using generator functions.

import { go } from '../src'

const shared = new SharedArrayBuffer(1_000_000)
const array = new Uint8Array(shared)
crypto.getRandomValues(array)

function* getEven(buffer: SharedArrayBuffer) {
  const data = new Uint8Array(buffer)
  for (let i = 0; i < data.length; ++i) {
    const value = Atomics.load(data, i)
    if (!(value % 2)) yield value
  }
}

const iter = go(getEven)(shared)
let ret
do {
  ret = await iter.next()
  if (ret.done) break

  console.log(ret.value)
} while (!ret.done)

This will function exactly like an async generator.

Tip

A regular array can be serialized just as well but results in cloning which is costly. See: supported data types See: SharedArrayBuffer

You can iterate through it using the for await syntax

for await (const chunk of go(getEven)(shared)) { ... }

You can pass data back to the thread

// Dramatized example for demonstration. DO NOT DO THIS
function* pwValidator (password: string) {
  while (true) {
    const attempt = yield 'Enter a password'
    if (attempt === password) {
      yield 'Correct!'
      break
    } else yield 'Incorrect!'
  }
}

const guesser = go(pwValidator)('super secret password')
await guesser.next()

const guess = 'wrong password'
const result = await guesser.next(guess)
console.log(result.value)

Tip

You can read more into the specifics of how generator functions pass values between yields here

Context and Imports

If your function uses an out-of-scope variable, you can pass it to your coroutine via the context parameter

import suspectdata from './suspects.json' with { type: 'json' }

const CRIME_SCENE = 'The Museum'
const CRIME_DATE = '2025-07-17T20:03:11.952Z'

interface Suspect {
  name: string
  locations: Partial<Record<string, string>>
}

const suspects: Suspect[] = suspectdata

function findPerp () {
  return suspects.find((s) => s.locations[CRIME_DATE] === CRIME_SCENE)?.name
}

const suspect = await go(findPerp, {
  CRIME_SCENE,
  CRIME_DATE,
  suspects
})()
console.log(suspect)

Firstly, notice an optimization we can do. Instead of importing the data here and passing it to the worker, we can import it directly into the worker.

Secondly, simply logging the suspect isn't enough. We must show them for who they are in all of their evil glory. To accomplish this, we'll style the text red using styleText from 'util'

const CRIME_SCENE = 'The Museum'
const CRIME_DATE = '2025-07-17T20:03:11.952Z'

interface Suspect {
  name: string
  locations: Partial<Record<string, string>>
}

function findPerp () {
  // @ts-expect-error
  declare const suspects: Suspect[]
  // @ts-expect-error
  declare const styleText: typeof import('util').styleText

  const name = suspects.find((s) => s.locations[CRIME_DATE] === CRIME_SCENE)?.name
  return name && styleText('red', name)
}

const suspect = await go(findPerp, {
  CRIME_SCENE,
  CRIME_DATE
}, {
  './suspects.json': ['default as suspects'],
  // Due to the behavior of the worker api, relative modules are usually resolved from the CWD, not the file directory. Bun exposes an API for module location resolution:
  // [Bun.resolveSync('./suspects.json', import.meta.dirname)]: ['default as suspects'],
  util: ['styleText']
})()
console.log(suspect)

Caution

Try to avoid passing large amounts of data to workers via context, function call parameters, or generator passing If the dataset already exists in a file and isn't required in the main thread, import it in the coroutine.

About

Inspired by Go's Goroutines, this package adds an easy ability to trivially multithread (and potentially multiprocess) your code (supports NodeJS and Bun)

Topics

Resources

License

Stars

Watchers

Forks