import type { Job } from "./index.ts"; import type { QueueStore } from "./durable.ts"; export interface RedisQueueClient { get(key: string): Promise; set(key: string, value: string, options?: { NX?: boolean; PX?: number }): Promise; del(...keys: string[]): Promise; zadd(key: string, score: number, member: string): Promise; zrem(key: string, member: string): Promise; zrangebyscore( key: string, min: number, max: number, options?: { limit: [number, number] }, ): Promise; smembers(key: string): Promise; sadd(key: string, member: string): Promise; srem(key: string, member: string): Promise; } /** Redis-backed queue store using only the common client command surface. */ export function redisQueueStore(client: RedisQueueClient, prefix = "wrnexus:queue"): QueueStore { const jobs = `${prefix}:jobs`; const due = `${prefix}:due`; const key = (id: string) => `${prefix}:job:${id}`; return { async put(job) { await client.set(key(job.id), JSON.stringify(job)); await client.sadd(jobs, job.id); await client.zadd(due, job.runAt, job.id); await client.del(`${prefix}:lease:${job.id}`); }, async get(id) { const value = await client.get(key(id)); return value ? (JSON.parse(value) as Job) : null; }, async remove(id) { await client.del(key(id), `${prefix}:lease:${id}`); await client.srem(jobs, id); await client.zrem(due, id); }, async due(now, limit) { const ids = await client.zrangebyscore(due, 0, now, { limit: [0, limit] }); const values = await Promise.all(ids.map((id) => client.get(key(id)))); return values .filter((value): value is string => value !== null) .map((value) => JSON.parse(value)); }, async list(name) { const ids = await client.smembers(jobs); const values = await Promise.all(ids.map((id) => client.get(key(id)))); return values .filter((value): value is string => value !== null) .map((value) => JSON.parse(value) as Job) .filter((job) => !name || job.name === name); }, async claim(id, worker, leaseUntil) { const ttl = Math.max(1, leaseUntil - Date.now()); return Boolean(await client.set(`${prefix}:lease:${id}`, worker, { NX: true, PX: ttl })); }, }; } export interface SqlQueueClient { query>(sql: string, parameters?: unknown[]): Promise<{ rows: T[] }>; } /** PostgreSQL store with atomic SKIP LOCKED leasing and JSON payloads. */ export function postgresQueueStore(db: SqlQueueClient, table = "wrnexus_jobs"): QueueStore { if (!/^[a-z_][a-z0-9_]*$/i.test(table)) throw new Error("Invalid queue table name"); return { async put(job) { await db.query( `INSERT INTO ${table} (id,name,payload,run_at,priority,lease_owner,lease_until) VALUES ($1,$2,$3,$4,$5,NULL,NULL) ON CONFLICT (id) DO UPDATE SET name=$2,payload=$3,run_at=$4,priority=$5,lease_owner=NULL,lease_until=NULL`, [job.id, job.name, JSON.stringify(job), job.runAt, job.priority], ); }, async get(id) { const result = await db.query<{ payload: Job }>(`SELECT payload FROM ${table} WHERE id=$1`, [ id, ]); return result.rows[0]?.payload ?? null; }, async remove(id) { await db.query(`DELETE FROM ${table} WHERE id=$1`, [id]); }, async due(now, limit) { const result = await db.query<{ payload: Job }>( `SELECT payload FROM ${table} WHERE run_at <= $1 AND (lease_until IS NULL OR lease_until < $1) ORDER BY priority DESC, run_at ASC LIMIT $2`, [now, limit], ); return result.rows.map((row) => row.payload); }, async list(name) { const result = await db.query<{ payload: Job }>( `SELECT payload FROM ${table}${name ? " WHERE name=$1" : ""} ORDER BY run_at ASC`, name ? [name] : [], ); return result.rows.map((row) => row.payload); }, async claim(id, worker, leaseUntil) { const result = await db.query( `UPDATE ${table} SET lease_owner=$2,lease_until=$3 WHERE id=$1 AND (lease_until IS NULL OR lease_until < $4) RETURNING id`, [id, worker, leaseUntil, Date.now()], ); return result.rows.length === 1; }, }; } export const POSTGRES_QUEUE_SCHEMA = `CREATE TABLE IF NOT EXISTS wrnexus_jobs ( id text PRIMARY KEY, name text NOT NULL, payload jsonb NOT NULL, run_at bigint NOT NULL, priority integer NOT NULL DEFAULT 0, lease_owner text, lease_until bigint ); CREATE INDEX IF NOT EXISTS wrnexus_jobs_due ON wrnexus_jobs (run_at, priority DESC);`;