threads.js

repository·master·Indexed 25 days ago

https://github.com/andywer/threads.js

A library for offloading CPU-intensive tasks to worker threads across Node.js, web browsers, and Electron using a single, uniform API. It provides a transparent way to spawn workers, expose functions via a simple API, and handle complex data using custom serializers or Transfer() for binary data. Supports Node.js 12+ (native worker_threads), Node.js 8-11 (via tiny-worker), and major web browsers.

Tokens
12K
Snippets
34
Records
94
Agent score
85%

What's inside threads.js

  1. Configure Parcel bundler for threads.js

    master

    To use threads.js with Parcel, you must import threads/register once at the very beginning of your application's master code. This registers the threads.js Worker implementation as the global Worker for your platform.

    import { spawn } from "threads"
    import "threads/register"
    
    // ...
    
    const work = await spawn(new Worker("./worker"))
  2. Set up the documentation site locally

    master

    To run the documentation site locally, use Bundler to install dependencies into a vendor directory and then start the Jekyll server.

    Ensure you have Ruby and Bundler installed on your system before running these commands.

    bundle install --path vendor/bundle
    bundle exec jekyll serve --baseurl ''
  3. Configure Webpack with threads-plugin

    master

    To automatically bundle workers and resolve new Worker("./unbundled-path") expressions, use threads-plugin. This eliminates the need for worker-loader or manual entry point definitions.

    Note for TypeScript users: Ensure your ts-loader configuration sets module: "esnext" so that import/export statements remain intact for the plugin to process.

    npm install -D threads-plugin
    const ThreadsPlugin = require('threads-plugin')
    
    module.exports = {
      // ...
      plugins: [
        new ThreadsPlugin()
      ]
      // ...
    }

    TypeScript configuration requirement:

    module: {
      rules: [
        {
          test: /\.ts$/,
          loader: "ts-loader",
          options: {
            compilerOptions: {
              module: "esnext"
            }
          }
        }
      ]
    }
  4. Configure Electron ASAR unpacking for workers

    master

    Because workers packaged inside an ASAR archive cannot be easily spawned, you must use the asarUnpack option in your Electron configuration (e.g., in package.json) to unpack the worker files. threads.js will automatically locate the workers in the unpacked directory.

    "asarUnpack": {
      "dist/main/0.bundle.worker.js",
      "dist/main/0.bundle.worker.js.map"
    }
  5. Handle errors in worker threads

    master

    Error handling is transparent. If an error is thrown inside a worker, the Promise returned by the master thread's call to the worker function will be rejected with that error, including the original stack trace.

    // master.js
    import { spawn, Thread, Worker } from "threads"
    
    const counter = await spawn(new Worker("./workers/counter"))
    
    try {
      await counter.increment()
      await counter.increment()
      await counter.decrement()
    
      console.log(`Counter is now at ${await counter.getCount()}`)
    } catch (error) {
      console.error("Counter thread errored:", error)
    } finally {
      await Thread.terminate(counter)
    }
  6. Implement type-safe workers with TypeScript

    master

    You can achieve type safety by declaring the type of the worker returned by spawn(). A best practice is to export the type from the worker module itself and use it in the master thread.

    // counter.ts
    import { expose } from "threads/worker"
    
    let currentCount = 0
    
    const counter = {
      getCount() {
        return currentCount
      },
      increment() {
        return ++currentCount
      },
      decrement() {
        return --currentCount
      }
    }
    
    export type Counter = typeof counter
    
    expose(counter)
    // master.ts
    import { spawn, Thread, Worker } from "threads"
    import { Counter } from "./workers/counter"
    
    const counter = await spawn<Counter>(new Worker("./workers/counter"))
    console.log(`Initial counter: ${await counter.getCount()}`)
    
    await counter.increment()
    console.log(`Updated counter: ${await counter.getCount()}`)
    
    await Thread.terminate(counter)
  7. Install threads.js

    master

    To use threads.js in Node.js 12+, install the threads package. If you need to support Node.js versions 8 through 11, you must also install tiny-worker as an optional dependency to provide a fallback for worker_threads.

    npm install threads tiny-worker
  8. Use spawn() and expose() for basic worker functionality

    master

    The core API of threads.js revolves around spawn() to start a worker and expose() to make functionality available to the master thread.

    • spawn(new Worker(path)): Spawns a worker. If the worker returns a primitive, the master receives a Promise resolving to that value. If the worker returns a Promise or Observable, the master receives a proxy for that Promise or Observable.
    • expose(value): Used inside the worker to make a function or an object callable from the master thread. If an object is exposed, spawn() returns an object containing proxies to all the object's functions.
    • Thread.terminate(worker): Terminates the specified worker thread.
    // master.js
    import { spawn, Thread, Worker } from "threads"
    
    async function main() {
      const add = await spawn(new Worker("./workers/add"))
      const sum = await add(2, 3)
    
      console.log(`2 + 3 = ${sum}`)
    
      await Thread.terminate(add)
    }
    
    main().catch(console.error)
    // workers/add.js
    import { expose } from "threads/worker"
    
    expose(function add(a, b) {
      return a + b
    })
  9. Implement and register custom message serializers

    master

    By default, data passed between threads must be compatible with the Structured clone algorithm (which converts class instances into plain objects). To pass complex data like class instances, you must implement a SerializerImplementation and register it using registerSerializer() in both the main thread and the worker thread.

    Registration Rules:

    • Register serializers early, before calling spawn() or expose().
    • You can register multiple serializers. They are chained in reverse order of registration: the last one registered is tried first. If a serializer doesn't recognize the data, it calls its defaultHandler to pass it to the next serializer in the chain.
    • registerSerializer is available from both threads (main thread) and threads/worker (worker thread).
    import { registerSerializer, SerializerImplementation } from "threads"
    
    // 1. Define your serializer
    const MySerializer: SerializerImplementation = {
      deserialize(message, defaultHandler) {
        if (message && message.__type === "$$MyClass") {
          return MyClass.deserialize(message as any)
        } else {
          return defaultHandler(message)
        }
      },
      serialize(thing, defaultHandler) {
        if (thing instanceof MyClass) {
          return thing.serialize()
        } else {
          return defaultHandler(thing)
        }
      }
    }
    
    // 2. Register it in both main and worker threads
    registerSerializer(MySerializer)
  10. Quick start with threads.js

    master

    To use threads.js, you can spawn a worker using spawn() and define the worker's interface using expose() from threads/worker. Always import Worker from threads instead of using the global Worker to ensure cross-platform compatibility and access to additional functionality. Use Thread.terminate() to clean up workers when they are no longer needed.

    // master.js
    import { spawn, Thread, Worker } from "threads"
    
    async function main() {
      const auth = await spawn(new Worker("./workers/auth"))
      const hashed = await auth.hashPassword("Super secret password", "1234")
    
      console.log("Hashed password:", hashed)
    
      await Thread.terminate(auth)
    }
    
    main().catch(console.error)
    
    // workers/auth.js - will be run in worker thread
    import sha256 from "js-sha256"
    import { expose } from "threads/worker"
    
    expose({
      hashPassword(password, salt) {
        return sha256(password + salt)
      }
    })
  11. Use Thread Pools to manage worker concurrency

    master

    A Pool allows you to create a set of workers and queue worker calls. Instead of overwhelming workers with many tasks at once, the pool manages a queue and executes tasks in a controlled way with limited concurrency. Tasks are pulled from the queue as previous tasks finish.

    Use a Pool when you have a large volume of work to offload to workers and want to maintain a specific concurrency level.

    import { spawn, Pool, Worker } from "threads"
    
    const pool = Pool(() => spawn(new Worker("./workers/multiplier")), 8 /* optional size */)
    
    pool.queue(async multiplier => {
      const multiplied = await multiplier(2, 3)
      console.log(`2 * 3 = ${multiplied}`)
    })
    
    await pool.completed()
    await pool.terminate()