Skip to content

Commit

Permalink
Tracking behind a flag
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra committed Nov 20, 2024
1 parent 3ba7746 commit 2b7b1bb
Show file tree
Hide file tree
Showing 3 changed files with 107 additions and 15 deletions.
28 changes: 24 additions & 4 deletions src/lwc/signals/__tests__/computed.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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);

Expand All @@ -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);

Expand All @@ -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);

Expand Down
34 changes: 31 additions & 3 deletions src/lwc/signals/__tests__/effect.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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;

Expand All @@ -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;

Expand All @@ -40,6 +68,6 @@ describe("effects", () => {
expect(effectTracker).toBe(1);

signal.value.push(1);
expect(effectTracker).toBe(2);
expect(effectTracker).toBe(1);
});
});
60 changes: 52 additions & 8 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
Expand Up @@ -84,8 +84,52 @@ type StorageFn<T> = (value: T) => State<T> & { [key: string]: unknown };
type SignalOptions<T> = {
storage: StorageFn<T>;
debounce?: number;
track?: boolean;
};

interface TrackableState<T> {
get(): T;
set(value: T): void;
}

class UntrackedState<T> implements TrackableState<T> {
private _value: T;

constructor(value: T) {
this._value = value;
}

get() {
return this._value;
}

set(value: T) {
this._value = value;
}
}

class TrackedState<T> implements TrackableState<T> {
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
Expand Down Expand Up @@ -113,15 +157,14 @@ function $signal<T>(
value: T,
options?: Partial<SignalOptions<T>>
): Signal<T> & Omit<ReturnType<StorageFn<T>>, "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<T> = shouldTrack ? new TrackedState(value, notifySubscribers) : new UntrackedState(value);

const _storageOption: State<T> =
options?.storage?.(state) ?? useInMemoryStorage(state);
options?.storage?.(trackableState.get()) ?? useInMemoryStorage(trackableState.get());
const subscribers: Set<VoidFunction> = new Set();

function getter() {
Expand All @@ -136,7 +179,8 @@ function $signal<T>(
if (newValue === _storageOption.get()) {
return;
}
_storageOption.set(membrane.getProxy(newValue));
trackableState.set(newValue);
_storageOption.set(trackableState.get());
notifySubscribers();
}

Expand Down

0 comments on commit 2b7b1bb

Please sign in to comment.