Skip to content

Repository files navigation

pollocks

A TypeScript job queue library backed by PostgreSQL. Jobs are stored as rows, locked via PostgreSQL functions, and processed by workers that support both polling and real-time NOTIFY/LISTEN.

Features

  • Durable job storage in PostgreSQL (13+)
  • Pattern-based job routing to handlers
  • Parallel workers with configurable concurrency
  • Two processing modes: polling and PostgreSQL NOTIFY/LISTEN
  • Automatic retries with configurable lock durations
  • Batch job creation and acquisition
  • Typed event system for observability
  • SQL migrations managed automatically via Umzug

Requirements

  • Node.js 18+ or Bun
  • PostgreSQL 13+
  • TypeScript 5+

Installation

npm install pollocks pg

pg is a peer dependency. You provide your own Pool instance.

Quick start

Run migrations

pollocks manages its own schema. Call migrate() once at startup:

importpgfrom"pg";import{Tools}from"pollocks";constpool=newpg.Pool({connectionString: "postgres://user:pass@localhost:5432/mydb",});consttools=newTools(pool);awaittools.migrate();

Add jobs

const{ id }=awaittools.addJob({pattern: "send-email",payload: {to: "user@example.com",subject: "Welcome",body: "Your account is ready.",},});

Process jobs with a worker

import{Worker}from"pollocks";constworker=newWorker(pool,{"send-email": async(job)=>{const{ to, subject, body }=job.payloadas{to: string;subject: string;body: string;};awaitsendEmail(to,subject,body);},});awaitworker.start();// Graceful shutdownprocess.on("SIGTERM",async()=>{awaitworker.stop();awaitpool.end();});

API

Tools

The Tools class provides direct access to all job queue operations.

import{Tools}from"pollocks";consttools=newTools(pool);

tools.migrate(): Promise<void>

Runs all pending SQL migrations. Safe to call on every startup; already-applied migrations are skipped.

tools.addJob(input): Promise<{ id: string }>

Enqueue a single job.

awaittools.addJob({pattern: "send-email",// required, routes to a handlerpayload: {to: "user@example.com"},// optional, defaults to {}runAfter: newDate("2025-01-01"),// optional, defaults to nowlockFor: 3600,// optional, lock duration in seconds, defaults to 3600});
FieldTypeDefaultDescription
patternstringrequiredRoutes the job to a matching handler
payloadRecord<string, unknown> | unknown[]{}Arbitrary JSON data attached to the job
runAfterDate | string | numbernew Date()Earliest time the job becomes eligible
lockFornumber3600Seconds a job stays locked during processing

tools.addJobs(inputs): Promise<{ id: string }[]>

Enqueue multiple jobs in a single database call.

constjobs=awaittools.addJobs([{pattern: "send-email",payload: {to: "a@example.com"}},{pattern: "send-email",payload: {to: "b@example.com"}},]);// jobs = [{ id: "01HX..." }, { id: "01HX..." }]

tools.acquireJob(lockedBy?, patterns?): Promise<Job | undefined>

Lock and return a single eligible job. Returns undefined if no job is available.

constjob=awaittools.acquireJob("worker-1",["send-email"]);

tools.acquireJobs(max, lockedBy?, patterns?): Promise<Job[]>

Lock and return up to max eligible jobs.

constjobs=awaittools.acquireJobs(10,"worker-1",["send-email"]);

tools.completeJob(id): Promise<void>

Mark a job as completed.

tools.completeJobs(ids): Promise<void>

Mark multiple jobs as completed.

tools.failJob(id, error?): Promise<void>

Mark a job as failed. The job will be retried if it has remaining attempts.

awaittools.failJob(job.id,"Connection timeout");

Worker

The Worker class handles job processing with automatic acquisition, execution, completion, and failure handling.

import{Worker}from"pollocks";constworker=newWorker(pool,handlers,config);

Parameters:

ParameterTypeDescription
poolPoolA pg connection pool
handlersMessageHandlersMap of pattern names to handler functions
configWorkerConfigOptional configuration

WorkerConfig

FieldTypeDefaultDescription
parallelismnumber1Number of concurrent runner loops
mode"poll" | "listen""poll"Processing mode
pollIntervalMsnumber2000Milliseconds between poll cycles
lockedBystringauto-generated ULIDIdentifier for this worker instance

worker.start(): Promise<void>

Start processing jobs. Spawns parallelism runner loops.

worker.stop(): Promise<void>

Graceful shutdown. Waits for all in-flight jobs to finish before returning.

worker.kill(): Promise<void>

Immediate shutdown. Marks all active jobs as failed and returns without waiting.

Events

The worker.events emitter provides typed events for observability:

worker.events.on("start",({ patterns })=>{console.log(`Listening for: ${patterns.join(", ")}`);});worker.events.on("success",({ job, durationMs })=>{console.log(`Job ${job.id} completed in ${durationMs}ms`);});worker.events.on("failure",({ job, error, durationMs })=>{console.error(`Job ${job.id} failed after ${durationMs}ms:`,error);});
EventPayloadDescription
start{ patterns: string[] }Worker has started
stop{}Worker has stopped
shutdown{ forced: boolean }Worker was killed
poll{ runnerId: number }Runner is polling for jobs
listen{ runnerId: number, pattern: string }Runner received a notification
acquire{ runnerId: number, job: Job }Runner acquired a job
success{ runnerId: number, job: Job, durationMs: number }Job completed successfully
failure{ runnerId: number, job: Job, error: unknown, durationMs: number }Job failed

Job

The Job type represents a row in the jobs table:

typeJob={id: string;created_at: Date;updated_at: Date|null;payload: Record<string,unknown>|unknown[];pattern: string;locked_by: string|null;locked_until: Date|null;locked_at: Date|null;last_error: string|null;run_after: Date;lock_for: number;attempts: number;max_attempts: number;};

Worker modes

Poll mode

The default mode. Each runner loop calls acquireJob() on a fixed interval. Simple, reliable, and works with any PostgreSQL deployment including managed services that restrict LISTEN.

constworker=newWorker(pool,handlers,{mode: "poll",pollIntervalMs: 1000,});

Listen mode

Uses PostgreSQL NOTIFY/LISTEN for near-instant job delivery. When a job is added, a notification triggers the worker to acquire it immediately. A periodic poll runs as a safety net to catch any missed notifications.

constworker=newWorker(pool,handlers,{mode: "listen",});

Listen mode holds one additional database connection for the LISTEN subscription.

License

MIT

About

PostgreSQL-backed job queue for Node.js/Bun

Resources

Contributing

Stars

3 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages