a reactive (signals based) hypermedia web framework (wip)
stormlightlabs.github.io/volt/
hypermedia
frontend
signals
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}