@zeix/cause-effect 0.15.2 → 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 +136 -166
- package/eslint.config.js +1 -1
- package/index.dev.js +125 -129
- package/index.js +1 -1
- package/index.ts +22 -22
- package/package.json +1 -1
- package/src/computed.ts +40 -29
- package/src/effect.ts +15 -12
- package/src/errors.ts +8 -0
- package/src/signal.ts +6 -6
- package/src/state.ts +27 -20
- package/src/store.ts +99 -121
- package/src/system.ts +122 -0
- package/src/util.ts +1 -6
- package/test/batch.test.ts +18 -11
- package/test/benchmark.test.ts +4 -4
- package/test/computed.test.ts +507 -71
- package/test/effect.test.ts +60 -60
- package/test/match.test.ts +25 -25
- package/test/resolve.test.ts +16 -16
- package/test/signal.test.ts +7 -7
- package/test/state.test.ts +212 -25
- package/test/store.test.ts +476 -183
- package/test/util/dependency-graph.ts +1 -1
- package/types/index.d.ts +8 -8
- package/types/src/collection.d.ts +26 -0
- package/types/src/computed.d.ts +9 -9
- package/types/src/effect.d.ts +3 -3
- package/types/src/errors.d.ts +4 -1
- package/types/src/state.d.ts +5 -5
- package/types/src/store.d.ts +27 -41
- package/types/src/system.d.ts +44 -0
- package/types/src/util.d.ts +1 -2
- package/src/scheduler.ts +0 -172
package/test/computed.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
-
|
|
4
|
-
|
|
3
|
+
createComputed,
|
|
4
|
+
createEffect,
|
|
5
|
+
createState,
|
|
5
6
|
isComputed,
|
|
6
7
|
isState,
|
|
7
8
|
match,
|
|
8
9
|
resolve,
|
|
9
|
-
state,
|
|
10
10
|
UNSET,
|
|
11
11
|
} from '../'
|
|
12
12
|
|
|
@@ -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 =
|
|
23
|
-
const doubled =
|
|
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 =
|
|
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 =
|
|
35
|
-
const derived =
|
|
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 =
|
|
41
|
-
const derived =
|
|
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 =
|
|
48
|
-
const promised =
|
|
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 =
|
|
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 =
|
|
63
|
-
const error =
|
|
64
|
-
const promised =
|
|
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 =
|
|
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 =
|
|
79
|
+
const a = createComputed(async () => {
|
|
80
80
|
await wait(100)
|
|
81
81
|
return 10
|
|
82
82
|
})
|
|
83
|
-
const b =
|
|
83
|
+
const b = createComputed(async () => {
|
|
84
84
|
await wait(100)
|
|
85
85
|
return 20
|
|
86
86
|
})
|
|
87
|
-
const c =
|
|
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 =
|
|
101
|
-
const a =
|
|
102
|
-
const b =
|
|
103
|
-
const c =
|
|
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 =
|
|
109
|
-
const a =
|
|
110
|
-
const b =
|
|
111
|
-
const c =
|
|
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 =
|
|
119
|
-
const a =
|
|
120
|
-
const b =
|
|
121
|
-
const c =
|
|
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 =
|
|
135
|
-
const a =
|
|
136
|
-
const b =
|
|
137
|
-
const c =
|
|
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 =
|
|
152
|
-
const a =
|
|
153
|
-
const b =
|
|
154
|
-
const c =
|
|
155
|
-
const d =
|
|
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 =
|
|
168
|
-
const b =
|
|
167
|
+
const a = createState(3)
|
|
168
|
+
const b = createState(4)
|
|
169
169
|
let count = 0
|
|
170
|
-
const sum =
|
|
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 =
|
|
193
|
-
const a =
|
|
192
|
+
const x = createState('a')
|
|
193
|
+
const a = createComputed(() => {
|
|
194
194
|
x.get()
|
|
195
195
|
return 'foo'
|
|
196
196
|
})
|
|
197
|
-
const b =
|
|
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 =
|
|
213
|
-
const a =
|
|
214
|
-
const b =
|
|
215
|
-
const c =
|
|
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 =
|
|
230
|
-
const b =
|
|
231
|
-
const c =
|
|
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 =
|
|
242
|
-
const a =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
280
|
-
const derived =
|
|
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
|
-
|
|
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 =
|
|
312
|
-
const errorProne =
|
|
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 =
|
|
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 =
|
|
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 =
|
|
358
|
+
const source = createState(1)
|
|
359
359
|
let computationCount = 0
|
|
360
|
-
const derived =
|
|
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 =
|
|
379
|
+
const source = createState(1)
|
|
380
380
|
let computationCount = 0
|
|
381
|
-
const derived =
|
|
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 =
|
|
405
|
-
const derived =
|
|
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
|
})
|