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

♻️ run eval inline via per-node scope; remove the eval loop

Each node captures its task scope (useScope); eval repoints the current
routine's scope at the node's scope, runs the op inline, and restores —
immediate, non-serial, re-entrant by nesting. Removes the channel/eval-loop,
CallEval, box, and EvalOwner. Focus stays synchronous and no longer deadlocks
on cross-node setFocused during dispatch.

GA3 now waits a turn (sleep 0) for concurrent component init, like its siblings —
inline eval no longer grants the incidental scheduler turn the channel did.

Charles Lowell (Jun 27, 2026, 4:58 PM +0300) dd8a9237 ffcb9972

+32 -68
+3 -2
packages/freedom/src/lib/freedom.ts
··· 5 5 type Operation, 6 6 spawn, 7 7 suspend, 8 + useScope, 8 9 withResolvers, 9 10 } from "effection"; 10 11 import { createApi } from "effection/experimental"; ··· 14 15 type JsonValue, 15 16 type Node, 16 17 } from "./types.ts"; 17 - import { NodeContext, NodeImpl, spawnEvalLoop } from "./node.ts"; 18 + import { NodeContext, NodeImpl } from "./node.ts"; 18 19 import { TreeContext } from "./state.ts"; 19 20 import { validateJsonValue } from "./validate.ts"; 20 21 ··· 78 79 parent._children.add(child); 79 80 tree.nodes.set(child.id, child); 80 81 yield* NodeContext.set(child); 81 - yield* spawnEvalLoop(child); 82 + child.scope = yield* useScope(); 82 83 ready.resolve(); 83 84 try { 84 85 yield* component();
+25 -63
packages/freedom/src/lib/node.ts
··· 1 1 // oxlint-disable bombshell-dev/no-generic-error 2 2 // oxlint-disable max-params 3 3 import { 4 - type Channel, 5 4 type Context, 6 - createChannel, 7 5 createContext, 8 6 Err, 9 7 Ok, 10 8 type Operation, 11 9 type Result, 12 - spawn, 13 - type Stream, 14 - withResolvers, 10 + type Scope, 15 11 } from "effection"; 16 12 import type { JsonValue, Node, NodeData, NodeDataKey } from "./types.ts"; 17 13 ··· 38 34 } 39 35 } 40 36 41 - interface CallEval { 42 - operation: () => Operation<unknown>; 43 - resolve: (result: Result<unknown>) => void; 44 - } 45 - 46 - function box<T>(op: () => Operation<T>): Operation<Result<T>> { 47 - return { 48 - *[Symbol.iterator]() { 49 - try { 50 - return Ok(yield* op()); 51 - } catch (error) { 52 - return Err(error as Error); 53 - } 54 - }, 55 - }; 56 - } 57 - 58 37 export class NodeImpl implements Node { 59 38 _props: Record<string, JsonValue> = {}; 60 39 _children: Set<NodeImpl> = new Set(); 61 40 _sortFn: ((a: Node, b: Node) => number) | undefined = undefined; 62 - _channel: Channel<CallEval, never> = createChannel<CallEval, never>(); 63 41 data: NodeData = new NodeDataImpl(); 42 + scope!: Scope; 64 43 65 44 constructor( 66 45 readonly id: string, ··· 95 74 } 96 75 97 76 *eval<T>(op: () => Operation<T>): Operation<Result<T>> { 98 - // Re-entrant call from inside this node's own eval loop: running through 99 - // the channel would deadlock (the loop is busy awaiting us), so run inline. 100 - const owner = yield* EvalOwner.get(); 101 - if (owner === this) { 102 - return yield* box(op); 77 + // Run `op` inline with the current routine's scope temporarily repointed at 78 + // this node's scope, so the op sees this node's contexts/middleware. 79 + const restore = (yield { 80 + description: "freedom: enter node scope", 81 + enter: ( 82 + resolve: (result: Result<() => void>) => void, 83 + routine: { scope: Scope }, 84 + ) => { 85 + const original = routine.scope; 86 + routine.scope = this.scope; 87 + resolve(Ok(() => { 88 + routine.scope = original; 89 + })); 90 + return (resolveExit: (result: Result<void>) => void) => 91 + resolveExit(Ok()); 92 + }, 93 + }) as () => void; 94 + try { 95 + return Ok(yield* op()); 96 + } catch (error) { 97 + return Err(error as Error); 98 + } finally { 99 + restore(); 103 100 } 104 - const resolver = withResolvers<Result<T>>(); 105 - yield* this._channel.send({ 106 - resolve: resolver.resolve as (result: Result<unknown>) => void, 107 - operation: op as () => Operation<unknown>, 108 - }); 109 - return yield* resolver.operation; 110 101 } 111 102 112 103 remove(): Operation<void> { ··· 114 105 } 115 106 } 116 107 117 - export function* spawnEvalLoop(node: NodeImpl): Operation<void> { 118 - const ready = withResolvers<void>(); 119 - 120 - yield* spawn(function* () { 121 - const sub = yield* node._channel as Stream<CallEval, never>; 122 - // Mark this task's scope as the owner of node's eval loop, so a re-entrant 123 - // node.eval() running within it short-circuits inline instead of deadlocking. 124 - yield* EvalOwner.set(node); 125 - ready.resolve(); 126 - 127 - while (true) { 128 - const next = yield* sub.next(); 129 - if (next.done) { 130 - break; 131 - } 132 - const call = next.value; 133 - const result = yield* box(call.operation); 134 - call.resolve(result); 135 - } 136 - }); 137 - 138 - yield* ready.operation; 139 - } 140 - 141 108 export const NodeContext: Context<NodeImpl> = createContext<NodeImpl>( 142 109 "freedom:current-node", 143 110 ); 144 - 145 - // Set within a node's eval loop to identify re-entrant self-eval calls. 146 - const EvalOwner: Context<NodeImpl> = createContext<NodeImpl>( 147 - "freedom:eval-owner", 148 - );
+3 -3
packages/freedom/src/lib/tree.ts
··· 4 4 resource, 5 5 spawn, 6 6 suspend, 7 + useScope, 7 8 withResolvers, 8 9 } from "effection"; 9 10 import type { Component, Tree } from "./types.ts"; 10 - import { NodeContext, NodeImpl, spawnEvalLoop } from "./node.ts"; 11 + import { NodeContext, NodeImpl } from "./node.ts"; 11 12 import { TreeContext, type TreeState } from "./state.ts"; 12 13 import { DispatchApi } from "./dispatch.ts"; 13 14 import { FreedomApi } from "./freedom.ts"; ··· 41 42 42 43 // Spawn root node scope 43 44 yield* spawn(function* () { 45 + rootNode.scope = yield* useScope(); 44 46 yield* NodeContext.set(rootNode); 45 47 46 48 // Mark dirty after every mutation ··· 71 73 state.markDirty(); 72 74 }, 73 75 }); 74 - 75 - yield* spawnEvalLoop(rootNode); 76 76 77 77 // Subscribe to events, then spawn the event loop 78 78 const sub = yield* events;
+1
packages/freedom/test/freedom.test.ts
··· 537 537 }, 538 538 }); 539 539 }); 540 + yield* sleep(0); 540 541 let result = yield* tree.root.eval(function* () { 541 542 return yield* get("k"); 542 543 });