Skip to content

Commit

Permalink
fix: Fix issues with equality
Browse files Browse the repository at this point in the history
  • Loading branch information
cesarParra authored Nov 29, 2024
1 parent cd6b31a commit fef168f
Show file tree
Hide file tree
Showing 10 changed files with 288 additions and 96 deletions.
7 changes: 4 additions & 3 deletions force-app/lwc/signals/core.js
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useInMemoryStorage } from "./use";
import { debounce, deepEqual } from "./utils";
import { debounce } from "./utils/debounce";
import { isEqual } from "./utils/isEqual";
import { ObservableMembrane } from "./observable-membrane/observable-membrane";
const context = [];
function _getCurrentObserver() {
Expand Down Expand Up @@ -129,7 +130,7 @@ function $signal(value, options) {
return _storageOption.get();
}
function setter(newValue) {
if (deepEqual(newValue, _storageOption.get())) {
if (isEqual(newValue, _storageOption.get())) {
return;
}
trackableState.set(newValue);
Expand Down Expand Up @@ -191,7 +192,7 @@ function $resource(fn, source, options) {
let data = null;
if (_fetchWhen()) {
const derivedSource = derivedSourceFn();
if (!_isInitialLoad && deepEqual(derivedSource, _previousParams)) {
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
// No need to fetch the data again if the params haven't changed
return;
}
Expand Down
18 changes: 0 additions & 18 deletions force-app/lwc/signals/utils.js

This file was deleted.

9 changes: 9 additions & 0 deletions force-app/lwc/signals/utils/debounce.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
export function debounce(func, delay) {
let debounceTimer = null;
return (...args) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = window.setTimeout(() => func(...args), delay);
};
}
48 changes: 48 additions & 0 deletions force-app/lwc/signals/utils/isEqual.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,48 @@
function isPlainObject(value) {
return value?.constructor === Object;
}
export function isEqual(a, b) {
if (Object.is(a, b)) return true;
if (typeof a !== typeof b) return false;
if (Array.isArray(a) && Array.isArray(b)) return isSameArray(a, b);
if (a instanceof Date && b instanceof Date)
return a.getTime() === b.getTime();
if (a instanceof RegExp && b instanceof RegExp)
return a.toString() === b.toString();
if (isPlainObject(a) && isPlainObject(b)) return isSameObject(a, b);
if (a instanceof ArrayBuffer && b instanceof ArrayBuffer)
return dataViewsAreEqual(new DataView(a), new DataView(b));
if (a instanceof DataView && b instanceof DataView)
return dataViewsAreEqual(a, b);
if (isTypedArray(a) && isTypedArray(b)) {
if (a.byteLength !== b.byteLength) return false;
return isSameArray(a, b);
}
return false;
}
function isSameObject(a, b) {
// check if the objects have the same keys
const keys1 = Object.keys(a);
const keys2 = Object.keys(b);
if (!isEqual(keys1, keys2)) return false;
// check if the values of each key in the objects are equal
for (const key of keys1) {
if (!isEqual(a[key], b[key])) return false;
}
// the objects are deeply equal
return true;
}
function isSameArray(a, b) {
if (a.length !== b.length) return false;
return a.every((element, index) => isEqual(element, b[index]));
}
function dataViewsAreEqual(a, b) {
if (a.byteLength !== b.byteLength) return false;
for (let offset = 0; offset < a.byteLength; offset++) {
if (a.getUint8(offset) !== b.getUint8(offset)) return false;
}
return true;
}
function isTypedArray(value) {
return ArrayBuffer.isView(value) && !(value instanceof DataView);
}
141 changes: 141 additions & 0 deletions src/lwc/signals/__tests__/isEqual.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,141 @@
import { isEqual } from "../utils/isEqual";

describe("isEqual", () => {
test("correctly compares against undefined", () => {
expect(isEqual(undefined, undefined)).toBe(true);
expect(isEqual(undefined, null)).toBe(false);
expect(isEqual(undefined, 1)).toBe(false);
expect(isEqual(undefined, "foo")).toBe(false);
expect(isEqual(undefined, {})).toBe(false);
expect(isEqual(undefined, [])).toBe(false);
});

test("correct compares against nulls", () => {
expect(isEqual(null, null)).toBe(true);
expect(isEqual(null, 1)).toBe(false);
expect(isEqual(null, "foo")).toBe(false);
expect(isEqual(null, {})).toBe(false);
expect(isEqual(null, [])).toBe(false);
});

test("compares simple values", () => {
expect(isEqual(1, 1)).toBe(true);
expect(isEqual(1, 2)).toBe(false);
expect(isEqual("foo", "foo")).toBe(true);
expect(isEqual("foo", "bar")).toBe(false);
});

test("compares objects", () => {
expect(isEqual({}, {})).toBe(true);
expect(isEqual({ foo: "bar" }, { foo: "bar" })).toBe(true);
expect(isEqual({ foo: "bar" }, { foo: "baz" })).toBe(false);
expect(isEqual({ foo: "bar" }, { bar: "baz" })).toBe(false);
expect(isEqual({ foo: "bar" }, { foo: "bar", bar: "baz" })).toBe(false);
});

test("deep compares objects", () => {
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "baz" } })).toBe(true);
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "qux" } })).toBe(false);
expect(isEqual({ foo: { bar: "baz" } }, { foo: { baz: "qux" } })).toBe(false);
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "baz", baz: "qux" } })).toBe(false);
});

test("compares arrays", () => {
expect(isEqual([], [])).toBe(true);
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false);
expect(isEqual([1, 2, 3], [1, 2])).toBe(false);
expect(isEqual([1, 2, 3], [1, 2, 3, 4])).toBe(false);
});

test("compares maps", () => {
const map1 = new Map();
map1.set("foo", "bar");
const map2 = new Map();

expect(isEqual(map1, map2)).toBe(false);
});

test("compares dates", () => {
expect(isEqual(new Date(1), new Date(1))).toBe(true);
expect(isEqual(new Date(1), new Date(2))).toBe(false);
});

test("compares nested arrays", () => {
expect(isEqual([[1], [2], [3]], [[1], [2], [3]])).toBe(true);
expect(isEqual([[1], [2], [3]], [[1], [2], [4]])).toBe(false);
});

test("compares nested objects and arrays", () => {
expect(isEqual({ a: { b: [1] } }, { a: { b: [1] } })).toBe(true);
expect(isEqual({ a: { b: [1] } }, { a: { b: [2] } })).toBe(false);
});

const testFunction = () => { return 1 };
test("functions", () => {
expect(isEqual(() => { return 1 }, () => { return 2 })).toBe(false);
expect(isEqual(testFunction, testFunction)).toBe(true);
});

test("objects with functions", () => {
expect(isEqual({ a: () => 1 }, { a: () => 1 })).toBe(false);
expect(isEqual({ a: testFunction }, { a: testFunction })).toBe(true);
});

test("regExp", () => {
expect(isEqual(/a(.*)/, /a(.*)/)).toBe(true);
expect(isEqual(/a/, /b.*/)).toBe(false);
});

test("deepEquals with Error objects", () => {
const error1 = new Error("test error");
const error2 = new Error("test error");
expect(isEqual(error1, error1)).toBe(true);
expect(isEqual(error1, error2)).toBe(false);
});

test("array buffers", () => {
const buffer1 = new ArrayBuffer(2);
const buffer1View = new Uint8Array(buffer1);
buffer1View.set([42, 43]);

const buffer2 = new ArrayBuffer(2);
const buffer2View = new Uint8Array(buffer2);
buffer2View.set([42, 43]);

const buffer3 = new ArrayBuffer(2);
const buffer3View = new Uint8Array(buffer3);
buffer3View.set([42, 44]);

const buffer4 = new ArrayBuffer(3);

expect(isEqual(buffer1, buffer2)).toBe(true);
expect(isEqual(buffer1, buffer3)).toBe(false);
expect(isEqual(buffer1, buffer4)).toBe(false);
});

test("typed arrays", () => {
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true);
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2]))).toBe(false);
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false);
});

test("data views", () => {
const buffer1 = new ArrayBuffer(2);
const buffer2 = new ArrayBuffer(2);
const buffer3 = new ArrayBuffer(3);

const view1 = new DataView(buffer1);
const view2 = new DataView(buffer2);
const view3 = new DataView(buffer3);

view1.setUint8(0, 42);
view1.setUint8(1, 43);

view2.setUint8(0, 42);
view2.setUint8(1, 43);

expect(isEqual(view1, view2)).toBe(true);
expect(isEqual(view1, view3)).toBe(false);
});
});
50 changes: 0 additions & 50 deletions src/lwc/signals/__tests__/utils.test.ts

This file was deleted.

7 changes: 4 additions & 3 deletions src/lwc/signals/core.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { useInMemoryStorage, State } from "./use";
import { debounce, deepEqual } from "./utils";
import { debounce } from "./utils/debounce";
import { isEqual } from "./utils/isEqual";
import { ObservableMembrane } from "./observable-membrane/observable-membrane";

type ReadOnlySignal<T> = {
Expand Down Expand Up @@ -180,7 +181,7 @@ function $signal<T>(
}

function setter(newValue: T) {
if (deepEqual(newValue, _storageOption.get())) {
if (isEqual(newValue, _storageOption.get())) {
return;
}
trackableState.set(newValue);
Expand Down Expand Up @@ -347,7 +348,7 @@ function $resource<ReturnType, Params>(
let data: ReturnType | null = null;
if (_fetchWhen()) {
const derivedSource = derivedSourceFn();
if (!_isInitialLoad && deepEqual(derivedSource,_previousParams)) {
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
// No need to fetch the data again if the params haven't changed
return;
}
Expand Down
22 changes: 0 additions & 22 deletions src/lwc/signals/utils.ts

This file was deleted.

12 changes: 12 additions & 0 deletions src/lwc/signals/utils/debounce.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
export function debounce<F extends (...args: unknown[]) => unknown>(
func: F,
delay: number
): (...args: Parameters<F>) => void {
let debounceTimer: number | null = null;
return (...args: Parameters<F>) => {
if (debounceTimer) {
clearTimeout(debounceTimer);
}
debounceTimer = window.setTimeout(() => func(...args), delay);
};
}
Loading

0 comments on commit fef168f

Please sign in to comment.