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
15 changes: 9 additions & 6 deletions src/persist.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,8 +55,11 @@ export class FileStore<T = string> implements QueueStore<T> {
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;
}
Expand All @@ -74,11 +77,11 @@ export class FileStore<T = string> implements QueueStore<T> {
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;
}
}

Expand Down Expand Up @@ -107,5 +110,5 @@ export class MemoryStore<T = string> implements QueueStore<T> {
return [...this.events];
}

public dir(_dir: string): void {}
public dir(): void {}
}
75 changes: 75 additions & 0 deletions tests/persist_test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down Expand Up @@ -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<false>((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", () => {
Expand Down