@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.
Files changed (89) hide show
  1. package/.ai-context.md +163 -232
  2. package/.cursorrules +41 -35
  3. package/.github/copilot-instructions.md +166 -116
  4. package/ARCHITECTURE.md +274 -0
  5. package/CLAUDE.md +199 -143
  6. package/COLLECTION_REFACTORING.md +161 -0
  7. package/GUIDE.md +298 -0
  8. package/README.md +232 -197
  9. package/REQUIREMENTS.md +100 -0
  10. package/bench/reactivity.bench.ts +577 -0
  11. package/index.dev.js +1325 -997
  12. package/index.js +1 -1
  13. package/index.ts +58 -74
  14. package/package.json +4 -1
  15. package/src/errors.ts +118 -74
  16. package/src/graph.ts +601 -0
  17. package/src/nodes/collection.ts +474 -0
  18. package/src/nodes/effect.ts +149 -0
  19. package/src/nodes/list.ts +588 -0
  20. package/src/nodes/memo.ts +120 -0
  21. package/src/nodes/sensor.ts +139 -0
  22. package/src/nodes/state.ts +135 -0
  23. package/src/nodes/store.ts +383 -0
  24. package/src/nodes/task.ts +146 -0
  25. package/src/signal.ts +112 -66
  26. package/src/util.ts +26 -57
  27. package/test/batch.test.ts +96 -62
  28. package/test/benchmark.test.ts +473 -487
  29. package/test/collection.test.ts +466 -706
  30. package/test/effect.test.ts +293 -696
  31. package/test/list.test.ts +335 -592
  32. package/test/memo.test.ts +380 -0
  33. package/test/regression.test.ts +156 -0
  34. package/test/scope.test.ts +191 -0
  35. package/test/sensor.test.ts +454 -0
  36. package/test/signal.test.ts +220 -213
  37. package/test/state.test.ts +217 -265
  38. package/test/store.test.ts +346 -446
  39. package/test/task.test.ts +395 -0
  40. package/test/untrack.test.ts +167 -0
  41. package/types/index.d.ts +13 -15
  42. package/types/src/errors.d.ts +73 -17
  43. package/types/src/graph.d.ts +208 -0
  44. package/types/src/nodes/collection.d.ts +64 -0
  45. package/types/src/nodes/effect.d.ts +48 -0
  46. package/types/src/nodes/list.d.ts +65 -0
  47. package/types/src/nodes/memo.d.ts +57 -0
  48. package/types/src/nodes/sensor.d.ts +75 -0
  49. package/types/src/nodes/state.d.ts +78 -0
  50. package/types/src/nodes/store.d.ts +51 -0
  51. package/types/src/nodes/task.d.ts +73 -0
  52. package/types/src/signal.d.ts +43 -29
  53. package/types/src/util.d.ts +9 -16
  54. package/archive/benchmark.ts +0 -683
  55. package/archive/collection.ts +0 -253
  56. package/archive/composite.ts +0 -85
  57. package/archive/computed.ts +0 -195
  58. package/archive/list.ts +0 -483
  59. package/archive/memo.ts +0 -139
  60. package/archive/state.ts +0 -90
  61. package/archive/store.ts +0 -298
  62. package/archive/task.ts +0 -189
  63. package/src/classes/collection.ts +0 -245
  64. package/src/classes/computed.ts +0 -349
  65. package/src/classes/list.ts +0 -343
  66. package/src/classes/ref.ts +0 -70
  67. package/src/classes/state.ts +0 -102
  68. package/src/classes/store.ts +0 -262
  69. package/src/diff.ts +0 -138
  70. package/src/effect.ts +0 -93
  71. package/src/match.ts +0 -45
  72. package/src/resolve.ts +0 -49
  73. package/src/system.ts +0 -257
  74. package/test/computed.test.ts +0 -1108
  75. package/test/diff.test.ts +0 -955
  76. package/test/match.test.ts +0 -388
  77. package/test/ref.test.ts +0 -353
  78. package/test/resolve.test.ts +0 -154
  79. package/types/src/classes/collection.d.ts +0 -45
  80. package/types/src/classes/computed.d.ts +0 -94
  81. package/types/src/classes/list.d.ts +0 -43
  82. package/types/src/classes/ref.d.ts +0 -35
  83. package/types/src/classes/state.d.ts +0 -49
  84. package/types/src/classes/store.d.ts +0 -52
  85. package/types/src/diff.d.ts +0 -28
  86. package/types/src/effect.d.ts +0 -15
  87. package/types/src/match.d.ts +0 -21
  88. package/types/src/resolve.d.ts +0 -29
  89. package/types/src/system.d.ts +0 -78
@@ -0,0 +1,208 @@
1
+ import { type Guard } from './errors';
2
+ type SourceFields<T extends {}> = {
3
+ value: T;
4
+ sinks: Edge | null;
5
+ sinksTail: Edge | null;
6
+ stop?: Cleanup;
7
+ };
8
+ type OptionsFields<T extends {}> = {
9
+ equals: (a: T, b: T) => boolean;
10
+ guard?: Guard<T>;
11
+ };
12
+ type SinkFields = {
13
+ fn: unknown;
14
+ flags: number;
15
+ sources: Edge | null;
16
+ sourcesTail: Edge | null;
17
+ };
18
+ type OwnerFields = {
19
+ cleanup: Cleanup | Cleanup[] | null;
20
+ };
21
+ type AsyncFields = {
22
+ controller: AbortController | undefined;
23
+ error: Error | undefined;
24
+ };
25
+ type StateNode<T extends {}> = SourceFields<T> & OptionsFields<T>;
26
+ type MemoNode<T extends {}> = SourceFields<T> & OptionsFields<T> & SinkFields & {
27
+ fn: MemoCallback<T>;
28
+ error: Error | undefined;
29
+ };
30
+ type TaskNode<T extends {}> = SourceFields<T> & OptionsFields<T> & SinkFields & AsyncFields & {
31
+ fn: (prev: T, abort: AbortSignal) => Promise<T>;
32
+ };
33
+ type EffectNode = SinkFields & OwnerFields & {
34
+ fn: EffectCallback;
35
+ };
36
+ type Scope = OwnerFields;
37
+ type SourceNode = SourceFields<unknown & {}>;
38
+ type SinkNode = MemoNode<unknown & {}> | TaskNode<unknown & {}> | EffectNode;
39
+ type OwnerNode = EffectNode | Scope;
40
+ type Edge = {
41
+ source: SourceNode;
42
+ sink: SinkNode;
43
+ nextSource: Edge | null;
44
+ prevSink: Edge | null;
45
+ nextSink: Edge | null;
46
+ };
47
+ type Signal<T extends {}> = {
48
+ get(): T;
49
+ };
50
+ /**
51
+ * A cleanup function that can be called to dispose of resources.
52
+ */
53
+ type Cleanup = () => void;
54
+ type MaybeCleanup = Cleanup | undefined | void;
55
+ /**
56
+ * Options for configuring signal behavior.
57
+ *
58
+ * @template T - The type of value in the signal
59
+ */
60
+ type SignalOptions<T extends {}> = {
61
+ /**
62
+ * Optional type guard to validate values.
63
+ * If provided, will throw an error if an invalid value is set.
64
+ */
65
+ guard?: Guard<T>;
66
+ /**
67
+ * Optional custom equality function.
68
+ * Used to determine if a new value is different from the old value.
69
+ * Defaults to reference equality (===).
70
+ */
71
+ equals?: (a: T, b: T) => boolean;
72
+ };
73
+ type ComputedOptions<T extends {}> = SignalOptions<T> & {
74
+ /**
75
+ * Optional initial value.
76
+ * Useful for reducer patterns so that calculations start with a value of correct type.
77
+ */
78
+ value?: T;
79
+ };
80
+ /**
81
+ * A callback function for memos that computes a value based on the previous value.
82
+ *
83
+ * @template T - The type of value computed
84
+ * @param prev - The previous computed value
85
+ * @returns The new computed value
86
+ */
87
+ type MemoCallback<T extends {}> = (prev: T | undefined) => T;
88
+ /**
89
+ * A callback function for tasks that asynchronously computes a value.
90
+ *
91
+ * @template T - The type of value computed
92
+ * @param prev - The previous computed value
93
+ * @param signal - An AbortSignal that will be triggered if the task is aborted
94
+ * @returns A promise that resolves to the new computed value
95
+ */
96
+ type TaskCallback<T extends {}> = (prev: T | undefined, signal: AbortSignal) => Promise<T>;
97
+ /**
98
+ * A callback function for effects that can perform side effects.
99
+ *
100
+ * @param match - A function to register cleanup callbacks that will be called before the effect re-runs or is disposed
101
+ */
102
+ type EffectCallback = () => MaybeCleanup;
103
+ declare const TYPE_STATE = "State";
104
+ declare const TYPE_MEMO = "Memo";
105
+ declare const TYPE_TASK = "Task";
106
+ declare const TYPE_SENSOR = "Sensor";
107
+ declare const TYPE_LIST = "List";
108
+ declare const TYPE_COLLECTION = "Collection";
109
+ declare const TYPE_STORE = "Store";
110
+ declare const FLAG_CLEAN = 0;
111
+ declare const FLAG_DIRTY: number;
112
+ declare let activeSink: SinkNode | null;
113
+ declare let activeOwner: OwnerNode | null;
114
+ declare let batchDepth: number;
115
+ declare const DEFAULT_EQUALITY: <T extends {}>(a: T, b: T) => boolean;
116
+ /**
117
+ * Equality function that always returns false, causing propagation on every update.
118
+ * Use with `createSensor` for observing mutable objects where the reference stays the same
119
+ * but internal state changes (e.g., DOM elements observed via MutationObserver).
120
+ *
121
+ * @example
122
+ * ```ts
123
+ * const el = createSensor<HTMLElement>((set) => {
124
+ * const node = document.getElementById('box')!;
125
+ * set(node);
126
+ * const obs = new MutationObserver(() => set(node));
127
+ * obs.observe(node, { attributes: true });
128
+ * return () => obs.disconnect();
129
+ * }, { value: node, equals: SKIP_EQUALITY });
130
+ * ```
131
+ */
132
+ declare const SKIP_EQUALITY: (_a?: unknown, _b?: unknown) => boolean;
133
+ declare function link(source: SourceNode, sink: SinkNode): void;
134
+ declare function unlink(edge: Edge): Edge | null;
135
+ declare function trimSources(node: SinkNode): void;
136
+ declare function propagate(node: SinkNode, newFlag?: number): void;
137
+ declare function setState<T extends {}>(node: StateNode<T>, next: T): void;
138
+ declare function registerCleanup(owner: OwnerNode, fn: Cleanup): void;
139
+ declare function runCleanup(owner: OwnerNode): void;
140
+ declare function runEffect(node: EffectNode): void;
141
+ declare function refresh(node: SinkNode): void;
142
+ declare function flush(): void;
143
+ /**
144
+ * Batches multiple signal updates together.
145
+ * Effects will not run until the batch completes.
146
+ * Batches can be nested; effects run when the outermost batch completes.
147
+ *
148
+ * @param fn - The function to execute within the batch
149
+ *
150
+ * @example
151
+ * ```ts
152
+ * const count = createState(0);
153
+ * const double = createMemo(() => count.get() * 2);
154
+ *
155
+ * batch(() => {
156
+ * count.set(1);
157
+ * count.set(2);
158
+ * count.set(3);
159
+ * // Effects run only once at the end with count = 3
160
+ * });
161
+ * ```
162
+ */
163
+ declare function batch(fn: () => void): void;
164
+ /**
165
+ * Runs a callback without tracking dependencies.
166
+ * Any signal reads inside the callback will not create edges to the current active sink.
167
+ *
168
+ * @param fn - The function to execute without tracking
169
+ * @returns The return value of the function
170
+ *
171
+ * @example
172
+ * ```ts
173
+ * const count = createState(0);
174
+ * const label = createState('Count');
175
+ *
176
+ * createEffect(() => {
177
+ * // Only re-runs when count changes, not when label changes
178
+ * const name = untrack(() => label.get());
179
+ * console.log(`${name}: ${count.get()}`);
180
+ * });
181
+ * ```
182
+ */
183
+ declare function untrack<T>(fn: () => T): T;
184
+ /**
185
+ * Creates a new ownership scope for managing cleanup of nested effects and resources.
186
+ * All effects created within the scope will be automatically disposed when the scope is disposed.
187
+ * Scopes can be nested - disposing a parent scope disposes all child scopes.
188
+ *
189
+ * @param fn - The function to execute within the scope, may return a cleanup function
190
+ * @returns A dispose function that cleans up the scope
191
+ *
192
+ * @example
193
+ * ```ts
194
+ * const dispose = createScope(() => {
195
+ * const count = createState(0);
196
+ *
197
+ * createEffect(() => {
198
+ * console.log(count.get());
199
+ * });
200
+ *
201
+ * return () => console.log('Scope disposed');
202
+ * });
203
+ *
204
+ * dispose(); // Cleans up the effect and runs cleanup callbacks
205
+ * ```
206
+ */
207
+ 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 as defaultEquals, 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, };
@@ -0,0 +1,64 @@
1
+ import { type Cleanup, type Signal } from '../graph';
2
+ import { type DiffResult, type KeyConfig, type List } from './list';
3
+ type CollectionSource<T extends {}> = List<T> | Collection<T>;
4
+ type DeriveCollectionCallback<T extends {}, U extends {}> = ((sourceValue: U) => T) | ((sourceValue: U, abort: AbortSignal) => Promise<T>);
5
+ type Collection<T extends {}> = {
6
+ readonly [Symbol.toStringTag]: 'Collection';
7
+ readonly [Symbol.isConcatSpreadable]: true;
8
+ [Symbol.iterator](): IterableIterator<Signal<T>>;
9
+ keys(): IterableIterator<string>;
10
+ get(): T[];
11
+ at(index: number): Signal<T> | undefined;
12
+ byKey(key: string): Signal<T> | undefined;
13
+ keyAt(index: number): string | undefined;
14
+ indexOfKey(key: string): number;
15
+ deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R>;
16
+ deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>;
17
+ readonly length: number;
18
+ };
19
+ type CollectionOptions<T extends {}> = {
20
+ value?: T[];
21
+ keyConfig?: KeyConfig<T>;
22
+ createItem?: (key: string, value: T) => Signal<T>;
23
+ };
24
+ type CollectionCallback = (applyChanges: (changes: DiffResult) => void) => Cleanup;
25
+ /**
26
+ * Creates a derived Collection from a List or another Collection with item-level memoization.
27
+ * Sync callbacks use createMemo, async callbacks use createTask.
28
+ * Structural changes are tracked reactively via the source's keys.
29
+ *
30
+ * @since 0.18.0
31
+ * @param source - The source List or Collection to derive from
32
+ * @param callback - Transformation function applied to each item
33
+ * @returns A Collection signal
34
+ */
35
+ declare function deriveCollection<T extends {}, U extends {}>(source: CollectionSource<U>, callback: (sourceValue: U) => T): Collection<T>;
36
+ declare function deriveCollection<T extends {}, U extends {}>(source: CollectionSource<U>, callback: (sourceValue: U, abort: AbortSignal) => Promise<T>): Collection<T>;
37
+ /**
38
+ * Creates an externally-driven Collection with a watched lifecycle.
39
+ * Items are managed by the start callback via `applyChanges(diffResult)`.
40
+ * The collection activates when first accessed by an effect and deactivates when no longer watched.
41
+ *
42
+ * @since 0.18.0
43
+ * @param start - Callback invoked when the collection starts being watched, receives applyChanges helper
44
+ * @param options - Optional configuration including initial value, key generation, and item signal creation
45
+ * @returns A read-only Collection signal
46
+ */
47
+ declare function createCollection<T extends {}>(start: CollectionCallback, options?: CollectionOptions<T>): Collection<T>;
48
+ /**
49
+ * Checks if a value is a Collection signal.
50
+ *
51
+ * @since 0.17.2
52
+ * @param value - The value to check
53
+ * @returns True if the value is a Collection
54
+ */
55
+ declare function isCollection<T extends {}>(value: unknown): value is Collection<T>;
56
+ /**
57
+ * Checks if a value is a valid Collection source (List or Collection).
58
+ *
59
+ * @since 0.17.2
60
+ * @param value - The value to check
61
+ * @returns True if the value is a List or Collection
62
+ */
63
+ 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, };
@@ -0,0 +1,48 @@
1
+ import { type Cleanup, type EffectCallback, type MaybeCleanup, type Signal } from '../graph';
2
+ type MaybePromise<T> = T | Promise<T>;
3
+ type MatchHandlers<T extends Signal<unknown & {}>[]> = {
4
+ ok: (values: {
5
+ [K in keyof T]: T[K] extends Signal<infer V> ? V : never;
6
+ }) => MaybePromise<MaybeCleanup>;
7
+ err?: (errors: readonly Error[]) => MaybePromise<MaybeCleanup>;
8
+ nil?: () => MaybePromise<MaybeCleanup>;
9
+ };
10
+ /**
11
+ * Creates a reactive effect that automatically runs when its dependencies change.
12
+ * Effects run immediately upon creation and re-run when any tracked signal changes.
13
+ * Effects are executed during the flush phase, after all updates have been batched.
14
+ *
15
+ * @since 0.1.0
16
+ * @param fn - The effect function that can track dependencies and register cleanup callbacks
17
+ * @returns A cleanup function that can be called to dispose of the effect
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * const count = createState(0);
22
+ * const dispose = createEffect(() => {
23
+ * console.log('Count is:', count.get());
24
+ * });
25
+ *
26
+ * count.set(1); // Logs: "Count is: 1"
27
+ * dispose(); // Stop the effect
28
+ * ```
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * // With cleanup
33
+ * createEffect(() => {
34
+ * const timer = setInterval(() => console.log(count.get()), 1000);
35
+ * return () => clearInterval(timer);
36
+ * });
37
+ * ```
38
+ */
39
+ declare function createEffect(fn: EffectCallback): Cleanup;
40
+ /**
41
+ * Runs handlers based on the current values of signals.
42
+ * Must be called within an active owner (effect or scope) so async cleanup can be registered.
43
+ *
44
+ * @since 0.15.0
45
+ * @throws RequiredOwnerError If called without an active owner.
46
+ */
47
+ declare function match<T extends Signal<unknown & {}>[]>(signals: T, handlers: MatchHandlers<T>): MaybeCleanup;
48
+ export { type MaybePromise, type MatchHandlers, createEffect, match };
@@ -0,0 +1,65 @@
1
+ import { type Cleanup, TYPE_LIST } from '../graph';
2
+ import { type Collection } from './collection';
3
+ import { type State } from './state';
4
+ type UnknownRecord = Record<string, unknown>;
5
+ type DiffResult = {
6
+ changed: boolean;
7
+ add: UnknownRecord;
8
+ change: UnknownRecord;
9
+ remove: UnknownRecord;
10
+ };
11
+ type KeyConfig<T> = string | ((item: T) => string);
12
+ type ListOptions<T extends {}> = {
13
+ keyConfig?: KeyConfig<T>;
14
+ watched?: () => Cleanup;
15
+ };
16
+ type List<T extends {}> = {
17
+ readonly [Symbol.toStringTag]: 'List';
18
+ readonly [Symbol.isConcatSpreadable]: true;
19
+ [Symbol.iterator](): IterableIterator<State<T>>;
20
+ readonly length: number;
21
+ get(): T[];
22
+ set(newValue: T[]): void;
23
+ update(fn: (oldValue: T[]) => T[]): void;
24
+ at(index: number): State<T> | undefined;
25
+ keys(): IterableIterator<string>;
26
+ byKey(key: string): State<T> | undefined;
27
+ keyAt(index: number): string | undefined;
28
+ indexOfKey(key: string): number;
29
+ add(value: T): string;
30
+ remove(keyOrIndex: string | number): void;
31
+ sort(compareFn?: (a: T, b: T) => number): void;
32
+ splice(start: number, deleteCount?: number, ...items: T[]): T[];
33
+ deriveCollection<R extends {}>(callback: (sourceValue: T) => R): Collection<R>;
34
+ deriveCollection<R extends {}>(callback: (sourceValue: T, abort: AbortSignal) => Promise<R>): Collection<R>;
35
+ };
36
+ /**
37
+ * Checks if two values are equal with cycle detection
38
+ *
39
+ * @since 0.15.0
40
+ * @param {T} a - First value to compare
41
+ * @param {T} b - Second value to compare
42
+ * @param {WeakSet<object>} visited - Set to track visited objects for cycle detection
43
+ * @returns {boolean} Whether the two values are equal
44
+ */
45
+ /** Shallow equality check for string arrays */
46
+ declare function keysEqual(a: string[], b: string[]): boolean;
47
+ declare function isEqual<T>(a: T, b: T, visited?: WeakSet<object>): boolean;
48
+ /**
49
+ * Creates a reactive list with stable keys and per-item reactivity.
50
+ *
51
+ * @since 0.18.0
52
+ * @param initialValue - Initial array of items
53
+ * @param options - Optional configuration for key generation and watch lifecycle
54
+ * @returns A List signal
55
+ */
56
+ declare function createList<T extends {}>(initialValue: T[], options?: ListOptions<T>): List<T>;
57
+ /**
58
+ * Checks if a value is a List signal.
59
+ *
60
+ * @since 0.15.0
61
+ * @param value - The value to check
62
+ * @returns True if the value is a List
63
+ */
64
+ 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, };
@@ -0,0 +1,57 @@
1
+ import { type ComputedOptions, type MemoCallback } from '../graph';
2
+ /**
3
+ * A derived reactive computation that caches its result.
4
+ * Automatically tracks dependencies and recomputes when they change.
5
+ *
6
+ * @template T - The type of value computed by the memo
7
+ */
8
+ type Memo<T extends {}> = {
9
+ readonly [Symbol.toStringTag]: 'Memo';
10
+ /**
11
+ * Gets the current value of the memo.
12
+ * Recomputes if dependencies have changed since last access.
13
+ * When called inside another reactive context, creates a dependency.
14
+ * @returns The computed value
15
+ * @throws UnsetSignalValueError If the memo value is still unset when read.
16
+ */
17
+ get(): T;
18
+ };
19
+ /**
20
+ * Creates a derived reactive computation that caches its result.
21
+ * The computation automatically tracks dependencies and recomputes when they change.
22
+ * Uses lazy evaluation - only computes when the value is accessed.
23
+ *
24
+ * @since 0.18.0
25
+ * @template T - The type of value computed by the memo
26
+ * @param fn - The computation function that receives the previous value
27
+ * @param options - Optional configuration for the memo
28
+ * @returns A Memo object with a get() method
29
+ *
30
+ * @example
31
+ * ```ts
32
+ * const count = createState(0);
33
+ * const doubled = createMemo(() => count.get() * 2);
34
+ * console.log(doubled.get()); // 0
35
+ * count.set(5);
36
+ * console.log(doubled.get()); // 10
37
+ * ```
38
+ *
39
+ * @example
40
+ * ```ts
41
+ * // Using previous value
42
+ * const sum = createMemo((prev) => prev + count.get(), { value: 0, equals: Object.is });
43
+ * ```
44
+ */
45
+ declare function createMemo<T extends {}>(fn: (prev: T) => T, options: ComputedOptions<T> & {
46
+ value: T;
47
+ }): Memo<T>;
48
+ declare function createMemo<T extends {}>(fn: MemoCallback<T>, options?: ComputedOptions<T>): Memo<T>;
49
+ /**
50
+ * Checks if a value is a Memo signal.
51
+ *
52
+ * @since 0.18.0
53
+ * @param value - The value to check
54
+ * @returns True if the value is a Memo
55
+ */
56
+ declare function isMemo<T extends {} = unknown & {}>(value: unknown): value is Memo<T>;
57
+ export { createMemo, isMemo, type Memo };
@@ -0,0 +1,75 @@
1
+ import { type Cleanup, type ComputedOptions } from '../graph';
2
+ /**
3
+ * A read-only signal that tracks external input and updates a state value as long as it is active.
4
+ *
5
+ * @template T - The type of value produced by the sensor
6
+ */
7
+ type Sensor<T extends {}> = {
8
+ readonly [Symbol.toStringTag]: 'Sensor';
9
+ /**
10
+ * Gets the current value of the sensor.
11
+ * Updates its state value if the sensor is active.
12
+ * When called inside another reactive context, creates a dependency.
13
+ * @returns The sensor value
14
+ * @throws UnsetSignalValueError If the sensor value is still unset when read.
15
+ */
16
+ get(): T;
17
+ };
18
+ /**
19
+ * A callback function for sensors when the sensor starts being watched.
20
+ *
21
+ * @template T - The type of value observed
22
+ * @param set - A function to set the observed value
23
+ * @returns A cleanup function when the sensor stops being watched
24
+ */
25
+ type SensorCallback<T extends {}> = (set: (next: T) => void) => Cleanup;
26
+ /**
27
+ * Creates a sensor that tracks external input and updates a state value as long as it is active.
28
+ * Sensors get activated when they are first accessed by an effect and deactivated when they are
29
+ * no longer watched. This lazy activation pattern ensures resources are only consumed when needed.
30
+ *
31
+ * @since 0.18.0
32
+ * @template T - The type of value stored in the state
33
+ * @param start - The callback function that starts the sensor and returns a cleanup function.
34
+ * @param options - Optional options for the sensor.
35
+ * @param options.value - Optional initial value. Avoids `UnsetSignalValueError` on first read
36
+ * before the start callback fires. Essential for the mutable-object observation pattern.
37
+ * @param options.equals - Optional equality function. Defaults to `Object.is`. Use `SKIP_EQUALITY`
38
+ * for mutable objects where the reference stays the same but internal state changes.
39
+ * @param options.guard - Optional type guard to validate values.
40
+ * @returns A read-only sensor signal.
41
+ *
42
+ * @example Tracking external values
43
+ * ```ts
44
+ * const mousePos = createSensor<{ x: number; y: number }>((set) => {
45
+ * const handler = (e: MouseEvent) => {
46
+ * set({ x: e.clientX, y: e.clientY });
47
+ * };
48
+ * window.addEventListener('mousemove', handler);
49
+ * return () => window.removeEventListener('mousemove', handler);
50
+ * });
51
+ * ```
52
+ *
53
+ * @example Observing a mutable object
54
+ * ```ts
55
+ * import { createSensor, SKIP_EQUALITY } from 'cause-effect';
56
+ *
57
+ * const el = createSensor<HTMLElement>((set) => {
58
+ * const node = document.getElementById('box')!;
59
+ * set(node);
60
+ * const obs = new MutationObserver(() => set(node));
61
+ * obs.observe(node, { attributes: true });
62
+ * return () => obs.disconnect();
63
+ * }, { value: node, equals: SKIP_EQUALITY });
64
+ * ```
65
+ */
66
+ declare function createSensor<T extends {}>(start: SensorCallback<T>, options?: ComputedOptions<T>): Sensor<T>;
67
+ /**
68
+ * Checks if a value is a Sensor signal.
69
+ *
70
+ * @since 0.18.0
71
+ * @param value - The value to check
72
+ * @returns True if the value is a Sensor
73
+ */
74
+ declare function isSensor<T extends {} = unknown & {}>(value: unknown): value is Sensor<T>;
75
+ export { createSensor, isSensor, type Sensor, type SensorCallback };
@@ -0,0 +1,78 @@
1
+ import { type SignalOptions } from '../graph';
2
+ /**
3
+ * A callback function for states that updates a value based on the previous value.
4
+ *
5
+ * @template T - The type of value
6
+ * @param prev - The previous state value
7
+ * @returns The new state value
8
+ */
9
+ type UpdateCallback<T extends {}> = (prev: T) => T;
10
+ /**
11
+ * A mutable reactive state container.
12
+ * Changes to the state will automatically propagate to dependent computations and effects.
13
+ *
14
+ * @template T - The type of value stored in the state
15
+ */
16
+ type State<T extends {}> = {
17
+ readonly [Symbol.toStringTag]: 'State';
18
+ /**
19
+ * Gets the current value of the state.
20
+ * When called inside a memo, task, or effect, creates a dependency.
21
+ * @returns The current value
22
+ */
23
+ get(): T;
24
+ /**
25
+ * Sets a new value for the state.
26
+ * If the new value is different (according to the equality function), all dependents will be notified.
27
+ * @param next - The new value to set
28
+ */
29
+ set(next: T): void;
30
+ /**
31
+ * Updates the state with a new value computed by a callback function.
32
+ * The callback receives the current value as an argument.
33
+ * @param fn - The callback function to compute the new value
34
+ */
35
+ update(fn: UpdateCallback<T>): void;
36
+ };
37
+ /**
38
+ * Creates a mutable reactive state container.
39
+ *
40
+ * @since 0.9.0
41
+ * @template T - The type of value stored in the state
42
+ * @param value - The initial value
43
+ * @param options - Optional configuration for the state
44
+ * @returns A State object with get() and set() methods
45
+ *
46
+ * @example
47
+ * ```ts
48
+ * const count = createState(0);
49
+ * count.set(1);
50
+ * console.log(count.get()); // 1
51
+ * ```
52
+ *
53
+ * @example
54
+ * ```ts
55
+ * // With type guard
56
+ * const count = createState(0, {
57
+ * guard: (v): v is number => typeof v === 'number'
58
+ * });
59
+ * ```
60
+ */
61
+ declare function createState<T extends {}>(value: T, options?: SignalOptions<T>): State<T>;
62
+ /**
63
+ * Checks if a value is a State signal.
64
+ *
65
+ * @since 0.9.0
66
+ * @param value - The value to check
67
+ * @returns True if the value is a State
68
+ *
69
+ * @example
70
+ * ```ts
71
+ * const state = createState(0);
72
+ * if (isState(state)) {
73
+ * state.set(1); // TypeScript knows state has set()
74
+ * }
75
+ * ```
76
+ */
77
+ declare function isState<T extends {} = unknown & {}>(value: unknown): value is State<T>;
78
+ export { createState, isState, type State, type UpdateCallback };
@@ -0,0 +1,51 @@
1
+ import { type Cleanup, TYPE_STORE } from '../graph';
2
+ import { type List, type UnknownRecord } from './list';
3
+ import { type State } from './state';
4
+ type StoreOptions = {
5
+ watched?: () => Cleanup;
6
+ };
7
+ type BaseStore<T extends UnknownRecord> = {
8
+ readonly [Symbol.toStringTag]: 'Store';
9
+ readonly [Symbol.isConcatSpreadable]: false;
10
+ [Symbol.iterator](): IterableIterator<[
11
+ string,
12
+ State<T[keyof T] & {}> | Store<UnknownRecord> | List<unknown & {}>
13
+ ]>;
14
+ keys(): IterableIterator<string>;
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
+ get(): T;
17
+ set(newValue: T): void;
18
+ update(fn: (oldValue: T) => T): void;
19
+ add<K extends keyof T & string>(key: K, value: T[K]): K;
20
+ remove(key: string): void;
21
+ };
22
+ type Store<T extends UnknownRecord> = BaseStore<T> & {
23
+ [K in keyof T]: 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;
24
+ };
25
+ /**
26
+ * Creates a reactive store with deeply nested reactive properties.
27
+ * Each property becomes its own signal (State for primitives, nested Store for objects, List for arrays).
28
+ * Properties are accessible directly via proxy.
29
+ *
30
+ * @since 0.15.0
31
+ * @param initialValue - Initial object value of the store
32
+ * @param options - Optional configuration for watch lifecycle
33
+ * @returns A Store with reactive properties
34
+ *
35
+ * @example
36
+ * ```ts
37
+ * const user = createStore({ name: 'Alice', age: 30 });
38
+ * user.name.set('Bob'); // Only name subscribers react
39
+ * console.log(user.get()); // { name: 'Bob', age: 30 }
40
+ * ```
41
+ */
42
+ declare function createStore<T extends UnknownRecord>(initialValue: T, options?: StoreOptions): Store<T>;
43
+ /**
44
+ * Checks if a value is a Store signal.
45
+ *
46
+ * @since 0.15.0
47
+ * @param value - The value to check
48
+ * @returns True if the value is a Store
49
+ */
50
+ declare function isStore<T extends UnknownRecord>(value: unknown): value is Store<T>;
51
+ export { createStore, isStore, type Store, type StoreOptions, TYPE_STORE };