From 2b7b1bb6a7ccc6e589f7b61aa5006d0677f489e6 Mon Sep 17 00:00:00 2001 From: cesarParra Date: Wed, 20 Nov 2024 16:37:36 -0400 Subject: [PATCH] Tracking behind a flag --- src/lwc/signals/__tests__/computed.test.ts | 28 ++++++++-- src/lwc/signals/__tests__/effect.test.ts | 34 ++++++++++-- src/lwc/signals/core.ts | 60 +++++++++++++++++++--- 3 files changed, 107 insertions(+), 15 deletions(-) diff --git a/src/lwc/signals/__tests__/computed.test.ts b/src/lwc/signals/__tests__/computed.test.ts index 2a96fe6..4537994 100644 --- a/src/lwc/signals/__tests__/computed.test.ts +++ b/src/lwc/signals/__tests__/computed.test.ts @@ -12,8 +12,8 @@ describe("computed values", () => { expect(computed.value).toBe(2); }); - test("are recomputed when the source is an object and has changes", () => { - const signal = $signal({a: 0}); + test("are recomputed when the source is an object and has changes when the signal is being tracked", () => { + const signal = $signal({ a: 0 }, { track: true }); const computed = $computed(() => signal.value.a * 2); expect(computed.value).toBe(0); @@ -22,8 +22,18 @@ describe("computed values", () => { expect(computed.value).toBe(2); }); - test("are recomputed when the source is an array with gets a push", () => { - const signal = $signal([0]); + test("are not recomputed when the source is an object and has changes when the signal is not being tracked", () => { + const signal = $signal({ a: 0 }); + const computed = $computed(() => signal.value.a * 2); + expect(computed.value).toBe(0); + + signal.value.a = 1; + + expect(computed.value).toBe(0); + }); + + test("are recomputed when the source is an array with gets a push when the signal is tracked", () => { + const signal = $signal([0], { track: true }); const computed = $computed(() => signal.value.length); expect(computed.value).toBe(1); @@ -32,6 +42,16 @@ describe("computed values", () => { expect(computed.value).toBe(2); }); + test("are not recomputed when the source is an array with gets a push when the signal is not tracked", () => { + const signal = $signal([0]); + const computed = $computed(() => signal.value.length); + expect(computed.value).toBe(1); + + signal.value.push(1); + + expect(computed.value).toBe(1); + }); + test("do not recompute when the same value is set in the source signal", () => { const signal = $signal(0); diff --git a/src/lwc/signals/__tests__/effect.test.ts b/src/lwc/signals/__tests__/effect.test.ts index 4d30203..6e14646 100644 --- a/src/lwc/signals/__tests__/effect.test.ts +++ b/src/lwc/signals/__tests__/effect.test.ts @@ -15,7 +15,21 @@ describe("effects", () => { expect(effectTracker).toBe(1); }); - test("react to updates in an object signal", () => { + test("react to updates in an object signal when tracking is on", () => { + const signal = $signal({ a: 0 }, { track: true }); + let effectTracker = 0; + + $effect(() => { + effectTracker = signal.value.a; + }); + + expect(effectTracker).toBe(0); + + signal.value.a = 1; + expect(effectTracker).toBe(1); + }); + + test("does not react to updates in an object signal when tracking is off", () => { const signal = $signal({ a: 0 }); let effectTracker = 0; @@ -26,10 +40,24 @@ describe("effects", () => { expect(effectTracker).toBe(0); signal.value.a = 1; + expect(effectTracker).toBe(0); + }); + + test("react to updates in an array signal that gets a push when tracking is on", () => { + const signal = $signal([0], { track: true }); + let effectTracker = 0; + + $effect(() => { + effectTracker = signal.value.length; + }); + expect(effectTracker).toBe(1); + + signal.value.push(1); + expect(effectTracker).toBe(2); }); - test("react to updates in an array signal that gets a push", () => { + test("does not react to updates in an array signal that gets a push when tracking is off", () => { const signal = $signal([0]); let effectTracker = 0; @@ -40,6 +68,6 @@ describe("effects", () => { expect(effectTracker).toBe(1); signal.value.push(1); - expect(effectTracker).toBe(2); + expect(effectTracker).toBe(1); }); }); diff --git a/src/lwc/signals/core.ts b/src/lwc/signals/core.ts index 6bcf5bc..267d3e7 100644 --- a/src/lwc/signals/core.ts +++ b/src/lwc/signals/core.ts @@ -84,8 +84,52 @@ type StorageFn = (value: T) => State & { [key: string]: unknown }; type SignalOptions = { storage: StorageFn; debounce?: number; + track?: boolean; }; +interface TrackableState { + get(): T; + set(value: T): void; +} + +class UntrackedState implements TrackableState { + private _value: T; + + constructor(value: T) { + this._value = value; + } + + get() { + return this._value; + } + + set(value: T) { + this._value = value; + } +} + +class TrackedState implements TrackableState { + private _value: T; + private _membrane: ObservableMembrane; + + constructor(value: T, onChangeCallback: VoidFunction) { + this._membrane = new ObservableMembrane({ + valueMutated() { + onChangeCallback(); + } + }); + this._value = this._membrane.getProxy(value); + } + + get() { + return this._value; + } + + set(value: T) { + this._value = this._membrane.getProxy(value); + } +} + /** * Creates a new signal with the provided value. A signal is a reactive * primitive that can be used to store and update values. Signals can be @@ -113,15 +157,14 @@ function $signal( value: T, options?: Partial> ): Signal & Omit>, "get" | "set"> { - const membrane = new ObservableMembrane({ - valueMutated() { - notifySubscribers(); - } - }); - const state = membrane.getProxy(value); + // Defaults to not tracking changes through the Observable Membrane. + // The Observable Membrane proxies the passed in object to track changes + // to objects and arrays, but this introduces a performance overhead. + const shouldTrack = options?.track ?? false; + const trackableState: TrackableState = shouldTrack ? new TrackedState(value, notifySubscribers) : new UntrackedState(value); const _storageOption: State = - options?.storage?.(state) ?? useInMemoryStorage(state); + options?.storage?.(trackableState.get()) ?? useInMemoryStorage(trackableState.get()); const subscribers: Set = new Set(); function getter() { @@ -136,7 +179,8 @@ function $signal( if (newValue === _storageOption.get()) { return; } - _storageOption.set(membrane.getProxy(newValue)); + trackableState.set(newValue); + _storageOption.set(trackableState.get()); notifySubscribers(); }