@zeix/cause-effect 0.18.0 → 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 +14 -3
- package/.github/copilot-instructions.md +15 -5
- package/ARCHITECTURE.md +15 -13
- package/CHANGELOG.md +29 -0
- package/CLAUDE.md +9 -7
- package/README.md +23 -5
- package/context7.json +4 -0
- package/examples/events-sensor.ts +187 -0
- package/examples/selector-sensor.ts +173 -0
- package/index.dev.js +276 -222
- package/index.js +1 -1
- package/index.ts +4 -2
- package/package.json +2 -2
- package/skills/changelog-keeper/SKILL.md +59 -0
- package/skills/changelog-keeper/agents/openai.yaml +4 -0
- package/src/graph.ts +13 -2
- package/src/nodes/collection.ts +166 -128
- package/src/nodes/list.ts +105 -104
- package/src/nodes/memo.ts +31 -3
- package/src/nodes/sensor.ts +27 -17
- package/src/nodes/state.ts +2 -2
- package/src/nodes/store.ts +55 -60
- package/src/nodes/task.ts +31 -3
- package/test/collection.test.ts +40 -51
- package/test/memo.test.ts +194 -0
- package/test/task.test.ts +134 -0
- package/types/index.d.ts +5 -5
- package/types/src/graph.d.ts +12 -2
- package/types/src/nodes/collection.d.ts +12 -7
- package/types/src/nodes/list.d.ts +12 -11
- package/types/src/nodes/memo.d.ts +6 -0
- package/types/src/nodes/sensor.d.ts +15 -9
- package/types/src/nodes/store.d.ts +4 -4
- package/types/src/nodes/task.d.ts +6 -0
- package/COLLECTION_REFACTORING.md +0 -161
package/test/memo.test.ts
CHANGED
|
@@ -1,6 +1,9 @@
|
|
|
1
1
|
import { describe, expect, test } from 'bun:test'
|
|
2
2
|
import {
|
|
3
|
+
batch,
|
|
4
|
+
createEffect,
|
|
3
5
|
createMemo,
|
|
6
|
+
createScope,
|
|
4
7
|
createState,
|
|
5
8
|
isMemo,
|
|
6
9
|
isState,
|
|
@@ -377,4 +380,195 @@ describe('Memo', () => {
|
|
|
377
380
|
}).toThrow('[Memo] Signal value cannot be null or undefined')
|
|
378
381
|
})
|
|
379
382
|
})
|
|
383
|
+
|
|
384
|
+
describe('options.watched', () => {
|
|
385
|
+
test('should call watched on first effect access', () => {
|
|
386
|
+
let watchedCount = 0
|
|
387
|
+
const externalValue = 1
|
|
388
|
+
|
|
389
|
+
const memo = createMemo(() => externalValue, {
|
|
390
|
+
value: 0,
|
|
391
|
+
watched: _invalidate => {
|
|
392
|
+
watchedCount++
|
|
393
|
+
return () => {}
|
|
394
|
+
},
|
|
395
|
+
})
|
|
396
|
+
|
|
397
|
+
expect(watchedCount).toBe(0)
|
|
398
|
+
|
|
399
|
+
const dispose = createScope(() => {
|
|
400
|
+
createEffect(() => {
|
|
401
|
+
void memo.get()
|
|
402
|
+
})
|
|
403
|
+
})
|
|
404
|
+
|
|
405
|
+
expect(watchedCount).toBe(1)
|
|
406
|
+
dispose()
|
|
407
|
+
})
|
|
408
|
+
|
|
409
|
+
test('should call cleanup when last effect stops watching', () => {
|
|
410
|
+
let cleanedUp = false
|
|
411
|
+
const externalValue = 1
|
|
412
|
+
|
|
413
|
+
const memo = createMemo(() => externalValue, {
|
|
414
|
+
value: 0,
|
|
415
|
+
watched: _invalidate => {
|
|
416
|
+
return () => {
|
|
417
|
+
cleanedUp = true
|
|
418
|
+
}
|
|
419
|
+
},
|
|
420
|
+
})
|
|
421
|
+
|
|
422
|
+
const dispose = createScope(() => {
|
|
423
|
+
createEffect(() => {
|
|
424
|
+
void memo.get()
|
|
425
|
+
})
|
|
426
|
+
})
|
|
427
|
+
|
|
428
|
+
expect(cleanedUp).toBe(false)
|
|
429
|
+
dispose()
|
|
430
|
+
expect(cleanedUp).toBe(true)
|
|
431
|
+
})
|
|
432
|
+
|
|
433
|
+
test('should recompute memo when invalidate is called', () => {
|
|
434
|
+
let externalValue = 10
|
|
435
|
+
let computeCount = 0
|
|
436
|
+
let invalidate!: () => void
|
|
437
|
+
|
|
438
|
+
const memo = createMemo(
|
|
439
|
+
() => {
|
|
440
|
+
computeCount++
|
|
441
|
+
return externalValue
|
|
442
|
+
},
|
|
443
|
+
{
|
|
444
|
+
value: 0,
|
|
445
|
+
watched: inv => {
|
|
446
|
+
invalidate = inv
|
|
447
|
+
return () => {}
|
|
448
|
+
},
|
|
449
|
+
},
|
|
450
|
+
)
|
|
451
|
+
|
|
452
|
+
let observed = 0
|
|
453
|
+
const dispose = createScope(() => {
|
|
454
|
+
createEffect(() => {
|
|
455
|
+
observed = memo.get()
|
|
456
|
+
})
|
|
457
|
+
})
|
|
458
|
+
|
|
459
|
+
expect(observed).toBe(10)
|
|
460
|
+
expect(computeCount).toBe(1)
|
|
461
|
+
|
|
462
|
+
externalValue = 20
|
|
463
|
+
invalidate()
|
|
464
|
+
expect(observed).toBe(20)
|
|
465
|
+
expect(computeCount).toBe(2)
|
|
466
|
+
|
|
467
|
+
dispose()
|
|
468
|
+
})
|
|
469
|
+
|
|
470
|
+
test('should defer flush when invalidate is called inside batch', () => {
|
|
471
|
+
let externalValue = 1
|
|
472
|
+
let invalidate!: () => void
|
|
473
|
+
|
|
474
|
+
const memo = createMemo(() => externalValue, {
|
|
475
|
+
value: 0,
|
|
476
|
+
watched: inv => {
|
|
477
|
+
invalidate = inv
|
|
478
|
+
return () => {}
|
|
479
|
+
},
|
|
480
|
+
})
|
|
481
|
+
|
|
482
|
+
let observed = 0
|
|
483
|
+
const dispose = createScope(() => {
|
|
484
|
+
createEffect(() => {
|
|
485
|
+
observed = memo.get()
|
|
486
|
+
})
|
|
487
|
+
})
|
|
488
|
+
|
|
489
|
+
expect(observed).toBe(1)
|
|
490
|
+
|
|
491
|
+
batch(() => {
|
|
492
|
+
externalValue = 2
|
|
493
|
+
invalidate()
|
|
494
|
+
expect(observed).toBe(1) // not yet flushed
|
|
495
|
+
})
|
|
496
|
+
expect(observed).toBe(2) // flushed after batch
|
|
497
|
+
|
|
498
|
+
dispose()
|
|
499
|
+
})
|
|
500
|
+
|
|
501
|
+
test('should re-activate watched after cleanup and new effect access', () => {
|
|
502
|
+
let watchedCount = 0
|
|
503
|
+
const externalValue = 1
|
|
504
|
+
|
|
505
|
+
const memo = createMemo(() => externalValue, {
|
|
506
|
+
value: 0,
|
|
507
|
+
watched: _invalidate => {
|
|
508
|
+
watchedCount++
|
|
509
|
+
return () => {}
|
|
510
|
+
},
|
|
511
|
+
})
|
|
512
|
+
|
|
513
|
+
const dispose1 = createScope(() => {
|
|
514
|
+
createEffect(() => {
|
|
515
|
+
void memo.get()
|
|
516
|
+
})
|
|
517
|
+
})
|
|
518
|
+
expect(watchedCount).toBe(1)
|
|
519
|
+
dispose1()
|
|
520
|
+
|
|
521
|
+
const dispose2 = createScope(() => {
|
|
522
|
+
createEffect(() => {
|
|
523
|
+
void memo.get()
|
|
524
|
+
})
|
|
525
|
+
})
|
|
526
|
+
expect(watchedCount).toBe(2)
|
|
527
|
+
dispose2()
|
|
528
|
+
})
|
|
529
|
+
|
|
530
|
+
test('should work with both tracked dependencies and watched', () => {
|
|
531
|
+
const source = createState(1)
|
|
532
|
+
let externalValue = 100
|
|
533
|
+
let computeCount = 0
|
|
534
|
+
let invalidate!: () => void
|
|
535
|
+
|
|
536
|
+
const memo = createMemo(
|
|
537
|
+
() => {
|
|
538
|
+
computeCount++
|
|
539
|
+
return source.get() + externalValue
|
|
540
|
+
},
|
|
541
|
+
{
|
|
542
|
+
value: 0,
|
|
543
|
+
watched: inv => {
|
|
544
|
+
invalidate = inv
|
|
545
|
+
return () => {}
|
|
546
|
+
},
|
|
547
|
+
},
|
|
548
|
+
)
|
|
549
|
+
|
|
550
|
+
let observed = 0
|
|
551
|
+
const dispose = createScope(() => {
|
|
552
|
+
createEffect(() => {
|
|
553
|
+
observed = memo.get()
|
|
554
|
+
})
|
|
555
|
+
})
|
|
556
|
+
|
|
557
|
+
expect(observed).toBe(101)
|
|
558
|
+
expect(computeCount).toBe(1)
|
|
559
|
+
|
|
560
|
+
// Tracked dependency triggers recomputation
|
|
561
|
+
source.set(2)
|
|
562
|
+
expect(observed).toBe(102)
|
|
563
|
+
expect(computeCount).toBe(2)
|
|
564
|
+
|
|
565
|
+
// External invalidation triggers recomputation
|
|
566
|
+
externalValue = 200
|
|
567
|
+
invalidate()
|
|
568
|
+
expect(observed).toBe(202)
|
|
569
|
+
expect(computeCount).toBe(3)
|
|
570
|
+
|
|
571
|
+
dispose()
|
|
572
|
+
})
|
|
573
|
+
})
|
|
380
574
|
})
|
package/test/task.test.ts
CHANGED
|
@@ -2,6 +2,7 @@ import { describe, expect, test } from 'bun:test'
|
|
|
2
2
|
import {
|
|
3
3
|
createEffect,
|
|
4
4
|
createMemo,
|
|
5
|
+
createScope,
|
|
5
6
|
createState,
|
|
6
7
|
createTask,
|
|
7
8
|
isMemo,
|
|
@@ -392,4 +393,137 @@ describe('Task', () => {
|
|
|
392
393
|
}).toThrow('[Task] Signal value cannot be null or undefined')
|
|
393
394
|
})
|
|
394
395
|
})
|
|
396
|
+
|
|
397
|
+
describe('options.watched', () => {
|
|
398
|
+
test('should call watched on first effect access', () => {
|
|
399
|
+
let watchedCount = 0
|
|
400
|
+
|
|
401
|
+
const task = createTask(
|
|
402
|
+
async () => {
|
|
403
|
+
await wait(10)
|
|
404
|
+
return 1
|
|
405
|
+
},
|
|
406
|
+
{
|
|
407
|
+
value: 0,
|
|
408
|
+
watched: _invalidate => {
|
|
409
|
+
watchedCount++
|
|
410
|
+
return () => {}
|
|
411
|
+
},
|
|
412
|
+
},
|
|
413
|
+
)
|
|
414
|
+
|
|
415
|
+
expect(watchedCount).toBe(0)
|
|
416
|
+
|
|
417
|
+
const dispose = createScope(() => {
|
|
418
|
+
createEffect(() => {
|
|
419
|
+
void task.get()
|
|
420
|
+
})
|
|
421
|
+
})
|
|
422
|
+
|
|
423
|
+
expect(watchedCount).toBe(1)
|
|
424
|
+
dispose()
|
|
425
|
+
})
|
|
426
|
+
|
|
427
|
+
test('should call cleanup when last effect stops watching', () => {
|
|
428
|
+
let cleanedUp = false
|
|
429
|
+
|
|
430
|
+
const task = createTask(
|
|
431
|
+
async () => {
|
|
432
|
+
await wait(10)
|
|
433
|
+
return 1
|
|
434
|
+
},
|
|
435
|
+
{
|
|
436
|
+
value: 0,
|
|
437
|
+
watched: _invalidate => {
|
|
438
|
+
return () => {
|
|
439
|
+
cleanedUp = true
|
|
440
|
+
}
|
|
441
|
+
},
|
|
442
|
+
},
|
|
443
|
+
)
|
|
444
|
+
|
|
445
|
+
const dispose = createScope(() => {
|
|
446
|
+
createEffect(() => {
|
|
447
|
+
void task.get()
|
|
448
|
+
})
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
expect(cleanedUp).toBe(false)
|
|
452
|
+
dispose()
|
|
453
|
+
expect(cleanedUp).toBe(true)
|
|
454
|
+
})
|
|
455
|
+
|
|
456
|
+
test('should re-execute task when invalidate is called', async () => {
|
|
457
|
+
let externalValue = 10
|
|
458
|
+
let computeCount = 0
|
|
459
|
+
let invalidate!: () => void
|
|
460
|
+
|
|
461
|
+
const task = createTask(
|
|
462
|
+
async () => {
|
|
463
|
+
computeCount++
|
|
464
|
+
await wait(10)
|
|
465
|
+
return externalValue
|
|
466
|
+
},
|
|
467
|
+
{
|
|
468
|
+
value: 0,
|
|
469
|
+
watched: inv => {
|
|
470
|
+
invalidate = inv
|
|
471
|
+
return () => {}
|
|
472
|
+
},
|
|
473
|
+
},
|
|
474
|
+
)
|
|
475
|
+
|
|
476
|
+
let observed = 0
|
|
477
|
+
const dispose = createScope(() => {
|
|
478
|
+
createEffect(() => {
|
|
479
|
+
observed = task.get()
|
|
480
|
+
})
|
|
481
|
+
})
|
|
482
|
+
|
|
483
|
+
await wait(20)
|
|
484
|
+
expect(observed).toBe(10)
|
|
485
|
+
expect(computeCount).toBe(1)
|
|
486
|
+
|
|
487
|
+
externalValue = 20
|
|
488
|
+
invalidate()
|
|
489
|
+
await wait(20)
|
|
490
|
+
expect(observed).toBe(20)
|
|
491
|
+
expect(computeCount).toBe(2)
|
|
492
|
+
|
|
493
|
+
dispose()
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
test('should abort in-flight task when invalidate is called', async () => {
|
|
497
|
+
let wasAborted = false
|
|
498
|
+
let invalidate!: () => void
|
|
499
|
+
|
|
500
|
+
const task = createTask(
|
|
501
|
+
async (_prev, signal) => {
|
|
502
|
+
await wait(100)
|
|
503
|
+
if (signal.aborted) wasAborted = true
|
|
504
|
+
return 1
|
|
505
|
+
},
|
|
506
|
+
{
|
|
507
|
+
value: 0,
|
|
508
|
+
watched: inv => {
|
|
509
|
+
invalidate = inv
|
|
510
|
+
return () => {}
|
|
511
|
+
},
|
|
512
|
+
},
|
|
513
|
+
)
|
|
514
|
+
|
|
515
|
+
const dispose = createScope(() => {
|
|
516
|
+
createEffect(() => {
|
|
517
|
+
void task.get()
|
|
518
|
+
})
|
|
519
|
+
})
|
|
520
|
+
|
|
521
|
+
await wait(10) // task is in-flight
|
|
522
|
+
invalidate() // should trigger re-execution, aborting the current one
|
|
523
|
+
await wait(110)
|
|
524
|
+
expect(wasAborted).toBe(true)
|
|
525
|
+
|
|
526
|
+
dispose()
|
|
527
|
+
})
|
|
528
|
+
})
|
|
395
529
|
})
|
package/types/index.d.ts
CHANGED
|
@@ -1,15 +1,15 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* @name Cause & Effect
|
|
3
|
-
* @version 0.18.
|
|
3
|
+
* @version 0.18.1
|
|
4
4
|
* @author Esther Brunner
|
|
5
5
|
*/
|
|
6
6
|
export { CircularDependencyError, type Guard, InvalidCallbackError, InvalidSignalValueError, NullishSignalValueError, RequiredOwnerError, UnsetSignalValueError, } from './src/errors';
|
|
7
|
-
export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, untrack, } from './src/graph';
|
|
8
|
-
export { type Collection, type CollectionCallback, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
|
|
7
|
+
export { batch, type Cleanup, type ComputedOptions, createScope, type EffectCallback, type MaybeCleanup, type MemoCallback, type Signal, type SignalOptions, SKIP_EQUALITY, type TaskCallback, untrack, } from './src/graph';
|
|
8
|
+
export { type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, createCollection, type DeriveCollectionCallback, isCollection, } from './src/nodes/collection';
|
|
9
9
|
export { createEffect, type MatchHandlers, type MaybePromise, match, } from './src/nodes/effect';
|
|
10
|
-
export { createList,
|
|
10
|
+
export { createList, isEqual, isList, type KeyConfig, type List, type ListOptions, } from './src/nodes/list';
|
|
11
11
|
export { createMemo, isMemo, type Memo } from './src/nodes/memo';
|
|
12
|
-
export { createSensor, isSensor, type Sensor, type SensorCallback, } from './src/nodes/sensor';
|
|
12
|
+
export { createSensor, isSensor, type Sensor, type SensorCallback, type SensorOptions, } from './src/nodes/sensor';
|
|
13
13
|
export { createState, isState, type State, type UpdateCallback, } from './src/nodes/state';
|
|
14
14
|
export { createStore, isStore, type Store, type StoreOptions, } from './src/nodes/store';
|
|
15
15
|
export { createTask, isTask, type Task } from './src/nodes/task';
|
package/types/src/graph.d.ts
CHANGED
|
@@ -76,6 +76,16 @@ type ComputedOptions<T extends {}> = SignalOptions<T> & {
|
|
|
76
76
|
* Useful for reducer patterns so that calculations start with a value of correct type.
|
|
77
77
|
*/
|
|
78
78
|
value?: T;
|
|
79
|
+
/**
|
|
80
|
+
* Optional callback invoked when the signal is first watched by an effect.
|
|
81
|
+
* Receives an `invalidate` function that marks the signal dirty and triggers re-evaluation.
|
|
82
|
+
* Must return a cleanup function that is called when the signal is no longer watched.
|
|
83
|
+
*
|
|
84
|
+
* This enables lazy resource activation for computed signals that need to
|
|
85
|
+
* react to external events (e.g. DOM mutations, timers) in addition to
|
|
86
|
+
* tracked signal dependencies.
|
|
87
|
+
*/
|
|
88
|
+
watched?: (invalidate: () => void) => Cleanup;
|
|
79
89
|
};
|
|
80
90
|
/**
|
|
81
91
|
* A callback function for memos that computes a value based on the previous value.
|
|
@@ -97,7 +107,7 @@ type TaskCallback<T extends {}> = (prev: T | undefined, signal: AbortSignal) =>
|
|
|
97
107
|
/**
|
|
98
108
|
* A callback function for effects that can perform side effects.
|
|
99
109
|
*
|
|
100
|
-
* @
|
|
110
|
+
* @returns An optional cleanup function that will be called before the effect re-runs or is disposed
|
|
101
111
|
*/
|
|
102
112
|
type EffectCallback = () => MaybeCleanup;
|
|
103
113
|
declare const TYPE_STATE = "State";
|
|
@@ -205,4 +215,4 @@ declare function untrack<T>(fn: () => T): T;
|
|
|
205
215
|
* ```
|
|
206
216
|
*/
|
|
207
217
|
declare function createScope(fn: () => MaybeCleanup): Cleanup;
|
|
208
|
-
export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY
|
|
218
|
+
export { type Cleanup, type ComputedOptions, type EffectCallback, type EffectNode, type MaybeCleanup, type MemoCallback, type MemoNode, type Scope, type Signal, type SignalOptions, type SinkNode, type StateNode, type TaskCallback, type TaskNode, activeOwner, activeSink, batch, batchDepth, createScope, DEFAULT_EQUALITY, SKIP_EQUALITY, FLAG_CLEAN, FLAG_DIRTY, flush, link, propagate, refresh, registerCleanup, runCleanup, runEffect, setState, trimSources, TYPE_COLLECTION, TYPE_LIST, TYPE_MEMO, TYPE_SENSOR, TYPE_STATE, TYPE_STORE, TYPE_TASK, unlink, untrack, };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { type Cleanup, type Signal } from '../graph';
|
|
2
|
-
import { type
|
|
2
|
+
import { type KeyConfig, type List } from './list';
|
|
3
3
|
type CollectionSource<T extends {}> = List<T> | Collection<T>;
|
|
4
4
|
type DeriveCollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
|
|
5
5
|
type Collection<T extends {}> = {
|
|
@@ -16,12 +16,17 @@ type Collection<T extends {}> = {
|
|
|
16
16
|
deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>;
|
|
17
17
|
readonly length: number;
|
|
18
18
|
};
|
|
19
|
+
type CollectionChanges<T> = {
|
|
20
|
+
add?: T[];
|
|
21
|
+
change?: T[];
|
|
22
|
+
remove?: T[];
|
|
23
|
+
};
|
|
19
24
|
type CollectionOptions<T extends {}> = {
|
|
20
25
|
value?: T[];
|
|
21
26
|
keyConfig?: KeyConfig<T>;
|
|
22
|
-
createItem?: (
|
|
27
|
+
createItem?: (value: T) => Signal<T>;
|
|
23
28
|
};
|
|
24
|
-
type CollectionCallback = (
|
|
29
|
+
type CollectionCallback<T extends {}> = (apply: (changes: CollectionChanges<T>) => void) => Cleanup;
|
|
25
30
|
/**
|
|
26
31
|
* Creates a derived Collection from a List or another Collection with item-level memoization.
|
|
27
32
|
* Sync callbacks use createMemo, async callbacks use createTask.
|
|
@@ -36,15 +41,15 @@ declare function deriveCollection<T extends {}, U extends {}>(source: Collection
|
|
|
36
41
|
declare function deriveCollection<T extends {}, U extends {}>(source: CollectionSource<U>, callback: (sourceValue: U, abort: AbortSignal) => Promise<T>): Collection<T>;
|
|
37
42
|
/**
|
|
38
43
|
* Creates an externally-driven Collection with a watched lifecycle.
|
|
39
|
-
* Items are managed
|
|
44
|
+
* Items are managed via the `applyChanges(changes)` helper passed to the watched callback.
|
|
40
45
|
* The collection activates when first accessed by an effect and deactivates when no longer watched.
|
|
41
46
|
*
|
|
42
47
|
* @since 0.18.0
|
|
43
|
-
* @param
|
|
48
|
+
* @param watched - Callback invoked when the collection starts being watched, receives applyChanges helper
|
|
44
49
|
* @param options - Optional configuration including initial value, key generation, and item signal creation
|
|
45
50
|
* @returns A read-only Collection signal
|
|
46
51
|
*/
|
|
47
|
-
declare function createCollection<T extends {}>(
|
|
52
|
+
declare function createCollection<T extends {}>(watched: CollectionCallback<T>, options?: CollectionOptions<T>): Collection<T>;
|
|
48
53
|
/**
|
|
49
54
|
* Checks if a value is a Collection signal.
|
|
50
55
|
*
|
|
@@ -61,4 +66,4 @@ declare function isCollection<T extends {}>(value: unknown): value is Collection
|
|
|
61
66
|
* @returns True if the value is a List or Collection
|
|
62
67
|
*/
|
|
63
68
|
declare function isCollectionSource<T extends {}>(value: unknown): value is CollectionSource<T>;
|
|
64
|
-
export { createCollection, deriveCollection, isCollection, isCollectionSource, type Collection, type CollectionCallback, type CollectionOptions, type CollectionSource, type DeriveCollectionCallback, };
|
|
69
|
+
export { createCollection, deriveCollection, isCollection, isCollectionSource, type Collection, type CollectionCallback, type CollectionChanges, type CollectionOptions, type CollectionSource, type DeriveCollectionCallback, };
|
|
@@ -8,7 +8,7 @@ type DiffResult = {
|
|
|
8
8
|
change: UnknownRecord;
|
|
9
9
|
remove: UnknownRecord;
|
|
10
10
|
};
|
|
11
|
-
type KeyConfig<T> = string | ((item: T) => string);
|
|
11
|
+
type KeyConfig<T> = string | ((item: T) => string | undefined);
|
|
12
12
|
type ListOptions<T extends {}> = {
|
|
13
13
|
keyConfig?: KeyConfig<T>;
|
|
14
14
|
watched?: () => Cleanup;
|
|
@@ -19,8 +19,8 @@ type List<T extends {}> = {
|
|
|
19
19
|
[Symbol.iterator](): IterableIterator<State<T>>;
|
|
20
20
|
readonly length: number;
|
|
21
21
|
get(): T[];
|
|
22
|
-
set(
|
|
23
|
-
update(fn: (
|
|
22
|
+
set(next: T[]): void;
|
|
23
|
+
update(fn: (prev: T[]) => T[]): void;
|
|
24
24
|
at(index: number): State<T> | undefined;
|
|
25
25
|
keys(): IterableIterator<string>;
|
|
26
26
|
byKey(key: string): State<T> | undefined;
|
|
@@ -37,23 +37,24 @@ type List<T extends {}> = {
|
|
|
37
37
|
* Checks if two values are equal with cycle detection
|
|
38
38
|
*
|
|
39
39
|
* @since 0.15.0
|
|
40
|
-
* @param
|
|
41
|
-
* @param
|
|
42
|
-
* @param
|
|
43
|
-
* @returns
|
|
40
|
+
* @param a - First value to compare
|
|
41
|
+
* @param b - Second value to compare
|
|
42
|
+
* @param visited - Set to track visited objects for cycle detection
|
|
43
|
+
* @returns Whether the two values are equal
|
|
44
44
|
*/
|
|
45
|
+
declare function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean;
|
|
45
46
|
/** Shallow equality check for string arrays */
|
|
46
47
|
declare function keysEqual(a: string[], b: string[]): boolean;
|
|
47
|
-
declare function
|
|
48
|
+
declare function getKeyGenerator<T extends {}>(keyConfig?: KeyConfig<T>): [(item: T) => string, boolean];
|
|
48
49
|
/**
|
|
49
50
|
* Creates a reactive list with stable keys and per-item reactivity.
|
|
50
51
|
*
|
|
51
52
|
* @since 0.18.0
|
|
52
|
-
* @param
|
|
53
|
+
* @param value - Initial array of items
|
|
53
54
|
* @param options - Optional configuration for key generation and watch lifecycle
|
|
54
55
|
* @returns A List signal
|
|
55
56
|
*/
|
|
56
|
-
declare function createList<T extends {}>(
|
|
57
|
+
declare function createList<T extends {}>(value: T[], options?: ListOptions<T>): List<T>;
|
|
57
58
|
/**
|
|
58
59
|
* Checks if a value is a List signal.
|
|
59
60
|
*
|
|
@@ -62,4 +63,4 @@ declare function createList<T extends {}>(initialValue: T[], options?: ListOptio
|
|
|
62
63
|
* @returns True if the value is a List
|
|
63
64
|
*/
|
|
64
65
|
declare function isList<T extends {}>(value: unknown): value is List<T>;
|
|
65
|
-
export { type DiffResult, type KeyConfig, type List, type ListOptions, type UnknownRecord, createList, isEqual, isList, keysEqual, TYPE_LIST, };
|
|
66
|
+
export { type DiffResult, type KeyConfig, type List, type ListOptions, type UnknownRecord, createList, isEqual, isList, getKeyGenerator, keysEqual, TYPE_LIST, };
|
|
@@ -25,6 +25,12 @@ type Memo<T extends {}> = {
|
|
|
25
25
|
* @template T - The type of value computed by the memo
|
|
26
26
|
* @param fn - The computation function that receives the previous value
|
|
27
27
|
* @param options - Optional configuration for the memo
|
|
28
|
+
* @param options.value - Optional initial value for reducer patterns
|
|
29
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
|
|
30
|
+
* @param options.guard - Optional type guard to validate values
|
|
31
|
+
* @param options.watched - Optional callback invoked when the memo is first watched by an effect.
|
|
32
|
+
* Receives an `invalidate` function to mark the memo dirty and trigger recomputation.
|
|
33
|
+
* Must return a cleanup function called when no effects are watching.
|
|
28
34
|
* @returns A Memo object with a get() method
|
|
29
35
|
*
|
|
30
36
|
* @example
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { type Cleanup, type
|
|
1
|
+
import { type Cleanup, type SignalOptions } from '../graph';
|
|
2
2
|
/**
|
|
3
3
|
* A read-only signal that tracks external input and updates a state value as long as it is active.
|
|
4
4
|
*
|
|
@@ -8,7 +8,6 @@ type Sensor<T extends {}> = {
|
|
|
8
8
|
readonly [Symbol.toStringTag]: 'Sensor';
|
|
9
9
|
/**
|
|
10
10
|
* Gets the current value of the sensor.
|
|
11
|
-
* Updates its state value if the sensor is active.
|
|
12
11
|
* When called inside another reactive context, creates a dependency.
|
|
13
12
|
* @returns The sensor value
|
|
14
13
|
* @throws UnsetSignalValueError If the sensor value is still unset when read.
|
|
@@ -22,6 +21,13 @@ type Sensor<T extends {}> = {
|
|
|
22
21
|
* @param set - A function to set the observed value
|
|
23
22
|
* @returns A cleanup function when the sensor stops being watched
|
|
24
23
|
*/
|
|
24
|
+
type SensorOptions<T extends {}> = SignalOptions<T> & {
|
|
25
|
+
/**
|
|
26
|
+
* Optional initial value. Avoids `UnsetSignalValueError` on first read
|
|
27
|
+
* before the watched callback fires.
|
|
28
|
+
*/
|
|
29
|
+
value?: T;
|
|
30
|
+
};
|
|
25
31
|
type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
|
|
26
32
|
/**
|
|
27
33
|
* Creates a sensor that tracks external input and updates a state value as long as it is active.
|
|
@@ -29,12 +35,12 @@ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
|
|
|
29
35
|
* no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
|
|
30
36
|
*
|
|
31
37
|
* @since 0.18.0
|
|
32
|
-
* @template T - The type of value
|
|
33
|
-
* @param
|
|
34
|
-
* @param options - Optional
|
|
38
|
+
* @template T - The type of value produced by the sensor
|
|
39
|
+
* @param watched - The callback invoked when the sensor starts being watched, receives a `set` function and returns a cleanup function.
|
|
40
|
+
* @param options - Optional configuration for the sensor.
|
|
35
41
|
* @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
|
|
36
|
-
* before the
|
|
37
|
-
* @param options.equals - Optional equality function. Defaults to
|
|
42
|
+
* before the watched callback fires. Essential for the mutable-object observation pattern.
|
|
43
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`). Use `SKIP_EQUALITY`
|
|
38
44
|
* for mutable objects where the reference stays the same but internal state changes.
|
|
39
45
|
* @param options.guard - Optional type guard to validate values.
|
|
40
46
|
* @returns A read-only sensor signal.
|
|
@@ -63,7 +69,7 @@ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
|
|
|
63
69
|
* }, { value: node, equals: SKIP_EQUALITY });
|
|
64
70
|
* ```
|
|
65
71
|
*/
|
|
66
|
-
declare function createSensor<T extends {}>(
|
|
72
|
+
declare function createSensor<T extends {}>(watched: SensorCallback<T>, options?: SensorOptions<T>): Sensor<T>;
|
|
67
73
|
/**
|
|
68
74
|
* Checks if a value is a Sensor signal.
|
|
69
75
|
*
|
|
@@ -72,4 +78,4 @@ declare function createSensor<T extends {}>(start: SensorCallback<T>, options?:
|
|
|
72
78
|
* @returns True if the value is a Sensor
|
|
73
79
|
*/
|
|
74
80
|
declare function isSensor<T extends {} = unknown & {}>(value: unknown): value is Sensor<T>;
|
|
75
|
-
export { createSensor, isSensor, type Sensor, type SensorCallback };
|
|
81
|
+
export { createSensor, isSensor, type Sensor, type SensorCallback, type SensorOptions, };
|
|
@@ -14,8 +14,8 @@ type BaseStore<T extends UnknownRecord> = {
|
|
|
14
14
|
keys(): IterableIterator<string>;
|
|
15
15
|
byKey<K extends keyof T & string>(key: K): T[K] extends readonly (infer U extends {})[] ? List<U> : T[K] extends UnknownRecord ? Store<T[K]> : T[K] extends unknown & {} ? State<T[K] & {}> : State<T[K] & {}> | undefined;
|
|
16
16
|
get(): T;
|
|
17
|
-
set(
|
|
18
|
-
update(fn: (
|
|
17
|
+
set(next: T): void;
|
|
18
|
+
update(fn: (prev: T) => T): void;
|
|
19
19
|
add<K extends keyof T & string>(key: K, value: T[K]): K;
|
|
20
20
|
remove(key: string): void;
|
|
21
21
|
};
|
|
@@ -28,7 +28,7 @@ type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
|
28
28
|
* Properties are accessible directly via proxy.
|
|
29
29
|
*
|
|
30
30
|
* @since 0.15.0
|
|
31
|
-
* @param
|
|
31
|
+
* @param value - Initial object value of the store
|
|
32
32
|
* @param options - Optional configuration for watch lifecycle
|
|
33
33
|
* @returns A Store with reactive properties
|
|
34
34
|
*
|
|
@@ -39,7 +39,7 @@ type Store<T extends UnknownRecord> = BaseStore<T> & {
|
|
|
39
39
|
* console.log(user.get()); // { name: 'Bob', age: 30 }
|
|
40
40
|
* ```
|
|
41
41
|
*/
|
|
42
|
-
declare function createStore<T extends UnknownRecord>(
|
|
42
|
+
declare function createStore<T extends UnknownRecord>(value: T, options?: StoreOptions): Store<T>;
|
|
43
43
|
/**
|
|
44
44
|
* Checks if a value is a Store signal.
|
|
45
45
|
*
|
|
@@ -36,6 +36,12 @@ type Task<T extends {}> = {
|
|
|
36
36
|
* @template T - The type of value resolved by the task
|
|
37
37
|
* @param fn - The async computation function that receives the previous value and an AbortSignal
|
|
38
38
|
* @param options - Optional configuration for the task
|
|
39
|
+
* @param options.value - Optional initial value for reducer patterns
|
|
40
|
+
* @param options.equals - Optional equality function. Defaults to strict equality (`===`)
|
|
41
|
+
* @param options.guard - Optional type guard to validate values
|
|
42
|
+
* @param options.watched - Optional callback invoked when the task is first watched by an effect.
|
|
43
|
+
* Receives an `invalidate` function to mark the task dirty and trigger re-execution.
|
|
44
|
+
* Must return a cleanup function called when no effects are watching.
|
|
39
45
|
* @returns A Task object with get(), isPending(), and abort() methods
|
|
40
46
|
*
|
|
41
47
|
* @example
|