[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.

✨ createChild positional insert via { before }

Add an options arg to createChild so a fresh child can be spliced before an
existing sibling: createChild(name, { before }). Rebuilds the children Set to
splice in position. Throws when before is not a current child. options is left
open for future positioning hints (after, at).

Spec: C14/C15, N7, N22.

Charles Lowell (Jun 29, 2026, 11:17 AM +0300) 7f95abca 55129a85

+96 -12
+23 -6
packages/freedom/specs/freedom-spec.md
··· 197 197 set(key: string, value: JsonValue): void; 198 198 update(key: string, fn: (prev: JsonValue | undefined) => JsonValue): void; 199 199 unset(key: string): void; 200 - createChild(name?: string): Node; 200 + createChild(name?: string, options?: { before?: Node }): Node; 201 201 sort(fn?: (a: Node, b: Node) => number): void; 202 202 remove(): Promise<void>; 203 203 destroy(): Promise<void>; ··· 250 250 251 251 ### 5.4 Lifecycle 252 252 253 - N7. A node is created by `parent.createChild(name?)`: the constructor creates the 254 - child's scope (a child of the parent's), the child is attached to the parent's 255 - children, and it is returned synchronously. 253 + N7. A node is created by `parent.createChild(name?, options?)`: the constructor 254 + creates the child's scope (a child of the parent's), the child is attached to the 255 + parent's children, and it is returned synchronously. If `options.before` is 256 + given, the child is inserted immediately before that sibling in insertion order 257 + instead of being appended (C14). 256 258 257 259 N8. A node is destroyed by `node.remove()` (detach + teardown) or directly by 258 260 `node.destroy()`, which disposes the node's scope — halting all descendant node ··· 305 307 306 308 N21. Installing or clearing a sort function via `sort()` MUST emit a 307 309 notification (§8), because the iteration order of children may have changed. 310 + 311 + N22. A child MAY be inserted at a specific position via 312 + `createChild(name, { before })` (C14). This sets its position in **insertion 313 + order**; it does not bypass an active sort function (N20). The `options` object 314 + is the extension point for future positioning hints (e.g. `after`, `at`). 308 315 309 316 ### 5.7 Node Data 310 317 ··· 366 373 set(key: string, value: JsonValue): void; 367 374 update(key: string, fn: (prev: JsonValue | undefined) => JsonValue): void; 368 375 unset(key: string): void; 369 - createChild(name?: string): Node; 376 + createChild(name?: string, options?: { before?: Node }): Node; 370 377 sort(fn?: (a: Node, b: Node) => number): void; 371 378 remove(): Promise<void>; 372 379 destroy(): Promise<void>; ··· 388 395 node.set(key, value): void 389 396 node.update(key, fn: (prev: JsonValue | undefined) => JsonValue): void 390 397 node.unset(key): void 391 - node.createChild(name?): Node 398 + node.createChild(name?, options?: { before?: Node }): Node 392 399 node.sort(fn?: (a: Node, b: Node) => number): void 393 400 node.remove(): Promise<void> 394 401 node.destroy(): Promise<void> ··· 447 454 **synchronously**. 448 455 449 456 C13. `createChild` marks the tree dirty (§8). 457 + 458 + C14. `createChild(name?, options?)`: if `options.before` is provided, it MUST be a 459 + current child of this node; the new child is inserted **immediately before** it in 460 + insertion order. If `before` is not a current child, `createChild` throws. If 461 + `before` is omitted, the child is appended. 462 + 463 + C15. The `before` position sets the child's place in **insertion order**. An 464 + active sort function (N18) still reorders at read time, with insertion order as 465 + the equal-compare tiebreaker (N20). `options` is an open object reserved for 466 + future positioning hints (e.g. `after`, `at`). 450 467 451 468 **sort** 452 469
+27 -5
packages/freedom/src/lib/node.ts
··· 7 7 type Scope, 8 8 } from "effection"; 9 9 import { createApi } from "effection/experimental"; 10 - import type { JsonValue, Node, NodeData, NodeDataKey } from "./types.ts"; 10 + import type { 11 + CreateChildOptions, 12 + JsonValue, 13 + Node, 14 + NodeData, 15 + NodeDataKey, 16 + } from "./types.ts"; 11 17 import { TreeContext } from "./state.ts"; 12 18 import { validateJsonValue } from "./validate.ts"; 13 19 ··· 95 101 NodeApi.invoke(this.scope, "unset", [this, key]); 96 102 } 97 103 98 - createChild(name = ""): Node { 99 - return NodeApi.invoke(this.scope, "createChild", [this, name]); 104 + createChild(name = "", options?: CreateChildOptions): Node { 105 + return NodeApi.invoke(this.scope, "createChild", [this, name, options]); 100 106 } 101 107 102 108 sort(fn?: (a: Node, b: Node) => number): void { ··· 139 145 node.scope.expect(TreeContext).markDirty(); 140 146 } 141 147 }, 142 - createChild(node: NodeImpl, name: string): Node { 148 + createChild(node: NodeImpl, name: string, options?: CreateChildOptions): Node { 143 149 const state = node.scope.expect(TreeContext); 144 150 const child = new NodeImpl(state.nextId(), name, node); 145 - node._children.add(child); 151 + const before = options?.before; 152 + if (before) { 153 + if (!node._children.has(before as NodeImpl)) { 154 + throw new Error("createChild: `before` is not a child of this node"); 155 + } 156 + // Set has no positional insert, so rebuild it with `child` spliced in. 157 + const reordered = new Set<NodeImpl>(); 158 + for (const existing of node._children) { 159 + if (existing === before) { 160 + reordered.add(child); 161 + } 162 + reordered.add(existing); 163 + } 164 + node._children = reordered; 165 + } else { 166 + node._children.add(child); 167 + } 146 168 state.nodes.set(child.id, child); 147 169 state.markDirty(); 148 170 return child;
+5 -1
packages/freedom/src/lib/types.ts
··· 26 26 expect<T>(key: NodeDataKey<T>): T; 27 27 } 28 28 29 + export interface CreateChildOptions { 30 + before?: Node; 31 + } 32 + 29 33 export interface Node { 30 34 readonly id: string; 31 35 readonly name: string; ··· 38 42 set(key: string, value: JsonValue): void; 39 43 update(key: string, fn: (prev: JsonValue | undefined) => JsonValue): void; 40 44 unset(key: string): void; 41 - createChild(name?: string): Node; 45 + createChild(name?: string, options?: CreateChildOptions): Node; 42 46 sort(fn?: (a: Node, b: Node) => number): void; 43 47 destroy(): Promise<void>; 44 48 remove(): Promise<void>;
+41
packages/freedom/test/freedom.test.ts
··· 108 108 root.destroy(); 109 109 }); 110 110 111 + it("createChild inserts before a sibling", () => { 112 + const root = createRoot(); 113 + root.node.createChild("A"); 114 + const c = root.node.createChild("C"); 115 + root.node.createChild("B", { before: c }); 116 + expect([...root.node.children].map((n) => n.name)).toEqual(["A", "B", "C"]); 117 + root.destroy(); 118 + }); 119 + 120 + it("createChild before the first child inserts at the front", () => { 121 + const root = createRoot(); 122 + const a = root.node.createChild("A"); 123 + root.node.createChild("Z", { before: a }); 124 + expect([...root.node.children].map((n) => n.name)).toEqual(["Z", "A"]); 125 + root.destroy(); 126 + }); 127 + 128 + it("createChild throws when before is not a child", () => { 129 + const root = createRoot(); 130 + const a = root.node.createChild("A"); 131 + const stranger = root.node.createChild("B").createChild("nested"); 132 + expect(() => a.createChild("x", { before: stranger })).toThrow(); 133 + root.destroy(); 134 + }); 135 + 136 + it("before sets insertion order under an active sort tiebreaker", () => { 137 + const root = createRoot(); 138 + const a = root.node.createChild("A"); 139 + const c = root.node.createChild("C"); 140 + const b = root.node.createChild("B", { before: c }); 141 + for (const n of [a, b, c]) { 142 + n.set("priority", 1); 143 + } 144 + root.node.sort((x, y) => 145 + (x.props["priority"] as number) - (y.props["priority"] as number) 146 + ); 147 + // all equal -> insertion order (with B spliced before C) is the tiebreaker 148 + expect([...root.node.children].map((n) => n.name)).toEqual(["A", "B", "C"]); 149 + root.destroy(); 150 + }); 151 + 111 152 it("custom sort reorders children", () => { 112 153 const root = createRoot(); 113 154 const a = root.node.createChild("A");