A tiny, promise-based task queue that runs jobs sequentially with explicit pause/resume and cancellation controls. Designed to keep a single consumer in-order, surface errors deterministically, and make queue status observable for UI or orchestration hooks.
- ✅ Single runner, in-order task execution (no accidental parallelism)
- ⏸️ Explicit pause/resume with backpressure-friendly blocking
- 🛑 Cancellation that flushes pending work and blocks stale generations from changing state
- 🚨 Optional
pauseOnErrorflow that captures the last task error and waits for a resume signal - 🕹️ Status callbacks for wiring into logs, metrics, or UI (
Idle→Processing→Paused/Cancelled) - 📦 Zero dependencies, ESM + CJS builds, typed with TypeScript
npm install @schie/queueRequires Node.js 20+.
import { Queue, QueueStatus } from '@schie/queue'
const queue = new Queue({
onStatusChange: (status) => console.log('status:', QueueStatus[status])
})
queue.addTask(async () => {
await doWork('first')
})
queue.addTask(async () => {
await doWork('second')
})
// Pause new work mid-flight
queue.pauseQueue()
setTimeout(() => {
// Clear any previous error and resume processing
queue.resumeQueue()
}, 500)const queue = new Queue({
onStatusChange: (status) => console.log('status:', QueueStatus[status]),
pauseOnError: true
})
queue.addTask(async () => {
throw new Error('oops')
})
queue.addTask(async () => doWork('after error')) // waits until resume
// When a task fails:
// - status flips to Paused
// - lastTaskError is set
// - processing waits until resumeQueue() is called
if (queue.lastTaskError) {
console.error('last error:', queue.lastTaskError.message)
queue.clearLastError()
queue.resumeQueue()
}queue.addTask(async () => doWork('maybe cancel me'))
queue.cancelQueue() // clears pending tasks and sets status Cancelled
// Later, adding a task resurrects the queue into a fresh generation
queue.addTask(async () => doWork('fresh start')) // status returns to Idle → Processingtype QueueOptions = {
onStatusChange?: (status: QueueStatus) => void;
pauseOnError?: boolean;
};
new Queue(options?: QueueOptions);onStatusChangefires only on real status transitions.pauseOnErrortoggles whether task errors pause the queue and are surfaced vialastTaskError(defaults tofalse).
addTask(task: () => Promise<void>)— enqueue a task; auto-starts if idle and auto-resurrects after cancellation.pauseQueue()— transition toPausedif currently processing.resumeQueue()— clearslastTaskError, transitions back toProcessing, and unblocks paused processing. Also restarts if idle with pending work.cancelQueue()— set status toCancelled, flush pending tasks, and invalidate any in-flight runner.clearQueue()— remove pending tasks; leaves statusIdlewhen not processing/paused/cancelled.clearLastError()— resetlastTaskErrorwithout changing status.addNextTask(task: () => Promise<void>)— enqueue a task to run before other pending tasks (after the current in-flight task); auto-starts if idle and auto-resurrects after cancellation.
status: QueueStatus— current lifecycle state (Idle,Processing,Paused,Cancelled).isProcessing | isPaused | isCancelled | isIdle— boolean helpers.size: number— pending task count.lastTaskError: Error | null— most recent error whenpauseOnErroris enabled.
- Single runner: tasks execute sequentially; the queue never introduces parallelism.
- Generation guard: cancellation increments an internal version so stale runners cannot revert status later.
- Draining: when the queue empties (and not cancelled), status returns to
Idle. - Pausing: while paused, processing blocks until
resumeQueueis called.
npm test -- --watchman=false— run the Jest suite with coverage (keep it at 100%).npm run build— emit ESM/CJS builds and types.npm run lint— lint the repo (plus lockfile validation).
PRs are welcome. Please:
- Keep behavior changes aligned with the TypeScript source and this README.
- Preserve the single-consumer, in-order contract and status integrity.
- Add or update tests to maintain 100% coverage.
- Use the provided
npmscripts instead of bespoke commands.