@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/signal.test.ts
CHANGED
|
@@ -1,16 +1,15 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type Computed,
|
|
4
|
-
|
|
4
|
+
createComputed,
|
|
5
|
+
createState,
|
|
6
|
+
createStore,
|
|
5
7
|
isComputed,
|
|
6
8
|
isState,
|
|
7
9
|
isStore,
|
|
8
10
|
type Signal,
|
|
9
11
|
type State,
|
|
10
12
|
type Store,
|
|
11
|
-
state,
|
|
12
|
-
store,
|
|
13
|
-
toMutableSignal,
|
|
14
13
|
toSignal,
|
|
15
14
|
type UnknownRecord,
|
|
16
15
|
} from '..'
|
|
@@ -20,31 +19,28 @@ import {
|
|
|
20
19
|
describe('toSignal', () => {
|
|
21
20
|
describe('type inference and runtime behavior', () => {
|
|
22
21
|
test('converts array to Store<Record<string, T>>', () => {
|
|
23
|
-
const
|
|
22
|
+
const result = toSignal([
|
|
24
23
|
{ id: 1, name: 'Alice' },
|
|
25
24
|
{ id: 2, name: 'Bob' },
|
|
26
|
-
]
|
|
27
|
-
const result = toSignal(arr)
|
|
25
|
+
])
|
|
28
26
|
|
|
29
27
|
// Runtime behavior
|
|
30
28
|
expect(isStore(result)).toBe(true)
|
|
31
29
|
expect(result['0'].get()).toEqual({ id: 1, name: 'Alice' })
|
|
32
30
|
expect(result['1'].get()).toEqual({ id: 2, name: 'Bob' })
|
|
33
31
|
|
|
34
|
-
// Type inference test - now correctly returns Store<Record<
|
|
35
|
-
const typedResult: Store<
|
|
36
|
-
Record<string, { id: number; name: string }>
|
|
37
|
-
> = result
|
|
32
|
+
// Type inference test - now correctly returns Store<Record<number, {id: number, name: string}>>
|
|
33
|
+
const typedResult: Store<{ id: number; name: string }[]> = result
|
|
38
34
|
expect(typedResult).toBeDefined()
|
|
39
35
|
})
|
|
40
36
|
|
|
41
37
|
test('converts empty array to Store<Record<string, never>>', () => {
|
|
42
|
-
const
|
|
43
|
-
const result = toSignal(arr)
|
|
38
|
+
const result = toSignal([])
|
|
44
39
|
|
|
45
40
|
// Runtime behavior
|
|
46
41
|
expect(isStore(result)).toBe(true)
|
|
47
|
-
expect(
|
|
42
|
+
expect(result.length).toBe(0)
|
|
43
|
+
expect(Object.keys(result).length).toBe(1) // length property
|
|
48
44
|
})
|
|
49
45
|
|
|
50
46
|
test('converts record to Store<T>', () => {
|
|
@@ -62,7 +58,7 @@ describe('toSignal', () => {
|
|
|
62
58
|
})
|
|
63
59
|
|
|
64
60
|
test('passes through existing Store unchanged', () => {
|
|
65
|
-
const originalStore =
|
|
61
|
+
const originalStore = createStore({ count: 5 })
|
|
66
62
|
const result = toSignal(originalStore)
|
|
67
63
|
|
|
68
64
|
// Runtime behavior
|
|
@@ -76,7 +72,7 @@ describe('toSignal', () => {
|
|
|
76
72
|
})
|
|
77
73
|
|
|
78
74
|
test('passes through existing State unchanged', () => {
|
|
79
|
-
const originalState =
|
|
75
|
+
const originalState = createState(42)
|
|
80
76
|
const result = toSignal(originalState)
|
|
81
77
|
|
|
82
78
|
// Runtime behavior
|
|
@@ -90,7 +86,7 @@ describe('toSignal', () => {
|
|
|
90
86
|
})
|
|
91
87
|
|
|
92
88
|
test('passes through existing Computed unchanged', () => {
|
|
93
|
-
const originalComputed =
|
|
89
|
+
const originalComputed = createComputed(() => 'hello world')
|
|
94
90
|
const result = toSignal(originalComputed)
|
|
95
91
|
|
|
96
92
|
// Runtime behavior
|
|
@@ -145,11 +141,10 @@ describe('toSignal', () => {
|
|
|
145
141
|
|
|
146
142
|
describe('edge cases', () => {
|
|
147
143
|
test('handles nested arrays', () => {
|
|
148
|
-
const
|
|
144
|
+
const result = toSignal([
|
|
149
145
|
[1, 2],
|
|
150
146
|
[3, 4],
|
|
151
|
-
]
|
|
152
|
-
const result = toSignal(nestedArr)
|
|
147
|
+
])
|
|
153
148
|
|
|
154
149
|
expect(isStore(result)).toBe(true)
|
|
155
150
|
// With the fixed behavior, nested arrays should be recovered as arrays
|
|
@@ -184,119 +179,13 @@ describe('toSignal', () => {
|
|
|
184
179
|
})
|
|
185
180
|
})
|
|
186
181
|
|
|
187
|
-
describe('toMutableSignal', () => {
|
|
188
|
-
describe('type inference and runtime behavior', () => {
|
|
189
|
-
test('converts array to Store<Record<string, T>>', () => {
|
|
190
|
-
const arr = [
|
|
191
|
-
{ id: 1, name: 'Alice' },
|
|
192
|
-
{ id: 2, name: 'Bob' },
|
|
193
|
-
]
|
|
194
|
-
const result = toMutableSignal(arr)
|
|
195
|
-
|
|
196
|
-
// Runtime behavior
|
|
197
|
-
expect(isStore(result)).toBe(true)
|
|
198
|
-
expect(result['0'].get()).toEqual({ id: 1, name: 'Alice' })
|
|
199
|
-
expect(result['1'].get()).toEqual({ id: 2, name: 'Bob' })
|
|
200
|
-
|
|
201
|
-
// Type inference test - now correctly returns Store<Record<string, {id: number, name: string}>>
|
|
202
|
-
const typedResult: Store<
|
|
203
|
-
Record<string, { id: number; name: string }>
|
|
204
|
-
> = result
|
|
205
|
-
expect(typedResult).toBeDefined()
|
|
206
|
-
})
|
|
207
|
-
|
|
208
|
-
test('converts record to Store<T>', () => {
|
|
209
|
-
const record = { name: 'Alice', age: 30 }
|
|
210
|
-
const result = toMutableSignal(record)
|
|
211
|
-
|
|
212
|
-
// Runtime behavior
|
|
213
|
-
expect(isStore(result)).toBe(true)
|
|
214
|
-
expect(result.name.get()).toBe('Alice')
|
|
215
|
-
expect(result.age.get()).toBe(30)
|
|
216
|
-
|
|
217
|
-
// Type inference test - should be Store<{name: string, age: number}>
|
|
218
|
-
const typedResult: Store<{ name: string; age: number }> = result
|
|
219
|
-
expect(typedResult).toBeDefined()
|
|
220
|
-
})
|
|
221
|
-
|
|
222
|
-
test('passes through existing Store unchanged', () => {
|
|
223
|
-
const originalStore = store({ count: 5 })
|
|
224
|
-
const result = toMutableSignal(originalStore)
|
|
225
|
-
|
|
226
|
-
// Runtime behavior
|
|
227
|
-
expect(result).toBe(originalStore) // Should be the same instance
|
|
228
|
-
expect(isStore(result)).toBe(true)
|
|
229
|
-
expect(result.count.get()).toBe(5)
|
|
230
|
-
})
|
|
231
|
-
|
|
232
|
-
test('passes through existing State unchanged', () => {
|
|
233
|
-
const originalState = state(42)
|
|
234
|
-
const result = toMutableSignal(originalState)
|
|
235
|
-
|
|
236
|
-
// Runtime behavior
|
|
237
|
-
expect(result).toBe(originalState) // Should be the same instance
|
|
238
|
-
expect(isState(result)).toBe(true)
|
|
239
|
-
expect(result.get()).toBe(42)
|
|
240
|
-
|
|
241
|
-
// Type inference test - should be State<number>
|
|
242
|
-
const typedResult: State<number> = result
|
|
243
|
-
expect(typedResult).toBeDefined()
|
|
244
|
-
})
|
|
245
|
-
|
|
246
|
-
test('converts primitive to State<T>', () => {
|
|
247
|
-
const num = 42
|
|
248
|
-
const result = toMutableSignal(num)
|
|
249
|
-
|
|
250
|
-
// Runtime behavior - primitives are correctly converted to State
|
|
251
|
-
expect(isState(result)).toBe(true)
|
|
252
|
-
expect(result.get()).toBe(42)
|
|
253
|
-
})
|
|
254
|
-
|
|
255
|
-
test('converts object to State<T> (not Store)', () => {
|
|
256
|
-
const obj = new Date('2024-01-01')
|
|
257
|
-
const result = toMutableSignal(obj)
|
|
258
|
-
|
|
259
|
-
// Runtime behavior - objects are correctly converted to State
|
|
260
|
-
expect(isState(result)).toBe(true)
|
|
261
|
-
expect(result.get()).toBe(obj)
|
|
262
|
-
|
|
263
|
-
// Type inference test - should be State<Date>
|
|
264
|
-
const typedResult: State<Date> = result
|
|
265
|
-
expect(typedResult).toBeDefined()
|
|
266
|
-
})
|
|
267
|
-
})
|
|
268
|
-
|
|
269
|
-
describe('differences from toSignal', () => {
|
|
270
|
-
test('does not accept functions (only mutable signals)', () => {
|
|
271
|
-
// toMutableSignal should not have a function overload
|
|
272
|
-
// This test documents the expected behavior difference
|
|
273
|
-
const fn = () => 'test'
|
|
274
|
-
const result = toMutableSignal(fn)
|
|
275
|
-
|
|
276
|
-
// Should treat function as a regular value and create State
|
|
277
|
-
expect(isState(result)).toBe(true)
|
|
278
|
-
expect(result.get()).toBe(fn)
|
|
279
|
-
})
|
|
280
|
-
|
|
281
|
-
test('does not accept Computed signals', () => {
|
|
282
|
-
// toMutableSignal should not accept Computed signals
|
|
283
|
-
const comp = computed(() => 'computed value')
|
|
284
|
-
const result = toMutableSignal(comp)
|
|
285
|
-
|
|
286
|
-
// Should treat Computed as a regular object and create State
|
|
287
|
-
expect(isState(result)).toBe(true)
|
|
288
|
-
expect(result.get()).toBe(comp)
|
|
289
|
-
})
|
|
290
|
-
})
|
|
291
|
-
})
|
|
292
|
-
|
|
293
182
|
describe('Signal compatibility', () => {
|
|
294
183
|
test('all results implement Signal<T> interface', () => {
|
|
295
184
|
const arraySignal = toSignal([1, 2, 3])
|
|
296
185
|
const recordSignal = toSignal({ a: 1, b: 2 })
|
|
297
186
|
const primitiveSignal = toSignal(42)
|
|
298
187
|
const functionSignal = toSignal(() => 'hello')
|
|
299
|
-
const stateSignal = toSignal(
|
|
188
|
+
const stateSignal = toSignal(createState(true))
|
|
300
189
|
|
|
301
190
|
// All should have get() method
|
|
302
191
|
expect(typeof arraySignal.get).toBe('function')
|
|
@@ -356,14 +245,7 @@ describe('Type precision tests', () => {
|
|
|
356
245
|
|
|
357
246
|
describe('Type inference issues', () => {
|
|
358
247
|
test('demonstrates current type inference problem', () => {
|
|
359
|
-
|
|
360
|
-
// instead of the element type, causing type compatibility problems
|
|
361
|
-
const items = [{ id: 1 }, { id: 2 }]
|
|
362
|
-
const result = toSignal(items)
|
|
363
|
-
|
|
364
|
-
// This should work but may have type issues in external libraries
|
|
365
|
-
// The return type should be Store<Record<string, {id: number}>>
|
|
366
|
-
// But currently it might be inferred as Store<Record<string, {id: number}[]>>
|
|
248
|
+
const result = toSignal([{ id: 1 }, { id: 2 }])
|
|
367
249
|
|
|
368
250
|
// Let's verify the actual behavior
|
|
369
251
|
expect(isStore(result)).toBe(true)
|
|
@@ -371,7 +253,7 @@ describe('Type precision tests', () => {
|
|
|
371
253
|
expect(result['1'].get()).toEqual({ id: 2 })
|
|
372
254
|
|
|
373
255
|
// Type assertion test - this should now work with correct typing
|
|
374
|
-
const typedResult: Store<
|
|
256
|
+
const typedResult: Store<{ id: number }[]> = result
|
|
375
257
|
expect(typedResult).toBeDefined()
|
|
376
258
|
|
|
377
259
|
// Simulate external library usage where P[K] represents element type
|
|
@@ -397,19 +279,11 @@ describe('Type precision tests', () => {
|
|
|
397
279
|
})
|
|
398
280
|
|
|
399
281
|
test('verifies fixed type inference for external library compatibility', () => {
|
|
400
|
-
// This test ensures the fix for the type inference issue works
|
|
401
|
-
// Fixed: toSignal<T extends unknown & {}>(value: T[]): Store<Record<string, T>>
|
|
402
|
-
// Now T = {id: number} (element type), T[] = {id: number}[] (array of elements)
|
|
403
|
-
// Return type: Store<Record<string, {id: number}>> (correct)
|
|
404
|
-
|
|
405
282
|
const items = [
|
|
406
283
|
{ id: 1, name: 'Alice' },
|
|
407
284
|
{ id: 2, name: 'Bob' },
|
|
408
285
|
]
|
|
409
286
|
const signal = toSignal(items)
|
|
410
|
-
|
|
411
|
-
// Type should be Store<Record<string, {id: number, name: string}>>
|
|
412
|
-
// Each property signal should be Signal<{id: number, name: string}>
|
|
413
287
|
const firstItemSignal = signal['0']
|
|
414
288
|
const secondItemSignal = signal['1']
|
|
415
289
|
|
|
@@ -419,9 +293,7 @@ describe('Type precision tests', () => {
|
|
|
419
293
|
expect(secondItemSignal.get()).toEqual({ id: 2, name: 'Bob' })
|
|
420
294
|
|
|
421
295
|
// Type inference should now work correctly:
|
|
422
|
-
const properlyTyped: Store<
|
|
423
|
-
Record<string, { id: number; name: string }>
|
|
424
|
-
> = signal
|
|
296
|
+
const properlyTyped: Store<{ id: number; name: string }[]> = signal
|
|
425
297
|
expect(properlyTyped).toBeDefined()
|
|
426
298
|
|
|
427
299
|
// These should work without type errors in external libraries
|
package/test/state.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import { isComputed, isState
|
|
2
|
+
import { createState, isComputed, isState } from '../'
|
|
3
3
|
|
|
4
4
|
/* === Tests === */
|
|
5
5
|
|
|
6
6
|
describe('State', () => {
|
|
7
7
|
describe('State type guard', () => {
|
|
8
8
|
test('isState identifies state signals', () => {
|
|
9
|
-
const count =
|
|
9
|
+
const count = createState(42)
|
|
10
10
|
expect(isState(count)).toBe(true)
|
|
11
11
|
expect(isComputed(count)).toBe(false)
|
|
12
12
|
})
|
|
@@ -14,28 +14,28 @@ describe('State', () => {
|
|
|
14
14
|
|
|
15
15
|
describe('Boolean cause', () => {
|
|
16
16
|
test('should be boolean', () => {
|
|
17
|
-
const cause =
|
|
17
|
+
const cause = createState(false)
|
|
18
18
|
expect(typeof cause.get()).toBe('boolean')
|
|
19
19
|
})
|
|
20
20
|
|
|
21
21
|
test('should set initial value to false', () => {
|
|
22
|
-
const cause =
|
|
22
|
+
const cause = createState(false)
|
|
23
23
|
expect(cause.get()).toBe(false)
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
test('should set initial value to true', () => {
|
|
27
|
-
const cause =
|
|
27
|
+
const cause = createState(true)
|
|
28
28
|
expect(cause.get()).toBe(true)
|
|
29
29
|
})
|
|
30
30
|
|
|
31
31
|
test('should set new value with .set(true)', () => {
|
|
32
|
-
const cause =
|
|
32
|
+
const cause = createState(false)
|
|
33
33
|
cause.set(true)
|
|
34
34
|
expect(cause.get()).toBe(true)
|
|
35
35
|
})
|
|
36
36
|
|
|
37
37
|
test('should toggle initial value with .set(v => !v)', () => {
|
|
38
|
-
const cause =
|
|
38
|
+
const cause = createState(false)
|
|
39
39
|
cause.update(v => !v)
|
|
40
40
|
expect(cause.get()).toBe(true)
|
|
41
41
|
})
|
|
@@ -43,23 +43,23 @@ describe('State', () => {
|
|
|
43
43
|
|
|
44
44
|
describe('Number cause', () => {
|
|
45
45
|
test('should be number', () => {
|
|
46
|
-
const cause =
|
|
46
|
+
const cause = createState(0)
|
|
47
47
|
expect(typeof cause.get()).toBe('number')
|
|
48
48
|
})
|
|
49
49
|
|
|
50
50
|
test('should set initial value to 0', () => {
|
|
51
|
-
const cause =
|
|
51
|
+
const cause = createState(0)
|
|
52
52
|
expect(cause.get()).toBe(0)
|
|
53
53
|
})
|
|
54
54
|
|
|
55
55
|
test('should set new value with .set(42)', () => {
|
|
56
|
-
const cause =
|
|
56
|
+
const cause = createState(0)
|
|
57
57
|
cause.set(42)
|
|
58
58
|
expect(cause.get()).toBe(42)
|
|
59
59
|
})
|
|
60
60
|
|
|
61
61
|
test('should increment value with .set(v => ++v)', () => {
|
|
62
|
-
const cause =
|
|
62
|
+
const cause = createState(0)
|
|
63
63
|
cause.update(v => ++v)
|
|
64
64
|
expect(cause.get()).toBe(1)
|
|
65
65
|
})
|
|
@@ -67,23 +67,23 @@ describe('State', () => {
|
|
|
67
67
|
|
|
68
68
|
describe('String cause', () => {
|
|
69
69
|
test('should be string', () => {
|
|
70
|
-
const cause =
|
|
70
|
+
const cause = createState('foo')
|
|
71
71
|
expect(typeof cause.get()).toBe('string')
|
|
72
72
|
})
|
|
73
73
|
|
|
74
74
|
test('should set initial value to "foo"', () => {
|
|
75
|
-
const cause =
|
|
75
|
+
const cause = createState('foo')
|
|
76
76
|
expect(cause.get()).toBe('foo')
|
|
77
77
|
})
|
|
78
78
|
|
|
79
79
|
test('should set new value with .set("bar")', () => {
|
|
80
|
-
const cause =
|
|
80
|
+
const cause = createState('foo')
|
|
81
81
|
cause.set('bar')
|
|
82
82
|
expect(cause.get()).toBe('bar')
|
|
83
83
|
})
|
|
84
84
|
|
|
85
85
|
test('should upper case value with .set(v => v.toUpperCase())', () => {
|
|
86
|
-
const cause =
|
|
86
|
+
const cause = createState('foo')
|
|
87
87
|
cause.update(v => (v ? v.toUpperCase() : ''))
|
|
88
88
|
expect(cause.get()).toBe('FOO')
|
|
89
89
|
})
|
|
@@ -91,56 +91,243 @@ describe('State', () => {
|
|
|
91
91
|
|
|
92
92
|
describe('Array cause', () => {
|
|
93
93
|
test('should be array', () => {
|
|
94
|
-
const cause =
|
|
94
|
+
const cause = createState([1, 2, 3])
|
|
95
95
|
expect(Array.isArray(cause.get())).toBe(true)
|
|
96
96
|
})
|
|
97
97
|
|
|
98
98
|
test('should set initial value to [1, 2, 3]', () => {
|
|
99
|
-
const cause =
|
|
99
|
+
const cause = createState([1, 2, 3])
|
|
100
100
|
expect(cause.get()).toEqual([1, 2, 3])
|
|
101
101
|
})
|
|
102
102
|
|
|
103
103
|
test('should set new value with .set([4, 5, 6])', () => {
|
|
104
|
-
const cause =
|
|
104
|
+
const cause = createState([1, 2, 3])
|
|
105
105
|
cause.set([4, 5, 6])
|
|
106
106
|
expect(cause.get()).toEqual([4, 5, 6])
|
|
107
107
|
})
|
|
108
108
|
|
|
109
109
|
test('should reflect current value of array after modification', () => {
|
|
110
110
|
const array = [1, 2, 3]
|
|
111
|
-
const cause =
|
|
111
|
+
const cause = createState(array)
|
|
112
112
|
array.push(4) // don't do this! the result will be correct, but we can't trigger effects
|
|
113
113
|
expect(cause.get()).toEqual([1, 2, 3, 4])
|
|
114
114
|
})
|
|
115
115
|
|
|
116
116
|
test('should set new value with .set([...array, 4])', () => {
|
|
117
117
|
const array = [1, 2, 3]
|
|
118
|
-
const cause =
|
|
118
|
+
const cause = createState(array)
|
|
119
119
|
cause.set([...array, 4]) // use destructuring instead!
|
|
120
120
|
expect(cause.get()).toEqual([1, 2, 3, 4])
|
|
121
121
|
})
|
|
122
|
+
|
|
123
|
+
describe('Input Validation', () => {
|
|
124
|
+
test('should throw NullishSignalValueError when initialValue is nullish', () => {
|
|
125
|
+
expect(() => {
|
|
126
|
+
// @ts-expect-error - Testing invalid input
|
|
127
|
+
createState(null)
|
|
128
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
129
|
+
|
|
130
|
+
expect(() => {
|
|
131
|
+
// @ts-expect-error - Testing invalid input
|
|
132
|
+
createState(undefined)
|
|
133
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
134
|
+
})
|
|
135
|
+
|
|
136
|
+
test('should throw NullishSignalValueError when newValue is nullish in set()', () => {
|
|
137
|
+
const state = createState(42)
|
|
138
|
+
|
|
139
|
+
expect(() => {
|
|
140
|
+
// @ts-expect-error - Testing invalid input
|
|
141
|
+
state.set(null)
|
|
142
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
143
|
+
|
|
144
|
+
expect(() => {
|
|
145
|
+
// @ts-expect-error - Testing invalid input
|
|
146
|
+
state.set(undefined)
|
|
147
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
148
|
+
})
|
|
149
|
+
|
|
150
|
+
test('should throw specific error types for nullish values', () => {
|
|
151
|
+
try {
|
|
152
|
+
// @ts-expect-error - Testing invalid input
|
|
153
|
+
createState(null)
|
|
154
|
+
expect(true).toBe(false) // Should not reach here
|
|
155
|
+
} catch (error) {
|
|
156
|
+
expect(error).toBeInstanceOf(TypeError)
|
|
157
|
+
expect(error.name).toBe('NullishSignalValueError')
|
|
158
|
+
expect(error.message).toBe(
|
|
159
|
+
'Nullish signal values are not allowed in state',
|
|
160
|
+
)
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const state = createState(42)
|
|
164
|
+
try {
|
|
165
|
+
// @ts-expect-error - Testing invalid input
|
|
166
|
+
state.set(null)
|
|
167
|
+
expect(true).toBe(false) // Should not reach here
|
|
168
|
+
} catch (error) {
|
|
169
|
+
expect(error).toBeInstanceOf(TypeError)
|
|
170
|
+
expect(error.name).toBe('NullishSignalValueError')
|
|
171
|
+
expect(error.message).toBe(
|
|
172
|
+
'Nullish signal values are not allowed in state',
|
|
173
|
+
)
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
|
|
177
|
+
test('should allow valid non-nullish values', () => {
|
|
178
|
+
// These should not throw
|
|
179
|
+
expect(() => {
|
|
180
|
+
createState(0)
|
|
181
|
+
}).not.toThrow()
|
|
182
|
+
|
|
183
|
+
expect(() => {
|
|
184
|
+
createState('')
|
|
185
|
+
}).not.toThrow()
|
|
186
|
+
|
|
187
|
+
expect(() => {
|
|
188
|
+
createState(false)
|
|
189
|
+
}).not.toThrow()
|
|
190
|
+
|
|
191
|
+
expect(() => {
|
|
192
|
+
createState({})
|
|
193
|
+
}).not.toThrow()
|
|
194
|
+
|
|
195
|
+
expect(() => {
|
|
196
|
+
createState([])
|
|
197
|
+
}).not.toThrow()
|
|
198
|
+
|
|
199
|
+
const state = createState(42)
|
|
200
|
+
expect(() => {
|
|
201
|
+
state.set(0)
|
|
202
|
+
}).not.toThrow()
|
|
203
|
+
|
|
204
|
+
expect(() => {
|
|
205
|
+
// @ts-expect-error - Testing valid input of invalid type
|
|
206
|
+
state.set('')
|
|
207
|
+
}).not.toThrow()
|
|
208
|
+
})
|
|
209
|
+
|
|
210
|
+
test('should throw InvalidCallbackError for non-function updater in update()', () => {
|
|
211
|
+
const state = createState(42)
|
|
212
|
+
|
|
213
|
+
expect(() => {
|
|
214
|
+
// @ts-expect-error - Testing invalid input
|
|
215
|
+
state.update(null)
|
|
216
|
+
}).toThrow('Invalid state update callback null')
|
|
217
|
+
|
|
218
|
+
expect(() => {
|
|
219
|
+
// @ts-expect-error - Testing invalid input
|
|
220
|
+
state.update(undefined)
|
|
221
|
+
}).toThrow('Invalid state update callback undefined')
|
|
222
|
+
|
|
223
|
+
expect(() => {
|
|
224
|
+
// @ts-expect-error - Testing invalid input
|
|
225
|
+
state.update('not a function')
|
|
226
|
+
}).toThrow('Invalid state update callback "not a function"')
|
|
227
|
+
|
|
228
|
+
expect(() => {
|
|
229
|
+
// @ts-expect-error - Testing invalid input
|
|
230
|
+
state.update(42)
|
|
231
|
+
}).toThrow('Invalid state update callback 42')
|
|
232
|
+
})
|
|
233
|
+
|
|
234
|
+
test('should throw specific error type for non-function updater', () => {
|
|
235
|
+
const state = createState(42)
|
|
236
|
+
|
|
237
|
+
try {
|
|
238
|
+
// @ts-expect-error - Testing invalid input
|
|
239
|
+
state.update(null)
|
|
240
|
+
expect(true).toBe(false) // Should not reach here
|
|
241
|
+
} catch (error) {
|
|
242
|
+
expect(error).toBeInstanceOf(TypeError)
|
|
243
|
+
expect(error.name).toBe('InvalidCallbackError')
|
|
244
|
+
expect(error.message).toBe(
|
|
245
|
+
'Invalid state update callback null',
|
|
246
|
+
)
|
|
247
|
+
}
|
|
248
|
+
})
|
|
249
|
+
|
|
250
|
+
test('should handle updater function that throws an error', () => {
|
|
251
|
+
const state = createState(42)
|
|
252
|
+
|
|
253
|
+
expect(() => {
|
|
254
|
+
state.update(() => {
|
|
255
|
+
throw new Error('Updater error')
|
|
256
|
+
})
|
|
257
|
+
}).toThrow('Updater error')
|
|
258
|
+
|
|
259
|
+
// State should remain unchanged after error
|
|
260
|
+
expect(state.get()).toBe(42)
|
|
261
|
+
})
|
|
262
|
+
|
|
263
|
+
test('should handle updater function that returns nullish value', () => {
|
|
264
|
+
const state = createState(42)
|
|
265
|
+
|
|
266
|
+
expect(() => {
|
|
267
|
+
// @ts-expect-error - Testing invalid return value
|
|
268
|
+
state.update(() => null)
|
|
269
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
270
|
+
|
|
271
|
+
expect(() => {
|
|
272
|
+
// @ts-expect-error - Testing invalid return value
|
|
273
|
+
state.update(() => undefined)
|
|
274
|
+
}).toThrow('Nullish signal values are not allowed in state')
|
|
275
|
+
|
|
276
|
+
// State should remain unchanged after error
|
|
277
|
+
expect(state.get()).toBe(42)
|
|
278
|
+
})
|
|
279
|
+
|
|
280
|
+
test('should handle valid updater functions', () => {
|
|
281
|
+
const numberState = createState(10)
|
|
282
|
+
expect(() => {
|
|
283
|
+
numberState.update(x => x + 5)
|
|
284
|
+
}).not.toThrow()
|
|
285
|
+
expect(numberState.get()).toBe(15)
|
|
286
|
+
|
|
287
|
+
const stringState = createState('hello')
|
|
288
|
+
expect(() => {
|
|
289
|
+
stringState.update(x => x.toUpperCase())
|
|
290
|
+
}).not.toThrow()
|
|
291
|
+
expect(stringState.get()).toBe('HELLO')
|
|
292
|
+
|
|
293
|
+
const arrayState = createState([1, 2, 3])
|
|
294
|
+
expect(() => {
|
|
295
|
+
arrayState.update(arr => [...arr, 4])
|
|
296
|
+
}).not.toThrow()
|
|
297
|
+
expect(arrayState.get()).toEqual([1, 2, 3, 4])
|
|
298
|
+
|
|
299
|
+
const objectState = createState({ count: 0 })
|
|
300
|
+
expect(() => {
|
|
301
|
+
objectState.update(obj => ({
|
|
302
|
+
...obj,
|
|
303
|
+
count: obj.count + 1,
|
|
304
|
+
}))
|
|
305
|
+
}).not.toThrow()
|
|
306
|
+
expect(objectState.get()).toEqual({ count: 1 })
|
|
307
|
+
})
|
|
308
|
+
})
|
|
122
309
|
})
|
|
123
310
|
|
|
124
311
|
describe('Object cause', () => {
|
|
125
312
|
test('should be object', () => {
|
|
126
|
-
const cause =
|
|
313
|
+
const cause = createState({ a: 'a', b: 1 })
|
|
127
314
|
expect(typeof cause.get()).toBe('object')
|
|
128
315
|
})
|
|
129
316
|
|
|
130
317
|
test('should set initial value to { a: "a", b: 1 }', () => {
|
|
131
|
-
const cause =
|
|
318
|
+
const cause = createState({ a: 'a', b: 1 })
|
|
132
319
|
expect(cause.get()).toEqual({ a: 'a', b: 1 })
|
|
133
320
|
})
|
|
134
321
|
|
|
135
322
|
test('should set new value with .set({ c: true })', () => {
|
|
136
|
-
const cause =
|
|
323
|
+
const cause = createState<Record<string, unknown>>({ a: 'a', b: 1 })
|
|
137
324
|
cause.set({ c: true })
|
|
138
325
|
expect(cause.get()).toEqual({ c: true })
|
|
139
326
|
})
|
|
140
327
|
|
|
141
328
|
test('should reflect current value of object after modification', () => {
|
|
142
329
|
const obj = { a: 'a', b: 1 }
|
|
143
|
-
const cause =
|
|
330
|
+
const cause = createState<Record<string, unknown>>(obj)
|
|
144
331
|
// @ts-expect-error Property 'c' does not exist on type '{ a: string; b: number; }'. (ts 2339)
|
|
145
332
|
obj.c = true // don't do this! the result will be correct, but we can't trigger effects
|
|
146
333
|
expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
|
|
@@ -148,7 +335,7 @@ describe('State', () => {
|
|
|
148
335
|
|
|
149
336
|
test('should set new value with .set({...obj, c: true})', () => {
|
|
150
337
|
const obj = { a: 'a', b: 1 }
|
|
151
|
-
const cause =
|
|
338
|
+
const cause = createState<Record<string, unknown>>(obj)
|
|
152
339
|
cause.set({ ...obj, c: true }) // use destructuring instead!
|
|
153
340
|
expect(cause.get()).toEqual({ a: 'a', b: 1, c: true })
|
|
154
341
|
})
|