release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
import type { Job } from "./index.ts";
|
||||
import type { QueueStore } from "./durable.ts";
|
||||
|
||||
export interface RedisQueueClient {
|
||||
get(key: string): Promise<string | null>;
|
||||
set(key: string, value: string, options?: { NX?: boolean; PX?: number }): Promise<unknown>;
|
||||
del(...keys: string[]): Promise<unknown>;
|
||||
zadd(key: string, score: number, member: string): Promise<unknown>;
|
||||
zrem(key: string, member: string): Promise<unknown>;
|
||||
zrangebyscore(
|
||||
key: string,
|
||||
min: number,
|
||||
max: number,
|
||||
options?: { limit: [number, number] },
|
||||
): Promise<string[]>;
|
||||
smembers(key: string): Promise<string[]>;
|
||||
sadd(key: string, member: string): Promise<unknown>;
|
||||
srem(key: string, member: string): Promise<unknown>;
|
||||
}
|
||||
|
||||
/** 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<T = Record<string, unknown>>(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);`;
|
||||
Reference in New Issue
Block a user