Something went wrong. Try again.
Monorepo for Tangled tangled.org
Something went wrong. Try again.
TypeScript
at sl/gitmirror
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173import { untrack } from "svelte";
// 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.
export const seededValue = <S, T>( seed: () => S | Promise<S>, map: (value: S) => T, failure: T): { readonly value: T | undefined } => { const initial = untrack(() => { const current = seed(); return current instanceof Promise ? undefined : map(current); }); let value = $state<T | undefined>(initial);
$effect(() => { const current = seed(); if (!(current instanceof Promise)) { value = map(current); return; } let active = true; value = undefined; void current .then((resolved) => { if (active) value = map(resolved); }) .catch(() => { if (active) value = failure; }); return () => { active = false; }; });
return { get value() { return value; } };};
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 | { key: string; value: number; up: boolean }>(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 loading: boolean; readonly failed: boolean; begin(intent?: boolean): void; created(rkey: string): void; deleted(): void; fail(): void; resetFailure(): void;}
export const createOptimisticRelation = ( options: OptimisticRelationOptions): OptimisticRelation => { let status = $state<"idle" | "loading" | "failed">("idle"); let pending = $state<boolean | null>(null); let committed = $state<null | { key: string; rkey: string | null }>(null); const currentKey = $derived(options.key()); const loaded = $derived(options.loadedRkey()); const rkey = $derived(committed?.key === currentKey ? committed.rkey : (loaded ?? null)); const known = $derived(loaded !== undefined || committed?.key === currentKey); const active = $derived(pending ?? rkey !== null);
$effect(() => { if (committed === null) return; if (committed.key !== currentKey || loaded === committed.rkey) { committed = null; status = "idle"; } });
const set = (next: string | null): void => { status = "idle"; pending = null; committed = { key: currentKey, rkey: next }; };
return { get rkey() { return rkey; }, get known() { return known; }, get active() { return active; }, get loading() { return status === "loading"; }, get failed() { return status === "failed"; }, begin(intent) { status = "loading"; pending = intent ?? null; }, created: set, deleted: () => set(null), fail() { status = "failed"; pending = null; }, resetFailure() { status = "idle"; } };};