a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 4.8 kB view raw
1/** 2 * Signal registry for debugging and introspection. 3 * 4 * Tracks all signals with metadata for development tooling. 5 * Uses WeakMap to avoid memory leaks - signals can be garbage collected. 6 */ 7 8import type { Optional } from "$types/helpers"; 9import type { ComputedSignal, Signal, SignalMetadata, SignalType } from "$types/volt"; 10 11type SignalInfo = { id: string; type: SignalType; name?: string; value: unknown; createdAt: number; age: number }; 12 13type ReactiveInfo = { id: string; type: SignalType; name?: string; value: unknown; createdAt: number; age: number }; 14 15type RegistryStats = { totalSignals: number; regularSignals: number; computedSignals: number; reactiveObjects: number }; 16 17let nextId = 1; 18const signalMetadata = new WeakMap<Signal<unknown> | ComputedSignal<unknown>, SignalMetadata>(); 19const allSignals = new Set<WeakRef<Signal<unknown> | ComputedSignal<unknown>>>(); 20const reactiveMetadata = new WeakMap<object, SignalMetadata>(); 21const allReactives = new Set<WeakRef<object>>(); 22 23/** 24 * Register a signal in the debug registry. Should be called when a signal or computed is created. 25 */ 26export function registerSignal(sig: Signal<unknown> | ComputedSignal<unknown>, type: SignalType, name?: string): void { 27 if (signalMetadata.has(sig)) { 28 return; 29 } 30 31 const metadata: SignalMetadata = { id: `${type}-${nextId++}`, type, name, createdAt: Date.now() }; 32 33 signalMetadata.set(sig, metadata); 34 allSignals.add(new WeakRef(sig)); 35} 36 37export function getSignalMetadata(sig: Signal<unknown> | ComputedSignal<unknown>): SignalMetadata | undefined { 38 return signalMetadata.get(sig); 39} 40 41/** 42 * Get all currently tracked signals. 43 * Automatically cleans up garbage-collected signals. 44 */ 45export function getAllSignals(): Array<Signal<unknown> | ComputedSignal<unknown>> { 46 const active: Array<Signal<unknown> | ComputedSignal<unknown>> = []; 47 const toDelete: Array<WeakRef<Signal<unknown> | ComputedSignal<unknown>>> = []; 48 49 for (const ref of allSignals) { 50 const sig = ref.deref(); 51 if (sig) { 52 active.push(sig); 53 } else { 54 toDelete.push(ref); 55 } 56 } 57 58 for (const ref of toDelete) { 59 allSignals.delete(ref); 60 } 61 62 return active; 63} 64 65export function getSignalInfo(sig: Signal<unknown> | ComputedSignal<unknown>): Optional<SignalInfo> { 66 const metadata = signalMetadata.get(sig); 67 if (!metadata) { 68 return undefined; 69 } 70 71 return { 72 id: metadata.id, 73 type: metadata.type, 74 name: metadata.name, 75 value: sig.get(), 76 createdAt: metadata.createdAt, 77 age: Date.now() - metadata.createdAt, 78 }; 79} 80 81export function nameSignal(sig: Signal<unknown> | ComputedSignal<unknown>, name: string): void { 82 const metadata = signalMetadata.get(sig); 83 if (metadata) { 84 metadata.name = name; 85 } 86} 87 88export function clearRegistry(): void { 89 allSignals.clear(); 90 allReactives.clear(); 91 nextId = 1; 92} 93 94export function getRegistryStats(): RegistryStats { 95 const signals = getAllSignals(); 96 let regularSignals = 0; 97 let computedSignals = 0; 98 99 for (const sig of signals) { 100 const metadata = signalMetadata.get(sig); 101 if (metadata) { 102 if (metadata.type === "signal") { 103 regularSignals++; 104 } else { 105 computedSignals++; 106 } 107 } 108 } 109 110 const reactives = getAllReactives(); 111 112 return { totalSignals: signals.length, regularSignals, computedSignals, reactiveObjects: reactives.length }; 113} 114 115export function registerReactive(obj: object, name?: string): void { 116 if (reactiveMetadata.has(obj)) { 117 return; 118 } 119 120 const metadata: SignalMetadata = { id: `reactive-${nextId++}`, type: "reactive", name, createdAt: Date.now() }; 121 122 reactiveMetadata.set(obj, metadata); 123 allReactives.add(new WeakRef(obj)); 124} 125 126export function getReactiveMetadata(obj: object): SignalMetadata | undefined { 127 return reactiveMetadata.get(obj); 128} 129 130/** 131 * Get all currently tracked reactive objects. 132 * Automatically cleans up garbage-collected objects. 133 */ 134export function getAllReactives(): object[] { 135 const active: object[] = []; 136 const toDelete: Array<WeakRef<object>> = []; 137 138 for (const ref of allReactives) { 139 const obj = ref.deref(); 140 if (obj) { 141 active.push(obj); 142 } else { 143 toDelete.push(ref); 144 } 145 } 146 147 for (const ref of toDelete) { 148 allReactives.delete(ref); 149 } 150 151 return active; 152} 153 154export function getReactiveInfo(obj: object): Optional<ReactiveInfo> { 155 const metadata = reactiveMetadata.get(obj); 156 if (!metadata) { 157 return undefined; 158 } 159 160 return { 161 id: metadata.id, 162 type: metadata.type, 163 name: metadata.name, 164 value: obj, 165 createdAt: metadata.createdAt, 166 age: Date.now() - metadata.createdAt, 167 }; 168} 169 170export function nameReactive(obj: object, name: string): void { 171 const metadata = reactiveMetadata.get(obj); 172 if (metadata) { 173 metadata.name = name; 174 } 175}