Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Error handling and circular dependency management. #19

Merged
merged 4 commits into from
Nov 29, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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