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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@e280/comrade

v0.1.0

Published

web-workers of the world unite

Readme

🤖 Comrade

  • comrade aims to be the best web worker library for typescript
  • bidirectional by default — you can call worker functions, and they can call you
  • clusters can magically schedule async calls across web workers
  • seamless browser and node compatibility
  • async rpc powered by renraku
  • a project for https://e280.org/

Web-workers of the world unite!

Install comrade

npm i @e280/comrade

Make your schematic type

your Schematic tells comrade about your functions

// schematic.ts

import {AsSchematic} from "@e280/comrade"

export type MySchematic = AsSchematic<{

  // functions on the worker. main thread can call these.
  work: {
    add(a: number, b: number): Promise<number>
    sub(a: number, b: number): Promise<number>
  }

  // functions on main thread. workers can call these.
  host: {
    mul(a: number, b: number): Promise<number>
    div(a: number, b: number): Promise<number>
  }
}>

💁 notearbitrary nesting is fine, actually

export type MySchematic = AsSchematic<{
  work: {
    add(a: number, b: number): Promise<number>
    nesty: {
      is: {
        besty(a: number, b: number): Promise<number>
      }
    }
  }
}>
await work.add(2, 3) // 5
await work.nesty.is.besty(2, 3) // 5

Make your worker

// worker.ts

import {Comrade} from "@e280/comrade"
import {MySchematic} from "./schematic.js"

await Comrade.worker<MySchematic>(shell => ({
  async add(a, b) {
    return a + b
  },
  async sub(a, b) {
    return a - b
  },
}))

💁 terminology

  • the shell gives you access to the other side's functionality
    async add(a, b) {
    
      // calling the host (from the worker)
      await shell.host.mul(2, 3)
    
      return a + b
    },
  • the shell.transfer lets you mark transferables for your returns (for zero-copy transfers)
    async getNiceBytes(a, b) {
      const bytes = new Uint8Array([0xB0, 0x0B, 0x13, 0x5])
    
      shell.transfer = [bytes]
    
      return bytes
    },

😱 bundler warning
you're probably going to have to bundle your worker module, especially since for some reason the spec/browser people never finished importmap support in workers, so a bundler is required to resolve dependencies in workers 🤷

Do the work

so, now you have a choice — you can either spin up a single worker, or you can spin up a cluster of workers.

  • spin up a single worker thread
    // thread.ts
    
    import {Comrade} from "@e280/comrade"
    import {MySchematic} from "./schematic.js"
    
    const thread = await Comrade.thread<MySchematic>({
    
      // relative url to your worker module
      workerUrl: new URL("./worker.js", import.meta.url),
    
      // functions on the main thread, workers can call these
      setupHost: shell => ({
        async mul(a: number, b: number) {
          return a * b
        },
        async div(a: number, b: number) {
          return a / b
        },
      }),
    })
    
    // calling worker functions
    await thread.work.add(2, 3) // 5
    await thread.work.sub(3, 2) // 1
    
    // terminate the workers when you're all done
    thread.terminate()
  • spin up a cluster of workers
    // cluster.ts
    
    import {Comrade} from "@e280/comrade"
    import {MySchematic} from "./schematic.js"
    
    const cluster = await Comrade.cluster<MySchematic>({
    
      // relative url to your worker module
      workerUrl: new URL("./worker.js", import.meta.url),
    
      // functions on the main thread, workers can call these
      setupHost: shell => ({
        async mul(a: number, b: number) {
          return a * b
        },
        async div(a: number, b: number) {
          return a / b
        },
      }),
    })
    
    // calling a worker functions
    await cluster.work.add(2, 3) // 5
    await cluster.work.sub(3, 2) // 1
    
    // terminate the workers when you're all done
    cluster.terminate()
    • each call is a queued task, and tasks are round-robin distributed across the worker pool
    • your work must be stateless — when you call a work function, you don't know which worker will respond
    • the number of workers in the pool will be your hardware concurrency minus one (eg, on an eight-core cpu, we expect 7 workers in the pool)

Now let's get more organized

the helpers host and work help you export functions from separate files.

// work.ts
export const setupWork = Comrade.work<MySchematic>(shell => {
  async add(a, b) {
    return a + b
  },
  async sub(a, b) {
    return a - b
  },
})
// host.ts
export const setupHost = Comrade.host<MySchematic>(shell => {
  async mul(a: number, b: number) {
    return a * b
  },
  async div(a: number, b: number) {
    return a / b
  },
})

use these in your workers, threads, or clusters

await Comrade.worker<MySchematic>(setupWork)
const thread = await Comrade.thread<MySchematic>({workerUrl, setupHost})
const cluster = await Comrade.cluster<MySchematic>({workerUrl, setupHost})

Mocks — fake it 'till you make it

for testing purposes, you can skip the whole worker/thread/cluster situation and create a mock setup like this

// mocks.ts
import {setupWork} from "./work.js"
import {setupHost} from "./host.js"

export const {work, host} = Comrade.mocks<MySchematic>({setupWork, setupHost})

await work.add(2, 3) // 5
await host.mul(2, 3) // 6

Logging

by default, comrade uses an ErrorTap which logs errors to the console.

if you want more verbose noisy logging (logging every request):

  • pass a logger tap to Comrade.thread
    import {LoggerTap} from "@e280/comrade"
    
    const thread = await Comrade.thread<MySchematic>({
      workerUrl,
      setupHost,
      tap: new LoggerTap(), // 👈 passing in a logger tap
    })
  • pass a logger tap to Comrade.cluster
    const cluster = await Comrade.cluster<MySchematic>({
      workerUrl,
      setupHost,
      tap: new LoggerTap(), // 👈 passing in a logger tap
    })
  • pass a logger tap to Comrade.mocks
    const {host, work} = await Comrade.mocks<MySchematic>({
      setupHost,
      setupWork,
      tap: new LoggerTap(), // 👈 passing in a logger tap
    })

if you want silence (not even errors), provide a dud tap:

import {DudTap} from "@e280/comrade"

const thread = await Comrade.thread<MySchematic>({
  workerUrl,
  setupHost,
  tap: new DudTap(), // 👈 dud tap does nothing, total silence
})

Tune the calls

this advancedness is brought to you by renraku

Transferables aren't copied

you can provide an array of transferables on any api call

import {tune} from "@e280/comrade"

// some example data
const buffer = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]).buffer

  //                      🤫                😲
  //                      👇                👇
await cluster.work.hello[tune]({transfer: [buffer]})({
  lol: "whatever",
  buffer, // <-- this gets transfered speedy-fastly, not copied (we like this)
})

that's good for outgoing requests, but now you also need to set transferables for your responses, which is done like this

await Comrade.worker<MySchematic>(shell => ({
  async coolAction() {
    const buffer = new Uint8Array([0xDE, 0xAD, 0xBE, 0xEF]).buffer

    // set transferables for this response
    shell.transfer = [buffer] // <-- will be transferred, not copied

    return {hello: "world", buffer}
  },
}))

Notifications get no response

you can also make a call a notification, which means no response will be sent back (just shouting into the void)

import {tune} from "@e280/comrade"

  //                               🫢
  //                               👇
await cluster.work.goodbye[tune]({notify: true})({
  lol: "whatever",
})

💖 Made with open source love

build with us at https://e280.org/ but only if you're cool