From 25b3dda08daebc12114b0c898ee7724836e33799 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 29 Nov 2024 10:37:21 -0400 Subject: [PATCH 1/4] Initial commit --- src/lwc/signals/__tests__/computed.test.ts | 9 +++++ src/lwc/signals/core.ts | 44 +++++++++++++++++++--- 2 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/lwc/signals/__tests__/computed.test.ts b/src/lwc/signals/__tests__/computed.test.ts index 4666178..3d0b446 100644 --- a/src/lwc/signals/__tests__/computed.test.ts +++ b/src/lwc/signals/__tests__/computed.test.ts @@ -46,4 +46,13 @@ describe("computed values", () => { expect(computed.value).toBe(2); expect(anotherComputed.value).toBe(4); }); + + // test("throws an error when a circular dependency is detected", () => { + // const signal = $signal(0); + // const computed = $computed(() => signal.value * 2); + // const computed2 = $computed(() => computed.value + 1); + // + // // TODO + // + // }); }); diff --git a/src/lwc/signals/core.ts b/src/lwc/signals/core.ts index 6861975..67aa2b6 100644 --- a/src/lwc/signals/core.ts +++ b/src/lwc/signals/core.ts @@ -51,8 +51,27 @@ function $effect(fn: VoidFunction): void { execute(); } +interface ComputedNode { + signal: Signal; + error: unknown; + state: symbol; +} + +const UNSET = Symbol("UNSET"); +const COMPUTING = Symbol("COMPUTING"); +const ERRORED = Symbol("ERRORED"); +const READY = Symbol("READY"); + type ComputedFunction = () => T; +function computedGetter(node: ComputedNode) { + if (node.signal.value === 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 +88,30 @@ 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; + } catch (error) { + computedNode.state = ERRORED; + computedNode.error = error; + } + + computedNode.state = READY; }); - return computedSignal.readOnly as ReadOnlySignal; + return computedGetter(computedNode); } type StorageFn = (value: T) => State & { [key: string]: unknown }; From e9df5c991a6c19931c5298be6f071227b3e58cf6 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 29 Nov 2024 11:39:25 -0400 Subject: [PATCH 2/4] Effects throw when there is a circular dependency --- src/lwc/signals/__tests__/computed.test.ts | 11 +------- src/lwc/signals/__tests__/effect.test.ts | 9 ++++++ src/lwc/signals/core.ts | 32 ++++++++++++++++------ 3 files changed, 34 insertions(+), 18 deletions(-) diff --git a/src/lwc/signals/__tests__/computed.test.ts b/src/lwc/signals/__tests__/computed.test.ts index 3d0b446..18ee1e5 100644 --- a/src/lwc/signals/__tests__/computed.test.ts +++ b/src/lwc/signals/__tests__/computed.test.ts @@ -1,4 +1,4 @@ -import { $computed, $signal } from "../core"; +import { $computed, $effect, $signal } from "../core"; describe("computed values", () => { test("can be created from a source signal", () => { @@ -46,13 +46,4 @@ describe("computed values", () => { expect(computed.value).toBe(2); expect(anotherComputed.value).toBe(4); }); - - // test("throws an error when a circular dependency is detected", () => { - // const signal = $signal(0); - // const computed = $computed(() => signal.value * 2); - // const computed2 = $computed(() => computed.value + 1); - // - // // TODO - // - // }); }); 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 67aa2b6..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(); } @@ -57,15 +79,10 @@ interface ComputedNode { state: symbol; } -const UNSET = Symbol("UNSET"); -const COMPUTING = Symbol("COMPUTING"); -const ERRORED = Symbol("ERRORED"); -const READY = Symbol("READY"); - type ComputedFunction = () => T; function computedGetter(node: ComputedNode) { - if (node.signal.value === ERRORED) { + if (node.state === ERRORED) { throw node.error; } @@ -103,12 +120,11 @@ function $computed(fn: ComputedFunction): ReadOnlySignal { computedNode.state = COMPUTING; computedNode.signal.value = fn(); computedNode.error = null; + computedNode.state = READY; } catch (error) { computedNode.state = ERRORED; computedNode.error = error; } - - computedNode.state = READY; }); return computedGetter(computedNode); From d28e9b949d93acefd2c9120052ea45f362eaacf4 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 29 Nov 2024 13:48:17 -0400 Subject: [PATCH 3/4] Effects throw when there is a circular dependency --- force-app/lwc/signals/core.js | 45 +++++++++++++++++++++++++++++++---- 1 file changed, 40 insertions(+), 5 deletions(-) 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) { From 0199c0be592010e2c6382e4183c71603313a0d97 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Fri, 29 Nov 2024 14:23:23 -0400 Subject: [PATCH 4/4] Removing unused import --- src/lwc/signals/__tests__/computed.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/lwc/signals/__tests__/computed.test.ts b/src/lwc/signals/__tests__/computed.test.ts index 18ee1e5..4666178 100644 --- a/src/lwc/signals/__tests__/computed.test.ts +++ b/src/lwc/signals/__tests__/computed.test.ts @@ -1,4 +1,4 @@ -import { $computed, $effect, $signal } from "../core"; +import { $computed, $signal } from "../core"; describe("computed values", () => { test("can be created from a source signal", () => {