@zeix/cause-effect 0.17.3 → 0.18.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 +163 -232
- package/.cursorrules +41 -35
- package/.github/copilot-instructions.md +166 -116
- package/ARCHITECTURE.md +274 -0
- package/CLAUDE.md +199 -143
- package/COLLECTION_REFACTORING.md +161 -0
- package/GUIDE.md +298 -0
- package/README.md +232 -197
- package/REQUIREMENTS.md +100 -0
- package/bench/reactivity.bench.ts +577 -0
- package/index.dev.js +1325 -997
- package/index.js +1 -1
- package/index.ts +58 -74
- package/package.json +4 -1
- package/src/errors.ts +118 -74
- package/src/graph.ts +601 -0
- package/src/nodes/collection.ts +474 -0
- package/src/nodes/effect.ts +149 -0
- package/src/nodes/list.ts +588 -0
- package/src/nodes/memo.ts +120 -0
- package/src/nodes/sensor.ts +139 -0
- package/src/nodes/state.ts +135 -0
- package/src/nodes/store.ts +383 -0
- package/src/nodes/task.ts +146 -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 +466 -706
- package/test/effect.test.ts +293 -696
- package/test/list.test.ts +335 -592
- package/test/memo.test.ts +380 -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 +395 -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 +208 -0
- package/types/src/nodes/collection.d.ts +64 -0
- package/types/src/nodes/effect.d.ts +48 -0
- package/types/src/nodes/list.d.ts +65 -0
- package/types/src/nodes/memo.d.ts +57 -0
- package/types/src/nodes/sensor.d.ts +75 -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 +73 -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,612 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
+
batch,
|
|
4
|
+
createCollection,
|
|
3
5
|
createEffect,
|
|
4
|
-
|
|
5
|
-
|
|
6
|
+
createList,
|
|
7
|
+
createScope,
|
|
8
|
+
createState,
|
|
9
|
+
type DiffResult,
|
|
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: (_key, 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
|
+
)
|
|
135
|
+
|
|
136
|
+
expect(started).toBe(false)
|
|
160
137
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
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
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
createEffect(() => {
|
|
201
|
-
lastArray = doubled.get()
|
|
202
|
-
arrayEffectRuns++
|
|
175
|
+
describe('applyChanges', () => {
|
|
176
|
+
test('should add items', () => {
|
|
177
|
+
let apply: ((changes: DiffResult) => void) | undefined
|
|
178
|
+
const col = createCollection<number>(applyChanges => {
|
|
179
|
+
apply = applyChanges
|
|
180
|
+
return () => {}
|
|
203
181
|
})
|
|
204
182
|
|
|
205
|
-
|
|
206
|
-
|
|
183
|
+
const values: number[][] = []
|
|
184
|
+
const dispose = createScope(() => {
|
|
185
|
+
createEffect(() => {
|
|
186
|
+
values.push(col.get())
|
|
187
|
+
})
|
|
188
|
+
})
|
|
207
189
|
|
|
208
|
-
|
|
209
|
-
expect(lastArray).toEqual([2, 20, 6])
|
|
210
|
-
expect(arrayEffectRuns).toBe(2)
|
|
211
|
-
})
|
|
212
|
-
})
|
|
190
|
+
expect(values).toEqual([[]])
|
|
213
191
|
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
192
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
193
|
+
apply!({
|
|
194
|
+
changed: true,
|
|
195
|
+
add: { a: 1, b: 2 },
|
|
196
|
+
change: {},
|
|
197
|
+
remove: {},
|
|
198
|
+
})
|
|
219
199
|
|
|
220
|
-
expect(
|
|
221
|
-
expect(
|
|
222
|
-
expect(
|
|
223
|
-
expect(signals[2].get()).toBe(6)
|
|
224
|
-
})
|
|
200
|
+
expect(values.length).toBe(2)
|
|
201
|
+
expect(values[1]).toEqual([1, 2])
|
|
202
|
+
expect(col.length).toBe(2)
|
|
225
203
|
|
|
226
|
-
|
|
227
|
-
const numbers = new List([1, 2, 3])
|
|
228
|
-
const collection = new DerivedCollection(numbers, (x: number) => x)
|
|
229
|
-
expect(collection[Symbol.isConcatSpreadable]).toBe(true)
|
|
204
|
+
dispose()
|
|
230
205
|
})
|
|
231
|
-
})
|
|
232
206
|
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
const
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
207
|
+
test('should change item values', () => {
|
|
208
|
+
let apply: ((changes: DiffResult) => void) | undefined
|
|
209
|
+
const col = createCollection(
|
|
210
|
+
applyChanges => {
|
|
211
|
+
apply = applyChanges
|
|
212
|
+
return () => {}
|
|
213
|
+
},
|
|
214
|
+
{
|
|
215
|
+
value: [{ id: 'x', val: 1 }],
|
|
216
|
+
keyConfig: item => item.id,
|
|
217
|
+
},
|
|
239
218
|
)
|
|
240
|
-
expect(collection.length).toBe(0)
|
|
241
|
-
expect(collection.get()).toEqual([])
|
|
242
|
-
})
|
|
243
219
|
|
|
244
|
-
|
|
245
|
-
const
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
220
|
+
const values: { id: string; val: number }[][] = []
|
|
221
|
+
const dispose = createScope(() => {
|
|
222
|
+
createEffect(() => {
|
|
223
|
+
values.push(col.get())
|
|
224
|
+
})
|
|
225
|
+
})
|
|
249
226
|
|
|
250
|
-
|
|
251
|
-
expect(processed.get()).toEqual([3])
|
|
252
|
-
})
|
|
227
|
+
expect(values[0]).toEqual([{ id: 'x', val: 1 }])
|
|
253
228
|
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
229
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
230
|
+
apply!({
|
|
231
|
+
changed: true,
|
|
232
|
+
add: {},
|
|
233
|
+
change: { x: { id: 'x', val: 42 } },
|
|
234
|
+
remove: {},
|
|
235
|
+
})
|
|
259
236
|
|
|
260
|
-
expect(
|
|
261
|
-
expect(
|
|
262
|
-
})
|
|
263
|
-
})
|
|
237
|
+
expect(values.length).toBe(2)
|
|
238
|
+
expect(values[1]).toEqual([{ id: 'x', val: 42 }])
|
|
264
239
|
|
|
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
|
-
})
|
|
240
|
+
dispose()
|
|
241
|
+
})
|
|
282
242
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
}),
|
|
300
|
-
)
|
|
243
|
+
test('should remove items', () => {
|
|
244
|
+
let apply: ((changes: DiffResult) => void) | 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
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
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)
|
|
267
|
+
expect(values[0]).toEqual([
|
|
268
|
+
{ id: 'a', v: 1 },
|
|
269
|
+
{ id: 'b', v: 2 },
|
|
270
|
+
{ id: 'c', v: 3 },
|
|
271
|
+
])
|
|
272
|
+
|
|
273
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
274
|
+
apply!({
|
|
275
|
+
changed: true,
|
|
276
|
+
add: {},
|
|
277
|
+
change: {},
|
|
278
|
+
remove: { b: null },
|
|
339
279
|
})
|
|
340
280
|
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
const quadrupled = doubled.deriveCollection(
|
|
348
|
-
(x: number) => x * 2,
|
|
349
|
-
)
|
|
350
|
-
|
|
351
|
-
let collectionValue: number[] = []
|
|
352
|
-
let effectRuns = 0
|
|
353
|
-
createEffect(() => {
|
|
354
|
-
collectionValue = quadrupled.get()
|
|
355
|
-
effectRuns++
|
|
356
|
-
})
|
|
281
|
+
expect(values.length).toBe(2)
|
|
282
|
+
expect(values[1]).toEqual([
|
|
283
|
+
{ id: 'a', v: 1 },
|
|
284
|
+
{ id: 'c', v: 3 },
|
|
285
|
+
])
|
|
286
|
+
expect(col.length).toBe(2)
|
|
357
287
|
|
|
358
|
-
|
|
359
|
-
|
|
288
|
+
dispose()
|
|
289
|
+
})
|
|
360
290
|
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
291
|
+
test('should handle mixed add/change/remove', () => {
|
|
292
|
+
let apply: ((changes: DiffResult) => void) | undefined
|
|
293
|
+
const col = createCollection(
|
|
294
|
+
applyChanges => {
|
|
295
|
+
apply = applyChanges
|
|
296
|
+
return () => {}
|
|
297
|
+
},
|
|
298
|
+
{
|
|
299
|
+
value: [
|
|
300
|
+
{ id: 'a', v: 1 },
|
|
301
|
+
{ id: 'b', v: 2 },
|
|
302
|
+
],
|
|
303
|
+
keyConfig: item => item.id,
|
|
304
|
+
},
|
|
305
|
+
)
|
|
364
306
|
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
307
|
+
const values: { id: string; v: number }[][] = []
|
|
308
|
+
const dispose = createScope(() => {
|
|
309
|
+
createEffect(() => {
|
|
310
|
+
values.push(col.get())
|
|
311
|
+
})
|
|
368
312
|
})
|
|
369
313
|
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
(x: number) => x * 2,
|
|
378
|
-
)
|
|
314
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
315
|
+
apply!({
|
|
316
|
+
changed: true,
|
|
317
|
+
add: { c: { id: 'c', v: 3 } },
|
|
318
|
+
change: { a: { id: 'a', v: 10 } },
|
|
319
|
+
remove: { b: null },
|
|
320
|
+
})
|
|
379
321
|
|
|
380
|
-
|
|
322
|
+
expect(values.length).toBe(2)
|
|
323
|
+
expect(values[1]).toEqual([
|
|
324
|
+
{ id: 'a', v: 10 },
|
|
325
|
+
{ id: 'c', v: 3 },
|
|
326
|
+
])
|
|
381
327
|
|
|
382
|
-
|
|
383
|
-
expect(quadrupled.get()).toEqual([4, 12, 16])
|
|
384
|
-
})
|
|
328
|
+
dispose()
|
|
385
329
|
})
|
|
386
330
|
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
(
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
async (x: number, abort: AbortSignal) => {
|
|
397
|
-
await new Promise(resolve => setTimeout(resolve, 10))
|
|
398
|
-
if (abort.aborted) throw new Error('Aborted')
|
|
399
|
-
return x * 2
|
|
400
|
-
},
|
|
401
|
-
)
|
|
331
|
+
test('should skip when changed is false', () => {
|
|
332
|
+
let apply: ((changes: DiffResult) => void) | 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!({ changed: false, add: {}, change: {}, remove: {} })
|
|
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: ((changes: DiffResult) => void) | undefined
|
|
361
|
+
const col = createCollection<string>(applyChanges => {
|
|
362
|
+
apply = applyChanges
|
|
363
|
+
return () => {}
|
|
427
364
|
})
|
|
428
365
|
|
|
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
|
-
|
|
366
|
+
let effectCount = 0
|
|
367
|
+
const dispose = createScope(() => {
|
|
469
368
|
createEffect(() => {
|
|
470
|
-
|
|
369
|
+
void col.length
|
|
370
|
+
effectCount++
|
|
471
371
|
})
|
|
372
|
+
})
|
|
472
373
|
|
|
473
|
-
|
|
474
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
374
|
+
expect(effectCount).toBe(1)
|
|
475
375
|
|
|
476
|
-
|
|
376
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
377
|
+
apply!({
|
|
378
|
+
changed: true,
|
|
379
|
+
add: { a: 'hello' },
|
|
380
|
+
change: {},
|
|
381
|
+
remove: {},
|
|
382
|
+
})
|
|
477
383
|
|
|
478
|
-
|
|
384
|
+
expect(effectCount).toBe(2)
|
|
385
|
+
expect(col.length).toBe(1)
|
|
479
386
|
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
expect(result1?.slug).toBe('alice')
|
|
483
|
-
expect(typeof result1?.timestamp).toBe('number')
|
|
387
|
+
dispose()
|
|
388
|
+
})
|
|
484
389
|
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
390
|
+
test('should batch multiple calls', () => {
|
|
391
|
+
let apply: ((changes: DiffResult) => void) | undefined
|
|
392
|
+
const col = createCollection<number>(applyChanges => {
|
|
393
|
+
apply = applyChanges
|
|
394
|
+
return () => {}
|
|
489
395
|
})
|
|
490
396
|
|
|
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[][] = []
|
|
397
|
+
let effectCount = 0
|
|
398
|
+
const dispose = createScope(() => {
|
|
506
399
|
createEffect(() => {
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
effectValues.push(currentValue)
|
|
400
|
+
void col.get()
|
|
401
|
+
effectCount++
|
|
510
402
|
})
|
|
403
|
+
})
|
|
511
404
|
|
|
512
|
-
|
|
513
|
-
await new Promise(resolve => setTimeout(resolve, 50))
|
|
405
|
+
expect(effectCount).toBe(1)
|
|
514
406
|
|
|
515
|
-
|
|
516
|
-
|
|
407
|
+
batch(() => {
|
|
408
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
409
|
+
apply!({
|
|
410
|
+
changed: true,
|
|
411
|
+
add: { a: 1 },
|
|
412
|
+
change: {},
|
|
413
|
+
remove: {},
|
|
414
|
+
})
|
|
415
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
416
|
+
apply!({
|
|
417
|
+
changed: true,
|
|
418
|
+
add: { b: 2 },
|
|
419
|
+
change: {},
|
|
420
|
+
remove: {},
|
|
421
|
+
})
|
|
422
|
+
})
|
|
517
423
|
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
asyncQuadrupled.at(1)?.get()
|
|
521
|
-
asyncQuadrupled.at(2)?.get()
|
|
424
|
+
expect(effectCount).toBe(2)
|
|
425
|
+
expect(col.get()).toEqual([1, 2])
|
|
522
426
|
|
|
523
|
-
|
|
524
|
-
|
|
427
|
+
dispose()
|
|
428
|
+
})
|
|
429
|
+
})
|
|
525
430
|
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
431
|
+
describe('deriveCollection', () => {
|
|
432
|
+
test('should transform list values with sync callback', () => {
|
|
433
|
+
const numbers = createList([1, 2, 3])
|
|
434
|
+
const doubled = numbers.deriveCollection((v: number) => v * 2)
|
|
530
435
|
|
|
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
|
-
})
|
|
436
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
437
|
+
expect(doubled.length).toBe(3)
|
|
567
438
|
})
|
|
568
439
|
|
|
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
|
-
})
|
|
440
|
+
test('should transform values with async callback', async () => {
|
|
441
|
+
const numbers = createList([1, 2, 3])
|
|
442
|
+
const doubled = numbers.deriveCollection(
|
|
443
|
+
async (v: number, abort: AbortSignal) => {
|
|
444
|
+
await wait(10)
|
|
445
|
+
if (abort.aborted) throw new Error('Aborted')
|
|
446
|
+
return v * 2
|
|
447
|
+
},
|
|
448
|
+
)
|
|
587
449
|
|
|
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[] = []
|
|
450
|
+
// Trigger computation
|
|
451
|
+
for (let i = 0; i < doubled.length; i++) {
|
|
452
|
+
try {
|
|
453
|
+
doubled.at(i)?.get()
|
|
454
|
+
} catch {
|
|
455
|
+
// UnsetSignalValueError before resolution
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
|
|
459
|
+
await wait(50)
|
|
460
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
461
|
+
})
|
|
608
462
|
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
463
|
+
test('should handle empty source list', () => {
|
|
464
|
+
const empty = createList<number>([])
|
|
465
|
+
const doubled = empty.deriveCollection((v: number) => v * 2)
|
|
612
466
|
|
|
613
|
-
|
|
614
|
-
|
|
467
|
+
expect(doubled.get()).toEqual([])
|
|
468
|
+
expect(doubled.length).toBe(0)
|
|
469
|
+
})
|
|
615
470
|
|
|
616
|
-
|
|
617
|
-
|
|
471
|
+
test('should return Signal at index', () => {
|
|
472
|
+
const list = createList([1, 2, 3])
|
|
473
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
474
|
+
|
|
475
|
+
expect(doubled.at(0)?.get()).toBe(2)
|
|
476
|
+
expect(doubled.at(1)?.get()).toBe(4)
|
|
477
|
+
expect(doubled.at(2)?.get()).toBe(6)
|
|
478
|
+
expect(doubled.at(5)).toBeUndefined()
|
|
618
479
|
})
|
|
619
480
|
|
|
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
|
-
})
|
|
481
|
+
test('should return Signal by source key', () => {
|
|
482
|
+
const list = createList([10, 20])
|
|
483
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
636
484
|
|
|
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
|
-
})
|
|
485
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
486
|
+
const key0 = list.keyAt(0)!
|
|
487
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
488
|
+
const key1 = list.keyAt(1)!
|
|
661
489
|
|
|
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
|
-
})
|
|
490
|
+
expect(doubled.byKey(key0)?.get()).toBe(20)
|
|
491
|
+
expect(doubled.byKey(key1)?.get()).toBe(40)
|
|
678
492
|
})
|
|
679
493
|
|
|
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
|
-
})
|
|
494
|
+
test('should support keyAt, indexOfKey, and keys', () => {
|
|
495
|
+
const list = createList([10, 20, 30])
|
|
496
|
+
const col = list.deriveCollection((v: number) => v)
|
|
694
497
|
|
|
695
|
-
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
701
|
-
|
|
702
|
-
)
|
|
498
|
+
const key0 = col.keyAt(0)
|
|
499
|
+
expect(key0).toBeDefined()
|
|
500
|
+
expect(typeof key0).toBe('string')
|
|
501
|
+
// biome-ignore lint/style/noNonNullAssertion: index is within bounds
|
|
502
|
+
expect(col.indexOfKey(key0!)).toBe(0)
|
|
503
|
+
expect([...col.keys()]).toHaveLength(3)
|
|
504
|
+
})
|
|
703
505
|
|
|
704
|
-
|
|
705
|
-
|
|
506
|
+
test('should support for...of via Symbol.iterator', () => {
|
|
507
|
+
const list = createList([1, 2, 3])
|
|
508
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
706
509
|
|
|
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
|
-
})
|
|
510
|
+
const signals = [...doubled]
|
|
511
|
+
expect(signals).toHaveLength(3)
|
|
512
|
+
expect(signals[0].get()).toBe(2)
|
|
513
|
+
expect(signals[1].get()).toBe(4)
|
|
514
|
+
expect(signals[2].get()).toBe(6)
|
|
745
515
|
})
|
|
746
|
-
})
|
|
747
516
|
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
const
|
|
517
|
+
test('should react to source additions', () => {
|
|
518
|
+
const list = createList([1, 2])
|
|
519
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
751
520
|
|
|
752
|
-
let
|
|
753
|
-
let
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
},
|
|
758
|
-
unwatched: () => {
|
|
759
|
-
collectionUnwatchCalled = true
|
|
760
|
-
},
|
|
521
|
+
let result: number[] = []
|
|
522
|
+
let effectCount = 0
|
|
523
|
+
createEffect(() => {
|
|
524
|
+
result = doubled.get()
|
|
525
|
+
effectCount++
|
|
761
526
|
})
|
|
762
527
|
|
|
763
|
-
expect(
|
|
528
|
+
expect(result).toEqual([2, 4])
|
|
529
|
+
expect(effectCount).toBe(1)
|
|
764
530
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
})
|
|
531
|
+
list.add(3)
|
|
532
|
+
expect(result).toEqual([2, 4, 6])
|
|
533
|
+
expect(effectCount).toBe(2)
|
|
534
|
+
})
|
|
770
535
|
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
536
|
+
test('should react to source removals', () => {
|
|
537
|
+
const list = createList([1, 2, 3])
|
|
538
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
774
539
|
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
expect(
|
|
540
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
541
|
+
list.remove(1)
|
|
542
|
+
expect(doubled.get()).toEqual([2, 6])
|
|
543
|
+
expect(doubled.length).toBe(2)
|
|
778
544
|
})
|
|
779
545
|
|
|
780
|
-
test('
|
|
781
|
-
|
|
782
|
-
const
|
|
783
|
-
|
|
784
|
-
|
|
785
|
-
|
|
546
|
+
test('should react to item mutations', () => {
|
|
547
|
+
const list = createList([1, 2])
|
|
548
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
549
|
+
|
|
550
|
+
let result: number[] = []
|
|
551
|
+
createEffect(() => {
|
|
552
|
+
result = doubled.get()
|
|
786
553
|
})
|
|
787
554
|
|
|
788
|
-
|
|
789
|
-
|
|
790
|
-
|
|
791
|
-
|
|
792
|
-
|
|
793
|
-
|
|
794
|
-
|
|
795
|
-
|
|
555
|
+
expect(result).toEqual([2, 4])
|
|
556
|
+
list.at(0)?.set(5)
|
|
557
|
+
expect(result).toEqual([10, 4])
|
|
558
|
+
})
|
|
559
|
+
|
|
560
|
+
test('async collection should react to changes', async () => {
|
|
561
|
+
const list = createList([1, 2])
|
|
562
|
+
const doubled = list.deriveCollection(
|
|
563
|
+
async (v: number, abort: AbortSignal) => {
|
|
564
|
+
await wait(5)
|
|
565
|
+
if (abort.aborted) throw new Error('Aborted')
|
|
566
|
+
return v * 2
|
|
796
567
|
},
|
|
797
|
-
|
|
568
|
+
)
|
|
798
569
|
|
|
799
|
-
|
|
800
|
-
|
|
801
|
-
|
|
802
|
-
collectionValue = uppercased.get()
|
|
570
|
+
const values: number[][] = []
|
|
571
|
+
createEffect(() => {
|
|
572
|
+
values.push([...doubled.get()])
|
|
803
573
|
})
|
|
804
574
|
|
|
805
|
-
|
|
806
|
-
expect(
|
|
807
|
-
expect(collectionValue).toEqual(['HELLO', 'WORLD'])
|
|
575
|
+
await wait(20)
|
|
576
|
+
expect(values[values.length - 1]).toEqual([2, 4])
|
|
808
577
|
|
|
809
|
-
|
|
810
|
-
|
|
811
|
-
|
|
812
|
-
|
|
813
|
-
})
|
|
578
|
+
list.add(3)
|
|
579
|
+
await wait(20)
|
|
580
|
+
expect(values[values.length - 1]).toEqual([2, 4, 6])
|
|
581
|
+
})
|
|
814
582
|
|
|
815
|
-
|
|
583
|
+
test('should chain from collection', () => {
|
|
584
|
+
const list = createList([1, 2, 3])
|
|
585
|
+
const doubled = list.deriveCollection((v: number) => v * 2)
|
|
586
|
+
const quadrupled = doubled.deriveCollection((v: number) => v * 2)
|
|
816
587
|
|
|
817
|
-
|
|
818
|
-
collectionCleanup()
|
|
819
|
-
expect(collectionUnwatchedCalled).toBe(true)
|
|
588
|
+
expect(quadrupled.get()).toEqual([4, 8, 12])
|
|
820
589
|
|
|
821
|
-
|
|
590
|
+
list.add(4)
|
|
591
|
+
expect(quadrupled.get()).toEqual([4, 8, 12, 16])
|
|
822
592
|
})
|
|
823
593
|
|
|
824
|
-
test('
|
|
825
|
-
const
|
|
594
|
+
test('should chain from createCollection source', () => {
|
|
595
|
+
const col = createCollection(() => () => {}, { value: [1, 2, 3] })
|
|
596
|
+
const doubled = col.deriveCollection((v: number) => v * 2)
|
|
826
597
|
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
watched: () => {
|
|
831
|
-
collectionWatchedCalled = true
|
|
832
|
-
},
|
|
833
|
-
unwatched: () => {
|
|
834
|
-
collectionUnwatchedCalled = true
|
|
835
|
-
},
|
|
836
|
-
})
|
|
598
|
+
expect(doubled.get()).toEqual([2, 4, 6])
|
|
599
|
+
expect(isCollection(doubled)).toBe(true)
|
|
600
|
+
})
|
|
837
601
|
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
const
|
|
841
|
-
|
|
602
|
+
test('should propagate errors from per-item memos', () => {
|
|
603
|
+
const list = createList([1, 2, 3])
|
|
604
|
+
const mapped = list.deriveCollection((v: number) => {
|
|
605
|
+
if (v === 2) throw new Error('bad item')
|
|
606
|
+
return v * 2
|
|
842
607
|
})
|
|
843
608
|
|
|
844
|
-
expect(
|
|
845
|
-
expect(effectValue).toBe(3)
|
|
846
|
-
expect(collectionUnwatchedCalled).toBe(false)
|
|
847
|
-
|
|
848
|
-
cleanup()
|
|
849
|
-
expect(collectionUnwatchedCalled).toBe(true)
|
|
609
|
+
expect(() => mapped.get()).toThrow('bad item')
|
|
850
610
|
})
|
|
851
611
|
})
|
|
852
612
|
})
|