/** * The zustand `persist` ↔ `KVStore` seam (lib/store/kv-persist.ts). * * The seam routes a store's persistence through the KV contract and **nothing * more**: it reads and writes the KV scope and never touches a legacy * `localStorage` key. There is no migration of pre-cutover data — an empty KV * scope hydrates the store to defaults. * * What remains load-bearing is async-storage *steady state*: because a KV * backend can be remote and can fail, no failed read, out-of-order write, or * clear-vs-write race may end with the user's data replaced by defaults, and a * write the backend refused must either be replayed on recovery or reported as * lost. Most cases below are therefore *sequences*, not single calls. */ import { describe, it, expect, vi, beforeEach, afterEach, type Mock } from 'vitest'; import { BrowserKVStore, type DeviceSafeKVStore, type KVScope, type KVStore, } from '@openmaic/storage'; import { createKVPersistStorage, DEFAULT_RECOVERY_BACKOFF_MS } from '@/lib/store/kv-persist'; import { resetPersistHealth, subscribeToPersistHealth, type PersistHealthEvent, } from '@/lib/store/persist-health'; /** * Let the seam's detached recovery chain and the health channel's deferred * publish both run to completion. Several turns: recovery is scheduled on one, * the notice it does or does not cancel on the next. */ const flushTasks = async () => { for (let i = 0; i < 12; i++) await new Promise((resolve) => setTimeout(resolve, 0)); }; /** * A `KVStore` decorator that can be told to fail, or to hold an operation until * the test releases it. Faults are the subject of these cases, so they are * injected at the backend rather than mocked at the adapter. */ class ControllableKV implements DeviceSafeKVStore { // Device-safe: it wraps a local BrowserKVStore, so its device scope stays on // the machine. The brand lets it back a `'device'`-scoped persist store, which // #1002's `kvPersistStorage` requires of any device backend. readonly servesDeviceScopeLocally = true as const; failGet = false; failSet = false; failRemove = false; private setGate: { match: (key: string) => boolean; wait: Promise } | null = null; private readGate: { match: (key: string) => boolean; wait: Promise } | null = null; constructor(private readonly inner: KVStore) {} /** Hold the next matching `set` until the returned function is called. */ stallSet(match: (key: string) => boolean): () => void { let release!: () => void; const wait = new Promise((resolve) => { release = resolve; }); this.setGate = { match, wait }; return release; } /** * Hold the next matching `get` *after* it has read, so the caller resolves * with the value as of the moment it asked. That is the only way to model a * reader whose view of the store is a snapshot older than the store itself — * the normal state of affairs against a remote backend. */ stallGetAfterRead(match: (key: string) => boolean): () => void { let release!: () => void; const wait = new Promise((resolve) => { release = resolve; }); this.readGate = { match, wait }; return release; } async get(key: string, scope?: KVScope): Promise { if (this.failGet) throw new Error('kv get failed'); if (this.readGate?.match(key)) { const { wait } = this.readGate; this.readGate = null; const snapshot = await this.inner.get(key, scope); await wait; return snapshot; } return this.inner.get(key, scope); } async set(key: string, value: T, scope?: KVScope): Promise { if (this.failSet) throw new Error('kv set failed'); if (this.setGate?.match(key)) { const { wait } = this.setGate; this.setGate = null; await wait; } return this.inner.set(key, value, scope); } async remove(key: string, scope?: KVScope): Promise { if (this.failRemove) throw new Error('kv remove failed'); return this.inner.remove(key, scope); } async keys(prefix?: string, scope?: KVScope): Promise { return this.inner.keys(prefix, scope); } } const NAME = 'settings-storage'; const isBlobKey = (key: string) => key === NAME; interface Prefs { nickname: string; } /** In-memory `Storage`, isolated per test — nothing ambient is touched. */ class MemoryStorage implements Storage { private readonly entries = new Map(); get length(): number { return this.entries.size; } clear(): void { this.entries.clear(); } getItem(key: string): string | null { return this.entries.get(key) ?? null; } key(index: number): string | null { return [...this.entries.keys()][index] ?? null; } removeItem(key: string): void { this.entries.delete(key); } setItem(key: string, value: string): void { this.entries.set(key, value); } [name: string]: unknown; } function harness() { const backing = new MemoryStorage(); const kv = new ControllableKV(new BrowserKVStore({ storage: backing })); return { backing, kv, /** A fresh adapter over the same KV backend — what a page reload builds. */ storage: (scope: KVScope = 'account') => createKVPersistStorage(scope, { kv }), }; } type PersistStorageUnderTest = ReturnType['storage']>; /** `setItem` is refused until a read has settled the key, so hydrate first. */ async function hydrated(storage: PersistStorageUnderTest): Promise { await storage.getItem(NAME); return storage; } /** Health events raised during a test, in order. */ let health: PersistHealthEvent[] = []; /** Just the ones the user would actually see as a standing problem. */ const problems = () => health.filter((e) => e.status !== 'recovered').map((e) => e.name); beforeEach(() => { vi.spyOn(console, 'error').mockImplementation(() => {}); vi.spyOn(console, 'warn').mockImplementation(() => {}); resetPersistHealth(); health = []; subscribeToPersistHealth((event) => health.push(event)); }); afterEach(() => { vi.restoreAllMocks(); resetPersistHealth(); }); describe('createKVPersistStorage — round trip', () => { it('reads back what it wrote, envelope intact', async () => { const persist = await hydrated(harness().storage()); await persist.setItem(NAME, { state: { nickname: 'Ada' }, version: 4 }); expect(await persist.getItem(NAME)).toEqual({ state: { nickname: 'Ada' }, version: 4 }); }); it('returns null for a store that was never written', async () => { expect(await harness().storage().getItem(NAME)).toBeNull(); }); it('removeItem clears the entry', async () => { const persist = await hydrated(harness().storage()); await persist.setItem(NAME, { state: { nickname: 'Ada' } }); await persist.removeItem(NAME); expect(await persist.getItem(NAME)).toBeNull(); }); }); describe('createKVPersistStorage — empty KV hydrates defaults, no migration', () => { it('an empty KV scope reads null and the key becomes writable', async () => { // The whole "no migration" story at the adapter level: nothing is read but // the KV scope. Empty KV → null (zustand keeps its defaults) → settled, so // the user's first edit persists. const h = harness(); const persist = h.storage(); expect(await persist.getItem(NAME)).toBeNull(); await persist.setItem(NAME, { state: { nickname: 'Ada' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'Ada' }, version: 4 }); }); it('the adapter takes no legacy storage — there is nothing it could migrate', () => { // A structural guarantee, not a runtime one: the deps have no legacy-source // field, so no code path can read a pre-cutover raw key. const persist = createKVPersistStorage('account', { kv: harness().kv }); expect(typeof persist.getItem).toBe('function'); expect(Object.keys({ kv: harness().kv })).toEqual(['kv']); }); }); describe('createKVPersistStorage — scope', () => { it('writes under the scope it was given, and only that scope', async () => { const h = harness(); const persist = await hydrated(h.storage('device')); await persist.setItem(NAME, { state: { nickname: 'Ada' } }); expect(await h.kv.get(NAME, 'device')).toEqual({ state: { nickname: 'Ada' } }); expect(await h.kv.get(NAME, 'account')).toBeNull(); }); it('does not read across scopes', async () => { const h = harness(); await h.kv.set(NAME, { state: { nickname: 'Ada' } }, 'account'); // `device` data is defined as never leaving the machine, so the two scopes // stay disjoint even on a shared backend. expect(await h.storage('device').getItem(NAME)).toBeNull(); }); }); describe('createKVPersistStorage — a failed read never persists defaults', () => { it('returns null and refuses writes when the KV read fails', async () => { // A failed read is not an empty store. The key is left unsettled so the // always-on initializer's `set()` cannot land defaults over a value we // could not read. const h = harness(); h.kv.failGet = true; const persist = h.storage(); expect(await persist.getItem(NAME)).toBeNull(); await persist.setItem(NAME, { state: { nickname: '' }, version: 4 }); h.kv.failGet = false; expect(await h.kv.get(NAME, 'account')).toBeNull(); await flushTasks(); expect(problems()).toContain(NAME); }); it('surfaces a failed write instead of swallowing it', async () => { const h = harness(); const persist = await hydrated(h.storage()); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'Ada' } }); expect(console.error).toHaveBeenCalled(); }); it('refuses to persist before any read has settled the key', async () => { const h = harness(); await h.storage().setItem(NAME, { state: { nickname: 'premature' } }); expect(await h.kv.get(NAME, 'account')).toBeNull(); }); }); describe('createKVPersistStorage — writes issued during hydration', () => { it('refuses a write issued while the read is still in flight', async () => { // The gate has to be read when `setItem` is *called*. Checked inside the // queued task instead, this write waits behind `getItem`, finds the gate // opened by the very read it raced, and lands its pre-hydration snapshot // on top of the stored value. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); const releaseRead = h.kv.stallGetAfterRead(isBlobKey); const load = persist.getItem(NAME); // zustand issues this from a `set()` that ran before hydration resolved. const prematureWrite = persist.setItem(NAME, { state: { nickname: '' }, version: 4 }); releaseRead(); await Promise.all([load, prematureWrite]); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'stored' }, version: 4 }); }); it('allows the write zustand issues once hydration has resolved', async () => { const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'edited' }, version: 4 }); }); }); describe('createKVPersistStorage — write ordering', () => { it('applies writes in call order even when an earlier one resolves late', async () => { const h = harness(); const persist = await hydrated(h.storage()); const release = h.kv.stallSet(isBlobKey); const first = persist.setItem(NAME, { state: { nickname: 'first' }, version: 4 }); const second = persist.setItem(NAME, { state: { nickname: 'second' }, version: 4 }); release(); await Promise.all([first, second]); // Unserialized, the stalled first write lands last and silently rolls the // newer value back. expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'second' }, version: 4, }); }); it('a failed write closes the key rather than reporting a save that did not happen', async () => { const h = harness(); const persist = await hydrated(h.storage()); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'doomed' } }); await flushTasks(); expect(problems()).toEqual([NAME]); h.kv.failSet = false; await persist.setItem(NAME, { state: { nickname: 'Ada' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toBeNull(); }); it('reopens the key once a read confirms the backend is back', async () => { const h = harness(); const persist = await hydrated(h.storage()); await persist.setItem(NAME, { state: { nickname: 'stored' }, version: 4 }); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); h.kv.failSet = false; await persist.getItem(NAME); // The refused edit was taken against real hydrated data, so it is the // newest copy there is and gets replayed rather than dropped. expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'edited' }, version: 4 }); }); it('does not leave the queue wedged after a failed write', async () => { const h = harness(); const persist = await hydrated(h.storage()); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'doomed' } }); h.kv.failSet = false; // A read still runs — the chain is intact even though the key is closed. await expect(persist.getItem(NAME)).resolves.toBeNull(); }); it('does not replay a stale snapshot over a newer write that succeeded', async () => { // Both writes are admitted while the key is open, so both are queued. The // first fails and is remembered; the second lands. Replaying the first on // recovery would undo the second. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); await persist.getItem(NAME); const inner = h.kv; let sets = 0; const originalSet = inner.set.bind(inner); inner.set = async (key, value, scope) => { if (key === NAME && ++sets === 1) throw new Error('kv set failed'); return originalSet(key, value, scope); }; const first = persist.setItem(NAME, { state: { nickname: 'A' }, version: 4 }); const second = persist.setItem(NAME, { state: { nickname: 'B' }, version: 4 }); await Promise.all([first, second]); inner.set = originalSet; await persist.getItem(NAME); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'B' }, version: 4 }); }); }); describe('createKVPersistStorage — clearing closes the gate synchronously', () => { it('refuses a write issued while the clear is in flight', async () => { // An always-on initializer issues `set()` on every load. Admitted here, that // write queues behind the delete and puts the credentials the user just // cleared straight back. const h = harness(); const persist = await hydrated(h.storage()); await persist.setItem(NAME, { state: { nickname: 'secret' }, version: 4 }); const clearing = persist.removeItem(NAME); const racingWrite = persist.setItem(NAME, { state: { nickname: 'secret' }, version: 4 }); await Promise.all([clearing, racingWrite]); expect(await h.kv.get(NAME, 'account')).toBeNull(); }); it('does not replay a refused write across a clear', async () => { // Replaying here would resurrect exactly what the user asked to delete. const h = harness(); const persist = await hydrated(h.storage()); await persist.setItem(NAME, { state: { nickname: 'secret' }, version: 4 }); const clearing = persist.removeItem(NAME); await persist.setItem(NAME, { state: { nickname: 'secret' }, version: 4 }); await clearing; expect(await persist.getItem(NAME)).toBeNull(); expect(await h.kv.get(NAME, 'account')).toBeNull(); }); it('accepts writes again once the clear has completed', async () => { const h = harness(); const persist = await hydrated(h.storage()); await persist.removeItem(NAME); await persist.setItem(NAME, { state: { nickname: 'after' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'after' }, version: 4 }); }); it('propagates a backend failure instead of reporting a clear that did not happen', async () => { const h = harness(); const persist = await hydrated(h.storage()); await persist.setItem(NAME, { state: { nickname: 'Ada' }, version: 4 }); h.kv.failRemove = true; await expect(persist.removeItem(NAME)).rejects.toThrow(/Could not remove/); h.kv.failRemove = false; expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'Ada' }, version: 4 }); }); }); describe('createKVPersistStorage — refused writes ask for recovery, then say so', () => { it('asks the store to rehydrate on the first refusal only', async () => { const h = harness(); h.kv.failGet = true; const onWriteRefused = vi.fn(); const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused, recoveryBackoffMs: [0], }); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await persist.setItem(NAME, { state: { nickname: 'b' } }); await flushTasks(); expect(onWriteRefused).toHaveBeenCalledTimes(1); }); it('logs every refusal, not just the first', async () => { const h = harness(); h.kv.failGet = true; const persist = h.storage(); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await persist.setItem(NAME, { state: { nickname: 'b' } }); await persist.setItem(NAME, { state: { nickname: 'c' } }); const refusals = (console.error as unknown as Mock).mock.calls.filter((call) => String(call[0] ?? '').includes('Refusing to persist'), ); expect(refusals).toHaveLength(3); }); it('unblocks persistence when the recovery attempt works', async () => { const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' } }, 'account'); h.kv.failGet = true; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: async () => { h.kv.failGet = false; await persist.getItem(NAME); }, }); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await flushTasks(); await persist.setItem(NAME, { state: { nickname: 'b' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'b' }, version: 4 }); }); it('stays quiet through a recovery with nothing owed', async () => { // Storage broke and came back before anything was written. The warning is // published on a later task precisely so a recovery this quick can cancel // it before anyone sees it. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' } }, 'account'); const persist = h.storage(); h.kv.failGet = true; await persist.getItem(NAME); h.kv.failGet = false; await persist.getItem(NAME); await flushTasks(); expect(problems()).toEqual([]); }); it('raises a durable notice when recovery does not work', async () => { const h = harness(); h.kv.failGet = true; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: async () => { // The backend is still down, so the retry changes nothing. await persist.getItem(NAME); }, }); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await flushTasks(); expect(problems()).toEqual([NAME]); }); it('raises the notice when the recovery attempt itself throws', async () => { const h = harness(); h.kv.failGet = true; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: () => Promise.reject(new Error('rehydrate failed')), }); await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await flushTasks(); expect(problems()).toEqual([NAME]); }); }); describe('createKVPersistStorage — a replay is owed until it lands', () => { it('does not drop the edit when the recovering read works but the replay write fails', async () => { // Readable but not writable is a real state — a quota-exhausted backend // answers reads perfectly. Retiring the snapshot on the strength of the // read alone loses the edit silently. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); await persist.getItem(NAME); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); // Reads work again; writes still do not. expect(await persist.getItem(NAME)).toEqual({ state: { nickname: 'edited' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'stored' }, version: 4 }); // The edit is still owed, so the next recovery writes it rather than // handing back the stale stored value. h.kv.failSet = false; expect(await persist.getItem(NAME)).toEqual({ state: { nickname: 'edited' }, version: 4 }); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'edited' }, version: 4 }); }); it('eventually tells the user when the replay can never land', async () => { const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); await persist.getItem(NAME); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); await persist.getItem(NAME); await persist.getItem(NAME); await flushTasks(); expect(problems()).toContain(NAME); }); it('clears the debt once the replay lands', async () => { const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); await persist.getItem(NAME); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); h.kv.failSet = false; await persist.getItem(NAME); // A later write wins outright; the replay is not owed a second time. await persist.setItem(NAME, { state: { nickname: 'newer' }, version: 4 }); await persist.getItem(NAME); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'newer' }, version: 4 }); }); }); describe('createKVPersistStorage — a lost edit is reported, not swallowed', () => { it('says so when an edit made while storage was down cannot be replayed', async () => { // The edit was taken while the store held only defaults (the read failed // before it ever loaded a value), so replaying it would overwrite the // stored value with defaults. It cannot be replayed — and the user is told. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); h.kv.failGet = true; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: async () => { h.kv.failGet = false; await persist.getItem(NAME); }, }); await persist.getItem(NAME); // fails → defaults, unsettled await persist.setItem(NAME, { state: { nickname: 'edited' } }); await flushTasks(); expect(health.map((event) => event.status)).toContain('changes-lost'); }); }); describe('createKVPersistStorage — unreachable browser storage is a failure, not an empty store', () => { /** A browser whose `localStorage` getter throws, as privacy modes do. */ function withHostileLocalStorage(): () => void { const restore = Object.getOwnPropertyDescriptor(globalThis, 'localStorage'); Object.defineProperty(globalThis, 'localStorage', { configurable: true, get() { throw new Error('localStorage is denied'); }, }); vi.stubGlobal('window', {}); return () => { if (restore) Object.defineProperty(globalThis, 'localStorage', restore); else Reflect.deleteProperty(globalThis, 'localStorage'); vi.unstubAllGlobals(); }; } it('signals instead of hydrating an empty store in silence', async () => { const restore = withHostileLocalStorage(); try { const persist = createKVPersistStorage('account'); expect(await persist.getItem(NAME)).toBeNull(); await persist.setItem(NAME, { state: { nickname: 'lost' } }); await flushTasks(); expect(problems()).toEqual([NAME]); } finally { restore(); } }); it('reports a clear it could not perform', async () => { const restore = withHostileLocalStorage(); try { const persist = createKVPersistStorage('account'); await expect(persist.removeItem(NAME)).rejects.toThrow(/unreachable/); } finally { restore(); } }); }); describe('createKVPersistStorage — recovery is bounded and re-armable', () => { it('stops retrying a backend that reads but cannot write', async () => { // The treadmill: the recovering read succeeds, the key settles, the replay // write fails, and that failure asks for recovery again. A quota-exhausted // backend behaves exactly like this, and unbounded it is a loop that never // lets the page settle. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const attempts: string[] = []; const persist = createKVPersistStorage('account', { kv: h.kv, recoveryBackoffMs: [0, 0, 0], onWriteRefused: async (name) => { attempts.push(name); await persist.getItem(NAME); }, }); await persist.getItem(NAME); h.kv.failSet = true; await persist.setItem(NAME, { state: { nickname: 'edited' }, version: 4 }); for (let i = 0; i < 40; i++) await new Promise((resolve) => setTimeout(resolve, 0)); // Exactly the budget: not "a few", and emphatically not one per lap. expect(attempts).toHaveLength(3); expect(health.map((event) => event.status)).toContain('changes-lost'); }); it('ships a schedule that actually backs off', () => { // A guard on the shipped default: an all-zero schedule would leave the cap // as the only thing between a flaky backend and a busy loop. expect(DEFAULT_RECOVERY_BACKOFF_MS.length).toBeGreaterThan(1); const delays = [...DEFAULT_RECOVERY_BACKOFF_MS]; expect(delays.at(-1)).toBeGreaterThan(0); for (let i = 1; i < delays.length; i++) { expect(delays[i]).toBeGreaterThan(delays[i - 1]); } }); it('re-arms the budget once storage answers', async () => { const h = harness(); const asked: string[] = []; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: (name) => void asked.push(name), recoveryBackoffMs: [0, 0], }); h.kv.failGet = true; await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await flushTasks(); const spentOnFirstFault = asked.length; expect(spentOnFirstFault).toBe(2); // Storage comes back and the key settles clean... h.kv.failGet = false; await persist.getItem(NAME); await flushTasks(); expect(asked).toHaveLength(spentOnFirstFault); // ...so a later failure gets a fresh budget. h.kv.failGet = true; await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'b' } }); await flushTasks(); expect(asked.length).toBe(spentOnFirstFault + 2); }); it('spends the budget once, however many writes are refused', async () => { const h = harness(); const asked: string[] = []; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: (name) => void asked.push(name), recoveryBackoffMs: [0, 0, 0], }); h.kv.failGet = true; await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await persist.setItem(NAME, { state: { nickname: 'b' } }); await persist.setItem(NAME, { state: { nickname: 'c' } }); await persist.getItem(NAME); await flushTasks(); expect(asked).toHaveLength(3); }); it('stops for good once the budget is spent', async () => { const h = harness(); const asked: string[] = []; const persist = createKVPersistStorage('account', { kv: h.kv, onWriteRefused: (name) => void asked.push(name), recoveryBackoffMs: [0, 0], }); h.kv.failGet = true; await persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: 'a' } }); await flushTasks(); await flushTasks(); await flushTasks(); expect(asked).toHaveLength(2); }); }); describe('createKVPersistStorage — cold-start writes are not an alarm', () => { it('says nothing to the user when a healthy backend simply had not hydrated yet', async () => { // An always-on initializer writes on every load, routinely before the // first read resolves. Nothing is broken; a sticky red toast here would be // a bug report about a working app. const h = harness(); await h.kv.set(NAME, { state: { nickname: 'stored' }, version: 4 }, 'account'); const persist = h.storage(); const releaseRead = h.kv.stallGetAfterRead(isBlobKey); const load = persist.getItem(NAME); await persist.setItem(NAME, { state: { nickname: '' }, version: 4 }); releaseRead(); await load; await flushTasks(); expect(health).toEqual([]); expect(await h.kv.get(NAME, 'account')).toEqual({ state: { nickname: 'stored' }, version: 4 }); }); }); describe('createKVPersistStorage — no browser storage (SSR)', () => { beforeEach(() => { vi.unstubAllGlobals(); }); it('degrades to a no-op instead of throwing', async () => { // No `kv` injected and no ambient localStorage: the store module is // evaluated on the server too, where persist must simply do nothing. const persist = createKVPersistStorage('account'); expect(await persist.getItem(NAME)).toBeNull(); await expect(persist.setItem(NAME, { state: { nickname: 'Ada' } })).resolves.toBeUndefined(); await expect(persist.removeItem(NAME)).resolves.toBeUndefined(); }); });