pglite-queue 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1 @@
1
+ {"version":3,"sources":["../src/index.ts","../src/queue.ts","../src/types.ts","../src/events.ts","../src/backoff.ts","../src/cron.ts","../src/worker.ts","../src/schema.ts","../src/delay.ts"],"sourcesContent":["export { Queue } from './queue.js'\nexport { parseCron } from './cron.js'\nexport { parseDelay } from './delay.js'\nexport { calculateBackoff } from './backoff.js'\n\nexport type {\n Job,\n JobContext,\n JobHandler,\n JobOptions,\n JobStatus,\n JobFilter,\n QueueOptions,\n QueueEventMap,\n QueueEventName,\n BackoffOptions,\n} from './types.js'\n","import { PGlite } from '@electric-sql/pglite'\nimport type {\n QueueOptions,\n JobOptions,\n JobHandler,\n HandlerEntry,\n Job,\n JobRow,\n JobFilter,\n QueueEventMap,\n QueueEventName,\n} from './types.js'\nimport { rowToJob } from './types.js'\nimport { TypedEmitter } from './events.js'\nimport { Worker } from './worker.js'\nimport { runMigrations, recoverStalledJobs } from './schema.js'\nimport { parseDelay } from './delay.js'\nimport { parseCron } from './cron.js'\n\nconst ADD_JOB_SQL = `\nINSERT INTO pglite_queue_jobs (task, data, status, priority, run_at, max_attempts)\nVALUES ($1, $2, $3, $4, NOW() + make_interval(secs => $5::double precision), $6)\nRETURNING *;\n`\n\nconst ADD_CRON_SQL = `\nINSERT INTO pglite_queue_jobs (task, data, status, priority, run_at, max_attempts, cron_expr, cron_name)\nVALUES ($1, $2, 'delayed', $3, $4, $5, $6, $7)\nON CONFLICT (cron_name) WHERE cron_name IS NOT NULL AND status NOT IN ('completed', 'failed')\nDO UPDATE SET run_at = EXCLUDED.run_at, data = EXCLUDED.data, cron_expr = EXCLUDED.cron_expr\nRETURNING *;\n`\n\nconst GET_JOB_SQL = `SELECT * FROM pglite_queue_jobs WHERE id = $1;`\n\nconst REMOVE_JOB_SQL = `DELETE FROM pglite_queue_jobs WHERE id = $1;`\n\nexport class Queue {\n private db: PGlite | null = null\n private ownsDb: boolean\n private dataDir: string\n private concurrency: number\n private pollInterval: number\n private shutdownTimeout: number\n private handleSignals: boolean\n\n private handlers = new Map<string, HandlerEntry>()\n private emitter = new TypedEmitter()\n private worker: Worker | null = null\n private pollTimer: ReturnType<typeof setInterval> | null = null\n private unsubscribeNotify: (() => void) | null = null\n private started = false\n private signalHandlers: { signal: string; handler: () => void }[] = []\n private migrated = false\n\n constructor(options: QueueOptions = {}) {\n if (options.db) {\n this.db = options.db\n this.ownsDb = false\n this.dataDir = ''\n } else {\n this.ownsDb = true\n this.dataDir = options.dataDir ?? 'memory://'\n }\n\n this.concurrency = options.concurrency ?? 1\n this.pollInterval = options.pollInterval ?? 5000\n this.shutdownTimeout = options.shutdownTimeout ?? 30000\n this.handleSignals = options.handleSignals ?? false\n }\n\n /**\n * Register a job handler for a task name.\n */\n define<T = unknown>(task: string, handler: JobHandler<T>, options?: { concurrency?: number }): void {\n this.handlers.set(task, {\n handler: handler as JobHandler,\n concurrency: options?.concurrency,\n })\n }\n\n /**\n * Add a job to the queue.\n */\n async add<T = unknown>(task: string, data: T, options?: JobOptions): Promise<Job<T>> {\n const db = await this.ensureReady()\n\n const delaySecs = options?.delay ? parseDelay(options.delay) / 1000 : 0\n const status = delaySecs > 0 ? 'delayed' : 'pending'\n const priority = options?.priority ?? 0\n const maxAttempts = (options?.retry ?? 0) + 1\n\n const result = await db.query<JobRow>(ADD_JOB_SQL, [\n task,\n JSON.stringify(data),\n status,\n priority,\n delaySecs,\n maxAttempts,\n ])\n\n return rowToJob<T>(result.rows[0])\n }\n\n /**\n * Register a recurring cron job.\n */\n async every<T = unknown>(cronExpr: string, task: string, data?: T, options?: JobOptions): Promise<Job<T>> {\n const db = await this.ensureReady()\n\n // Validate the cron expression\n const cron = parseCron(cronExpr)\n const nextRun = cron.nextRun()\n\n const priority = options?.priority ?? 0\n const maxAttempts = (options?.retry ?? 0) + 1\n\n const result = await db.query<JobRow>(ADD_CRON_SQL, [\n task,\n JSON.stringify(data ?? {}),\n priority,\n nextRun.toISOString(),\n maxAttempts,\n cronExpr,\n task, // cron_name defaults to task name\n ])\n\n return rowToJob<T>(result.rows[0])\n }\n\n /**\n * Start processing jobs.\n */\n async start(): Promise<void> {\n if (this.started) return\n this.started = true\n\n const db = await this.ensureReady()\n\n // Recover stalled jobs from previous crash\n const recovered = await recoverStalledJobs(db)\n if (recovered > 0) {\n this.emitter.emit('error', new Error(`Recovered ${recovered} stalled jobs from previous crash`))\n }\n\n // Create worker\n this.worker = new Worker(db, {\n concurrency: this.concurrency,\n handlers: this.handlers,\n emitter: this.emitter,\n })\n\n // Subscribe to NOTIFY for instant job pickup\n try {\n const unsub = await db.listen('pglite_queue_new_job', () => {\n if (this.worker && !this.worker.isStopping) {\n void this.worker.poll()\n }\n })\n this.unsubscribeNotify = () => unsub()\n } catch {\n // LISTEN not supported — fall back to polling only\n }\n\n // Start fallback polling interval\n this.pollTimer = setInterval(() => {\n if (this.worker && !this.worker.isStopping) {\n void this.worker.poll()\n }\n }, this.pollInterval)\n\n // Register signal handlers if requested\n if (this.handleSignals) {\n for (const signal of ['SIGINT', 'SIGTERM'] as const) {\n const handler = () => {\n void this.stop()\n }\n process.on(signal, handler)\n this.signalHandlers.push({ signal, handler })\n }\n }\n\n // Initial poll to pick up any pending jobs\n void this.worker.poll()\n }\n\n /**\n * Gracefully stop processing. Waits for active jobs to finish.\n */\n async stop(): Promise<void> {\n if (!this.started) return\n this.started = false\n\n // Stop polling\n if (this.pollTimer) {\n clearInterval(this.pollTimer)\n this.pollTimer = null\n }\n\n // Unsubscribe from NOTIFY\n if (this.unsubscribeNotify) {\n this.unsubscribeNotify()\n this.unsubscribeNotify = null\n }\n\n // Signal worker to stop\n if (this.worker) {\n this.worker.stop()\n\n // Wait for active jobs with timeout\n if (this.worker.active > 0) {\n const timeout = new Promise<void>((resolve) =>\n setTimeout(resolve, this.shutdownTimeout)\n )\n await Promise.race([this.worker.waitForDrain(), timeout])\n }\n }\n\n // Remove signal handlers\n for (const { signal, handler } of this.signalHandlers) {\n process.removeListener(signal, handler)\n }\n this.signalHandlers = []\n\n // Close DB if we own it\n if (this.ownsDb && this.db) {\n await this.db.close()\n this.db = null\n }\n }\n\n /**\n * Get a job by ID.\n */\n async getJob<T = unknown>(id: number): Promise<Job<T> | null> {\n const db = await this.ensureReady()\n const result = await db.query<JobRow>(GET_JOB_SQL, [id])\n return result.rows[0] ? rowToJob<T>(result.rows[0]) : null\n }\n\n /**\n * Get jobs matching a filter.\n */\n async getJobs<T = unknown>(filter?: JobFilter): Promise<Job<T>[]> {\n const db = await this.ensureReady()\n\n const conditions: string[] = []\n const params: unknown[] = []\n let paramIdx = 1\n\n if (filter?.status) {\n const statuses = Array.isArray(filter.status) ? filter.status : [filter.status]\n conditions.push(`status = ANY($${paramIdx})`)\n params.push(statuses)\n paramIdx++\n }\n\n if (filter?.task) {\n conditions.push(`task = $${paramIdx}`)\n params.push(filter.task)\n paramIdx++\n }\n\n const where = conditions.length > 0 ? `WHERE ${conditions.join(' AND ')}` : ''\n const limit = filter?.limit ? `LIMIT ${filter.limit}` : ''\n const offset = filter?.offset ? `OFFSET ${filter.offset}` : ''\n\n const sql = `SELECT * FROM pglite_queue_jobs ${where} ORDER BY created_at DESC ${limit} ${offset};`\n const result = await db.query<JobRow>(sql, params)\n return result.rows.map((row) => rowToJob<T>(row))\n }\n\n /**\n * Remove a job by ID.\n */\n async removeJob(id: number): Promise<boolean> {\n const db = await this.ensureReady()\n const result = await db.query(REMOVE_JOB_SQL, [id])\n return (result.affectedRows ?? 0) > 0\n }\n\n /**\n * Remove all completed/failed jobs.\n */\n async clean(status?: 'completed' | 'failed'): Promise<number> {\n const db = await this.ensureReady()\n const statuses = status ? [status] : ['completed', 'failed']\n const result = await db.query(\n `DELETE FROM pglite_queue_jobs WHERE status = ANY($1);`,\n [statuses]\n )\n return result.affectedRows ?? 0\n }\n\n /**\n * Delete all jobs (for testing).\n */\n async obliterate(): Promise<void> {\n const db = await this.ensureReady()\n await db.exec('DELETE FROM pglite_queue_jobs;')\n }\n\n /**\n * Get job counts by status.\n */\n async counts(): Promise<Record<string, number>> {\n const db = await this.ensureReady()\n const result = await db.query<{ status: string; count: string }>(\n `SELECT status, COUNT(*)::text as count FROM pglite_queue_jobs GROUP BY status;`\n )\n const counts: Record<string, number> = {}\n for (const row of result.rows) {\n counts[row.status] = parseInt(row.count, 10)\n }\n return counts\n }\n\n // --- Event delegation ---\n\n on<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.on(event, listener)\n return this\n }\n\n off<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.off(event, listener)\n return this\n }\n\n once<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.once(event, listener)\n return this\n }\n\n // --- Internal ---\n\n private async ensureDb(): Promise<PGlite> {\n if (!this.db) {\n this.db = new PGlite(this.dataDir)\n await this.db.waitReady\n }\n return this.db\n }\n\n private async ensureReady(): Promise<PGlite> {\n const db = await this.ensureDb()\n if (!this.migrated) {\n await runMigrations(db)\n this.migrated = true\n }\n return db\n }\n}\n","import type { PGlite } from '@electric-sql/pglite'\n\n// --- Job Status ---\n\nexport type JobStatus = 'pending' | 'active' | 'completed' | 'failed' | 'delayed'\n\n// --- Job Row (database representation) ---\n\nexport interface JobRow {\n id: number\n task: string\n data: unknown\n status: JobStatus\n priority: number\n run_at: string\n started_at: string | null\n completed_at: string | null\n failed_at: string | null\n attempts: number\n max_attempts: number\n last_error: string | null\n progress: number\n result: unknown\n cron_expr: string | null\n cron_name: string | null\n created_at: string\n updated_at: string\n}\n\n// --- Job (public representation) ---\n\nexport interface Job<T = unknown> {\n id: number\n task: string\n data: T\n status: JobStatus\n priority: number\n runAt: Date\n startedAt: Date | null\n completedAt: Date | null\n failedAt: Date | null\n attempts: number\n maxAttempts: number\n lastError: string | null\n progress: number\n result: unknown\n cronExpr: string | null\n createdAt: Date\n updatedAt: Date\n}\n\n// --- Job Context (passed to handlers) ---\n\nexport interface JobContext<T = unknown> {\n id: number\n task: string\n data: T\n attempts: number\n maxAttempts: number\n progress: (pct: number) => Promise<void>\n}\n\n// --- Handler ---\n\nexport type JobHandler<T = unknown> = (job: JobContext<T>) => Promise<unknown>\n\nexport interface HandlerEntry {\n handler: JobHandler\n concurrency?: number\n}\n\n// --- Options ---\n\nexport interface BackoffOptions {\n type?: 'exponential' | 'fixed'\n baseDelay?: number\n maxDelay?: number\n factor?: number\n}\n\nexport interface JobOptions {\n retry?: number\n delay?: number | string\n priority?: number\n backoff?: BackoffOptions\n}\n\nexport interface QueueOptions {\n dataDir?: string\n db?: PGlite\n concurrency?: number\n pollInterval?: number\n shutdownTimeout?: number\n handleSignals?: boolean\n}\n\n// --- Events ---\n\nexport interface QueueEventMap {\n active: [job: Job]\n completed: [job: Job]\n failed: [job: Job, error: Error]\n retrying: [job: Job, attempt: number]\n progress: [job: Job, progress: number]\n drained: []\n error: [error: Error]\n}\n\nexport type QueueEventName = keyof QueueEventMap\n\n// --- Filters ---\n\nexport interface JobFilter {\n status?: JobStatus | JobStatus[]\n task?: string\n limit?: number\n offset?: number\n}\n\n// --- Helpers ---\n\nexport function rowToJob<T = unknown>(row: JobRow): Job<T> {\n return {\n id: row.id,\n task: row.task,\n data: row.data as T,\n status: row.status,\n priority: row.priority,\n runAt: new Date(row.run_at),\n startedAt: row.started_at ? new Date(row.started_at) : null,\n completedAt: row.completed_at ? new Date(row.completed_at) : null,\n failedAt: row.failed_at ? new Date(row.failed_at) : null,\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n lastError: row.last_error,\n progress: row.progress,\n result: row.result,\n cronExpr: row.cron_expr,\n createdAt: new Date(row.created_at),\n updatedAt: new Date(row.updated_at),\n }\n}\n","import { EventEmitter } from 'node:events'\nimport type { QueueEventMap, QueueEventName } from './types.js'\n\nexport class TypedEmitter {\n private emitter = new EventEmitter()\n\n on<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.on(event, listener as (...args: unknown[]) => void)\n return this\n }\n\n off<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.off(event, listener as (...args: unknown[]) => void)\n return this\n }\n\n once<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this {\n this.emitter.once(event, listener as (...args: unknown[]) => void)\n return this\n }\n\n emit<K extends QueueEventName>(event: K, ...args: QueueEventMap[K]): boolean {\n return this.emitter.emit(event, ...args)\n }\n\n removeAllListeners(event?: QueueEventName): this {\n this.emitter.removeAllListeners(event)\n return this\n }\n}\n","import type { BackoffOptions } from './types.js'\n\n/**\n * Calculate backoff delay in milliseconds for a given attempt number.\n * Attempt numbering starts at 1.\n */\nexport function calculateBackoff(attempt: number, options?: BackoffOptions): number {\n const type = options?.type ?? 'exponential'\n const baseDelay = options?.baseDelay ?? 1_000\n const maxDelay = options?.maxDelay ?? 300_000\n const factor = options?.factor ?? 2\n\n if (type === 'fixed') {\n return baseDelay\n }\n\n // Exponential: base * factor^(attempt - 1)\n const exponential = baseDelay * Math.pow(factor, attempt - 1)\n\n // Add 0-25% jitter to prevent thundering herd\n const jitter = exponential * Math.random() * 0.25\n\n return Math.min(Math.round(exponential + jitter), maxDelay)\n}\n","// Minimal 5-field cron expression parser (UTC).\n// Format: minute hour day-of-month month day-of-week\n// Supports: star, star/N, N-M, N,M,O, and single values.\n// Day-of-week: 0-6 (0 = Sunday).\n\ninterface FieldDef {\n min: number\n max: number\n}\n\nconst FIELDS: FieldDef[] = [\n { min: 0, max: 59 }, // minute\n { min: 0, max: 23 }, // hour\n { min: 1, max: 31 }, // day of month\n { min: 1, max: 12 }, // month\n { min: 0, max: 6 }, // day of week\n]\n\nfunction parseField(field: string, def: FieldDef): Set<number> {\n const values = new Set<number>()\n\n for (const part of field.split(',')) {\n const trimmed = part.trim()\n\n if (trimmed === '*') {\n for (let i = def.min; i <= def.max; i++) values.add(i)\n continue\n }\n\n const stepMatch = trimmed.match(/^(?:(\\d+)-(\\d+)|\\*)\\/(\\d+)$/)\n if (stepMatch) {\n const start = stepMatch[1] !== undefined ? parseInt(stepMatch[1], 10) : def.min\n const end = stepMatch[2] !== undefined ? parseInt(stepMatch[2], 10) : def.max\n const step = parseInt(stepMatch[3] || '1', 10)\n if (step === 0) throw new Error(`Invalid cron step: \"${trimmed}\"`)\n for (let i = start; i <= end; i += step) values.add(i)\n continue\n }\n\n const rangeStepMatch = trimmed.match(/^(\\d+)-(\\d+)\\/(\\d+)$/)\n if (rangeStepMatch) {\n const start = parseInt(rangeStepMatch[1], 10)\n const end = parseInt(rangeStepMatch[2], 10)\n const step = parseInt(rangeStepMatch[3], 10)\n if (step === 0) throw new Error(`Invalid cron step: \"${trimmed}\"`)\n for (let i = start; i <= end; i += step) values.add(i)\n continue\n }\n\n const rangeMatch = trimmed.match(/^(\\d+)-(\\d+)$/)\n if (rangeMatch) {\n const start = parseInt(rangeMatch[1], 10)\n const end = parseInt(rangeMatch[2], 10)\n for (let i = start; i <= end; i++) values.add(i)\n continue\n }\n\n const num = parseInt(trimmed, 10)\n if (isNaN(num) || num < def.min || num > def.max) {\n throw new Error(`Invalid cron value \"${trimmed}\" for range ${def.min}-${def.max}`)\n }\n values.add(num)\n }\n\n return values\n}\n\nexport interface CronExpression {\n nextRun(from?: Date): Date\n matches(date: Date): boolean\n}\n\nexport function parseCron(expr: string): CronExpression {\n const parts = expr.trim().split(/\\s+/)\n if (parts.length !== 5) {\n throw new Error(`Invalid cron expression: \"${expr}\". Expected 5 fields (minute hour dom month dow).`)\n }\n\n const minutes = parseField(parts[0], FIELDS[0])\n const hours = parseField(parts[1], FIELDS[1])\n const doms = parseField(parts[2], FIELDS[2])\n const months = parseField(parts[3], FIELDS[3])\n const dows = parseField(parts[4], FIELDS[4])\n\n function matches(date: Date): boolean {\n return (\n minutes.has(date.getUTCMinutes()) &&\n hours.has(date.getUTCHours()) &&\n doms.has(date.getUTCDate()) &&\n months.has(date.getUTCMonth() + 1) &&\n dows.has(date.getUTCDay())\n )\n }\n\n function nextRun(from?: Date): Date {\n const d = from ? new Date(from.getTime()) : new Date()\n // Advance by 1 minute, floor to minute boundary\n d.setUTCSeconds(0, 0)\n d.setUTCMinutes(d.getUTCMinutes() + 1)\n\n // Safety limit: search up to 4 years ahead\n const limit = d.getTime() + 4 * 365 * 24 * 60 * 60 * 1000\n\n while (d.getTime() < limit) {\n if (!months.has(d.getUTCMonth() + 1)) {\n d.setUTCMonth(d.getUTCMonth() + 1, 1)\n d.setUTCHours(0, 0, 0, 0)\n continue\n }\n if (!doms.has(d.getUTCDate()) || !dows.has(d.getUTCDay())) {\n d.setUTCDate(d.getUTCDate() + 1)\n d.setUTCHours(0, 0, 0, 0)\n continue\n }\n if (!hours.has(d.getUTCHours())) {\n d.setUTCHours(d.getUTCHours() + 1, 0, 0, 0)\n continue\n }\n if (!minutes.has(d.getUTCMinutes())) {\n d.setUTCMinutes(d.getUTCMinutes() + 1, 0, 0)\n continue\n }\n return d\n }\n\n throw new Error(`No matching cron time found within 4 years for expression: \"${expr}\"`)\n }\n\n return { nextRun, matches }\n}\n","import type { PGlite } from '@electric-sql/pglite'\nimport type { HandlerEntry, JobRow, JobContext } from './types.js'\nimport { rowToJob } from './types.js'\nimport { calculateBackoff } from './backoff.js'\nimport { parseCron } from './cron.js'\nimport { TypedEmitter } from './events.js'\n\nconst FETCH_SQL = `\nUPDATE pglite_queue_jobs\nSET status = 'active', started_at = NOW(), attempts = attempts + 1\nWHERE id = (\n SELECT id FROM pglite_queue_jobs\n WHERE status IN ('pending', 'delayed')\n AND run_at <= NOW()\n ORDER BY priority ASC, run_at ASC, id ASC\n LIMIT 1\n FOR UPDATE SKIP LOCKED\n)\nRETURNING *;\n`\n\nconst COMPLETE_SQL = `\nUPDATE pglite_queue_jobs\nSET status = 'completed', completed_at = NOW(), result = $2, progress = 100\nWHERE id = $1\nRETURNING *;\n`\n\nconst FAIL_SQL = `\nUPDATE pglite_queue_jobs\nSET status = 'failed', failed_at = NOW(), last_error = $2\nWHERE id = $1\nRETURNING *;\n`\n\nconst RETRY_SQL = `\nUPDATE pglite_queue_jobs\nSET status = 'delayed', failed_at = NOW(), last_error = $2, run_at = NOW() + make_interval(secs => $3::double precision)\nWHERE id = $1\nRETURNING *;\n`\n\nconst PROGRESS_SQL = `\nUPDATE pglite_queue_jobs SET progress = $2 WHERE id = $1;\n`\n\nconst INSERT_CRON_NEXT_SQL = `\nINSERT INTO pglite_queue_jobs (task, data, status, priority, run_at, max_attempts, cron_expr, cron_name)\nVALUES ($1, $2, 'delayed', $3, $4, $5, $6, $7)\nON CONFLICT (cron_name) WHERE cron_name IS NOT NULL AND status NOT IN ('completed', 'failed')\nDO UPDATE SET run_at = EXCLUDED.run_at, data = EXCLUDED.data;\n`\n\nexport interface WorkerOptions {\n concurrency: number\n handlers: Map<string, HandlerEntry>\n emitter: TypedEmitter\n}\n\nexport class Worker {\n private db: PGlite\n private handlers: Map<string, HandlerEntry>\n private emitter: TypedEmitter\n private concurrency: number\n private activeCount = 0\n private stopping = false\n private pollInProgress = false\n private pollQueued = false\n private drainResolve: (() => void) | null = null\n\n constructor(db: PGlite, options: WorkerOptions) {\n this.db = db\n this.handlers = options.handlers\n this.emitter = options.emitter\n this.concurrency = options.concurrency\n }\n\n async poll(): Promise<void> {\n if (this.stopping) return\n\n if (this.pollInProgress) {\n this.pollQueued = true\n return\n }\n\n this.pollInProgress = true\n try {\n await this.doPoll()\n } catch {\n // DB might be closing — ignore\n } finally {\n this.pollInProgress = false\n if (this.pollQueued && !this.stopping) {\n this.pollQueued = false\n void this.poll()\n }\n }\n }\n\n private async doPoll(): Promise<void> {\n while (this.activeCount < this.concurrency && !this.stopping) {\n const row = await this.fetchOne()\n if (!row) break\n\n const entry = this.handlers.get(row.task)\n if (!entry) {\n await this.db.query(\n `UPDATE pglite_queue_jobs SET status = 'pending', started_at = NULL, attempts = attempts - 1 WHERE id = $1`,\n [row.id]\n )\n continue\n }\n\n this.activeCount++\n const job = rowToJob(row)\n this.emitter.emit('active', job)\n\n void this.processJob(row, entry).finally(() => {\n this.activeCount--\n if (this.activeCount === 0 && this.drainResolve) {\n this.drainResolve()\n this.drainResolve = null\n }\n if (!this.stopping) {\n void this.poll()\n }\n })\n }\n\n if (this.activeCount === 0) {\n this.emitter.emit('drained')\n }\n }\n\n private async fetchOne(): Promise<JobRow | null> {\n const result = await this.db.query<JobRow>(FETCH_SQL)\n return result.rows[0] ?? null\n }\n\n private async processJob(row: JobRow, entry: HandlerEntry): Promise<void> {\n // row.attempts is already incremented by the FETCH SQL\n const context: JobContext = {\n id: row.id,\n task: row.task,\n data: row.data,\n attempts: row.attempts,\n maxAttempts: row.max_attempts,\n progress: async (pct: number) => {\n if (this.stopping) return\n const clamped = Math.max(0, Math.min(100, Math.round(pct)))\n try {\n await this.db.query(PROGRESS_SQL, [row.id, clamped])\n this.emitter.emit('progress', rowToJob({ ...row, progress: clamped }), clamped)\n } catch {\n // DB closing — ignore\n }\n },\n }\n\n try {\n const result = await entry.handler(context)\n\n if (this.stopping) return\n\n try {\n const completedResult = await this.db.query<JobRow>(COMPLETE_SQL, [\n row.id,\n result !== undefined ? JSON.stringify(result) : null,\n ])\n const completedJob = rowToJob(completedResult.rows[0])\n this.emitter.emit('completed', completedJob)\n\n if (row.cron_expr) {\n await this.scheduleNextCron(row)\n }\n } catch {\n // DB closing — ignore\n }\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n\n if (this.stopping) return\n\n try {\n // row.attempts is already the current attempt count (incremented by FETCH SQL)\n if (row.attempts < row.max_attempts) {\n const backoffMs = calculateBackoff(row.attempts)\n const backoffSecs = backoffMs / 1000\n\n const retryResult = await this.db.query<JobRow>(RETRY_SQL, [row.id, error.message, backoffSecs])\n const retryJob = rowToJob(retryResult.rows[0])\n this.emitter.emit('retrying', retryJob, row.attempts)\n } else {\n const failResult = await this.db.query<JobRow>(FAIL_SQL, [row.id, error.message])\n const failedJob = rowToJob(failResult.rows[0])\n this.emitter.emit('failed', failedJob, error)\n }\n\n if (row.cron_expr) {\n await this.scheduleNextCron(row)\n }\n } catch {\n // DB closing — ignore\n }\n }\n }\n\n private async scheduleNextCron(row: JobRow): Promise<void> {\n try {\n const cron = parseCron(row.cron_expr!)\n const nextRun = cron.nextRun()\n\n await this.db.query(INSERT_CRON_NEXT_SQL, [\n row.task,\n JSON.stringify(row.data),\n row.priority,\n nextRun.toISOString(),\n row.max_attempts,\n row.cron_expr,\n row.cron_name,\n ])\n } catch (err) {\n const error = err instanceof Error ? err : new Error(String(err))\n this.emitter.emit('error', error)\n }\n }\n\n waitForDrain(): Promise<void> {\n if (this.activeCount === 0) return Promise.resolve()\n return new Promise((resolve) => {\n this.drainResolve = resolve\n })\n }\n\n stop(): void {\n this.stopping = true\n }\n\n get active(): number {\n return this.activeCount\n }\n\n get isStopping(): boolean {\n return this.stopping\n }\n}\n","import type { PGlite } from '@electric-sql/pglite'\n\nconst MIGRATION_SQL = `\nCREATE TABLE IF NOT EXISTS pglite_queue_jobs (\n id BIGSERIAL PRIMARY KEY,\n task TEXT NOT NULL,\n data JSONB NOT NULL DEFAULT '{}'::jsonb,\n status TEXT NOT NULL DEFAULT 'pending',\n priority INTEGER NOT NULL DEFAULT 0,\n run_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n started_at TIMESTAMPTZ,\n completed_at TIMESTAMPTZ,\n failed_at TIMESTAMPTZ,\n attempts INTEGER NOT NULL DEFAULT 0,\n max_attempts INTEGER NOT NULL DEFAULT 1,\n last_error TEXT,\n progress INTEGER NOT NULL DEFAULT 0 CHECK (progress >= 0 AND progress <= 100),\n result JSONB,\n cron_expr TEXT,\n cron_name TEXT,\n created_at TIMESTAMPTZ NOT NULL DEFAULT NOW(),\n updated_at TIMESTAMPTZ NOT NULL DEFAULT NOW()\n);\n\nCREATE INDEX IF NOT EXISTS idx_pglite_queue_pickup\n ON pglite_queue_jobs (priority ASC, run_at ASC, id ASC)\n WHERE status IN ('pending', 'delayed');\n\nCREATE INDEX IF NOT EXISTS idx_pglite_queue_task\n ON pglite_queue_jobs (task);\n\nCREATE INDEX IF NOT EXISTS idx_pglite_queue_status\n ON pglite_queue_jobs (status);\n\nDO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_indexes\n WHERE indexname = 'idx_pglite_queue_cron_name'\n ) THEN\n CREATE UNIQUE INDEX idx_pglite_queue_cron_name\n ON pglite_queue_jobs (cron_name)\n WHERE cron_name IS NOT NULL AND status NOT IN ('completed', 'failed');\n END IF;\nEND $$;\n\nCREATE OR REPLACE FUNCTION pglite_queue_update_timestamp()\nRETURNS TRIGGER AS $$\nBEGIN\n NEW.updated_at = NOW();\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nDO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_pglite_queue_updated_at'\n ) THEN\n CREATE TRIGGER trg_pglite_queue_updated_at\n BEFORE UPDATE ON pglite_queue_jobs\n FOR EACH ROW\n EXECUTE FUNCTION pglite_queue_update_timestamp();\n END IF;\nEND $$;\n\nCREATE OR REPLACE FUNCTION pglite_queue_notify_new_job()\nRETURNS TRIGGER AS $$\nBEGIN\n PERFORM pg_notify('pglite_queue_new_job', NEW.id::text);\n RETURN NEW;\nEND;\n$$ LANGUAGE plpgsql;\n\nDO $$ BEGIN\n IF NOT EXISTS (\n SELECT 1 FROM pg_trigger WHERE tgname = 'trg_pglite_queue_notify'\n ) THEN\n CREATE TRIGGER trg_pglite_queue_notify\n AFTER INSERT ON pglite_queue_jobs\n FOR EACH ROW\n EXECUTE FUNCTION pglite_queue_notify_new_job();\n END IF;\nEND $$;\n`\n\nconst RECOVERY_SQL = `\nUPDATE pglite_queue_jobs\nSET\n status = CASE\n WHEN attempts >= max_attempts THEN 'failed'\n ELSE 'pending'\n END,\n started_at = NULL,\n last_error = COALESCE(last_error, 'Process crashed during execution')\nWHERE status = 'active';\n`\n\nexport async function runMigrations(db: PGlite): Promise<void> {\n await db.exec(MIGRATION_SQL)\n}\n\nexport async function recoverStalledJobs(db: PGlite): Promise<number> {\n const result = await db.query(RECOVERY_SQL)\n return result.affectedRows ?? 0\n}\n","const UNITS: Record<string, number> = {\n s: 1_000,\n m: 60_000,\n h: 3_600_000,\n d: 86_400_000,\n}\n\n/**\n * Parse a delay value into milliseconds.\n * Accepts a number (ms) or a string like '5s', '10m', '2h', '1d'.\n */\nexport function parseDelay(input: string | number): number {\n if (typeof input === 'number') return input\n\n const match = input.trim().match(/^(\\d+(?:\\.\\d+)?)\\s*(s|m|h|d)$/i)\n if (!match) {\n throw new Error(`Invalid delay format: \"${input}\". Use a number (ms) or a string like \"5s\", \"10m\", \"2h\", \"1d\".`)\n }\n\n const value = parseFloat(match[1])\n const unit = match[2].toLowerCase()\n return Math.round(value * UNITS[unit])\n}\n"],"mappings":"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","names":[]}
@@ -0,0 +1,157 @@
1
+ import { PGlite } from '@electric-sql/pglite';
2
+
3
+ type JobStatus = 'pending' | 'active' | 'completed' | 'failed' | 'delayed';
4
+ interface Job<T = unknown> {
5
+ id: number;
6
+ task: string;
7
+ data: T;
8
+ status: JobStatus;
9
+ priority: number;
10
+ runAt: Date;
11
+ startedAt: Date | null;
12
+ completedAt: Date | null;
13
+ failedAt: Date | null;
14
+ attempts: number;
15
+ maxAttempts: number;
16
+ lastError: string | null;
17
+ progress: number;
18
+ result: unknown;
19
+ cronExpr: string | null;
20
+ createdAt: Date;
21
+ updatedAt: Date;
22
+ }
23
+ interface JobContext<T = unknown> {
24
+ id: number;
25
+ task: string;
26
+ data: T;
27
+ attempts: number;
28
+ maxAttempts: number;
29
+ progress: (pct: number) => Promise<void>;
30
+ }
31
+ type JobHandler<T = unknown> = (job: JobContext<T>) => Promise<unknown>;
32
+ interface BackoffOptions {
33
+ type?: 'exponential' | 'fixed';
34
+ baseDelay?: number;
35
+ maxDelay?: number;
36
+ factor?: number;
37
+ }
38
+ interface JobOptions {
39
+ retry?: number;
40
+ delay?: number | string;
41
+ priority?: number;
42
+ backoff?: BackoffOptions;
43
+ }
44
+ interface QueueOptions {
45
+ dataDir?: string;
46
+ db?: PGlite;
47
+ concurrency?: number;
48
+ pollInterval?: number;
49
+ shutdownTimeout?: number;
50
+ handleSignals?: boolean;
51
+ }
52
+ interface QueueEventMap {
53
+ active: [job: Job];
54
+ completed: [job: Job];
55
+ failed: [job: Job, error: Error];
56
+ retrying: [job: Job, attempt: number];
57
+ progress: [job: Job, progress: number];
58
+ drained: [];
59
+ error: [error: Error];
60
+ }
61
+ type QueueEventName = keyof QueueEventMap;
62
+ interface JobFilter {
63
+ status?: JobStatus | JobStatus[];
64
+ task?: string;
65
+ limit?: number;
66
+ offset?: number;
67
+ }
68
+
69
+ declare class Queue {
70
+ private db;
71
+ private ownsDb;
72
+ private dataDir;
73
+ private concurrency;
74
+ private pollInterval;
75
+ private shutdownTimeout;
76
+ private handleSignals;
77
+ private handlers;
78
+ private emitter;
79
+ private worker;
80
+ private pollTimer;
81
+ private unsubscribeNotify;
82
+ private started;
83
+ private signalHandlers;
84
+ private migrated;
85
+ constructor(options?: QueueOptions);
86
+ /**
87
+ * Register a job handler for a task name.
88
+ */
89
+ define<T = unknown>(task: string, handler: JobHandler<T>, options?: {
90
+ concurrency?: number;
91
+ }): void;
92
+ /**
93
+ * Add a job to the queue.
94
+ */
95
+ add<T = unknown>(task: string, data: T, options?: JobOptions): Promise<Job<T>>;
96
+ /**
97
+ * Register a recurring cron job.
98
+ */
99
+ every<T = unknown>(cronExpr: string, task: string, data?: T, options?: JobOptions): Promise<Job<T>>;
100
+ /**
101
+ * Start processing jobs.
102
+ */
103
+ start(): Promise<void>;
104
+ /**
105
+ * Gracefully stop processing. Waits for active jobs to finish.
106
+ */
107
+ stop(): Promise<void>;
108
+ /**
109
+ * Get a job by ID.
110
+ */
111
+ getJob<T = unknown>(id: number): Promise<Job<T> | null>;
112
+ /**
113
+ * Get jobs matching a filter.
114
+ */
115
+ getJobs<T = unknown>(filter?: JobFilter): Promise<Job<T>[]>;
116
+ /**
117
+ * Remove a job by ID.
118
+ */
119
+ removeJob(id: number): Promise<boolean>;
120
+ /**
121
+ * Remove all completed/failed jobs.
122
+ */
123
+ clean(status?: 'completed' | 'failed'): Promise<number>;
124
+ /**
125
+ * Delete all jobs (for testing).
126
+ */
127
+ obliterate(): Promise<void>;
128
+ /**
129
+ * Get job counts by status.
130
+ */
131
+ counts(): Promise<Record<string, number>>;
132
+ on<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
133
+ off<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
134
+ once<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
135
+ private ensureDb;
136
+ private ensureReady;
137
+ }
138
+
139
+ interface CronExpression {
140
+ nextRun(from?: Date): Date;
141
+ matches(date: Date): boolean;
142
+ }
143
+ declare function parseCron(expr: string): CronExpression;
144
+
145
+ /**
146
+ * Parse a delay value into milliseconds.
147
+ * Accepts a number (ms) or a string like '5s', '10m', '2h', '1d'.
148
+ */
149
+ declare function parseDelay(input: string | number): number;
150
+
151
+ /**
152
+ * Calculate backoff delay in milliseconds for a given attempt number.
153
+ * Attempt numbering starts at 1.
154
+ */
155
+ declare function calculateBackoff(attempt: number, options?: BackoffOptions): number;
156
+
157
+ export { type BackoffOptions, type Job, type JobContext, type JobFilter, type JobHandler, type JobOptions, type JobStatus, Queue, type QueueEventMap, type QueueEventName, type QueueOptions, calculateBackoff, parseCron, parseDelay };
@@ -0,0 +1,157 @@
1
+ import { PGlite } from '@electric-sql/pglite';
2
+
3
+ type JobStatus = 'pending' | 'active' | 'completed' | 'failed' | 'delayed';
4
+ interface Job<T = unknown> {
5
+ id: number;
6
+ task: string;
7
+ data: T;
8
+ status: JobStatus;
9
+ priority: number;
10
+ runAt: Date;
11
+ startedAt: Date | null;
12
+ completedAt: Date | null;
13
+ failedAt: Date | null;
14
+ attempts: number;
15
+ maxAttempts: number;
16
+ lastError: string | null;
17
+ progress: number;
18
+ result: unknown;
19
+ cronExpr: string | null;
20
+ createdAt: Date;
21
+ updatedAt: Date;
22
+ }
23
+ interface JobContext<T = unknown> {
24
+ id: number;
25
+ task: string;
26
+ data: T;
27
+ attempts: number;
28
+ maxAttempts: number;
29
+ progress: (pct: number) => Promise<void>;
30
+ }
31
+ type JobHandler<T = unknown> = (job: JobContext<T>) => Promise<unknown>;
32
+ interface BackoffOptions {
33
+ type?: 'exponential' | 'fixed';
34
+ baseDelay?: number;
35
+ maxDelay?: number;
36
+ factor?: number;
37
+ }
38
+ interface JobOptions {
39
+ retry?: number;
40
+ delay?: number | string;
41
+ priority?: number;
42
+ backoff?: BackoffOptions;
43
+ }
44
+ interface QueueOptions {
45
+ dataDir?: string;
46
+ db?: PGlite;
47
+ concurrency?: number;
48
+ pollInterval?: number;
49
+ shutdownTimeout?: number;
50
+ handleSignals?: boolean;
51
+ }
52
+ interface QueueEventMap {
53
+ active: [job: Job];
54
+ completed: [job: Job];
55
+ failed: [job: Job, error: Error];
56
+ retrying: [job: Job, attempt: number];
57
+ progress: [job: Job, progress: number];
58
+ drained: [];
59
+ error: [error: Error];
60
+ }
61
+ type QueueEventName = keyof QueueEventMap;
62
+ interface JobFilter {
63
+ status?: JobStatus | JobStatus[];
64
+ task?: string;
65
+ limit?: number;
66
+ offset?: number;
67
+ }
68
+
69
+ declare class Queue {
70
+ private db;
71
+ private ownsDb;
72
+ private dataDir;
73
+ private concurrency;
74
+ private pollInterval;
75
+ private shutdownTimeout;
76
+ private handleSignals;
77
+ private handlers;
78
+ private emitter;
79
+ private worker;
80
+ private pollTimer;
81
+ private unsubscribeNotify;
82
+ private started;
83
+ private signalHandlers;
84
+ private migrated;
85
+ constructor(options?: QueueOptions);
86
+ /**
87
+ * Register a job handler for a task name.
88
+ */
89
+ define<T = unknown>(task: string, handler: JobHandler<T>, options?: {
90
+ concurrency?: number;
91
+ }): void;
92
+ /**
93
+ * Add a job to the queue.
94
+ */
95
+ add<T = unknown>(task: string, data: T, options?: JobOptions): Promise<Job<T>>;
96
+ /**
97
+ * Register a recurring cron job.
98
+ */
99
+ every<T = unknown>(cronExpr: string, task: string, data?: T, options?: JobOptions): Promise<Job<T>>;
100
+ /**
101
+ * Start processing jobs.
102
+ */
103
+ start(): Promise<void>;
104
+ /**
105
+ * Gracefully stop processing. Waits for active jobs to finish.
106
+ */
107
+ stop(): Promise<void>;
108
+ /**
109
+ * Get a job by ID.
110
+ */
111
+ getJob<T = unknown>(id: number): Promise<Job<T> | null>;
112
+ /**
113
+ * Get jobs matching a filter.
114
+ */
115
+ getJobs<T = unknown>(filter?: JobFilter): Promise<Job<T>[]>;
116
+ /**
117
+ * Remove a job by ID.
118
+ */
119
+ removeJob(id: number): Promise<boolean>;
120
+ /**
121
+ * Remove all completed/failed jobs.
122
+ */
123
+ clean(status?: 'completed' | 'failed'): Promise<number>;
124
+ /**
125
+ * Delete all jobs (for testing).
126
+ */
127
+ obliterate(): Promise<void>;
128
+ /**
129
+ * Get job counts by status.
130
+ */
131
+ counts(): Promise<Record<string, number>>;
132
+ on<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
133
+ off<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
134
+ once<K extends QueueEventName>(event: K, listener: (...args: QueueEventMap[K]) => void): this;
135
+ private ensureDb;
136
+ private ensureReady;
137
+ }
138
+
139
+ interface CronExpression {
140
+ nextRun(from?: Date): Date;
141
+ matches(date: Date): boolean;
142
+ }
143
+ declare function parseCron(expr: string): CronExpression;
144
+
145
+ /**
146
+ * Parse a delay value into milliseconds.
147
+ * Accepts a number (ms) or a string like '5s', '10m', '2h', '1d'.
148
+ */
149
+ declare function parseDelay(input: string | number): number;
150
+
151
+ /**
152
+ * Calculate backoff delay in milliseconds for a given attempt number.
153
+ * Attempt numbering starts at 1.
154
+ */
155
+ declare function calculateBackoff(attempt: number, options?: BackoffOptions): number;
156
+
157
+ export { type BackoffOptions, type Job, type JobContext, type JobFilter, type JobHandler, type JobOptions, type JobStatus, Queue, type QueueEventMap, type QueueEventName, type QueueOptions, calculateBackoff, parseCron, parseDelay };