release: WRNexusJS 0.8.0
Quality / quality (ubuntu-latest) (push) Failing after 21s
Quality / quality (windows-latest) (push) Canceled after 0s

This commit is contained in:
2026-08-02 23:18:51 +05:30
parent 87507edf59
commit 586a6db8ff
625 changed files with 243608 additions and 11210 deletions
+73
View File
@@ -135,6 +135,44 @@ test("deduplicates, lists and cancels queued work", async () => {
expect(queue.cancel(first.id)).toBe(false);
});
test("enforces capacity and can retry dead-lettered work", async () => {
const queue = createQueue({ capacity: 1, maxAttempts: 1 });
const job = await queue.add("work", {});
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CAPACITY");
queue.process("work", () => {
throw new Error("nope");
});
await queue.drain();
expect(queue.failed().map(({ id }) => id)).toEqual([job.id]);
expect(await queue.retry(job.id)).toBe(true);
expect(queue.failed()).toHaveLength(0);
expect(queue.size()).toBe(1);
});
test("cancels active work with an AbortSignal and force-shuts down", async () => {
const queue = createQueue();
let aborted = false;
queue.process(
"work",
(_job, { signal }) =>
new Promise<void>((resolve) => {
signal.addEventListener("abort", () => {
aborted = true;
resolve();
});
}),
);
const job = await queue.add("work", {});
const draining = queue.drain();
await Promise.resolve();
expect(queue.cancel(job.id)).toBe(true);
await draining;
expect(aborted).toBe(true);
expect(queue.size()).toBe(0);
await queue.shutdown({ force: true });
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CLOSED");
});
test("runs typed workflows and parses supported cron expressions", async () => {
const workflow = defineWorkflow<number>("double-and-label", [
{ name: "double", run: (value: number) => value * 2 },
@@ -190,3 +228,38 @@ test("durable queue does not remove a job before its handler succeeds", async ()
expect(visibleDuringHandler).toBe(true);
expect(await store.get(job.id)).toBeNull();
});
test("durable queue supports capacity, cancellation, and shutdown", async () => {
const store = memoryQueueStore();
const queue = createDurableQueue({ store, capacity: 1 });
const job = await queue.add("work", {});
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CAPACITY");
expect((await queue.list()).map(({ id }) => id)).toEqual([job.id]);
expect(await queue.cancel(job.id)).toBe(true);
expect(await queue.cancel(job.id)).toBe(false);
await queue.shutdown();
await expect(queue.add("work", {})).rejects.toThrow("WRN-QUEUE-CLOSED");
});
test("force shutdown aborts an active durable job", async () => {
const store = memoryQueueStore();
const queue = createDurableQueue({ store });
let aborted = false;
queue.process(
"work",
(_job, { signal }) =>
new Promise<void>((resolve) => {
signal.addEventListener("abort", () => {
aborted = true;
resolve();
});
}),
);
const job = await queue.add("work", {});
const draining = queue.drain();
await Promise.resolve();
await queue.shutdown({ force: true });
await draining;
expect(aborted).toBe(true);
expect(await store.get(job.id)).toBeNull();
});
+132
View File
@@ -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);
});
+53
View File
@@ -0,0 +1,53 @@
import { describe, expect, test } from "bun:test";
import { createWorkflowEngine, defineDurableWorkflow, memoryWorkflowStore } from "../src/index.ts";
describe("durable workflows", () => {
test("runs dependency steps and persists results", async () => {
const store = memoryWorkflowStore();
const engine = createWorkflowEngine(store);
const workflow = defineDurableWorkflow({
name: "report",
steps: [
{ name: "load", run: (input: number) => input * 2 },
{ name: "render", dependsOn: ["load"], run: (input: number) => `report:${input}` },
],
});
const result = await engine.start(workflow, 4, "report-1");
expect(result.status).toBe("completed");
expect(result.results.render).toBe("report:8");
expect((await store.get("report-1"))?.progress).toBe(100);
});
test("pauses for a durable human approval and resumes", async () => {
const engine = createWorkflowEngine();
const workflow = defineDurableWorkflow({
name: "publish",
steps: [
{ name: "draft", run: () => "ready" },
{ name: "approve", dependsOn: ["draft"], approval: true, run: (input: string) => input },
{ name: "publish", dependsOn: ["approve"], run: () => "published" },
],
});
const waiting = await engine.start(workflow, null, "publish-1");
expect(waiting).toMatchObject({ status: "waiting-approval", waitingFor: "approve" });
const completed = await engine.approve(workflow, "publish-1", "approve", "user-42");
expect(completed.status).toBe("completed");
expect(completed.results["approve:approval"]).toMatchObject({ actor: "user-42" });
});
test("rejects invalid dependency graphs", () => {
expect(() =>
defineDurableWorkflow({
name: "bad",
steps: [{ name: "a", dependsOn: ["missing"], run() {} }],
}),
).toThrow("WRN-WORKFLOW-DEPENDENCY");
expect(() =>
defineDurableWorkflow({
name: "cycle",
steps: [
{ name: "a", dependsOn: ["b"], run() {} },
{ name: "b", dependsOn: ["a"], run() {} },
],
}),
).toThrow("WRN-WORKFLOW-CYCLE");
});
});