Skip to content

Commit

Permalink
feat: Error handling and cyclical dependency management (#20)
Browse files Browse the repository at this point in the history
* Initial commit

* Effects throw when there is a circular dependency

* Effects throw when there is a circular dependency

* Removing unused import
  • Loading branch information
cesarParra authored Nov 29, 2024
1 parent 7b65921 commit 97fa7fe
Show file tree
Hide file tree
Showing 3 changed files with 104 additions and 10 deletions.
45 changes: 40 additions & 5 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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
Expand All @@ -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) {
Expand Down
9 changes: 9 additions & 0 deletions src/lwc/signals/__tests__/effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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();
});
});
60 changes: 55 additions & 5 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -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();
}
Expand All @@ -51,8 +73,22 @@ function $effect(fn: VoidFunction): void {
execute();
}

interface ComputedNode<T> {
signal: Signal<T | undefined>;
error: unknown;
state: symbol;
}

type ComputedFunction<T> = () => T;

function computedGetter<T>(node: ComputedNode<T>) {
if (node.state === ERRORED) {
throw node.error;
}

return node.signal.readOnly as ReadOnlySignal<T>;
}

/**
* Creates a new computed value that will be updated whenever the signals
* it reads from change. Returns a read-only signal that contains the
Expand All @@ -69,15 +105,29 @@ type ComputedFunction<T> = () => T;
* @param fn The function that returns the computed value.
*/
function $computed<T>(fn: ComputedFunction<T>): ReadOnlySignal<T> {
// The initial value is undefined, as it will be computed
// when the effect runs for the first time
const computedSignal: Signal<T | undefined> = $signal(undefined);
const computedNode: ComputedNode<T> = {
signal: $signal<T | undefined>(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<T>;
return computedGetter(computedNode);
}

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

0 comments on commit 97fa7fe

Please sign in to comment.