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

✨ synchronous node mutation interception via NodeApi + scope.around

Charles Lowell (Jun 29, 2026, 9:10 AM +0300) f39c8d0e 228ab197

+71 -22
+1
packages/freedom/src/lib/mod.ts
··· 11 11 export { createNodeData } from "./types.ts"; 12 12 13 13 export { createRoot } from "./root.ts"; 14 + export { NodeApi } from "./node.ts"; 14 15 export { useTree } from "./tree.ts"; 15 16 16 17 export {
+48 -20
packages/freedom/src/lib/node.ts
··· 10 10 type Result, 11 11 type Scope, 12 12 } from "effection"; 13 + import { createApi } from "effection/experimental"; 13 14 import type { JsonValue, Node, NodeData, NodeDataKey } from "./types.ts"; 14 15 import { TreeContext } from "./state.ts"; 15 16 import { validateJsonValue } from "./validate.ts"; ··· 110 111 } 111 112 112 113 get(key: string): JsonValue | undefined { 113 - return this._props[key]; 114 + return NodeApi.invoke(this.scope, "get", [this, key]); 114 115 } 115 116 116 117 set(key: string, value: JsonValue): void { 117 - validateJsonValue(value); 118 - this._props[key] = value; 119 - this.scope.expect(TreeContext).markDirty(); 118 + NodeApi.invoke(this.scope, "set", [this, key, value]); 120 119 } 121 120 122 121 update(key: string, fn: (prev: JsonValue | undefined) => JsonValue): void { 123 - const value = fn(this._props[key]); 124 - validateJsonValue(value); 125 - this._props[key] = value; 126 - this.scope.expect(TreeContext).markDirty(); 122 + NodeApi.invoke(this.scope, "update", [this, key, fn]); 127 123 } 128 124 129 125 unset(key: string): void { 130 - if (key in this._props) { 131 - delete this._props[key]; 132 - this.scope.expect(TreeContext).markDirty(); 133 - } 126 + NodeApi.invoke(this.scope, "unset", [this, key]); 134 127 } 135 128 136 129 createChild(name = ""): Node { 137 - const state = this.scope.expect(TreeContext); 138 - const child = new NodeImpl(state.nextId(), name, this); 139 - this._children.add(child); 140 - state.nodes.set(child.id, child); 141 - state.markDirty(); 142 - return child; 130 + return NodeApi.invoke(this.scope, "createChild", [this, name]); 143 131 } 144 132 145 133 sort(fn?: (a: Node, b: Node) => number): void { 146 - this._sortFn = fn; 147 - this.scope.expect(TreeContext).markDirty(); 134 + NodeApi.invoke(this.scope, "sort", [this, fn]); 148 135 } 149 136 150 137 destroy(): Promise<void> { ··· 155 142 throw new Error("Cannot remove root node"); 156 143 } 157 144 } 145 + 146 + // Synchronous node mutation API. Core methods take the node first; interceptors 147 + // are installed per scope via `node.scope.around(NodeApi, ...)`. 148 + export const NodeApi = createApi("freedom:node", { 149 + get(node: NodeImpl, key: string): JsonValue | undefined { 150 + return node._props[key]; 151 + }, 152 + set(node: NodeImpl, key: string, value: JsonValue): void { 153 + validateJsonValue(value); 154 + node._props[key] = value; 155 + node.scope.expect(TreeContext).markDirty(); 156 + }, 157 + update( 158 + node: NodeImpl, 159 + key: string, 160 + fn: (prev: JsonValue | undefined) => JsonValue, 161 + ): void { 162 + const value = fn(node._props[key]); 163 + validateJsonValue(value); 164 + node._props[key] = value; 165 + node.scope.expect(TreeContext).markDirty(); 166 + }, 167 + unset(node: NodeImpl, key: string): void { 168 + if (key in node._props) { 169 + delete node._props[key]; 170 + node.scope.expect(TreeContext).markDirty(); 171 + } 172 + }, 173 + createChild(node: NodeImpl, name: string): Node { 174 + const state = node.scope.expect(TreeContext); 175 + const child = new NodeImpl(state.nextId(), name, node); 176 + node._children.add(child); 177 + state.nodes.set(child.id, child); 178 + state.markDirty(); 179 + return child; 180 + }, 181 + sort(node: NodeImpl, fn: ((a: Node, b: Node) => number) | undefined): void { 182 + node._sortFn = fn; 183 + node.scope.expect(TreeContext).markDirty(); 184 + }, 185 + }); 158 186 159 187 export const NodeContext: Context<NodeImpl> = createContext<NodeImpl>( 160 188 "freedom:current-node",
+2 -1
packages/freedom/src/lib/types.ts
··· 1 - import type { Operation, Result, Stream } from "effection"; 1 + import type { Operation, Result, Scope, Stream } from "effection"; 2 2 3 3 export type JsonValue = 4 4 | string ··· 35 35 readonly children: Iterable<Node>; 36 36 readonly parent: Node | undefined; 37 37 readonly data: NodeData; 38 + readonly scope: Scope; 38 39 get(key: string): JsonValue | undefined; 39 40 set(key: string, value: JsonValue): void; 40 41 update(key: string, fn: (prev: JsonValue | undefined) => JsonValue): void;
+20 -1
packages/freedom/test/root.test.ts
··· 1 1 import { describe, expect, it } from "../test/suite.ts"; 2 - import { createRoot } from "../src/index.ts"; 2 + import { createRoot, NodeApi } from "../src/index.ts"; 3 3 4 4 describe("createRoot", () => { 5 5 it("returns a root with a parentless node that has an id", () => { ··· 20 20 expect(a.id).not.toEqual(b.id); 21 21 expect(a.id).not.toEqual(root.node.id); 22 22 expect([...root.node.children]).toEqual([a, b]); 23 + 24 + root.destroy(); 25 + }); 26 + 27 + it("a sync interceptor (scope.around NodeApi) transforms a mutation", () => { 28 + const root = createRoot(); 29 + root.node.scope.around(NodeApi, { 30 + set([node, key, value], next) { 31 + next(node, key, typeof value === "number" ? value * 10 : value); 32 + }, 33 + }); 34 + 35 + root.node.set("n", 5); 36 + expect(root.node.props["n"]).toEqual(50); 37 + 38 + // interceptor is inherited by descendants via the scope chain 39 + const child = root.node.createChild("c"); 40 + child.set("m", 2); 41 + expect(child.props["m"]).toEqual(20); 23 42 24 43 root.destroy(); 25 44 });