Monorepo for Tangled
tangled.org
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};