a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
0
fork

Configure Feed

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

at main 241 lines 5.5 kB view raw
1import type { ComputedSignal, Signal } from "$types/volt"; 2import { report } from "./error"; 3import { recordDep, startTracking, stopTracking } from "./tracker"; 4 5/** 6 * Creates a new signal with the given initial value. 7 * 8 * Signals are reactive primitives that notify subscribers when their value changes. 9 * When accessed inside a computed() or effect(), they are automatically tracked as dependencies. 10 * 11 * @param initialValue - The initial value of the signal 12 * @returns A Signal object with get, set, and subscribe methods 13 * 14 * @example 15 * const count = signal(0); 16 * count.subscribe(value => console.log('Count:', value)); 17 * count.set(1); // Logs: Count: 1 18 */ 19export function signal<T>(initialValue: T): Signal<T> { 20 let value = initialValue; 21 const subscribers = new Set<(value: T) => void>(); 22 23 const notify = () => { 24 const snapshot = [...subscribers]; 25 for (const callback of snapshot) { 26 try { 27 callback(value); 28 } catch (error) { 29 report(error as Error, { source: "effect" }); 30 } 31 } 32 }; 33 34 const sig: Signal<T> = { 35 get() { 36 recordDep(sig); 37 return value; 38 }, 39 40 set(newValue: T) { 41 if (value === newValue) { 42 return; 43 } 44 45 value = newValue; 46 notify(); 47 }, 48 49 subscribe(callback: (value: T) => void) { 50 subscribers.add(callback); 51 52 return () => { 53 subscribers.delete(callback); 54 }; 55 }, 56 }; 57 58 return sig; 59} 60 61/** 62 * Creates a computed signal that derives its value from other signals. 63 * 64 * Dependencies are automatically tracked by detecting which signals are accessed 65 * during the computation function execution. The computation is re-run whenever 66 * any of its dependencies change. 67 * 68 * @param compute - Function that computes the derived value 69 * @returns A ComputedSignal with get and subscribe methods 70 * 71 * @example 72 * const count = signal(5); 73 * const doubled = computed(() => count.get() * 2); 74 * doubled.get(); // 10 75 * count.set(10); 76 * doubled.get(); // 20 77 */ 78export function computed<T>(compute: () => T): ComputedSignal<T> { 79 let value: T; 80 let isInitialized = false; 81 let isRecomputing = false; 82 const subs = new Set<(value: T) => void>(); 83 const unsubscribers: Array<() => void> = []; 84 85 const notify = () => { 86 const snapshot = [...subs]; 87 for (const cb of snapshot) { 88 try { 89 cb(value); 90 } catch (error) { 91 report(error as Error, { source: "effect" }); 92 } 93 } 94 }; 95 96 const recompute = () => { 97 if (isRecomputing) { 98 throw new Error("Circular dependency detected in computed signal"); 99 } 100 101 isRecomputing = true; 102 let shouldNotify = false; 103 104 try { 105 for (const unsub of unsubscribers) { 106 unsub(); 107 } 108 unsubscribers.length = 0; 109 110 startTracking(comp); 111 try { 112 const newValue = compute(); 113 114 if (!isInitialized || value !== newValue) { 115 value = newValue; 116 isInitialized = true; 117 shouldNotify = subs.size > 0; 118 } 119 } catch (error) { 120 report(error as Error, { source: "effect" }); 121 throw error; 122 } finally { 123 const deps = stopTracking(); 124 125 for (const dep of deps) { 126 const unsub = dep.subscribe(recompute); 127 unsubscribers.push(unsub); 128 } 129 } 130 } finally { 131 isRecomputing = false; 132 } 133 134 if (shouldNotify) { 135 notify(); 136 } 137 }; 138 139 const comp: ComputedSignal<T> = { 140 get() { 141 if (!isInitialized) { 142 recompute(); 143 } 144 145 recordDep(comp); 146 return value; 147 }, 148 149 subscribe(callback: (value: T) => void) { 150 if (!isInitialized) { 151 recompute(); 152 } 153 154 subs.add(callback); 155 156 return () => { 157 subs.delete(callback); 158 }; 159 }, 160 }; 161 162 return comp; 163} 164 165/** 166 * Creates a side effect that runs when dependencies change. 167 * 168 * Dependencies are automatically tracked by detecting which signals are accessed 169 * during the effect function execution. The effect is re-run whenever any of its 170 * dependencies change. 171 * 172 * @param cb - Function to run as a side effect. Can return a cleanup function. 173 * @returns Cleanup function to stop the effect 174 * 175 * @example 176 * const count = signal(0); 177 * const cleanup = effect(() => { 178 * console.log('Count changed:', count.get()); 179 * }); 180 */ 181export function effect(cb: () => void | (() => void)): () => void { 182 let cleanup: (() => void) | void; 183 const unsubscribers: Array<() => void> = []; 184 let isDisposed = false; 185 186 const runEffect = () => { 187 if (isDisposed) { 188 return; 189 } 190 191 for (const unsub of unsubscribers) { 192 unsub(); 193 } 194 unsubscribers.length = 0; 195 196 if (cleanup) { 197 try { 198 cleanup(); 199 } catch (error) { 200 report(error as Error, { source: "effect" }); 201 } 202 cleanup = undefined; 203 } 204 205 startTracking(); 206 try { 207 cleanup = cb(); 208 } catch (error) { 209 report(error as Error, { source: "effect" }); 210 } finally { 211 const deps = stopTracking(); 212 213 for (const dep of deps) { 214 const unsub = dep.subscribe(runEffect); 215 unsubscribers.push(unsub); 216 } 217 } 218 }; 219 220 runEffect(); 221 222 return () => { 223 isDisposed = true; 224 225 if (cleanup) { 226 try { 227 cleanup(); 228 } catch (error) { 229 report(error as Error, { source: "effect" }); 230 } 231 } 232 233 for (const unsubscribe of unsubscribers) { 234 try { 235 unsubscribe(); 236 } catch (error) { 237 report(error as Error, { source: "effect" }); 238 } 239 } 240 }; 241}