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

ravenflow

v0.4.1

Published

Type-safe job queue management for TypeScript

Readme

ravenflow

Typed background jobs for Node.js, on BullMQ. Redis holds the work in flight; MongoDB holds the backlogs and the history.

  • Types end to end. A send is checked against the job's declaration.
  • A client and a worker. Your API sends work, a worker process runs it, and both share one config.
  • Two triggers. A clock, and a spool: a backlog in MongoDB drained at the job's pace.
  • What production asks for. Chains, deduplication, retries, a rate held across the fleet, levers to pause a job, and a record of every execution.

Install

npm install ravenflow

Requires Node.js 20.19+, Redis and MongoDB. For development:

docker run -d -p 6379:6379 redis:7
docker run -d -p 27017:27017 mongo:7

Create and run a job

1. Declare it. A job is a handler from a typed payload to a typed result. The config registers it under a name.

// config.ts
import { defineConfig, defineJob } from 'ravenflow';

export const sendWelcomeEmail = defineJob<{ to: string; name: string }, { messageId: string }>({
  handler: async ({ params, logger }) => {
    logger.info({ to: params.to }, 'sending the welcome email');
    const { id } = await emailTransport.send({ to: params.to, subject: `Welcome, ${params.name}` });
    return { messageId: id };
  },
});

export const config = defineConfig({
  defaultQueue: 'default',
  jobs: { sendWelcomeEmail },
});

2. Send it from any process: your API, a CLI, another job.

// client.ts
import { createRavenflowClient } from 'ravenflow';
import { config } from './config';

const client = createRavenflowClient(config, {
  redis: { host: 'localhost', port: 6379 },
  mongo: 'mongodb://localhost:27017/myapp',
});

await client.jobs.sendWelcomeEmail.send({ to: '[email protected]', name: 'Ada' });

3. Run it in a worker. A worker serves one queue.

// worker.ts
import { createRavenflowWorker } from 'ravenflow';
import { config } from './config';

const worker = createRavenflowWorker(config, {
  redis: { host: 'localhost', port: 6379 },
  mongo: 'mongodb://localhost:27017/myapp',
  queue: 'default',
  concurrency: 10,
});

await worker.start();
process.once('SIGTERM', () => void worker.close());

The handler receives { params, logger, signal, job, continueWith, deduplicationId }. What it returns is the job's result; what it throws fails the job.

A send takes options: { delayMs, priority, deduplicationId }.

Triggers

Without a trigger, a client sends the job. A trigger feeds it for you.

A clock

import { defineJob } from 'ravenflow';

export const cleanupTempFiles = defineJob<void, { removed: number }>({
  trigger: { cron: '0 3 * * *' },
  handler: async () => ({ removed: await purgeTempDir() }),
});

One occurrence at a time across the fleet. A worker registers the schedules of its queue's jobs when it starts. A clock has nothing to pass, so the payload must be void or all-optional: anything else does not compile.

A spool

import { defineJob } from 'ravenflow';

export const enrichDocument = defineJob<PendingDocument, void>({
  pace: { perSecond: 10 },
  trigger: { spool: { name: 'pending_documents' } },
  handler: async ({ params }) => enrich(params),
});
await client.jobs.enrichDocument.pushMany([
  { url: 'https://example.com/a', tenantId: 'acme' },
  { url: 'https://example.com/b', tenantId: 'globex' },
]);

Items wait in MongoDB (rav:spool-backlog:<name>). Every 30 seconds, the worker serving the job's queue releases at most one tick's work at the job's pace into Redis, and an item leaves the backlog once its run ends. Options: tickMs, canPull, expireUnclaimedAfterMs.

Advanced

Chains. A handler that returns continueWith(nextParams) ends its link and queues the next one, each link a job of its own with its own retries.

import { defineJob } from 'ravenflow';

export const syncCatalog = defineJob<{ cursor?: string; synced?: number }, { synced: number }>({
  trigger: { cron: '0 2 * * *' },
  handler: async ({ params, continueWith }) => {
    const page = await api.fetchPage(params.cursor);
    await db.products.upsertMany(page.items);
    const synced = (params.synced ?? 0) + page.items.length;
    if (page.next) return continueWith({ cursor: page.next, synced });
    return { synced };
  },
});

Deduplication. deduplicate: true keeps one run of the job in flight at a time, deduplicate: (params) => id one per id, and send(params, { deduplicationId }) chooses for one send. A duplicate is ignored, and send resolves with the job already holding the id.

Retries. attempts: 5, backoff: { type: 'exponential', delayMs: 1_000 }. A handler runs at least once, not exactly once: make it safe to run twice.

Pace. pace: { perSecond: 2 } holds a rate for the whole job, across every worker process. A job that comes too early waits its turn without spending an attempt.

Levers. Every job has client.jobs.<name>.controls:

| Lever | On | Effect | | --- | --- | --- | | pauseExecution() / resumeExecution() | every job | Jobs are deferred, not lost, and no attempt is spent. | | pauseTrigger() / resumeTrigger() | a clock or a spool | Occurrences are skipped; a spool stops draining. | | pauseIngest() / resumeIngest() | a spool | push throws a SpoolIngestPausedError. | | purgeBacklog() | a spool | Drops the items never claimed. |

Job results. A worker writes one record per execution to MongoDB (rav:job-results): the params, the result or the error and its causes, what the handler logged, how long it ran. Kept 14 days, and counted per minute into rav:aggregated-results.

Withdrawal. A worker that cannot reach MongoDB for 30 seconds closes itself, sets process.exitCode to 1 and calls withdrawal.onWithdraw(reason). Let your process manager restart it.

Configuration

| defineConfig | Default | | --- | --- | | defaultQueue, jobs | required | | logger: { level, pretty } | debug and readable; info and JSON with NODE_ENV=production | | queuesRetention: { completedCount, failedCount } | 1000 each | | jobResultsRetentionHours | 336 | | aggregatedResultsRetentionHours | 2160 |

| createRavenflowWorker | Default | | --- | --- | | redis, mongo, queue | required | | concurrency | 5 | | withdrawal: { afterMs, onWithdraw } | 30 000 ms |

License

MIT