diff --git a/fuzz/deep_fuzz.ts b/fuzz/deep_fuzz.ts index b22a611..66a1f6b 100644 --- a/fuzz/deep_fuzz.ts +++ b/fuzz/deep_fuzz.ts @@ -3,6 +3,7 @@ import * as Persistency from "../src/persist.ts"; import { createHandler } from "../src/handler.ts"; import { RateLimiter } from "../src/rate_limiter.ts"; import { parseConfig, ConfigError } from "../src/config.ts"; +import type { JsonValue } from "../src/payload.ts"; console.log("=== STARTING DEEP PROPERTY & INVARIANT FUZZING ==="); @@ -183,7 +184,7 @@ console.log("\n--- Testing Persistence Invariants ---"); { const tempDir = await Deno.makeTempDir(); try { - const store = new Persistency.FileStore(); + const store = new Persistency.FileStore(); store.dir(tempDir); const mgr = new QueueManager(store, 100, 50, true); @@ -191,14 +192,14 @@ console.log("\n--- Testing Persistence Invariants ---"); mgr.enqueue("q1", { a: [1, 2, { b: "hello" }], c: true }); mgr.enqueue("q1", "second"); mgr.enqueue("q2", 42); - mgr.enqueue("q2", null as any); // if enqueued directly in manager + mgr.enqueue("q2", null); // if enqueued directly in manager mgr.enqueue("q3", ""); mgr.dequeue("q1"); // dequeue "first" mgr.save(); // Create fresh manager from same store - const store2 = new Persistency.FileStore(); + const store2 = new Persistency.FileStore(); store2.dir(tempDir); const mgr2 = new QueueManager(store2, 100, 50, true); mgr2.load(); diff --git a/src/persist.ts b/src/persist.ts index b91f65a..05d425f 100644 --- a/src/persist.ts +++ b/src/persist.ts @@ -1,3 +1,5 @@ +import type { JsonValue } from "./payload.ts"; + export interface QueueEvent { queue: string; payload: T; @@ -39,7 +41,8 @@ export interface QueueStore { close(): void; } -export class FileStore implements QueueStore { +/** Persists payloads as JSON; T must be JSON-compatible. */ +export class FileStore implements QueueStore { private directory: string = ''; private writeHandle: Deno.FsFile | null = null; private encoder = new TextEncoder(); diff --git a/tests/manager_test.ts b/tests/manager_test.ts index 979c391..150be25 100644 --- a/tests/manager_test.ts +++ b/tests/manager_test.ts @@ -676,6 +676,35 @@ Deno.test("load snapshots remaining items so a second load restores them", () => assertEquals(loaded2.dequeue("jobs"), "keep-me"); }); +Deno.test("FileStore reloads nested JSON payloads through QueueManager", () => { + const tmpDir = Deno.makeTempDirSync(); + try { + const expected: Payload = { + name: "job", + metadata: { + retries: 3, + active: true, + tags: ["urgent", "batch"], + lastError: null, + }, + }; + const firstStore = new Persistency.FileStore(); + firstStore.dir(tmpDir); + const firstManager = new QueueManager(firstStore); + firstManager.enqueue("jobs", expected); + firstStore.close(); + + const reopenedStore = new Persistency.FileStore(); + reopenedStore.dir(tmpDir); + const reopenedManager = new QueueManager(reopenedStore); + reopenedManager.load(); + assertEquals(reopenedManager.peek("jobs"), expected); + reopenedStore.close(); + } finally { + Deno.removeSync(tmpDir, { recursive: true }); + } +}); + Deno.test("FileStore load snapshots remaining items onto disk", () => { const tmp = Deno.makeTempDirSync(); try { diff --git a/tests/persist_test.ts b/tests/persist_test.ts index 4eadec4..1d7c43c 100644 --- a/tests/persist_test.ts +++ b/tests/persist_test.ts @@ -6,6 +6,9 @@ import { RateLimiter } from "../src/rate_limiter.ts"; import { parseConfig, ConfigError } from "../src/config.ts"; import type { Payload } from "../src/payload.ts"; +// @ts-expect-error FileStore can only persist JSON-compatible payloads. +new Persistency.FileStore(); + // Shared helpers const API_TOKEN = "test-token"; const authHeaders = { "Authorization": `Bearer ${API_TOKEN}` }; @@ -180,6 +183,26 @@ Deno.test("persist FileStore.saveEvent() waits for an existing file lock", async Deno.removeSync(tmpDir, { recursive: true }); }); +Deno.test("FileStore preserves the existing JSON-lines format", () => { + const tmpDir = Deno.makeTempDirSync(); + const store = new Persistency.FileStore(); + store.dir(tmpDir); + try { + store.saveEvent("q", "hello", true); + store.saveEvent("q", { nested: [1, true, "x"] }, true); + store.close(); + + assertEquals( + Deno.readTextFileSync(tmpDir + "/persist.dat"), + '{"queue":"q","payload":"hello","enqueue":true,"dequeue":false}\n' + + '{"queue":"q","payload":{"nested":[1,true,"x"]},"enqueue":true,"dequeue":false}\n', + ); + } finally { + store.close(); + Deno.removeSync(tmpDir, { recursive: true }); + } +}); + // ── FileStore.clear() creates file if absent ───────────────────────────────── Deno.test("persist FileStore.clear() creates file when it does not exist", () => { @@ -208,7 +231,7 @@ Deno.test("persist FileStore.clear() truncates existing content", () => { Deno.test("persist FileStore.loadState() reads large file correctly", () => { const tmpDir = Deno.makeTempDirSync(); - const p = new Persistency.FileStore(); + const p = new Persistency.FileStore<{ payload: string; i: number }>(); p.dir(tmpDir + "/"); p.clear(); for (let i = 0; i < 100; i++) {