atomirx 0.0.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +1666 -0
- package/coverage/base.css +224 -0
- package/coverage/block-navigation.js +87 -0
- package/coverage/clover.xml +1440 -0
- package/coverage/coverage-final.json +14 -0
- package/coverage/favicon.png +0 -0
- package/coverage/index.html +131 -0
- package/coverage/prettify.css +1 -0
- package/coverage/prettify.js +2 -0
- package/coverage/sort-arrow-sprite.png +0 -0
- package/coverage/sorter.js +210 -0
- package/coverage/src/core/atom.ts.html +889 -0
- package/coverage/src/core/batch.ts.html +223 -0
- package/coverage/src/core/define.ts.html +805 -0
- package/coverage/src/core/emitter.ts.html +919 -0
- package/coverage/src/core/equality.ts.html +631 -0
- package/coverage/src/core/hook.ts.html +460 -0
- package/coverage/src/core/index.html +281 -0
- package/coverage/src/core/isAtom.ts.html +100 -0
- package/coverage/src/core/isPromiseLike.ts.html +133 -0
- package/coverage/src/core/onCreateHook.ts.html +136 -0
- package/coverage/src/core/scheduleNotifyHook.ts.html +94 -0
- package/coverage/src/core/types.ts.html +523 -0
- package/coverage/src/core/withUse.ts.html +253 -0
- package/coverage/src/index.html +116 -0
- package/coverage/src/index.ts.html +106 -0
- package/dist/core/atom.d.ts +63 -0
- package/dist/core/atom.test.d.ts +1 -0
- package/dist/core/atomState.d.ts +104 -0
- package/dist/core/atomState.test.d.ts +1 -0
- package/dist/core/batch.d.ts +126 -0
- package/dist/core/batch.test.d.ts +1 -0
- package/dist/core/define.d.ts +173 -0
- package/dist/core/define.test.d.ts +1 -0
- package/dist/core/derived.d.ts +102 -0
- package/dist/core/derived.test.d.ts +1 -0
- package/dist/core/effect.d.ts +120 -0
- package/dist/core/effect.test.d.ts +1 -0
- package/dist/core/emitter.d.ts +237 -0
- package/dist/core/emitter.test.d.ts +1 -0
- package/dist/core/equality.d.ts +62 -0
- package/dist/core/equality.test.d.ts +1 -0
- package/dist/core/hook.d.ts +134 -0
- package/dist/core/hook.test.d.ts +1 -0
- package/dist/core/isAtom.d.ts +9 -0
- package/dist/core/isPromiseLike.d.ts +9 -0
- package/dist/core/isPromiseLike.test.d.ts +1 -0
- package/dist/core/onCreateHook.d.ts +79 -0
- package/dist/core/promiseCache.d.ts +134 -0
- package/dist/core/promiseCache.test.d.ts +1 -0
- package/dist/core/scheduleNotifyHook.d.ts +51 -0
- package/dist/core/select.d.ts +151 -0
- package/dist/core/selector.test.d.ts +1 -0
- package/dist/core/types.d.ts +279 -0
- package/dist/core/withUse.d.ts +38 -0
- package/dist/core/withUse.test.d.ts +1 -0
- package/dist/index-2ok7ilik.js +1217 -0
- package/dist/index-B_5SFzfl.cjs +1 -0
- package/dist/index.cjs +1 -0
- package/dist/index.d.ts +14 -0
- package/dist/index.js +20 -0
- package/dist/index.test.d.ts +1 -0
- package/dist/react/index.cjs +30 -0
- package/dist/react/index.d.ts +7 -0
- package/dist/react/index.js +823 -0
- package/dist/react/rx.d.ts +250 -0
- package/dist/react/rx.test.d.ts +1 -0
- package/dist/react/strictModeTest.d.ts +10 -0
- package/dist/react/useAction.d.ts +381 -0
- package/dist/react/useAction.test.d.ts +1 -0
- package/dist/react/useStable.d.ts +183 -0
- package/dist/react/useStable.test.d.ts +1 -0
- package/dist/react/useValue.d.ts +134 -0
- package/dist/react/useValue.test.d.ts +1 -0
- package/package.json +57 -0
- package/scripts/publish.js +198 -0
- package/src/core/atom.test.ts +369 -0
- package/src/core/atom.ts +189 -0
- package/src/core/atomState.test.ts +342 -0
- package/src/core/atomState.ts +256 -0
- package/src/core/batch.test.ts +257 -0
- package/src/core/batch.ts +172 -0
- package/src/core/define.test.ts +342 -0
- package/src/core/define.ts +243 -0
- package/src/core/derived.test.ts +381 -0
- package/src/core/derived.ts +339 -0
- package/src/core/effect.test.ts +196 -0
- package/src/core/effect.ts +184 -0
- package/src/core/emitter.test.ts +364 -0
- package/src/core/emitter.ts +392 -0
- package/src/core/equality.test.ts +392 -0
- package/src/core/equality.ts +182 -0
- package/src/core/hook.test.ts +227 -0
- package/src/core/hook.ts +177 -0
- package/src/core/isAtom.ts +27 -0
- package/src/core/isPromiseLike.test.ts +72 -0
- package/src/core/isPromiseLike.ts +16 -0
- package/src/core/onCreateHook.ts +92 -0
- package/src/core/promiseCache.test.ts +239 -0
- package/src/core/promiseCache.ts +279 -0
- package/src/core/scheduleNotifyHook.ts +53 -0
- package/src/core/select.ts +454 -0
- package/src/core/selector.test.ts +257 -0
- package/src/core/types.ts +311 -0
- package/src/core/withUse.test.ts +249 -0
- package/src/core/withUse.ts +56 -0
- package/src/index.test.ts +80 -0
- package/src/index.ts +51 -0
- package/src/react/index.ts +20 -0
- package/src/react/rx.test.tsx +416 -0
- package/src/react/rx.tsx +300 -0
- package/src/react/strictModeTest.tsx +71 -0
- package/src/react/useAction.test.ts +989 -0
- package/src/react/useAction.ts +605 -0
- package/src/react/useStable.test.ts +553 -0
- package/src/react/useStable.ts +288 -0
- package/src/react/useValue.test.ts +182 -0
- package/src/react/useValue.ts +261 -0
- package/tsconfig.json +9 -0
- package/v2.md +725 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,392 @@
|
|
|
1
|
+
import { describe, it, expect } from "vitest";
|
|
2
|
+
import {
|
|
3
|
+
strictEqual,
|
|
4
|
+
shallowEqual,
|
|
5
|
+
shallow2Equal,
|
|
6
|
+
shallow3Equal,
|
|
7
|
+
deepEqual,
|
|
8
|
+
resolveEquality,
|
|
9
|
+
equality,
|
|
10
|
+
createStableFn,
|
|
11
|
+
isStableFn,
|
|
12
|
+
tryStabilize,
|
|
13
|
+
} from "./equality";
|
|
14
|
+
|
|
15
|
+
describe("equality", () => {
|
|
16
|
+
describe("strictEqual", () => {
|
|
17
|
+
it("should return true for same reference", () => {
|
|
18
|
+
const obj = { a: 1 };
|
|
19
|
+
expect(strictEqual(obj, obj)).toBe(true);
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it("should return false for different references with same content", () => {
|
|
23
|
+
expect(strictEqual({ a: 1 }, { a: 1 })).toBe(false);
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
it("should return true for same primitives", () => {
|
|
27
|
+
expect(strictEqual(1, 1)).toBe(true);
|
|
28
|
+
expect(strictEqual("hello", "hello")).toBe(true);
|
|
29
|
+
expect(strictEqual(true, true)).toBe(true);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it("should handle NaN correctly (Object.is behavior)", () => {
|
|
33
|
+
expect(strictEqual(NaN, NaN)).toBe(true);
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
it("should distinguish +0 and -0", () => {
|
|
37
|
+
expect(strictEqual(0, -0)).toBe(false);
|
|
38
|
+
});
|
|
39
|
+
});
|
|
40
|
+
|
|
41
|
+
describe("shallowEqual", () => {
|
|
42
|
+
it("should return true for same reference", () => {
|
|
43
|
+
const obj = { a: 1 };
|
|
44
|
+
expect(shallowEqual(obj, obj)).toBe(true);
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
it("should return true for objects with same keys and values", () => {
|
|
48
|
+
expect(shallowEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true);
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
it("should return false for objects with different keys", () => {
|
|
52
|
+
expect(shallowEqual({ a: 1 }, { b: 1 })).toBe(false);
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
it("should return false for objects with different values", () => {
|
|
56
|
+
expect(shallowEqual({ a: 1 }, { a: 2 })).toBe(false);
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it("should return false for objects with different number of keys", () => {
|
|
60
|
+
expect(shallowEqual({ a: 1 }, { a: 1, b: 2 })).toBe(false);
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it("should return true for arrays with same elements", () => {
|
|
64
|
+
expect(shallowEqual([1, 2, 3], [1, 2, 3])).toBe(true);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it("should return false for arrays with different elements", () => {
|
|
68
|
+
expect(shallowEqual([1, 2, 3], [1, 2, 4])).toBe(false);
|
|
69
|
+
});
|
|
70
|
+
|
|
71
|
+
it("should return false for arrays with different lengths", () => {
|
|
72
|
+
expect(shallowEqual([1, 2], [1, 2, 3])).toBe(false);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
it("should return false for nested objects with different references", () => {
|
|
76
|
+
expect(shallowEqual({ nested: { a: 1 } }, { nested: { a: 1 } })).toBe(
|
|
77
|
+
false
|
|
78
|
+
);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it("should return true for nested objects with same reference", () => {
|
|
82
|
+
const nested = { a: 1 };
|
|
83
|
+
expect(shallowEqual({ nested }, { nested })).toBe(true);
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
it("should return false when comparing object to null", () => {
|
|
87
|
+
expect(shallowEqual({ a: 1 }, null as any)).toBe(false);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
it("should return false when comparing object to primitive", () => {
|
|
91
|
+
expect(shallowEqual({ a: 1 }, 1 as any)).toBe(false);
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it("should support custom item comparator", () => {
|
|
95
|
+
const customEqual = (a: unknown, b: unknown) =>
|
|
96
|
+
JSON.stringify(a) === JSON.stringify(b);
|
|
97
|
+
expect(
|
|
98
|
+
shallowEqual({ nested: { a: 1 } }, { nested: { a: 1 } }, customEqual)
|
|
99
|
+
).toBe(true);
|
|
100
|
+
});
|
|
101
|
+
});
|
|
102
|
+
|
|
103
|
+
describe("shallow2Equal", () => {
|
|
104
|
+
it("should compare 2 levels deep", () => {
|
|
105
|
+
expect(
|
|
106
|
+
shallow2Equal({ nested: { a: 1 } }, { nested: { a: 1 } })
|
|
107
|
+
).toBe(true);
|
|
108
|
+
});
|
|
109
|
+
|
|
110
|
+
it("should return false for 3 levels deep difference", () => {
|
|
111
|
+
expect(
|
|
112
|
+
shallow2Equal(
|
|
113
|
+
{ nested: { deep: { a: 1 } } },
|
|
114
|
+
{ nested: { deep: { a: 1 } } }
|
|
115
|
+
)
|
|
116
|
+
).toBe(false);
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
it("should work with arrays of objects", () => {
|
|
120
|
+
expect(shallow2Equal([{ id: 1 }], [{ id: 1 }])).toBe(true);
|
|
121
|
+
});
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
describe("shallow3Equal", () => {
|
|
125
|
+
it("should compare 3 levels deep", () => {
|
|
126
|
+
expect(
|
|
127
|
+
shallow3Equal(
|
|
128
|
+
{ nested: { deep: { a: 1 } } },
|
|
129
|
+
{ nested: { deep: { a: 1 } } }
|
|
130
|
+
)
|
|
131
|
+
).toBe(true);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
it("should return false for 4 levels deep difference", () => {
|
|
135
|
+
expect(
|
|
136
|
+
shallow3Equal(
|
|
137
|
+
{ l1: { l2: { l3: { a: 1 } } } },
|
|
138
|
+
{ l1: { l2: { l3: { a: 1 } } } }
|
|
139
|
+
)
|
|
140
|
+
).toBe(false);
|
|
141
|
+
});
|
|
142
|
+
});
|
|
143
|
+
|
|
144
|
+
describe("deepEqual", () => {
|
|
145
|
+
it("should compare deeply nested objects", () => {
|
|
146
|
+
expect(
|
|
147
|
+
deepEqual(
|
|
148
|
+
{ a: { b: { c: { d: 1 } } } },
|
|
149
|
+
{ a: { b: { c: { d: 1 } } } }
|
|
150
|
+
)
|
|
151
|
+
).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it("should return false for deeply nested differences", () => {
|
|
155
|
+
expect(
|
|
156
|
+
deepEqual(
|
|
157
|
+
{ a: { b: { c: { d: 1 } } } },
|
|
158
|
+
{ a: { b: { c: { d: 2 } } } }
|
|
159
|
+
)
|
|
160
|
+
).toBe(false);
|
|
161
|
+
});
|
|
162
|
+
|
|
163
|
+
it("should handle arrays", () => {
|
|
164
|
+
expect(deepEqual([1, [2, [3]]], [1, [2, [3]]])).toBe(true);
|
|
165
|
+
});
|
|
166
|
+
|
|
167
|
+
it("should handle Date objects", () => {
|
|
168
|
+
const date = new Date("2024-01-01");
|
|
169
|
+
expect(deepEqual(date, new Date("2024-01-01"))).toBe(true);
|
|
170
|
+
});
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
describe("resolveEquality", () => {
|
|
174
|
+
it("should return strictEqual for undefined", () => {
|
|
175
|
+
expect(resolveEquality(undefined)).toBe(strictEqual);
|
|
176
|
+
});
|
|
177
|
+
|
|
178
|
+
it("should return strictEqual for 'strict'", () => {
|
|
179
|
+
expect(resolveEquality("strict")).toBe(strictEqual);
|
|
180
|
+
});
|
|
181
|
+
|
|
182
|
+
it("should return shallowEqual for 'shallow'", () => {
|
|
183
|
+
expect(resolveEquality("shallow")).toBe(shallowEqual);
|
|
184
|
+
});
|
|
185
|
+
|
|
186
|
+
it("should return shallow2Equal for 'shallow2'", () => {
|
|
187
|
+
expect(resolveEquality("shallow2")).toBe(shallow2Equal);
|
|
188
|
+
});
|
|
189
|
+
|
|
190
|
+
it("should return shallow3Equal for 'shallow3'", () => {
|
|
191
|
+
expect(resolveEquality("shallow3")).toBe(shallow3Equal);
|
|
192
|
+
});
|
|
193
|
+
|
|
194
|
+
it("should return deepEqual for 'deep'", () => {
|
|
195
|
+
expect(resolveEquality("deep")).toBe(deepEqual);
|
|
196
|
+
});
|
|
197
|
+
|
|
198
|
+
it("should return custom function as-is", () => {
|
|
199
|
+
const customFn = (a: number, b: number) => a === b;
|
|
200
|
+
expect(resolveEquality(customFn)).toBe(customFn);
|
|
201
|
+
});
|
|
202
|
+
});
|
|
203
|
+
|
|
204
|
+
describe("equality helper", () => {
|
|
205
|
+
it("should be an alias for resolveEquality with shorthand", () => {
|
|
206
|
+
expect(equality("strict")).toBe(strictEqual);
|
|
207
|
+
expect(equality("shallow")).toBe(shallowEqual);
|
|
208
|
+
expect(equality("deep")).toBe(deepEqual);
|
|
209
|
+
});
|
|
210
|
+
});
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
describe("StableFn", () => {
|
|
214
|
+
describe("createStableFn", () => {
|
|
215
|
+
it("should create a callable wrapper", () => {
|
|
216
|
+
const fn = (x: number) => x * 2;
|
|
217
|
+
const stable = createStableFn(fn);
|
|
218
|
+
|
|
219
|
+
expect(stable(5)).toBe(10);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it("should preserve original function via getOriginal", () => {
|
|
223
|
+
const fn = (x: number) => x * 2;
|
|
224
|
+
const stable = createStableFn(fn);
|
|
225
|
+
|
|
226
|
+
expect(stable.getOriginal()).toBe(fn);
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
it("should return current function via getCurrent", () => {
|
|
230
|
+
const fn = (x: number) => x * 2;
|
|
231
|
+
const stable = createStableFn(fn);
|
|
232
|
+
|
|
233
|
+
expect(stable.getCurrent()).toBe(fn);
|
|
234
|
+
});
|
|
235
|
+
|
|
236
|
+
it("should allow updating current function via setCurrent", () => {
|
|
237
|
+
const fn1 = (x: number) => x * 2;
|
|
238
|
+
const fn2 = (x: number) => x * 3;
|
|
239
|
+
const stable = createStableFn(fn1);
|
|
240
|
+
|
|
241
|
+
stable.setCurrent(fn2);
|
|
242
|
+
|
|
243
|
+
expect(stable(5)).toBe(15);
|
|
244
|
+
expect(stable.getCurrent()).toBe(fn2);
|
|
245
|
+
expect(stable.getOriginal()).toBe(fn1);
|
|
246
|
+
});
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
describe("isStableFn", () => {
|
|
250
|
+
it("should return true for StableFn", () => {
|
|
251
|
+
const stable = createStableFn(() => 42);
|
|
252
|
+
expect(isStableFn(stable)).toBe(true);
|
|
253
|
+
});
|
|
254
|
+
|
|
255
|
+
it("should return false for regular function", () => {
|
|
256
|
+
expect(isStableFn(() => 42)).toBe(false);
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
it("should return false for non-function", () => {
|
|
260
|
+
expect(isStableFn({ a: 1 })).toBe(false);
|
|
261
|
+
expect(isStableFn(42)).toBe(false);
|
|
262
|
+
expect(isStableFn("string")).toBe(false);
|
|
263
|
+
});
|
|
264
|
+
|
|
265
|
+
it("should return false for function with partial StableFn interface", () => {
|
|
266
|
+
const partial = Object.assign(() => 42, { getOriginal: () => {} });
|
|
267
|
+
expect(isStableFn(partial)).toBe(false);
|
|
268
|
+
});
|
|
269
|
+
});
|
|
270
|
+
});
|
|
271
|
+
|
|
272
|
+
describe("tryStabilize", () => {
|
|
273
|
+
describe("first call (no previous value)", () => {
|
|
274
|
+
it("should return value as-is for non-function", () => {
|
|
275
|
+
const [result, wasStable] = tryStabilize(undefined, 42, strictEqual);
|
|
276
|
+
expect(result).toBe(42);
|
|
277
|
+
expect(wasStable).toBe(false);
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
it("should wrap function in StableFn", () => {
|
|
281
|
+
const fn = () => 42;
|
|
282
|
+
const [result, wasStable] = tryStabilize(undefined, fn, strictEqual);
|
|
283
|
+
|
|
284
|
+
expect(isStableFn(result)).toBe(true);
|
|
285
|
+
expect(result()).toBe(42);
|
|
286
|
+
expect(wasStable).toBe(false);
|
|
287
|
+
});
|
|
288
|
+
});
|
|
289
|
+
|
|
290
|
+
describe("subsequent calls with functions", () => {
|
|
291
|
+
it("should update existing StableFn and return stable", () => {
|
|
292
|
+
const fn1 = () => 1;
|
|
293
|
+
const fn2 = () => 2;
|
|
294
|
+
|
|
295
|
+
const [stable1] = tryStabilize(undefined, fn1, strictEqual);
|
|
296
|
+
const [stable2, wasStable] = tryStabilize(
|
|
297
|
+
{ value: stable1 },
|
|
298
|
+
fn2,
|
|
299
|
+
strictEqual
|
|
300
|
+
);
|
|
301
|
+
|
|
302
|
+
expect(stable2).toBe(stable1); // Same reference
|
|
303
|
+
expect(stable2()).toBe(2); // But calls new function
|
|
304
|
+
expect(wasStable).toBe(true);
|
|
305
|
+
});
|
|
306
|
+
|
|
307
|
+
it("should create new StableFn if previous was not StableFn", () => {
|
|
308
|
+
const fn = () => 42;
|
|
309
|
+
const [result, wasStable] = tryStabilize(
|
|
310
|
+
{ value: "not a stable fn" as any },
|
|
311
|
+
fn,
|
|
312
|
+
strictEqual
|
|
313
|
+
);
|
|
314
|
+
|
|
315
|
+
expect(isStableFn(result)).toBe(true);
|
|
316
|
+
expect(wasStable).toBe(false);
|
|
317
|
+
});
|
|
318
|
+
});
|
|
319
|
+
|
|
320
|
+
describe("Date handling", () => {
|
|
321
|
+
it("should return previous Date if timestamps match", () => {
|
|
322
|
+
const date1 = new Date("2024-01-01");
|
|
323
|
+
const date2 = new Date("2024-01-01");
|
|
324
|
+
|
|
325
|
+
const [result, wasStable] = tryStabilize(
|
|
326
|
+
{ value: date1 },
|
|
327
|
+
date2,
|
|
328
|
+
strictEqual
|
|
329
|
+
);
|
|
330
|
+
|
|
331
|
+
expect(result).toBe(date1);
|
|
332
|
+
expect(wasStable).toBe(true);
|
|
333
|
+
});
|
|
334
|
+
|
|
335
|
+
it("should return new Date if timestamps differ", () => {
|
|
336
|
+
const date1 = new Date("2024-01-01");
|
|
337
|
+
const date2 = new Date("2024-01-02");
|
|
338
|
+
|
|
339
|
+
const [result, wasStable] = tryStabilize(
|
|
340
|
+
{ value: date1 },
|
|
341
|
+
date2,
|
|
342
|
+
strictEqual
|
|
343
|
+
);
|
|
344
|
+
|
|
345
|
+
expect(result).toBe(date2);
|
|
346
|
+
expect(wasStable).toBe(false);
|
|
347
|
+
});
|
|
348
|
+
|
|
349
|
+
it("should return new Date if previous was not a Date", () => {
|
|
350
|
+
const date = new Date("2024-01-01");
|
|
351
|
+
|
|
352
|
+
const [result, wasStable] = tryStabilize(
|
|
353
|
+
{ value: "not a date" as any },
|
|
354
|
+
date,
|
|
355
|
+
strictEqual
|
|
356
|
+
);
|
|
357
|
+
|
|
358
|
+
expect(result).toBe(date);
|
|
359
|
+
expect(wasStable).toBe(false);
|
|
360
|
+
});
|
|
361
|
+
});
|
|
362
|
+
|
|
363
|
+
describe("equality-based stabilization", () => {
|
|
364
|
+
it("should return previous value if equal", () => {
|
|
365
|
+
const obj1 = { a: 1 };
|
|
366
|
+
const obj2 = { a: 1 };
|
|
367
|
+
|
|
368
|
+
const [result, wasStable] = tryStabilize(
|
|
369
|
+
{ value: obj1 },
|
|
370
|
+
obj2,
|
|
371
|
+
shallowEqual
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
expect(result).toBe(obj1);
|
|
375
|
+
expect(wasStable).toBe(true);
|
|
376
|
+
});
|
|
377
|
+
|
|
378
|
+
it("should return new value if not equal", () => {
|
|
379
|
+
const obj1 = { a: 1 };
|
|
380
|
+
const obj2 = { a: 2 };
|
|
381
|
+
|
|
382
|
+
const [result, wasStable] = tryStabilize(
|
|
383
|
+
{ value: obj1 },
|
|
384
|
+
obj2,
|
|
385
|
+
shallowEqual
|
|
386
|
+
);
|
|
387
|
+
|
|
388
|
+
expect(result).toBe(obj2);
|
|
389
|
+
expect(wasStable).toBe(false);
|
|
390
|
+
});
|
|
391
|
+
});
|
|
392
|
+
});
|
|
@@ -0,0 +1,182 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Equality utilities for comparing values.
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import type { AnyFunc, Equality, EqualityShorthand } from "./types";
|
|
6
|
+
import isEqual from "lodash/isEqual";
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Strict equality (Object.is).
|
|
10
|
+
*/
|
|
11
|
+
export function strictEqual<T>(a: T, b: T): boolean {
|
|
12
|
+
return Object.is(a, b);
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
/**
|
|
16
|
+
* Shallow equality for objects/arrays.
|
|
17
|
+
* Compares by reference for each top-level key/index.
|
|
18
|
+
*
|
|
19
|
+
* @param itemEqual - Optional comparator for each item/value (defaults to Object.is)
|
|
20
|
+
*/
|
|
21
|
+
export function shallowEqual<T>(
|
|
22
|
+
a: T,
|
|
23
|
+
b: T,
|
|
24
|
+
itemEqual: (a: unknown, b: unknown) => boolean = Object.is
|
|
25
|
+
): boolean {
|
|
26
|
+
if (Object.is(a, b)) return true;
|
|
27
|
+
if (typeof a !== "object" || a === null) return false;
|
|
28
|
+
if (typeof b !== "object" || b === null) return false;
|
|
29
|
+
|
|
30
|
+
const keysA = Object.keys(a);
|
|
31
|
+
const keysB = Object.keys(b);
|
|
32
|
+
|
|
33
|
+
if (keysA.length !== keysB.length) return false;
|
|
34
|
+
|
|
35
|
+
for (const key of keysA) {
|
|
36
|
+
if (!Object.prototype.hasOwnProperty.call(b, key)) return false;
|
|
37
|
+
if (!itemEqual((a as any)[key], (b as any)[key])) return false;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
return true;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* 2-level shallow equality.
|
|
45
|
+
* Compares keys/length, then shallow compares each item/value.
|
|
46
|
+
*
|
|
47
|
+
* @example
|
|
48
|
+
* [{ id: 1, data: obj }] vs [{ id: 1, data: obj }] // true (same obj ref)
|
|
49
|
+
*/
|
|
50
|
+
export function shallow2Equal<T>(a: T, b: T): boolean {
|
|
51
|
+
return shallowEqual(a, b, shallowEqual);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* 3-level shallow equality.
|
|
56
|
+
* Compares keys/length, then shallow2 compares each item/value.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* [{ id: 1, nested: { data: obj } }] vs [{ id: 1, nested: { data: obj } }] // true
|
|
60
|
+
*/
|
|
61
|
+
export function shallow3Equal<T>(a: T, b: T): boolean {
|
|
62
|
+
return shallowEqual(a, b, shallow2Equal);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Deep equality.
|
|
67
|
+
*/
|
|
68
|
+
export const deepEqual = isEqual;
|
|
69
|
+
|
|
70
|
+
/**
|
|
71
|
+
* Resolve equality strategy to a function.
|
|
72
|
+
*/
|
|
73
|
+
export function resolveEquality<T>(
|
|
74
|
+
e: Equality<T> | undefined
|
|
75
|
+
): (a: T, b: T) => boolean {
|
|
76
|
+
if (!e || e === "strict") return strictEqual;
|
|
77
|
+
if (e === "shallow") return shallowEqual;
|
|
78
|
+
if (e === "shallow2") return shallow2Equal;
|
|
79
|
+
if (e === "shallow3") return shallow3Equal;
|
|
80
|
+
if (e === "deep") return deepEqual;
|
|
81
|
+
return e;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
export function equality(shorthand: EqualityShorthand) {
|
|
85
|
+
return resolveEquality(shorthand);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// =============================================================================
|
|
89
|
+
// Value Stabilization
|
|
90
|
+
// =============================================================================
|
|
91
|
+
|
|
92
|
+
export type StableFn<TArgs extends any[], TResult> = ((...args: TArgs) => TResult) & {
|
|
93
|
+
getOriginal: () => (...args: TArgs) => TResult;
|
|
94
|
+
getCurrent: () => (...args: TArgs) => TResult;
|
|
95
|
+
setCurrent: (newFn: (...args: TArgs) => TResult) => void;
|
|
96
|
+
};
|
|
97
|
+
|
|
98
|
+
export function createStableFn<TArgs extends any[], TResult>(
|
|
99
|
+
fn: (...args: TArgs) => TResult
|
|
100
|
+
): StableFn<TArgs, TResult> {
|
|
101
|
+
const originalFn = fn;
|
|
102
|
+
let currentFn = fn;
|
|
103
|
+
return Object.assign(
|
|
104
|
+
(...args: TArgs) => {
|
|
105
|
+
return currentFn(...args);
|
|
106
|
+
},
|
|
107
|
+
{
|
|
108
|
+
getOriginal: () => originalFn,
|
|
109
|
+
getCurrent: () => currentFn,
|
|
110
|
+
setCurrent(newFn: (...args: TArgs) => TResult) {
|
|
111
|
+
currentFn = newFn;
|
|
112
|
+
},
|
|
113
|
+
}
|
|
114
|
+
);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/**
|
|
118
|
+
* Check if a value is a stable function wrapper.
|
|
119
|
+
*/
|
|
120
|
+
export function isStableFn<TArgs extends any[], TResult>(
|
|
121
|
+
value: unknown
|
|
122
|
+
): value is StableFn<TArgs, TResult> {
|
|
123
|
+
return (
|
|
124
|
+
typeof value === "function" &&
|
|
125
|
+
"getOriginal" in value &&
|
|
126
|
+
"getCurrent" in value &&
|
|
127
|
+
"setCurrent" in value
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
/**
|
|
132
|
+
* Stabilize a value with automatic function wrapper support.
|
|
133
|
+
*
|
|
134
|
+
* - Functions: Creates/updates stable wrapper (reference never changes)
|
|
135
|
+
* - Date objects: Compared by timestamp (uses deepEqual)
|
|
136
|
+
* - Other values: Returns previous if equal per equalityFn
|
|
137
|
+
*
|
|
138
|
+
* @param prev - Previous value container (or undefined for first call)
|
|
139
|
+
* @param next - New value
|
|
140
|
+
* @param equalityFn - Equality function for non-function/non-date values
|
|
141
|
+
* @returns Tuple of [stabilized value, wasStable]
|
|
142
|
+
*/
|
|
143
|
+
export function tryStabilize<T>(
|
|
144
|
+
prev: { value: T } | undefined,
|
|
145
|
+
next: T,
|
|
146
|
+
equalityFn: (a: T, b: T) => boolean
|
|
147
|
+
): [T, boolean] {
|
|
148
|
+
// First time - no previous value
|
|
149
|
+
if (!prev) {
|
|
150
|
+
if (typeof next === "function") {
|
|
151
|
+
return [createStableFn(next as AnyFunc) as T, false];
|
|
152
|
+
}
|
|
153
|
+
return [next, false];
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
// Handle functions with stable wrapper pattern
|
|
157
|
+
if (typeof next === "function") {
|
|
158
|
+
if (isStableFn(prev.value)) {
|
|
159
|
+
// Update existing stable wrapper with new function
|
|
160
|
+
prev.value.setCurrent(next as AnyFunc);
|
|
161
|
+
return [prev.value as T, true];
|
|
162
|
+
}
|
|
163
|
+
// Previous wasn't a stable fn, create new wrapper
|
|
164
|
+
return [createStableFn(next as AnyFunc) as T, false];
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (next && next instanceof Date) {
|
|
168
|
+
if (prev.value && prev.value instanceof Date) {
|
|
169
|
+
if (next.getTime() === prev.value.getTime()) {
|
|
170
|
+
return [prev.value, true];
|
|
171
|
+
}
|
|
172
|
+
}
|
|
173
|
+
return [next, false];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
// Non-functions: use equality comparison
|
|
177
|
+
if (equalityFn(prev.value, next)) {
|
|
178
|
+
return [prev.value, true];
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
return [next, false];
|
|
182
|
+
}
|