@zeix/cause-effect 0.15.2 → 0.16.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.
@@ -1,14 +1,14 @@
1
1
  import { describe, expect, test } from 'bun:test'
2
2
  import {
3
- computed,
4
- effect,
3
+ createComputed,
4
+ createEffect,
5
+ createState,
5
6
  isComputed,
6
7
  isState,
7
8
  match,
8
9
  resolve,
9
- state,
10
10
  UNSET,
11
- } from '../'
11
+ } from '..'
12
12
 
13
13
  /* === Utility Functions === */
14
14
 
@@ -19,38 +19,38 @@ const increment = (n: number) => (Number.isFinite(n) ? n + 1 : UNSET)
19
19
 
20
20
  describe('Computed', () => {
21
21
  test('should identify computed signals with isComputed()', () => {
22
- const count = state(42)
23
- const doubled = computed(() => count.get() * 2)
22
+ const count = createState(42)
23
+ const doubled = createComputed(() => count.get() * 2)
24
24
  expect(isComputed(doubled)).toBe(true)
25
25
  expect(isState(doubled)).toBe(false)
26
26
  })
27
27
 
28
28
  test('should compute a function', () => {
29
- const derived = computed(() => 1 + 2)
29
+ const derived = createComputed(() => 1 + 2)
30
30
  expect(derived.get()).toBe(3)
31
31
  })
32
32
 
33
33
  test('should compute function dependent on a signal', () => {
34
- const cause = state(42)
35
- const derived = computed(() => cause.get() + 1)
34
+ const cause = createState(42)
35
+ const derived = createComputed(() => cause.get() + 1)
36
36
  expect(derived.get()).toBe(43)
37
37
  })
38
38
 
39
39
  test('should compute function dependent on an updated signal', () => {
40
- const cause = state(42)
41
- const derived = computed(() => cause.get() + 1)
40
+ const cause = createState(42)
41
+ const derived = createComputed(() => cause.get() + 1)
42
42
  cause.set(24)
43
43
  expect(derived.get()).toBe(25)
44
44
  })
45
45
 
46
46
  test('should compute function dependent on an async signal', async () => {
47
- const status = state('pending')
48
- const promised = computed(async () => {
47
+ const status = createState('pending')
48
+ const promised = createComputed(async () => {
49
49
  await wait(100)
50
50
  status.set('success')
51
51
  return 42
52
52
  })
53
- const derived = computed(() => increment(promised.get()))
53
+ const derived = createComputed(() => increment(promised.get()))
54
54
  expect(derived.get()).toBe(UNSET)
55
55
  expect(status.get()).toBe('pending')
56
56
  await wait(110)
@@ -59,15 +59,15 @@ describe('Computed', () => {
59
59
  })
60
60
 
61
61
  test('should handle errors from an async signal gracefully', async () => {
62
- const status = state('pending')
63
- const error = state('')
64
- const promised = computed(async () => {
62
+ const status = createState('pending')
63
+ const error = createState('')
64
+ const promised = createComputed(async () => {
65
65
  await wait(100)
66
66
  status.set('error')
67
67
  error.set('error occurred')
68
68
  return 0
69
69
  })
70
- const derived = computed(() => increment(promised.get()))
70
+ const derived = createComputed(() => increment(promised.get()))
71
71
  expect(derived.get()).toBe(UNSET)
72
72
  expect(status.get()).toBe('pending')
73
73
  await wait(110)
@@ -76,15 +76,15 @@ describe('Computed', () => {
76
76
  })
77
77
 
78
78
  test('should compute task signals in parallel without waterfalls', async () => {
79
- const a = computed(async () => {
79
+ const a = createComputed(async () => {
80
80
  await wait(100)
81
81
  return 10
82
82
  })
83
- const b = computed(async () => {
83
+ const b = createComputed(async () => {
84
84
  await wait(100)
85
85
  return 20
86
86
  })
87
- const c = computed(() => {
87
+ const c = createComputed(() => {
88
88
  const aValue = a.get()
89
89
  const bValue = b.get()
90
90
  return aValue === UNSET || bValue === UNSET
@@ -97,28 +97,28 @@ describe('Computed', () => {
97
97
  })
98
98
 
99
99
  test('should compute function dependent on a chain of computed states dependent on a signal', () => {
100
- const x = state(42)
101
- const a = computed(() => x.get() + 1)
102
- const b = computed(() => a.get() * 2)
103
- const c = computed(() => b.get() + 1)
100
+ const x = createState(42)
101
+ const a = createComputed(() => x.get() + 1)
102
+ const b = createComputed(() => a.get() * 2)
103
+ const c = createComputed(() => b.get() + 1)
104
104
  expect(c.get()).toBe(87)
105
105
  })
106
106
 
107
107
  test('should compute function dependent on a chain of computed states dependent on an updated signal', () => {
108
- const x = state(42)
109
- const a = computed(() => x.get() + 1)
110
- const b = computed(() => a.get() * 2)
111
- const c = computed(() => b.get() + 1)
108
+ const x = createState(42)
109
+ const a = createComputed(() => x.get() + 1)
110
+ const b = createComputed(() => a.get() * 2)
111
+ const c = createComputed(() => b.get() + 1)
112
112
  x.set(24)
113
113
  expect(c.get()).toBe(51)
114
114
  })
115
115
 
116
116
  test('should drop X->B->X updates', () => {
117
117
  let count = 0
118
- const x = state(2)
119
- const a = computed(() => x.get() - 1)
120
- const b = computed(() => x.get() + a.get())
121
- const c = computed(() => {
118
+ const x = createState(2)
119
+ const a = createComputed(() => x.get() - 1)
120
+ const b = createComputed(() => x.get() + a.get())
121
+ const c = createComputed(() => {
122
122
  count++
123
123
  return `c: ${b.get()}`
124
124
  })
@@ -131,10 +131,10 @@ describe('Computed', () => {
131
131
 
132
132
  test('should only update every signal once (diamond graph)', () => {
133
133
  let count = 0
134
- const x = state('a')
135
- const a = computed(() => x.get())
136
- const b = computed(() => x.get())
137
- const c = computed(() => {
134
+ const x = createState('a')
135
+ const a = createComputed(() => x.get())
136
+ const b = createComputed(() => x.get())
137
+ const c = createComputed(() => {
138
138
  count++
139
139
  return `${a.get()} ${b.get()}`
140
140
  })
@@ -148,11 +148,11 @@ describe('Computed', () => {
148
148
 
149
149
  test('should only update every signal once (diamond graph + tail)', () => {
150
150
  let count = 0
151
- const x = state('a')
152
- const a = computed(() => x.get())
153
- const b = computed(() => x.get())
154
- const c = computed(() => `${a.get()} ${b.get()}`)
155
- const d = computed(() => {
151
+ const x = createState('a')
152
+ const a = createComputed(() => x.get())
153
+ const b = createComputed(() => x.get())
154
+ const c = createComputed(() => `${a.get()} ${b.get()}`)
155
+ const d = createComputed(() => {
156
156
  count++
157
157
  return c.get()
158
158
  })
@@ -164,10 +164,10 @@ describe('Computed', () => {
164
164
  })
165
165
 
166
166
  test('should update multiple times after multiple state changes', () => {
167
- const a = state(3)
168
- const b = state(4)
167
+ const a = createState(3)
168
+ const b = createState(4)
169
169
  let count = 0
170
- const sum = computed(() => {
170
+ const sum = createComputed(() => {
171
171
  count++
172
172
  return a.get() + b.get()
173
173
  })
@@ -189,12 +189,12 @@ describe('Computed', () => {
189
189
  */
190
190
  test('should bail out if result is the same', () => {
191
191
  let count = 0
192
- const x = state('a')
193
- const a = computed(() => {
192
+ const x = createState('a')
193
+ const a = createComputed(() => {
194
194
  x.get()
195
195
  return 'foo'
196
196
  })
197
- const b = computed(() => {
197
+ const b = createComputed(() => {
198
198
  count++
199
199
  return a.get()
200
200
  })
@@ -209,10 +209,10 @@ describe('Computed', () => {
209
209
 
210
210
  test('should block if result remains unchanged', () => {
211
211
  let count = 0
212
- const x = state(42)
213
- const a = computed(() => x.get() % 2)
214
- const b = computed(() => (a.get() ? 'odd' : 'even'))
215
- const c = computed(() => {
212
+ const x = createState(42)
213
+ const a = createComputed(() => x.get() % 2)
214
+ const b = createComputed(() => (a.get() ? 'odd' : 'even'))
215
+ const c = createComputed(() => {
216
216
  count++
217
217
  return `c: ${b.get()}`
218
218
  })
@@ -226,9 +226,9 @@ describe('Computed', () => {
226
226
  })
227
227
 
228
228
  test('should detect and throw error for circular dependencies', () => {
229
- const a = state(1)
230
- const b = computed(() => c.get() + 1)
231
- const c = computed(() => b.get() + a.get())
229
+ const a = createState(1)
230
+ const b = createComputed(() => c.get() + 1)
231
+ const c = createComputed(() => b.get() + a.get())
232
232
  expect(() => {
233
233
  b.get() // This should trigger the circular dependency
234
234
  }).toThrow('Circular dependency detected in computed')
@@ -238,14 +238,14 @@ describe('Computed', () => {
238
238
  test('should propagate error if an error occurred', () => {
239
239
  let okCount = 0
240
240
  let errCount = 0
241
- const x = state(0)
242
- const a = computed(() => {
241
+ const x = createState(0)
242
+ const a = createComputed(() => {
243
243
  if (x.get() === 1) throw new Error('Calculation error')
244
244
  return 1
245
245
  })
246
246
 
247
247
  // Replace matcher with try/catch in a computed
248
- const b = computed(() => {
248
+ const b = createComputed(() => {
249
249
  try {
250
250
  a.get() // just check if it works
251
251
  return `c: success`
@@ -254,7 +254,7 @@ describe('Computed', () => {
254
254
  return `c: recovered`
255
255
  }
256
256
  })
257
- const c = computed(() => {
257
+ const c = createComputed(() => {
258
258
  okCount++
259
259
  return b.get()
260
260
  })
@@ -276,15 +276,15 @@ describe('Computed', () => {
276
276
  })
277
277
 
278
278
  test('should create an effect that reacts on async computed changes', async () => {
279
- const cause = state(42)
280
- const derived = computed(async () => {
279
+ const cause = createState(42)
280
+ const derived = createComputed(async () => {
281
281
  await wait(100)
282
282
  return cause.get() + 1
283
283
  })
284
284
  let okCount = 0
285
285
  let nilCount = 0
286
286
  let result: number = 0
287
- effect(() => {
287
+ createEffect(() => {
288
288
  const resolved = resolve({ derived })
289
289
  match(resolved, {
290
290
  ok: ({ derived: v }) => {
@@ -308,12 +308,12 @@ describe('Computed', () => {
308
308
  })
309
309
 
310
310
  test('should handle complex computed signal with error and async dependencies', async () => {
311
- const toggleState = state(true)
312
- const errorProne = computed(() => {
311
+ const toggleState = createState(true)
312
+ const errorProne = createComputed(() => {
313
313
  if (toggleState.get()) throw new Error('Intentional error')
314
314
  return 42
315
315
  })
316
- const asyncValue = computed(async () => {
316
+ const asyncValue = createComputed(async () => {
317
317
  await wait(50)
318
318
  return 10
319
319
  })
@@ -322,7 +322,7 @@ describe('Computed', () => {
322
322
  let errCount = 0
323
323
  // let _result: number = 0
324
324
 
325
- const complexComputed = computed(() => {
325
+ const complexComputed = createComputed(() => {
326
326
  try {
327
327
  const x = errorProne.get()
328
328
  const y = asyncValue.get()
@@ -355,9 +355,9 @@ describe('Computed', () => {
355
355
  })
356
356
 
357
357
  test('should handle signal changes during async computation', async () => {
358
- const source = state(1)
358
+ const source = createState(1)
359
359
  let computationCount = 0
360
- const derived = computed(async abort => {
360
+ const derived = createComputed(async (_, abort) => {
361
361
  computationCount++
362
362
  expect(abort?.aborted).toBe(false)
363
363
  await wait(100)
@@ -376,9 +376,9 @@ describe('Computed', () => {
376
376
  })
377
377
 
378
378
  test('should handle multiple rapid changes during async computation', async () => {
379
- const source = state(1)
379
+ const source = createState(1)
380
380
  let computationCount = 0
381
- const derived = computed(async abort => {
381
+ const derived = createComputed(async (_, abort) => {
382
382
  computationCount++
383
383
  expect(abort?.aborted).toBe(false)
384
384
  await wait(100)
@@ -401,8 +401,8 @@ describe('Computed', () => {
401
401
  })
402
402
 
403
403
  test('should handle errors in aborted computations', async () => {
404
- const source = state(1)
405
- const derived = computed(async () => {
404
+ const source = createState(1)
405
+ const derived = createComputed(async () => {
406
406
  await wait(100)
407
407
  const value = source.get()
408
408
  if (value === 2) throw new Error('Intentional error')
@@ -422,4 +422,440 @@ describe('Computed', () => {
422
422
  await wait(100)
423
423
  expect(derived.get()).toBe(3)
424
424
  })
425
+
426
+ describe('Input Validation', () => {
427
+ test('should throw InvalidCallbackError when callback is not a function', () => {
428
+ expect(() => {
429
+ // @ts-expect-error - Testing invalid input
430
+ createComputed(null)
431
+ }).toThrow('Invalid computed callback null')
432
+
433
+ expect(() => {
434
+ // @ts-expect-error - Testing invalid input
435
+ createComputed(undefined)
436
+ }).toThrow('Invalid computed callback undefined')
437
+
438
+ expect(() => {
439
+ // @ts-expect-error - Testing invalid input
440
+ createComputed(42)
441
+ }).toThrow('Invalid computed callback 42')
442
+
443
+ expect(() => {
444
+ // @ts-expect-error - Testing invalid input
445
+ createComputed('not a function')
446
+ }).toThrow('Invalid computed callback "not a function"')
447
+
448
+ expect(() => {
449
+ // @ts-expect-error - Testing invalid input
450
+ createComputed({ not: 'a function' })
451
+ }).toThrow('Invalid computed callback {"not":"a function"}')
452
+
453
+ expect(() => {
454
+ // @ts-expect-error - Testing invalid input
455
+ createComputed((_a: unknown, _b: unknown, _c: unknown) => 42)
456
+ }).toThrow('Invalid computed callback (_a, _b, _c) => 42')
457
+ })
458
+
459
+ test('should throw NullishSignalValueError when initialValue is null', () => {
460
+ expect(() => {
461
+ // @ts-expect-error - Testing invalid input
462
+ createComputed(() => 42, null)
463
+ }).toThrow('Nullish signal values are not allowed in computed')
464
+ })
465
+
466
+ test('should throw specific error types for invalid inputs', () => {
467
+ try {
468
+ // @ts-expect-error - Testing invalid input
469
+ createComputed(null)
470
+ expect(true).toBe(false) // Should not reach here
471
+ } catch (error) {
472
+ expect(error).toBeInstanceOf(TypeError)
473
+ expect(error.name).toBe('InvalidCallbackError')
474
+ expect(error.message).toBe('Invalid computed callback null')
475
+ }
476
+
477
+ try {
478
+ // @ts-expect-error - Testing invalid input
479
+ createComputed(() => 42, null)
480
+ expect(true).toBe(false) // Should not reach here
481
+ } catch (error) {
482
+ expect(error).toBeInstanceOf(TypeError)
483
+ expect(error.name).toBe('NullishSignalValueError')
484
+ expect(error.message).toBe(
485
+ 'Nullish signal values are not allowed in computed',
486
+ )
487
+ }
488
+ })
489
+
490
+ test('should allow valid callbacks and non-nullish initialValues', () => {
491
+ // These should not throw
492
+ expect(() => {
493
+ createComputed(() => 42)
494
+ }).not.toThrow()
495
+
496
+ expect(() => {
497
+ createComputed(() => 42, 0)
498
+ }).not.toThrow()
499
+
500
+ expect(() => {
501
+ createComputed(() => 'foo', '')
502
+ }).not.toThrow()
503
+
504
+ expect(() => {
505
+ createComputed(() => true, false)
506
+ }).not.toThrow()
507
+
508
+ expect(() => {
509
+ createComputed(async () => ({ id: 42, name: 'John' }), UNSET)
510
+ }).not.toThrow()
511
+ })
512
+ })
513
+
514
+ describe('Initial Value and Old Value', () => {
515
+ test('should use initialValue when provided', () => {
516
+ const computed = createComputed(
517
+ (oldValue: number) => oldValue + 1,
518
+ 10,
519
+ )
520
+ expect(computed.get()).toBe(11)
521
+ })
522
+
523
+ test('should pass current value as oldValue to callback', () => {
524
+ const state = createState(5)
525
+ let receivedOldValue: number | undefined
526
+ const computed = createComputed((oldValue: number) => {
527
+ receivedOldValue = oldValue
528
+ return state.get() * 2
529
+ }, 0)
530
+
531
+ expect(computed.get()).toBe(10)
532
+ expect(receivedOldValue).toBe(0)
533
+
534
+ state.set(3)
535
+ expect(computed.get()).toBe(6)
536
+ expect(receivedOldValue).toBe(10)
537
+ })
538
+
539
+ test('should work as reducer function with oldValue', () => {
540
+ const increment = createState(0)
541
+ const sum = createComputed((oldValue: number) => {
542
+ const inc = increment.get()
543
+ return inc === 0 ? oldValue : oldValue + inc
544
+ }, 0)
545
+
546
+ expect(sum.get()).toBe(0)
547
+
548
+ increment.set(5)
549
+ expect(sum.get()).toBe(5)
550
+
551
+ increment.set(3)
552
+ expect(sum.get()).toBe(8)
553
+
554
+ increment.set(2)
555
+ expect(sum.get()).toBe(10)
556
+ })
557
+
558
+ test('should handle array accumulation with oldValue', () => {
559
+ const item = createState('')
560
+ const items = createComputed((oldValue: string[]) => {
561
+ const newItem = item.get()
562
+ return newItem === '' ? oldValue : [...oldValue, newItem]
563
+ }, [] as string[])
564
+
565
+ expect(items.get()).toEqual([])
566
+
567
+ item.set('first')
568
+ expect(items.get()).toEqual(['first'])
569
+
570
+ item.set('second')
571
+ expect(items.get()).toEqual(['first', 'second'])
572
+
573
+ item.set('third')
574
+ expect(items.get()).toEqual(['first', 'second', 'third'])
575
+ })
576
+
577
+ test('should handle counter with oldValue and multiple dependencies', () => {
578
+ const reset = createState(false)
579
+ const add = createState(0)
580
+ const counter = createComputed((oldValue: number) => {
581
+ if (reset.get()) return 0
582
+ const increment = add.get()
583
+ return increment === 0 ? oldValue : oldValue + increment
584
+ }, 0)
585
+
586
+ expect(counter.get()).toBe(0)
587
+
588
+ add.set(5)
589
+ expect(counter.get()).toBe(5)
590
+
591
+ add.set(3)
592
+ expect(counter.get()).toBe(8)
593
+
594
+ reset.set(true)
595
+ expect(counter.get()).toBe(0)
596
+
597
+ reset.set(false)
598
+ add.set(2)
599
+ expect(counter.get()).toBe(2)
600
+ })
601
+
602
+ test('should pass UNSET as oldValue when no initialValue provided', () => {
603
+ let receivedOldValue: number | undefined
604
+ const state = createState(42)
605
+ const computed = createComputed((oldValue: number) => {
606
+ receivedOldValue = oldValue
607
+ return state.get()
608
+ })
609
+
610
+ expect(computed.get()).toBe(42)
611
+ expect(receivedOldValue).toBe(UNSET)
612
+ })
613
+
614
+ test('should work with async computation and oldValue', async () => {
615
+ let receivedOldValue: number | undefined
616
+
617
+ const asyncComputed = createComputed(async (oldValue: number) => {
618
+ receivedOldValue = oldValue
619
+ await wait(50)
620
+ return oldValue + 5
621
+ }, 10)
622
+
623
+ // Initially returns initialValue before async computation completes
624
+ expect(asyncComputed.get()).toBe(10)
625
+
626
+ // Wait for async computation to complete
627
+ await wait(60)
628
+ expect(asyncComputed.get()).toBe(15) // 10 + 5
629
+ expect(receivedOldValue).toBe(10)
630
+ })
631
+
632
+ test('should handle object updates with oldValue', () => {
633
+ const key = createState('')
634
+ const value = createState('')
635
+ const obj = createComputed(
636
+ (oldValue: Record<string, string>) => {
637
+ const k = key.get()
638
+ const v = value.get()
639
+ if (k === '' || v === '') return oldValue
640
+ return { ...oldValue, [k]: v }
641
+ },
642
+ {} as Record<string, string>,
643
+ )
644
+
645
+ expect(obj.get()).toEqual({})
646
+
647
+ key.set('name')
648
+ value.set('Alice')
649
+ expect(obj.get()).toEqual({ name: 'Alice' })
650
+
651
+ key.set('age')
652
+ value.set('30')
653
+ expect(obj.get()).toEqual({ name: 'Alice', age: '30' })
654
+ })
655
+
656
+ test('should handle async computation with AbortSignal and oldValue', async () => {
657
+ const source = createState(1)
658
+ let computationCount = 0
659
+ const receivedOldValues: number[] = []
660
+
661
+ const asyncComputed = createComputed(
662
+ async (oldValue: number, abort: AbortSignal) => {
663
+ computationCount++
664
+ receivedOldValues.push(oldValue)
665
+
666
+ // Simulate async work
667
+ await wait(100)
668
+
669
+ // Check if computation was aborted
670
+ if (abort.aborted) {
671
+ return oldValue
672
+ }
673
+
674
+ return source.get() + oldValue
675
+ },
676
+ 0,
677
+ )
678
+
679
+ // Initial computation
680
+ expect(asyncComputed.get()).toBe(0) // Returns initialValue immediately
681
+
682
+ // Change source before first computation completes
683
+ source.set(2)
684
+
685
+ // Wait for computation to complete
686
+ await wait(110)
687
+
688
+ // Should have the result from the computation that wasn't aborted
689
+ expect(asyncComputed.get()).toBe(2) // 2 + 0 (initialValue was used as oldValue)
690
+ expect(computationCount).toBe(1) // Only one computation completed
691
+ expect(receivedOldValues).toEqual([0])
692
+ })
693
+
694
+ test('should work with error handling and oldValue', () => {
695
+ const shouldError = createState(false)
696
+ const counter = createState(1)
697
+
698
+ const computed = createComputed((oldValue: number) => {
699
+ if (shouldError.get()) {
700
+ throw new Error('Computation failed')
701
+ }
702
+ // Handle UNSET case by treating it as 0
703
+ const safeOldValue = oldValue === UNSET ? 0 : oldValue
704
+ return safeOldValue + counter.get()
705
+ }, 10)
706
+
707
+ expect(computed.get()).toBe(11) // 10 + 1
708
+
709
+ counter.set(5)
710
+ expect(computed.get()).toBe(16) // 11 + 5
711
+
712
+ // Trigger error
713
+ shouldError.set(true)
714
+ expect(() => computed.get()).toThrow('Computation failed')
715
+
716
+ // Recover from error
717
+ shouldError.set(false)
718
+ counter.set(2)
719
+
720
+ // After error, oldValue should be UNSET, so we treat it as 0 and get 0 + 2 = 2
721
+ expect(computed.get()).toBe(2)
722
+ })
723
+
724
+ test('should work with complex state transitions using oldValue', () => {
725
+ const action = createState<
726
+ 'increment' | 'decrement' | 'reset' | 'multiply'
727
+ >('increment')
728
+ const amount = createState(1)
729
+
730
+ const calculator = createComputed((oldValue: number) => {
731
+ const act = action.get()
732
+ const amt = amount.get()
733
+
734
+ switch (act) {
735
+ case 'increment':
736
+ return oldValue + amt
737
+ case 'decrement':
738
+ return oldValue - amt
739
+ case 'multiply':
740
+ return oldValue * amt
741
+ case 'reset':
742
+ return 0
743
+ default:
744
+ return oldValue
745
+ }
746
+ }, 0)
747
+
748
+ expect(calculator.get()).toBe(1) // 0 + 1
749
+
750
+ amount.set(5)
751
+ expect(calculator.get()).toBe(6) // 1 + 5
752
+
753
+ action.set('multiply')
754
+ amount.set(2)
755
+ expect(calculator.get()).toBe(12) // 6 * 2
756
+
757
+ action.set('decrement')
758
+ amount.set(3)
759
+ expect(calculator.get()).toBe(9) // 12 - 3
760
+
761
+ action.set('reset')
762
+ expect(calculator.get()).toBe(0)
763
+ })
764
+
765
+ test('should handle edge cases with initialValue and oldValue', () => {
766
+ // Test with null/undefined-like values
767
+ const nullishComputed = createComputed((oldValue: string) => {
768
+ return `${oldValue} updated`
769
+ }, '')
770
+
771
+ expect(nullishComputed.get()).toBe(' updated')
772
+
773
+ // Test with complex object initialValue
774
+ interface StateObject {
775
+ count: number
776
+ items: string[]
777
+ meta: { created: Date }
778
+ }
779
+
780
+ const now = new Date()
781
+ const objectComputed = createComputed(
782
+ (oldValue: StateObject) => ({
783
+ ...oldValue,
784
+ count: oldValue.count + 1,
785
+ items: [...oldValue.items, `item${oldValue.count + 1}`],
786
+ }),
787
+ {
788
+ count: 0,
789
+ items: [] as string[],
790
+ meta: { created: now },
791
+ },
792
+ )
793
+
794
+ const result = objectComputed.get()
795
+ expect(result.count).toBe(1)
796
+ expect(result.items).toEqual(['item1'])
797
+ expect(result.meta.created).toBe(now)
798
+ })
799
+
800
+ test('should preserve initialValue type consistency', () => {
801
+ // Test that oldValue type is consistent with initialValue
802
+ const stringComputed = createComputed((oldValue: string) => {
803
+ expect(typeof oldValue).toBe('string')
804
+ return oldValue.toUpperCase()
805
+ }, 'hello')
806
+
807
+ expect(stringComputed.get()).toBe('HELLO')
808
+
809
+ const numberComputed = createComputed((oldValue: number) => {
810
+ expect(typeof oldValue).toBe('number')
811
+ expect(Number.isFinite(oldValue)).toBe(true)
812
+ return oldValue * 2
813
+ }, 5)
814
+
815
+ expect(numberComputed.get()).toBe(10)
816
+ })
817
+
818
+ test('should work with chained computed using oldValue', () => {
819
+ const source = createState(1)
820
+
821
+ const first = createComputed(
822
+ (oldValue: number) => oldValue + source.get(),
823
+ 10,
824
+ )
825
+
826
+ const second = createComputed(
827
+ (oldValue: number) => oldValue + first.get(),
828
+ 20,
829
+ )
830
+
831
+ expect(first.get()).toBe(11) // 10 + 1
832
+ expect(second.get()).toBe(31) // 20 + 11
833
+
834
+ source.set(5)
835
+ expect(first.get()).toBe(16) // 11 + 5
836
+ expect(second.get()).toBe(47) // 31 + 16
837
+ })
838
+
839
+ test('should handle frequent updates with oldValue correctly', () => {
840
+ const trigger = createState(0)
841
+ let computationCount = 0
842
+
843
+ const accumulator = createComputed((oldValue: number) => {
844
+ computationCount++
845
+ return oldValue + trigger.get()
846
+ }, 100)
847
+
848
+ expect(accumulator.get()).toBe(100) // 100 + 0
849
+ expect(computationCount).toBe(1)
850
+
851
+ // Make rapid changes
852
+ for (let i = 1; i <= 5; i++) {
853
+ trigger.set(i)
854
+ accumulator.get() // Force evaluation
855
+ }
856
+
857
+ expect(computationCount).toBe(6) // Initial + 5 updates
858
+ expect(accumulator.get()).toBe(115) // Final accumulated value
859
+ })
860
+ })
425
861
  })