@zeix/cause-effect 0.17.2 → 0.18.0

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.
Files changed (94) hide show
  1. package/.ai-context.md +163 -226
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/.zed/settings.json +3 -0
  5. package/ARCHITECTURE.md +274 -0
  6. package/CLAUDE.md +197 -202
  7. package/COLLECTION_REFACTORING.md +161 -0
  8. package/GUIDE.md +298 -0
  9. package/README.md +241 -220
  10. package/REQUIREMENTS.md +100 -0
  11. package/bench/reactivity.bench.ts +577 -0
  12. package/index.dev.js +1326 -1174
  13. package/index.js +1 -1
  14. package/index.ts +58 -85
  15. package/package.json +9 -6
  16. package/src/errors.ts +118 -70
  17. package/src/graph.ts +601 -0
  18. package/src/nodes/collection.ts +474 -0
  19. package/src/nodes/effect.ts +149 -0
  20. package/src/nodes/list.ts +588 -0
  21. package/src/nodes/memo.ts +120 -0
  22. package/src/nodes/sensor.ts +139 -0
  23. package/src/nodes/state.ts +135 -0
  24. package/src/nodes/store.ts +383 -0
  25. package/src/nodes/task.ts +146 -0
  26. package/src/signal.ts +112 -64
  27. package/src/util.ts +26 -57
  28. package/test/batch.test.ts +96 -69
  29. package/test/benchmark.test.ts +473 -485
  30. package/test/collection.test.ts +455 -955
  31. package/test/effect.test.ts +293 -696
  32. package/test/list.test.ts +332 -857
  33. package/test/memo.test.ts +380 -0
  34. package/test/regression.test.ts +156 -0
  35. package/test/scope.test.ts +191 -0
  36. package/test/sensor.test.ts +454 -0
  37. package/test/signal.test.ts +220 -213
  38. package/test/state.test.ts +217 -271
  39. package/test/store.test.ts +346 -898
  40. package/test/task.test.ts +395 -0
  41. package/test/untrack.test.ts +167 -0
  42. package/test/util/dependency-graph.ts +2 -2
  43. package/tsconfig.build.json +11 -0
  44. package/tsconfig.json +5 -7
  45. package/types/index.d.ts +13 -15
  46. package/types/src/errors.d.ts +73 -19
  47. package/types/src/graph.d.ts +208 -0
  48. package/types/src/nodes/collection.d.ts +64 -0
  49. package/types/src/nodes/effect.d.ts +48 -0
  50. package/types/src/nodes/list.d.ts +65 -0
  51. package/types/src/nodes/memo.d.ts +57 -0
  52. package/types/src/nodes/sensor.d.ts +75 -0
  53. package/types/src/nodes/state.d.ts +78 -0
  54. package/types/src/nodes/store.d.ts +51 -0
  55. package/types/src/nodes/task.d.ts +73 -0
  56. package/types/src/signal.d.ts +43 -28
  57. package/types/src/util.d.ts +9 -16
  58. package/archive/benchmark.ts +0 -688
  59. package/archive/collection.ts +0 -310
  60. package/archive/computed.ts +0 -198
  61. package/archive/list.ts +0 -544
  62. package/archive/memo.ts +0 -140
  63. package/archive/state.ts +0 -90
  64. package/archive/store.ts +0 -357
  65. package/archive/task.ts +0 -191
  66. package/src/classes/collection.ts +0 -298
  67. package/src/classes/composite.ts +0 -171
  68. package/src/classes/computed.ts +0 -392
  69. package/src/classes/list.ts +0 -310
  70. package/src/classes/ref.ts +0 -96
  71. package/src/classes/state.ts +0 -131
  72. package/src/classes/store.ts +0 -227
  73. package/src/diff.ts +0 -138
  74. package/src/effect.ts +0 -96
  75. package/src/match.ts +0 -45
  76. package/src/resolve.ts +0 -49
  77. package/src/system.ts +0 -275
  78. package/test/computed.test.ts +0 -1126
  79. package/test/diff.test.ts +0 -955
  80. package/test/match.test.ts +0 -388
  81. package/test/ref.test.ts +0 -381
  82. package/test/resolve.test.ts +0 -154
  83. package/types/src/classes/collection.d.ts +0 -47
  84. package/types/src/classes/composite.d.ts +0 -15
  85. package/types/src/classes/computed.d.ts +0 -114
  86. package/types/src/classes/list.d.ts +0 -41
  87. package/types/src/classes/ref.d.ts +0 -48
  88. package/types/src/classes/state.d.ts +0 -61
  89. package/types/src/classes/store.d.ts +0 -51
  90. package/types/src/diff.d.ts +0 -28
  91. package/types/src/effect.d.ts +0 -15
  92. package/types/src/match.d.ts +0 -21
  93. package/types/src/resolve.d.ts +0 -29
  94. package/types/src/system.d.ts +0 -81
package/test/diff.test.ts DELETED
@@ -1,955 +0,0 @@
1
- import { describe, expect, test } from 'bun:test'
2
- import {
3
- CircularDependencyError,
4
- diff,
5
- isEqual,
6
- UNSET,
7
- type UnknownRecord,
8
- } from '../index.ts'
9
-
10
- describe('diff', () => {
11
- describe('basic object diffing', () => {
12
- test('should detect no changes for identical objects', () => {
13
- const obj1 = { a: 1, b: 'hello' }
14
- const obj2 = { a: 1, b: 'hello' }
15
- const result = diff(obj1, obj2)
16
-
17
- expect(result.changed).toBe(false)
18
- expect(Object.keys(result.add)).toHaveLength(0)
19
- expect(Object.keys(result.change)).toHaveLength(0)
20
- expect(Object.keys(result.remove)).toHaveLength(0)
21
- })
22
-
23
- test('should detect additions', () => {
24
- const obj1 = { a: 1 }
25
- const obj2 = { a: 1, b: 'new' }
26
- const result = diff<{ a: number; b?: string }>(obj1, obj2)
27
-
28
- expect(result.changed).toBe(true)
29
- expect(result.add).toEqual({ b: 'new' })
30
- expect(Object.keys(result.change)).toHaveLength(0)
31
- expect(Object.keys(result.remove)).toHaveLength(0)
32
- })
33
-
34
- test('should detect removals', () => {
35
- const obj1 = { a: 1, b: 'hello' }
36
- const obj2 = { a: 1 }
37
- const result = diff<{ a: number; b?: string }>(obj1, obj2)
38
-
39
- expect(result.changed).toBe(true)
40
- expect(Object.keys(result.add)).toHaveLength(0)
41
- expect(Object.keys(result.change)).toHaveLength(0)
42
- expect(result.remove).toEqual({ b: UNSET })
43
- })
44
-
45
- test('should detect changes', () => {
46
- const obj1 = { a: 1, b: 'hello' }
47
- const obj2 = { a: 2, b: 'hello' }
48
- const result = diff(obj1, obj2)
49
-
50
- expect(result.changed).toBe(true)
51
- expect(Object.keys(result.add)).toHaveLength(0)
52
- expect(result.change).toEqual({ a: 2 })
53
- expect(Object.keys(result.remove)).toHaveLength(0)
54
- })
55
-
56
- test('should detect multiple changes', () => {
57
- const obj1 = { a: 1, b: 'hello', c: true }
58
- const obj2 = { a: 2, d: 'new', c: true }
59
- const result = diff<{
60
- a: number
61
- b?: string
62
- c: boolean
63
- d?: string
64
- }>(obj1, obj2)
65
-
66
- expect(result.changed).toBe(true)
67
- expect(result.add).toEqual({ d: 'new' })
68
- expect(result.change).toEqual({ a: 2 })
69
- expect(result.remove).toEqual({ b: UNSET })
70
- })
71
- })
72
-
73
- describe('primitive value handling', () => {
74
- test('should handle string changes', () => {
75
- const obj1 = { text: 'hello' }
76
- const obj2 = { text: 'world' }
77
- const result = diff(obj1, obj2)
78
-
79
- expect(result.changed).toBe(true)
80
- expect(result.change).toEqual({ text: 'world' })
81
- })
82
-
83
- test('should handle number changes including special values', () => {
84
- const obj1 = { num: 42, nan: NaN, zero: -0 }
85
- const obj2 = { num: 43, nan: NaN, zero: +0 }
86
- const result = diff(obj1, obj2)
87
-
88
- expect(result.changed).toBe(true)
89
- expect(result.change).toEqual({ num: 43, zero: +0 })
90
- })
91
-
92
- test('should handle boolean changes', () => {
93
- const obj1 = { flag: true }
94
- const obj2 = { flag: false }
95
- const result = diff(obj1, obj2)
96
-
97
- expect(result.changed).toBe(true)
98
- expect(result.change).toEqual({ flag: false })
99
- })
100
- })
101
-
102
- describe('array handling', () => {
103
- test('should detect no changes in identical arrays', () => {
104
- const obj1 = { arr: [1, 2, 3] }
105
- const obj2 = { arr: [1, 2, 3] }
106
- const result = diff(obj1, obj2)
107
-
108
- expect(result.changed).toBe(false)
109
- })
110
-
111
- test('should detect changes in arrays', () => {
112
- const obj1 = { arr: [1, 2, 3] }
113
- const obj2 = { arr: [1, 2, 4] }
114
- const result = diff(obj1, obj2)
115
-
116
- expect(result.changed).toBe(true)
117
- expect(result.change).toEqual({ arr: [1, 2, 4] })
118
- })
119
-
120
- test('should detect length changes in arrays', () => {
121
- const obj1 = { arr: [1, 2, 3] }
122
- const obj2 = { arr: [1, 2] }
123
- const result = diff(obj1, obj2)
124
-
125
- expect(result.changed).toBe(true)
126
- expect(result.change).toEqual({ arr: [1, 2] })
127
- })
128
-
129
- test('should handle empty arrays', () => {
130
- const obj1 = { arr: [] as number[] }
131
- const obj2 = { arr: [1] }
132
- const result = diff(obj1, obj2)
133
-
134
- expect(result.changed).toBe(true)
135
- expect(result.change).toEqual({ arr: [1] })
136
- })
137
-
138
- test('should handle arrays with complex objects', () => {
139
- const obj1 = { arr: [{ id: 1, name: 'a' }] }
140
- const obj2 = { arr: [{ id: 1, name: 'b' }] }
141
- const result = diff(obj1, obj2)
142
-
143
- expect(result.changed).toBe(true)
144
- expect(result.change).toEqual({ arr: [{ id: 1, name: 'b' }] })
145
- })
146
-
147
- test('should handle nested arrays', () => {
148
- const obj1 = {
149
- matrix: [
150
- [1, 2],
151
- [3, 4],
152
- ],
153
- }
154
- const obj2 = {
155
- matrix: [
156
- [1, 2],
157
- [3, 5],
158
- ],
159
- }
160
- const result = diff(obj1, obj2)
161
-
162
- expect(result.changed).toBe(true)
163
- expect(result.change).toEqual({
164
- matrix: [
165
- [1, 2],
166
- [3, 5],
167
- ],
168
- })
169
- })
170
- })
171
-
172
- describe('nested object handling', () => {
173
- test('should detect no changes in nested objects', () => {
174
- const obj1 = {
175
- user: { id: 1, profile: { name: 'John', age: 30 } },
176
- }
177
- const obj2 = {
178
- user: { id: 1, profile: { name: 'John', age: 30 } },
179
- }
180
- const result = diff(obj1, obj2)
181
-
182
- expect(result.changed).toBe(false)
183
- })
184
-
185
- test('should detect changes in nested objects', () => {
186
- const obj1 = {
187
- user: { id: 1, profile: { name: 'John', age: 30 } },
188
- }
189
- const obj2 = {
190
- user: { id: 1, profile: { name: 'Jane', age: 30 } },
191
- }
192
- const result = diff(obj1, obj2)
193
-
194
- expect(result.changed).toBe(true)
195
- expect(result.change).toEqual({
196
- user: { id: 1, profile: { name: 'Jane', age: 30 } },
197
- })
198
- })
199
-
200
- test('should handle deeply nested structures', () => {
201
- const obj1 = {
202
- a: { b: { c: { d: { e: 'deep' } } } },
203
- }
204
- const obj2 = {
205
- a: { b: { c: { d: { e: 'deeper' } } } },
206
- }
207
- const result = diff(obj1, obj2)
208
-
209
- expect(result.changed).toBe(true)
210
- expect(result.change).toEqual({
211
- a: { b: { c: { d: { e: 'deeper' } } } },
212
- })
213
- })
214
- })
215
-
216
- describe('type change handling', () => {
217
- test('should handle changes from primitive to object', () => {
218
- const obj1 = { value: 'string' }
219
- const obj2 = { value: { type: 'object' } }
220
- const result = diff<{ value: string | { type: string } }>(
221
- obj1,
222
- obj2,
223
- )
224
-
225
- expect(result.changed).toBe(true)
226
- expect(result.change).toEqual({ value: { type: 'object' } })
227
- })
228
-
229
- test('should handle changes from array to object', () => {
230
- const obj1 = { data: [1, 2, 3] }
231
- const obj2 = { data: { 0: 1, 1: 2, 2: 3 } }
232
- const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
233
-
234
- expect(result.changed).toBe(true)
235
- expect(result.change).toEqual({ data: { 0: 1, 1: 2, 2: 3 } })
236
- })
237
-
238
- test('should handle changes from object to array', () => {
239
- const obj1 = { data: { a: 1, b: 2 } }
240
- const obj2 = { data: [1, 2] }
241
- const result = diff(obj1 as UnknownRecord, obj2 as UnknownRecord)
242
-
243
- expect(result.changed).toBe(true)
244
- expect(result.change).toEqual({ data: [1, 2] })
245
- })
246
- })
247
-
248
- describe('special object types', () => {
249
- test('should handle Date objects', () => {
250
- const date1 = new Date('2023-01-01')
251
- const date2 = new Date('2023-01-02')
252
- const obj1 = { timestamp: date1 }
253
- const obj2 = { timestamp: date2 }
254
- const result = diff(obj1, obj2)
255
-
256
- expect(result.changed).toBe(true)
257
- expect(result.change).toEqual({ timestamp: date2 })
258
- })
259
-
260
- test('should handle RegExp objects', () => {
261
- const regex1 = /hello/g
262
- const regex2 = /world/g
263
- const obj1 = { pattern: regex1 }
264
- const obj2 = { pattern: regex2 }
265
- const result = diff(obj1, obj2)
266
-
267
- expect(result.changed).toBe(true)
268
- expect(result.change).toEqual({ pattern: regex2 })
269
- })
270
-
271
- test('should handle identical special objects', () => {
272
- const date = new Date('2023-01-01')
273
- const obj1 = { timestamp: date }
274
- const obj2 = { timestamp: date }
275
- const result = diff(obj1, obj2)
276
-
277
- expect(result.changed).toBe(false)
278
- })
279
- })
280
-
281
- describe('edge cases and error handling', () => {
282
- test('should handle empty objects', () => {
283
- const result = diff({}, {})
284
- expect(result.changed).toBe(false)
285
- })
286
-
287
- test('should detect circular references and throw error', () => {
288
- const circular1: UnknownRecord = { a: 1 }
289
- circular1.self = circular1
290
-
291
- const circular2: UnknownRecord = { a: 1 }
292
- circular2.self = circular2
293
-
294
- expect(() => diff(circular1, circular2)).toThrow(
295
- CircularDependencyError,
296
- )
297
- })
298
-
299
- test('should handle objects with Symbol keys', () => {
300
- const sym = Symbol('test')
301
- const obj1 = {
302
- [sym]: 'value1',
303
- normal: 'prop',
304
- }
305
- const obj2 = {
306
- [sym]: 'value2',
307
- normal: 'prop',
308
- }
309
-
310
- // Since Object.keys() doesn't include symbols,
311
- // the diff should not detect the symbol property change
312
- const result = diff(obj1, obj2)
313
- expect(result.changed).toBe(false)
314
- })
315
-
316
- test('should handle objects with non-enumerable properties', () => {
317
- const obj1 = { a: 1 }
318
- const obj2 = { a: 1 }
319
-
320
- Object.defineProperty(obj1, 'hidden', {
321
- value: 'secret1',
322
- enumerable: false,
323
- })
324
-
325
- Object.defineProperty(obj2, 'hidden', {
326
- value: 'secret2',
327
- enumerable: false,
328
- })
329
-
330
- // Since Object.keys() doesn't include non-enumerable properties,
331
- // the diff should not detect the hidden property change
332
- const result = diff(obj1, obj2)
333
- expect(result.changed).toBe(false)
334
- })
335
- })
336
-
337
- describe('performance edge cases', () => {
338
- test('should handle large objects efficiently', () => {
339
- const createLargeObject = (size: number, seed: number = 0) => {
340
- const obj: Record<string, number> = {}
341
- for (let i = 0; i < size; i++) {
342
- obj[`prop${i}`] = i + seed
343
- }
344
- return obj
345
- }
346
-
347
- const obj1 = createLargeObject(1000)
348
- const obj2 = createLargeObject(1000, 1) // Same structure, different values
349
-
350
- const start = performance.now()
351
- const result = diff(obj1, obj2)
352
- const duration = performance.now() - start
353
-
354
- expect(result.changed).toBe(true)
355
- expect(Object.keys(result.change)).toHaveLength(1000)
356
- expect(duration).toBeLessThan(100) // Should complete within 100ms
357
- })
358
-
359
- test('should handle deeply nested structures without stack overflow', () => {
360
- // biome-ignore lint/suspicious/noExplicitAny: testing purposes
361
- const createDeepObject = (depth: number): any => {
362
- let obj: UnknownRecord = { value: 'leaf' }
363
- for (let i = 0; i < depth; i++) {
364
- obj = { [`level${i}`]: obj }
365
- }
366
- return obj
367
- }
368
-
369
- const obj1 = createDeepObject(100)
370
- const obj2 = createDeepObject(100)
371
- obj2.level99.level98.level97.value = 'changed'
372
-
373
- const result = diff(obj1, obj2)
374
- expect(result.changed).toBe(true)
375
- })
376
- })
377
-
378
- describe('optional keys handling', () => {
379
- type OptionalKeysType = {
380
- required: string
381
- optional?: number
382
- maybeUndefined?: string
383
- }
384
-
385
- test('should handle optional keys correctly', () => {
386
- const obj1: OptionalKeysType = {
387
- required: 'test',
388
- }
389
- const obj2: OptionalKeysType = {
390
- required: 'test',
391
- optional: 42,
392
- }
393
- const result = diff(obj1, obj2)
394
-
395
- expect(result.changed).toBe(true)
396
- expect(result.add).toEqual({ optional: 42 })
397
- })
398
-
399
- test('should handle undefined optional keys', () => {
400
- const obj1: OptionalKeysType = {
401
- required: 'test',
402
- maybeUndefined: 'defined',
403
- }
404
- const obj2: OptionalKeysType = {
405
- required: 'test',
406
- maybeUndefined: undefined,
407
- }
408
- const result = diff(obj1, obj2)
409
-
410
- expect(result.changed).toBe(true)
411
- expect(result.change).toEqual({ maybeUndefined: undefined })
412
- })
413
- })
414
-
415
- describe('array normalization to records', () => {
416
- test('should correctly normalize arrays to records for comparison', () => {
417
- const obj1 = { items: ['a', 'b', 'c'] }
418
- const obj2 = { items: ['a', 'x', 'c'] }
419
- const result = diff(obj1, obj2)
420
-
421
- expect(result.changed).toBe(true)
422
- expect(result.change).toEqual({ items: ['a', 'x', 'c'] })
423
- })
424
-
425
- test('should handle sparse arrays correctly', () => {
426
- const sparse1: string[] = []
427
- sparse1[0] = 'a'
428
- sparse1[2] = 'c'
429
-
430
- const sparse2: string[] = []
431
- sparse2[0] = 'a'
432
- sparse2[1] = 'b'
433
- sparse2[2] = 'c'
434
-
435
- const obj1 = { sparse: sparse1 }
436
- const obj2 = { sparse: sparse2 }
437
- const result = diff(obj1, obj2)
438
-
439
- expect(result.changed).toBe(true)
440
- expect(result.change).toEqual({ sparse: sparse2 })
441
- })
442
- })
443
-
444
- describe('isEqual function', () => {
445
- describe('primitives and fast paths', () => {
446
- test('should handle identical values', () => {
447
- expect(isEqual(1, 1)).toBe(true)
448
- expect(isEqual('hello', 'hello')).toBe(true)
449
- expect(isEqual(true, true)).toBe(true)
450
- expect(isEqual(null, null)).toBe(true)
451
- expect(isEqual(undefined, undefined)).toBe(true)
452
- })
453
-
454
- test('should handle different primitives', () => {
455
- expect(isEqual(1, 2)).toBe(false)
456
- expect(isEqual('hello', 'world')).toBe(false)
457
- expect(isEqual(true, false)).toBe(false)
458
- expect(isEqual(null, undefined)).toBe(false)
459
- })
460
-
461
- test('should handle special number values', () => {
462
- expect(isEqual(NaN, NaN)).toBe(true)
463
- expect(isEqual(-0, +0)).toBe(false)
464
- expect(isEqual(Infinity, Infinity)).toBe(true)
465
- expect(isEqual(-Infinity, Infinity)).toBe(false)
466
- })
467
-
468
- test('should handle type mismatches', () => {
469
- // @ts-expect-error deliberate type mismatch
470
- expect(isEqual(1, '1')).toBe(false)
471
- // @ts-expect-error deliberate type mismatch
472
- expect(isEqual(true, 1)).toBe(false)
473
- expect(isEqual(null, 0)).toBe(false)
474
- expect(isEqual(undefined, '')).toBe(false)
475
- })
476
-
477
- test('should handle same object reference', () => {
478
- const obj = { a: 1 }
479
- expect(isEqual(obj, obj)).toBe(true)
480
-
481
- const arr = [1, 2, 3]
482
- expect(isEqual(arr, arr)).toBe(true)
483
- })
484
- })
485
-
486
- describe('objects', () => {
487
- test('should compare objects with same content', () => {
488
- expect(isEqual({ a: 1, b: 2 }, { a: 1, b: 2 })).toBe(true)
489
- expect(isEqual({ a: 1, b: 2 }, { b: 2, a: 1 })).toBe(true)
490
- })
491
-
492
- test('should detect different object content', () => {
493
- expect(isEqual({ a: 1 }, { a: 2 })).toBe(false)
494
- expect(isEqual({ a: 1 }, { b: 1 })).toBe(false)
495
- expect(isEqual({ a: 1, b: 2 }, { a: 1 })).toBe(false)
496
- })
497
-
498
- test('should handle nested objects', () => {
499
- const obj1 = { user: { name: 'John', age: 30 } }
500
- const obj2 = { user: { name: 'John', age: 30 } }
501
- const obj3 = { user: { name: 'Jane', age: 30 } }
502
-
503
- expect(isEqual(obj1, obj2)).toBe(true)
504
- expect(isEqual(obj1, obj3)).toBe(false)
505
- })
506
-
507
- test('should handle empty objects', () => {
508
- expect(isEqual({}, {})).toBe(true)
509
- expect(isEqual({}, { a: 1 })).toBe(false)
510
- })
511
- })
512
-
513
- describe('arrays', () => {
514
- test('should compare arrays with same content', () => {
515
- expect(isEqual([1, 2, 3], [1, 2, 3])).toBe(true)
516
- expect(isEqual([], [])).toBe(true)
517
- })
518
-
519
- test('should detect different array content', () => {
520
- expect(isEqual([1, 2, 3], [1, 2, 4])).toBe(false)
521
- expect(isEqual([1, 2], [1, 2, 3])).toBe(false)
522
- expect(isEqual([1, 2, 3], [3, 2, 1])).toBe(false)
523
- })
524
-
525
- test('should handle nested arrays', () => {
526
- const arr1 = [
527
- [1, 2],
528
- [3, 4],
529
- ]
530
- const arr2 = [
531
- [1, 2],
532
- [3, 4],
533
- ]
534
- const arr3 = [
535
- [1, 2],
536
- [3, 5],
537
- ]
538
-
539
- expect(isEqual(arr1, arr2)).toBe(true)
540
- expect(isEqual(arr1, arr3)).toBe(false)
541
- })
542
-
543
- test('should handle arrays with objects', () => {
544
- const arr1 = [{ a: 1 }, { b: 2 }]
545
- const arr2 = [{ a: 1 }, { b: 2 }]
546
- const arr3 = [{ a: 2 }, { b: 2 }]
547
-
548
- expect(isEqual(arr1, arr2)).toBe(true)
549
- expect(isEqual(arr1, arr3)).toBe(false)
550
- })
551
- })
552
-
553
- describe('mixed types', () => {
554
- test('should handle array vs object', () => {
555
- expect(isEqual([1, 2], { 0: 1, 1: 2 })).toBe(false)
556
- expect(isEqual({ length: 2 }, [1, 2])).toBe(false)
557
- })
558
-
559
- test('should handle object vs primitive', () => {
560
- // @ts-expect-error deliberate type mismatch
561
- expect(isEqual({ a: 1 }, 'object')).toBe(false)
562
- // @ts-expect-error deliberate type mismatch
563
- expect(isEqual(42, { value: 42 })).toBe(false)
564
- })
565
-
566
- test('should handle complex mixed structures', () => {
567
- const obj1 = {
568
- data: [1, 2, { nested: true }],
569
- meta: { count: 3 },
570
- }
571
- const obj2 = {
572
- data: [1, 2, { nested: true }],
573
- meta: { count: 3 },
574
- }
575
- const obj3 = {
576
- data: [1, 2, { nested: false }],
577
- meta: { count: 3 },
578
- }
579
-
580
- expect(isEqual(obj1, obj2)).toBe(true)
581
- expect(isEqual(obj1, obj3)).toBe(false)
582
- })
583
- })
584
-
585
- describe('edge cases', () => {
586
- test('should handle circular references', () => {
587
- const circular1: UnknownRecord = { a: 1 }
588
- circular1.self = circular1
589
-
590
- const circular2: UnknownRecord = { a: 1 }
591
- circular2.self = circular2
592
-
593
- expect(() => isEqual(circular1, circular2)).toThrow(
594
- CircularDependencyError,
595
- )
596
- })
597
-
598
- test('should handle special objects', () => {
599
- const date1 = new Date('2023-01-01')
600
- const date2 = new Date('2023-01-01')
601
- const date3 = new Date('2023-01-02')
602
-
603
- // Different Date objects with same time should be false (reference equality for special objects)
604
- expect(isEqual(date1, date1)).toBe(true) // same reference
605
- expect(isEqual(date1, date2)).toBe(false) // different references
606
- expect(isEqual(date1, date3)).toBe(false)
607
- })
608
-
609
- test('should handle null and undefined edge cases', () => {
610
- expect(isEqual(null, null)).toBe(true)
611
- expect(isEqual(undefined, undefined)).toBe(true)
612
- expect(isEqual(null, undefined)).toBe(false)
613
- expect(isEqual({}, null)).toBe(false)
614
- expect(isEqual([], undefined)).toBe(false)
615
- })
616
- })
617
-
618
- describe('performance comparison', () => {
619
- test('should demonstrate isEqual vs Object.is difference', () => {
620
- // Objects with same content but different references
621
- const obj1 = {
622
- user: { name: 'John', age: 30 },
623
- items: [1, 2, 3],
624
- }
625
- const obj2 = {
626
- user: { name: 'John', age: 30 },
627
- items: [1, 2, 3],
628
- }
629
-
630
- // Object.is fails for content equality
631
- expect(Object.is(obj1, obj2)).toBe(false)
632
-
633
- // isEqual succeeds for content equality
634
- expect(isEqual(obj1, obj2)).toBe(true)
635
-
636
- // Both work for reference equality
637
- expect(Object.is(obj1, obj1)).toBe(true)
638
- expect(isEqual(obj1, obj1)).toBe(true)
639
-
640
- // Both work for primitive equality
641
- expect(Object.is(42, 42)).toBe(true)
642
- expect(isEqual(42, 42)).toBe(true)
643
- })
644
- })
645
- })
646
-
647
- describe('non-plain object type safety', () => {
648
- test('should handle Symbol objects without throwing TypeError', () => {
649
- const symbol = Symbol('test')
650
- const obj = { a: 1 }
651
-
652
- // These should not throw after we fix the bug
653
- // @ts-expect-error Testing runtime behavior with non-plain object types
654
- expect(() => diff(symbol, obj)).not.toThrow()
655
- // @ts-expect-error Testing runtime behavior with non-plain object types
656
- expect(() => diff(obj, symbol)).not.toThrow()
657
- // @ts-expect-error Testing runtime behavior with non-plain object types
658
- expect(() => isEqual(symbol, obj)).not.toThrow()
659
- })
660
-
661
- test('should report additions when diffing from Symbol to valid object', () => {
662
- const symbol = Symbol('test')
663
- const obj = { a: 1, b: 'hello' }
664
-
665
- // @ts-expect-error Testing runtime behavior with non-plain object types
666
- const result = diff(symbol, obj)
667
-
668
- expect(result.changed).toBe(true)
669
- expect(result.add).toEqual({ a: 1, b: 'hello' })
670
- expect(result.change).toEqual({})
671
- expect(result.remove).toEqual({})
672
- })
673
-
674
- test('should report removals when diffing from valid object to Symbol', () => {
675
- const obj = { a: 1, b: 'hello' }
676
- const symbol = Symbol('test')
677
-
678
- // @ts-expect-error Testing runtime behavior with non-plain object types
679
- const result = diff(obj, symbol)
680
-
681
- expect(result.changed).toBe(true)
682
- expect(result.add).toEqual({})
683
- expect(result.change).toEqual({})
684
- expect(result.remove).toEqual({ a: 1, b: 'hello' })
685
- })
686
-
687
- test('should handle Symbol to Symbol diff with no changes', () => {
688
- const symbol = Symbol('test')
689
-
690
- // @ts-expect-error Testing runtime behavior with non-plain object types
691
- const result = diff(symbol, symbol)
692
-
693
- expect(result.changed).toBe(false)
694
- expect(result.add).toEqual({})
695
- expect(result.change).toEqual({})
696
- expect(result.remove).toEqual({})
697
- })
698
-
699
- test('should handle different Symbols as changed', () => {
700
- const symbol1 = Symbol('test1')
701
- const symbol2 = Symbol('test2')
702
-
703
- // @ts-expect-error Testing runtime behavior with non-plain object types
704
- const result = diff(symbol1, symbol2)
705
-
706
- expect(result.changed).toBe(true)
707
- expect(result.add).toEqual({})
708
- expect(result.change).toEqual({})
709
- expect(result.remove).toEqual({})
710
- })
711
-
712
- test('should handle Date objects without throwing TypeError', () => {
713
- const date = new Date('2023-01-01')
714
- const obj = { a: 1 }
715
-
716
- // @ts-expect-error Testing runtime behavior with non-plain object types
717
- expect(() => diff(date, obj)).not.toThrow()
718
- // @ts-expect-error Testing runtime behavior with non-plain object types
719
- expect(() => diff(obj, date)).not.toThrow()
720
- // @ts-expect-error Testing runtime behavior with non-plain object types
721
- expect(() => isEqual(date, obj)).not.toThrow()
722
- })
723
-
724
- test('should report additions when diffing from Date to valid object', () => {
725
- const date = new Date('2023-01-01')
726
- const obj = { a: 1, b: 'hello' }
727
-
728
- // @ts-expect-error Testing runtime behavior with non-plain object types
729
- const result = diff(date, obj)
730
-
731
- expect(result.changed).toBe(true)
732
- expect(result.add).toEqual({ a: 1, b: 'hello' })
733
- expect(result.change).toEqual({})
734
- expect(result.remove).toEqual({})
735
- })
736
-
737
- test('should report removals when diffing from valid object to Date', () => {
738
- const obj = { a: 1, b: 'hello' }
739
- const date = new Date('2023-01-01')
740
-
741
- // @ts-expect-error Testing runtime behavior with non-plain object types
742
- const result = diff(obj, date)
743
-
744
- expect(result.changed).toBe(true)
745
- expect(result.add).toEqual({})
746
- expect(result.change).toEqual({})
747
- expect(result.remove).toEqual({ a: 1, b: 'hello' })
748
- })
749
-
750
- test('should handle Map objects without throwing TypeError', () => {
751
- const map = new Map([['key', 'value']])
752
- const obj = { a: 1 }
753
-
754
- // @ts-expect-error Testing runtime behavior with non-plain object types
755
- expect(() => diff(map, obj)).not.toThrow()
756
- // @ts-expect-error Testing runtime behavior with non-plain object types
757
- expect(() => diff(obj, map)).not.toThrow()
758
- // @ts-expect-error Testing runtime behavior with non-plain object types
759
- expect(() => isEqual(map, obj)).not.toThrow()
760
- })
761
-
762
- test('should report additions when diffing from Map to valid object', () => {
763
- const map = new Map([['key', 'value']])
764
- const obj = { x: 10, y: 20 }
765
-
766
- // @ts-expect-error Testing runtime behavior with non-plain object types
767
- const result = diff(map, obj)
768
-
769
- expect(result.changed).toBe(true)
770
- expect(result.add).toEqual({ x: 10, y: 20 })
771
- expect(result.change).toEqual({})
772
- expect(result.remove).toEqual({})
773
- })
774
-
775
- test('should handle Set objects without throwing TypeError', () => {
776
- const set = new Set([1, 2, 3])
777
- const obj = { a: 1 }
778
-
779
- // @ts-expect-error Testing runtime behavior with non-plain object types
780
- expect(() => diff(set, obj)).not.toThrow()
781
- // @ts-expect-error Testing runtime behavior with non-plain object types
782
- expect(() => diff(obj, set)).not.toThrow()
783
- // @ts-expect-error Testing runtime behavior with non-plain object types
784
- expect(() => isEqual(set, obj)).not.toThrow()
785
- })
786
-
787
- test('should handle Promise objects without throwing TypeError', () => {
788
- const promise = Promise.resolve('test')
789
- const obj = { a: 1 }
790
-
791
- // @ts-expect-error Testing runtime behavior with non-plain object types
792
- expect(() => diff(promise, obj)).not.toThrow()
793
- // @ts-expect-error Testing runtime behavior with non-plain object types
794
- expect(() => diff(obj, promise)).not.toThrow()
795
- // @ts-expect-error Testing runtime behavior with non-plain object types
796
- expect(() => isEqual(promise, obj)).not.toThrow()
797
- })
798
-
799
- test('should handle RegExp objects without throwing TypeError', () => {
800
- const regex = /test/g
801
- const obj = { a: 1 }
802
-
803
- // @ts-expect-error Testing runtime behavior with non-plain object types
804
- expect(() => diff(regex, obj)).not.toThrow()
805
- // @ts-expect-error Testing runtime behavior with non-plain object types
806
- expect(() => diff(obj, regex)).not.toThrow()
807
- // @ts-expect-error Testing runtime behavior with non-plain object types
808
- expect(() => isEqual(regex, obj)).not.toThrow()
809
- })
810
-
811
- test('should handle Function objects without throwing TypeError', () => {
812
- const func = () => 'test'
813
- const obj = { a: 1 }
814
-
815
- // @ts-expect-error Testing runtime behavior with non-plain object types
816
- expect(() => diff(func, obj)).not.toThrow()
817
- // @ts-expect-error Testing runtime behavior with non-plain object types
818
- expect(() => diff(obj, func)).not.toThrow()
819
- // @ts-expect-error Testing runtime behavior with non-plain object types
820
- expect(() => isEqual(func, obj)).not.toThrow()
821
- })
822
-
823
- test('should handle Error objects without throwing TypeError', () => {
824
- const error = new Error('test error')
825
- const obj = { a: 1 }
826
-
827
- // @ts-expect-error Testing runtime behavior with non-plain object types
828
- expect(() => diff(error, obj)).not.toThrow()
829
- // @ts-expect-error Testing runtime behavior with non-plain object types
830
- expect(() => diff(obj, error)).not.toThrow()
831
- // @ts-expect-error Testing runtime behavior with non-plain object types
832
- expect(() => isEqual(error, obj)).not.toThrow()
833
- })
834
-
835
- test('should handle WeakMap objects without throwing TypeError', () => {
836
- const weakMap = new WeakMap()
837
- const obj = { a: 1 }
838
-
839
- // @ts-expect-error Testing runtime behavior with non-plain object types
840
- expect(() => diff(weakMap, obj)).not.toThrow()
841
- // @ts-expect-error Testing runtime behavior with non-plain object types
842
- expect(() => diff(obj, weakMap)).not.toThrow()
843
- // @ts-expect-error Testing runtime behavior with non-plain object types
844
- expect(() => isEqual(weakMap, obj)).not.toThrow()
845
- })
846
-
847
- test('should handle WeakSet objects without throwing TypeError', () => {
848
- const weakSet = new WeakSet()
849
- const obj = { a: 1 }
850
-
851
- // @ts-expect-error Testing runtime behavior with non-plain object types
852
- expect(() => diff(weakSet, obj)).not.toThrow()
853
- // @ts-expect-error Testing runtime behavior with non-plain object types
854
- expect(() => diff(obj, weakSet)).not.toThrow()
855
- // @ts-expect-error Testing runtime behavior with non-plain object types
856
- expect(() => isEqual(weakSet, obj)).not.toThrow()
857
- })
858
-
859
- test('should handle ArrayBuffer objects without throwing TypeError', () => {
860
- const buffer = new ArrayBuffer(8)
861
- const obj = { a: 1 }
862
-
863
- // @ts-expect-error Testing runtime behavior with non-plain object types
864
- expect(() => diff(buffer, obj)).not.toThrow()
865
- // @ts-expect-error Testing runtime behavior with non-plain object types
866
- expect(() => diff(obj, buffer)).not.toThrow()
867
- // @ts-expect-error Testing runtime behavior with non-plain object types
868
- expect(() => isEqual(buffer, obj)).not.toThrow()
869
- })
870
-
871
- test('should handle class instances without throwing TypeError', () => {
872
- class TestClass {
873
- constructor(public value: string) {}
874
- }
875
- const instance = new TestClass('test')
876
- const obj = { a: 1 }
877
-
878
- // @ts-expect-error Testing runtime behavior with non-plain object types
879
- expect(() => diff(instance, obj)).not.toThrow()
880
- // @ts-expect-error Testing runtime behavior with non-plain object types
881
- expect(() => diff(obj, instance)).not.toThrow()
882
- // @ts-expect-error Testing runtime behavior with non-plain object types
883
- expect(() => isEqual(instance, obj)).not.toThrow()
884
- })
885
-
886
- test('should report additions/removals with mixed valid and invalid objects', () => {
887
- const func = () => 'test'
888
- const obj1 = { a: 1 }
889
- const obj2 = { b: 2 }
890
-
891
- // @ts-expect-error Testing runtime behavior with non-plain object types
892
- const result1 = diff(func, obj1)
893
- expect(result1.changed).toBe(true)
894
- expect(result1.add).toEqual({ a: 1 })
895
- expect(result1.remove).toEqual({})
896
-
897
- // @ts-expect-error Testing runtime behavior with non-plain object types
898
- const result2 = diff(obj2, func)
899
- expect(result2.changed).toBe(true)
900
- expect(result2.add).toEqual({})
901
- expect(result2.remove).toEqual({ b: 2 })
902
-
903
- // @ts-expect-error Testing runtime behavior with non-plain object types
904
- const result3 = diff(func, func)
905
- expect(result3.changed).toBe(false)
906
- expect(result3.add).toEqual({})
907
- expect(result3.remove).toEqual({})
908
- })
909
- })
910
- })
911
-
912
- describe('sparse array handling', () => {
913
- test('should properly diff sparse array representations', () => {
914
- // Simulate what happens in store: sparse array [10, 30, 50] with keys ["0", "2", "4"]
915
- // is represented as a regular array [10, 30, 50] when passed to diff()
916
- const oldSparseArray: Record<number, number> = [10, 30, 50] // What current() returns for sparse store
917
- const newDenseArray: Record<number, number> = [100, 200, 300] // What user wants to set
918
-
919
- const result = diff(oldSparseArray, newDenseArray)
920
-
921
- // The problem: diff sees this as simple value changes at indices 0, 1, 2
922
- // But the store actually has sparse keys "0", "2", "4"
923
- // So when reconcile tries to apply changes, only indices 0 and 2 work
924
- expect(result.change).toEqual({
925
- '0': 100, // This works (key "0" exists)
926
- '1': 200, // This fails (key "1" doesn't exist in sparse structure)
927
- '2': 300, // This works (key "2" exists)
928
- })
929
- expect(result.add).toEqual({})
930
- expect(result.remove).toEqual({})
931
- expect(result.changed).toBe(true)
932
- })
933
-
934
- test('should handle array-to-object conversion when context suggests sparse structure', () => {
935
- // This test demonstrates the core issue: we need context about the original structure
936
- // to properly handle sparse array replacement
937
- const oldSparseAsObject = { '0': 10, '2': 30, '4': 50 } // Actual sparse structure
938
- const newDenseArray: Record<number, number> = [100, 200, 300] // User input
939
-
940
- const result = diff(oldSparseAsObject, newDenseArray)
941
-
942
- // This should remove old sparse keys and add new dense keys
943
- expect(result.remove).toEqual({
944
- '4': UNSET, // Key "4" should be removed (key "2" gets reused)
945
- })
946
- expect(result.add).toEqual({
947
- '1': 200, // Key "1" should be added
948
- })
949
- expect(result.change).toEqual({
950
- '0': 100, // Key "0" changes value from 10 to 100
951
- '2': 300, // Key "2" changes value from 30 to 300
952
- })
953
- expect(result.changed).toBe(true)
954
- })
955
- })