clone

a deno framework for local-first web apps that open instantly, work offline and sync when users choose

feat/141-demo-review-fixes
lofi / package / runtime / notice-queue_test.ts
notice-queue_test.ts 4.2K
1import { assert, assertCount } from "./test-assert.ts";2import { createMemoryJournalStorage } from "./write-journal.ts";3import { NoticeQueue } from "./notice-queue.ts";45function queue(now: () => number = () => 1000): {6 q: NoticeQueue;7 storage: ReturnType<typeof createMemoryJournalStorage>;8 counts: number[];9} {10 const storage = createMemoryJournalStorage();11 const counts: number[] = [];12 return { q: new NoticeQueue(storage, now, (count) => counts.push(count)), storage, counts };13}1415Deno.test("enqueue is idempotent by id, so an at-least-once re-delivery adds one entry", async () => {16 const { q, counts } = queue();17 await q.enqueue({ id: "w1:notice#1", message: "Saved.", tone: "success", ttlMs: null });18 await q.enqueue({ id: "w1:notice#1", message: "Saved.", tone: "success", ttlMs: null });19 assertCount(q.list().length, 1, "the second enqueue with one id must be a no-op");20 assertCount(counts.at(-1) ?? -1, 1, "the active-notice count must reflect one entry");21});2223Deno.test("dismiss removes one entry and notifies subscribers", async () => {24 const { q } = queue();25 let notified = 0;26 q.subscribe(() => notified += 1);27 await q.enqueue({ id: "a", message: "one", tone: "info", ttlMs: null });28 await q.enqueue({ id: "b", message: "two", tone: "info", ttlMs: null });29 q.dismiss("a");30 assertCount(q.list().length, 1, "dismiss must drop exactly the named entry");31 assert(q.list()[0].id === "b", "the surviving entry must be the one not dismissed");32 assert(notified >= 3, "each mutation must notify subscribers");33});3435Deno.test("a TTL entry retires once its window closes", async () => {36 let clock = 1000;37 const { q } = queue(() => clock);38 await q.enqueue({ id: "ttl", message: "temporary", tone: "warning", ttlMs: 5000 });39 assertCount(q.list().length, 1, "the entry is live inside its window");40 clock = 6001;41 assertCount(q.list().length, 0, "list must not surface an entry past its TTL");42});4344Deno.test("an entry enqueued during the load window is merged, not clobbered", async () => {45 const storage = createMemoryJournalStorage(46 JSON.stringify({47 version: 1,48 entries: [{ id: "persisted", message: "old", tone: "info", createdAt: 1, expiresAt: null }],49 }),50 );51 const q = new NoticeQueue(storage, () => 1000);52 // An effect fires at a boot re-arm and enqueues before load() resolves.53 const enqueued = q.enqueue({ id: "boot", message: "fresh", tone: "success", ttlMs: null });54 await Promise.all([q.load(), enqueued]);55 const ids = q.list().map((entry) => entry.id).sort();56 assertCount(ids.length, 2, "the boot entry and the persisted entry both survive");57 assert(ids.includes("boot") && ids.includes("persisted"), "neither entry is clobbered");58});5960Deno.test("a persisted queue reloads its entries and drops expired ones", async () => {61 let clock = 1000;62 const storage = createMemoryJournalStorage();63 const first = new NoticeQueue(storage, () => clock);64 await first.enqueue({ id: "keep", message: "durable", tone: "info", ttlMs: null });65 await first.enqueue({ id: "drop", message: "fleeting", tone: "info", ttlMs: 100 });66 await first.flush();6768 clock = 5000;69 const second = new NoticeQueue(storage, () => clock);70 await second.load();71 const ids = second.list().map((entry) => entry.id);72 assert(ids.includes("keep"), "a persisted entry must survive a reload");73 assert(!ids.includes("drop"), "an entry whose TTL passed while away must not reappear");74});7576Deno.test("enqueue rejects a failed save and replay retries persistence", async () => {77 const saves: string[] = [];78 let attempts = 0;79 const q = new NoticeQueue({80 load: () => Promise.resolve(null),81 save(text) {82 attempts += 1;83 if (attempts === 1) return Promise.reject(new Error("quota"));84 saves.push(text);85 return Promise.resolve();86 },87 });88 const input = { id: "retry", message: "durable", tone: "error" as const, ttlMs: null };89 let rejected = false;90 try {91 await q.enqueue(input);92 } catch {93 rejected = true;94 }95 assert(rejected, "the effect handler must observe a failed durable save");96 await q.enqueue(input);97 assert(attempts === 2, "re-delivery must retry persistence for an in-memory duplicate");98 assert(saves[0]?.includes('"id":"retry"') === true, "the replayed entry must become durable");99});