@zeix/cause-effect 0.16.1 → 0.17.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/.ai-context.md +85 -21
- package/.cursorrules +11 -5
- package/.github/copilot-instructions.md +64 -13
- package/CLAUDE.md +143 -163
- package/LICENSE +1 -1
- package/README.md +248 -333
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +21 -21
- package/archive/list.ts +551 -0
- package/archive/memo.ts +139 -0
- package/{src → archive}/state.ts +13 -11
- package/archive/store.ts +368 -0
- package/archive/task.ts +194 -0
- package/eslint.config.js +1 -0
- package/index.dev.js +938 -509
- package/index.js +1 -1
- package/index.ts +50 -23
- package/package.json +1 -1
- package/src/classes/collection.ts +282 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +305 -0
- package/src/classes/ref.ts +68 -0
- package/src/classes/state.ts +98 -0
- package/src/classes/store.ts +210 -0
- package/src/diff.ts +26 -53
- package/src/effect.ts +9 -9
- package/src/errors.ts +71 -25
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -34
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +853 -0
- package/test/computed.test.ts +138 -130
- package/test/diff.test.ts +2 -2
- package/test/effect.test.ts +36 -35
- package/test/list.test.ts +754 -0
- package/test/match.test.ts +25 -25
- package/test/ref.test.ts +227 -0
- package/test/resolve.test.ts +17 -19
- package/test/signal.test.ts +70 -119
- package/test/state.test.ts +44 -44
- package/test/store.test.ts +253 -929
- package/types/index.d.ts +12 -9
- package/types/src/classes/collection.d.ts +46 -0
- package/types/src/classes/composite.d.ts +15 -0
- package/types/src/classes/computed.d.ts +97 -0
- package/types/src/classes/list.d.ts +41 -0
- package/types/src/classes/ref.d.ts +39 -0
- package/types/src/classes/state.d.ts +52 -0
- package/types/src/classes/store.d.ts +51 -0
- package/types/src/diff.d.ts +8 -12
- package/types/src/errors.d.ts +17 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -4
- package/src/store.ts +0 -474
- package/types/src/collection.d.ts +0 -26
- package/types/src/computed.d.ts +0 -33
- package/types/src/scheduler.d.ts +0 -55
- package/types/src/state.d.ts +0 -24
- package/types/src/store.d.ts +0 -65
package/test/effect.test.ts
CHANGED
|
@@ -1,13 +1,14 @@
|
|
|
1
1
|
import { describe, expect, mock, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
-
createComputed,
|
|
4
3
|
createEffect,
|
|
5
|
-
createState,
|
|
6
4
|
isAbortError,
|
|
5
|
+
Memo,
|
|
7
6
|
match,
|
|
8
7
|
resolve,
|
|
8
|
+
State,
|
|
9
|
+
Task,
|
|
9
10
|
UNSET,
|
|
10
|
-
} from '
|
|
11
|
+
} from '../index.ts'
|
|
11
12
|
|
|
12
13
|
/* === Utility Functions === */
|
|
13
14
|
|
|
@@ -17,7 +18,7 @@ const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
|
17
18
|
|
|
18
19
|
describe('Effect', () => {
|
|
19
20
|
test('should be triggered after a state change', () => {
|
|
20
|
-
const cause =
|
|
21
|
+
const cause = new State('foo')
|
|
21
22
|
let count = 0
|
|
22
23
|
createEffect(() => {
|
|
23
24
|
cause.get()
|
|
@@ -29,11 +30,11 @@ describe('Effect', () => {
|
|
|
29
30
|
})
|
|
30
31
|
|
|
31
32
|
test('should be triggered after computed async signals resolve without waterfalls', async () => {
|
|
32
|
-
const a =
|
|
33
|
+
const a = new Task(async () => {
|
|
33
34
|
await wait(100)
|
|
34
35
|
return 10
|
|
35
36
|
})
|
|
36
|
-
const b =
|
|
37
|
+
const b = new Task(async () => {
|
|
37
38
|
await wait(100)
|
|
38
39
|
return 20
|
|
39
40
|
})
|
|
@@ -56,7 +57,7 @@ describe('Effect', () => {
|
|
|
56
57
|
})
|
|
57
58
|
|
|
58
59
|
test('should be triggered repeatedly after repeated state change', async () => {
|
|
59
|
-
const cause =
|
|
60
|
+
const cause = new State(0)
|
|
60
61
|
let result = 0
|
|
61
62
|
let count = 0
|
|
62
63
|
createEffect(() => {
|
|
@@ -71,8 +72,8 @@ describe('Effect', () => {
|
|
|
71
72
|
})
|
|
72
73
|
|
|
73
74
|
test('should handle errors in effects with resolve handlers', () => {
|
|
74
|
-
const a =
|
|
75
|
-
const b =
|
|
75
|
+
const a = new State(1)
|
|
76
|
+
const b = new Memo(() => {
|
|
76
77
|
const v = a.get()
|
|
77
78
|
if (v > 5) throw new Error('Value too high')
|
|
78
79
|
return v * 2
|
|
@@ -109,8 +110,8 @@ describe('Effect', () => {
|
|
|
109
110
|
})
|
|
110
111
|
|
|
111
112
|
test('should handle errors in effects with resolve result', () => {
|
|
112
|
-
const a =
|
|
113
|
-
const b =
|
|
113
|
+
const a = new State(1)
|
|
114
|
+
const b = new Memo(() => {
|
|
114
115
|
const v = a.get()
|
|
115
116
|
if (v > 5) throw new Error('Value too high')
|
|
116
117
|
return v * 2
|
|
@@ -144,7 +145,7 @@ describe('Effect', () => {
|
|
|
144
145
|
})
|
|
145
146
|
|
|
146
147
|
test('should handle UNSET values in effects with resolve handlers', async () => {
|
|
147
|
-
const a =
|
|
148
|
+
const a = new Task(async () => {
|
|
148
149
|
await wait(100)
|
|
149
150
|
return 42
|
|
150
151
|
})
|
|
@@ -173,7 +174,7 @@ describe('Effect', () => {
|
|
|
173
174
|
})
|
|
174
175
|
|
|
175
176
|
test('should handle UNSET values in effects with resolve result', async () => {
|
|
176
|
-
const a =
|
|
177
|
+
const a = new Task(async () => {
|
|
177
178
|
await wait(100)
|
|
178
179
|
return 42
|
|
179
180
|
})
|
|
@@ -205,8 +206,8 @@ describe('Effect', () => {
|
|
|
205
206
|
console.error = mockConsoleError
|
|
206
207
|
|
|
207
208
|
try {
|
|
208
|
-
const a =
|
|
209
|
-
const b =
|
|
209
|
+
const a = new State(1)
|
|
210
|
+
const b = new Memo(() => {
|
|
210
211
|
const v = a.get()
|
|
211
212
|
if (v > 5) throw new Error('Value too high')
|
|
212
213
|
return v * 2
|
|
@@ -232,7 +233,7 @@ describe('Effect', () => {
|
|
|
232
233
|
})
|
|
233
234
|
|
|
234
235
|
test('should clean up subscriptions when disposed', () => {
|
|
235
|
-
const count =
|
|
236
|
+
const count = new State(42)
|
|
236
237
|
let received = 0
|
|
237
238
|
|
|
238
239
|
const cleanup = createEffect(() => {
|
|
@@ -250,7 +251,7 @@ describe('Effect', () => {
|
|
|
250
251
|
test('should detect and throw error for circular dependencies in effects', () => {
|
|
251
252
|
let okCount = 0
|
|
252
253
|
let errCount = 0
|
|
253
|
-
const count =
|
|
254
|
+
const count = new State(0)
|
|
254
255
|
|
|
255
256
|
createEffect(() => {
|
|
256
257
|
const resolved = resolve({ count })
|
|
@@ -298,7 +299,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
298
299
|
})
|
|
299
300
|
|
|
300
301
|
test('should abort async operations when signal changes', async () => {
|
|
301
|
-
const testSignal =
|
|
302
|
+
const testSignal = new State(1)
|
|
302
303
|
let operationAborted = false
|
|
303
304
|
let operationCompleted = false
|
|
304
305
|
let abortReason: DOMException | undefined
|
|
@@ -364,7 +365,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
364
365
|
console.error = mockConsoleError
|
|
365
366
|
|
|
366
367
|
try {
|
|
367
|
-
const testSignal =
|
|
368
|
+
const testSignal = new State(1)
|
|
368
369
|
|
|
369
370
|
createEffect(async abort => {
|
|
370
371
|
const result = resolve({ testSignal })
|
|
@@ -408,7 +409,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
408
409
|
test('should handle async effects that return cleanup functions', async () => {
|
|
409
410
|
let asyncEffectCompleted = false
|
|
410
411
|
let cleanupRegistered = false
|
|
411
|
-
const testSignal =
|
|
412
|
+
const testSignal = new State('initial')
|
|
412
413
|
|
|
413
414
|
const cleanup = createEffect(async () => {
|
|
414
415
|
const result = resolve({ testSignal })
|
|
@@ -431,7 +432,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
431
432
|
})
|
|
432
433
|
|
|
433
434
|
test('should handle rapid signal changes with concurrent async operations', async () => {
|
|
434
|
-
const testSignal =
|
|
435
|
+
const testSignal = new State(0)
|
|
435
436
|
let completedOperations = 0
|
|
436
437
|
let abortedOperations = 0
|
|
437
438
|
|
|
@@ -477,9 +478,9 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
477
478
|
console.error = mockConsoleError
|
|
478
479
|
|
|
479
480
|
try {
|
|
480
|
-
const testSignal =
|
|
481
|
+
const testSignal = new State(1)
|
|
481
482
|
|
|
482
|
-
const errorThrower =
|
|
483
|
+
const errorThrower = new Memo(() => {
|
|
483
484
|
const value = testSignal.get()
|
|
484
485
|
if (value > 5) throw new Error('Value too high')
|
|
485
486
|
return value
|
|
@@ -517,7 +518,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
517
518
|
test('should handle promise-based async effects', async () => {
|
|
518
519
|
let promiseResolved = false
|
|
519
520
|
let effectValue = ''
|
|
520
|
-
const testSignal =
|
|
521
|
+
const testSignal = new State('test-value')
|
|
521
522
|
|
|
522
523
|
createEffect(async abort => {
|
|
523
524
|
const result = resolve({ testSignal })
|
|
@@ -548,7 +549,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
548
549
|
})
|
|
549
550
|
|
|
550
551
|
test('should not create AbortController for sync functions', () => {
|
|
551
|
-
const testSignal =
|
|
552
|
+
const testSignal = new State('test')
|
|
552
553
|
let syncCallCount = 0
|
|
553
554
|
|
|
554
555
|
// Mock AbortController constructor to detect if it's called
|
|
@@ -579,7 +580,7 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
579
580
|
})
|
|
580
581
|
|
|
581
582
|
test('should handle concurrent async operations with abort', async () => {
|
|
582
|
-
const testSignal =
|
|
583
|
+
const testSignal = new State(1)
|
|
583
584
|
let operation1Completed = false
|
|
584
585
|
let operation1Aborted = false
|
|
585
586
|
|
|
@@ -629,8 +630,8 @@ describe('Effect - Async with AbortSignal', () => {
|
|
|
629
630
|
|
|
630
631
|
describe('Effect + Resolve Integration', () => {
|
|
631
632
|
test('should work with resolve discriminated union', () => {
|
|
632
|
-
const a =
|
|
633
|
-
const b =
|
|
633
|
+
const a = new State(10)
|
|
634
|
+
const b = new State('hello')
|
|
634
635
|
let effectRan = false
|
|
635
636
|
|
|
636
637
|
createEffect(() => {
|
|
@@ -647,7 +648,7 @@ describe('Effect + Resolve Integration', () => {
|
|
|
647
648
|
})
|
|
648
649
|
|
|
649
650
|
test('should work with match function', () => {
|
|
650
|
-
const a =
|
|
651
|
+
const a = new State(42)
|
|
651
652
|
let matchedValue = 0
|
|
652
653
|
|
|
653
654
|
createEffect(() => {
|
|
@@ -666,7 +667,7 @@ describe('Effect + Resolve Integration', () => {
|
|
|
666
667
|
describe('Effect - Race Conditions and Consistency', () => {
|
|
667
668
|
test('should handle race conditions between abort and cleanup properly', async () => {
|
|
668
669
|
// This test explores potential race conditions in effect cleanup
|
|
669
|
-
const testSignal =
|
|
670
|
+
const testSignal = new State(0)
|
|
670
671
|
let cleanupCallCount = 0
|
|
671
672
|
let abortCallCount = 0
|
|
672
673
|
let operationCount = 0
|
|
@@ -706,12 +707,12 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
706
707
|
|
|
707
708
|
test('should demonstrate difference in abort handling between computed and effect', async () => {
|
|
708
709
|
// This test shows why computed needs an abort listener but effect might not
|
|
709
|
-
const source =
|
|
710
|
+
const source = new State(1)
|
|
710
711
|
let computedRetries = 0
|
|
711
712
|
let effectRuns = 0
|
|
712
713
|
|
|
713
714
|
// Computed with abort listener (current implementation)
|
|
714
|
-
const comp =
|
|
715
|
+
const comp = new Task(async () => {
|
|
715
716
|
computedRetries++
|
|
716
717
|
await wait(30)
|
|
717
718
|
return source.get() * 2
|
|
@@ -741,7 +742,7 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
741
742
|
|
|
742
743
|
test('should prevent stale cleanup registration with generation counter approach', async () => {
|
|
743
744
|
// This test verifies that the currentController check prevents stale cleanups
|
|
744
|
-
const testSignal =
|
|
745
|
+
const testSignal = new State(0)
|
|
745
746
|
let cleanupCallCount = 0
|
|
746
747
|
let effectRunCount = 0
|
|
747
748
|
let staleCleanupAttempts = 0
|
|
@@ -782,11 +783,11 @@ describe('Effect - Race Conditions and Consistency', () => {
|
|
|
782
783
|
|
|
783
784
|
test('should demonstrate why computed needs immediate retry via abort listener', async () => {
|
|
784
785
|
// This test shows the performance benefit of immediate retry in computed
|
|
785
|
-
const source =
|
|
786
|
+
const source = new State(1)
|
|
786
787
|
let computeAttempts = 0
|
|
787
788
|
let finalValue: number = 0
|
|
788
789
|
|
|
789
|
-
const comp =
|
|
790
|
+
const comp = new Task(async () => {
|
|
790
791
|
computeAttempts++
|
|
791
792
|
await wait(30)
|
|
792
793
|
return source.get() * 2
|