@zeix/cause-effect 0.15.1 → 0.16.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.
- package/.ai-context.md +254 -0
- package/.cursorrules +54 -0
- package/.github/copilot-instructions.md +132 -0
- package/CLAUDE.md +319 -0
- package/README.md +167 -159
- package/eslint.config.js +1 -1
- package/index.dev.js +528 -407
- package/index.js +1 -1
- package/index.ts +36 -25
- package/package.json +1 -1
- package/src/computed.ts +41 -30
- package/src/diff.ts +57 -44
- package/src/effect.ts +15 -16
- package/src/errors.ts +64 -0
- package/src/match.ts +2 -2
- package/src/resolve.ts +2 -2
- package/src/signal.ts +27 -49
- package/src/state.ts +27 -19
- package/src/store.ts +410 -209
- package/src/system.ts +122 -0
- package/src/util.ts +45 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +508 -72
- package/test/diff.test.ts +321 -4
- package/test/effect.test.ts +61 -61
- package/test/match.test.ts +38 -28
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +19 -147
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +1370 -134
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +10 -9
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/diff.d.ts +5 -3
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +22 -0
- package/types/src/match.d.ts +1 -1
- package/types/src/resolve.d.ts +1 -1
- package/types/src/signal.d.ts +12 -19
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +40 -36
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +7 -5
- package/index.d.ts +0 -36
- package/src/scheduler.ts +0 -172
- package/types/test-new-effect.d.ts +0 -1
package/test/effect.test.ts
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
createComputed,
|
|
4
|
+
createEffect,
|
|
5
|
+
createState,
|
|
5
6
|
isAbortError,
|
|
6
7
|
match,
|
|
7
8
|
resolve,
|
|
8
|
-
state,
|
|
9
9
|
UNSET,
|
|
10
10
|
} from '../'
|
|
11
11
|
|
|
@@ -17,9 +17,9 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
|
17
17
|
|
|
18
18
|
describe('Effect', () => {
|
|
19
19
|
test('should be triggered after a state change', () => {
|
|
20
|
-
const cause =
|
|
20
|
+
const cause = createState('foo')
|
|
21
21
|
let count = 0
|
|
22
|
-
|
|
22
|
+
createEffect(() => {
|
|
23
23
|
cause.get()
|
|
24
24
|
count++
|
|
25
25
|
})
|
|
@@ -29,17 +29,17 @@ describe('Effect', () => {
|
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
test('should be triggered after computed async signals resolve without waterfalls', async () => {
|
|
32
|
-
const a =
|
|
32
|
+
const a = createComputed(async () => {
|
|
33
33
|
await wait(100)
|
|
34
34
|
return 10
|
|
35
35
|
})
|
|
36
|
-
const b =
|
|
36
|
+
const b = createComputed(async () => {
|
|
37
37
|
await wait(100)
|
|
38
38
|
return 20
|
|
39
39
|
})
|
|
40
40
|
let result = 0
|
|
41
41
|
let count = 0
|
|
42
|
-
|
|
42
|
+
createEffect(() => {
|
|
43
43
|
const resolved = resolve({ a, b })
|
|
44
44
|
match(resolved, {
|
|
45
45
|
ok: ({ a: aValue, b: bValue }) => {
|
|
@@ -56,10 +56,10 @@ describe('Effect', () => {
|
|
|
56
56
|
})
|
|
57
57
|
|
|
58
58
|
test('should be triggered repeatedly after repeated state change', async () => {
|
|
59
|
-
const cause =
|
|
59
|
+
const cause = createState(0)
|
|
60
60
|
let result = 0
|
|
61
61
|
let count = 0
|
|
62
|
-
|
|
62
|
+
createEffect(() => {
|
|
63
63
|
result = cause.get()
|
|
64
64
|
count++
|
|
65
65
|
})
|
|
@@ -71,15 +71,15 @@ describe('Effect', () => {
|
|
|
71
71
|
})
|
|
72
72
|
|
|
73
73
|
test('should handle errors in effects with resolve handlers', () => {
|
|
74
|
-
const a =
|
|
75
|
-
const b =
|
|
74
|
+
const a = createState(1)
|
|
75
|
+
const b = createComputed(() => {
|
|
76
76
|
const v = a.get()
|
|
77
77
|
if (v > 5) throw new Error('Value too high')
|
|
78
78
|
return v * 2
|
|
79
79
|
})
|
|
80
80
|
let normalCallCount = 0
|
|
81
81
|
let errorCallCount = 0
|
|
82
|
-
|
|
82
|
+
createEffect(() => {
|
|
83
83
|
const resolved = resolve({ b })
|
|
84
84
|
match(resolved, {
|
|
85
85
|
ok: () => {
|
|
@@ -109,15 +109,15 @@ describe('Effect', () => {
|
|
|
109
109
|
})
|
|
110
110
|
|
|
111
111
|
test('should handle errors in effects with resolve result', () => {
|
|
112
|
-
const a =
|
|
113
|
-
const b =
|
|
112
|
+
const a = createState(1)
|
|
113
|
+
const b = createComputed(() => {
|
|
114
114
|
const v = a.get()
|
|
115
115
|
if (v > 5) throw new Error('Value too high')
|
|
116
116
|
return v * 2
|
|
117
117
|
})
|
|
118
118
|
let normalCallCount = 0
|
|
119
119
|
let errorCallCount = 0
|
|
120
|
-
|
|
120
|
+
createEffect(() => {
|
|
121
121
|
const result = resolve({ b })
|
|
122
122
|
if (result.ok) {
|
|
123
123
|
normalCallCount++
|
|
@@ -144,13 +144,13 @@ describe('Effect', () => {
|
|
|
144
144
|
})
|
|
145
145
|
|
|
146
146
|
test('should handle UNSET values in effects with resolve handlers', async () => {
|
|
147
|
-
const a =
|
|
147
|
+
const a = createComputed(async () => {
|
|
148
148
|
await wait(100)
|
|
149
149
|
return 42
|
|
150
150
|
})
|
|
151
151
|
let normalCallCount = 0
|
|
152
152
|
let nilCount = 0
|
|
153
|
-
|
|
153
|
+
createEffect(() => {
|
|
154
154
|
const resolved = resolve({ a })
|
|
155
155
|
match(resolved, {
|
|
156
156
|
ok: values => {
|
|
@@ -173,13 +173,13 @@ describe('Effect', () => {
|
|
|
173
173
|
})
|
|
174
174
|
|
|
175
175
|
test('should handle UNSET values in effects with resolve result', async () => {
|
|
176
|
-
const a =
|
|
176
|
+
const a = createComputed(async () => {
|
|
177
177
|
await wait(100)
|
|
178
178
|
return 42
|
|
179
179
|
})
|
|
180
180
|
let normalCallCount = 0
|
|
181
181
|
let nilCount = 0
|
|
182
|
-
|
|
182
|
+
createEffect(() => {
|
|
183
183
|
const result = resolve({ a })
|
|
184
184
|
if (result.ok) {
|
|
185
185
|
normalCallCount++
|
|
@@ -205,15 +205,15 @@ describe('Effect', () => {
|
|
|
205
205
|
console.error = mockConsoleError
|
|
206
206
|
|
|
207
207
|
try {
|
|
208
|
-
const a =
|
|
209
|
-
const b =
|
|
208
|
+
const a = createState(1)
|
|
209
|
+
const b = createComputed(() => {
|
|
210
210
|
const v = a.get()
|
|
211
211
|
if (v > 5) throw new Error('Value too high')
|
|
212
212
|
return v * 2
|
|
213
213
|
})
|
|
214
214
|
|
|
215
215
|
// Create an effect without explicit error handling
|
|
216
|
-
|
|
216
|
+
createEffect(() => {
|
|
217
217
|
b.get()
|
|
218
218
|
})
|
|
219
219
|
|
|
@@ -232,10 +232,10 @@ describe('Effect', () => {
|
|
|
232
232
|
})
|
|
233
233
|
|
|
234
234
|
test('should clean up subscriptions when disposed', () => {
|
|
235
|
-
const count =
|
|
235
|
+
const count = createState(42)
|
|
236
236
|
let received = 0
|
|
237
237
|
|
|
238
|
-
const cleanup =
|
|
238
|
+
const cleanup = createEffect(() => {
|
|
239
239
|
received = count.get()
|
|
240
240
|
})
|
|
241
241
|
|
|
@@ -250,9 +250,9 @@ describe('Effect', () => {
|
|
|
250
250
|
test('should detect and throw error for circular dependencies in effects', () => {
|
|
251
251
|
let okCount = 0
|
|
252
252
|
let errCount = 0
|
|
253
|
-
const count =
|
|
253
|
+
const count = createState(0)
|
|
254
254
|
|
|
255
|
-
|
|
255
|
+
createEffect(() => {
|
|
256
256
|
const resolved = resolve({ count })
|
|
257
257
|
match(resolved, {
|
|
258
258
|
ok: () => {
|
|
@@ -264,7 +264,7 @@ describe('Effect', () => {
|
|
|
264
264
|
errCount++
|
|
265
265
|
expect(errors[0]).toBeInstanceOf(Error)
|
|
266
266
|
expect(errors[0].message).toBe(
|
|
267
|
-
'Circular dependency in effect
|
|
267
|
+
'Circular dependency detected in effect',
|
|
268
268
|
)
|
|
269
269
|
},
|
|
270
270
|
})
|
|
@@ -282,7 +282,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
282
282
|
let abortSignalReceived = false
|
|
283
283
|
let effectCompleted = false
|
|
284
284
|
|
|
285
|
-
|
|
285
|
+
createEffect(async (abort: AbortSignal) => {
|
|
286
286
|
expect(abort).toBeInstanceOf(AbortSignal)
|
|
287
287
|
expect(abort.aborted).toBe(false)
|
|
288
288
|
abortSignalReceived = true
|
|
@@ -298,12 +298,12 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
298
298
|
})
|
|
299
299
|
|
|
300
300
|
test('should abort async operations when signal changes', async () => {
|
|
301
|
-
const testSignal =
|
|
301
|
+
const testSignal = createState(1)
|
|
302
302
|
let operationAborted = false
|
|
303
303
|
let operationCompleted = false
|
|
304
304
|
let abortReason: DOMException | undefined
|
|
305
305
|
|
|
306
|
-
|
|
306
|
+
createEffect(async abort => {
|
|
307
307
|
const result = resolve({ testSignal })
|
|
308
308
|
if (!result.ok) return
|
|
309
309
|
|
|
@@ -340,7 +340,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
340
340
|
let operationAborted = false
|
|
341
341
|
let abortReason: DOMException | undefined
|
|
342
342
|
|
|
343
|
-
const cleanup =
|
|
343
|
+
const cleanup = createEffect(async abort => {
|
|
344
344
|
abort.addEventListener('abort', () => {
|
|
345
345
|
operationAborted = true
|
|
346
346
|
abortReason = abort.reason
|
|
@@ -364,9 +364,9 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
364
364
|
console.error = mockConsoleError
|
|
365
365
|
|
|
366
366
|
try {
|
|
367
|
-
const testSignal =
|
|
367
|
+
const testSignal = createState(1)
|
|
368
368
|
|
|
369
|
-
|
|
369
|
+
createEffect(async abort => {
|
|
370
370
|
const result = resolve({ testSignal })
|
|
371
371
|
if (!result.ok) return
|
|
372
372
|
|
|
@@ -408,9 +408,9 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
408
408
|
test('should handle async effects that return cleanup functions', async () => {
|
|
409
409
|
let asyncEffectCompleted = false
|
|
410
410
|
let cleanupRegistered = false
|
|
411
|
-
const testSignal =
|
|
411
|
+
const testSignal = createState('initial')
|
|
412
412
|
|
|
413
|
-
const cleanup =
|
|
413
|
+
const cleanup = createEffect(async () => {
|
|
414
414
|
const result = resolve({ testSignal })
|
|
415
415
|
if (!result.ok) return
|
|
416
416
|
|
|
@@ -431,11 +431,11 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
431
431
|
})
|
|
432
432
|
|
|
433
433
|
test('should handle rapid signal changes with concurrent async operations', async () => {
|
|
434
|
-
const testSignal =
|
|
434
|
+
const testSignal = createState(0)
|
|
435
435
|
let completedOperations = 0
|
|
436
436
|
let abortedOperations = 0
|
|
437
437
|
|
|
438
|
-
|
|
438
|
+
createEffect(async abort => {
|
|
439
439
|
const result = resolve({ testSignal })
|
|
440
440
|
if (!result.ok) return
|
|
441
441
|
|
|
@@ -477,15 +477,15 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
477
477
|
console.error = mockConsoleError
|
|
478
478
|
|
|
479
479
|
try {
|
|
480
|
-
const testSignal =
|
|
480
|
+
const testSignal = createState(1)
|
|
481
481
|
|
|
482
|
-
const errorThrower =
|
|
482
|
+
const errorThrower = createComputed(() => {
|
|
483
483
|
const value = testSignal.get()
|
|
484
484
|
if (value > 5) throw new Error('Value too high')
|
|
485
485
|
return value
|
|
486
486
|
})
|
|
487
487
|
|
|
488
|
-
|
|
488
|
+
createEffect(async () => {
|
|
489
489
|
const result = resolve({ errorThrower })
|
|
490
490
|
if (result.ok) {
|
|
491
491
|
// Normal operation
|
|
@@ -517,9 +517,9 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
517
517
|
test('should handle promise-based async effects', async () => {
|
|
518
518
|
let promiseResolved = false
|
|
519
519
|
let effectValue = ''
|
|
520
|
-
const testSignal =
|
|
520
|
+
const testSignal = createState('test-value')
|
|
521
521
|
|
|
522
|
-
|
|
522
|
+
createEffect(async abort => {
|
|
523
523
|
const result = resolve({ testSignal })
|
|
524
524
|
if (!result.ok) return
|
|
525
525
|
|
|
@@ -548,7 +548,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
548
548
|
})
|
|
549
549
|
|
|
550
550
|
test('should not create AbortController for sync functions', () => {
|
|
551
|
-
const testSignal =
|
|
551
|
+
const testSignal = createState('test')
|
|
552
552
|
let syncCallCount = 0
|
|
553
553
|
|
|
554
554
|
// Mock AbortController constructor to detect if it's called
|
|
@@ -563,7 +563,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
563
563
|
}
|
|
564
564
|
|
|
565
565
|
try {
|
|
566
|
-
|
|
566
|
+
createEffect(() => {
|
|
567
567
|
const result = resolve({ testSignal })
|
|
568
568
|
if (result.ok) {
|
|
569
569
|
syncCallCount++
|
|
@@ -579,11 +579,11 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
579
579
|
})
|
|
580
580
|
|
|
581
581
|
test('should handle concurrent async operations with abort', async () => {
|
|
582
|
-
const testSignal =
|
|
582
|
+
const testSignal = createState(1)
|
|
583
583
|
let operation1Completed = false
|
|
584
584
|
let operation1Aborted = false
|
|
585
585
|
|
|
586
|
-
|
|
586
|
+
createEffect(async abort => {
|
|
587
587
|
const result = resolve({ testSignal })
|
|
588
588
|
if (!result.ok) return
|
|
589
589
|
|
|
@@ -629,11 +629,11 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
629
629
|
|
|
630
630
|
describe('Effect + Resolve Integration', () => {
|
|
631
631
|
test('should work with resolve discriminated union', () => {
|
|
632
|
-
const a =
|
|
633
|
-
const b =
|
|
632
|
+
const a = createState(10)
|
|
633
|
+
const b = createState('hello')
|
|
634
634
|
let effectRan = false
|
|
635
635
|
|
|
636
|
-
|
|
636
|
+
createEffect(() => {
|
|
637
637
|
const result = resolve({ a, b })
|
|
638
638
|
|
|
639
639
|
if (result.ok) {
|
|
@@ -647,10 +647,10 @@ describe('Effect + Resolve Integration', () => {
|
|
|
647
647
|
})
|
|
648
648
|
|
|
649
649
|
test('should work with match function', () => {
|
|
650
|
-
const a =
|
|
650
|
+
const a = createState(42)
|
|
651
651
|
let matchedValue = 0
|
|
652
652
|
|
|
653
|
-
|
|
653
|
+
createEffect(() => {
|
|
654
654
|
const result = resolve({ a })
|
|
655
655
|
match(result, {
|
|
656
656
|
ok: values => {
|
|
@@ -666,12 +666,12 @@ describe('Effect + Resolve Integration', () => {
|
|
|
666
666
|
describe('Effect - Race Conditions and Consistency', () => {
|
|
667
667
|
test('should handle race conditions between abort and cleanup properly', async () => {
|
|
668
668
|
// This test explores potential race conditions in effect cleanup
|
|
669
|
-
const testSignal =
|
|
669
|
+
const testSignal = createState(0)
|
|
670
670
|
let cleanupCallCount = 0
|
|
671
671
|
let abortCallCount = 0
|
|
672
672
|
let operationCount = 0
|
|
673
673
|
|
|
674
|
-
|
|
674
|
+
createEffect(async abort => {
|
|
675
675
|
testSignal.get()
|
|
676
676
|
++operationCount
|
|
677
677
|
|
|
@@ -706,19 +706,19 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
706
706
|
|
|
707
707
|
test('should demonstrate difference in abort handling between computed and effect', async () => {
|
|
708
708
|
// This test shows why computed needs an abort listener but effect might not
|
|
709
|
-
const source =
|
|
709
|
+
const source = createState(1)
|
|
710
710
|
let computedRetries = 0
|
|
711
711
|
let effectRuns = 0
|
|
712
712
|
|
|
713
713
|
// Computed with abort listener (current implementation)
|
|
714
|
-
const comp =
|
|
714
|
+
const comp = createComputed(async () => {
|
|
715
715
|
computedRetries++
|
|
716
716
|
await wait(30)
|
|
717
717
|
return source.get() * 2
|
|
718
718
|
})
|
|
719
719
|
|
|
720
720
|
// Effect without abort listener (current implementation)
|
|
721
|
-
|
|
721
|
+
createEffect(async () => {
|
|
722
722
|
effectRuns++
|
|
723
723
|
// Must access the source to make effect reactive
|
|
724
724
|
source.get()
|
|
@@ -741,12 +741,12 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
741
741
|
|
|
742
742
|
test('should prevent stale cleanup registration with generation counter approach', async () => {
|
|
743
743
|
// This test verifies that the currentController check prevents stale cleanups
|
|
744
|
-
const testSignal =
|
|
744
|
+
const testSignal = createState(0)
|
|
745
745
|
let cleanupCallCount = 0
|
|
746
746
|
let effectRunCount = 0
|
|
747
747
|
let staleCleanupAttempts = 0
|
|
748
748
|
|
|
749
|
-
|
|
749
|
+
createEffect(async () => {
|
|
750
750
|
effectRunCount++
|
|
751
751
|
const currentRun = effectRunCount
|
|
752
752
|
testSignal.get() // Make reactive
|
|
@@ -782,11 +782,11 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
782
782
|
|
|
783
783
|
test('should demonstrate why computed needs immediate retry via abort listener', async () => {
|
|
784
784
|
// This test shows the performance benefit of immediate retry in computed
|
|
785
|
-
const source =
|
|
785
|
+
const source = createState(1)
|
|
786
786
|
let computeAttempts = 0
|
|
787
787
|
let finalValue: number = 0
|
|
788
788
|
|
|
789
|
-
const comp =
|
|
789
|
+
const comp = createComputed(async () => {
|
|
790
790
|
computeAttempts++
|
|
791
791
|
await wait(30)
|
|
792
792
|
return source.get() * 2
|
package/test/match.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { createComputed, createState, match, resolve, UNSET } from '../'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('Match Function', () => {
|
|
7
7
|
test('should call ok handler for successful resolution', () => {
|
|
8
|
-
const a =
|
|
9
|
-
const b =
|
|
8
|
+
const a = createState(10)
|
|
9
|
+
const b = createState('hello')
|
|
10
10
|
let okCalled = false
|
|
11
11
|
let okValues: { a: number; b: string } | null = null
|
|
12
12
|
|
|
@@ -32,8 +32,8 @@ describe('Match Function', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('should call nil handler for pending signals', () => {
|
|
35
|
-
const a =
|
|
36
|
-
const b =
|
|
35
|
+
const a = createState(10)
|
|
36
|
+
const b = createState(UNSET)
|
|
37
37
|
let nilCalled = false
|
|
38
38
|
|
|
39
39
|
match(resolve({ a, b }), {
|
|
@@ -52,8 +52,8 @@ describe('Match Function', () => {
|
|
|
52
52
|
})
|
|
53
53
|
|
|
54
54
|
test('should call error handler for error signals', () => {
|
|
55
|
-
const a =
|
|
56
|
-
const b =
|
|
55
|
+
const a = createState(10)
|
|
56
|
+
const b = createComputed(() => {
|
|
57
57
|
throw new Error('Test error')
|
|
58
58
|
})
|
|
59
59
|
let errCalled = false
|
|
@@ -77,18 +77,22 @@ describe('Match Function', () => {
|
|
|
77
77
|
expect((errValue as unknown as Error).message).toBe('Test error')
|
|
78
78
|
})
|
|
79
79
|
|
|
80
|
-
test('should handle missing handlers gracefully', () => {
|
|
81
|
-
const a =
|
|
80
|
+
test('should handle missing optional handlers gracefully', () => {
|
|
81
|
+
const a = createState(10)
|
|
82
82
|
const result = resolve({ a })
|
|
83
83
|
|
|
84
|
-
// Should not throw even with
|
|
84
|
+
// Should not throw even with only required ok handler (err and nil are optional)
|
|
85
85
|
expect(() => {
|
|
86
|
-
match(result, {
|
|
86
|
+
match(result, {
|
|
87
|
+
ok: () => {
|
|
88
|
+
// This handler is required, but err and nil are optional
|
|
89
|
+
},
|
|
90
|
+
})
|
|
87
91
|
}).not.toThrow()
|
|
88
92
|
})
|
|
89
93
|
|
|
90
94
|
test('should return void always', () => {
|
|
91
|
-
const a =
|
|
95
|
+
const a = createState(42)
|
|
92
96
|
|
|
93
97
|
const returnValue = match(resolve({ a }), {
|
|
94
98
|
ok: () => {
|
|
@@ -101,7 +105,7 @@ describe('Match Function', () => {
|
|
|
101
105
|
})
|
|
102
106
|
|
|
103
107
|
test('should handle handler errors by calling error handler', () => {
|
|
104
|
-
const a =
|
|
108
|
+
const a = createState(10)
|
|
105
109
|
let handlerErrorCalled = false
|
|
106
110
|
let handlerError: Error | null = null
|
|
107
111
|
|
|
@@ -121,7 +125,7 @@ describe('Match Function', () => {
|
|
|
121
125
|
})
|
|
122
126
|
|
|
123
127
|
test('should rethrow handler errors if no error handler available', () => {
|
|
124
|
-
const a =
|
|
128
|
+
const a = createState(10)
|
|
125
129
|
|
|
126
130
|
expect(() => {
|
|
127
131
|
match(resolve({ a }), {
|
|
@@ -133,12 +137,15 @@ describe('Match Function', () => {
|
|
|
133
137
|
})
|
|
134
138
|
|
|
135
139
|
test('should combine existing errors with handler errors', () => {
|
|
136
|
-
const a =
|
|
140
|
+
const a = createComputed(() => {
|
|
137
141
|
throw new Error('Signal error')
|
|
138
142
|
})
|
|
139
143
|
let allErrors: readonly Error[] | null = null
|
|
140
144
|
|
|
141
145
|
match(resolve({ a }), {
|
|
146
|
+
ok: () => {
|
|
147
|
+
// This won't be called since there are errors, but it's required
|
|
148
|
+
},
|
|
142
149
|
err: errors => {
|
|
143
150
|
// First call with signal error
|
|
144
151
|
if (errors.length === 1) {
|
|
@@ -160,9 +167,9 @@ describe('Match Function', () => {
|
|
|
160
167
|
})
|
|
161
168
|
|
|
162
169
|
test('should work with complex type inference', () => {
|
|
163
|
-
const user =
|
|
164
|
-
const posts =
|
|
165
|
-
const settings =
|
|
170
|
+
const user = createState({ id: 1, name: 'Alice' })
|
|
171
|
+
const posts = createState([{ id: 1, title: 'Hello' }])
|
|
172
|
+
const settings = createState({ theme: 'dark' })
|
|
166
173
|
|
|
167
174
|
let typeTestPassed = false
|
|
168
175
|
|
|
@@ -187,8 +194,8 @@ describe('Match Function', () => {
|
|
|
187
194
|
})
|
|
188
195
|
|
|
189
196
|
test('should handle side effects only pattern', () => {
|
|
190
|
-
const count =
|
|
191
|
-
const name =
|
|
197
|
+
const count = createState(5)
|
|
198
|
+
const name = createState('test')
|
|
192
199
|
let sideEffectExecuted = false
|
|
193
200
|
let capturedData = ''
|
|
194
201
|
|
|
@@ -207,15 +214,18 @@ describe('Match Function', () => {
|
|
|
207
214
|
})
|
|
208
215
|
|
|
209
216
|
test('should handle multiple error types correctly', () => {
|
|
210
|
-
const error1 =
|
|
217
|
+
const error1 = createComputed(() => {
|
|
211
218
|
throw new Error('First error')
|
|
212
219
|
})
|
|
213
|
-
const error2 =
|
|
220
|
+
const error2 = createComputed(() => {
|
|
214
221
|
throw new Error('Second error')
|
|
215
222
|
})
|
|
216
223
|
let errorMessages: string[] = []
|
|
217
224
|
|
|
218
225
|
match(resolve({ error1, error2 }), {
|
|
226
|
+
ok: () => {
|
|
227
|
+
// This won't be called since there are errors, but it's required
|
|
228
|
+
},
|
|
219
229
|
err: errors => {
|
|
220
230
|
errorMessages = errors.map(e => e.message)
|
|
221
231
|
},
|
|
@@ -230,7 +240,7 @@ describe('Match Function', () => {
|
|
|
230
240
|
const wait = (ms: number) =>
|
|
231
241
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
232
242
|
|
|
233
|
-
const asyncSignal =
|
|
243
|
+
const asyncSignal = createComputed(async () => {
|
|
234
244
|
await wait(10)
|
|
235
245
|
return 'async result'
|
|
236
246
|
})
|
|
@@ -270,7 +280,7 @@ describe('Match Function', () => {
|
|
|
270
280
|
})
|
|
271
281
|
|
|
272
282
|
test('should maintain referential transparency', () => {
|
|
273
|
-
const a =
|
|
283
|
+
const a = createState(42)
|
|
274
284
|
const result = resolve({ a })
|
|
275
285
|
let callCount = 0
|
|
276
286
|
|
|
@@ -293,7 +303,7 @@ describe('Match Function', () => {
|
|
|
293
303
|
|
|
294
304
|
describe('Match Function Integration', () => {
|
|
295
305
|
test('should work seamlessly with resolve', () => {
|
|
296
|
-
const data =
|
|
306
|
+
const data = createState({ id: 1, value: 'test' })
|
|
297
307
|
let processed = false
|
|
298
308
|
let processedValue = ''
|
|
299
309
|
|
|
@@ -312,12 +322,12 @@ describe('Match Function Integration', () => {
|
|
|
312
322
|
const wait = (ms: number) =>
|
|
313
323
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
314
324
|
|
|
315
|
-
const syncData =
|
|
316
|
-
const asyncData =
|
|
325
|
+
const syncData = createState('available')
|
|
326
|
+
const asyncData = createComputed(async () => {
|
|
317
327
|
await wait(10)
|
|
318
328
|
return 'loaded'
|
|
319
329
|
})
|
|
320
|
-
const errorData =
|
|
330
|
+
const errorData = createComputed(() => {
|
|
321
331
|
throw new Error('Failed to load')
|
|
322
332
|
})
|
|
323
333
|
|
package/test/resolve.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { createComputed, resolve, createState, UNSET } from '../'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('Resolve Function', () => {
|
|
7
7
|
test('should return discriminated union for successful resolution', () => {
|
|
8
|
-
const a =
|
|
9
|
-
const b =
|
|
8
|
+
const a = createState(10)
|
|
9
|
+
const b = createState('hello')
|
|
10
10
|
|
|
11
11
|
const result = resolve({ a, b })
|
|
12
12
|
|
|
@@ -20,8 +20,8 @@ describe('Resolve Function', () => {
|
|
|
20
20
|
})
|
|
21
21
|
|
|
22
22
|
test('should return discriminated union for pending signals', () => {
|
|
23
|
-
const a =
|
|
24
|
-
const b =
|
|
23
|
+
const a = createState(10)
|
|
24
|
+
const b = createState(UNSET)
|
|
25
25
|
|
|
26
26
|
const result = resolve({ a, b })
|
|
27
27
|
|
|
@@ -32,8 +32,8 @@ describe('Resolve Function', () => {
|
|
|
32
32
|
})
|
|
33
33
|
|
|
34
34
|
test('should return discriminated union for error signals', () => {
|
|
35
|
-
const a =
|
|
36
|
-
const b =
|
|
35
|
+
const a = createState(10)
|
|
36
|
+
const b = createComputed(() => {
|
|
37
37
|
throw new Error('Test error')
|
|
38
38
|
})
|
|
39
39
|
|
|
@@ -49,11 +49,11 @@ describe('Resolve Function', () => {
|
|
|
49
49
|
})
|
|
50
50
|
|
|
51
51
|
test('should handle mixed error and valid signals', () => {
|
|
52
|
-
const valid =
|
|
53
|
-
const error1 =
|
|
52
|
+
const valid = createState('valid')
|
|
53
|
+
const error1 = createComputed(() => {
|
|
54
54
|
throw new Error('Error 1')
|
|
55
55
|
})
|
|
56
|
-
const error2 =
|
|
56
|
+
const error2 = createComputed(() => {
|
|
57
57
|
throw new Error('Error 2')
|
|
58
58
|
})
|
|
59
59
|
|
|
@@ -69,8 +69,8 @@ describe('Resolve Function', () => {
|
|
|
69
69
|
})
|
|
70
70
|
|
|
71
71
|
test('should prioritize pending over errors', () => {
|
|
72
|
-
const pending =
|
|
73
|
-
const error =
|
|
72
|
+
const pending = createState(UNSET)
|
|
73
|
+
const error = createComputed(() => {
|
|
74
74
|
throw new Error('Test error')
|
|
75
75
|
})
|
|
76
76
|
|
|
@@ -91,8 +91,8 @@ describe('Resolve Function', () => {
|
|
|
91
91
|
})
|
|
92
92
|
|
|
93
93
|
test('should handle complex nested object signals', () => {
|
|
94
|
-
const user =
|
|
95
|
-
const settings =
|
|
94
|
+
const user = createState({ name: 'Alice', age: 25 })
|
|
95
|
+
const settings = createState({ theme: 'dark', lang: 'en' })
|
|
96
96
|
|
|
97
97
|
const result = resolve({ user, settings })
|
|
98
98
|
|
|
@@ -109,7 +109,7 @@ describe('Resolve Function', () => {
|
|
|
109
109
|
const wait = (ms: number) =>
|
|
110
110
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
111
111
|
|
|
112
|
-
const asyncSignal =
|
|
112
|
+
const asyncSignal = createComputed(async () => {
|
|
113
113
|
await wait(10)
|
|
114
114
|
return 'async result'
|
|
115
115
|
})
|
|
@@ -133,7 +133,7 @@ describe('Resolve Function', () => {
|
|
|
133
133
|
const wait = (ms: number) =>
|
|
134
134
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
135
135
|
|
|
136
|
-
const asyncError =
|
|
136
|
+
const asyncError = createComputed(async () => {
|
|
137
137
|
await wait(10)
|
|
138
138
|
throw new Error('Async error')
|
|
139
139
|
})
|