import { expect, test } from "bun:test"; import { createSelectorCache, createSnapshotCache } from "../src/snapshot-cache.ts"; function fakeSource(initial: { count: number }) { let state = { ...initial }; const listeners = new Set<() => void>(); return { snapshot: () => Object.freeze({ ...state }), subscribe(listener: () => void) { listeners.add(listener); return () => listeners.delete(listener); }, mutate(next: { count: number }) { state = next; for (const listener of [...listeners]) listener(); }, }; } test("returns a referentially identical snapshot until a mutation occurs", () => { const source = fakeSource({ count: 0 }); const cache = createSnapshotCache(source); const first = cache.getSnapshot(); const second = cache.getSnapshot(); expect(first).toBe(second); source.mutate({ count: 1 }); const third = cache.getSnapshot(); expect(third).not.toBe(first); expect(third.count).toBe(1); }); test("dispose unsubscribes from the source", () => { const source = fakeSource({ count: 0 }); const cache = createSnapshotCache(source); cache.getSnapshot(); cache.dispose(); source.mutate({ count: 5 }); expect(cache.getSnapshot().count).toBe(0); }); test("selector cache keeps a stable result when the selected value is unchanged", () => { const source = fakeSource({ count: 0 }); const cache = createSnapshotCache(source); const select = createSelectorCache(cache.getSnapshot, (state) => state.count); const first = select(); expect(select()).toBe(first); // A new snapshot object, but the selected value is unchanged. source.mutate({ count: 0 }); expect(select()).toBe(first); source.mutate({ count: 2 }); expect(select()).toBe(2); }); test("an object-returning selector needs a custom equality function to stay stable", () => { const source = fakeSource({ count: 0 }); const cache = createSnapshotCache(source); // Default Object.is can never stabilize a selector that allocates: each call // produces a distinct reference. This is the classic useSyncExternalStore // infinite-render footgun, so island authors get an explicit escape hatch. const unstable = createSelectorCache(cache.getSnapshot, (state) => ({ label: `n=${state.count}`, })); const firstUnstable = unstable(); source.mutate({ count: 0 }); expect(unstable()).not.toBe(firstUnstable); const stable = createSelectorCache( cache.getSnapshot, (state) => ({ label: `n=${state.count}` }), (a, b) => a.label === b.label, ); const firstStable = stable(); source.mutate({ count: 0 }); expect(stable()).toBe(firstStable); source.mutate({ count: 2 }); expect(stable()).not.toBe(firstStable); expect(stable().label).toBe("n=2"); });