@zeix/cause-effect 0.17.0 → 0.17.2
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 +26 -5
- package/.cursorrules +8 -3
- package/.github/copilot-instructions.md +13 -4
- package/CLAUDE.md +191 -262
- package/README.md +268 -420
- package/archive/collection.ts +23 -25
- package/archive/computed.ts +5 -4
- package/archive/list.ts +21 -28
- package/archive/memo.ts +4 -2
- package/archive/state.ts +2 -1
- package/archive/store.ts +21 -32
- package/archive/task.ts +6 -9
- package/index.dev.js +411 -220
- package/index.js +1 -1
- package/index.ts +25 -8
- package/package.json +1 -1
- package/src/classes/collection.ts +103 -77
- package/src/classes/composite.ts +28 -33
- package/src/classes/computed.ts +90 -31
- package/src/classes/list.ts +39 -33
- package/src/classes/ref.ts +96 -0
- package/src/classes/state.ts +41 -8
- package/src/classes/store.ts +47 -30
- package/src/diff.ts +2 -1
- package/src/effect.ts +19 -9
- package/src/errors.ts +31 -1
- package/src/match.ts +5 -12
- package/src/resolve.ts +3 -2
- package/src/signal.ts +0 -1
- package/src/system.ts +159 -43
- package/src/util.ts +0 -10
- package/test/collection.test.ts +383 -67
- package/test/computed.test.ts +268 -11
- package/test/effect.test.ts +2 -2
- package/test/list.test.ts +249 -21
- package/test/ref.test.ts +381 -0
- package/test/state.test.ts +13 -13
- package/test/store.test.ts +473 -28
- package/types/index.d.ts +6 -5
- package/types/src/classes/collection.d.ts +27 -12
- package/types/src/classes/composite.d.ts +4 -4
- package/types/src/classes/computed.d.ts +17 -0
- package/types/src/classes/list.d.ts +6 -6
- package/types/src/classes/ref.d.ts +48 -0
- package/types/src/classes/state.d.ts +9 -0
- package/types/src/classes/store.d.ts +4 -4
- package/types/src/effect.d.ts +1 -2
- package/types/src/errors.d.ts +9 -1
- package/types/src/system.d.ts +40 -24
- package/types/src/util.d.ts +1 -3
package/test/collection.test.ts
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
-
Collection,
|
|
4
3
|
createEffect,
|
|
5
4
|
createStore,
|
|
5
|
+
DerivedCollection,
|
|
6
6
|
isCollection,
|
|
7
7
|
List,
|
|
8
8
|
UNSET,
|
|
@@ -12,7 +12,7 @@ describe('collection', () => {
|
|
|
12
12
|
describe('creation and basic operations', () => {
|
|
13
13
|
test('creates collection with initial values from list', () => {
|
|
14
14
|
const numbers = new List([1, 2, 3])
|
|
15
|
-
const doubled = new
|
|
15
|
+
const doubled = new DerivedCollection(
|
|
16
16
|
numbers,
|
|
17
17
|
(value: number) => value * 2,
|
|
18
18
|
)
|
|
@@ -24,7 +24,7 @@ describe('collection', () => {
|
|
|
24
24
|
})
|
|
25
25
|
|
|
26
26
|
test('creates collection from function source', () => {
|
|
27
|
-
const doubled = new
|
|
27
|
+
const doubled = new DerivedCollection(
|
|
28
28
|
() => new List([10, 20, 30]),
|
|
29
29
|
(value: number) => value * 2,
|
|
30
30
|
)
|
|
@@ -37,7 +37,7 @@ describe('collection', () => {
|
|
|
37
37
|
|
|
38
38
|
test('has Symbol.toStringTag of Collection', () => {
|
|
39
39
|
const list = new List([1, 2, 3])
|
|
40
|
-
const collection = new
|
|
40
|
+
const collection = new DerivedCollection(list, (x: number) => x)
|
|
41
41
|
expect(Object.prototype.toString.call(collection)).toBe(
|
|
42
42
|
'[object Collection]',
|
|
43
43
|
)
|
|
@@ -46,7 +46,7 @@ describe('collection', () => {
|
|
|
46
46
|
test('isCollection identifies collection instances correctly', () => {
|
|
47
47
|
const store = createStore({ a: 1 })
|
|
48
48
|
const list = new List([1, 2, 3])
|
|
49
|
-
const collection = new
|
|
49
|
+
const collection = new DerivedCollection(list, (x: number) => x)
|
|
50
50
|
|
|
51
51
|
expect(isCollection(collection)).toBe(true)
|
|
52
52
|
expect(isCollection(list)).toBe(false)
|
|
@@ -57,7 +57,7 @@ describe('collection', () => {
|
|
|
57
57
|
|
|
58
58
|
test('get() returns the complete collection value', () => {
|
|
59
59
|
const numbers = new List([1, 2, 3])
|
|
60
|
-
const doubled = new
|
|
60
|
+
const doubled = new DerivedCollection(
|
|
61
61
|
numbers,
|
|
62
62
|
(value: number) => value * 2,
|
|
63
63
|
)
|
|
@@ -71,13 +71,19 @@ describe('collection', () => {
|
|
|
71
71
|
describe('length property and sizing', () => {
|
|
72
72
|
test('length property works for collections', () => {
|
|
73
73
|
const numbers = new List([1, 2, 3, 4, 5])
|
|
74
|
-
const collection = new
|
|
74
|
+
const collection = new DerivedCollection(
|
|
75
|
+
numbers,
|
|
76
|
+
(x: number) => x * 2,
|
|
77
|
+
)
|
|
75
78
|
expect(collection.length).toBe(5)
|
|
76
79
|
})
|
|
77
80
|
|
|
78
81
|
test('length is reactive and updates with changes', () => {
|
|
79
82
|
const items = new List([1, 2])
|
|
80
|
-
const collection = new
|
|
83
|
+
const collection = new DerivedCollection(
|
|
84
|
+
items,
|
|
85
|
+
(x: number) => x * 2,
|
|
86
|
+
)
|
|
81
87
|
|
|
82
88
|
expect(collection.length).toBe(2)
|
|
83
89
|
items.add(3)
|
|
@@ -88,7 +94,7 @@ describe('collection', () => {
|
|
|
88
94
|
describe('index-based access', () => {
|
|
89
95
|
test('properties can be accessed via computed signals', () => {
|
|
90
96
|
const items = new List([10, 20, 30])
|
|
91
|
-
const doubled = new
|
|
97
|
+
const doubled = new DerivedCollection(items, (x: number) => x * 2)
|
|
92
98
|
|
|
93
99
|
expect(doubled.at(0)?.get()).toBe(20)
|
|
94
100
|
expect(doubled.at(1)?.get()).toBe(40)
|
|
@@ -97,13 +103,16 @@ describe('collection', () => {
|
|
|
97
103
|
|
|
98
104
|
test('returns undefined for non-existent properties', () => {
|
|
99
105
|
const items = new List([1, 2])
|
|
100
|
-
const collection = new
|
|
106
|
+
const collection = new DerivedCollection(items, (x: number) => x)
|
|
101
107
|
expect(collection[5]).toBeUndefined()
|
|
102
108
|
})
|
|
103
109
|
|
|
104
110
|
test('supports numeric key access', () => {
|
|
105
111
|
const numbers = new List([1, 2, 3])
|
|
106
|
-
const collection = new
|
|
112
|
+
const collection = new DerivedCollection(
|
|
113
|
+
numbers,
|
|
114
|
+
(x: number) => x * 2,
|
|
115
|
+
)
|
|
107
116
|
expect(collection.at(1)?.get()).toBe(4)
|
|
108
117
|
})
|
|
109
118
|
})
|
|
@@ -111,7 +120,7 @@ describe('collection', () => {
|
|
|
111
120
|
describe('key-based access methods', () => {
|
|
112
121
|
test('byKey() returns computed signal for existing keys', () => {
|
|
113
122
|
const numbers = new List([1, 2, 3])
|
|
114
|
-
const doubled = new
|
|
123
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
115
124
|
|
|
116
125
|
const key0 = numbers.keyAt(0)
|
|
117
126
|
const key1 = numbers.keyAt(1)
|
|
@@ -130,7 +139,7 @@ describe('collection', () => {
|
|
|
130
139
|
|
|
131
140
|
test('keyAt() and indexOfKey() work correctly', () => {
|
|
132
141
|
const numbers = new List([5, 10, 15])
|
|
133
|
-
const doubled = new
|
|
142
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
134
143
|
|
|
135
144
|
const key0 = doubled.keyAt(0)
|
|
136
145
|
const key1 = doubled.keyAt(1)
|
|
@@ -147,7 +156,7 @@ describe('collection', () => {
|
|
|
147
156
|
describe('reactivity', () => {
|
|
148
157
|
test('collection-level get() is reactive', () => {
|
|
149
158
|
const numbers = new List([1, 2, 3])
|
|
150
|
-
const doubled = new
|
|
159
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
151
160
|
|
|
152
161
|
let lastArray: number[] = []
|
|
153
162
|
createEffect(() => {
|
|
@@ -161,7 +170,7 @@ describe('collection', () => {
|
|
|
161
170
|
|
|
162
171
|
test('individual signal reactivity works', () => {
|
|
163
172
|
const items = new List([{ count: 1 }, { count: 2 }])
|
|
164
|
-
const doubled = new
|
|
173
|
+
const doubled = new DerivedCollection(
|
|
165
174
|
items,
|
|
166
175
|
(item: { count: number }) => ({ count: item.count * 2 }),
|
|
167
176
|
)
|
|
@@ -184,7 +193,7 @@ describe('collection', () => {
|
|
|
184
193
|
|
|
185
194
|
test('updates are reactive', () => {
|
|
186
195
|
const numbers = new List([1, 2, 3])
|
|
187
|
-
const doubled = new
|
|
196
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
188
197
|
|
|
189
198
|
let lastArray: number[] = []
|
|
190
199
|
let arrayEffectRuns = 0
|
|
@@ -205,7 +214,7 @@ describe('collection', () => {
|
|
|
205
214
|
describe('iteration and spreading', () => {
|
|
206
215
|
test('supports for...of iteration', () => {
|
|
207
216
|
const numbers = new List([1, 2, 3])
|
|
208
|
-
const doubled = new
|
|
217
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
209
218
|
const signals = [...doubled]
|
|
210
219
|
|
|
211
220
|
expect(signals).toHaveLength(3)
|
|
@@ -216,51 +225,51 @@ describe('collection', () => {
|
|
|
216
225
|
|
|
217
226
|
test('Symbol.isConcatSpreadable is true', () => {
|
|
218
227
|
const numbers = new List([1, 2, 3])
|
|
219
|
-
const collection = new
|
|
228
|
+
const collection = new DerivedCollection(numbers, (x: number) => x)
|
|
220
229
|
expect(collection[Symbol.isConcatSpreadable]).toBe(true)
|
|
221
230
|
})
|
|
222
231
|
})
|
|
223
232
|
|
|
224
|
-
describe('
|
|
225
|
-
test('
|
|
233
|
+
describe('Hooks', () => {
|
|
234
|
+
test('triggers HOOK_ADD when items are added', () => {
|
|
226
235
|
const numbers = new List([1, 2])
|
|
227
|
-
const doubled = new
|
|
236
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
228
237
|
|
|
229
|
-
let
|
|
238
|
+
let addedKeys: readonly string[] | undefined
|
|
230
239
|
doubled.on('add', keys => {
|
|
231
|
-
|
|
240
|
+
addedKeys = keys
|
|
232
241
|
})
|
|
233
242
|
|
|
234
243
|
numbers.add(3)
|
|
235
|
-
expect(
|
|
236
|
-
|
|
237
|
-
expect(doubled.byKey(
|
|
244
|
+
expect(addedKeys).toHaveLength(1)
|
|
245
|
+
const doubledKey = addedKeys?.[0]
|
|
246
|
+
if (doubledKey) expect(doubled.byKey(doubledKey)?.get()).toBe(6)
|
|
238
247
|
})
|
|
239
248
|
|
|
240
|
-
test('
|
|
249
|
+
test('triggers HOOK_REMOVE when items are removed', () => {
|
|
241
250
|
const items = new List([1, 2, 3])
|
|
242
|
-
const doubled = new
|
|
251
|
+
const doubled = new DerivedCollection(items, (x: number) => x * 2)
|
|
243
252
|
|
|
244
|
-
let
|
|
253
|
+
let removedKeys: readonly string[] | undefined
|
|
245
254
|
doubled.on('remove', keys => {
|
|
246
|
-
|
|
255
|
+
removedKeys = keys
|
|
247
256
|
})
|
|
248
257
|
|
|
249
258
|
items.remove(1)
|
|
250
|
-
expect(
|
|
259
|
+
expect(removedKeys).toHaveLength(1)
|
|
251
260
|
})
|
|
252
261
|
|
|
253
|
-
test('
|
|
262
|
+
test('triggers HOOK_SORT when source is sorted', () => {
|
|
254
263
|
const numbers = new List([3, 1, 2])
|
|
255
|
-
const doubled = new
|
|
264
|
+
const doubled = new DerivedCollection(numbers, (x: number) => x * 2)
|
|
256
265
|
|
|
257
|
-
let
|
|
266
|
+
let order: readonly string[] | undefined
|
|
258
267
|
doubled.on('sort', newOrder => {
|
|
259
|
-
|
|
268
|
+
order = newOrder
|
|
260
269
|
})
|
|
261
270
|
|
|
262
271
|
numbers.sort((a, b) => a - b)
|
|
263
|
-
expect(
|
|
272
|
+
expect(order).toHaveLength(3)
|
|
264
273
|
expect(doubled.get()).toEqual([2, 4, 6])
|
|
265
274
|
})
|
|
266
275
|
})
|
|
@@ -268,14 +277,17 @@ describe('collection', () => {
|
|
|
268
277
|
describe('edge cases', () => {
|
|
269
278
|
test('handles empty collections correctly', () => {
|
|
270
279
|
const empty = new List<number>([])
|
|
271
|
-
const collection = new
|
|
280
|
+
const collection = new DerivedCollection(
|
|
281
|
+
empty,
|
|
282
|
+
(x: number) => x * 2,
|
|
283
|
+
)
|
|
272
284
|
expect(collection.length).toBe(0)
|
|
273
285
|
expect(collection.get()).toEqual([])
|
|
274
286
|
})
|
|
275
287
|
|
|
276
288
|
test('handles UNSET values', () => {
|
|
277
289
|
const list = new List([1, 2, 3])
|
|
278
|
-
const processed = new
|
|
290
|
+
const processed = new DerivedCollection(list, (x: number) =>
|
|
279
291
|
x > 2 ? x : UNSET,
|
|
280
292
|
)
|
|
281
293
|
|
|
@@ -285,7 +297,7 @@ describe('collection', () => {
|
|
|
285
297
|
|
|
286
298
|
test('handles primitive values', () => {
|
|
287
299
|
const list = new List(['hello', 'world'])
|
|
288
|
-
const lengths = new
|
|
300
|
+
const lengths = new DerivedCollection(list, (str: string) => ({
|
|
289
301
|
length: str.length,
|
|
290
302
|
}))
|
|
291
303
|
|
|
@@ -298,7 +310,10 @@ describe('collection', () => {
|
|
|
298
310
|
describe('synchronous transformations', () => {
|
|
299
311
|
test('transforms collection values with sync callback', () => {
|
|
300
312
|
const numbers = new List([1, 2, 3])
|
|
301
|
-
const doubled = new
|
|
313
|
+
const doubled = new DerivedCollection(
|
|
314
|
+
numbers,
|
|
315
|
+
(x: number) => x * 2,
|
|
316
|
+
)
|
|
302
317
|
const quadrupled = doubled.deriveCollection(
|
|
303
318
|
(x: number) => x * 2,
|
|
304
319
|
)
|
|
@@ -314,7 +329,7 @@ describe('collection', () => {
|
|
|
314
329
|
{ name: 'Alice', age: 25 },
|
|
315
330
|
{ name: 'Bob', age: 30 },
|
|
316
331
|
])
|
|
317
|
-
const basicInfo = new
|
|
332
|
+
const basicInfo = new DerivedCollection(
|
|
318
333
|
users,
|
|
319
334
|
(user: { name: string; age: number }) => ({
|
|
320
335
|
displayName: user.name.toUpperCase(),
|
|
@@ -342,10 +357,13 @@ describe('collection', () => {
|
|
|
342
357
|
|
|
343
358
|
test('transforms string values to different types', () => {
|
|
344
359
|
const words = new List(['hello', 'world', 'test'])
|
|
345
|
-
const wordInfo = new
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
360
|
+
const wordInfo = new DerivedCollection(
|
|
361
|
+
words,
|
|
362
|
+
(word: string) => ({
|
|
363
|
+
word,
|
|
364
|
+
length: word.length,
|
|
365
|
+
}),
|
|
366
|
+
)
|
|
349
367
|
const analysis = wordInfo.deriveCollection(
|
|
350
368
|
(info: { word: string; length: number }) => ({
|
|
351
369
|
...info,
|
|
@@ -366,7 +384,10 @@ describe('collection', () => {
|
|
|
366
384
|
|
|
367
385
|
test('derived collection reactivity with sync transformations', () => {
|
|
368
386
|
const numbers = new List([1, 2, 3])
|
|
369
|
-
const doubled = new
|
|
387
|
+
const doubled = new DerivedCollection(
|
|
388
|
+
numbers,
|
|
389
|
+
(x: number) => x * 2,
|
|
390
|
+
)
|
|
370
391
|
const quadrupled = doubled.deriveCollection(
|
|
371
392
|
(x: number) => x * 2,
|
|
372
393
|
)
|
|
@@ -392,7 +413,10 @@ describe('collection', () => {
|
|
|
392
413
|
|
|
393
414
|
test('derived collection responds to source removal', () => {
|
|
394
415
|
const numbers = new List([1, 2, 3, 4])
|
|
395
|
-
const doubled = new
|
|
416
|
+
const doubled = new DerivedCollection(
|
|
417
|
+
numbers,
|
|
418
|
+
(x: number) => x * 2,
|
|
419
|
+
)
|
|
396
420
|
const quadrupled = doubled.deriveCollection(
|
|
397
421
|
(x: number) => x * 2,
|
|
398
422
|
)
|
|
@@ -407,7 +431,10 @@ describe('collection', () => {
|
|
|
407
431
|
describe('asynchronous transformations', () => {
|
|
408
432
|
test('transforms values with async callback', async () => {
|
|
409
433
|
const numbers = new List([1, 2, 3])
|
|
410
|
-
const doubled = new
|
|
434
|
+
const doubled = new DerivedCollection(
|
|
435
|
+
numbers,
|
|
436
|
+
(x: number) => x * 2,
|
|
437
|
+
)
|
|
411
438
|
|
|
412
439
|
const asyncQuadrupled = doubled.deriveCollection(
|
|
413
440
|
async (x: number, abort: AbortSignal) => {
|
|
@@ -448,7 +475,7 @@ describe('collection', () => {
|
|
|
448
475
|
{ id: 1, name: 'Alice' },
|
|
449
476
|
{ id: 2, name: 'Bob' },
|
|
450
477
|
])
|
|
451
|
-
const basicInfo = new
|
|
478
|
+
const basicInfo = new DerivedCollection(
|
|
452
479
|
users,
|
|
453
480
|
(user: { id: number; name: string }) => ({
|
|
454
481
|
userId: user.id,
|
|
@@ -507,7 +534,10 @@ describe('collection', () => {
|
|
|
507
534
|
|
|
508
535
|
test('async derived collection reactivity', async () => {
|
|
509
536
|
const numbers = new List([1, 2, 3])
|
|
510
|
-
const doubled = new
|
|
537
|
+
const doubled = new DerivedCollection(
|
|
538
|
+
numbers,
|
|
539
|
+
(x: number) => x * 2,
|
|
540
|
+
)
|
|
511
541
|
const asyncQuadrupled = doubled.deriveCollection(
|
|
512
542
|
async (x: number, abort: AbortSignal) => {
|
|
513
543
|
await new Promise(resolve => setTimeout(resolve, 10))
|
|
@@ -544,7 +574,10 @@ describe('collection', () => {
|
|
|
544
574
|
|
|
545
575
|
test('handles AbortSignal cancellation', async () => {
|
|
546
576
|
const numbers = new List([1, 2, 3])
|
|
547
|
-
const doubled = new
|
|
577
|
+
const doubled = new DerivedCollection(
|
|
578
|
+
numbers,
|
|
579
|
+
(x: number) => x * 2,
|
|
580
|
+
)
|
|
548
581
|
let abortCalled = false
|
|
549
582
|
|
|
550
583
|
const slowCollection = doubled.deriveCollection(
|
|
@@ -580,7 +613,10 @@ describe('collection', () => {
|
|
|
580
613
|
describe('derived collection chaining', () => {
|
|
581
614
|
test('chains multiple sync derivations', () => {
|
|
582
615
|
const numbers = new List([1, 2, 3])
|
|
583
|
-
const doubled = new
|
|
616
|
+
const doubled = new DerivedCollection(
|
|
617
|
+
numbers,
|
|
618
|
+
(x: number) => x * 2,
|
|
619
|
+
)
|
|
584
620
|
const quadrupled = doubled.deriveCollection(
|
|
585
621
|
(x: number) => x * 2,
|
|
586
622
|
)
|
|
@@ -595,7 +631,10 @@ describe('collection', () => {
|
|
|
595
631
|
|
|
596
632
|
test('chains sync and async derivations', async () => {
|
|
597
633
|
const numbers = new List([1, 2, 3])
|
|
598
|
-
const doubled = new
|
|
634
|
+
const doubled = new DerivedCollection(
|
|
635
|
+
numbers,
|
|
636
|
+
(x: number) => x * 2,
|
|
637
|
+
)
|
|
599
638
|
const quadrupled = doubled.deriveCollection(
|
|
600
639
|
(x: number) => x * 2,
|
|
601
640
|
)
|
|
@@ -625,7 +664,10 @@ describe('collection', () => {
|
|
|
625
664
|
describe('derived collection access methods', () => {
|
|
626
665
|
test('provides index-based access to computed signals', () => {
|
|
627
666
|
const numbers = new List([1, 2, 3])
|
|
628
|
-
const doubled = new
|
|
667
|
+
const doubled = new DerivedCollection(
|
|
668
|
+
numbers,
|
|
669
|
+
(x: number) => x * 2,
|
|
670
|
+
)
|
|
629
671
|
const quadrupled = doubled.deriveCollection(
|
|
630
672
|
(x: number) => x * 2,
|
|
631
673
|
)
|
|
@@ -638,7 +680,10 @@ describe('collection', () => {
|
|
|
638
680
|
|
|
639
681
|
test('supports key-based access', () => {
|
|
640
682
|
const numbers = new List([1, 2, 3])
|
|
641
|
-
const doubled = new
|
|
683
|
+
const doubled = new DerivedCollection(
|
|
684
|
+
numbers,
|
|
685
|
+
(x: number) => x * 2,
|
|
686
|
+
)
|
|
642
687
|
const quadrupled = doubled.deriveCollection(
|
|
643
688
|
(x: number) => x * 2,
|
|
644
689
|
)
|
|
@@ -660,7 +705,10 @@ describe('collection', () => {
|
|
|
660
705
|
|
|
661
706
|
test('supports iteration', () => {
|
|
662
707
|
const numbers = new List([1, 2, 3])
|
|
663
|
-
const doubled = new
|
|
708
|
+
const doubled = new DerivedCollection(
|
|
709
|
+
numbers,
|
|
710
|
+
(x: number) => x * 2,
|
|
711
|
+
)
|
|
664
712
|
const quadrupled = doubled.deriveCollection(
|
|
665
713
|
(x: number) => x * 2,
|
|
666
714
|
)
|
|
@@ -676,30 +724,37 @@ describe('collection', () => {
|
|
|
676
724
|
describe('derived collection event handling', () => {
|
|
677
725
|
test('emits add events when source adds items', () => {
|
|
678
726
|
const numbers = new List([1, 2])
|
|
679
|
-
const doubled = new
|
|
727
|
+
const doubled = new DerivedCollection(
|
|
728
|
+
numbers,
|
|
729
|
+
(x: number) => x * 2,
|
|
730
|
+
)
|
|
680
731
|
const quadrupled = doubled.deriveCollection(
|
|
681
732
|
(x: number) => x * 2,
|
|
682
733
|
)
|
|
683
734
|
|
|
684
|
-
let addedKeys: readonly string[]
|
|
735
|
+
let addedKeys: readonly string[] | undefined
|
|
685
736
|
quadrupled.on('add', keys => {
|
|
686
737
|
addedKeys = keys
|
|
687
738
|
})
|
|
688
739
|
|
|
689
740
|
numbers.add(3)
|
|
690
741
|
expect(addedKeys).toHaveLength(1)
|
|
691
|
-
|
|
692
|
-
|
|
742
|
+
const quadrupledKey = addedKeys?.[0]
|
|
743
|
+
if (quadrupledKey)
|
|
744
|
+
expect(quadrupled.byKey(quadrupledKey)?.get()).toBe(12)
|
|
693
745
|
})
|
|
694
746
|
|
|
695
747
|
test('emits remove events when source removes items', () => {
|
|
696
748
|
const numbers = new List([1, 2, 3])
|
|
697
|
-
const doubled = new
|
|
749
|
+
const doubled = new DerivedCollection(
|
|
750
|
+
numbers,
|
|
751
|
+
(x: number) => x * 2,
|
|
752
|
+
)
|
|
698
753
|
const quadrupled = doubled.deriveCollection(
|
|
699
754
|
(x: number) => x * 2,
|
|
700
755
|
)
|
|
701
756
|
|
|
702
|
-
let removedKeys: readonly string[]
|
|
757
|
+
let removedKeys: readonly string[] | undefined
|
|
703
758
|
quadrupled.on('remove', keys => {
|
|
704
759
|
removedKeys = keys
|
|
705
760
|
})
|
|
@@ -710,12 +765,15 @@ describe('collection', () => {
|
|
|
710
765
|
|
|
711
766
|
test('emits sort events when source is sorted', () => {
|
|
712
767
|
const numbers = new List([3, 1, 2])
|
|
713
|
-
const doubled = new
|
|
768
|
+
const doubled = new DerivedCollection(
|
|
769
|
+
numbers,
|
|
770
|
+
(x: number) => x * 2,
|
|
771
|
+
)
|
|
714
772
|
const quadrupled = doubled.deriveCollection(
|
|
715
773
|
(x: number) => x * 2,
|
|
716
774
|
)
|
|
717
775
|
|
|
718
|
-
let sortedKeys: readonly string[]
|
|
776
|
+
let sortedKeys: readonly string[] | undefined
|
|
719
777
|
quadrupled.on('sort', newOrder => {
|
|
720
778
|
sortedKeys = newOrder
|
|
721
779
|
})
|
|
@@ -729,7 +787,7 @@ describe('collection', () => {
|
|
|
729
787
|
describe('edge cases', () => {
|
|
730
788
|
test('handles empty collection derivation', () => {
|
|
731
789
|
const empty = new List<number>([])
|
|
732
|
-
const emptyCollection = new
|
|
790
|
+
const emptyCollection = new DerivedCollection(
|
|
733
791
|
empty,
|
|
734
792
|
(x: number) => x * 2,
|
|
735
793
|
)
|
|
@@ -743,7 +801,7 @@ describe('collection', () => {
|
|
|
743
801
|
|
|
744
802
|
test('handles UNSET values in transformation', () => {
|
|
745
803
|
const list = new List([1, 2, 3])
|
|
746
|
-
const filtered = new
|
|
804
|
+
const filtered = new DerivedCollection(list, (x: number) =>
|
|
747
805
|
x > 1 ? { value: x } : UNSET,
|
|
748
806
|
)
|
|
749
807
|
const doubled = filtered.deriveCollection(
|
|
@@ -759,7 +817,7 @@ describe('collection', () => {
|
|
|
759
817
|
{ id: 2, data: { value: 20, active: false } },
|
|
760
818
|
])
|
|
761
819
|
|
|
762
|
-
const processed = new
|
|
820
|
+
const processed = new DerivedCollection(
|
|
763
821
|
items,
|
|
764
822
|
(item: {
|
|
765
823
|
id: number
|
|
@@ -793,4 +851,262 @@ describe('collection', () => {
|
|
|
793
851
|
})
|
|
794
852
|
})
|
|
795
853
|
})
|
|
854
|
+
|
|
855
|
+
describe('hooks system', () => {
|
|
856
|
+
test('Collection HOOK_WATCH is called when effect accesses collection.get()', () => {
|
|
857
|
+
const numbers = new List([10, 20, 30])
|
|
858
|
+
const doubled = numbers.deriveCollection(x => x * 2)
|
|
859
|
+
|
|
860
|
+
let collectionHookWatchCalled = false
|
|
861
|
+
let collectionUnwatchCalled = false
|
|
862
|
+
|
|
863
|
+
// Set up HOOK_WATCH callback on the collection itself
|
|
864
|
+
doubled.on('watch', () => {
|
|
865
|
+
collectionHookWatchCalled = true
|
|
866
|
+
return () => {
|
|
867
|
+
collectionUnwatchCalled = true
|
|
868
|
+
}
|
|
869
|
+
})
|
|
870
|
+
|
|
871
|
+
expect(collectionHookWatchCalled).toBe(false)
|
|
872
|
+
|
|
873
|
+
// Access collection via collection.get() - this should trigger collection's HOOK_WATCH
|
|
874
|
+
let effectValue: number[] = []
|
|
875
|
+
const cleanup = createEffect(() => {
|
|
876
|
+
effectValue = doubled.get()
|
|
877
|
+
})
|
|
878
|
+
|
|
879
|
+
expect(collectionHookWatchCalled).toBe(true)
|
|
880
|
+
expect(effectValue).toEqual([20, 40, 60])
|
|
881
|
+
expect(collectionUnwatchCalled).toBe(false)
|
|
882
|
+
|
|
883
|
+
// Cleanup effect - should trigger unwatch
|
|
884
|
+
cleanup()
|
|
885
|
+
expect(collectionUnwatchCalled).toBe(true)
|
|
886
|
+
})
|
|
887
|
+
|
|
888
|
+
test('List item HOOK_WATCH is triggered when accessing collection items via collection.at().get()', () => {
|
|
889
|
+
const numbers = new List([42, 84])
|
|
890
|
+
const doubled = numbers.deriveCollection(x => x * 2)
|
|
891
|
+
|
|
892
|
+
// Set up hook on source item BEFORE creating the effect
|
|
893
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
894
|
+
const firstSourceItem = numbers.at(0)!
|
|
895
|
+
let sourceItemHookCalled = false
|
|
896
|
+
|
|
897
|
+
firstSourceItem.on('watch', () => {
|
|
898
|
+
sourceItemHookCalled = true
|
|
899
|
+
return () => {
|
|
900
|
+
// Note: Unwatch behavior in computed signals is complex and depends on
|
|
901
|
+
// internal watcher management. We focus on verifying hook triggering.
|
|
902
|
+
}
|
|
903
|
+
})
|
|
904
|
+
|
|
905
|
+
expect(sourceItemHookCalled).toBe(false)
|
|
906
|
+
|
|
907
|
+
// Access collection item - the computed signal internally calls sourceItem.get()
|
|
908
|
+
let effectValue: number | undefined
|
|
909
|
+
const cleanup = createEffect(() => {
|
|
910
|
+
const firstCollectionItem = doubled.at(0)
|
|
911
|
+
effectValue = firstCollectionItem?.get()
|
|
912
|
+
})
|
|
913
|
+
|
|
914
|
+
expect(sourceItemHookCalled).toBe(true) // Source item HOOK_WATCH triggered
|
|
915
|
+
expect(effectValue).toBe(84) // 42 * 2
|
|
916
|
+
|
|
917
|
+
cleanup()
|
|
918
|
+
})
|
|
919
|
+
|
|
920
|
+
test('Collection and List item hooks work independently', () => {
|
|
921
|
+
const items = new List(['hello', 'world'])
|
|
922
|
+
const uppercased = items.deriveCollection(x => x.toUpperCase())
|
|
923
|
+
|
|
924
|
+
let collectionHookCalled = false
|
|
925
|
+
let collectionUnwatchCalled = false
|
|
926
|
+
let sourceItemHookCalled = false
|
|
927
|
+
|
|
928
|
+
// Set up hooks on both collection and source item
|
|
929
|
+
uppercased.on('watch', () => {
|
|
930
|
+
collectionHookCalled = true
|
|
931
|
+
return () => {
|
|
932
|
+
collectionUnwatchCalled = true
|
|
933
|
+
}
|
|
934
|
+
})
|
|
935
|
+
|
|
936
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
937
|
+
const firstSourceItem = items.at(0)!
|
|
938
|
+
firstSourceItem.on('watch', () => {
|
|
939
|
+
sourceItemHookCalled = true
|
|
940
|
+
return () => {
|
|
941
|
+
// Source item unwatch behavior is complex in computed context
|
|
942
|
+
}
|
|
943
|
+
})
|
|
944
|
+
|
|
945
|
+
// Effect 1: Access collection-level data - triggers both hooks
|
|
946
|
+
let collectionValue: string[] = []
|
|
947
|
+
const collectionCleanup = createEffect(() => {
|
|
948
|
+
collectionValue = uppercased.get()
|
|
949
|
+
})
|
|
950
|
+
|
|
951
|
+
expect(collectionHookCalled).toBe(true)
|
|
952
|
+
expect(sourceItemHookCalled).toBe(true) // Source items accessed by collection.get()
|
|
953
|
+
expect(collectionValue).toEqual(['HELLO', 'WORLD'])
|
|
954
|
+
|
|
955
|
+
// Effect 2: Access individual collection item independently
|
|
956
|
+
let itemValue: string | undefined
|
|
957
|
+
const itemCleanup = createEffect(() => {
|
|
958
|
+
itemValue = uppercased.at(0)?.get()
|
|
959
|
+
})
|
|
960
|
+
|
|
961
|
+
expect(itemValue).toBe('HELLO')
|
|
962
|
+
|
|
963
|
+
// Clean up effects
|
|
964
|
+
collectionCleanup()
|
|
965
|
+
expect(collectionUnwatchCalled).toBe(true)
|
|
966
|
+
|
|
967
|
+
itemCleanup()
|
|
968
|
+
})
|
|
969
|
+
|
|
970
|
+
test('source List item hooks are properly managed when items are removed', () => {
|
|
971
|
+
const items = new List(['first', 'second', 'third'])
|
|
972
|
+
const processed = items.deriveCollection(x => x.toUpperCase())
|
|
973
|
+
|
|
974
|
+
let firstItemHookCalled = false
|
|
975
|
+
let secondItemHookCalled = false
|
|
976
|
+
|
|
977
|
+
// Set up hooks on multiple source items
|
|
978
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
979
|
+
const firstSourceItem = items.at(0)!
|
|
980
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
981
|
+
const secondSourceItem = items.at(1)!
|
|
982
|
+
|
|
983
|
+
firstSourceItem.on('watch', () => {
|
|
984
|
+
firstItemHookCalled = true
|
|
985
|
+
return () => {
|
|
986
|
+
// Collection computed signals manage source watching internally
|
|
987
|
+
}
|
|
988
|
+
})
|
|
989
|
+
|
|
990
|
+
secondSourceItem.on('watch', () => {
|
|
991
|
+
secondItemHookCalled = true
|
|
992
|
+
return () => {
|
|
993
|
+
// Collection computed signals manage source watching internally
|
|
994
|
+
}
|
|
995
|
+
})
|
|
996
|
+
|
|
997
|
+
// Access both collection items to trigger source hooks
|
|
998
|
+
let firstValue: string | undefined
|
|
999
|
+
let secondValue: string | undefined
|
|
1000
|
+
|
|
1001
|
+
const cleanup1 = createEffect(() => {
|
|
1002
|
+
firstValue = processed.at(0)?.get()
|
|
1003
|
+
})
|
|
1004
|
+
|
|
1005
|
+
const cleanup2 = createEffect(() => {
|
|
1006
|
+
secondValue = processed.at(1)?.get()
|
|
1007
|
+
})
|
|
1008
|
+
|
|
1009
|
+
// Both source item hooks should be triggered
|
|
1010
|
+
expect(firstItemHookCalled).toBe(true)
|
|
1011
|
+
expect(secondItemHookCalled).toBe(true)
|
|
1012
|
+
expect(firstValue).toBe('FIRST')
|
|
1013
|
+
expect(secondValue).toBe('SECOND')
|
|
1014
|
+
|
|
1015
|
+
cleanup1()
|
|
1016
|
+
cleanup2()
|
|
1017
|
+
})
|
|
1018
|
+
|
|
1019
|
+
test('newly added source List items have hooks triggered through collection access', () => {
|
|
1020
|
+
const numbers = new List<number>([])
|
|
1021
|
+
const squared = numbers.deriveCollection(x => x * x)
|
|
1022
|
+
|
|
1023
|
+
// Add first item to source list
|
|
1024
|
+
numbers.add(5)
|
|
1025
|
+
|
|
1026
|
+
// Set up hook on the newly added source item
|
|
1027
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
1028
|
+
const sourceItem = numbers.at(0)!
|
|
1029
|
+
let sourceHookCalled = false
|
|
1030
|
+
|
|
1031
|
+
sourceItem.on('watch', () => {
|
|
1032
|
+
sourceHookCalled = true
|
|
1033
|
+
return () => {
|
|
1034
|
+
// Hook cleanup managed by computed signal system
|
|
1035
|
+
}
|
|
1036
|
+
})
|
|
1037
|
+
|
|
1038
|
+
expect(sourceHookCalled).toBe(false)
|
|
1039
|
+
|
|
1040
|
+
// Access the collection item - should trigger source item hook
|
|
1041
|
+
let effectValue: number | undefined
|
|
1042
|
+
const cleanup = createEffect(() => {
|
|
1043
|
+
effectValue = squared.at(0)?.get()
|
|
1044
|
+
})
|
|
1045
|
+
|
|
1046
|
+
expect(sourceHookCalled).toBe(true) // Source hook triggered through collection
|
|
1047
|
+
expect(effectValue).toBe(25) // 5 * 5
|
|
1048
|
+
|
|
1049
|
+
cleanup()
|
|
1050
|
+
})
|
|
1051
|
+
|
|
1052
|
+
test('Collection length access triggers Collection HOOK_WATCH', () => {
|
|
1053
|
+
const numbers = new List([1, 2, 3])
|
|
1054
|
+
const doubled = numbers.deriveCollection(x => x * 2)
|
|
1055
|
+
|
|
1056
|
+
let collectionHookWatchCalled = false
|
|
1057
|
+
let collectionUnwatchCalled = false
|
|
1058
|
+
|
|
1059
|
+
doubled.on('watch', () => {
|
|
1060
|
+
collectionHookWatchCalled = true
|
|
1061
|
+
return () => {
|
|
1062
|
+
collectionUnwatchCalled = true
|
|
1063
|
+
}
|
|
1064
|
+
})
|
|
1065
|
+
|
|
1066
|
+
// Access via collection.length - this should trigger collection's HOOK_WATCH
|
|
1067
|
+
let effectValue: number = 0
|
|
1068
|
+
const cleanup = createEffect(() => {
|
|
1069
|
+
effectValue = doubled.length
|
|
1070
|
+
})
|
|
1071
|
+
|
|
1072
|
+
expect(collectionHookWatchCalled).toBe(true)
|
|
1073
|
+
expect(effectValue).toBe(3)
|
|
1074
|
+
expect(collectionUnwatchCalled).toBe(false)
|
|
1075
|
+
|
|
1076
|
+
cleanup()
|
|
1077
|
+
expect(collectionUnwatchCalled).toBe(true)
|
|
1078
|
+
})
|
|
1079
|
+
|
|
1080
|
+
test('chained collections maintain proper hook propagation to original source', () => {
|
|
1081
|
+
const numbers = new List([2, 3])
|
|
1082
|
+
const doubled = numbers.deriveCollection(x => x * 2)
|
|
1083
|
+
const quadrupled = doubled.deriveCollection(x => x * 2)
|
|
1084
|
+
|
|
1085
|
+
// Set up hook on original source item
|
|
1086
|
+
// biome-ignore lint/style/noNonNullAssertion: test
|
|
1087
|
+
const sourceItem = numbers.at(0)!
|
|
1088
|
+
let sourceHookCalled = false
|
|
1089
|
+
|
|
1090
|
+
sourceItem.on('watch', () => {
|
|
1091
|
+
sourceHookCalled = true
|
|
1092
|
+
return () => {
|
|
1093
|
+
// Chained computed signals manage cleanup through dependency chain
|
|
1094
|
+
}
|
|
1095
|
+
})
|
|
1096
|
+
|
|
1097
|
+
expect(sourceHookCalled).toBe(false)
|
|
1098
|
+
|
|
1099
|
+
// Access chained collection item - should trigger original source hook
|
|
1100
|
+
// Chain: quadrupled.at(0).get() -> doubled.at(0).get() -> numbers.at(0).get()
|
|
1101
|
+
let effectValue: number | undefined
|
|
1102
|
+
const cleanup = createEffect(() => {
|
|
1103
|
+
effectValue = quadrupled.at(0)?.get()
|
|
1104
|
+
})
|
|
1105
|
+
|
|
1106
|
+
expect(sourceHookCalled).toBe(true) // Original source hook triggered through chain
|
|
1107
|
+
expect(effectValue).toBe(8) // 2 * 2 * 2
|
|
1108
|
+
|
|
1109
|
+
cleanup()
|
|
1110
|
+
})
|
|
1111
|
+
})
|
|
796
1112
|
})
|