a reactive (signals based) hypermedia web framework (wip) stormlightlabs.github.io/volt/
hypermedia frontend signals
at main 11 kB view raw
1import { computed, signal } from "$core/signal"; 2import { 3 buildDependencyGraph, 4 detectCircularDependencies, 5 getDependencies, 6 getDependents, 7 getSignalDepth, 8 hasDependency, 9 recordDependencies, 10} from "$debug/graph"; 11import { registerSignal } from "$debug/registry"; 12import { describe, expect, it } from "vitest"; 13 14describe("debug/graph", () => { 15 describe("recordDependencies", () => { 16 it("records dependencies for a signal", () => { 17 const a = signal(1); 18 const b = signal(2); 19 const sum = computed(() => a.get() + b.get()); 20 21 recordDependencies(sum, [a, b]); 22 23 const deps = getDependencies(sum); 24 expect(deps).toHaveLength(2); 25 expect(deps).toContain(a); 26 expect(deps).toContain(b); 27 }); 28 29 it("records dependents bidirectionally", () => { 30 const a = signal(1); 31 const sum = computed(() => a.get() * 2); 32 33 recordDependencies(sum, [a]); 34 35 const dependents = getDependents(a); 36 expect(dependents).toHaveLength(1); 37 expect(dependents).toContain(sum); 38 }); 39 40 it("allows multiple dependents on one dependency", () => { 41 const a = signal(1); 42 const double = computed(() => a.get() * 2); 43 const triple = computed(() => a.get() * 3); 44 45 recordDependencies(double, [a]); 46 recordDependencies(triple, [a]); 47 48 const dependents = getDependents(a); 49 expect(dependents).toHaveLength(2); 50 expect(dependents).toContain(double); 51 expect(dependents).toContain(triple); 52 }); 53 54 it("accumulates dependencies on repeated calls", () => { 55 const a = signal(1); 56 const b = signal(2); 57 const c = signal(3); 58 const sum = computed(() => a.get() + b.get() + c.get()); 59 60 recordDependencies(sum, [a, b]); 61 recordDependencies(sum, [c]); 62 63 const deps = getDependencies(sum); 64 expect(deps).toHaveLength(3); 65 expect(deps).toContain(a); 66 expect(deps).toContain(b); 67 expect(deps).toContain(c); 68 }); 69 }); 70 71 describe("getDependencies", () => { 72 it("returns empty array for signal with no dependencies", () => { 73 const sig = signal(0); 74 const deps = getDependencies(sig); 75 expect(deps).toEqual([]); 76 }); 77 78 it("returns all recorded dependencies", () => { 79 const a = signal(1); 80 const b = signal(2); 81 const sum = computed(() => a.get() + b.get()); 82 83 recordDependencies(sum, [a, b]); 84 85 const deps = getDependencies(sum); 86 expect(deps).toHaveLength(2); 87 expect(deps).toContain(a); 88 expect(deps).toContain(b); 89 }); 90 }); 91 92 describe("getDependents", () => { 93 it("returns empty array for signal with no dependents", () => { 94 const sig = signal(0); 95 const deps = getDependents(sig); 96 expect(deps).toEqual([]); 97 }); 98 99 it("returns all dependents", () => { 100 const a = signal(1); 101 const double = computed(() => a.get() * 2); 102 const triple = computed(() => a.get() * 3); 103 104 recordDependencies(double, [a]); 105 recordDependencies(triple, [a]); 106 107 const dependents = getDependents(a); 108 expect(dependents).toHaveLength(2); 109 expect(dependents).toContain(double); 110 expect(dependents).toContain(triple); 111 }); 112 }); 113 114 describe("hasDependency", () => { 115 it("returns true when dependency exists", () => { 116 const a = signal(1); 117 const double = computed(() => a.get() * 2); 118 119 recordDependencies(double, [a]); 120 121 expect(hasDependency(double, a)).toBe(true); 122 }); 123 124 it("returns false when dependency does not exist", () => { 125 const a = signal(1); 126 const b = signal(2); 127 const double = computed(() => a.get() * 2); 128 129 recordDependencies(double, [a]); 130 expect(hasDependency(double, b)).toBe(false); 131 }); 132 133 it("returns false for signal with no dependencies", () => { 134 const a = signal(1); 135 const b = signal(2); 136 expect(hasDependency(a, b)).toBe(false); 137 }); 138 }); 139 140 describe("buildDependencyGraph", () => { 141 it("builds a graph with nodes and edges", () => { 142 const a = signal(1); 143 const b = signal(2); 144 const sum = computed(() => a.get() + b.get()); 145 146 registerSignal(a, "signal", "a"); 147 registerSignal(b, "signal", "b"); 148 registerSignal(sum, "computed", "sum"); 149 150 recordDependencies(sum, [a, b]); 151 152 const graph = buildDependencyGraph([a, b, sum]); 153 154 expect(graph.nodes).toHaveLength(3); 155 expect(graph.edges).toHaveLength(2); 156 }); 157 158 it("creates nodes with correct metadata", () => { 159 const a = signal(5); 160 registerSignal(a, "signal", "count"); 161 162 const graph = buildDependencyGraph([a]); 163 164 expect(graph.nodes).toHaveLength(1); 165 const node = graph.nodes[0]; 166 expect(node.signal).toBe(a); 167 expect(node.name).toBe("count"); 168 expect(node.type).toBe("signal"); 169 expect(node.value).toBe(5); 170 expect(node.id).toMatch(/^signal-\d+$/); 171 expect(node.dependencies).toEqual([]); 172 expect(node.dependents).toEqual([]); 173 }); 174 175 it("creates edges from dependencies to dependents", () => { 176 const a = signal(1); 177 const b = signal(2); 178 const sum = computed(() => a.get() + b.get()); 179 180 registerSignal(a, "signal", "a"); 181 registerSignal(b, "signal", "b"); 182 registerSignal(sum, "computed", "sum"); 183 184 recordDependencies(sum, [a, b]); 185 186 const graph = buildDependencyGraph([a, b, sum]); 187 const aId = graph.nodes.find((n) => n.name === "a")?.id; 188 const bId = graph.nodes.find((n) => n.name === "b")?.id; 189 const sumId = graph.nodes.find((n) => n.name === "sum")?.id; 190 191 expect(graph.edges).toContainEqual({ from: aId, to: sumId }); 192 expect(graph.edges).toContainEqual({ from: bId, to: sumId }); 193 }); 194 195 it("handles empty signal list", () => { 196 const graph = buildDependencyGraph([]); 197 expect(graph.nodes).toEqual([]); 198 expect(graph.edges).toEqual([]); 199 }); 200 201 it("includes dependency and dependent IDs in nodes", () => { 202 const a = signal(1); 203 const double = computed(() => a.get() * 2); 204 const quad = computed(() => double.get() * 2); 205 206 registerSignal(a, "signal", "a"); 207 registerSignal(double, "computed", "double"); 208 registerSignal(quad, "computed", "quad"); 209 210 recordDependencies(double, [a]); 211 recordDependencies(quad, [double]); 212 213 const graph = buildDependencyGraph([a, double, quad]); 214 215 const aNode = graph.nodes.find((n) => n.name === "a"); 216 const doubleNode = graph.nodes.find((n) => n.name === "double"); 217 const quadNode = graph.nodes.find((n) => n.name === "quad"); 218 219 expect(aNode?.dependencies).toEqual([]); 220 expect(aNode?.dependents).toEqual([doubleNode?.id]); 221 222 expect(doubleNode?.dependencies).toEqual([aNode?.id]); 223 expect(doubleNode?.dependents).toEqual([quadNode?.id]); 224 225 expect(quadNode?.dependencies).toEqual([doubleNode?.id]); 226 expect(quadNode?.dependents).toEqual([]); 227 }); 228 }); 229 230 describe("detectCircularDependencies", () => { 231 it("returns null when no cycle exists", () => { 232 const a = signal(1); 233 const double = computed(() => a.get() * 2); 234 235 recordDependencies(double, [a]); 236 237 const cycle = detectCircularDependencies(a); 238 expect(cycle).toBeNull(); 239 }); 240 241 it("detects direct self-dependency", () => { 242 const a = signal(1); 243 recordDependencies(a, [a]); 244 245 const cycle = detectCircularDependencies(a); 246 expect(cycle).not.toBeNull(); 247 expect(cycle).toContain(a); 248 }); 249 250 it("detects two-node cycle", () => { 251 const a = signal(1); 252 const b = computed(() => a.get() * 2); 253 254 recordDependencies(a, [b]); 255 recordDependencies(b, [a]); 256 257 const cycle = detectCircularDependencies(a); 258 expect(cycle).not.toBeNull(); 259 expect(cycle).toContain(a); 260 expect(cycle).toContain(b); 261 }); 262 263 it("detects multi-node cycle", () => { 264 const a = signal(1); 265 const b = computed(() => a.get() * 2); 266 const c = computed(() => b.get() * 2); 267 268 recordDependencies(a, [c]); 269 recordDependencies(b, [a]); 270 recordDependencies(c, [b]); 271 272 const cycle = detectCircularDependencies(a); 273 expect(cycle).not.toBeNull(); 274 expect(cycle).toContain(a); 275 expect(cycle).toContain(b); 276 expect(cycle).toContain(c); 277 }); 278 279 it("handles shared dependencies without false positives", () => { 280 const a = signal(1); 281 const b = computed(() => a.get() * 2); 282 const c = computed(() => a.get() * 3); 283 const sum = computed(() => b.get() + c.get()); 284 285 recordDependencies(b, [a]); 286 recordDependencies(c, [a]); 287 recordDependencies(sum, [b, c]); 288 289 const cycle = detectCircularDependencies(a); 290 expect(cycle).toBeNull(); 291 }); 292 }); 293 294 describe("getSignalDepth", () => { 295 it("returns 0 for signal with no dependencies", () => { 296 const sig = signal(0); 297 expect(getSignalDepth(sig)).toBe(0); 298 }); 299 300 it("returns 1 for signal depending on base signal", () => { 301 const a = signal(1); 302 const double = computed(() => a.get() * 2); 303 recordDependencies(double, [a]); 304 expect(getSignalDepth(double)).toBe(1); 305 }); 306 307 it("calculates depth for multi-level dependencies", () => { 308 const a = signal(1); 309 const double = computed(() => a.get() * 2); 310 const quad = computed(() => double.get() * 2); 311 const oct = computed(() => quad.get() * 2); 312 313 recordDependencies(double, [a]); 314 recordDependencies(quad, [double]); 315 recordDependencies(oct, [quad]); 316 317 expect(getSignalDepth(a)).toBe(0); 318 expect(getSignalDepth(double)).toBe(1); 319 expect(getSignalDepth(quad)).toBe(2); 320 expect(getSignalDepth(oct)).toBe(3); 321 }); 322 323 it("handles shared dependencies correctly", () => { 324 const a = signal(1); 325 const b = signal(2); 326 const double = computed(() => a.get() * 2); 327 const sum = computed(() => double.get() + b.get()); 328 329 recordDependencies(double, [a]); 330 recordDependencies(sum, [double, b]); 331 332 expect(getSignalDepth(sum)).toBe(2); 333 }); 334 335 it("uses maximum depth when multiple paths exist", () => { 336 const a = signal(1); 337 const b = computed(() => a.get() * 2); 338 const c = computed(() => b.get() * 2); 339 const d = computed(() => a.get() + c.get()); 340 341 recordDependencies(b, [a]); 342 recordDependencies(c, [b]); 343 recordDependencies(d, [a, c]); 344 345 expect(getSignalDepth(d)).toBe(3); 346 }); 347 348 it("handles circular dependencies gracefully", () => { 349 const a = signal(1); 350 const b = computed(() => a.get() * 2); 351 352 recordDependencies(a, [b]); 353 recordDependencies(b, [a]); 354 355 expect(() => getSignalDepth(a)).not.toThrow(); 356 }); 357 }); 358});