// optimistic overlays for eventually-consistent bobbin reads: mutations commit // locally (the pds write is authoritative) and reconcile on natural reloads. // `key` scopes an overlay to its subject so reused components drop it. // TODO(bobbin): read-your-writes (serve at-or-after a commit rev) would let // mutations invalidate loads immediately instead of waiting for navigation. interface OptimisticCountOptions { key: () => string; loaded: () => number | null | undefined; } export interface OptimisticCount { readonly value: number; readonly failed: boolean; adjust(delta: number): void; fail(): void; resetFailure(): void; } export const createOptimisticCount = (options: OptimisticCountOptions): OptimisticCount => { let failed = $state(false); // a bump is a bound: fresher data may pass it, never regress across it. let held = $state(null); const currentKey = $derived(options.key()); const loaded = $derived(Math.max(0, options.loaded() ?? 0)); const value = $derived.by(() => { if (held === null || held.key !== currentKey) return loaded; return held.up ? Math.max(loaded, held.value) : Math.min(loaded, held.value); }); $effect(() => { if (held === null) return; const caughtUp = held.up ? loaded >= held.value : loaded <= held.value; if (held.key !== currentKey || caughtUp) held = null; }); return { get value() { return value; }, get failed() { return failed; }, adjust(delta) { failed = false; held = { key: currentKey, value: Math.max(0, value + delta), up: delta > 0 }; }, fail() { failed = true; }, resetFailure() { failed = false; } }; }; interface OptimisticRelationOptions { key: () => string; loadedRkey: () => string | null | undefined; } export interface OptimisticRelation { readonly rkey: string | null; readonly known: boolean; readonly active: boolean; readonly failed: boolean; created(rkey: string): void; deleted(): void; fail(): void; resetFailure(): void; } export const createOptimisticRelation = ( options: OptimisticRelationOptions ): OptimisticRelation => { let failed = $state(false); let committed = $state(null); const currentKey = $derived(options.key()); const loaded = $derived(options.loadedRkey()); const rkey = $derived(committed?.key === currentKey ? committed.rkey : (loaded ?? null)); $effect(() => { if (committed === null) return; if (committed.key !== currentKey || loaded === committed.rkey) { committed = null; failed = false; } }); const set = (next: string | null): void => { failed = false; committed = { key: currentKey, rkey: next }; }; return { get rkey() { return rkey; }, get known() { return loaded !== undefined || committed?.key === currentKey; }, get active() { return rkey !== null; }, get failed() { return failed; }, created: set, deleted: () => set(null), fail() { failed = true; }, resetFailure() { failed = false; } }; };