clone

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

fix/175-template-quality-pass
lofi / package / runtime / write-handle_test.ts
write-handle_test.ts 3.6K
1import { assert, assertCount } from "./test-assert.ts";2import { createWriteHandle, WriteRejectedError } from "./write-handle.ts";34Deno.test("await resolves at saved with the write's value", async () => {5 const { handle, controller } = createWriteHandle<{ id: string }>("w1");6 controller.advance("saved", { id: "row-1" });7 const row = await handle;8 assert(row.id === "row-1", "the thenable must resolve with the saved value");9 assert(handle.stage === "saved", "stage must be saved after local durability");10});1112Deno.test("stages are monotonic and never regress", () => {13 const { handle, controller } = createWriteHandle<void>("w1");14 controller.advance("synced");15 controller.advance("saved");16 assert(handle.stage === "synced", "a later stage must not regress to an earlier one");17 controller.reject({ cause: "denied", code: "permission_denied", reason: "late" });18 assert(handle.stage === "synced", "a settled write must ignore late verdicts");19 assert(handle.reason === null, "a settled write must not carry a rejection");20});2122Deno.test("the public handle surface exposes no lifecycle mutators", () => {23 const { handle } = createWriteHandle<void>("w1");24 const exposed = handle as unknown as Record<string, unknown>;25 for (const name of ["advance", "reject", "fail", "setBatchId"]) {26 assert(typeof exposed[name] !== "function", `${name} must not be reachable on the handle`);27 }28});2930Deno.test("synced resolves with the value staged before confirmation", async () => {31 const { handle, controller } = createWriteHandle<{ id: string }>("w1");32 controller.advance("saving", { id: "row-9" });33 controller.advance("synced");34 const row = await handle.synced;35 assert(row.id === "row-9", "synced must resolve with the staged value");36});3738Deno.test("rejection settles both promises with WriteRejectedError and sets reason", async () => {39 const { handle, controller } = createWriteHandle<void>("w1");40 controller.advance("saved");41 controller.reject({ cause: "denied", code: "permission_denied", reason: "denied by store" });42 assert(handle.stage === "rejected", "the verdict must reach the stage");43 assert(handle.reason?.code === "permission_denied", "the rejection code must be readable");44 const error = await handle.synced.then(() => null, (thrown) => thrown as WriteRejectedError);45 assert(error instanceof WriteRejectedError, "synced must reject with WriteRejectedError");46 assert(error.writeId === "w1", "the error must carry the journal id");47});4849Deno.test("an unobserved rejected handle leaks no unhandled rejection", async () => {50 const { handle, controller } = createWriteHandle<void>("w1");51 controller.reject({ cause: "denied", code: null, reason: "refused" });52 // Nothing awaits the handle; settling the microtask queue must not surface53 // an unhandled rejection (the test runner fails the test if one escapes).54 await new Promise((resolve) => setTimeout(resolve, 0));55 assert(handle.stage === "rejected", "the refusal must still settle the stage");56});5758Deno.test("subscribers are level-triggered: late subscribers see current truth", () => {59 const { handle, controller } = createWriteHandle<void>("w1");60 controller.advance("saved");61 const seen: string[] = [];62 const stop = handle.subscribe(() => seen.push(handle.stage));63 assert(seen[0] === "saved", "a late subscriber must immediately observe the current stage");64 controller.advance("synced");65 assert(seen[1] === "synced", "subscribers must observe later transitions");66 stop();67 stop();68 controller.reject({ cause: "denied", code: null, reason: "ignored" });69 assertCount(seen.length, 2, "an unsubscribed listener must not be notified");70});