Skip to content

Commit 550c3a3

Browse files
committed
equality check fixes
1 parent cd6b31a commit 550c3a3

File tree

10 files changed

+288
-96
lines changed

10 files changed

+288
-96
lines changed

force-app/lwc/signals/core.js

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useInMemoryStorage } from "./use";
2-
import { debounce, deepEqual } from "./utils";
2+
import { debounce } from "./utils/debounce";
3+
import { isEqual } from "./utils/isEqual";
34
import { ObservableMembrane } from "./observable-membrane/observable-membrane";
45
const context = [];
56
function _getCurrentObserver() {
@@ -129,7 +130,7 @@ function $signal(value, options) {
129130
return _storageOption.get();
130131
}
131132
function setter(newValue) {
132-
if (deepEqual(newValue, _storageOption.get())) {
133+
if (isEqual(newValue, _storageOption.get())) {
133134
return;
134135
}
135136
trackableState.set(newValue);
@@ -191,7 +192,7 @@ function $resource(fn, source, options) {
191192
let data = null;
192193
if (_fetchWhen()) {
193194
const derivedSource = derivedSourceFn();
194-
if (!_isInitialLoad && deepEqual(derivedSource, _previousParams)) {
195+
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
195196
// No need to fetch the data again if the params haven't changed
196197
return;
197198
}

force-app/lwc/signals/utils.js

Lines changed: 0 additions & 18 deletions
This file was deleted.
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
export function debounce(func, delay) {
2+
let debounceTimer = null;
3+
return (...args) => {
4+
if (debounceTimer) {
5+
clearTimeout(debounceTimer);
6+
}
7+
debounceTimer = window.setTimeout(() => func(...args), delay);
8+
};
9+
}
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
function isPlainObject(value) {
2+
return value?.constructor === Object;
3+
}
4+
export function isEqual(a, b) {
5+
if (Object.is(a, b)) return true;
6+
if (typeof a !== typeof b) return false;
7+
if (Array.isArray(a) && Array.isArray(b)) return isSameArray(a, b);
8+
if (a instanceof Date && b instanceof Date)
9+
return a.getTime() === b.getTime();
10+
if (a instanceof RegExp && b instanceof RegExp)
11+
return a.toString() === b.toString();
12+
if (isPlainObject(a) && isPlainObject(b)) return isSameObject(a, b);
13+
if (a instanceof ArrayBuffer && b instanceof ArrayBuffer)
14+
return dataViewsAreEqual(new DataView(a), new DataView(b));
15+
if (a instanceof DataView && b instanceof DataView)
16+
return dataViewsAreEqual(a, b);
17+
if (isTypedArray(a) && isTypedArray(b)) {
18+
if (a.byteLength !== b.byteLength) return false;
19+
return isSameArray(a, b);
20+
}
21+
return false;
22+
}
23+
function isSameObject(a, b) {
24+
// check if the objects have the same keys
25+
const keys1 = Object.keys(a);
26+
const keys2 = Object.keys(b);
27+
if (!isEqual(keys1, keys2)) return false;
28+
// check if the values of each key in the objects are equal
29+
for (const key of keys1) {
30+
if (!isEqual(a[key], b[key])) return false;
31+
}
32+
// the objects are deeply equal
33+
return true;
34+
}
35+
function isSameArray(a, b) {
36+
if (a.length !== b.length) return false;
37+
return a.every((element, index) => isEqual(element, b[index]));
38+
}
39+
function dataViewsAreEqual(a, b) {
40+
if (a.byteLength !== b.byteLength) return false;
41+
for (let offset = 0; offset < a.byteLength; offset++) {
42+
if (a.getUint8(offset) !== b.getUint8(offset)) return false;
43+
}
44+
return true;
45+
}
46+
function isTypedArray(value) {
47+
return ArrayBuffer.isView(value) && !(value instanceof DataView);
48+
}
Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
import { isEqual } from "../utils/isEqual";
2+
3+
describe("isEqual", () => {
4+
test("correctly compares against undefined", () => {
5+
expect(isEqual(undefined, undefined)).toBe(true);
6+
expect(isEqual(undefined, null)).toBe(false);
7+
expect(isEqual(undefined, 1)).toBe(false);
8+
expect(isEqual(undefined, "foo")).toBe(false);
9+
expect(isEqual(undefined, {})).toBe(false);
10+
expect(isEqual(undefined, [])).toBe(false);
11+
});
12+
13+
test("correct compares against nulls", () => {
14+
expect(isEqual(null, null)).toBe(true);
15+
expect(isEqual(null, 1)).toBe(false);
16+
expect(isEqual(null, "foo")).toBe(false);
17+
expect(isEqual(null, {})).toBe(false);
18+
expect(isEqual(null, [])).toBe(false);
19+
});
20+
21+
test("compares simple values", () => {
22+
expect(isEqual(1, 1)).toBe(true);
23+
expect(isEqual(1, 2)).toBe(false);
24+
expect(isEqual("foo", "foo")).toBe(true);
25+
expect(isEqual("foo", "bar")).toBe(false);
26+
});
27+
28+
test("compares objects", () => {
29+
expect(isEqual({}, {})).toBe(true);
30+
expect(isEqual({ foo: "bar" }, { foo: "bar" })).toBe(true);
31+
expect(isEqual({ foo: "bar" }, { foo: "baz" })).toBe(false);
32+
expect(isEqual({ foo: "bar" }, { bar: "baz" })).toBe(false);
33+
expect(isEqual({ foo: "bar" }, { foo: "bar", bar: "baz" })).toBe(false);
34+
});
35+
36+
test("deep compares objects", () => {
37+
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "baz" } })).toBe(true);
38+
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "qux" } })).toBe(false);
39+
expect(isEqual({ foo: { bar: "baz" } }, { foo: { baz: "qux" } })).toBe(false);
40+
expect(isEqual({ foo: { bar: "baz" } }, { foo: { bar: "baz", baz: "qux" } })).toBe(false);
41+
});
42+
43+
test("compares arrays", () => {
44+
expect(isEqual([], [])).toBe(true);
45+
expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true);
46+
expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false);
47+
expect(isEqual([1, 2, 3], [1, 2])).toBe(false);
48+
expect(isEqual([1, 2, 3], [1, 2, 3, 4])).toBe(false);
49+
});
50+
51+
test("compares maps", () => {
52+
const map1 = new Map();
53+
map1.set("foo", "bar");
54+
const map2 = new Map();
55+
56+
expect(isEqual(map1, map2)).toBe(false);
57+
});
58+
59+
test("compares dates", () => {
60+
expect(isEqual(new Date(1), new Date(1))).toBe(true);
61+
expect(isEqual(new Date(1), new Date(2))).toBe(false);
62+
});
63+
64+
test("compares nested arrays", () => {
65+
expect(isEqual([[1], [2], [3]], [[1], [2], [3]])).toBe(true);
66+
expect(isEqual([[1], [2], [3]], [[1], [2], [4]])).toBe(false);
67+
});
68+
69+
test("compares nested objects and arrays", () => {
70+
expect(isEqual({ a: { b: [1] } }, { a: { b: [1] } })).toBe(true);
71+
expect(isEqual({ a: { b: [1] } }, { a: { b: [2] } })).toBe(false);
72+
});
73+
74+
const testFunction = () => { return 1 };
75+
test("functions", () => {
76+
expect(isEqual(() => { return 1 }, () => { return 2 })).toBe(false);
77+
expect(isEqual(testFunction, testFunction)).toBe(true);
78+
});
79+
80+
test("objects with functions", () => {
81+
expect(isEqual({ a: () => 1 }, { a: () => 1 })).toBe(false);
82+
expect(isEqual({ a: testFunction }, { a: testFunction })).toBe(true);
83+
});
84+
85+
test("regExp", () => {
86+
expect(isEqual(/a(.*)/, /a(.*)/)).toBe(true);
87+
expect(isEqual(/a/, /b.*/)).toBe(false);
88+
});
89+
90+
test("deepEquals with Error objects", () => {
91+
const error1 = new Error("test error");
92+
const error2 = new Error("test error");
93+
expect(isEqual(error1, error1)).toBe(true);
94+
expect(isEqual(error1, error2)).toBe(false);
95+
});
96+
97+
test("array buffers", () => {
98+
const buffer1 = new ArrayBuffer(2);
99+
const buffer1View = new Uint8Array(buffer1);
100+
buffer1View.set([42, 43]);
101+
102+
const buffer2 = new ArrayBuffer(2);
103+
const buffer2View = new Uint8Array(buffer2);
104+
buffer2View.set([42, 43]);
105+
106+
const buffer3 = new ArrayBuffer(2);
107+
const buffer3View = new Uint8Array(buffer3);
108+
buffer3View.set([42, 44]);
109+
110+
const buffer4 = new ArrayBuffer(3);
111+
112+
expect(isEqual(buffer1, buffer2)).toBe(true);
113+
expect(isEqual(buffer1, buffer3)).toBe(false);
114+
expect(isEqual(buffer1, buffer4)).toBe(false);
115+
});
116+
117+
test("typed arrays", () => {
118+
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 3]))).toBe(true);
119+
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2]))).toBe(false);
120+
expect(isEqual(new Uint8Array([1, 2, 3]), new Uint8Array([1, 2, 4]))).toBe(false);
121+
});
122+
123+
test("data views", () => {
124+
const buffer1 = new ArrayBuffer(2);
125+
const buffer2 = new ArrayBuffer(2);
126+
const buffer3 = new ArrayBuffer(3);
127+
128+
const view1 = new DataView(buffer1);
129+
const view2 = new DataView(buffer2);
130+
const view3 = new DataView(buffer3);
131+
132+
view1.setUint8(0, 42);
133+
view1.setUint8(1, 43);
134+
135+
view2.setUint8(0, 42);
136+
view2.setUint8(1, 43);
137+
138+
expect(isEqual(view1, view2)).toBe(true);
139+
expect(isEqual(view1, view3)).toBe(false);
140+
});
141+
});

src/lwc/signals/__tests__/utils.test.ts

Lines changed: 0 additions & 50 deletions
This file was deleted.

src/lwc/signals/core.ts

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
import { useInMemoryStorage, State } from "./use";
2-
import { debounce, deepEqual } from "./utils";
2+
import { debounce } from "./utils/debounce";
3+
import { isEqual } from "./utils/isEqual";
34
import { ObservableMembrane } from "./observable-membrane/observable-membrane";
45

56
type ReadOnlySignal<T> = {
@@ -180,7 +181,7 @@ function $signal<T>(
180181
}
181182

182183
function setter(newValue: T) {
183-
if (deepEqual(newValue, _storageOption.get())) {
184+
if (isEqual(newValue, _storageOption.get())) {
184185
return;
185186
}
186187
trackableState.set(newValue);
@@ -347,7 +348,7 @@ function $resource<ReturnType, Params>(
347348
let data: ReturnType | null = null;
348349
if (_fetchWhen()) {
349350
const derivedSource = derivedSourceFn();
350-
if (!_isInitialLoad && deepEqual(derivedSource,_previousParams)) {
351+
if (!_isInitialLoad && isEqual(derivedSource, _previousParams)) {
351352
// No need to fetch the data again if the params haven't changed
352353
return;
353354
}

src/lwc/signals/utils.ts

Lines changed: 0 additions & 22 deletions
This file was deleted.

src/lwc/signals/utils/debounce.ts

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,12 @@
1+
export function debounce<F extends (...args: unknown[]) => unknown>(
2+
func: F,
3+
delay: number
4+
): (...args: Parameters<F>) => void {
5+
let debounceTimer: number | null = null;
6+
return (...args: Parameters<F>) => {
7+
if (debounceTimer) {
8+
clearTimeout(debounceTimer);
9+
}
10+
debounceTimer = window.setTimeout(() => func(...args), delay);
11+
};
12+
}

0 commit comments

Comments
 (0)