release: WRNexusJS 0.8.0
This commit is contained in:
@@ -0,0 +1,102 @@
|
||||
import { describe, expect, test } from "bun:test";
|
||||
import {
|
||||
Bulkhead,
|
||||
CircuitBreaker,
|
||||
ResilienceError,
|
||||
durationMs,
|
||||
resilientCall,
|
||||
} from "../src/index.ts";
|
||||
|
||||
describe("resilience primitives", () => {
|
||||
test("parses durations and validates bad configuration", () => {
|
||||
expect(durationMs("1.5s")).toBe(1_500);
|
||||
expect(durationMs("2m")).toBe(120_000);
|
||||
expect(() => durationMs("soon" as never)).toThrow("Invalid duration");
|
||||
});
|
||||
|
||||
test("retries with exponential backoff and reports attempts", async () => {
|
||||
const waits: number[] = [];
|
||||
let calls = 0;
|
||||
const value = await resilientCall({
|
||||
retries: 2,
|
||||
retryDelay: 1,
|
||||
backoff: "exponential",
|
||||
onRetry: (_error, _attempt, wait) => waits.push(wait),
|
||||
run: async (_signal, attempt) => {
|
||||
calls += 1;
|
||||
if (attempt < 3) throw new Error("temporary");
|
||||
return "ready";
|
||||
},
|
||||
});
|
||||
expect(value).toBe("ready");
|
||||
expect(calls).toBe(3);
|
||||
expect(waits).toEqual([1, 2]);
|
||||
});
|
||||
|
||||
test("times out cooperative operations and supports fallback", async () => {
|
||||
const value = await resilientCall({
|
||||
timeout: "5ms",
|
||||
fallback: (error) => (error as ResilienceError).code,
|
||||
run: (signal) =>
|
||||
new Promise((_resolve, reject) =>
|
||||
signal.addEventListener("abort", () => reject(signal.reason)),
|
||||
),
|
||||
});
|
||||
expect(value).toBe("WRN-RESILIENCE-TIMEOUT");
|
||||
});
|
||||
|
||||
test("times out integrations that ignore cancellation", async () => {
|
||||
await expect(
|
||||
resilientCall({ timeout: "2ms", run: () => new Promise(() => {}) }),
|
||||
).rejects.toMatchObject({ code: "WRN-RESILIENCE-TIMEOUT" });
|
||||
});
|
||||
|
||||
test("opens a circuit and exposes health", async () => {
|
||||
const breaker = new CircuitBreaker({ failures: 2, resetAfter: "1h" });
|
||||
for (let index = 0; index < 2; index += 1) {
|
||||
await expect(
|
||||
breaker.execute(async () => {
|
||||
throw new Error("down");
|
||||
}),
|
||||
).rejects.toThrow("down");
|
||||
}
|
||||
expect(breaker.snapshot().state).toBe("open");
|
||||
await expect(breaker.execute(async () => "nope")).rejects.toMatchObject({
|
||||
code: "WRN-RESILIENCE-CIRCUIT-OPEN",
|
||||
});
|
||||
});
|
||||
|
||||
test("retains circuit state for a reused declarative configuration", async () => {
|
||||
const circuitBreaker = { failures: 1, resetAfter: "1h" } as const;
|
||||
await expect(
|
||||
resilientCall({
|
||||
circuitBreaker,
|
||||
run: async () => {
|
||||
throw new Error("down");
|
||||
},
|
||||
}),
|
||||
).rejects.toThrow("down");
|
||||
await expect(
|
||||
resilientCall({ circuitBreaker, run: async () => "unreachable" }),
|
||||
).rejects.toMatchObject({ code: "WRN-RESILIENCE-CIRCUIT-OPEN" });
|
||||
});
|
||||
|
||||
test("bulkhead bounds concurrency and queue depth", async () => {
|
||||
const bulkhead = new Bulkhead({ concurrency: 1, queue: 1 });
|
||||
let release!: () => void;
|
||||
const first = bulkhead.execute(
|
||||
() =>
|
||||
new Promise<void>((resolve) => {
|
||||
release = resolve;
|
||||
}),
|
||||
);
|
||||
const second = bulkhead.execute(async () => "second");
|
||||
await expect(bulkhead.execute(async () => "third")).rejects.toMatchObject({
|
||||
code: "WRN-RESILIENCE-BULKHEAD-FULL",
|
||||
});
|
||||
expect(bulkhead.snapshot).toEqual({ active: 1, queued: 1, capacity: 1 });
|
||||
release();
|
||||
await first;
|
||||
expect(await second).toBe("second");
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user