[READ-ONLY] Mirror of https://github.com/bombshell-dev/playground.
0

Configure Feed

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

✅ rewrite freedom.test.ts imperatively; buffer createRoot events via Queue

Charles Lowell (Jun 29, 2026, 9:26 AM +0300) 6d806d09 f39c8d0e

+193 -620
+7 -6
packages/freedom/src/lib/root.ts
··· 1 - import { createSignal } from "effection"; 1 + import { createQueue, createSignal } from "effection"; 2 2 import type { Root } from "./types.ts"; 3 3 import { NodeImpl } from "./node.ts"; 4 4 import { TreeContext, type TreeState } from "./state.ts"; ··· 6 6 7 7 export function createRoot(): Root { 8 8 const output = createSignal<void, never>(); 9 - const events = createSignal<unknown, void>(); 9 + // Internal, always-drained buffer: createRoot is synchronous, so the drain 10 + // loop subscribes asynchronously — a Queue keeps events dispatched before the 11 + // loop runs from being lost (bounded, since the loop drains immediately). 12 + const events = createQueue<unknown, void>(); 10 13 11 14 let counter = 0; 12 15 const state: TreeState = { 13 16 dirty: false, 14 17 output, 15 - events, 16 18 nodes: new Map(), 17 19 nextId() { 18 20 return `node-${++counter}`; ··· 28 30 29 31 // Dispatch loop: drain events through the demux middleware chain. 30 32 node.scope.run(function* () { 31 - const sub = yield* events; 32 33 while (true) { 33 - const next = yield* sub.next(); 34 + const next = yield* events.next(); 34 35 if (next.done) { 35 36 break; 36 37 } ··· 45 46 return { 46 47 node, 47 48 dispatch(event) { 48 - events.send(event); 49 + events.add(event); 49 50 }, 50 51 [Symbol.iterator]: output[Symbol.iterator], 51 52 destroy() {
-1
packages/freedom/src/lib/state.ts
··· 4 4 export interface TreeState { 5 5 dirty: boolean; 6 6 output: Signal<void, never>; 7 - events: Signal<unknown, void>; 8 7 nodes: Map<string, NodeImpl>; 9 8 nextId(): string; 10 9 markDirty(): void;
-1
packages/freedom/src/lib/tree.ts
··· 22 22 const state: TreeState = { 23 23 dirty: false, 24 24 output, 25 - events, 26 25 nodes: new Map(), 27 26 nextId() { 28 27 return `node-${++counter}`;
+186 -612
packages/freedom/test/freedom.test.ts
··· 1 1 import { describe, expect, it } from "../test/suite.ts"; 2 2 import { run, sleep } from "effection"; 3 - import { 4 - append, 5 - DispatchApi, 6 - FreedomApi, 7 - get, 8 - set, 9 - sort, 10 - unset, 11 - update, 12 - useNode, 13 - useTree, 14 - } from "../src/index.ts"; 3 + import { createRoot, DispatchApi, NodeApi } from "../src/index.ts"; 15 4 16 5 describe("JsonValue validation", () => { 17 - it("JV1-JV12: accepts valid JsonValues", async () => { 18 - await run(function* () { 19 - let tree = yield* useTree(function* () {}); 20 - let result = yield* tree.root.eval(function* () { 21 - yield* set("str", "hello"); 22 - yield* set("num", 42); 23 - yield* set("zero", 0); 24 - yield* set("neg", -1.5); 25 - yield* set("bool", true); 26 - yield* set("boolFalse", false); 27 - yield* set("nil", null); 28 - yield* set("arr", [1, "a", true, null]); 29 - yield* set("obj", { a: 1, b: "c" }); 30 - yield* set("nested", { nested: { deep: [1, 2] } }); 31 - yield* set("emptyArr", []); 32 - yield* set("emptyObj", {}); 33 - }); 34 - expect(result.ok).toBe(true); 35 - expect(tree.root.props["str"]).toEqual("hello"); 36 - expect(tree.root.props["num"]).toEqual(42); 37 - expect(tree.root.props["nil"]).toEqual(null); 38 - expect(tree.root.props["nested"]).toEqual({ nested: { deep: [1, 2] } }); 39 - }); 6 + it("accepts valid JsonValues", () => { 7 + const root = createRoot(); 8 + const { node } = root; 9 + node.set("str", "hello"); 10 + node.set("num", 42); 11 + node.set("zero", 0); 12 + node.set("neg", -1.5); 13 + node.set("bool", true); 14 + node.set("nil", null); 15 + node.set("arr", [1, "a", true, null]); 16 + node.set("obj", { a: 1, b: "c" }); 17 + node.set("nested", { nested: { deep: [1, 2] } }); 18 + expect(node.props["str"]).toEqual("hello"); 19 + expect(node.props["nil"]).toEqual(null); 20 + expect(node.props["nested"]).toEqual({ nested: { deep: [1, 2] } }); 21 + root.destroy(); 40 22 }); 41 23 42 - it("JV13: rejects undefined", async () => { 43 - await run(function* () { 44 - yield* useTree(function* () { 45 - try { 46 - // deno-lint-ignore no-explicit-any 47 - yield* set("k", undefined as any); 48 - expect(true).toBe(false); 49 - } catch (e) { 50 - expect((e as Error).message).toContain("undefined"); 51 - } 52 - }); 53 - }); 24 + it("rejects undefined", () => { 25 + const root = createRoot(); 26 + expect(() => root.node.set("k", undefined as unknown as null)).toThrow(); 27 + root.destroy(); 54 28 }); 55 29 56 - it("JV14-JV16: rejects NaN and Infinity", async () => { 57 - await run(function* () { 58 - yield* useTree(function* () { 59 - for (let val of [NaN, Infinity, -Infinity]) { 60 - try { 61 - yield* set("k", val); 62 - expect(true).toBe(false); 63 - } catch (_e) { 64 - // expected 65 - } 66 - } 67 - }); 68 - }); 30 + it("rejects NaN and Infinity", () => { 31 + const root = createRoot(); 32 + for (const val of [NaN, Infinity, -Infinity]) { 33 + expect(() => root.node.set("k", val)).toThrow(); 34 + } 35 + root.destroy(); 69 36 }); 70 37 71 - it("JV17-JV20: rejects non-JSON types", async () => { 72 - await run(function* () { 73 - yield* useTree(function* () { 74 - for (let val of [() => {}, Symbol(), new Date(), new Map()]) { 75 - try { 76 - // deno-lint-ignore no-explicit-any 77 - yield* set("k", val as any); 78 - expect(true).toBe(false); 79 - } catch (_e) { 80 - // expected 81 - } 82 - } 83 - }); 84 - }); 38 + it("rejects non-JSON types", () => { 39 + const root = createRoot(); 40 + for (const val of [() => {}, Symbol(), new Date(), new Map()]) { 41 + expect(() => root.node.set("k", val as unknown as null)).toThrow(); 42 + } 43 + root.destroy(); 85 44 }); 86 45 87 - it("JV21-JV23: validates update return values", async () => { 88 - await run(function* () { 89 - yield* useTree(function* () { 90 - yield* set("n", 1); 91 - yield* update("n", () => 42); 92 - 93 - try { 94 - yield* update("n", () => undefined as unknown as number); 95 - expect(true).toBe(false); 96 - } catch (e) { 97 - expect((e as Error).message).toContain("undefined"); 98 - } 99 - }); 100 - }); 46 + it("validates update return values", () => { 47 + const root = createRoot(); 48 + root.node.set("n", 1); 49 + root.node.update("n", () => 42); 50 + expect(root.node.props["n"]).toEqual(42); 51 + expect(() => root.node.update("n", () => undefined as unknown as number)) 52 + .toThrow(); 53 + root.destroy(); 101 54 }); 102 55 }); 103 56 104 - describe("Node lifecycle", () => { 105 - it("NL1-NL6: creates child nodes via append", async () => { 106 - await run(function* () { 107 - let tree = yield* useTree(function* () { 108 - yield* append("child", function* () { 109 - yield* set("init", true); 110 - }); 111 - }); 112 - yield* sleep(0); 113 - 114 - let children = [...tree.root.children]; 115 - expect(children.length).toEqual(1); 116 - expect(children[0].name).toEqual("child"); 117 - expect(children[0].parent).toBe(tree.root); 118 - expect(children[0].props["init"]).toEqual(true); 119 - }); 57 + describe("Property bag", () => { 58 + it("set replaces, get reads", () => { 59 + const root = createRoot(); 60 + const { node } = root; 61 + node.set("a", 1); 62 + node.set("a", 2); 63 + node.set("b", 2); 64 + expect(node.get("a")).toEqual(2); 65 + expect(node.props["b"]).toEqual(2); 66 + root.destroy(); 120 67 }); 121 68 122 - it("NL7-NL11: assigns unique ids", async () => { 123 - await run(function* () { 124 - let tree = yield* useTree(function* () { 125 - yield* append("a", function* () {}); 126 - yield* append("b", function* () {}); 127 - }); 128 - yield* sleep(0); 129 - 130 - expect(tree.root.id).toBeTruthy(); 131 - let children = [...tree.root.children]; 132 - expect(children[0].id).not.toEqual(children[1].id); 133 - expect(tree.root.id).not.toEqual(children[0].id); 134 - }); 69 + it("update transforms", () => { 70 + const root = createRoot(); 71 + root.node.set("n", 1); 72 + root.node.update("n", (v) => (v as number) + 1); 73 + expect(root.node.props["n"]).toEqual(2); 74 + root.destroy(); 135 75 }); 136 76 137 - it("NL12: remove() removes node from parent", async () => { 138 - await run(function* () { 139 - let tree = yield* useTree(function* () {}); 140 - let result = yield* tree.root.eval(function* () { 141 - let child = yield* append("child", function* () { 142 - yield* set("name", "child"); 143 - }); 144 - yield* child.remove(); 145 - }); 146 - expect(result.ok).toBe(true); 147 - expect([...tree.root.children].length).toEqual(0); 148 - }); 77 + it("unset removes the key", () => { 78 + const root = createRoot(); 79 + root.node.set("a", 1); 80 + root.node.unset("a"); 81 + expect("a" in root.node.props).toBe(false); 82 + root.destroy(); 149 83 }); 150 84 151 - it("N12: remove() on root raises error", async () => { 152 - await run(function* () { 153 - let tree = yield* useTree(function* () {}); 154 - let result = yield* tree.root.eval(function* () { 155 - yield* tree.root.remove(); 156 - }); 157 - expect(result.ok).toBe(false); 158 - }); 85 + it("unset of a missing key is a no-op", () => { 86 + const root = createRoot(); 87 + expect(() => root.node.unset("nope")).not.toThrow(); 88 + root.destroy(); 159 89 }); 160 90 161 - it("NL18: init-only component keeps node alive", async () => { 162 - await run(function* () { 163 - let tree = yield* useTree(function* () {}); 164 - yield* tree.root.eval(function* () { 165 - yield* set("alive", true); 166 - }); 167 - expect(tree.root.props["alive"]).toEqual(true); 168 - }); 91 + it("props is read-only", () => { 92 + const root = createRoot(); 93 + root.node.set("x", 1); 94 + expect(() => { 95 + (root.node.props as Record<string, number>)["x"] = 2; 96 + }).toThrow(); 97 + root.destroy(); 169 98 }); 170 99 }); 171 100 172 - describe("Property bag", () => { 173 - it("PB1-PB5: set operations", async () => { 174 - await run(function* () { 175 - let tree = yield* useTree(function* () {}); 176 - yield* tree.root.eval(function* () { 177 - yield* set("a", 1); 178 - yield* set("a", 2); 179 - yield* set("b", 2); 180 - yield* set("ns", { x: 1, y: 2 }); 181 - }); 182 - expect(tree.root.props["a"]).toEqual(2); 183 - expect(tree.root.props["b"]).toEqual(2); 184 - expect(tree.root.props["ns"]).toEqual({ x: 1, y: 2 }); 185 - }); 186 - }); 187 - 188 - it("PB6-PB8: update operations", async () => { 189 - await run(function* () { 190 - yield* useTree(function* () { 191 - yield* set("n", 1); 192 - yield* update("n", (v) => (v as number) + 1); 193 - yield* update("missing", (v) => v ?? 0); 194 - }); 195 - }); 196 - }); 197 - 198 - it("PB9: unset removes key", async () => { 199 - await run(function* () { 200 - let tree = yield* useTree(function* () { 201 - yield* set("a", 1); 202 - yield* unset("a"); 203 - }); 204 - expect("a" in tree.root.props).toBe(false); 205 - }); 101 + describe("Children and ordering", () => { 102 + it("createChild appends in insertion order", () => { 103 + const root = createRoot(); 104 + root.node.createChild("A"); 105 + root.node.createChild("B"); 106 + root.node.createChild("C"); 107 + expect([...root.node.children].map((c) => c.name)).toEqual(["A", "B", "C"]); 108 + root.destroy(); 206 109 }); 207 110 208 - it("PB10: unset nonexistent is no-op", async () => { 209 - await run(function* () { 210 - yield* useTree(function* () { 211 - yield* unset("nonexistent"); 212 - }); 213 - }); 111 + it("custom sort reorders children", () => { 112 + const root = createRoot(); 113 + const a = root.node.createChild("A"); 114 + const b = root.node.createChild("B"); 115 + const c = root.node.createChild("C"); 116 + a.set("priority", 3); 117 + b.set("priority", 1); 118 + c.set("priority", 2); 119 + root.node.sort((x, y) => 120 + (x.props["priority"] as number) - (y.props["priority"] as number) 121 + ); 122 + expect([...root.node.children].map((n) => n.name)).toEqual(["B", "C", "A"]); 123 + root.node.sort(undefined); 124 + expect([...root.node.children].map((n) => n.name)).toEqual(["A", "B", "C"]); 125 + root.destroy(); 214 126 }); 215 127 216 - it("PB11: props is read-only", async () => { 217 - await run(function* () { 218 - let tree = yield* useTree(function* () { 219 - yield* set("x", 1); 220 - }); 221 - expect(() => { 222 - // deno-lint-ignore no-explicit-any 223 - (tree.root.props as any)["x"] = 2; 224 - }).toThrow(); 225 - }); 128 + it("child ids are unique and the parent is wired", () => { 129 + const root = createRoot(); 130 + const a = root.node.createChild("a"); 131 + const b = a.createChild("b"); 132 + expect(b.parent).toBe(a); 133 + expect(new Set([root.node.id, a.id, b.id]).size).toEqual(3); 134 + root.destroy(); 226 135 }); 227 136 }); 228 137 229 - describe("Child ordering", () => { 230 - it("CO1-CO3: insertion order", async () => { 231 - await run(function* () { 232 - let tree = yield* useTree(function* () { 233 - yield* append("A", function* () {}); 234 - yield* append("B", function* () {}); 235 - yield* append("C", function* () {}); 236 - }); 237 - yield* sleep(0); 238 - 239 - let names = [...tree.root.children].map((c) => c.name); 240 - expect(names).toEqual(["A", "B", "C"]); 241 - }); 242 - }); 243 - 244 - it("CO4-CO6: custom sort", async () => { 245 - await run(function* () { 246 - let tree = yield* useTree(function* () { 247 - yield* sort((a, b) => { 248 - let ap = a.props["priority"] as number; 249 - let bp = b.props["priority"] as number; 250 - return ap - bp; 251 - }); 252 - yield* append("A", function* () { 253 - yield* set("priority", 3); 254 - }); 255 - yield* append("B", function* () { 256 - yield* set("priority", 1); 257 - }); 258 - yield* append("C", function* () { 259 - yield* set("priority", 2); 260 - }); 261 - }); 262 - yield* sleep(0); 263 - 264 - let names = [...tree.root.children].map((c) => c.name); 265 - expect(names).toEqual(["B", "C", "A"]); 138 + describe("Mutation interception (NodeApi)", () => { 139 + it("set interceptor can transform values", () => { 140 + const root = createRoot(); 141 + root.node.scope.around(NodeApi, { 142 + set([node, key, value], next) { 143 + next(node, key, key === "doubled" ? (value as number) * 2 : value); 144 + }, 266 145 }); 267 - }); 268 - }); 269 - 270 - describe("Node.eval", () => { 271 - it("runs operations in the node's scope", async () => { 272 - await run(function* () { 273 - let tree = yield* useTree(function* () { 274 - yield* set("before", true); 275 - }); 276 - 277 - let result = yield* tree.root.eval(function* () { 278 - yield* set("after", true); 279 - return 42; 280 - }); 281 - 282 - expect(result).toEqual({ ok: true, value: 42 }); 283 - expect(tree.root.props["after"]).toEqual(true); 284 - }); 285 - }); 286 - 287 - it("captures errors as Result", async () => { 288 - await run(function* () { 289 - let tree = yield* useTree(function* () {}); 290 - 291 - let result = yield* tree.root.eval(function* () { 292 - throw new Error("boom"); 293 - }); 294 - 295 - expect(result.ok).toBe(false); 296 - if (!result.ok) { 297 - expect(result.error.message).toEqual("boom"); 298 - } 299 - }); 300 - }); 301 - 302 - it("re-entrant eval on the current node runs inline without deadlock", async () => { 303 - await run(function* () { 304 - let tree = yield* useTree(function* () {}); 305 - 306 - let result = yield* tree.root.eval(function* () { 307 - // Re-entrant: eval the current node from inside its own eval loop. 308 - // Routing through the channel here would deadlock. 309 - let inner = yield* tree.root.eval(function* () { 310 - yield* set("inner", true); 311 - return "nested"; 312 - }); 313 - return inner; 314 - }); 315 - 316 - expect(result.ok).toBe(true); 317 - if (result.ok) { 318 - expect(result.value).toEqual({ ok: true, value: "nested" }); 319 - } 320 - expect(tree.root.props["inner"]).toEqual(true); 321 - }); 146 + root.node.set("doubled", 21); 147 + root.node.set("plain", 5); 148 + expect(root.node.props["doubled"]).toEqual(42); 149 + expect(root.node.props["plain"]).toEqual(5); 150 + root.destroy(); 322 151 }); 323 - }); 324 152 325 - describe("Tree and notification", () => { 326 - it("TN1: useTree returns tree with root", async () => { 327 - await run(function* () { 328 - let tree = yield* useTree(function* () {}); 329 - expect(tree.root).toBeTruthy(); 330 - expect(tree.root.parent).toBeUndefined(); 153 + it("get interceptor can transform reads", () => { 154 + const root = createRoot(); 155 + root.node.set("k", 1); 156 + root.node.scope.around(NodeApi, { 157 + get([node, key], next) { 158 + const val = next(node, key); 159 + return key === "k" ? (val as number) * 10 : val; 160 + }, 331 161 }); 162 + expect(root.node.get("k")).toEqual(10); 163 + root.destroy(); 332 164 }); 333 165 334 - it("TN2: root props visible via eval", async () => { 335 - await run(function* () { 336 - let tree = yield* useTree(function* () {}); 337 - yield* tree.root.eval(function* () { 338 - yield* set("ready", true); 339 - }); 340 - expect(tree.root.props["ready"]).toEqual(true); 166 + it("interceptors are inherited by descendants", () => { 167 + const root = createRoot(); 168 + let appended = 0; 169 + root.node.scope.around(NodeApi, { 170 + createChild([node, name], next) { 171 + appended++; 172 + return next(node, name); 173 + }, 341 174 }); 175 + const a = root.node.createChild("a"); 176 + a.createChild("b"); 177 + expect(appended).toEqual(2); 178 + root.destroy(); 342 179 }); 343 180 }); 344 181 345 - describe("Event dispatch", () => { 346 - it("ED1: middleware handles event via root.eval", async () => { 182 + describe("Dispatch and notification", () => { 183 + it("demux middleware handles an event", async () => { 347 184 await run(function* () { 185 + const root = createRoot(); 348 186 let handled = false; 349 - let tree = yield* useTree(function* () {}); 350 - yield* tree.root.eval(function* () { 351 - yield* DispatchApi.around({ 352 - *dispatch([event], next) { 353 - if (event === "ping") { 354 - handled = true; 355 - return { ok: true as const, value: true as const }; 356 - } 357 - return yield* next(event); 358 - }, 359 - }); 360 - }); 361 - tree.dispatch("ping"); 362 - yield* sleep(0); 363 - expect(handled).toBe(true); 364 - }); 365 - }); 366 - 367 - it("ED2: unhandled event", async () => { 368 - await run(function* () { 369 - let tree = yield* useTree(function* () {}); 370 - tree.dispatch("unknown"); 371 - yield* sleep(0); 372 - expect(tree.root).toBeTruthy(); 373 - }); 374 - }); 375 - 376 - it("ED3: sequential event processing", async () => { 377 - await run(function* () { 378 - let order: string[] = []; 379 - let tree = yield* useTree(function* () {}); 380 - yield* tree.root.eval(function* () { 381 - yield* DispatchApi.around({ 382 - *dispatch([event], _next) { 383 - order.push(event as string); 384 - yield* set("last", event as string); 187 + root.node.scope.around(DispatchApi, { 188 + *dispatch([event], next) { 189 + if (event === "ping") { 190 + handled = true; 385 191 return { ok: true as const, value: true as const }; 386 - }, 387 - }); 388 - }); 389 - tree.dispatch("first"); 390 - tree.dispatch("second"); 391 - yield* sleep(0); 392 - yield* sleep(0); 393 - expect(order).toEqual(["first", "second"]); 394 - expect(tree.root.props["last"]).toEqual("second"); 395 - }); 396 - }); 397 - 398 - it("ED4: middleware error captured, tree survives", async () => { 399 - await run(function* () { 400 - let tree = yield* useTree(function* () {}); 401 - yield* tree.root.eval(function* () { 402 - yield* DispatchApi.around({ 403 - *dispatch([_event], _next) { 404 - throw new Error("boom"); 405 - }, 406 - }); 192 + } 193 + return yield* next(event); 194 + }, 407 195 }); 408 - tree.dispatch("test"); 196 + root.dispatch("ping"); 409 197 yield* sleep(0); 410 - expect(tree.root).toBeTruthy(); 411 - 412 - // Subsequent dispatch works 413 - tree.dispatch("test2"); 414 - yield* sleep(0); 415 - }); 416 - }); 417 - 418 - it("ED-getNodeById: dispatch middleware resolves target node", async () => { 419 - await run(function* () { 420 - let resolved = false; 421 - let tree = yield* useTree(function* () {}); 422 - 423 - // Install middleware 424 - yield* tree.root.eval(function* () { 425 - yield* DispatchApi.around({ 426 - *dispatch([event], next) { 427 - let ev = event as { type: string; targetId: string }; 428 - if (ev.type === "focus") { 429 - let node = yield* DispatchApi.operations.getNodeById(ev.targetId); 430 - if (node) { 431 - resolved = true; 432 - expect(node.name).toEqual("target"); 433 - expect(node.props["found"]).toEqual(true); 434 - } 435 - return { ok: true as const, value: true as const }; 436 - } 437 - return yield* next(event); 438 - }, 439 - }); 440 - }); 441 - 442 - // Append child — spawn is lazy, so eval again to get the id 443 - // after the child has registered 444 - let id = yield* tree.root.eval(function* () { 445 - let child = yield* append("target", function* () { 446 - yield* set("found", true); 447 - }); 448 - return child.id; 449 - }); 450 - 451 - if (id.ok) { 452 - // By now the child spawn has run (eval sequentializes) 453 - tree.dispatch({ type: "focus", targetId: id.value }); 454 - yield* sleep(0); 455 - expect(resolved).toBe(true); 456 - } else { 457 - expect(true).toBe(false); 458 - } 198 + expect(handled).toBe(true); 199 + yield* root.destroy(); 459 200 }); 460 201 }); 461 - }); 462 202 463 - describe("Notification coalescing", () => { 464 - it("TN7: multiple sets in one dispatch = one notification", async () => { 203 + it("mutations in a dispatch cycle emit one coalesced notification", async () => { 465 204 await run(function* () { 466 - let tree = yield* useTree(function* () {}); 467 - yield* tree.root.eval(function* () { 468 - yield* DispatchApi.around({ 469 - *dispatch([_event], _next) { 470 - yield* set("a", 1); 471 - yield* set("b", 2); 472 - yield* set("c", 3); 473 - return { ok: true as const, value: true as const }; 474 - }, 475 - }); 205 + const root = createRoot(); 206 + root.node.scope.around(DispatchApi, { 207 + *dispatch([_event], _next) { 208 + root.node.set("a", 1); 209 + root.node.set("b", 2); 210 + root.node.set("c", 3); 211 + return { ok: true as const, value: true as const }; 212 + }, 476 213 }); 477 - 478 - let sub = yield* tree; 479 - tree.dispatch("multi-set"); 480 - let next = yield* sub.next(); 214 + const sub = yield* root; 215 + root.dispatch("multi"); 216 + const next = yield* sub.next(); 481 217 expect(next.done).toBe(false); 218 + expect(root.node.props["a"]).toEqual(1); 219 + yield* root.destroy(); 482 220 }); 483 221 }); 484 222 485 - it("TN9: no-change dispatch does not notify", async () => { 223 + it("a no-change dispatch does not notify", async () => { 486 224 await run(function* () { 487 - let tree = yield* useTree(function* () {}); 488 - yield* tree.root.eval(function* () { 489 - yield* DispatchApi.around({ 490 - *dispatch([_event], _next) { 491 - return { ok: true as const, value: true as const }; 492 - }, 493 - }); 225 + const root = createRoot(); 226 + root.node.scope.around(DispatchApi, { 227 + *dispatch([_event], _next) { 228 + return { ok: true as const, value: true as const }; 229 + }, 494 230 }); 495 - 496 - yield* tree; 497 - tree.dispatch("no-op"); 231 + yield* root; 232 + root.dispatch("noop"); 498 233 yield* sleep(0); 499 - // No notification emitted — dirty was false 500 - }); 501 - }); 502 - }); 503 - 504 - describe("get operation", () => { 505 - it("GA1: get returns stored value", async () => { 506 - await run(function* () { 507 - let tree = yield* useTree(function* () {}); 508 - yield* tree.root.eval(function* () { 509 - yield* set("k", 42); 510 - let val = yield* get("k"); 511 - expect(val).toEqual(42); 512 - }); 234 + yield* root.destroy(); 513 235 }); 514 236 }); 515 237 516 - it("GA2: get returns undefined for missing key", async () => { 238 + it("processes events sequentially", async () => { 517 239 await run(function* () { 518 - let tree = yield* useTree(function* () {}); 519 - yield* tree.root.eval(function* () { 520 - let val = yield* get("missing"); 521 - expect(val).toBeUndefined(); 522 - }); 523 - }); 524 - }); 525 - 526 - it("GA3: get middleware can intercept reads", async () => { 527 - await run(function* () { 528 - let tree = yield* useTree(function* () { 529 - yield* set("k", 1); 530 - yield* FreedomApi.around({ 531 - *get([key], next) { 532 - let val = yield* next(key); 533 - if (key === "k") { 534 - return (val as number) * 10; 535 - } 536 - return val; 537 - }, 538 - }); 240 + const root = createRoot(); 241 + const order: string[] = []; 242 + root.node.scope.around(DispatchApi, { 243 + *dispatch([event], _next) { 244 + order.push(event as string); 245 + return { ok: true as const, value: true as const }; 246 + }, 539 247 }); 248 + root.dispatch("first"); 249 + root.dispatch("second"); 540 250 yield* sleep(0); 541 - let result = yield* tree.root.eval(function* () { 542 - return yield* get("k"); 543 - }); 544 - expect(result).toEqual({ ok: true, value: 10 }); 545 - }); 546 - }); 547 - }); 548 - 549 - describe("remove operation", () => { 550 - it("RA1: remove destroys child node", async () => { 551 - await run(function* () { 552 - let tree = yield* useTree(function* () {}); 553 - let result = yield* tree.root.eval(function* () { 554 - let child = yield* append("child", function* () {}); 555 - yield* child.remove(); 556 - }); 557 - expect(result.ok).toBe(true); 558 - expect([...tree.root.children].length).toEqual(0); 559 - }); 560 - }); 561 - 562 - it("RA2: remove on root raises error", async () => { 563 - await run(function* () { 564 - let tree = yield* useTree(function* () {}); 565 - let result = yield* tree.root.eval(function* () { 566 - yield* tree.root.remove(); 567 - }); 568 - expect(result.ok).toBe(false); 569 - }); 570 - }); 571 - 572 - it("RA3: remove middleware can intercept removal", async () => { 573 - await run(function* () { 574 - let intercepted = false; 575 - let tree = yield* useTree(function* () { 576 - yield* FreedomApi.around({ 577 - *remove([node], next) { 578 - intercepted = true; 579 - yield* next(node); 580 - }, 581 - }); 582 - }); 583 - yield* tree.root.eval(function* () { 584 - let child = yield* append("child", function* () {}); 585 - yield* child.remove(); 586 - }); 587 - expect(intercepted).toBe(true); 588 - }); 589 - }); 590 - 591 - it("RA4: remove middleware runs before teardown", async () => { 592 - await run(function* () { 593 - let nameBeforeTeardown = ""; 594 - let tree = yield* useTree(function* () { 595 - yield* FreedomApi.around({ 596 - *remove([node], next) { 597 - nameBeforeTeardown = node.name; 598 - let found = [...tree.root.children].find( 599 - (c) => c.name === node.name, 600 - ); 601 - expect(found).toBeTruthy(); 602 - yield* next(node); 603 - }, 604 - }); 605 - }); 606 - yield* tree.root.eval(function* () { 607 - let child = yield* append("target", function* () {}); 608 - yield* child.remove(); 609 - }); 610 - expect(nameBeforeTeardown).toEqual("target"); 611 - }); 612 - }); 613 - }); 614 - 615 - describe("useNode operation", () => { 616 - it("UN1: returns root node in root component", async () => { 617 - await run(function* () { 618 - let tree = yield* useTree(function* () { 619 - let node = yield* useNode(); 620 - expect(node).toBe(tree.root); 621 - }); 622 - }); 623 - }); 624 - 625 - it("UN2: returns child node in child component", async () => { 626 - await run(function* () { 627 - let tree = yield* useTree(function* () { 628 - yield* append("child", function* () { 629 - let node = yield* useNode(); 630 - expect(node).not.toBe(tree.root); 631 - expect(node.name).toEqual("child"); 632 - }); 633 - }); 634 251 yield* sleep(0); 635 - }); 636 - }); 637 - 638 - it("UN3: returns eval target node via node.eval", async () => { 639 - await run(function* () { 640 - let tree = yield* useTree(function* () {}); 641 - let child = yield* tree.root.eval(function* () { 642 - return yield* append("target", function* () {}); 643 - }); 644 - if (!child.ok) throw child.error; 645 - 646 - let result = yield* child.value.eval(function* () { 647 - return yield* useNode(); 648 - }); 649 - if (!result.ok) throw result.error; 650 - 651 - expect(result.value).toBe(child.value); 652 - }); 653 - }); 654 - 655 - it("UN4: middleware can intercept useNode", async () => { 656 - await run(function* () { 657 - let tree = yield* useTree(function* () { 658 - yield* FreedomApi.around({ 659 - *useNode(_, next) { 660 - let node = yield* next(); 661 - return node; 662 - }, 663 - }); 664 - }); 665 - 666 - let intercepted = false; 667 - yield* tree.root.eval(function* () { 668 - let child = yield* append("child", function* () {}); 669 - yield* FreedomApi.around({ 670 - *useNode(_, next) { 671 - intercepted = true; 672 - return yield* next(); 673 - }, 674 - }); 675 - yield* child.eval(function* () { 676 - yield* useNode(); 677 - }); 678 - }); 679 - expect(intercepted).toBe(true); 252 + expect(order).toEqual(["first", "second"]); 253 + yield* root.destroy(); 680 254 }); 681 255 }); 682 256 });