This repository has no description
0

Configure Feed

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

core / web / src / lib / optimistic.svelte.ts
3.3 kB 128 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 loading: boolean; 68 readonly failed: boolean; 69 begin(): void; 70 created(rkey: string): void; 71 deleted(): void; 72 fail(): void; 73 resetFailure(): void; 74} 75 76export const createOptimisticRelation = ( 77 options: OptimisticRelationOptions 78): OptimisticRelation => { 79 let status = $state<"idle" | "loading" | "failed">("idle"); 80 let committed = $state<null | { key: string; rkey: string | null }>(null); 81 const currentKey = $derived(options.key()); 82 const loaded = $derived(options.loadedRkey()); 83 const rkey = $derived(committed?.key === currentKey ? committed.rkey : (loaded ?? null)); 84 const known = $derived(loaded !== undefined || committed?.key === currentKey); 85 const active = $derived(rkey !== null); 86 87 $effect(() => { 88 if (committed === null) return; 89 if (committed.key !== currentKey || loaded === committed.rkey) { 90 committed = null; 91 status = "idle"; 92 } 93 }); 94 95 const set = (next: string | null): void => { 96 status = "idle"; 97 committed = { key: currentKey, rkey: next }; 98 }; 99 100 return { 101 get rkey() { 102 return rkey; 103 }, 104 get known() { 105 return known; 106 }, 107 get active() { 108 return active; 109 }, 110 get loading() { 111 return status === "loading"; 112 }, 113 get failed() { 114 return status === "failed"; 115 }, 116 begin() { 117 status = "loading"; 118 }, 119 created: set, 120 deleted: () => set(null), 121 fail() { 122 status = "failed"; 123 }, 124 resetFailure() { 125 status = "idle"; 126 } 127 }; 128};