Skip to content

Commit 97fa7fe

Browse files
authored
feat: Error handling and cyclical dependency management (#20)
* Initial commit * Effects throw when there is a circular dependency * Effects throw when there is a circular dependency * Removing unused import
1 parent 7b65921 commit 97fa7fe

File tree

3 files changed

+104
-10
lines changed

3 files changed

+104
-10
lines changed

force-app/lwc/signals/core.js

Lines changed: 40 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,10 @@ const context = [];
66
function _getCurrentObserver() {
77
return context[context.length - 1];
88
}
9+
const UNSET = Symbol("UNSET");
10+
const COMPUTING = Symbol("COMPUTING");
11+
const ERRORED = Symbol("ERRORED");
12+
const READY = Symbol("READY");
913
/**
1014
* Creates a new effect that will be executed immediately and whenever
1115
* any of the signals it reads from change.
@@ -26,16 +30,34 @@ function _getCurrentObserver() {
2630
* @param fn The function to execute
2731
*/
2832
function $effect(fn) {
33+
const effectNode = {
34+
error: null,
35+
state: UNSET
36+
};
2937
const execute = () => {
38+
if (effectNode.state === COMPUTING) {
39+
throw new Error("Circular dependency detected");
40+
}
3041
context.push(execute);
3142
try {
43+
effectNode.state = COMPUTING;
3244
fn();
45+
effectNode.error = null;
46+
effectNode.state = READY;
3347
} finally {
3448
context.pop();
3549
}
3650
};
3751
execute();
3852
}
53+
function computedGetter(node) {
54+
if (node.state === ERRORED) {
55+
console.log("throwing error", node.error);
56+
throw node.error;
57+
}
58+
console.log("all good");
59+
return node.signal.readOnly;
60+
}
3961
/**
4062
* Creates a new computed value that will be updated whenever the signals
4163
* it reads from change. Returns a read-only signal that contains the
@@ -52,13 +74,26 @@ function $effect(fn) {
5274
* @param fn The function that returns the computed value.
5375
*/
5476
function $computed(fn) {
55-
// The initial value is undefined, as it will be computed
56-
// when the effect runs for the first time
57-
const computedSignal = $signal(undefined);
77+
const computedNode = {
78+
signal: $signal(undefined),
79+
error: null,
80+
state: UNSET
81+
};
5882
$effect(() => {
59-
computedSignal.value = fn();
83+
if (computedNode.state === COMPUTING) {
84+
throw new Error("Circular dependency detected");
85+
}
86+
try {
87+
computedNode.state = COMPUTING;
88+
computedNode.signal.value = fn();
89+
computedNode.error = null;
90+
computedNode.state = READY;
91+
} catch (error) {
92+
computedNode.state = ERRORED;
93+
computedNode.error = error;
94+
}
6095
});
61-
return computedSignal.readOnly;
96+
return computedGetter(computedNode);
6297
}
6398
class UntrackedState {
6499
constructor(value) {

src/lwc/signals/__tests__/effect.test.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,4 +14,13 @@ describe("effects", () => {
1414
signal.value = 1;
1515
expect(effectTracker).toBe(1);
1616
});
17+
18+
test("throw an error when a circular dependency is detected", () => {
19+
expect(() => {
20+
const signal = $signal(0);
21+
$effect(() => {
22+
signal.value = signal.value++;
23+
});
24+
}).toThrow();
25+
});
1726
});

src/lwc/signals/core.ts

Lines changed: 55 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,6 +19,16 @@ function _getCurrentObserver(): VoidFunction | undefined {
1919
return context[context.length - 1];
2020
}
2121

22+
const UNSET = Symbol("UNSET");
23+
const COMPUTING = Symbol("COMPUTING");
24+
const ERRORED = Symbol("ERRORED");
25+
const READY = Symbol("READY");
26+
27+
interface EffectNode {
28+
error: unknown;
29+
state: symbol;
30+
}
31+
2232
/**
2333
* Creates a new effect that will be executed immediately and whenever
2434
* any of the signals it reads from change.
@@ -39,10 +49,22 @@ function _getCurrentObserver(): VoidFunction | undefined {
3949
* @param fn The function to execute
4050
*/
4151
function $effect(fn: VoidFunction): void {
52+
const effectNode: EffectNode = {
53+
error: null,
54+
state: UNSET
55+
}
56+
4257
const execute = () => {
58+
if (effectNode.state === COMPUTING) {
59+
throw new Error("Circular dependency detected");
60+
}
61+
4362
context.push(execute);
4463
try {
64+
effectNode.state = COMPUTING;
4565
fn();
66+
effectNode.error = null;
67+
effectNode.state = READY;
4668
} finally {
4769
context.pop();
4870
}
@@ -51,8 +73,22 @@ function $effect(fn: VoidFunction): void {
5173
execute();
5274
}
5375

76+
interface ComputedNode<T> {
77+
signal: Signal<T | undefined>;
78+
error: unknown;
79+
state: symbol;
80+
}
81+
5482
type ComputedFunction<T> = () => T;
5583

84+
function computedGetter<T>(node: ComputedNode<T>) {
85+
if (node.state === ERRORED) {
86+
throw node.error;
87+
}
88+
89+
return node.signal.readOnly as ReadOnlySignal<T>;
90+
}
91+
5692
/**
5793
* Creates a new computed value that will be updated whenever the signals
5894
* it reads from change. Returns a read-only signal that contains the
@@ -69,15 +105,29 @@ type ComputedFunction<T> = () => T;
69105
* @param fn The function that returns the computed value.
70106
*/
71107
function $computed<T>(fn: ComputedFunction<T>): ReadOnlySignal<T> {
72-
// The initial value is undefined, as it will be computed
73-
// when the effect runs for the first time
74-
const computedSignal: Signal<T | undefined> = $signal(undefined);
108+
const computedNode: ComputedNode<T> = {
109+
signal: $signal<T | undefined>(undefined),
110+
error: null,
111+
state: UNSET
112+
};
75113

76114
$effect(() => {
77-
computedSignal.value = fn();
115+
if (computedNode.state === COMPUTING) {
116+
throw new Error("Circular dependency detected");
117+
}
118+
119+
try {
120+
computedNode.state = COMPUTING;
121+
computedNode.signal.value = fn();
122+
computedNode.error = null;
123+
computedNode.state = READY;
124+
} catch (error) {
125+
computedNode.state = ERRORED;
126+
computedNode.error = error;
127+
}
78128
});
79129

80-
return computedSignal.readOnly as ReadOnlySignal<T>;
130+
return computedGetter(computedNode);
81131
}
82132

83133
type StorageFn<T> = (value: T) => State<T> & { [key: string]: unknown };

0 commit comments

Comments
 (0)