diff --git a/src/lwc/signals/__tests__/computed.test.ts b/src/lwc/signals/__tests__/computed.test.ts index 102104c..8208fb7 100644 --- a/src/lwc/signals/__tests__/computed.test.ts +++ b/src/lwc/signals/__tests__/computed.test.ts @@ -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(() => {}); diff --git a/src/lwc/signals/__tests__/effect.test.ts b/src/lwc/signals/__tests__/effect.test.ts index ebd66e2..e3ed483 100644 --- a/src/lwc/signals/__tests__/effect.test.ts +++ b/src/lwc/signals/__tests__/effect.test.ts @@ -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 { @@ -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(() => { diff --git a/src/lwc/signals/core.ts b/src/lwc/signals/core.ts index e69d63c..72a812a 100644 --- a/src/lwc/signals/core.ts +++ b/src/lwc/signals/core.ts @@ -15,6 +15,10 @@ export type Signal = { peek(): T; }; +type Effect = { + identifier: string | symbol; +}; + const context: VoidFunction[] = []; function _getCurrentObserver(): VoidFunction | undefined { @@ -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() }; /** @@ -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): void { +function $effect(fn: VoidFunction, props?: Partial): Effect { const _props = { ...defaultEffectProps, ...props }; const effectNode: EffectNode = { error: null, @@ -92,21 +96,38 @@ function $effect(fn: VoidFunction, props?: Partial): 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; type ComputedProps = { - 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 = { + identifier: Symbol() +}; + +type Computed = ReadOnlySignal & { + identifier: string | symbol; }; /** @@ -128,7 +149,8 @@ type ComputedProps = { function $computed( fn: ComputedFunction, props?: Partial> -): ReadOnlySignal { +): Computed { + const _props = { ...defaultComputedProps, ...props }; const computedSignal: Signal = $signal(undefined, { track: true }); @@ -150,10 +172,13 @@ function $computed( }, { _fromComputed: true, - identifier: props?.identifier ?? null + identifier: _props.identifier } ); - return computedSignal.readOnly as ReadOnlySignal; + + const returnValue = computedSignal.readOnly as Computed; + returnValue.identifier = _props.identifier; + return returnValue; } type StorageFn = (value: T) => State & { [key: string]: unknown };