@zeix/cause-effect 0.17.3 → 0.18.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 +169 -227
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +176 -116
- package/ARCHITECTURE.md +276 -0
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +201 -143
- package/GUIDE.md +298 -0
- package/README.md +246 -193
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +1390 -1008
- package/index.js +1 -1
- package/index.ts +60 -74
- package/package.json +5 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/errors.ts +118 -74
- package/src/graph.ts +612 -0
- package/src/nodes/collection.ts +512 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +589 -0
- package/src/nodes/memo.ts +148 -0
- package/src/nodes/sensor.ts +149 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +378 -0
- package/src/nodes/task.ts +174 -0
- package/src/signal.ts +112 -66
- package/src/util.ts +26 -57
- package/test/batch.test.ts +96 -62
- package/test/benchmark.test.ts +473 -487
- package/test/collection.test.ts +456 -707
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +574 -0
- package/test/regression.test.ts +156 -0
- package/test/scope.test.ts +191 -0
- package/test/sensor.test.ts +454 -0
- package/test/signal.test.ts +220 -213
- package/test/state.test.ts +217 -265
- package/test/store.test.ts +346 -446
- package/test/task.test.ts +529 -0
- package/test/untrack.test.ts +167 -0
- package/types/index.d.ts +13 -15
- package/types/src/errors.d.ts +73 -17
- package/types/src/graph.d.ts +218 -0
- package/types/src/nodes/collection.d.ts +69 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +66 -0
- package/types/src/nodes/memo.d.ts +63 -0
- package/types/src/nodes/sensor.d.ts +81 -0
- package/types/src/nodes/state.d.ts +78 -0
- package/types/src/nodes/store.d.ts +51 -0
- package/types/src/nodes/task.d.ts +79 -0
- package/types/src/signal.d.ts +43 -29
- package/types/src/util.d.ts +9 -16
- package/archive/benchmark.ts +0 -683
- package/archive/collection.ts +0 -253
- package/archive/composite.ts +0 -85
- package/archive/computed.ts +0 -195
- package/archive/list.ts +0 -483
- package/archive/memo.ts +0 -139
- package/archive/state.ts +0 -90
- package/archive/store.ts +0 -298
- package/archive/task.ts +0 -189
- package/src/classes/collection.ts +0 -245
- package/src/classes/computed.ts +0 -349
- package/src/classes/list.ts +0 -343
- package/src/classes/ref.ts +0 -70
- package/src/classes/state.ts +0 -102
- package/src/classes/store.ts +0 -262
- package/src/diff.ts +0 -138
- package/src/effect.ts +0 -93
- package/src/match.ts +0 -45
- package/src/resolve.ts +0 -49
- package/src/system.ts +0 -257
- package/test/computed.test.ts +0 -1108
- package/test/diff.test.ts +0 -955
- package/test/match.test.ts +0 -388
- package/test/ref.test.ts +0 -353
- package/test/resolve.test.ts +0 -154
- package/types/src/classes/collection.d.ts +0 -45
- package/types/src/classes/computed.d.ts +0 -94
- package/types/src/classes/list.d.ts +0 -43
- package/types/src/classes/ref.d.ts +0 -35
- package/types/src/classes/state.d.ts +0 -49
- package/types/src/classes/store.d.ts +0 -52
- package/types/src/diff.d.ts +0 -28
- package/types/src/effect.d.ts +0 -15
- package/types/src/match.d.ts +0 -21
- package/types/src/resolve.d.ts +0 -29
- package/types/src/system.d.ts +0 -78
package/test/list.test.ts
CHANGED
|
@@ -1,714 +1,457 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
3
|
createEffect,
|
|
4
|
-
|
|
5
|
-
|
|
4
|
+
createList,
|
|
5
|
+
createMemo,
|
|
6
6
|
isList,
|
|
7
|
-
|
|
8
|
-
List,
|
|
9
|
-
Memo,
|
|
10
|
-
State,
|
|
11
|
-
UNSET,
|
|
7
|
+
isMemo,
|
|
12
8
|
} from '../index.ts'
|
|
13
9
|
|
|
14
|
-
describe('
|
|
15
|
-
describe('
|
|
16
|
-
test('
|
|
17
|
-
const
|
|
18
|
-
expect(
|
|
19
|
-
expect(numbers.at(1)?.get()).toBe(2)
|
|
20
|
-
expect(numbers.at(2)?.get()).toBe(3)
|
|
10
|
+
describe('List', () => {
|
|
11
|
+
describe('createList', () => {
|
|
12
|
+
test('should return initial values from get()', () => {
|
|
13
|
+
const list = createList([1, 2, 3])
|
|
14
|
+
expect(list.get()).toEqual([1, 2, 3])
|
|
21
15
|
})
|
|
22
16
|
|
|
23
|
-
test('
|
|
24
|
-
const list =
|
|
25
|
-
expect(list[Symbol.toStringTag]).toBe('List')
|
|
26
|
-
})
|
|
27
|
-
|
|
28
|
-
test('isList identifies list instances correctly', () => {
|
|
29
|
-
const store = createStore({ a: 1 })
|
|
30
|
-
const list = new List([1])
|
|
31
|
-
const state = new State(1)
|
|
32
|
-
const computed = new Memo(() => 1)
|
|
33
|
-
|
|
34
|
-
expect(isList(list)).toBe(true)
|
|
35
|
-
expect(isStore(store)).toBe(true)
|
|
36
|
-
expect(isList(state)).toBe(false)
|
|
37
|
-
expect(isList(computed)).toBe(false)
|
|
38
|
-
expect(isList({})).toBe(false)
|
|
39
|
-
})
|
|
40
|
-
|
|
41
|
-
test('get() returns the complete list value', () => {
|
|
42
|
-
const numbers = new List([1, 2, 3])
|
|
43
|
-
expect(numbers.get()).toEqual([1, 2, 3])
|
|
44
|
-
|
|
45
|
-
// Nested structures
|
|
46
|
-
const participants = new List([
|
|
17
|
+
test('should work with object items', () => {
|
|
18
|
+
const list = createList([
|
|
47
19
|
{ name: 'Alice', tags: ['admin'] },
|
|
48
20
|
{ name: 'Bob', tags: ['user'] },
|
|
49
21
|
])
|
|
50
|
-
expect(
|
|
22
|
+
expect(list.get()).toEqual([
|
|
51
23
|
{ name: 'Alice', tags: ['admin'] },
|
|
52
24
|
{ name: 'Bob', tags: ['user'] },
|
|
53
25
|
])
|
|
54
26
|
})
|
|
55
|
-
})
|
|
56
27
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
expect(
|
|
61
|
-
expect(typeof numbers.length).toBe('number')
|
|
28
|
+
test('should handle empty initial array', () => {
|
|
29
|
+
const list = createList<number>([])
|
|
30
|
+
expect(list.get()).toEqual([])
|
|
31
|
+
expect(list.length).toBe(0)
|
|
62
32
|
})
|
|
63
33
|
|
|
64
|
-
test('
|
|
65
|
-
const
|
|
66
|
-
expect(
|
|
67
|
-
items.add(3)
|
|
68
|
-
expect(items.length).toBe(3)
|
|
69
|
-
items.remove(1)
|
|
70
|
-
expect(items.length).toBe(2)
|
|
34
|
+
test('should have Symbol.toStringTag of "List"', () => {
|
|
35
|
+
const list = createList([1])
|
|
36
|
+
expect(list[Symbol.toStringTag]).toBe('List')
|
|
71
37
|
})
|
|
72
|
-
})
|
|
73
38
|
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
expect(items.at(0)?.get()).toBe('a')
|
|
78
|
-
expect(items.at(1)?.get()).toBe('b')
|
|
79
|
-
items.at(0)?.set('alpha')
|
|
80
|
-
items.at(1)?.set('beta')
|
|
81
|
-
expect(items.at(0)?.get()).toBe('alpha')
|
|
82
|
-
expect(items.at(1)?.get()).toBe('beta')
|
|
39
|
+
test('should have Symbol.isConcatSpreadable set to true', () => {
|
|
40
|
+
const list = createList([1])
|
|
41
|
+
expect(list[Symbol.isConcatSpreadable]).toBe(true)
|
|
83
42
|
})
|
|
43
|
+
})
|
|
84
44
|
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
expect(
|
|
45
|
+
describe('isList', () => {
|
|
46
|
+
test('should identify list signals', () => {
|
|
47
|
+
expect(isList(createList([1]))).toBe(true)
|
|
88
48
|
})
|
|
89
|
-
})
|
|
90
49
|
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
expect(
|
|
50
|
+
test('should return false for non-list values', () => {
|
|
51
|
+
expect(isList(42)).toBe(false)
|
|
52
|
+
expect(isList(null)).toBe(false)
|
|
53
|
+
expect(isList({})).toBe(false)
|
|
54
|
+
expect(isMemo(createList([1]))).toBe(false)
|
|
96
55
|
})
|
|
56
|
+
})
|
|
97
57
|
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(
|
|
102
|
-
expect(
|
|
58
|
+
describe('at', () => {
|
|
59
|
+
test('should return State signal at index', () => {
|
|
60
|
+
const list = createList(['a', 'b', 'c'])
|
|
61
|
+
expect(list.at(0)?.get()).toBe('a')
|
|
62
|
+
expect(list.at(1)?.get()).toBe('b')
|
|
63
|
+
expect(list.at(2)?.get()).toBe('c')
|
|
103
64
|
})
|
|
104
65
|
|
|
105
|
-
test('
|
|
106
|
-
const
|
|
107
|
-
|
|
108
|
-
expect(() => items.add(null)).toThrow()
|
|
66
|
+
test('should return undefined for out-of-bounds index', () => {
|
|
67
|
+
const list = createList(['a'])
|
|
68
|
+
expect(list.at(5)).toBeUndefined()
|
|
109
69
|
})
|
|
110
70
|
|
|
111
|
-
test('
|
|
112
|
-
const
|
|
113
|
-
|
|
114
|
-
expect(
|
|
71
|
+
test('should allow mutation via returned State signal', () => {
|
|
72
|
+
const list = createList(['a', 'b'])
|
|
73
|
+
list.at(0)?.set('alpha')
|
|
74
|
+
expect(list.at(0)?.get()).toBe('alpha')
|
|
115
75
|
})
|
|
116
76
|
})
|
|
117
77
|
|
|
118
|
-
describe('
|
|
119
|
-
test('
|
|
120
|
-
const
|
|
78
|
+
describe('set', () => {
|
|
79
|
+
test('should replace entire array', () => {
|
|
80
|
+
const list = createList([1, 2, 3])
|
|
81
|
+
list.set([4, 5])
|
|
82
|
+
expect(list.get()).toEqual([4, 5])
|
|
83
|
+
expect(list.length).toBe(2)
|
|
84
|
+
})
|
|
85
|
+
|
|
86
|
+
test('should diff and update changed items', () => {
|
|
87
|
+
const list = createList([1, 2, 3])
|
|
88
|
+
const signal0 = list.at(0)
|
|
89
|
+
list.set([10, 2, 3])
|
|
90
|
+
// Same signal reference, updated value
|
|
91
|
+
expect(signal0?.get()).toBe(10)
|
|
92
|
+
})
|
|
93
|
+
|
|
94
|
+
test('should keep stable keys when reordering with content-based keyConfig', () => {
|
|
95
|
+
type Item = { id: string; val: number }
|
|
96
|
+
const list = createList<Item>(
|
|
97
|
+
[
|
|
98
|
+
{ id: 'a', val: 1 },
|
|
99
|
+
{ id: 'b', val: 2 },
|
|
100
|
+
{ id: 'c', val: 3 },
|
|
101
|
+
],
|
|
102
|
+
{ keyConfig: item => item.id },
|
|
103
|
+
)
|
|
121
104
|
|
|
122
|
-
|
|
123
|
-
|
|
105
|
+
// Grab signal references by key before reorder
|
|
106
|
+
const signalA = list.byKey('a')
|
|
107
|
+
const signalB = list.byKey('b')
|
|
108
|
+
const signalC = list.byKey('c')
|
|
124
109
|
|
|
125
|
-
|
|
126
|
-
|
|
110
|
+
// Reverse order
|
|
111
|
+
list.set([
|
|
112
|
+
{ id: 'c', val: 3 },
|
|
113
|
+
{ id: 'b', val: 2 },
|
|
114
|
+
{ id: 'a', val: 1 },
|
|
115
|
+
])
|
|
127
116
|
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
expect(
|
|
131
|
-
|
|
117
|
+
// Keys should follow items, not positions
|
|
118
|
+
expect(list.byKey('a')?.get()).toEqual({ id: 'a', val: 1 })
|
|
119
|
+
expect(list.byKey('b')?.get()).toEqual({ id: 'b', val: 2 })
|
|
120
|
+
expect(list.byKey('c')?.get()).toEqual({ id: 'c', val: 3 })
|
|
132
121
|
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
createEffect(() => {
|
|
138
|
-
lastValue = numbers.get()
|
|
139
|
-
effectCount++
|
|
140
|
-
})
|
|
122
|
+
// Signal references should be preserved (same State objects)
|
|
123
|
+
expect(list.byKey('a')).toBe(signalA)
|
|
124
|
+
expect(list.byKey('b')).toBe(signalB)
|
|
125
|
+
expect(list.byKey('c')).toBe(signalC)
|
|
141
126
|
|
|
142
|
-
|
|
143
|
-
expect(
|
|
127
|
+
// Key order should match new array order
|
|
128
|
+
expect([...list.keys()]).toEqual(['c', 'b', 'a'])
|
|
129
|
+
})
|
|
144
130
|
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
131
|
+
test('should detect duplicates in set() with content-based keyConfig', () => {
|
|
132
|
+
const list = createList([{ id: 'a', val: 1 }], {
|
|
133
|
+
keyConfig: item => item.id,
|
|
134
|
+
})
|
|
135
|
+
expect(() =>
|
|
136
|
+
list.set([
|
|
137
|
+
{ id: 'a', val: 1 },
|
|
138
|
+
{ id: 'a', val: 2 },
|
|
139
|
+
]),
|
|
140
|
+
).toThrow('already exists')
|
|
148
141
|
})
|
|
149
142
|
})
|
|
150
143
|
|
|
151
|
-
describe('
|
|
152
|
-
test('
|
|
153
|
-
const
|
|
154
|
-
|
|
155
|
-
expect(
|
|
156
|
-
expect(numbers.get()).toEqual([1, 4])
|
|
144
|
+
describe('update', () => {
|
|
145
|
+
test('should update via callback', () => {
|
|
146
|
+
const list = createList([1, 2])
|
|
147
|
+
list.update(arr => [...arr, 3])
|
|
148
|
+
expect(list.get()).toEqual([1, 2, 3])
|
|
157
149
|
})
|
|
150
|
+
})
|
|
158
151
|
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const
|
|
162
|
-
|
|
163
|
-
expect(
|
|
152
|
+
describe('add', () => {
|
|
153
|
+
test('should append item and return key', () => {
|
|
154
|
+
const list = createList(['apple', 'banana'])
|
|
155
|
+
const key = list.add('cherry')
|
|
156
|
+
expect(typeof key).toBe('string')
|
|
157
|
+
expect(list.at(2)?.get()).toBe('cherry')
|
|
158
|
+
expect(list.byKey(key)?.get()).toBe('cherry')
|
|
164
159
|
})
|
|
165
160
|
|
|
166
|
-
test('
|
|
167
|
-
const
|
|
168
|
-
|
|
169
|
-
expect(
|
|
170
|
-
expect(numbers.get()).toEqual([1, 4, 5, 3])
|
|
161
|
+
test('should throw for null value', () => {
|
|
162
|
+
const list = createList([1])
|
|
163
|
+
// @ts-expect-error - Testing invalid input
|
|
164
|
+
expect(() => list.add(null)).toThrow()
|
|
171
165
|
})
|
|
172
166
|
|
|
173
|
-
test('
|
|
174
|
-
const
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
expect(
|
|
167
|
+
test('should throw DuplicateKeyError for duplicate keys', () => {
|
|
168
|
+
const list = createList([{ id: 'a', val: 1 }], {
|
|
169
|
+
keyConfig: item => item.id,
|
|
170
|
+
})
|
|
171
|
+
expect(() => list.add({ id: 'a', val: 2 })).toThrow(
|
|
172
|
+
'already exists',
|
|
173
|
+
)
|
|
178
174
|
})
|
|
179
175
|
})
|
|
180
176
|
|
|
181
|
-
describe('
|
|
182
|
-
test('
|
|
183
|
-
const
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
177
|
+
describe('remove', () => {
|
|
178
|
+
test('should remove by index', () => {
|
|
179
|
+
const list = createList(['a', 'b', 'c'])
|
|
180
|
+
list.remove(1)
|
|
181
|
+
expect(list.get()).toEqual(['a', 'c'])
|
|
182
|
+
expect(list.length).toBe(2)
|
|
183
|
+
})
|
|
184
|
+
|
|
185
|
+
test('should remove by key', () => {
|
|
186
|
+
const list = createList(
|
|
187
|
+
[
|
|
188
|
+
{ id: 'x', val: 1 },
|
|
189
|
+
{ id: 'y', val: 2 },
|
|
190
|
+
],
|
|
191
|
+
{ keyConfig: item => item.id },
|
|
192
|
+
)
|
|
193
|
+
list.remove('x')
|
|
194
|
+
expect(list.get()).toEqual([{ id: 'y', val: 2 }])
|
|
192
195
|
})
|
|
193
196
|
|
|
194
|
-
test('
|
|
195
|
-
const
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
itemEffectRuns++
|
|
201
|
-
})
|
|
197
|
+
test('should handle non-existent index gracefully', () => {
|
|
198
|
+
const list = createList(['a'])
|
|
199
|
+
expect(() => list.remove(5)).not.toThrow()
|
|
200
|
+
expect(list.get()).toEqual(['a'])
|
|
201
|
+
})
|
|
202
|
+
})
|
|
202
203
|
|
|
203
|
-
|
|
204
|
-
|
|
204
|
+
describe('sort', () => {
|
|
205
|
+
test('should sort with default string comparison', () => {
|
|
206
|
+
const list = createList([3, 1, 2])
|
|
207
|
+
list.sort()
|
|
208
|
+
expect(list.get()).toEqual([1, 2, 3])
|
|
209
|
+
})
|
|
205
210
|
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
211
|
+
test('should sort with custom compare function', () => {
|
|
212
|
+
const list = createList([3, 1, 2])
|
|
213
|
+
list.sort((a, b) => b - a)
|
|
214
|
+
expect(list.get()).toEqual([3, 2, 1])
|
|
209
215
|
})
|
|
210
216
|
|
|
211
|
-
test('
|
|
212
|
-
const
|
|
213
|
-
let
|
|
214
|
-
let
|
|
217
|
+
test('should trigger effects on sort', () => {
|
|
218
|
+
const list = createList([3, 1, 2])
|
|
219
|
+
let effectCount = 0
|
|
220
|
+
let lastValue: number[] = []
|
|
215
221
|
createEffect(() => {
|
|
216
|
-
|
|
217
|
-
|
|
222
|
+
lastValue = list.get()
|
|
223
|
+
effectCount++
|
|
218
224
|
})
|
|
219
225
|
|
|
220
|
-
expect(
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
expect(lastArray).toEqual([1, 2, 3])
|
|
225
|
-
expect(arrayEffectRuns).toBe(2)
|
|
226
|
+
expect(effectCount).toBe(1)
|
|
227
|
+
list.sort()
|
|
228
|
+
expect(effectCount).toBe(2)
|
|
229
|
+
expect(lastValue).toEqual([1, 2, 3])
|
|
226
230
|
})
|
|
227
231
|
})
|
|
228
232
|
|
|
229
|
-
describe('
|
|
230
|
-
test('
|
|
231
|
-
const
|
|
232
|
-
const
|
|
233
|
-
|
|
234
|
-
)
|
|
235
|
-
|
|
236
|
-
expect(sum.get()).toBe(6)
|
|
237
|
-
numbers.add(4)
|
|
238
|
-
expect(sum.get()).toBe(10)
|
|
233
|
+
describe('splice', () => {
|
|
234
|
+
test('should remove elements', () => {
|
|
235
|
+
const list = createList([1, 2, 3, 4])
|
|
236
|
+
const deleted = list.splice(1, 2)
|
|
237
|
+
expect(deleted).toEqual([2, 3])
|
|
238
|
+
expect(list.get()).toEqual([1, 4])
|
|
239
239
|
})
|
|
240
240
|
|
|
241
|
-
test('
|
|
242
|
-
const
|
|
243
|
-
const
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
})
|
|
247
|
-
|
|
248
|
-
expect(sum.get()).toBe(6)
|
|
249
|
-
|
|
250
|
-
numbers.add(4)
|
|
251
|
-
expect(sum.get()).toBe(10)
|
|
252
|
-
|
|
253
|
-
numbers.remove(0)
|
|
254
|
-
const finalArray = numbers.get()
|
|
255
|
-
expect(finalArray).toEqual([2, 3, 4])
|
|
256
|
-
expect(sum.get()).toBe(9)
|
|
241
|
+
test('should insert elements', () => {
|
|
242
|
+
const list = createList([1, 3])
|
|
243
|
+
const deleted = list.splice(1, 0, 2)
|
|
244
|
+
expect(deleted).toEqual([])
|
|
245
|
+
expect(list.get()).toEqual([1, 2, 3])
|
|
257
246
|
})
|
|
258
247
|
|
|
259
|
-
test('
|
|
260
|
-
const
|
|
248
|
+
test('should replace elements', () => {
|
|
249
|
+
const list = createList([1, 2, 3])
|
|
250
|
+
const deleted = list.splice(1, 1, 4, 5)
|
|
251
|
+
expect(deleted).toEqual([2])
|
|
252
|
+
expect(list.get()).toEqual([1, 4, 5, 3])
|
|
253
|
+
})
|
|
261
254
|
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
}
|
|
269
|
-
return total
|
|
270
|
-
})
|
|
255
|
+
test('should handle negative start index', () => {
|
|
256
|
+
const list = createList([1, 2, 3])
|
|
257
|
+
const deleted = list.splice(-1, 1, 4)
|
|
258
|
+
expect(deleted).toEqual([3])
|
|
259
|
+
expect(list.get()).toEqual([1, 2, 4])
|
|
260
|
+
})
|
|
271
261
|
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
262
|
+
test('should throw DuplicateKeyError for duplicate keys on insert', () => {
|
|
263
|
+
const list = createList(
|
|
264
|
+
[
|
|
265
|
+
{ id: 'a', val: 1 },
|
|
266
|
+
{ id: 'b', val: 2 },
|
|
267
|
+
],
|
|
268
|
+
{ keyConfig: item => item.id },
|
|
269
|
+
)
|
|
270
|
+
expect(() => list.splice(1, 0, { id: 'a', val: 3 })).toThrow(
|
|
271
|
+
'already exists',
|
|
272
|
+
)
|
|
275
273
|
})
|
|
276
274
|
})
|
|
277
275
|
|
|
278
|
-
describe('
|
|
279
|
-
test('
|
|
280
|
-
const
|
|
281
|
-
|
|
282
|
-
expect(signals).toHaveLength(3)
|
|
283
|
-
expect(signals[0].get()).toBe(10)
|
|
284
|
-
expect(signals[1].get()).toBe(20)
|
|
285
|
-
expect(signals[2].get()).toBe(30)
|
|
276
|
+
describe('length', () => {
|
|
277
|
+
test('should return item count', () => {
|
|
278
|
+
const list = createList([1, 2, 3])
|
|
279
|
+
expect(list.length).toBe(3)
|
|
286
280
|
})
|
|
287
281
|
|
|
288
|
-
test('
|
|
289
|
-
const
|
|
290
|
-
expect(
|
|
282
|
+
test('should update reactively with add and remove', () => {
|
|
283
|
+
const list = createList([1, 2])
|
|
284
|
+
expect(list.length).toBe(2)
|
|
285
|
+
list.add(3)
|
|
286
|
+
expect(list.length).toBe(3)
|
|
287
|
+
list.remove(0)
|
|
288
|
+
expect(list.length).toBe(2)
|
|
291
289
|
})
|
|
292
290
|
})
|
|
293
291
|
|
|
294
|
-
describe('
|
|
295
|
-
test('
|
|
296
|
-
const
|
|
297
|
-
|
|
298
|
-
expect(
|
|
292
|
+
describe('Key-based Access', () => {
|
|
293
|
+
test('keyAt should return key at index', () => {
|
|
294
|
+
const list = createList([10, 20, 30])
|
|
295
|
+
const key0 = list.keyAt(0)
|
|
296
|
+
expect(key0).toBeDefined()
|
|
297
|
+
expect(typeof key0).toBe('string')
|
|
299
298
|
})
|
|
300
299
|
|
|
301
|
-
test('
|
|
302
|
-
const list =
|
|
303
|
-
|
|
300
|
+
test('indexOfKey should return index for key', () => {
|
|
301
|
+
const list = createList([10, 20])
|
|
302
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
303
|
+
const key = list.keyAt(0)!
|
|
304
|
+
expect(list.indexOfKey(key)).toBe(0)
|
|
304
305
|
})
|
|
305
306
|
|
|
306
|
-
test('
|
|
307
|
-
const list =
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
expect(list.
|
|
307
|
+
test('byKey should return State signal for key', () => {
|
|
308
|
+
const list = createList([10, 20])
|
|
309
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
310
|
+
const key = list.keyAt(0)!
|
|
311
|
+
expect(list.byKey(key)?.get()).toBe(10)
|
|
311
312
|
})
|
|
312
|
-
})
|
|
313
|
-
|
|
314
|
-
describe('deriveCollection() method', () => {
|
|
315
|
-
describe('synchronous transformations', () => {
|
|
316
|
-
test('transforms list values with sync callback', () => {
|
|
317
|
-
const numbers = new List([1, 2, 3])
|
|
318
|
-
const doubled = numbers.deriveCollection(
|
|
319
|
-
(value: number) => value * 2,
|
|
320
|
-
)
|
|
321
|
-
|
|
322
|
-
expect(isCollection(doubled)).toBe(true)
|
|
323
|
-
expect(doubled.length).toBe(3)
|
|
324
|
-
expect(doubled.get()).toEqual([2, 4, 6])
|
|
325
|
-
})
|
|
326
|
-
|
|
327
|
-
test('transforms object values with sync callback', () => {
|
|
328
|
-
const users = new List([
|
|
329
|
-
{ name: 'Alice', age: 25 },
|
|
330
|
-
{ name: 'Bob', age: 30 },
|
|
331
|
-
])
|
|
332
|
-
const userInfo = users.deriveCollection(user => ({
|
|
333
|
-
displayName: user.name.toUpperCase(),
|
|
334
|
-
isAdult: user.age >= 18,
|
|
335
|
-
}))
|
|
336
|
-
|
|
337
|
-
expect(userInfo.length).toBe(2)
|
|
338
|
-
expect(userInfo.get()).toEqual([
|
|
339
|
-
{ displayName: 'ALICE', isAdult: true },
|
|
340
|
-
{ displayName: 'BOB', isAdult: true },
|
|
341
|
-
])
|
|
342
|
-
})
|
|
343
|
-
|
|
344
|
-
test('transforms string values to different types', () => {
|
|
345
|
-
const words = new List(['hello', 'world', 'test'])
|
|
346
|
-
const wordLengths = words.deriveCollection((word: string) => ({
|
|
347
|
-
word,
|
|
348
|
-
length: word.length,
|
|
349
|
-
}))
|
|
350
|
-
|
|
351
|
-
expect(wordLengths.get()).toEqual([
|
|
352
|
-
{ word: 'hello', length: 5 },
|
|
353
|
-
{ word: 'world', length: 5 },
|
|
354
|
-
{ word: 'test', length: 4 },
|
|
355
|
-
])
|
|
356
|
-
})
|
|
357
|
-
|
|
358
|
-
test('collection reactivity with sync transformations', () => {
|
|
359
|
-
const numbers = new List([1, 2])
|
|
360
|
-
const doubled = numbers.deriveCollection(
|
|
361
|
-
(value: number) => value * 2,
|
|
362
|
-
)
|
|
363
|
-
|
|
364
|
-
let collectionValue: number[] = []
|
|
365
|
-
let effectRuns = 0
|
|
366
|
-
createEffect(() => {
|
|
367
|
-
collectionValue = doubled.get()
|
|
368
|
-
effectRuns++
|
|
369
|
-
})
|
|
370
|
-
|
|
371
|
-
expect(collectionValue).toEqual([2, 4])
|
|
372
|
-
expect(effectRuns).toBe(1)
|
|
373
|
-
|
|
374
|
-
// Add new item
|
|
375
|
-
numbers.add(3)
|
|
376
|
-
expect(collectionValue).toEqual([2, 4, 6])
|
|
377
|
-
expect(effectRuns).toBe(2)
|
|
378
|
-
|
|
379
|
-
// Modify existing item
|
|
380
|
-
numbers.at(0)?.set(5)
|
|
381
|
-
expect(collectionValue).toEqual([10, 4, 6])
|
|
382
|
-
expect(effectRuns).toBe(3)
|
|
383
|
-
})
|
|
384
|
-
|
|
385
|
-
test('collection responds to source removal', () => {
|
|
386
|
-
const numbers = new List([1, 2, 3])
|
|
387
|
-
const doubled = numbers.deriveCollection(
|
|
388
|
-
(value: number) => value * 2,
|
|
389
|
-
)
|
|
390
|
-
|
|
391
|
-
expect(doubled.get()).toEqual([2, 4, 6])
|
|
392
313
|
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
314
|
+
test('keys should return iterator of all keys', () => {
|
|
315
|
+
const list = createList([10, 20, 30])
|
|
316
|
+
const allKeys = [...list.keys()]
|
|
317
|
+
expect(allKeys).toHaveLength(3)
|
|
318
|
+
expect(list.byKey(allKeys[0])?.get()).toBe(10)
|
|
397
319
|
})
|
|
320
|
+
})
|
|
398
321
|
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
// Trigger initial computation by accessing the collection
|
|
412
|
-
const initialLength = asyncDoubled.length
|
|
413
|
-
expect(initialLength).toBe(3)
|
|
414
|
-
|
|
415
|
-
// Access each computed signal to trigger computation
|
|
416
|
-
for (let i = 0; i < asyncDoubled.length; i++) {
|
|
417
|
-
asyncDoubled.at(i)?.get()
|
|
418
|
-
}
|
|
419
|
-
|
|
420
|
-
// Allow async operations to complete
|
|
421
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
422
|
-
|
|
423
|
-
expect(asyncDoubled.get()).toEqual([2, 4, 6])
|
|
424
|
-
})
|
|
425
|
-
|
|
426
|
-
test('async collection with object transformation', async () => {
|
|
427
|
-
const users = new List([
|
|
428
|
-
{ id: 1, name: 'Alice' },
|
|
429
|
-
{ id: 2, name: 'Bob' },
|
|
430
|
-
])
|
|
431
|
-
|
|
432
|
-
const enrichedUsers = users.deriveCollection(
|
|
433
|
-
async (
|
|
434
|
-
user: { id: number; name: string },
|
|
435
|
-
abort: AbortSignal,
|
|
436
|
-
) => {
|
|
437
|
-
// Simulate API call
|
|
438
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
439
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
440
|
-
|
|
441
|
-
return {
|
|
442
|
-
...user,
|
|
443
|
-
slug: user.name.toLowerCase(),
|
|
444
|
-
timestamp: Date.now(),
|
|
445
|
-
}
|
|
446
|
-
},
|
|
447
|
-
)
|
|
448
|
-
|
|
449
|
-
// Trigger initial computation by accessing each computed signal
|
|
450
|
-
for (let i = 0; i < enrichedUsers.length; i++) {
|
|
451
|
-
enrichedUsers.at(i)?.get()
|
|
452
|
-
}
|
|
453
|
-
|
|
454
|
-
// Allow async operations to complete
|
|
455
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
456
|
-
|
|
457
|
-
const result = enrichedUsers.get()
|
|
458
|
-
expect(result).toHaveLength(2)
|
|
459
|
-
expect(result[0].slug).toBe('alice')
|
|
460
|
-
expect(result[1].slug).toBe('bob')
|
|
461
|
-
expect(typeof result[0].timestamp).toBe('number')
|
|
462
|
-
})
|
|
463
|
-
|
|
464
|
-
test('async collection reactivity', async () => {
|
|
465
|
-
const numbers = new List([1, 2])
|
|
466
|
-
const asyncDoubled = numbers.deriveCollection(
|
|
467
|
-
async (value: number, abort: AbortSignal) => {
|
|
468
|
-
await new Promise(resolve => setTimeout(resolve, 5))
|
|
469
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
470
|
-
return value * 2
|
|
471
|
-
},
|
|
472
|
-
)
|
|
473
|
-
|
|
474
|
-
const effectValues: number[][] = []
|
|
475
|
-
|
|
476
|
-
// Set up effect to track changes reactively
|
|
477
|
-
createEffect(() => {
|
|
478
|
-
const currentValue = asyncDoubled.get()
|
|
479
|
-
effectValues.push([...currentValue])
|
|
480
|
-
})
|
|
481
|
-
|
|
482
|
-
// Allow initial computation
|
|
483
|
-
await new Promise(resolve => setTimeout(resolve, 20))
|
|
484
|
-
expect(effectValues[effectValues.length - 1]).toEqual([2, 4])
|
|
485
|
-
|
|
486
|
-
// Add new item
|
|
487
|
-
numbers.add(3)
|
|
488
|
-
await new Promise(resolve => setTimeout(resolve, 20))
|
|
489
|
-
expect(effectValues[effectValues.length - 1]).toEqual([2, 4, 6])
|
|
490
|
-
|
|
491
|
-
// Modify existing item
|
|
492
|
-
numbers.at(0)?.set(5)
|
|
493
|
-
await new Promise(resolve => setTimeout(resolve, 20))
|
|
494
|
-
expect(effectValues[effectValues.length - 1]).toEqual([
|
|
495
|
-
10, 4, 6,
|
|
496
|
-
])
|
|
497
|
-
})
|
|
498
|
-
|
|
499
|
-
test('handles AbortSignal cancellation', async () => {
|
|
500
|
-
const numbers = new List([1])
|
|
501
|
-
let abortCalled = false
|
|
502
|
-
|
|
503
|
-
const slowCollection = numbers.deriveCollection(
|
|
504
|
-
async (value: number, abort: AbortSignal) => {
|
|
505
|
-
return new Promise<number>((resolve, reject) => {
|
|
506
|
-
const timeout = setTimeout(
|
|
507
|
-
() => resolve(value * 2),
|
|
508
|
-
50,
|
|
509
|
-
)
|
|
510
|
-
abort.addEventListener('abort', () => {
|
|
511
|
-
clearTimeout(timeout)
|
|
512
|
-
abortCalled = true
|
|
513
|
-
reject(new Error('Aborted'))
|
|
514
|
-
})
|
|
515
|
-
})
|
|
516
|
-
},
|
|
517
|
-
)
|
|
518
|
-
|
|
519
|
-
// Trigger initial computation
|
|
520
|
-
slowCollection.at(0)?.get()
|
|
521
|
-
|
|
522
|
-
// Change the value to trigger cancellation of the first computation
|
|
523
|
-
numbers.at(0)?.set(2)
|
|
524
|
-
|
|
525
|
-
// Allow some time for operations
|
|
526
|
-
await new Promise(resolve => setTimeout(resolve, 100))
|
|
527
|
-
|
|
528
|
-
expect(abortCalled).toBe(true)
|
|
529
|
-
expect(slowCollection.get()).toEqual([4]) // Last value (2 * 2)
|
|
530
|
-
})
|
|
322
|
+
describe('options.keyConfig', () => {
|
|
323
|
+
test('should use function to generate keys', () => {
|
|
324
|
+
const list = createList(
|
|
325
|
+
[
|
|
326
|
+
{ id: 'a', value: 1 },
|
|
327
|
+
{ id: 'b', value: 2 },
|
|
328
|
+
],
|
|
329
|
+
{ keyConfig: item => item.id },
|
|
330
|
+
)
|
|
331
|
+
expect(list.byKey('a')?.get()).toEqual({ id: 'a', value: 1 })
|
|
332
|
+
expect(list.byKey('b')?.get()).toEqual({ id: 'b', value: 2 })
|
|
531
333
|
})
|
|
532
334
|
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
(value: number) => value * 2,
|
|
541
|
-
)
|
|
335
|
+
test('should use string prefix for auto-generated keys', () => {
|
|
336
|
+
const list = createList([1, 2, 3], { keyConfig: 'item-' })
|
|
337
|
+
expect(list.keyAt(0)).toBe('item-0')
|
|
338
|
+
expect(list.keyAt(1)).toBe('item-1')
|
|
339
|
+
expect(list.keyAt(2)).toBe('item-2')
|
|
340
|
+
})
|
|
341
|
+
})
|
|
542
342
|
|
|
543
|
-
|
|
343
|
+
describe('Iteration', () => {
|
|
344
|
+
test('should support for...of via Symbol.iterator', () => {
|
|
345
|
+
const list = createList([10, 20, 30])
|
|
346
|
+
const signals = [...list]
|
|
347
|
+
expect(signals).toHaveLength(3)
|
|
348
|
+
expect(signals[0].get()).toBe(10)
|
|
349
|
+
expect(signals[1].get()).toBe(20)
|
|
350
|
+
expect(signals[2].get()).toBe(30)
|
|
351
|
+
})
|
|
352
|
+
})
|
|
544
353
|
|
|
545
|
-
|
|
546
|
-
|
|
354
|
+
describe('Reactivity', () => {
|
|
355
|
+
test('get() should trigger effects on structural changes', () => {
|
|
356
|
+
const list = createList([1, 2, 3])
|
|
357
|
+
let lastArray: number[] = []
|
|
358
|
+
createEffect(() => {
|
|
359
|
+
lastArray = list.get()
|
|
547
360
|
})
|
|
548
361
|
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
(value: number) => value * 2,
|
|
553
|
-
)
|
|
554
|
-
const asyncSquared = doubled.deriveCollection(
|
|
555
|
-
async (value: number, abort: AbortSignal) => {
|
|
556
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
557
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
558
|
-
return value * value
|
|
559
|
-
},
|
|
560
|
-
)
|
|
561
|
-
|
|
562
|
-
// Trigger initial computation by accessing each computed signal
|
|
563
|
-
for (let i = 0; i < asyncSquared.length; i++) {
|
|
564
|
-
asyncSquared.at(i)?.get()
|
|
565
|
-
}
|
|
566
|
-
|
|
567
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
568
|
-
expect(asyncSquared.get()).toEqual([4, 16]) // (1*2)^2, (2*2)^2
|
|
569
|
-
})
|
|
362
|
+
expect(lastArray).toEqual([1, 2, 3])
|
|
363
|
+
list.add(4)
|
|
364
|
+
expect(lastArray).toEqual([1, 2, 3, 4])
|
|
570
365
|
})
|
|
571
366
|
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
)
|
|
578
|
-
|
|
579
|
-
expect(doubled.at(0)?.get()).toBe(2)
|
|
580
|
-
expect(doubled.at(1)?.get()).toBe(4)
|
|
581
|
-
expect(doubled.at(2)?.get()).toBe(6)
|
|
582
|
-
expect(doubled.at(3)).toBeUndefined()
|
|
367
|
+
test('individual item signals should trigger effects', () => {
|
|
368
|
+
const list = createList([{ count: 5 }])
|
|
369
|
+
let lastCount = 0
|
|
370
|
+
let effectCount = 0
|
|
371
|
+
createEffect(() => {
|
|
372
|
+
lastCount = list.at(0)?.get().count ?? 0
|
|
373
|
+
effectCount++
|
|
583
374
|
})
|
|
584
375
|
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
const doubled = numbers.deriveCollection(
|
|
588
|
-
(value: number) => value * 2,
|
|
589
|
-
)
|
|
590
|
-
|
|
591
|
-
const key0 = numbers.keyAt(0)
|
|
592
|
-
const key1 = numbers.keyAt(1)
|
|
593
|
-
|
|
594
|
-
expect(key0).toBeDefined()
|
|
595
|
-
expect(key1).toBeDefined()
|
|
596
|
-
|
|
597
|
-
if (key0 && key1) {
|
|
598
|
-
expect(doubled.byKey(key0)?.get()).toBe(20)
|
|
599
|
-
expect(doubled.byKey(key1)?.get()).toBe(40)
|
|
600
|
-
}
|
|
601
|
-
})
|
|
376
|
+
expect(lastCount).toBe(5)
|
|
377
|
+
expect(effectCount).toBe(1)
|
|
602
378
|
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
(value: number) => value * 2,
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
const signals = [...doubled]
|
|
610
|
-
expect(signals).toHaveLength(3)
|
|
611
|
-
expect(signals[0].get()).toBe(2)
|
|
612
|
-
expect(signals[1].get()).toBe(4)
|
|
613
|
-
expect(signals[2].get()).toBe(6)
|
|
614
|
-
})
|
|
379
|
+
list.at(0)?.set({ count: 10 })
|
|
380
|
+
expect(lastCount).toBe(10)
|
|
381
|
+
expect(effectCount).toBe(2)
|
|
615
382
|
})
|
|
616
383
|
|
|
617
|
-
|
|
618
|
-
|
|
619
|
-
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
)
|
|
623
|
-
|
|
624
|
-
expect(doubled.get()).toEqual([])
|
|
625
|
-
expect(doubled.length).toBe(0)
|
|
626
|
-
})
|
|
627
|
-
|
|
628
|
-
test('handles UNSET values in transformation', () => {
|
|
629
|
-
const list = new List([1, UNSET, 3])
|
|
630
|
-
const processed = list.deriveCollection(value => {
|
|
631
|
-
return value === UNSET ? 0 : value * 2
|
|
632
|
-
})
|
|
633
|
-
|
|
634
|
-
// UNSET values are filtered out before transformation
|
|
635
|
-
expect(processed.get()).toEqual([2, 6])
|
|
636
|
-
})
|
|
384
|
+
test('computed signals should react to list changes', () => {
|
|
385
|
+
const list = createList([1, 2, 3])
|
|
386
|
+
const sum = createMemo(() =>
|
|
387
|
+
list.get().reduce((acc, n) => acc + n, 0),
|
|
388
|
+
)
|
|
637
389
|
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
const transformed = items.deriveCollection(item => ({
|
|
645
|
-
itemId: item.id,
|
|
646
|
-
processedValue: item.data.value * 2,
|
|
647
|
-
status: item.data.active ? 'enabled' : 'disabled',
|
|
648
|
-
}))
|
|
649
|
-
|
|
650
|
-
expect(transformed.get()).toEqual([
|
|
651
|
-
{ itemId: 1, processedValue: 20, status: 'enabled' },
|
|
652
|
-
{ itemId: 2, processedValue: 40, status: 'disabled' },
|
|
653
|
-
])
|
|
654
|
-
})
|
|
390
|
+
expect(sum.get()).toBe(6)
|
|
391
|
+
list.add(4)
|
|
392
|
+
expect(sum.get()).toBe(10)
|
|
393
|
+
list.remove(0)
|
|
394
|
+
expect(sum.get()).toBe(9)
|
|
655
395
|
})
|
|
656
396
|
})
|
|
657
397
|
|
|
658
|
-
describe('
|
|
659
|
-
test('
|
|
660
|
-
let
|
|
661
|
-
let
|
|
662
|
-
const
|
|
398
|
+
describe('options.watched', () => {
|
|
399
|
+
test('should call watched on first subscriber and cleanup on last unsubscribe', () => {
|
|
400
|
+
let watchedCalled = false
|
|
401
|
+
let unwatchedCalled = false
|
|
402
|
+
const list = createList([10, 20], {
|
|
663
403
|
watched: () => {
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
404
|
+
watchedCalled = true
|
|
405
|
+
return () => {
|
|
406
|
+
unwatchedCalled = true
|
|
407
|
+
}
|
|
668
408
|
},
|
|
669
409
|
})
|
|
670
410
|
|
|
671
|
-
expect(
|
|
411
|
+
expect(watchedCalled).toBe(false)
|
|
672
412
|
|
|
673
|
-
|
|
674
|
-
|
|
675
|
-
const cleanup = createEffect(() => {
|
|
676
|
-
effectValue = numbers.get()
|
|
413
|
+
const dispose = createEffect(() => {
|
|
414
|
+
list.get()
|
|
677
415
|
})
|
|
678
416
|
|
|
679
|
-
expect(
|
|
680
|
-
expect(
|
|
681
|
-
expect(listUnwatchedCalled).toBe(false)
|
|
417
|
+
expect(watchedCalled).toBe(true)
|
|
418
|
+
expect(unwatchedCalled).toBe(false)
|
|
682
419
|
|
|
683
|
-
|
|
684
|
-
|
|
685
|
-
expect(listUnwatchedCalled).toBe(true)
|
|
420
|
+
dispose()
|
|
421
|
+
expect(unwatchedCalled).toBe(true)
|
|
686
422
|
})
|
|
687
423
|
|
|
688
|
-
test('
|
|
689
|
-
let
|
|
690
|
-
|
|
691
|
-
const numbers = new List([1, 2, 3], {
|
|
424
|
+
test('should activate on length access', () => {
|
|
425
|
+
let watchedCalled = false
|
|
426
|
+
const list = createList([1, 2], {
|
|
692
427
|
watched: () => {
|
|
693
|
-
|
|
694
|
-
|
|
695
|
-
unwatched: () => {
|
|
696
|
-
listUnwatchedCalled = true
|
|
428
|
+
watchedCalled = true
|
|
429
|
+
return () => {}
|
|
697
430
|
},
|
|
698
431
|
})
|
|
699
432
|
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
const cleanup = createEffect(() => {
|
|
703
|
-
effectValue = numbers.length
|
|
433
|
+
const dispose = createEffect(() => {
|
|
434
|
+
void list.length
|
|
704
435
|
})
|
|
705
436
|
|
|
706
|
-
expect(
|
|
707
|
-
|
|
708
|
-
|
|
437
|
+
expect(watchedCalled).toBe(true)
|
|
438
|
+
dispose()
|
|
439
|
+
})
|
|
440
|
+
})
|
|
441
|
+
|
|
442
|
+
describe('Input Validation', () => {
|
|
443
|
+
test('should throw for non-array initial value', () => {
|
|
444
|
+
expect(() => {
|
|
445
|
+
// @ts-expect-error - Testing invalid input
|
|
446
|
+
createList('not an array')
|
|
447
|
+
}).toThrow()
|
|
448
|
+
})
|
|
709
449
|
|
|
710
|
-
|
|
711
|
-
expect(
|
|
450
|
+
test('should throw for null initial value', () => {
|
|
451
|
+
expect(() => {
|
|
452
|
+
// @ts-expect-error - Testing invalid input
|
|
453
|
+
createList(null)
|
|
454
|
+
}).toThrow()
|
|
712
455
|
})
|
|
713
456
|
})
|
|
714
457
|
})
|