Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 4 additions & 3 deletions fuzz/deep_fuzz.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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 ===");

Expand Down Expand Up @@ -183,22 +184,22 @@ console.log("\n--- Testing Persistence Invariants ---");
{
const tempDir = await Deno.makeTempDir();
try {
const store = new Persistency.FileStore<unknown>();
const store = new Persistency.FileStore<JsonValue>();
store.dir(tempDir);
const mgr = new QueueManager(store, 100, 50, true);

// Enqueue various complex types
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<unknown>();
const store2 = new Persistency.FileStore<JsonValue>();
store2.dir(tempDir);
const mgr2 = new QueueManager(store2, 100, 50, true);
mgr2.load();
Expand Down
5 changes: 4 additions & 1 deletion src/persist.ts
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
import type { JsonValue } from "./payload.ts";

export interface QueueEvent<T> {
queue: string;
payload: T;
Expand Down Expand Up @@ -39,7 +41,8 @@ export interface QueueStore<T = string> {
close(): void;
}

export class FileStore<T = string> implements QueueStore<T> {
/** Persists payloads as JSON; T must be JSON-compatible. */
export class FileStore<T extends JsonValue = string> implements QueueStore<T> {
private directory: string = '';
private writeHandle: Deno.FsFile | null = null;
private encoder = new TextEncoder();
Expand Down
29 changes: 29 additions & 0 deletions tests/manager_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Payload>();
firstStore.dir(tmpDir);
const firstManager = new QueueManager<Payload>(firstStore);
firstManager.enqueue("jobs", expected);
firstStore.close();

const reopenedStore = new Persistency.FileStore<Payload>();
reopenedStore.dir(tmpDir);
const reopenedManager = new QueueManager<Payload>(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 {
Expand Down
25 changes: 24 additions & 1 deletion tests/persist_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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<Date>();

// Shared helpers
const API_TOKEN = "test-token";
const authHeaders = { "Authorization": `Bearer ${API_TOKEN}` };
Expand Down Expand Up @@ -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<Payload>();
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", () => {
Expand Down Expand Up @@ -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<any>();
const p = new Persistency.FileStore<{ payload: string; i: number }>();
p.dir(tmpDir + "/");
p.clear();
for (let i = 0; i < 100; i++) {
Expand Down
Loading