Monorepo for Tangled tangled.org
2

Configure Feed

Select the types of activity you want to include in your feed.

core / web / src / lib / optimistic.svelte.ts
3.1 kB 118 lines
1// optimistic overlays for eventually-consistent bobbin reads: mutations commit 2// locally (the pds write is authoritative) and reconcile on natural reloads. 3// `key` scopes an overlay to its subject so reused components drop it. 4// TODO(bobbin): read-your-writes (serve at-or-after a commit rev) would let 5// mutations invalidate loads immediately instead of waiting for navigation. 6 7interface OptimisticCountOptions { 8 key: () => string; 9 loaded: () => number | null | undefined; 10} 11 12export interface OptimisticCount { 13 readonly value: number; 14 readonly failed: boolean; 15 adjust(delta: number): void; 16 fail(): void; 17 resetFailure(): void; 18} 19 20export const createOptimisticCount = (options: OptimisticCountOptions): OptimisticCount => { 21 let failed = $state(false); 22 // a bump is a bound: fresher data may pass it, never regress across it. 23 let held = $state<null | { key: string; value: number; up: boolean }>(null); 24 const currentKey = $derived(options.key()); 25 const loaded = $derived(Math.max(0, options.loaded() ?? 0)); 26 27 const value = $derived.by(() => { 28 if (held === null || held.key !== currentKey) return loaded; 29 return held.up ? Math.max(loaded, held.value) : Math.min(loaded, held.value); 30 }); 31 32 $effect(() => { 33 if (held === null) return; 34 const caughtUp = held.up ? loaded >= held.value : loaded <= held.value; 35 if (held.key !== currentKey || caughtUp) held = null; 36 }); 37 38 return { 39 get value() { 40 return value; 41 }, 42 get failed() { 43 return failed; 44 }, 45 adjust(delta) { 46 failed = false; 47 held = { key: currentKey, value: Math.max(0, value + delta), up: delta > 0 }; 48 }, 49 fail() { 50 failed = true; 51 }, 52 resetFailure() { 53 failed = false; 54 } 55 }; 56}; 57 58interface OptimisticRelationOptions { 59 key: () => string; 60 loadedRkey: () => string | null | undefined; 61} 62 63export interface OptimisticRelation { 64 readonly rkey: string | null; 65 readonly known: boolean; 66 readonly active: boolean; 67 readonly failed: boolean; 68 created(rkey: string): void; 69 deleted(): void; 70 fail(): void; 71 resetFailure(): void; 72} 73 74export const createOptimisticRelation = ( 75 options: OptimisticRelationOptions 76): OptimisticRelation => { 77 let failed = $state(false); 78 let committed = $state<null | { key: string; rkey: string | null }>(null); 79 const currentKey = $derived(options.key()); 80 const loaded = $derived(options.loadedRkey()); 81 const rkey = $derived(committed?.key === currentKey ? committed.rkey : (loaded ?? null)); 82 83 $effect(() => { 84 if (committed === null) return; 85 if (committed.key !== currentKey || loaded === committed.rkey) { 86 committed = null; 87 failed = false; 88 } 89 }); 90 91 const set = (next: string | null): void => { 92 failed = false; 93 committed = { key: currentKey, rkey: next }; 94 }; 95 96 return { 97 get rkey() { 98 return rkey; 99 }, 100 get known() { 101 return loaded !== undefined || committed?.key === currentKey; 102 }, 103 get active() { 104 return rkey !== null; 105 }, 106 get failed() { 107 return failed; 108 }, 109 created: set, 110 deleted: () => set(null), 111 fail() { 112 failed = true; 113 }, 114 resetFailure() { 115 failed = false; 116 } 117 }; 118};