feat(offline): add IndexedDB mutation queue (enqueue, dequeue, mark synced/conflict)

This commit is contained in:
Tyler
2026-06-17 14:06:42 -06:00
parent 6a9807a3be
commit d3b8e4f7cd
4 changed files with 149 additions and 0 deletions
+64
View File
@@ -0,0 +1,64 @@
// @vitest-environment jsdom
// src/lib/offline/queue.test.ts
import { describe, it, expect, beforeEach } from "vitest";
import "fake-indexeddb/auto";
import { enqueueAction, dequeueAction, getQueuedActions, markSynced, markConflict } from "./queue";
beforeEach(async () => {
// Clear the queue between tests
const { getOfflineDB } = await import("./db");
const db = await getOfflineDB();
await db.clear("queue");
});
describe("offline queue", () => {
it("enqueueAction stores an action with a generated clientActionId", async () => {
const action = await enqueueAction("markOrderReady", { orderId: "abc" });
expect(action.id).toMatch(/^[0-9a-f-]{36}$/i);
expect(action.actionName).toBe("markOrderReady");
expect(action.status).toBe("pending");
expect(action.attempts).toBe(0);
});
it("enqueueAction is idempotent on the same clientActionId", async () => {
const a = await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "fixed-id" });
const b = await enqueueAction("markOrderReady", { orderId: "abc" }, { id: "fixed-id" });
expect(a.id).toBe(b.id);
expect(a.id).toBe("fixed-id");
const all = await getQueuedActions();
expect(all).toHaveLength(1);
});
it("getQueuedActions returns pending actions in createdAt order", async () => {
const a = await enqueueAction("a", {}, { id: "a" });
// Small delay to ensure ordering
await new Promise((r) => setTimeout(r, 5));
const b = await enqueueAction("b", {}, { id: "b" });
const all = await getQueuedActions();
expect(all.map((x) => x.id)).toEqual([a.id, b.id]);
});
it("dequeueAction removes the action and returns its data", async () => {
const a = await enqueueAction("markOrderReady", { orderId: "abc" });
const removed = await dequeueAction(a.id);
expect(removed?.id).toBe(a.id);
const all = await getQueuedActions();
expect(all).toHaveLength(0);
});
it("markSynced removes the action", async () => {
const a = await enqueueAction("markOrderReady", { orderId: "abc" });
await markSynced(a.id);
const all = await getQueuedActions();
expect(all).toHaveLength(0);
});
it("markConflict moves the action to conflict status", async () => {
const a = await enqueueAction("markOrderReady", { orderId: "abc" });
await markConflict(a.id, "stale updated_at");
const all = await getQueuedActions();
expect(all).toHaveLength(1);
expect(all[0].status).toBe("conflict");
expect(all[0].error).toBe("stale updated_at");
});
});