Skip to content

Commit

Permalink
feat: Effects and computed have a default identifier.
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed Dec 6, 2024
1 parent 2d09e5a commit 7db87fb
Show file tree
Hide file tree
Showing 3 changed files with 101 additions and 13 deletions.
5 changes: 5 additions & 0 deletions src/lwc/signals/__tests__/computed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -86,6 +86,11 @@ describe("computed values", () => {
spy.mockRestore();
});

test("have a default identifier", () => {
const computed = $computed(() => {});
expect(computed.identifier).toBeDefined();
});

test("console errors with an identifier when one was provided", () => {
const spy = jest.spyOn(console, "error").mockImplementation(() => {});

Expand Down
58 changes: 58 additions & 0 deletions src/lwc/signals/__tests__/effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,11 @@ describe("effects", () => {
}).toThrow();
});

test("return an object with an identifier", () => {
const effect = $effect(() => {});
expect(effect.identifier).toBeDefined();
});

test("console errors when an effect throws an error", () => {
const spy = jest.spyOn(console, "error").mockImplementation(() => {});
try {
Expand All @@ -52,6 +57,59 @@ describe("effects", () => {
spy.mockRestore();
});

test("console errors with the default identifier", () => {
const spy = jest.spyOn(console, "error").mockImplementation(() => {});

const signal = $signal(0);
const effect = $effect(() => {
if (signal.value === 1) {
throw new Error("test");
}
});

try {
signal.value = 1;
} catch (e) {
expect(spy).toHaveBeenCalledWith(expect.stringContaining(effect.identifier.toString()), expect.any(Error));
}

spy.mockRestore();
});

test("allow for the identifier to be overridden", () => {
const signal = $signal(0);
const effect = $effect(() => {
if (signal.value === 1) {
throw new Error("test");
}
}, {
identifier: "test-identifier"
});

expect(effect.identifier).toBe("test-identifier");
});

test("console errors with a custom identifier if provided", () => {
const spy = jest.spyOn(console, "error").mockImplementation(() => {});

const signal = $signal(0);
$effect(() => {
if (signal.value === 1) {
throw new Error("test");
}
}, {
identifier: "test-identifier"
});

try {
signal.value = 1;
} catch (e) {
expect(spy).toHaveBeenCalledWith(expect.stringContaining("test-identifier"), expect.any(Error));
}

spy.mockRestore();
});

test("allow for errors to be handled through a custom function", () => {
const customErrorHandlerFn = jest.fn();
$effect(() => {
Expand Down
51 changes: 38 additions & 13 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,10 @@ export type Signal<T> = {
peek(): T;
};

type Effect = {
identifier: string | symbol;
};

const context: VoidFunction[] = [];

function _getCurrentObserver(): VoidFunction | undefined {
Expand All @@ -33,13 +37,13 @@ interface EffectNode {

type EffectProps = {
_fromComputed: boolean;
identifier: string | null;
identifier: string | symbol;
errorHandler?: (error: unknown) => void;
};

const defaultEffectProps: EffectProps = {
_fromComputed: false,
identifier: null
identifier: Symbol()
};

/**
Expand All @@ -62,7 +66,7 @@ const defaultEffectProps: EffectProps = {
* @param fn The function to execute
* @param props Options to configure the effect
*/
function $effect(fn: VoidFunction, props?: Partial<EffectProps>): void {
function $effect(fn: VoidFunction, props?: Partial<EffectProps>): Effect {
const _props = { ...defaultEffectProps, ...props };
const effectNode: EffectNode = {
error: null,
Expand Down Expand Up @@ -92,21 +96,38 @@ function $effect(fn: VoidFunction, props?: Partial<EffectProps>): void {
};

execute();

return {
identifier: _props.identifier
};
}

function handleEffectError(error: unknown, props: EffectProps) {
const source =
(props._fromComputed ? "Computed" : "Effect") +
(props.identifier ? ` (${props.identifier})` : "");
const errorMessage = `An error occurred in a ${source} function`;
console.error(errorMessage, error);
const errorTemplate = `
LWC Signals: An error occurred in a reactive function \n
Type: ${props._fromComputed ? "Computed" : "Effect"} \n
Identifier: ${props.identifier.toString()}
`.trim();

console.error(errorTemplate, error);
throw error;
}

type ComputedFunction<T> = () => T;
type ComputedProps<T> = {
identifier: string | null;
errorHandler?: (error: unknown, previousValue: T | undefined) => T | undefined;
identifier: string | symbol;
errorHandler?: (
error: unknown,
previousValue: T | undefined
) => T | undefined;
};

const defaultComputedProps: ComputedProps<unknown> = {
identifier: Symbol()
};

type Computed<T> = ReadOnlySignal<T> & {
identifier: string | symbol;
};

/**
Expand All @@ -128,7 +149,8 @@ type ComputedProps<T> = {
function $computed<T>(
fn: ComputedFunction<T>,
props?: Partial<ComputedProps<T>>
): ReadOnlySignal<T> {
): Computed<T> {
const _props = { ...defaultComputedProps, ...props };
const computedSignal: Signal<T | undefined> = $signal(undefined, {
track: true
});
Expand All @@ -150,10 +172,13 @@ function $computed<T>(
},
{
_fromComputed: true,
identifier: props?.identifier ?? null
identifier: _props.identifier
}
);
return computedSignal.readOnly as ReadOnlySignal<T>;

const returnValue = computedSignal.readOnly as Computed<T>;
returnValue.identifier = _props.identifier;
return returnValue;
}

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

0 comments on commit 7db87fb

Please sign in to comment.