@zeix/cause-effect 0.16.1 → 0.17.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 +71 -21
- package/.cursorrules +3 -2
- package/.github/copilot-instructions.md +59 -13
- package/CLAUDE.md +170 -24
- package/LICENSE +1 -1
- package/README.md +156 -52
- package/archive/benchmark.ts +688 -0
- package/archive/collection.ts +312 -0
- package/{src → archive}/computed.ts +19 -19
- package/archive/list.ts +551 -0
- package/archive/memo.ts +138 -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 +899 -503
- package/index.js +1 -1
- package/index.ts +41 -22
- package/package.json +1 -1
- package/src/classes/collection.ts +272 -0
- package/src/classes/composite.ts +176 -0
- package/src/classes/computed.ts +333 -0
- package/src/classes/list.ts +304 -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 +50 -25
- package/src/signal.ts +58 -41
- package/src/system.ts +79 -42
- package/src/util.ts +16 -30
- package/test/batch.test.ts +15 -17
- package/test/benchmark.test.ts +4 -4
- package/test/collection.test.ts +796 -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/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 +10 -8
- package/types/src/classes/collection.d.ts +32 -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/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 +12 -11
- package/types/src/signal.d.ts +27 -14
- package/types/src/system.d.ts +41 -20
- package/types/src/util.d.ts +6 -3
- 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/match.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { Memo, match, resolve, State, Task, UNSET } from '../index.ts'
|
|
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 = new State(10)
|
|
9
|
+
const b = new State('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 = new State(10)
|
|
36
|
+
const b = new State(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 = new State(10)
|
|
56
|
+
const b = new Memo(() => {
|
|
57
57
|
throw new Error('Test error')
|
|
58
58
|
})
|
|
59
59
|
let errCalled = false
|
|
@@ -78,7 +78,7 @@ describe('Match Function', () => {
|
|
|
78
78
|
})
|
|
79
79
|
|
|
80
80
|
test('should handle missing optional handlers gracefully', () => {
|
|
81
|
-
const a =
|
|
81
|
+
const a = new State(10)
|
|
82
82
|
const result = resolve({ a })
|
|
83
83
|
|
|
84
84
|
// Should not throw even with only required ok handler (err and nil are optional)
|
|
@@ -92,7 +92,7 @@ describe('Match Function', () => {
|
|
|
92
92
|
})
|
|
93
93
|
|
|
94
94
|
test('should return void always', () => {
|
|
95
|
-
const a =
|
|
95
|
+
const a = new State(42)
|
|
96
96
|
|
|
97
97
|
const returnValue = match(resolve({ a }), {
|
|
98
98
|
ok: () => {
|
|
@@ -105,7 +105,7 @@ describe('Match Function', () => {
|
|
|
105
105
|
})
|
|
106
106
|
|
|
107
107
|
test('should handle handler errors by calling error handler', () => {
|
|
108
|
-
const a =
|
|
108
|
+
const a = new State(10)
|
|
109
109
|
let handlerErrorCalled = false
|
|
110
110
|
let handlerError: Error | null = null
|
|
111
111
|
|
|
@@ -125,7 +125,7 @@ describe('Match Function', () => {
|
|
|
125
125
|
})
|
|
126
126
|
|
|
127
127
|
test('should rethrow handler errors if no error handler available', () => {
|
|
128
|
-
const a =
|
|
128
|
+
const a = new State(10)
|
|
129
129
|
|
|
130
130
|
expect(() => {
|
|
131
131
|
match(resolve({ a }), {
|
|
@@ -137,7 +137,7 @@ describe('Match Function', () => {
|
|
|
137
137
|
})
|
|
138
138
|
|
|
139
139
|
test('should combine existing errors with handler errors', () => {
|
|
140
|
-
const a =
|
|
140
|
+
const a = new Memo(() => {
|
|
141
141
|
throw new Error('Signal error')
|
|
142
142
|
})
|
|
143
143
|
let allErrors: readonly Error[] | null = null
|
|
@@ -167,9 +167,9 @@ describe('Match Function', () => {
|
|
|
167
167
|
})
|
|
168
168
|
|
|
169
169
|
test('should work with complex type inference', () => {
|
|
170
|
-
const user =
|
|
171
|
-
const posts =
|
|
172
|
-
const settings =
|
|
170
|
+
const user = new State({ id: 1, name: 'Alice' })
|
|
171
|
+
const posts = new State([{ id: 1, title: 'Hello' }])
|
|
172
|
+
const settings = new State({ theme: 'dark' })
|
|
173
173
|
|
|
174
174
|
let typeTestPassed = false
|
|
175
175
|
|
|
@@ -194,8 +194,8 @@ describe('Match Function', () => {
|
|
|
194
194
|
})
|
|
195
195
|
|
|
196
196
|
test('should handle side effects only pattern', () => {
|
|
197
|
-
const count =
|
|
198
|
-
const name =
|
|
197
|
+
const count = new State(5)
|
|
198
|
+
const name = new State('test')
|
|
199
199
|
let sideEffectExecuted = false
|
|
200
200
|
let capturedData = ''
|
|
201
201
|
|
|
@@ -214,10 +214,10 @@ describe('Match Function', () => {
|
|
|
214
214
|
})
|
|
215
215
|
|
|
216
216
|
test('should handle multiple error types correctly', () => {
|
|
217
|
-
const error1 =
|
|
217
|
+
const error1 = new Memo(() => {
|
|
218
218
|
throw new Error('First error')
|
|
219
219
|
})
|
|
220
|
-
const error2 =
|
|
220
|
+
const error2 = new Memo(() => {
|
|
221
221
|
throw new Error('Second error')
|
|
222
222
|
})
|
|
223
223
|
let errorMessages: string[] = []
|
|
@@ -240,7 +240,7 @@ describe('Match Function', () => {
|
|
|
240
240
|
const wait = (ms: number) =>
|
|
241
241
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
242
242
|
|
|
243
|
-
const asyncSignal =
|
|
243
|
+
const asyncSignal = new Task(async () => {
|
|
244
244
|
await wait(10)
|
|
245
245
|
return 'async result'
|
|
246
246
|
})
|
|
@@ -280,7 +280,7 @@ describe('Match Function', () => {
|
|
|
280
280
|
})
|
|
281
281
|
|
|
282
282
|
test('should maintain referential transparency', () => {
|
|
283
|
-
const a =
|
|
283
|
+
const a = new State(42)
|
|
284
284
|
const result = resolve({ a })
|
|
285
285
|
let callCount = 0
|
|
286
286
|
|
|
@@ -303,7 +303,7 @@ describe('Match Function', () => {
|
|
|
303
303
|
|
|
304
304
|
describe('Match Function Integration', () => {
|
|
305
305
|
test('should work seamlessly with resolve', () => {
|
|
306
|
-
const data =
|
|
306
|
+
const data = new State({ id: 1, value: 'test' })
|
|
307
307
|
let processed = false
|
|
308
308
|
let processedValue = ''
|
|
309
309
|
|
|
@@ -322,12 +322,12 @@ describe('Match Function Integration', () => {
|
|
|
322
322
|
const wait = (ms: number) =>
|
|
323
323
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
324
324
|
|
|
325
|
-
const syncData =
|
|
326
|
-
const asyncData =
|
|
325
|
+
const syncData = new State('available')
|
|
326
|
+
const asyncData = new Task(async () => {
|
|
327
327
|
await wait(10)
|
|
328
328
|
return 'loaded'
|
|
329
329
|
})
|
|
330
|
-
const errorData =
|
|
330
|
+
const errorData = new Memo(() => {
|
|
331
331
|
throw new Error('Failed to load')
|
|
332
332
|
})
|
|
333
333
|
|
package/test/resolve.test.ts
CHANGED
|
@@ -1,12 +1,12 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
|
-
import {
|
|
2
|
+
import { Memo, resolve, State, Task, UNSET } from '../index.ts'
|
|
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 = new State(10)
|
|
9
|
+
const b = new State('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 = new State(10)
|
|
24
|
+
const b = new State(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 = new State(10)
|
|
36
|
+
const b = new Memo(() => {
|
|
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 = new State('valid')
|
|
53
|
+
const error1 = new Memo(() => {
|
|
54
54
|
throw new Error('Error 1')
|
|
55
55
|
})
|
|
56
|
-
const error2 =
|
|
56
|
+
const error2 = new Memo(() => {
|
|
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 = new State(UNSET)
|
|
73
|
+
const error = new Memo(() => {
|
|
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 = new State({ name: 'Alice', age: 25 })
|
|
95
|
+
const settings = new State({ 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 = new Task(async () => {
|
|
113
113
|
await wait(10)
|
|
114
114
|
return 'async result'
|
|
115
115
|
})
|
|
@@ -124,16 +124,14 @@ describe('Resolve Function', () => {
|
|
|
124
124
|
|
|
125
125
|
result = resolve({ asyncSignal })
|
|
126
126
|
expect(result.ok).toBe(true)
|
|
127
|
-
if (result.ok)
|
|
128
|
-
expect(result.values.asyncSignal).toBe('async result')
|
|
129
|
-
}
|
|
127
|
+
if (result.ok) expect(result.values.asyncSignal).toBe('async result')
|
|
130
128
|
})
|
|
131
129
|
|
|
132
130
|
test('should handle async computed signals that error', async () => {
|
|
133
131
|
const wait = (ms: number) =>
|
|
134
132
|
new Promise(resolve => setTimeout(resolve, ms))
|
|
135
133
|
|
|
136
|
-
const asyncError =
|
|
134
|
+
const asyncError = new Task(async () => {
|
|
137
135
|
await wait(10)
|
|
138
136
|
throw new Error('Async error')
|
|
139
137
|
})
|
package/test/signal.test.ts
CHANGED
|
@@ -1,51 +1,50 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
type Computed,
|
|
4
|
-
|
|
5
|
-
createState,
|
|
6
|
-
createStore,
|
|
4
|
+
createSignal,
|
|
7
5
|
isComputed,
|
|
6
|
+
isList,
|
|
8
7
|
isState,
|
|
9
8
|
isStore,
|
|
9
|
+
type List,
|
|
10
10
|
type Signal,
|
|
11
|
-
|
|
11
|
+
State,
|
|
12
12
|
type Store,
|
|
13
|
-
toSignal,
|
|
14
13
|
type UnknownRecord,
|
|
15
|
-
} from '
|
|
14
|
+
} from '../index.ts'
|
|
16
15
|
|
|
17
16
|
/* === Tests === */
|
|
18
17
|
|
|
19
|
-
describe('
|
|
18
|
+
describe('createSignal', () => {
|
|
20
19
|
describe('type inference and runtime behavior', () => {
|
|
21
|
-
test('converts array to
|
|
22
|
-
const result =
|
|
20
|
+
test('converts array to List<T>', () => {
|
|
21
|
+
const result = createSignal([
|
|
23
22
|
{ id: 1, name: 'Alice' },
|
|
24
23
|
{ id: 2, name: 'Bob' },
|
|
25
24
|
])
|
|
26
25
|
|
|
27
26
|
// Runtime behavior
|
|
28
|
-
expect(
|
|
29
|
-
expect(result
|
|
30
|
-
expect(result
|
|
27
|
+
expect(isList(result)).toBe(true)
|
|
28
|
+
expect(result.at(0)?.get()).toEqual({ id: 1, name: 'Alice' })
|
|
29
|
+
expect(result.at(1)?.get()).toEqual({ id: 2, name: 'Bob' })
|
|
31
30
|
|
|
32
|
-
// Type inference test - now correctly returns
|
|
33
|
-
const typedResult:
|
|
31
|
+
// Type inference test - now correctly returns List<{ id: number; name: string }>
|
|
32
|
+
const typedResult: List<{ id: number; name: string }> = result
|
|
34
33
|
expect(typedResult).toBeDefined()
|
|
35
34
|
})
|
|
36
35
|
|
|
37
36
|
test('converts empty array to ArrayStore<never[]>', () => {
|
|
38
|
-
const result =
|
|
37
|
+
const result = createSignal([])
|
|
39
38
|
|
|
40
39
|
// Runtime behavior
|
|
41
|
-
expect(
|
|
40
|
+
expect(isList(result)).toBe(true)
|
|
42
41
|
expect(result.length).toBe(0)
|
|
43
42
|
expect(Object.keys(result).length).toBe(0)
|
|
44
43
|
})
|
|
45
44
|
|
|
46
45
|
test('converts record to Store<T>', () => {
|
|
47
46
|
const record = { name: 'Alice', age: 30 }
|
|
48
|
-
const result =
|
|
47
|
+
const result = createSignal(record)
|
|
49
48
|
|
|
50
49
|
// Runtime behavior
|
|
51
50
|
expect(isStore(result)).toBe(true)
|
|
@@ -57,51 +56,9 @@ describe('toSignal', () => {
|
|
|
57
56
|
expect(typedResult).toBeDefined()
|
|
58
57
|
})
|
|
59
58
|
|
|
60
|
-
test('passes through existing Store unchanged', () => {
|
|
61
|
-
const originalStore = createStore({ count: 5 })
|
|
62
|
-
const result = toSignal(originalStore)
|
|
63
|
-
|
|
64
|
-
// Runtime behavior
|
|
65
|
-
expect(result).toBe(originalStore) // Should be the same instance
|
|
66
|
-
expect(isStore(result)).toBe(true)
|
|
67
|
-
expect(result.count.get()).toBe(5)
|
|
68
|
-
|
|
69
|
-
// Type inference test - should remain Store<{count: number}>
|
|
70
|
-
const typedResult: Store<{ count: number }> = result
|
|
71
|
-
expect(typedResult).toBeDefined()
|
|
72
|
-
})
|
|
73
|
-
|
|
74
|
-
test('passes through existing State unchanged', () => {
|
|
75
|
-
const originalState = createState(42)
|
|
76
|
-
const result = toSignal(originalState)
|
|
77
|
-
|
|
78
|
-
// Runtime behavior
|
|
79
|
-
expect(result).toBe(originalState) // Should be the same instance
|
|
80
|
-
expect(isState(result)).toBe(true)
|
|
81
|
-
expect(result.get()).toBe(42)
|
|
82
|
-
|
|
83
|
-
// Type inference test - should remain State<number>
|
|
84
|
-
const typedResult: State<number> = result
|
|
85
|
-
expect(typedResult).toBeDefined()
|
|
86
|
-
})
|
|
87
|
-
|
|
88
|
-
test('passes through existing Computed unchanged', () => {
|
|
89
|
-
const originalComputed = createComputed(() => 'hello world')
|
|
90
|
-
const result = toSignal(originalComputed)
|
|
91
|
-
|
|
92
|
-
// Runtime behavior
|
|
93
|
-
expect(result).toBe(originalComputed) // Should be the same instance
|
|
94
|
-
expect(isComputed(result)).toBe(true)
|
|
95
|
-
expect(result.get()).toBe('hello world')
|
|
96
|
-
|
|
97
|
-
// Type inference test - should remain Computed<string>
|
|
98
|
-
const typedResult: Computed<string> = result
|
|
99
|
-
expect(typedResult).toBeDefined()
|
|
100
|
-
})
|
|
101
|
-
|
|
102
59
|
test('converts function to Computed<T>', () => {
|
|
103
60
|
const fn = () => Math.random()
|
|
104
|
-
const result =
|
|
61
|
+
const result = createSignal(fn)
|
|
105
62
|
|
|
106
63
|
// Runtime behavior - functions are correctly converted to Computed
|
|
107
64
|
expect(isComputed(result)).toBe(true)
|
|
@@ -114,7 +71,7 @@ describe('toSignal', () => {
|
|
|
114
71
|
|
|
115
72
|
test('converts primitive to State<T>', () => {
|
|
116
73
|
const num = 42
|
|
117
|
-
const result =
|
|
74
|
+
const result = createSignal(num)
|
|
118
75
|
|
|
119
76
|
// Runtime behavior - primitives are correctly converted to State
|
|
120
77
|
expect(isState(result)).toBe(true)
|
|
@@ -127,7 +84,7 @@ describe('toSignal', () => {
|
|
|
127
84
|
|
|
128
85
|
test('converts object to State<T>', () => {
|
|
129
86
|
const obj = new Date('2024-01-01')
|
|
130
|
-
const result =
|
|
87
|
+
const result = createSignal(obj)
|
|
131
88
|
|
|
132
89
|
// Runtime behavior - objects are correctly converted to State
|
|
133
90
|
expect(isState(result)).toBe(true)
|
|
@@ -141,15 +98,15 @@ describe('toSignal', () => {
|
|
|
141
98
|
|
|
142
99
|
describe('edge cases', () => {
|
|
143
100
|
test('handles nested arrays', () => {
|
|
144
|
-
const result =
|
|
101
|
+
const result = createSignal([
|
|
145
102
|
[1, 2],
|
|
146
103
|
[3, 4],
|
|
147
104
|
])
|
|
148
105
|
|
|
149
|
-
expect(
|
|
106
|
+
expect(isList(result)).toBe(true)
|
|
150
107
|
// With the fixed behavior, nested arrays should be recovered as arrays
|
|
151
|
-
const firstElement = result
|
|
152
|
-
const secondElement = result
|
|
108
|
+
const firstElement = result.at(0)?.get()
|
|
109
|
+
const secondElement = result.at(1)?.get()
|
|
153
110
|
|
|
154
111
|
// The expected behavior - nested arrays are recovered as arrays
|
|
155
112
|
expect(firstElement).toEqual([1, 2])
|
|
@@ -158,34 +115,23 @@ describe('toSignal', () => {
|
|
|
158
115
|
|
|
159
116
|
test('handles arrays with mixed types', () => {
|
|
160
117
|
const mixedArr = [1, 'hello', { key: 'value' }]
|
|
161
|
-
const result =
|
|
162
|
-
|
|
163
|
-
expect(isStore(result)).toBe(true)
|
|
164
|
-
expect(result['0'].get()).toBe(1)
|
|
165
|
-
expect(result['1'].get()).toBe('hello')
|
|
166
|
-
expect(result['2'].get()).toEqual({ key: 'value' })
|
|
167
|
-
})
|
|
168
|
-
|
|
169
|
-
test('handles sparse arrays', () => {
|
|
170
|
-
const sparseArr = new Array(3)
|
|
171
|
-
sparseArr[1] = 'middle'
|
|
172
|
-
const result = toSignal(sparseArr)
|
|
118
|
+
const result = createSignal(mixedArr)
|
|
173
119
|
|
|
174
|
-
expect(
|
|
175
|
-
expect(
|
|
176
|
-
expect(result
|
|
177
|
-
expect(
|
|
120
|
+
expect(isList(result)).toBe(true)
|
|
121
|
+
expect(result.at(0)?.get()).toBe(1)
|
|
122
|
+
expect(result.at(1)?.get()).toBe('hello')
|
|
123
|
+
expect(result.at(2)?.get()).toEqual({ key: 'value' })
|
|
178
124
|
})
|
|
179
125
|
})
|
|
180
126
|
})
|
|
181
127
|
|
|
182
128
|
describe('Signal compatibility', () => {
|
|
183
129
|
test('all results implement Signal<T> interface', () => {
|
|
184
|
-
const arraySignal =
|
|
185
|
-
const recordSignal =
|
|
186
|
-
const primitiveSignal =
|
|
187
|
-
const functionSignal =
|
|
188
|
-
const stateSignal =
|
|
130
|
+
const arraySignal = createSignal([1, 2, 3])
|
|
131
|
+
const recordSignal = createSignal({ a: 1, b: 2 })
|
|
132
|
+
const primitiveSignal = createSignal(42)
|
|
133
|
+
const functionSignal = createSignal(() => 'hello')
|
|
134
|
+
const stateSignal = createSignal(new State(true))
|
|
189
135
|
|
|
190
136
|
// All should have get() method
|
|
191
137
|
expect(typeof arraySignal.get).toBe('function')
|
|
@@ -210,16 +156,16 @@ describe('Type precision tests', () => {
|
|
|
210
156
|
test('array type should infer element type correctly', () => {
|
|
211
157
|
// Test that arrays infer the correct element type
|
|
212
158
|
const stringArray = ['a', 'b', 'c']
|
|
213
|
-
const stringArraySignal =
|
|
159
|
+
const stringArraySignal = createSignal(stringArray)
|
|
214
160
|
|
|
215
|
-
// Should be
|
|
216
|
-
expect(stringArraySignal
|
|
161
|
+
// Should be List<string>
|
|
162
|
+
expect(stringArraySignal.at(0)?.get()).toBe('a')
|
|
217
163
|
|
|
218
164
|
const numberArray = [1, 2, 3]
|
|
219
|
-
const numberArraySignal =
|
|
165
|
+
const numberArraySignal = createSignal(numberArray)
|
|
220
166
|
|
|
221
|
-
// Should be
|
|
222
|
-
expect(typeof numberArraySignal
|
|
167
|
+
// Should be List<number>
|
|
168
|
+
expect(typeof numberArraySignal.at(0)?.get()).toBe('number')
|
|
223
169
|
})
|
|
224
170
|
|
|
225
171
|
test('complex object arrays maintain precise typing', () => {
|
|
@@ -234,31 +180,31 @@ describe('Type precision tests', () => {
|
|
|
234
180
|
{ id: 2, name: 'Bob', email: 'bob@example.com' },
|
|
235
181
|
]
|
|
236
182
|
|
|
237
|
-
const usersSignal =
|
|
183
|
+
const usersSignal = createSignal(users)
|
|
238
184
|
|
|
239
185
|
// Should maintain User type for each element
|
|
240
|
-
const firstUser = usersSignal
|
|
241
|
-
expect(firstUser
|
|
242
|
-
expect(firstUser
|
|
243
|
-
expect(firstUser
|
|
186
|
+
const firstUser = usersSignal.at(0)?.get()
|
|
187
|
+
expect(firstUser?.id).toBe(1)
|
|
188
|
+
expect(firstUser?.name).toBe('Alice')
|
|
189
|
+
expect(firstUser?.email).toBe('alice@example.com')
|
|
244
190
|
})
|
|
245
191
|
|
|
246
192
|
describe('Type inference issues', () => {
|
|
247
193
|
test('demonstrates current type inference problem', () => {
|
|
248
|
-
const result =
|
|
194
|
+
const result = createSignal([{ id: 1 }, { id: 2 }])
|
|
249
195
|
|
|
250
196
|
// Let's verify the actual behavior
|
|
251
|
-
expect(
|
|
252
|
-
expect(result
|
|
253
|
-
expect(result
|
|
197
|
+
expect(isList(result)).toBe(true)
|
|
198
|
+
expect(result.at(0)?.get()).toEqual({ id: 1 })
|
|
199
|
+
expect(result.at(1)?.get()).toEqual({ id: 2 })
|
|
254
200
|
|
|
255
201
|
// Type assertion test - this should now work with correct typing
|
|
256
|
-
const typedResult:
|
|
202
|
+
const typedResult: List<{ id: number }> = result
|
|
257
203
|
expect(typedResult).toBeDefined()
|
|
258
204
|
|
|
259
205
|
// Simulate external library usage where P[K] represents element type
|
|
260
206
|
interface ExternalLibraryConstraint<P extends UnknownRecord> {
|
|
261
|
-
process<K extends keyof P>(signal: Signal<P[K]>): void
|
|
207
|
+
process<K extends keyof P>(signal: Signal<P[K] & {}>): void
|
|
262
208
|
}
|
|
263
209
|
|
|
264
210
|
// This should work if types are correct
|
|
@@ -266,7 +212,7 @@ describe('Type precision tests', () => {
|
|
|
266
212
|
Record<string, { id: number }>
|
|
267
213
|
> = {
|
|
268
214
|
process: <K extends keyof Record<string, { id: number }>>(
|
|
269
|
-
signal: Signal<Record<string, { id: number }>[K]>,
|
|
215
|
+
signal: Signal<Record<string, { id: number }>[K] & {}>,
|
|
270
216
|
) => {
|
|
271
217
|
// Process the signal
|
|
272
218
|
const value = signal.get()
|
|
@@ -275,7 +221,8 @@ describe('Type precision tests', () => {
|
|
|
275
221
|
}
|
|
276
222
|
|
|
277
223
|
// This call should work without type errors
|
|
278
|
-
|
|
224
|
+
const item = result.at(0)
|
|
225
|
+
if (item) processor.process(item)
|
|
279
226
|
})
|
|
280
227
|
|
|
281
228
|
test('verifies fixed type inference for external library compatibility', () => {
|
|
@@ -283,23 +230,26 @@ describe('Type precision tests', () => {
|
|
|
283
230
|
{ id: 1, name: 'Alice' },
|
|
284
231
|
{ id: 2, name: 'Bob' },
|
|
285
232
|
]
|
|
286
|
-
const signal =
|
|
287
|
-
const firstItemSignal = signal
|
|
288
|
-
const secondItemSignal = signal
|
|
233
|
+
const signal = createSignal(items)
|
|
234
|
+
const firstItemSignal = signal.at(0)
|
|
235
|
+
const secondItemSignal = signal.at(1)
|
|
289
236
|
|
|
290
237
|
// Runtime behavior works correctly
|
|
291
|
-
expect(
|
|
292
|
-
expect(firstItemSignal
|
|
293
|
-
expect(secondItemSignal
|
|
238
|
+
expect(isList(signal)).toBe(true)
|
|
239
|
+
expect(firstItemSignal?.get()).toEqual({ id: 1, name: 'Alice' })
|
|
240
|
+
expect(secondItemSignal?.get()).toEqual({ id: 2, name: 'Bob' })
|
|
294
241
|
|
|
295
242
|
// Type inference should now work correctly:
|
|
296
|
-
const properlyTyped:
|
|
243
|
+
const properlyTyped: List<{ id: number; name: string }> = signal
|
|
297
244
|
expect(properlyTyped).toBeDefined()
|
|
298
245
|
|
|
299
246
|
// These should work without type errors in external libraries
|
|
300
247
|
// that expect Signal<P[K]> where P[K] is the individual element type
|
|
301
|
-
interface ExternalAPI<P extends
|
|
302
|
-
process<K extends keyof P>(
|
|
248
|
+
interface ExternalAPI<P extends Record<string, object>> {
|
|
249
|
+
process<K extends keyof P>(
|
|
250
|
+
key: K,
|
|
251
|
+
signal: Signal<P[K] & object>,
|
|
252
|
+
): P[K]
|
|
303
253
|
}
|
|
304
254
|
|
|
305
255
|
const api: ExternalAPI<
|
|
@@ -309,15 +259,16 @@ describe('Type precision tests', () => {
|
|
|
309
259
|
}
|
|
310
260
|
|
|
311
261
|
// These calls should work with proper typing now
|
|
312
|
-
const result1 = api.process('0', firstItemSignal)
|
|
313
|
-
const result2 =
|
|
262
|
+
const result1 = firstItemSignal && api.process('0', firstItemSignal)
|
|
263
|
+
const result2 =
|
|
264
|
+
secondItemSignal && api.process('1', secondItemSignal)
|
|
314
265
|
|
|
315
266
|
expect(result1).toEqual({ id: 1, name: 'Alice' })
|
|
316
267
|
expect(result2).toEqual({ id: 2, name: 'Bob' })
|
|
317
268
|
|
|
318
269
|
// Verify the types are precise
|
|
319
|
-
expect(typeof result1
|
|
320
|
-
expect(typeof result1
|
|
270
|
+
expect(typeof result1?.id).toBe('number')
|
|
271
|
+
expect(typeof result1?.name).toBe('string')
|
|
321
272
|
})
|
|
322
273
|
})
|
|
323
274
|
})
|