@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/collection.test.ts
CHANGED
|
@@ -1,852 +1,601 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
+
batch,
|
|
4
|
+
type CollectionChanges,
|
|
5
|
+
createCollection,
|
|
3
6
|
createEffect,
|
|
4
|
-
|
|
5
|
-
|
|
7
|
+
createList,
|
|
8
|
+
createScope,
|
|
9
|
+
createState,
|
|
6
10
|
isCollection,
|
|
7
|
-
|
|
8
|
-
UNSET,
|
|
11
|
+
isList,
|
|
9
12
|
} from '../index.ts'
|
|
10
13
|
|
|
11
|
-
|
|
12
|
-
describe('creation and basic operations', () => {
|
|
13
|
-
test('creates collection with initial values from list', () => {
|
|
14
|
-
const numbers = new List([1, 2, 3])
|
|
15
|
-
const doubled = new DerivedCollection(
|
|
16
|
-
numbers,
|
|
17
|
-
(value: number) => value * 2,
|
|
18
|
-
)
|
|
14
|
+
/* === Utility Functions === */
|
|
19
15
|
|
|
20
|
-
|
|
21
|
-
expect(doubled.at(0)?.get()).toBe(2)
|
|
22
|
-
expect(doubled.at(1)?.get()).toBe(4)
|
|
23
|
-
expect(doubled.at(2)?.get()).toBe(6)
|
|
24
|
-
})
|
|
16
|
+
const wait = (ms: number) => new Promise(resolve => setTimeout(resolve, ms))
|
|
25
17
|
|
|
26
|
-
|
|
27
|
-
const doubled = new DerivedCollection(
|
|
28
|
-
() => new List([10, 20, 30]),
|
|
29
|
-
(value: number) => value * 2,
|
|
30
|
-
)
|
|
18
|
+
/* === Tests === */
|
|
31
19
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
})
|
|
20
|
+
describe('Collection', () => {
|
|
21
|
+
describe('createCollection', () => {
|
|
22
|
+
test('should create a collection with initial values', () => {
|
|
23
|
+
const col = createCollection(() => () => {}, { value: [1, 2, 3] })
|
|
37
24
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
expect(Object.prototype.toString.call(collection)).toBe(
|
|
42
|
-
'[object Collection]',
|
|
43
|
-
)
|
|
25
|
+
expect(col.get()).toEqual([1, 2, 3])
|
|
26
|
+
expect(col.length).toBe(3)
|
|
27
|
+
expect(isCollection(col)).toBe(true)
|
|
44
28
|
})
|
|
45
29
|
|
|
46
|
-
test('
|
|
47
|
-
const
|
|
48
|
-
const list = new List([1, 2, 3])
|
|
49
|
-
const collection = new DerivedCollection(list, (x: number) => x)
|
|
30
|
+
test('should create an empty collection', () => {
|
|
31
|
+
const col = createCollection<number>(() => () => {})
|
|
50
32
|
|
|
51
|
-
expect(
|
|
52
|
-
expect(
|
|
53
|
-
expect(isCollection(store)).toBe(false)
|
|
54
|
-
expect(isCollection({})).toBe(false)
|
|
55
|
-
expect(isCollection(null)).toBe(false)
|
|
33
|
+
expect(col.get()).toEqual([])
|
|
34
|
+
expect(col.length).toBe(0)
|
|
56
35
|
})
|
|
57
36
|
|
|
58
|
-
test('
|
|
59
|
-
const
|
|
60
|
-
|
|
61
|
-
numbers,
|
|
62
|
-
(value: number) => value * 2,
|
|
63
|
-
)
|
|
64
|
-
|
|
65
|
-
const result = doubled.get()
|
|
66
|
-
expect(result).toEqual([2, 4, 6])
|
|
67
|
-
expect(Array.isArray(result)).toBe(true)
|
|
37
|
+
test('should have Symbol.toStringTag of "Collection"', () => {
|
|
38
|
+
const col = createCollection(() => () => {}, { value: [1] })
|
|
39
|
+
expect(col[Symbol.toStringTag]).toBe('Collection')
|
|
68
40
|
})
|
|
69
|
-
})
|
|
70
41
|
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
const collection = new DerivedCollection(
|
|
75
|
-
numbers,
|
|
76
|
-
(x: number) => x * 2,
|
|
77
|
-
)
|
|
78
|
-
expect(collection.length).toBe(5)
|
|
42
|
+
test('should have Symbol.isConcatSpreadable set to true', () => {
|
|
43
|
+
const col = createCollection(() => () => {}, { value: [1] })
|
|
44
|
+
expect(col[Symbol.isConcatSpreadable]).toBe(true)
|
|
79
45
|
})
|
|
80
46
|
|
|
81
|
-
test('
|
|
82
|
-
const
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
47
|
+
test('should support at(), byKey(), keyAt(), indexOfKey()', () => {
|
|
48
|
+
const col = createCollection(() => () => {}, {
|
|
49
|
+
value: [
|
|
50
|
+
{ id: 'a', name: 'Alice' },
|
|
51
|
+
{ id: 'b', name: 'Bob' },
|
|
52
|
+
],
|
|
53
|
+
keyConfig: item => item.id,
|
|
54
|
+
})
|
|
87
55
|
|
|
88
|
-
expect(
|
|
89
|
-
|
|
90
|
-
expect(
|
|
56
|
+
expect(col.keyAt(0)).toBe('a')
|
|
57
|
+
expect(col.keyAt(1)).toBe('b')
|
|
58
|
+
expect(col.indexOfKey('b')).toBe(1)
|
|
59
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
60
|
+
expect(col.byKey('a')!.get()).toEqual({ id: 'a', name: 'Alice' })
|
|
61
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
62
|
+
expect(col.at(1)!.get()).toEqual({ id: 'b', name: 'Bob' })
|
|
91
63
|
})
|
|
92
|
-
})
|
|
93
64
|
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
65
|
+
test('should support iteration', () => {
|
|
66
|
+
const col = createCollection(() => () => {}, {
|
|
67
|
+
value: [10, 20, 30],
|
|
68
|
+
})
|
|
98
69
|
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
expect(
|
|
70
|
+
const values = []
|
|
71
|
+
for (const signal of col) values.push(signal.get())
|
|
72
|
+
expect(values).toEqual([10, 20, 30])
|
|
102
73
|
})
|
|
103
74
|
|
|
104
|
-
test('
|
|
105
|
-
const
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
75
|
+
test('should support custom key config with string prefix', () => {
|
|
76
|
+
const col = createCollection(() => () => {}, {
|
|
77
|
+
value: [10, 20],
|
|
78
|
+
keyConfig: 'item-',
|
|
79
|
+
})
|
|
109
80
|
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
(x: number) => x * 2,
|
|
115
|
-
)
|
|
116
|
-
expect(collection.at(1)?.get()).toBe(4)
|
|
81
|
+
expect(col.keyAt(0)).toBe('item-0')
|
|
82
|
+
expect(col.keyAt(1)).toBe('item-1')
|
|
83
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
84
|
+
expect(col.byKey('item-0')!.get()).toBe(10)
|
|
117
85
|
})
|
|
118
|
-
})
|
|
119
|
-
|
|
120
|
-
describe('key-based access methods', () => {
|
|
121
|
-
test('byKey() returns computed signal for existing keys', () => {
|
|
122
|
-
const numbers = new List([1, 2, 3])
|
|
123
|
-
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
124
86
|
|
|
125
|
-
|
|
126
|
-
|
|
87
|
+
test('should support custom createItem factory', () => {
|
|
88
|
+
let guardCalled = false
|
|
89
|
+
const col = createCollection(() => () => {}, {
|
|
90
|
+
value: [5, 10],
|
|
91
|
+
createItem: value =>
|
|
92
|
+
createState(value, {
|
|
93
|
+
guard: (v): v is number => {
|
|
94
|
+
guardCalled = true
|
|
95
|
+
return typeof v === 'number'
|
|
96
|
+
},
|
|
97
|
+
}),
|
|
98
|
+
})
|
|
127
99
|
|
|
128
|
-
expect(
|
|
129
|
-
expect(
|
|
130
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
131
|
-
expect(doubled.byKey(key0!)).toBeDefined()
|
|
132
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
133
|
-
expect(doubled.byKey(key1!)).toBeDefined()
|
|
134
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
135
|
-
expect(doubled.byKey(key0!)?.get()).toBe(2)
|
|
136
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
137
|
-
expect(doubled.byKey(key1!)?.get()).toBe(4)
|
|
100
|
+
expect(col.get()).toEqual([5, 10])
|
|
101
|
+
expect(guardCalled).toBe(true)
|
|
138
102
|
})
|
|
103
|
+
})
|
|
139
104
|
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
const
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
const key1 = doubled.keyAt(1)
|
|
105
|
+
describe('isCollection', () => {
|
|
106
|
+
test('should identify collection signals', () => {
|
|
107
|
+
const col = createCollection(() => () => {}, { value: [1] })
|
|
108
|
+
expect(isCollection(col)).toBe(true)
|
|
109
|
+
})
|
|
146
110
|
|
|
147
|
-
|
|
148
|
-
expect(
|
|
149
|
-
|
|
150
|
-
expect(
|
|
151
|
-
|
|
152
|
-
|
|
111
|
+
test('should return false for non-collection values', () => {
|
|
112
|
+
expect(isCollection(42)).toBe(false)
|
|
113
|
+
expect(isCollection(null)).toBe(false)
|
|
114
|
+
expect(isCollection({})).toBe(false)
|
|
115
|
+
expect(
|
|
116
|
+
isList(createCollection(() => () => {}, { value: [1] })),
|
|
117
|
+
).toBe(false)
|
|
153
118
|
})
|
|
154
119
|
})
|
|
155
120
|
|
|
156
|
-
describe('
|
|
157
|
-
test('
|
|
158
|
-
|
|
159
|
-
|
|
121
|
+
describe('Watched Lifecycle', () => {
|
|
122
|
+
test('should call start callback on first effect access', () => {
|
|
123
|
+
let started = false
|
|
124
|
+
let cleaned = false
|
|
125
|
+
|
|
126
|
+
const col = createCollection(
|
|
127
|
+
() => {
|
|
128
|
+
started = true
|
|
129
|
+
return () => {
|
|
130
|
+
cleaned = true
|
|
131
|
+
}
|
|
132
|
+
},
|
|
133
|
+
{ value: [1] },
|
|
134
|
+
)
|
|
160
135
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
136
|
+
expect(started).toBe(false)
|
|
137
|
+
|
|
138
|
+
const dispose = createScope(() => {
|
|
139
|
+
createEffect(() => {
|
|
140
|
+
void col.length
|
|
141
|
+
})
|
|
164
142
|
})
|
|
165
143
|
|
|
166
|
-
expect(
|
|
167
|
-
|
|
168
|
-
|
|
144
|
+
expect(started).toBe(true)
|
|
145
|
+
expect(cleaned).toBe(false)
|
|
146
|
+
|
|
147
|
+
dispose()
|
|
148
|
+
expect(cleaned).toBe(true)
|
|
169
149
|
})
|
|
170
150
|
|
|
171
|
-
test('
|
|
172
|
-
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
151
|
+
test('should activate via keys() access in effect', () => {
|
|
152
|
+
let started = false
|
|
153
|
+
const col = createCollection(
|
|
154
|
+
() => {
|
|
155
|
+
started = true
|
|
156
|
+
return () => {}
|
|
157
|
+
},
|
|
158
|
+
{ value: [1] },
|
|
176
159
|
)
|
|
177
160
|
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
161
|
+
expect(started).toBe(false)
|
|
162
|
+
|
|
163
|
+
const dispose = createScope(() => {
|
|
164
|
+
createEffect(() => {
|
|
165
|
+
void Array.from(col.keys())
|
|
166
|
+
})
|
|
183
167
|
})
|
|
184
168
|
|
|
185
|
-
expect(
|
|
186
|
-
expect(itemEffectRuns).toBe(1)
|
|
169
|
+
expect(started).toBe(true)
|
|
187
170
|
|
|
188
|
-
|
|
189
|
-
expect(lastItem).toEqual({ count: 10 })
|
|
190
|
-
// Effect runs twice: once initially, once for change
|
|
191
|
-
expect(itemEffectRuns).toEqual(2)
|
|
171
|
+
dispose()
|
|
192
172
|
})
|
|
173
|
+
})
|
|
193
174
|
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
175
|
+
describe('applyChanges', () => {
|
|
176
|
+
test('should add items', () => {
|
|
177
|
+
let apply:
|
|
178
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
179
|
+
| undefined
|
|
180
|
+
const col = createCollection<number>(applyChanges => {
|
|
181
|
+
apply = applyChanges
|
|
182
|
+
return () => {}
|
|
183
|
+
})
|
|
197
184
|
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
185
|
+
const values: number[][] = []
|
|
186
|
+
const dispose = createScope(() => {
|
|
187
|
+
createEffect(() => {
|
|
188
|
+
values.push(col.get())
|
|
189
|
+
})
|
|
203
190
|
})
|
|
204
191
|
|
|
205
|
-
expect(
|
|
206
|
-
expect(arrayEffectRuns).toBe(1)
|
|
192
|
+
expect(values).toEqual([[]])
|
|
207
193
|
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
expect(arrayEffectRuns).toBe(2)
|
|
211
|
-
})
|
|
212
|
-
})
|
|
194
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
195
|
+
apply!({ add: [1, 2] })
|
|
213
196
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
218
|
-
const signals = [...doubled]
|
|
197
|
+
expect(values.length).toBe(2)
|
|
198
|
+
expect(values[1]).toEqual([1, 2])
|
|
199
|
+
expect(col.length).toBe(2)
|
|
219
200
|
|
|
220
|
-
|
|
221
|
-
expect(signals[0].get()).toBe(2)
|
|
222
|
-
expect(signals[1].get()).toBe(4)
|
|
223
|
-
expect(signals[2].get()).toBe(6)
|
|
201
|
+
dispose()
|
|
224
202
|
})
|
|
225
203
|
|
|
226
|
-
test('
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
204
|
+
test('should change item values', () => {
|
|
205
|
+
let apply:
|
|
206
|
+
| ((
|
|
207
|
+
changes: CollectionChanges<{ id: string; val: number }>,
|
|
208
|
+
) => void)
|
|
209
|
+
| undefined
|
|
210
|
+
const col = createCollection(
|
|
211
|
+
applyChanges => {
|
|
212
|
+
apply = applyChanges
|
|
213
|
+
return () => {}
|
|
214
|
+
},
|
|
215
|
+
{
|
|
216
|
+
value: [{ id: 'x', val: 1 }],
|
|
217
|
+
keyConfig: item => item.id,
|
|
218
|
+
},
|
|
239
219
|
)
|
|
240
|
-
expect(collection.length).toBe(0)
|
|
241
|
-
expect(collection.get()).toEqual([])
|
|
242
|
-
})
|
|
243
220
|
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
221
|
+
const values: { id: string; val: number }[][] = []
|
|
222
|
+
const dispose = createScope(() => {
|
|
223
|
+
createEffect(() => {
|
|
224
|
+
values.push(col.get())
|
|
225
|
+
})
|
|
226
|
+
})
|
|
249
227
|
|
|
250
|
-
|
|
251
|
-
expect(processed.get()).toEqual([3])
|
|
252
|
-
})
|
|
228
|
+
expect(values[0]).toEqual([{ id: 'x', val: 1 }])
|
|
253
229
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
const lengths = new DerivedCollection(list, (str: string) => ({
|
|
257
|
-
length: str.length,
|
|
258
|
-
}))
|
|
230
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
231
|
+
apply!({ change: [{ id: 'x', val: 42 }] })
|
|
259
232
|
|
|
260
|
-
expect(
|
|
261
|
-
expect(
|
|
262
|
-
})
|
|
263
|
-
})
|
|
233
|
+
expect(values.length).toBe(2)
|
|
234
|
+
expect(values[1]).toEqual([{ id: 'x', val: 42 }])
|
|
264
235
|
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
test('transforms collection values with sync callback', () => {
|
|
268
|
-
const numbers = new List([1, 2, 3])
|
|
269
|
-
const doubled = new DerivedCollection(
|
|
270
|
-
numbers,
|
|
271
|
-
(x: number) => x * 2,
|
|
272
|
-
)
|
|
273
|
-
const quadrupled = doubled.deriveCollection(
|
|
274
|
-
(x: number) => x * 2,
|
|
275
|
-
)
|
|
276
|
-
|
|
277
|
-
expect(quadrupled.length).toBe(3)
|
|
278
|
-
expect(quadrupled.at(0)?.get()).toBe(4)
|
|
279
|
-
expect(quadrupled.at(1)?.get()).toBe(8)
|
|
280
|
-
expect(quadrupled.at(2)?.get()).toBe(12)
|
|
281
|
-
})
|
|
236
|
+
dispose()
|
|
237
|
+
})
|
|
282
238
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
239
|
+
test('should remove items', () => {
|
|
240
|
+
let apply:
|
|
241
|
+
| ((
|
|
242
|
+
changes: CollectionChanges<{ id: string; v: number }>,
|
|
243
|
+
) => void)
|
|
244
|
+
| undefined
|
|
245
|
+
const col = createCollection(
|
|
246
|
+
applyChanges => {
|
|
247
|
+
apply = applyChanges
|
|
248
|
+
return () => {}
|
|
249
|
+
},
|
|
250
|
+
{
|
|
251
|
+
value: [
|
|
252
|
+
{ id: 'a', v: 1 },
|
|
253
|
+
{ id: 'b', v: 2 },
|
|
254
|
+
{ id: 'c', v: 3 },
|
|
255
|
+
],
|
|
256
|
+
keyConfig: item => item.id,
|
|
257
|
+
},
|
|
258
|
+
)
|
|
301
259
|
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
})
|
|
307
|
-
expect(detailedInfo.at(1)?.get()).toEqual({
|
|
308
|
-
displayName: 'BOB',
|
|
309
|
-
isAdult: true,
|
|
310
|
-
category: 'adult',
|
|
260
|
+
const values: { id: string; v: number }[][] = []
|
|
261
|
+
const dispose = createScope(() => {
|
|
262
|
+
createEffect(() => {
|
|
263
|
+
values.push(col.get())
|
|
311
264
|
})
|
|
312
265
|
})
|
|
313
266
|
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
word,
|
|
320
|
-
length: word.length,
|
|
321
|
-
}),
|
|
322
|
-
)
|
|
323
|
-
const analysis = wordInfo.deriveCollection(
|
|
324
|
-
(info: { word: string; length: number }) => ({
|
|
325
|
-
...info,
|
|
326
|
-
isLong: info.length > 4,
|
|
327
|
-
}),
|
|
328
|
-
)
|
|
329
|
-
|
|
330
|
-
expect(analysis.at(0)?.get().word).toBe('hello')
|
|
331
|
-
expect(analysis.at(0)?.get().length).toBe(5)
|
|
332
|
-
expect(analysis.at(0)?.get().isLong).toBe(true)
|
|
333
|
-
expect(analysis.at(1)?.get().word).toBe('world')
|
|
334
|
-
expect(analysis.at(1)?.get().length).toBe(5)
|
|
335
|
-
expect(analysis.at(1)?.get().isLong).toBe(true)
|
|
336
|
-
expect(analysis.at(2)?.get().word).toBe('test')
|
|
337
|
-
expect(analysis.at(2)?.get().length).toBe(4)
|
|
338
|
-
expect(analysis.at(2)?.get().isLong).toBe(false)
|
|
339
|
-
})
|
|
267
|
+
expect(values[0]).toEqual([
|
|
268
|
+
{ id: 'a', v: 1 },
|
|
269
|
+
{ id: 'b', v: 2 },
|
|
270
|
+
{ id: 'c', v: 3 },
|
|
271
|
+
])
|
|
340
272
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
let collectionValue: number[] = []
|
|
352
|
-
let effectRuns = 0
|
|
353
|
-
createEffect(() => {
|
|
354
|
-
collectionValue = quadrupled.get()
|
|
355
|
-
effectRuns++
|
|
356
|
-
})
|
|
273
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
274
|
+
apply!({ remove: [{ id: 'b', v: 2 }] })
|
|
275
|
+
|
|
276
|
+
expect(values.length).toBe(2)
|
|
277
|
+
expect(values[1]).toEqual([
|
|
278
|
+
{ id: 'a', v: 1 },
|
|
279
|
+
{ id: 'c', v: 3 },
|
|
280
|
+
])
|
|
281
|
+
expect(col.length).toBe(2)
|
|
357
282
|
|
|
358
|
-
|
|
359
|
-
|
|
283
|
+
dispose()
|
|
284
|
+
})
|
|
360
285
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
286
|
+
test('should handle mixed add/change/remove', () => {
|
|
287
|
+
let apply:
|
|
288
|
+
| ((
|
|
289
|
+
changes: CollectionChanges<{ id: string; v: number }>,
|
|
290
|
+
) => void)
|
|
291
|
+
| undefined
|
|
292
|
+
const col = createCollection(
|
|
293
|
+
applyChanges => {
|
|
294
|
+
apply = applyChanges
|
|
295
|
+
return () => {}
|
|
296
|
+
},
|
|
297
|
+
{
|
|
298
|
+
value: [
|
|
299
|
+
{ id: 'a', v: 1 },
|
|
300
|
+
{ id: 'b', v: 2 },
|
|
301
|
+
],
|
|
302
|
+
keyConfig: item => item.id,
|
|
303
|
+
},
|
|
304
|
+
)
|
|
364
305
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
306
|
+
const values: { id: string; v: number }[][] = []
|
|
307
|
+
const dispose = createScope(() => {
|
|
308
|
+
createEffect(() => {
|
|
309
|
+
values.push(col.get())
|
|
310
|
+
})
|
|
368
311
|
})
|
|
369
312
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
const quadrupled = doubled.deriveCollection(
|
|
377
|
-
(x: number) => x * 2,
|
|
378
|
-
)
|
|
313
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
314
|
+
apply!({
|
|
315
|
+
add: [{ id: 'c', v: 3 }],
|
|
316
|
+
change: [{ id: 'a', v: 10 }],
|
|
317
|
+
remove: [{ id: 'b', v: 2 }],
|
|
318
|
+
})
|
|
379
319
|
|
|
380
|
-
|
|
320
|
+
expect(values.length).toBe(2)
|
|
321
|
+
expect(values[1]).toEqual([
|
|
322
|
+
{ id: 'a', v: 10 },
|
|
323
|
+
{ id: 'c', v: 3 },
|
|
324
|
+
])
|
|
381
325
|
|
|
382
|
-
|
|
383
|
-
expect(quadrupled.get()).toEqual([4, 12, 16])
|
|
384
|
-
})
|
|
326
|
+
dispose()
|
|
385
327
|
})
|
|
386
328
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
399
|
-
return x * 2
|
|
400
|
-
},
|
|
401
|
-
)
|
|
329
|
+
test('should skip when no changes provided', () => {
|
|
330
|
+
let apply:
|
|
331
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
332
|
+
| undefined
|
|
333
|
+
const col = createCollection(
|
|
334
|
+
applyChanges => {
|
|
335
|
+
apply = applyChanges
|
|
336
|
+
return () => {}
|
|
337
|
+
},
|
|
338
|
+
{ value: [1] },
|
|
339
|
+
)
|
|
402
340
|
|
|
403
|
-
|
|
404
|
-
|
|
341
|
+
let callCount = 0
|
|
342
|
+
const dispose = createScope(() => {
|
|
343
|
+
createEffect(() => {
|
|
344
|
+
void col.get()
|
|
345
|
+
callCount++
|
|
346
|
+
})
|
|
347
|
+
})
|
|
405
348
|
|
|
406
|
-
|
|
407
|
-
expect(asyncQuadrupled.at(0)).toBeDefined()
|
|
408
|
-
expect(asyncQuadrupled.at(1)).toBeDefined()
|
|
409
|
-
expect(asyncQuadrupled.at(2)).toBeDefined()
|
|
349
|
+
expect(callCount).toBe(1)
|
|
410
350
|
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
let effectRuns = 0
|
|
351
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
352
|
+
apply!({})
|
|
414
353
|
|
|
415
|
-
|
|
416
|
-
const values = asyncQuadrupled.get()
|
|
417
|
-
results.push(...values)
|
|
418
|
-
effectRuns++
|
|
419
|
-
})
|
|
354
|
+
expect(callCount).toBe(1)
|
|
420
355
|
|
|
421
|
-
|
|
422
|
-
|
|
356
|
+
dispose()
|
|
357
|
+
})
|
|
423
358
|
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
359
|
+
test('should trigger effects on structural changes', () => {
|
|
360
|
+
let apply:
|
|
361
|
+
| ((changes: CollectionChanges<string>) => void)
|
|
362
|
+
| undefined
|
|
363
|
+
const col = createCollection<string>(applyChanges => {
|
|
364
|
+
apply = applyChanges
|
|
365
|
+
return () => {}
|
|
427
366
|
})
|
|
428
367
|
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
{ id: 1, name: 'Alice' },
|
|
432
|
-
{ id: 2, name: 'Bob' },
|
|
433
|
-
])
|
|
434
|
-
const basicInfo = new DerivedCollection(
|
|
435
|
-
users,
|
|
436
|
-
(user: { id: number; name: string }) => ({
|
|
437
|
-
userId: user.id,
|
|
438
|
-
displayName: user.name.toUpperCase(),
|
|
439
|
-
}),
|
|
440
|
-
)
|
|
441
|
-
|
|
442
|
-
const enrichedUsers = basicInfo.deriveCollection(
|
|
443
|
-
async (
|
|
444
|
-
info: { userId: number; displayName: string },
|
|
445
|
-
abort: AbortSignal,
|
|
446
|
-
) => {
|
|
447
|
-
// Simulate async enrichment
|
|
448
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
449
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
450
|
-
|
|
451
|
-
return {
|
|
452
|
-
...info,
|
|
453
|
-
slug: info.displayName
|
|
454
|
-
.toLowerCase()
|
|
455
|
-
.replace(/\s+/g, '-'),
|
|
456
|
-
timestamp: Date.now(),
|
|
457
|
-
}
|
|
458
|
-
},
|
|
459
|
-
)
|
|
460
|
-
|
|
461
|
-
// Use effect to test async behavior
|
|
462
|
-
let enrichedResults: Array<{
|
|
463
|
-
userId: number
|
|
464
|
-
displayName: string
|
|
465
|
-
slug: string
|
|
466
|
-
timestamp: number
|
|
467
|
-
}> = []
|
|
468
|
-
|
|
368
|
+
let effectCount = 0
|
|
369
|
+
const dispose = createScope(() => {
|
|
469
370
|
createEffect(() => {
|
|
470
|
-
|
|
371
|
+
void col.length
|
|
372
|
+
effectCount++
|
|
471
373
|
})
|
|
374
|
+
})
|
|
472
375
|
|
|
473
|
-
|
|
474
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
376
|
+
expect(effectCount).toBe(1)
|
|
475
377
|
|
|
476
|
-
|
|
378
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
379
|
+
apply!({ add: ['hello'] })
|
|
477
380
|
|
|
478
|
-
|
|
381
|
+
expect(effectCount).toBe(2)
|
|
382
|
+
expect(col.length).toBe(1)
|
|
479
383
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
expect(result1?.slug).toBe('alice')
|
|
483
|
-
expect(typeof result1?.timestamp).toBe('number')
|
|
384
|
+
dispose()
|
|
385
|
+
})
|
|
484
386
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
387
|
+
test('should batch multiple calls', () => {
|
|
388
|
+
let apply:
|
|
389
|
+
| ((changes: CollectionChanges<number>) => void)
|
|
390
|
+
| undefined
|
|
391
|
+
const col = createCollection<number>(applyChanges => {
|
|
392
|
+
apply = applyChanges
|
|
393
|
+
return () => {}
|
|
489
394
|
})
|
|
490
395
|
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
const doubled = new DerivedCollection(
|
|
494
|
-
numbers,
|
|
495
|
-
(x: number) => x * 2,
|
|
496
|
-
)
|
|
497
|
-
const asyncQuadrupled = doubled.deriveCollection(
|
|
498
|
-
async (x: number, abort: AbortSignal) => {
|
|
499
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
500
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
501
|
-
return x * 2
|
|
502
|
-
},
|
|
503
|
-
)
|
|
504
|
-
|
|
505
|
-
const effectValues: number[][] = []
|
|
396
|
+
let effectCount = 0
|
|
397
|
+
const dispose = createScope(() => {
|
|
506
398
|
createEffect(() => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
effectValues.push(currentValue)
|
|
399
|
+
void col.get()
|
|
400
|
+
effectCount++
|
|
510
401
|
})
|
|
402
|
+
})
|
|
511
403
|
|
|
512
|
-
|
|
513
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
404
|
+
expect(effectCount).toBe(1)
|
|
514
405
|
|
|
515
|
-
|
|
516
|
-
|
|
406
|
+
batch(() => {
|
|
407
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
408
|
+
apply!({ add: [1] })
|
|
409
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
410
|
+
apply!({ add: [2] })
|
|
411
|
+
})
|
|
517
412
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
asyncQuadrupled.at(1)?.get()
|
|
521
|
-
asyncQuadrupled.at(2)?.get()
|
|
413
|
+
expect(effectCount).toBe(2)
|
|
414
|
+
expect(col.get()).toEqual([1, 2])
|
|
522
415
|
|
|
523
|
-
|
|
524
|
-
|
|
416
|
+
dispose()
|
|
417
|
+
})
|
|
418
|
+
})
|
|
525
419
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
420
|
+
describe('deriveCollection', () => {
|
|
421
|
+
test('should transform list values with sync callback', () => {
|
|
422
|
+
const numbers = createList([1, 2, 3])
|
|
423
|
+
const doubled = numbers.deriveCollection((v: number) => v * 2)
|
|
530
424
|
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
const doubled = new DerivedCollection(
|
|
534
|
-
numbers,
|
|
535
|
-
(x: number) => x * 2,
|
|
536
|
-
)
|
|
537
|
-
let abortCalled = false
|
|
538
|
-
|
|
539
|
-
const slowCollection = doubled.deriveCollection(
|
|
540
|
-
async (x: number, abort: AbortSignal) => {
|
|
541
|
-
abort.addEventListener('abort', () => {
|
|
542
|
-
abortCalled = true
|
|
543
|
-
})
|
|
544
|
-
|
|
545
|
-
// Long delay to allow cancellation
|
|
546
|
-
const timeout = new Promise(resolve =>
|
|
547
|
-
setTimeout(resolve, 100),
|
|
548
|
-
)
|
|
549
|
-
await timeout
|
|
550
|
-
|
|
551
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
552
|
-
return x * 2
|
|
553
|
-
},
|
|
554
|
-
)
|
|
555
|
-
|
|
556
|
-
// Start computation
|
|
557
|
-
const _awaited = slowCollection.at(0)?.get()
|
|
558
|
-
|
|
559
|
-
// Change source to trigger cancellation
|
|
560
|
-
numbers.at(0)?.set(10)
|
|
561
|
-
|
|
562
|
-
// Wait for potential abort
|
|
563
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
564
|
-
|
|
565
|
-
expect(abortCalled).toBe(true)
|
|
566
|
-
})
|
|
425
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
426
|
+
expect(doubled.length).toBe(3)
|
|
567
427
|
})
|
|
568
428
|
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
(
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
)
|
|
579
|
-
const octupled = quadrupled.deriveCollection(
|
|
580
|
-
(x: number) => x * 2,
|
|
581
|
-
)
|
|
582
|
-
|
|
583
|
-
expect(octupled.at(0)?.get()).toBe(8)
|
|
584
|
-
expect(octupled.at(1)?.get()).toBe(16)
|
|
585
|
-
expect(octupled.at(2)?.get()).toBe(24)
|
|
586
|
-
})
|
|
429
|
+
test('should transform values with async callback', async () => {
|
|
430
|
+
const numbers = createList([1, 2, 3])
|
|
431
|
+
const doubled = numbers.deriveCollection(
|
|
432
|
+
async (v: number, abort: AbortSignal) => {
|
|
433
|
+
await wait(10)
|
|
434
|
+
if (abort.aborted) throw new Error('Aborted')
|
|
435
|
+
return v * 2
|
|
436
|
+
},
|
|
437
|
+
)
|
|
587
438
|
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
601
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
602
|
-
return x * 2
|
|
603
|
-
},
|
|
604
|
-
)
|
|
605
|
-
|
|
606
|
-
// Use effect to test chained async behavior
|
|
607
|
-
let chainedResults: number[] = []
|
|
439
|
+
// Trigger computation
|
|
440
|
+
for (let i = 0; i < doubled.length; i++) {
|
|
441
|
+
try {
|
|
442
|
+
doubled.at(i)?.get()
|
|
443
|
+
} catch {
|
|
444
|
+
// UnsetSignalValueError before resolution
|
|
445
|
+
}
|
|
446
|
+
}
|
|
447
|
+
|
|
448
|
+
await wait(50)
|
|
449
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
450
|
+
})
|
|
608
451
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
452
|
+
test('should handle empty source list', () => {
|
|
453
|
+
const empty = createList<number>([])
|
|
454
|
+
const doubled = empty.deriveCollection((v: number) => v * 2)
|
|
612
455
|
|
|
613
|
-
|
|
614
|
-
|
|
456
|
+
expect(doubled.get()).toEqual([])
|
|
457
|
+
expect(doubled.length).toBe(0)
|
|
458
|
+
})
|
|
615
459
|
|
|
616
|
-
|
|
617
|
-
|
|
460
|
+
test('should return Signal at index', () => {
|
|
461
|
+
const list = createList([1, 2, 3])
|
|
462
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
463
|
+
|
|
464
|
+
expect(doubled.at(0)?.get()).toBe(2)
|
|
465
|
+
expect(doubled.at(1)?.get()).toBe(4)
|
|
466
|
+
expect(doubled.at(2)?.get()).toBe(6)
|
|
467
|
+
expect(doubled.at(5)).toBeUndefined()
|
|
618
468
|
})
|
|
619
469
|
|
|
620
|
-
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
const doubled = new DerivedCollection(
|
|
624
|
-
numbers,
|
|
625
|
-
(x: number) => x * 2,
|
|
626
|
-
)
|
|
627
|
-
const quadrupled = doubled.deriveCollection(
|
|
628
|
-
(x: number) => x * 2,
|
|
629
|
-
)
|
|
630
|
-
|
|
631
|
-
expect(quadrupled.at(0)?.get()).toBe(4)
|
|
632
|
-
expect(quadrupled.at(1)?.get()).toBe(8)
|
|
633
|
-
expect(quadrupled.at(2)?.get()).toBe(12)
|
|
634
|
-
expect(quadrupled.at(10)).toBeUndefined()
|
|
635
|
-
})
|
|
470
|
+
test('should return Signal by source key', () => {
|
|
471
|
+
const list = createList([10, 20])
|
|
472
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
636
473
|
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
(x: number) => x * 2,
|
|
642
|
-
)
|
|
643
|
-
const quadrupled = doubled.deriveCollection(
|
|
644
|
-
(x: number) => x * 2,
|
|
645
|
-
)
|
|
646
|
-
|
|
647
|
-
const key0 = quadrupled.keyAt(0)
|
|
648
|
-
const key1 = quadrupled.keyAt(1)
|
|
649
|
-
|
|
650
|
-
expect(key0).toBeDefined()
|
|
651
|
-
expect(key1).toBeDefined()
|
|
652
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
653
|
-
expect(quadrupled.byKey(key0!)).toBeDefined()
|
|
654
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
655
|
-
expect(quadrupled.byKey(key1!)).toBeDefined()
|
|
656
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
657
|
-
expect(quadrupled.byKey(key0!)?.get()).toBe(4)
|
|
658
|
-
// biome-ignore lint/style/noNonNullAssertion: test
|
|
659
|
-
expect(quadrupled.byKey(key1!)?.get()).toBe(8)
|
|
660
|
-
})
|
|
474
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
475
|
+
const key0 = list.keyAt(0)!
|
|
476
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
477
|
+
const key1 = list.keyAt(1)!
|
|
661
478
|
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
const doubled = new DerivedCollection(
|
|
665
|
-
numbers,
|
|
666
|
-
(x: number) => x * 2,
|
|
667
|
-
)
|
|
668
|
-
const quadrupled = doubled.deriveCollection(
|
|
669
|
-
(x: number) => x * 2,
|
|
670
|
-
)
|
|
671
|
-
|
|
672
|
-
const signals = [...quadrupled]
|
|
673
|
-
expect(signals).toHaveLength(3)
|
|
674
|
-
expect(signals[0].get()).toBe(4)
|
|
675
|
-
expect(signals[1].get()).toBe(8)
|
|
676
|
-
expect(signals[2].get()).toBe(12)
|
|
677
|
-
})
|
|
479
|
+
expect(doubled.byKey(key0)?.get()).toBe(20)
|
|
480
|
+
expect(doubled.byKey(key1)?.get()).toBe(40)
|
|
678
481
|
})
|
|
679
482
|
|
|
680
|
-
|
|
681
|
-
|
|
682
|
-
|
|
683
|
-
const emptyCollection = new DerivedCollection(
|
|
684
|
-
empty,
|
|
685
|
-
(x: number) => x * 2,
|
|
686
|
-
)
|
|
687
|
-
const derived = emptyCollection.deriveCollection(
|
|
688
|
-
(x: number) => x * 2,
|
|
689
|
-
)
|
|
690
|
-
|
|
691
|
-
expect(derived.length).toBe(0)
|
|
692
|
-
expect(derived.get()).toEqual([])
|
|
693
|
-
})
|
|
483
|
+
test('should support keyAt, indexOfKey, and keys', () => {
|
|
484
|
+
const list = createList([10, 20, 30])
|
|
485
|
+
const col = list.deriveCollection((v: number) => v)
|
|
694
486
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
487
|
+
const key0 = col.keyAt(0)
|
|
488
|
+
expect(key0).toBeDefined()
|
|
489
|
+
expect(typeof key0).toBe('string')
|
|
490
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
491
|
+
expect(col.indexOfKey(key0!)).toBe(0)
|
|
492
|
+
expect([...col.keys()]).toHaveLength(3)
|
|
493
|
+
})
|
|
703
494
|
|
|
704
|
-
|
|
705
|
-
|
|
495
|
+
test('should support for...of via Symbol.iterator', () => {
|
|
496
|
+
const list = createList([1, 2, 3])
|
|
497
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
706
498
|
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
const processed = new DerivedCollection(
|
|
714
|
-
items,
|
|
715
|
-
(item: {
|
|
716
|
-
id: number
|
|
717
|
-
data: { value: number; active: boolean }
|
|
718
|
-
}) => ({
|
|
719
|
-
itemId: item.id,
|
|
720
|
-
processedValue: item.data.value * 2,
|
|
721
|
-
status: item.data.active ? 'active' : 'inactive',
|
|
722
|
-
}),
|
|
723
|
-
)
|
|
724
|
-
|
|
725
|
-
const enhanced = processed.deriveCollection(
|
|
726
|
-
(item: {
|
|
727
|
-
itemId: number
|
|
728
|
-
processedValue: number
|
|
729
|
-
status: string
|
|
730
|
-
}) => ({
|
|
731
|
-
...item,
|
|
732
|
-
category: item.processedValue > 15 ? 'high' : 'low',
|
|
733
|
-
}),
|
|
734
|
-
)
|
|
735
|
-
|
|
736
|
-
expect(enhanced.at(0)?.get().itemId).toBe(1)
|
|
737
|
-
expect(enhanced.at(0)?.get().processedValue).toBe(20)
|
|
738
|
-
expect(enhanced.at(0)?.get().status).toBe('active')
|
|
739
|
-
expect(enhanced.at(0)?.get().category).toBe('high')
|
|
740
|
-
expect(enhanced.at(1)?.get().itemId).toBe(2)
|
|
741
|
-
expect(enhanced.at(1)?.get().processedValue).toBe(40)
|
|
742
|
-
expect(enhanced.at(1)?.get().status).toBe('inactive')
|
|
743
|
-
expect(enhanced.at(1)?.get().category).toBe('high')
|
|
744
|
-
})
|
|
499
|
+
const signals = [...doubled]
|
|
500
|
+
expect(signals).toHaveLength(3)
|
|
501
|
+
expect(signals[0].get()).toBe(2)
|
|
502
|
+
expect(signals[1].get()).toBe(4)
|
|
503
|
+
expect(signals[2].get()).toBe(6)
|
|
745
504
|
})
|
|
746
|
-
})
|
|
747
505
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
506
|
+
test('should react to source additions', () => {
|
|
507
|
+
const list = createList([1, 2])
|
|
508
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
751
509
|
|
|
752
|
-
let
|
|
753
|
-
let
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
},
|
|
758
|
-
unwatched: () => {
|
|
759
|
-
collectionUnwatchCalled = true
|
|
760
|
-
},
|
|
510
|
+
let result: number[] = []
|
|
511
|
+
let effectCount = 0
|
|
512
|
+
createEffect(() => {
|
|
513
|
+
result = doubled.get()
|
|
514
|
+
effectCount++
|
|
761
515
|
})
|
|
762
516
|
|
|
763
|
-
expect(
|
|
517
|
+
expect(result).toEqual([2, 4])
|
|
518
|
+
expect(effectCount).toBe(1)
|
|
764
519
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
})
|
|
520
|
+
list.add(3)
|
|
521
|
+
expect(result).toEqual([2, 4, 6])
|
|
522
|
+
expect(effectCount).toBe(2)
|
|
523
|
+
})
|
|
770
524
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
525
|
+
test('should react to source removals', () => {
|
|
526
|
+
const list = createList([1, 2, 3])
|
|
527
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
774
528
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
expect(
|
|
529
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
530
|
+
list.remove(1)
|
|
531
|
+
expect(doubled.get()).toEqual([2, 6])
|
|
532
|
+
expect(doubled.length).toBe(2)
|
|
778
533
|
})
|
|
779
534
|
|
|
780
|
-
test('
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
535
|
+
test('should react to item mutations', () => {
|
|
536
|
+
const list = createList([1, 2])
|
|
537
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
538
|
+
|
|
539
|
+
let result: number[] = []
|
|
540
|
+
createEffect(() => {
|
|
541
|
+
result = doubled.get()
|
|
786
542
|
})
|
|
787
543
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
544
|
+
expect(result).toEqual([2, 4])
|
|
545
|
+
list.at(0)?.set(5)
|
|
546
|
+
expect(result).toEqual([10, 4])
|
|
547
|
+
})
|
|
548
|
+
|
|
549
|
+
test('async collection should react to changes', async () => {
|
|
550
|
+
const list = createList([1, 2])
|
|
551
|
+
const doubled = list.deriveCollection(
|
|
552
|
+
async (v: number, abort: AbortSignal) => {
|
|
553
|
+
await wait(5)
|
|
554
|
+
if (abort.aborted) throw new Error('Aborted')
|
|
555
|
+
return v * 2
|
|
796
556
|
},
|
|
797
|
-
|
|
557
|
+
)
|
|
798
558
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
collectionValue = uppercased.get()
|
|
559
|
+
const values: number[][] = []
|
|
560
|
+
createEffect(() => {
|
|
561
|
+
values.push([...doubled.get()])
|
|
803
562
|
})
|
|
804
563
|
|
|
805
|
-
|
|
806
|
-
expect(
|
|
807
|
-
expect(collectionValue).toEqual(['HELLO', 'WORLD'])
|
|
564
|
+
await wait(20)
|
|
565
|
+
expect(values[values.length - 1]).toEqual([2, 4])
|
|
808
566
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
})
|
|
567
|
+
list.add(3)
|
|
568
|
+
await wait(20)
|
|
569
|
+
expect(values[values.length - 1]).toEqual([2, 4, 6])
|
|
570
|
+
})
|
|
814
571
|
|
|
815
|
-
|
|
572
|
+
test('should chain from collection', () => {
|
|
573
|
+
const list = createList([1, 2, 3])
|
|
574
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
575
|
+
const quadrupled = doubled.deriveCollection((v: number) => v * 2)
|
|
816
576
|
|
|
817
|
-
|
|
818
|
-
collectionCleanup()
|
|
819
|
-
expect(collectionUnwatchedCalled).toBe(true)
|
|
577
|
+
expect(quadrupled.get()).toEqual([4, 8, 12])
|
|
820
578
|
|
|
821
|
-
|
|
579
|
+
list.add(4)
|
|
580
|
+
expect(quadrupled.get()).toEqual([4, 8, 12, 16])
|
|
822
581
|
})
|
|
823
582
|
|
|
824
|
-
test('
|
|
825
|
-
const
|
|
583
|
+
test('should chain from createCollection source', () => {
|
|
584
|
+
const col = createCollection(() => () => {}, { value: [1, 2, 3] })
|
|
585
|
+
const doubled = col.deriveCollection((v: number) => v * 2)
|
|
826
586
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
watched: () => {
|
|
831
|
-
collectionWatchedCalled = true
|
|
832
|
-
},
|
|
833
|
-
unwatched: () => {
|
|
834
|
-
collectionUnwatchedCalled = true
|
|
835
|
-
},
|
|
836
|
-
})
|
|
587
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
588
|
+
expect(isCollection(doubled)).toBe(true)
|
|
589
|
+
})
|
|
837
590
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
591
|
+
test('should propagate errors from per-item memos', () => {
|
|
592
|
+
const list = createList([1, 2, 3])
|
|
593
|
+
const mapped = list.deriveCollection((v: number) => {
|
|
594
|
+
if (v === 2) throw new Error('bad item')
|
|
595
|
+
return v * 2
|
|
842
596
|
})
|
|
843
597
|
|
|
844
|
-
expect(
|
|
845
|
-
expect(effectValue).toBe(3)
|
|
846
|
-
expect(collectionUnwatchedCalled).toBe(false)
|
|
847
|
-
|
|
848
|
-
cleanup()
|
|
849
|
-
expect(collectionUnwatchedCalled).toBe(true)
|
|
598
|
+
expect(() => mapped.get()).toThrow('bad item')
|
|
850
599
|
})
|
|
851
600
|
})
|
|
852
601
|
})
|