release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,132 @@
|
||||
import { expect, test } from "bun:test";
|
||||
import {
|
||||
addBatch,
|
||||
createDurableQueue,
|
||||
createQueueScheduler,
|
||||
memoryQueueStore,
|
||||
postgresQueueStore,
|
||||
queueDashboardSnapshot,
|
||||
redisQueueStore,
|
||||
runQueueDaemon,
|
||||
type Job,
|
||||
type RedisQueueClient,
|
||||
} from "../src/index.ts";
|
||||
|
||||
const job: Job = {
|
||||
id: "one",
|
||||
name: "email",
|
||||
data: { to: "a@b.test" },
|
||||
attempts: 0,
|
||||
maxAttempts: 3,
|
||||
runAt: 10,
|
||||
priority: 2,
|
||||
createdAt: 1,
|
||||
};
|
||||
|
||||
test("Redis queue store persists, orders and atomically leases jobs", async () => {
|
||||
const strings = new Map<string, string>();
|
||||
const sets = new Map<string, Set<string>>();
|
||||
const scores = new Map<string, Map<string, number>>();
|
||||
const client: RedisQueueClient = {
|
||||
async get(key) {
|
||||
return strings.get(key) ?? null;
|
||||
},
|
||||
async set(key, value, options) {
|
||||
if (options?.NX && strings.has(key)) return null;
|
||||
strings.set(key, value);
|
||||
return "OK";
|
||||
},
|
||||
async del(...keys) {
|
||||
for (const key of keys) strings.delete(key);
|
||||
return keys.length;
|
||||
},
|
||||
async zadd(key, score, member) {
|
||||
(scores.get(key) ?? scores.set(key, new Map()).get(key)!).set(member, score);
|
||||
return 1;
|
||||
},
|
||||
async zrem(key, member) {
|
||||
return scores.get(key)?.delete(member) ? 1 : 0;
|
||||
},
|
||||
async zrangebyscore(key, min, max, options) {
|
||||
return [...(scores.get(key) ?? [])]
|
||||
.filter(([, score]) => score >= min && score <= max)
|
||||
.sort((a, b) => a[1] - b[1])
|
||||
.slice(...options!.limit)
|
||||
.map(([id]) => id);
|
||||
},
|
||||
async smembers(key) {
|
||||
return [...(sets.get(key) ?? [])];
|
||||
},
|
||||
async sadd(key, member) {
|
||||
(sets.get(key) ?? sets.set(key, new Set()).get(key)!).add(member);
|
||||
return 1;
|
||||
},
|
||||
async srem(key, member) {
|
||||
return sets.get(key)?.delete(member) ? 1 : 0;
|
||||
},
|
||||
};
|
||||
const store = redisQueueStore(client);
|
||||
await store.put(job);
|
||||
expect(await store.due(10, 5)).toEqual([job]);
|
||||
expect(await store.claim!(job.id, "a", Date.now() + 1000)).toBeTrue();
|
||||
expect(await store.claim!(job.id, "b", Date.now() + 1000)).toBeFalse();
|
||||
await store.remove(job.id);
|
||||
expect(await store.list()).toEqual([]);
|
||||
});
|
||||
|
||||
test("standalone daemon schedules, drains and shuts down on abort", async () => {
|
||||
const queue = createDurableQueue();
|
||||
let processed = 0;
|
||||
queue.process("pulse", async () => {
|
||||
processed++;
|
||||
});
|
||||
const scheduler = createQueueScheduler(queue, [{ name: "pulse", data: {}, everyMs: 10 }], {
|
||||
pollMs: 10,
|
||||
});
|
||||
const controller = new AbortController();
|
||||
setTimeout(() => controller.abort(), 35);
|
||||
await runQueueDaemon(queue, scheduler, { signal: controller.signal, pollMs: 10 });
|
||||
expect(processed).toBeGreaterThan(0);
|
||||
expect(scheduler.snapshot().running).toBeFalse();
|
||||
});
|
||||
|
||||
test("PostgreSQL queue store parameterizes values and validates identifiers", async () => {
|
||||
const calls: Array<{ sql: string; args?: unknown[] }> = [];
|
||||
const store = postgresQueueStore({
|
||||
async query(sql, args) {
|
||||
calls.push({ sql, args });
|
||||
return { rows: [] };
|
||||
},
|
||||
});
|
||||
await store.put(job);
|
||||
expect(calls[0]!.sql).toContain("ON CONFLICT");
|
||||
expect(calls[0]!.args?.[0]).toBe("one");
|
||||
expect(() =>
|
||||
postgresQueueStore(
|
||||
{
|
||||
async query() {
|
||||
return { rows: [] };
|
||||
},
|
||||
},
|
||||
"jobs; DROP TABLE jobs",
|
||||
),
|
||||
).toThrow("Invalid queue table");
|
||||
});
|
||||
|
||||
test("scheduler, batch and dashboard provide operational queue APIs", async () => {
|
||||
let now = 1_000;
|
||||
const queue = createDurableQueue({ store: memoryQueueStore(), now: () => now });
|
||||
await addBatch(queue, "email", [{ id: 1 }, { id: 2 }], { idempotencyKey: "emails" });
|
||||
const scheduler = createQueueScheduler(queue, [{ name: "cleanup", data: {}, everyMs: 500 }], {
|
||||
now: () => now,
|
||||
});
|
||||
expect(await scheduler.tick()).toBe(1);
|
||||
expect(await scheduler.tick()).toBe(0);
|
||||
now = 1_500;
|
||||
expect(await scheduler.tick()).toBe(1);
|
||||
expect(await queueDashboardSnapshot(queue)).toMatchObject({
|
||||
pending: 4,
|
||||
byName: { email: 2, cleanup: 2 },
|
||||
});
|
||||
expect(scheduler.snapshot().schedules).toBe(1);
|
||||
});
|
||||
Reference in New Issue
Block a user