Skip to content
AkkilMGPublic

About

Lightweight structured async task orchestration for Node.js — concurrency, cancellation, retries, task groups, and plugins. Zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

orqis

Structured async task orchestration for Node.js

The gap between p-limit and Bull — without Redis, without boilerplate.

npm version npm downloads Bundle Size License: MIT TypeScript Node


Install

npm install orqis
# or
pnpm add orqis
# or
yarn add orqis

Requirements: Node.js ≥ 16. Works with ESM (import) and CommonJS (require) — zero configuration. Zero production dependencies.


Quick Start

import { TaskQueue } from 'orqis';

const queue = new TaskQueue({ concurrency: 5 });

const urls = [
  'https://api.example.com/users/1',
  'https://api.example.com/users/2',
  // ... hundreds more
];

const users = await queue.addAll(
  urls.map(url => async ({ signal }) => {
    const res = await fetch(url, { signal });
    if (!res.ok) throw new Error(`HTTP ${res.status}`);
    return res.json();
  })
);

console.log(`Fetched ${users.length} users`);

That's it. Concurrency capped at 5. Cancellation-ready. TypeScript-inferred.


Quick Decision Matrix

Your Need Use This
Cap concurrency, nothing else p-limit (~1 KB)
Concurrency + priorities + timeouts p-queue
Concurrency + retries + cancellation + events + plugins orqis (~5 KB)
Redis-backed persistent queue across servers BullMQ
Cron / recurring jobs Bree or node-cron
API rate limiting Bottleneck

Orqis is for in-process async orchestration. If your tasks live in a single Node.js process and you need more than a concurrency cap but less than a full Redis-backed job queue, Orqis is the right tool.


The Problem Orqis Solves

You're building a CLI tool, a build script, or a background task runner. You have async work — lots of it — and you need more than raw Promise.all.

Promise.all starts everything at once. Feed it 10,000 URLs and you'll saturate the CPU, exhaust file handles, and get rate-limited by every API in sight. You need:

  • Concurrency control — process 8 at a time, not 10,000
  • Retries — transient failures happen; tasks should recover without boilerplate
  • Timeouts — a hung network call shouldn't stall your entire pipeline
  • Cancellation — Ctrl-C should clean up, not leak connections
  • Observability — which tasks ran, which failed, how long they took

The existing solutions force a compromise: reach for p-limit and build everything else yourself, or adopt Bull/BullMQ and bring in Redis middleware infrastructure. Orqis fills the gap — production-grade orchestration with zero infrastructure.


Why Orqis

Resilience

  • Built-in retry + backoff — exponential, fixed, with jitter. No p-retry wrapper needed.
  • Per-task timeouts — hung requests get aborted, not awaited forever.
  • stopOnError — halt the pipeline on first failure for build scripts and CI.

Control

  • AbortSignal-native cancellation — ctx.signal on every task. Pass it to fetch, streams, database clients. Cancellation propagates cleanly through the call chain.
  • TaskGroup scoped cancellation — cancel only the compile phase without touching lint. The missing primitive for structured concurrency.
  • Priority queue — binary max-heap; critical tasks jump the line.

Observability

  • Lifecycle events — start, success, error, retry, timeout, cancel, idle, empty, active. Every event has typed payloads.
  • Plugin system — onBefore, onAfter, onError, onCancel hooks. Built-in logging, metrics, and retry observer. Write custom plugins for Sentry, OpenTelemetry, caching, etc.
  • onIdle() / onEmpty() promises — await completion without polling.

Simplicity

  • Zero dependencies — 5 KB minzipped. No node_modules baggage, no CVE surface.
  • TypeScript-first — strict generics, typed events, no any leakage.
  • Dual ESM + CJS — one package, both module systems, no config.
  • No infrastructure — runs in-process. No Redis, no Docker, no worker processes.

When Should You Use Orqis

Use Orqis when your answer to one or more of these is "yes":

  • You're writing a CLI, build script, test runner, or data pipeline in Node.js
  • You need to run N async tasks but cap parallelism below some limit
  • Some of those tasks call unreliable external services that need retry logic
  • You need to handle Ctrl-C / graceful shutdown cleanly
  • You want to log or monitor task progress without wiring up a full observability stack
  • You have batches of related tasks that should be cancellable as a unit
  • You need different tasks to run with different priorities (critical path vs. background)

Consider alternatives when:

  • Tasks must survive process restarts → BullMQ (Redis-backed)
  • Work distributes across machines → BullMQ or Agenda
  • Recurring cron-style jobs needed → Bree or node-cron
  • Only need concurrency control → p-limit
  • Reactive stream processing → RxJS

Core Concepts

Tasks

A task is any async function. It receives { signal: AbortSignal } and returns a value. That's the entire interface.

const task = async ({ signal }: { signal: AbortSignal }) => {
  const data = await fetch('/api', { signal });
  return data.json();
};

const result = await queue.add(task);

Pending vs Active

Tasks exist in one of two states. Pending tasks are queued and waiting for a concurrency slot. Active tasks are running. queue.size is pending; queue.pending is active.

Method Effect
queue.clear() Removes pending tasks (running continue)
queue.cancel() Removes pending + aborts running
queue.pause() Stops dequeuing (running continue)

queue.onIdle() vs queue.onEmpty()

onIdle() resolves when pending = 0 AND running = 0 — everything is done. onEmpty() resolves when the pending queue drains, even if tasks are still running.

TaskGroup

A group is a named batch of tasks that can be awaited or cancelled as a unit, without affecting other work on the same queue.

import { TaskGroup } from 'orqis/group';

const queue  = new TaskQueue({ concurrency: 8 });
const build  = new TaskGroup(queue, { id: 'build' });
const test   = new TaskGroup(queue, { id: 'test' });

build.cancel();            // only build tasks abort
await test.onComplete();   // wait for only test tasks

Comparison

orqis p-limit p-queue Bottleneck Bull
Size ~5 KB ~1 KB ~25 KB publish ~15 KB ~229 KB
Dependencies 0 0 1 0 8+
ESM + CJS ✅ ✅ ESM only ✅ ✅
TypeScript-first ✅ ✅ ✅ ❌ ✅
Concurrency cap ✅ ✅ ✅ ✅ ✅
Priority queue ✅ ❌ ✅ ✅ ✅
AbortSignal cancellation ✅ ❌ partial ❌ ❌
Structured concurrency (TaskGroup) ✅ ❌ ❌ ❌ ❌
Built-in retry + backoff ✅ ❌ ❌ ❌ ✅
Per-task timeouts ✅ ❌ ✅ ❌ ✅
Lifecycle events ✅ ❌ ✅ ❌ ✅
Plugin system ✅ ❌ ❌ ❌ ❌
Infrastructure required none none none none Redis
Maintenance status Active Active Active Stale (2019) Active

The only things Orqis deliberately omits are persistence, clustering, and cron scheduling — all of which require external infrastructure that most in-process use cases don't need.


Graceful Shutdown

import { TaskQueue } from 'orqis';

const queue = new TaskQueue({ concurrency: 8 });

process.on('SIGINT', async () => {
  console.log('\nShutting down gracefully...');
  queue.pause();
  await queue.onIdle();
  process.exit(0);
});

process.on('SIGTERM', () => {
  queue.cancel();
  process.exit(1);
});

API at a Glance

new TaskQueue(options?)

// Methods
queue.add(task, opts?)      // → Promise<T>
queue.addAll(tasks, opts?)  // → Promise<T[]>
queue.onIdle()              // → Promise<void>
queue.onEmpty()             // → Promise<void>
queue.pause() / resume()
queue.clear() / cancel()
queue.on/off/once(event, fn)

// Properties
queue.size      // pending count
queue.pending   // active count
queue.isPaused
queue.signal    // AbortSignal

// Events
'on start'     → { id }
'on success'   → { id, result, durationMs }
'on error'     → { id, error, attempt }
'on retry'     → { id, attempt, delay }
'on timeout'   → { id }
'on cancel'    → { id }
'on idle' / 'on empty' / 'on active'

Full docs: API Reference


Plugin System

import { loggingPlugin, metricsPlugin, retryObserverPlugin } from 'orqis/plugins';

queue.use(loggingPlugin({ prefix: '[build]', verbose: true }));

const { plugin, snapshot } = metricsPlugin();
queue.use(plugin);
await queue.onIdle();
console.log(snapshot());

queue.use(retryObserverPlugin((id, attempt, delay) => {
  console.warn(`Task ${id} retrying (${attempt}) in ${delay}ms`);
}));

See the Plugin Guide for custom plugin examples (cache, Sentry, OpenTelemetry).


What's Next (Roadmap)

Version Focus
v1.0 queue.use() on TaskQueue, rate limiting, 100% coverage
v1.x OpenTelemetry plugin, queue.stats(), progress events
v2.0 Browser/Worker support, Deno/Bun, optional persistence

See the full roadmap.


Documentation

Page Description
Getting Started Step-by-step from install to first queue
Core Concepts How Orqis works internally
Recipes Copy-paste patterns for common scenarios
Plugin System Logging, metrics, tracing, and custom middleware
API Reference Complete type signatures and option tables
Migration Guide Moving from p-limit, p-queue, async, fastq
Testing Guide How to test queue-based code reliably
Comparison Detailed survey of the ecosystem
Roadmap Planned features and release milestones
Wiki Troubleshooting, FAQs, common errors

Contributing

git clone https://github.com/AkkilMG/orqis.git
cd orqis && npm install
npm test          # run test suite
npm run build     # compile dist/
npm run typecheck # type-check src/
npm run lint      # ESLint

Read CONTRIBUTING.md before opening a PR. Bug reports go in Issues. Questions go in Discussions.


License

MIT © Akkil M G

About

Lightweight structured async task orchestration for Node.js — concurrency, cancellation, retries, task groups, and plugins. Zero dependencies.

Topics

Resources

Code of conduct

Contributing

Security policy

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages