8000 GitHub - dmop/puru: Go-style concurrency and parallelism for JavaScript. Works on Node.js and Bun. · GitHub
[go: up one dir, main page]

Skip to content
dmopPublic

About

Go-style concurrency and parallelism for JavaScript. Works on Node.js and Bun.

Resources

Contributing

Security policy

Stars

54 stars

Watchers

0 watching

Forks

Latest commit

 

History

36 Commits

Folders and files

Repository files navigation

puru (プール)

npm version npm downloads bundle size zero dependencies license

Go-style concurrency and parallelism for JavaScript. Worker threads deliver true CPU parallelism across cores. Go-style chan, WaitGroup, ErrGroup, select, and context manage concurrency — zero dependencies, no worker files, no boilerplate.

import { spawn } from '@dmop/puru'

const { result } = spawn(() => {
  let sum = 0
  for (let i = 0; i < 100_000_000; i++) sum += i
  return sum
})

console.log(await result) // runs off the main thread

Before / After

Raw worker_threadspuru
const { Worker } = require('worker_threads')
const worker = new Worker('./worker.js')
worker.postMessage({ n: 40 })
worker.on('message', (result) => {
  console.log(result)
  worker.terminate()
})
worker.on('error', reject)

// worker.js (separate file)
const { parentPort } = require('worker_threads')
parentPort.on('message', ({ n }) => {
  parentPort.postMessage(fibonacci(n))
})
import { spawn } from '@dmop/puru'

const { result } = spawn(() => {
  function fibonacci(n: number): number {
    if (n <= 1) return n
    return fibonacci(n - 1) + fibonacci(n - 2)
  }
  return fibonacci(40)
})

try {
  console.log(await result)
} catch (err) {
  console.error(err)
}

One file. No message plumbing. Automatic pooling.

Install

Zero runtime dependencies — just the library itself.

npm install @dmop/puru
# or
bun add @dmop/puru

Quick Start

import { spawn, WaitGroup, chan, task } from '@dmop/puru'

// CPU work on a dedicated worker
const { result } = spawn(() => {
  function fibonacci(n: number): number {
    if (n <= 1) return n
    return fibonacci(n - 1) + fibonacci(n - 2)
  }
  return fibonacci(40)
})

// Reusable worker logic with explicit arguments
const crunch = task((n: number) => {
  let sum = 0
  for (let i = 0; i < n; i++) sum += i
  return sum
})

// Parallel batch — wait for all
const wg = new WaitGroup()
wg.spawn(() => 21 * 2)
wg.spawn(() => 6 * 7)
const [a, b] = await wg.wait()

const bigNumber = await result
const heavySum = await crunch(1_000_000)
console.log({ a, b, bigNumber, heavySum })

// Cross-thread channels
const ch = chan<number>(10)
spawn(async ({ ch }) => {
  for (let i = 0; i < 10; i++) await ch.send(i)
  ch.close()
}, { channels: { ch } })

for await (const item of ch) console.log(item)

Performance

Measured on Apple M1 Pro (8 cores). Full results in BENCHMARKS.md.

Benchmark Single-threaded puru Speedup
Fibonacci (fib(38) x8) 4,345 ms 2,131 ms 2.0x
Prime counting (2M range) 335 ms 77 ms 4.4x
100 concurrent async tasks 1,140 ms 16 ms 73x
Fan-out pipeline (4 workers) 176 ms 51 ms 3.4x

Spawn overhead: ~0.1-0.5ms. Use for tasks above ~5ms.

Two Modes

Mode Kind Use it for What happens
spawn(fn) Parallelism CPU-bound work Dedicated worker thread — runs on a separate CPU core
spawn(fn, { concurrent: true }) Concurrency Async / I/O work Shares a worker's event loop (M:N scheduling)

When To Use What

Situation Tool
One heavy CPU task spawn(fn)
Same logic, many inputs task(fn)
Wait for all tasks WaitGroup
Fail-fast, cancel the rest ErrGroup (with setLimit() for throttling)
Timeouts and cancellation context + spawn(fn, { ctx })
Producer/consumer pipelines chan() + select()

The Big Rule

Functions passed to spawn() cannot capture outer variables. They are serialized as text and sent to a worker — closures don't survive.

const x = 42
spawn(() => x + 1)          // ReferenceError at runtime

spawn(() => {
  const x = 42               // define inside
  return x + 1
})                            // works

Use task(fn) to pass arguments to reusable worker functions.

What's Included

Coordination: chan() · WaitGroup · ErrGroup · select() · context

Synchronization: Mutex · RWMutex · Once · Cond

Timing: after() · ticker() · Timer

Ergonomics: task() · configure() · stats() · directional channels · channel len/cap

All modeled after Go's concurrency primitives. Full API in docs/API.md.

Why Not Just Use...

Promise.all() — Great for cheap async work. Use puru when work is CPU-heavy or you need the main thread to stay responsive.

worker_threads — Powerful but low-level: separate files, manual messaging, manual pooling, no channels/WaitGroup/select. puru keeps the power, removes the ceremony.

Cluster — Cluster adds processes for request throughput. puru offloads heavy work inside each process. They compose well together.

Runtimes

Runtime Status
Node.js >= 20 Full support
Bun Full support
Deno Planned

Testing

import { configure } from '@dmop/puru'
configure({ adapter: 'inline' }) // runs on main thread, no real workers

Docs

Limitations

  • spawn() functions cannot capture outer variables (see The Big Rule)
  • Channel values must be structured-cloneable (no functions, symbols, WeakRefs)
  • null is reserved as the channel-closed sentinel
  • task() arguments must be JSON-serializable

License

MIT

About

Go-style concurrency and parallelism for JavaScript. Works on Node.js and Bun.

Resources

Contributing

Security policy

Stars

54 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages

0