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

@hecto/async

v3.0.0

Published

Lazy cancellable Task and AsyncIter utilities for TypeScript

Readme

@hecto/async

Composable async tasks with cancellation, retry, and concurrency control.

pnpm add @hecto/async
import { Task } from '@hecto/async'
import { pipe } from '@hecto/fp'
import { map as mapResult } from '@hecto/fp/result'

const fetchUser = pipe(
  Task.tryPromise(
    () => fetch('/api/user').then((response) => response.json()),
    (cause) => ({ type: 'FetchUserFailed' as const, cause }),
  ),
  Task.timeout(5000),
  Task.retry({ attempts: 3, backoff: 'exponential' }),
  Task.map(mapResult((data) => data.user)),
)

const result = await Task.run(fetchUser)

Lazy async iteration is available from the root namespace or the focused subpath:

import { AsyncIter, Task } from '@hecto/async'

const values = AsyncIter.from([3, 1, 2])
const mapped = AsyncIter.mapConcurrent(
  values,
  async (value, _index, signal) => {
    await fetch(`/work/${value}`, { signal })
    return value * 2
  },
  { concurrency: 2 },
)

const ordered = await Task.run(AsyncIter.collect(mapped))
// [6, 2, 4] -- output order always matches source order

What's in the box

  • Task: lazy cancellable execution with one value parameter; expected failures are Task<Result<A, E>>
  • AsyncIter: lazy sync/async iterable pipelines with end-to-end cancellation
  • Combinators: value map/flatMap/tap, plus Result-aware mapError, catchError, and match
  • Concurrency: all, allSettled, race, any, parallel, sequential
  • Resilience: retry, timeout, fallback
  • Flow control: throttle, debounce, rateLimit
  • Collection: mapAsync, filterAsync, forEachAsync, reduceAsync, collectAsync

Docs

Workers (Bun)

CPU work runs on a Bun worker thread so the main thread stays responsive. Importing @hecto/async/workers starts no worker; @hecto/async/workers/bun is Bun-only (a bun export condition) and starts workers lazily on the first prepare/submit.

Task file — a named export in its own module; nothing but plain data crosses the boundary. Take WorkerContext as the second parameter when the loop needs cooperative cancellation:

// report.worker.ts
import type { WorkerContext } from '@hecto/async/workers'

export async function calculateReport(input: { values: readonly number[] }, context: WorkerContext) {
  let total = 0
  for (let i = 0; i < input.values.length; i++) {
    total += input.values[i]!
    if ((i & 4095) === 0) await context.checkpoint() // yields so CANCEL is seen, throws if aborted
  }
  return { total }
}

Reference — taskModule is inert; the typeof import is erased so the parent never loads task code:

import { taskModule } from '@hecto/async/workers'

export const calculateReportTask = taskModule<typeof import('./report.worker.js')>(
  new URL('./report.worker.js', import.meta.url),
).task('calculateReport')

Pool:

import { createPool } from '@hecto/async/workers/bun'

const pool = createPool({ size: 2, maxQueue: 32 })
try {
  const { total } = await pool.run(calculateReportTask, { values: [1, 2, 3] })
  const job = pool.submit(calculateReportTask, { values: [4, 5] }, { timeoutMs: 1000 })
  await job.result
  await job.completion
} finally {
  const closed = await pool.close({ graceMs: 1000 })
  // closed.status is 'drained' | 'incomplete'; closed.nativeExit is always 'unknown' on Bun 1.4.
}

Limits — size (default max(1, min(4, cores - 1))), maxQueue 64, maxPendingBytes 32 MiB, maxPayloadBytes 8 MiB, taskTimeoutMs 30 s, cancelGraceMs 100 ms, closeGraceMs 5 s. Input is snapshotted with structuredClone on admission (default copy; pass transfer to move ArrayBuffers). A full queue rejects synchronously with WorkerError failure: 'overloaded'; every infrastructure failure is a guarded WorkerError with a bounded failure discriminant, and cancellation is a WorkerCancelledError recognised by isCancellation. Three consecutive crashes mark the pool unhealthy.

nativeExit: 'unknown' — Bun reports close/terminate acknowledgements only; nothing confirms a thread has physically exited. close() therefore reports drained once every registered task settled and each worker acknowledged STOPPED, and nativeExit: 'unknown' always. Do not build shutdown logic that assumes the thread is gone.

Measured on a 14-core M-series host under foreign load (benchmarks/workers/overhead.mjs, responsiveness.mjs): ~19 µs per job sequentially and ~9 µs pipelined for a trivial task; a pool of 4 beats the main thread from about 0.5 ms of CPU per job; a 5 ms main-thread interval keeps a p99 under 7 ms while one or four 500 ms tasks run, against ~500 ms inline.