- commit
- f6ce64d72ca8a6cbf579c02a7419480b3ec06f11
- parent
- f7e59b6c3c2a167b518f88d0d6eb89d342ae9175
- author
- Dami <dami.etoile@gmail.com>
| M | apps/ | 8 | ++- |
| M | apps/ | 78 | +++++++++++++++++++++++++++++- |
@@ -111,7 +111,7 @@ export class ChecklistStore {
111111 112112 async #settle(mutation: MutationHandle): Promise<void> {113113 const generation = ++this.#writeGeneration;114- this.#set({ durability: "none", error: null });114+ this.#set({ status: "ready", durability: "none", error: null });115115 try {116116 await mutation.wait({ tier: "local" });117117 this.#diagnostics.localWaitCalls += 1;@@ -126,12 +126,14 @@ export class ChecklistStore {
126126 }127127 },128128 (error) => {129- if (generation === this.#writeGeneration) this.#fail(error);129+ this.#diagnostics.mutationErrors += 1;130+ this.#fail(error);130131 },131132 );132133 }133134 } catch (error) {134- if (generation === this.#writeGeneration) this.#fail(error);135+ this.#diagnostics.mutationErrors += 1;136+ this.#fail(error);135137 throw error;136138 }137139 }@@ -29,20 +29,36 @@ function diagnostics(): RuntimeDiagnostics {
2929 };3030 }3131 32-function fakeDb() {32+function fakeDb(options: { allowGlobal?: boolean } = {}) {3333 let subscriber: ((delta: { all: unknown[] }) => void) | null = null;3434 let unsubscribeCalls = 0;3535 let rejectNextWrite: Error | null = null;36+ let deferredNextWrite: {37+ promise: Promise<void>;38+ resolve(): void;39+ reject(error: Error): void;40+ } | null = null;41+ let globalWaitCalls = 0;3642 const writes: Array<{ operation: "insert" | "update" | "delete"; value: unknown }> = [];3743 3844 function handle() {45+ const deferredWrite = deferredNextWrite;46+ deferredNextWrite = null;3947 return {4048 wait: ({ tier }: { tier: "local" | "global" }) => {49+ if (tier === "global") {50+ globalWaitCalls += 1;51+ if (!options.allowGlobal) {52+ return Promise.reject(new Error("unconfigured stores must not request global waits"));53+ }54+ return Promise.resolve();55+ }4156 if (tier === "local" && rejectNextWrite) {4257 const error = rejectNextWrite;4358 rejectNextWrite = null;4459 return Promise.reject(error);4560 }61+ if (tier === "local" && deferredWrite) return deferredWrite.promise;4662 return Promise.resolve();4763 },4864 };@@ -79,7 +95,18 @@ function fakeDb() {
7995 failNextWrite(message: string) {8096 rejectNextWrite = new Error(message);8197 },98+ deferNextWrite() {99+ let resolve!: () => void;100+ let reject!: (error: Error) => void;101+ const promise = new Promise<void>((accept, decline) => {102+ resolve = accept;103+ reject = decline;104+ });105+ deferredNextWrite = { promise, resolve, reject };106+ return deferredNextWrite;107+ },82108 writes,109+ globalWaitCalls: () => globalWaitCalls,83110 unsubscribeCalls: () => unsubscribeCalls,84111 };85112 }@@ -135,16 +162,23 @@ test("every checklist mutation waits for local durability", async () => {
135162 "create, edit, complete, and delete must reach the Jazz boundary",136163 );137164 assert(store.getSnapshot().durability === "local", "the UI may claim local after the wait");165+ assertCount(166+ db.globalWaitCalls(),167+ 0,168+ "an unconfigured store must never request global durability",169+ );138170 });139171 140172 test("configured sync advances the latest write from local to global durability", async () => {141- const store = new ChecklistStore(fakeDb().value, diagnostics(), true);173+ const db = fakeDb({ allowGlobal: true });174+ const store = new ChecklistStore(db.value, diagnostics(), true);142175 await store.create("sync after reconnect");143176 await Promise.resolve();144177 assert(145178 store.getSnapshot().durability === "global",146179 "the public mutation handle must make completed global durability visible",147180 );181+ assertCount(db.globalWaitCalls(), 1, "configured sync must request global durability");148182 });149183 150184 test("local write failures remain visible and reject the mutation", async () => {@@ -162,6 +196,46 @@ test("local write failures remain visible and reject the mutation", async () =>
162196 assert(store.getSnapshot().error === "disk full", "the durable-write cause must remain visible");163197 });164198 199+test("a superseded local-write failure remains visible", async () => {200+ const db = fakeDb();201+ const counts = diagnostics();202+ const store = new ChecklistStore(db.value, counts);203+ const firstWrite = db.deferNextWrite();204+205+ const firstMutation = store.create("first");206+ await Promise.resolve();207+ await store.create("newer");208+ firstWrite.reject(new Error("first write lost"));209+210+ let rejected = false;211+ try {212+ await firstMutation;213+ } catch {214+ rejected = true;215+ }216+ assert(rejected, "the superseded mutation promise must still reject");217+ assert(store.getSnapshot().status === "error", "the stale rejection must reach UI state");218+ assert(store.getSnapshot().durability === "failed", "the stale rejection must be durable state");219+ assert(store.getSnapshot().error === "first write lost", "the stale cause must remain visible");220+ assertCount(counts.mutationErrors, 1, "the stale failure must remain diagnosable");221+});222+223+test("retrying after failure clears the error status immediately", async () => {224+ const db = fakeDb();225+ const store = new ChecklistStore(db.value, diagnostics());226+ db.failNextWrite("disk full");227+ await store.create("fails").catch(() => undefined);228+ const retryWrite = db.deferNextWrite();229+230+ const retry = store.create("retry");231+ await Promise.resolve();232+ assert(store.getSnapshot().status === "ready", "retry must leave the failed list status");233+ assert(store.getSnapshot().error === null, "retry must not render a null error message");234+ assert(store.getSnapshot().durability === "none", "retry is pending until local durability");235+ retryWrite.resolve();236+ await retry;237+});238+165239 test("public mutation errors update observable diagnostics", () => {166240 const counts = diagnostics();167241 const store = new ChecklistStore(fakeDb().value, counts);