From aff39b502aed19a08df444ebb42e399aedc5f366 Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 23 Jul 2026 13:04:01 +0100 Subject: [PATCH 1/2] refactor: clear persist quality gate --- src/persist.ts | 15 +++++++++------ 1 file changed, 9 insertions(+), 6 deletions(-) diff --git a/src/persist.ts b/src/persist.ts index a0362e5..5a686e8 100644 --- a/src/persist.ts +++ b/src/persist.ts @@ -55,8 +55,11 @@ export class FileStore implements QueueStore { const chunks: Uint8Array[] = []; const chunk = new Uint8Array(4096); let totalRead = 0; - let read: number | null; - while ((read = file.readSync(chunk)) !== null && read > 0) { + while (true) { + const read = file.readSync(chunk); + if (read === null || read <= 0) { + break; + } chunks.push(chunk.slice(0, read)); totalRead += read; } @@ -74,11 +77,11 @@ export class FileStore implements QueueStore { file.unlockSync(); file.close(); } - } catch (_e) { - if (_e instanceof Deno.errors.NotFound) { + } catch (error) { + if (error instanceof Deno.errors.NotFound) { return []; } - throw _e; + throw error; } } @@ -107,5 +110,5 @@ export class MemoryStore implements QueueStore { return [...this.events]; } - public dir(_dir: string): void {} + public dir(): void {} } From b8e25c90260f8851daecde08afae4e8d3a34d3db Mon Sep 17 00:00:00 2001 From: Jonathan Baldie Date: Thu, 23 Jul 2026 13:12:05 +0100 Subject: [PATCH 2/2] test: prove persist ordering and locking --- tests/persist_test.ts | 75 +++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 75 insertions(+) diff --git a/tests/persist_test.ts b/tests/persist_test.ts index be915f1..572c6fc 100644 --- a/tests/persist_test.ts +++ b/tests/persist_test.ts @@ -25,6 +25,18 @@ Deno.test("persist MemoryStore.saveEvent() appends", () => { assertEquals(p.loadState().length, 1); }); +Deno.test("persist MemoryStore replays events in append order", () => { + const p = new Persistency.MemoryStore(); + p.saveEvent("q", "first", true); + p.saveEvent("q", "second", true); + p.saveEvent("q", "first", false); + + assertEquals( + p.loadState().map((event) => [event.payload, event.enqueue]), + [["first", true], ["second", true], ["first", false]], + ); +}); + Deno.test("persist MemoryStore.clear() clears events", () => { const p = new Persistency.MemoryStore(); p.saveEvent("q", "anything", true); @@ -98,6 +110,69 @@ Deno.test("persist FileStore.saveEvent() does not overwrite existing content", ( Deno.removeSync(tmpDir, { recursive: true }); }); +Deno.test("persist FileStore.saveEvent() waits for an existing file lock", async () => { + const tmpDir = Deno.makeTempDirSync(); + const markerPath = tmpDir + "/ready"; + const persistPath = tmpDir + "/persist.dat"; + const persistModule = new URL("../src/persist.ts", import.meta.url).href; + const lockedFile = Deno.openSync(persistPath, { write: true, create: true }); + lockedFile.lockSync(true); + + const childCode = ` + import { FileStore } from ${JSON.stringify(persistModule)}; + Deno.writeTextFileSync(${JSON.stringify(markerPath)}, "ready"); + const store = new FileStore(); + store.dir(${JSON.stringify(tmpDir)}); + store.saveEvent("q", "from-child", true); + `; + const child = new Deno.Command(Deno.execPath(), { + args: ["eval", "--no-config", childCode], + stdout: "null", + stderr: "piped", + }).spawn(); + const statusPromise = child.status; + const stderrPromise = new Response(child.stderr).text(); + + try { + let markerReady = false; + for (let attempt = 0; attempt < 500 && !markerReady; attempt++) { + try { + markerReady = Deno.statSync(markerPath).isFile; + } catch (error) { + if (!(error instanceof Deno.errors.NotFound)) { + throw error; + } + } + await new Promise((resolve) => setTimeout(resolve, 10)); + } + if (!markerReady) { + const status = await statusPromise; + throw new Error( + `lock contender exited ${status.code}: ${await stderrPromise}`, + ); + } + const completedWhileLocked = await Promise.race([ + statusPromise.then(() => true), + new Promise((resolve) => setTimeout(() => resolve(false), 100)), + ]); + assertEquals(completedWhileLocked, false); + } finally { + lockedFile.unlockSync(); + lockedFile.close(); + } + + const status = await statusPromise; + assertEquals( + status.success, + true, + await stderrPromise, + ); + const store = new Persistency.FileStore(); + store.dir(tmpDir); + assertEquals(store.loadState()[0].payload, "from-child"); + Deno.removeSync(tmpDir, { recursive: true }); +}); + // ── FileStore.clear() creates file if absent ───────────────────────────────── Deno.test("persist FileStore.clear() creates file when it does not exist", () => {