diff --git a/force-app/lwc/signals/core.js b/force-app/lwc/signals/core.js index 6d4fbee..47d6505 100644 --- a/force-app/lwc/signals/core.js +++ b/force-app/lwc/signals/core.js @@ -6,6 +6,10 @@ const context = []; function _getCurrentObserver() { return context[context.length - 1]; } +const UNSET = Symbol("UNSET"); +const COMPUTING = Symbol("COMPUTING"); +const ERRORED = Symbol("ERRORED"); +const READY = Symbol("READY"); /** * Creates a new effect that will be executed immediately and whenever * any of the signals it reads from change. @@ -26,16 +30,34 @@ function _getCurrentObserver() { * @param fn The function to execute */ function $effect(fn) { + const effectNode = { + error: null, + state: UNSET + }; const execute = () => { + if (effectNode.state === COMPUTING) { + throw new Error("Circular dependency detected"); + } context.push(execute); try { + effectNode.state = COMPUTING; fn(); + effectNode.error = null; + effectNode.state = READY; } finally { context.pop(); } }; execute(); } +function computedGetter(node) { + if (node.state === ERRORED) { + console.log("throwing error", node.error); + throw node.error; + } + console.log("all good"); + return node.signal.readOnly; +} /** * Creates a new computed value that will be updated whenever the signals * it reads from change. Returns a read-only signal that contains the @@ -52,13 +74,26 @@ function $effect(fn) { * @param fn The function that returns the computed value. */ function $computed(fn) { - // The initial value is undefined, as it will be computed - // when the effect runs for the first time - const computedSignal = $signal(undefined); + const computedNode = { + signal: $signal(undefined), + error: null, + state: UNSET + }; $effect(() => { - computedSignal.value = fn(); + if (computedNode.state === COMPUTING) { + throw new Error("Circular dependency detected"); + } + try { + computedNode.state = COMPUTING; + computedNode.signal.value = fn(); + computedNode.error = null; + computedNode.state = READY; + } catch (error) { + computedNode.state = ERRORED; + computedNode.error = error; + } }); - return computedSignal.readOnly; + return computedGetter(computedNode); } class UntrackedState { constructor(value) { diff --git a/src/lwc/signals/__tests__/effect.test.ts b/src/lwc/signals/__tests__/effect.test.ts index ff43aff..5476483 100644 --- a/src/lwc/signals/__tests__/effect.test.ts +++ b/src/lwc/signals/__tests__/effect.test.ts @@ -14,4 +14,13 @@ describe("effects", () => { signal.value = 1; expect(effectTracker).toBe(1); }); + + test("throw an error when a circular dependency is detected", () => { + expect(() => { + const signal = $signal(0); + $effect(() => { + signal.value = signal.value++; + }); + }).toThrow(); + }); }); diff --git a/src/lwc/signals/core.ts b/src/lwc/signals/core.ts index 6861975..0a01b66 100644 --- a/src/lwc/signals/core.ts +++ b/src/lwc/signals/core.ts @@ -19,6 +19,16 @@ function _getCurrentObserver(): VoidFunction | undefined { return context[context.length - 1]; } +const UNSET = Symbol("UNSET"); +const COMPUTING = Symbol("COMPUTING"); +const ERRORED = Symbol("ERRORED"); +const READY = Symbol("READY"); + +interface EffectNode { + error: unknown; + state: symbol; +} + /** * Creates a new effect that will be executed immediately and whenever * any of the signals it reads from change. @@ -39,10 +49,22 @@ function _getCurrentObserver(): VoidFunction | undefined { * @param fn The function to execute */ function $effect(fn: VoidFunction): void { + const effectNode: EffectNode = { + error: null, + state: UNSET + } + const execute = () => { + if (effectNode.state === COMPUTING) { + throw new Error("Circular dependency detected"); + } + context.push(execute); try { + effectNode.state = COMPUTING; fn(); + effectNode.error = null; + effectNode.state = READY; } finally { context.pop(); } @@ -51,8 +73,22 @@ function $effect(fn: VoidFunction): void { execute(); } +interface ComputedNode { + signal: Signal; + error: unknown; + state: symbol; +} + type ComputedFunction = () => T; +function computedGetter(node: ComputedNode) { + if (node.state === ERRORED) { + throw node.error; + } + + return node.signal.readOnly as ReadOnlySignal; +} + /** * Creates a new computed value that will be updated whenever the signals * it reads from change. Returns a read-only signal that contains the @@ -69,15 +105,29 @@ type ComputedFunction = () => T; * @param fn The function that returns the computed value. */ function $computed(fn: ComputedFunction): ReadOnlySignal { - // The initial value is undefined, as it will be computed - // when the effect runs for the first time - const computedSignal: Signal = $signal(undefined); + const computedNode: ComputedNode = { + signal: $signal(undefined), + error: null, + state: UNSET + }; $effect(() => { - computedSignal.value = fn(); + if (computedNode.state === COMPUTING) { + throw new Error("Circular dependency detected"); + } + + try { + computedNode.state = COMPUTING; + computedNode.signal.value = fn(); + computedNode.error = null; + computedNode.state = READY; + } catch (error) { + computedNode.state = ERRORED; + computedNode.error = error; + } }); - return computedSignal.readOnly as ReadOnlySignal; + return computedGetter(computedNode); } type StorageFn = (value: T) => State & { [key: string]: unknown };