@alwatr/signal 4.1.0 → 5.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +143 -0
- package/README.md +390 -41
- package/dist/computed-signal.d.ts +95 -0
- package/dist/computed-signal.d.ts.map +1 -0
- package/dist/effect-signal.d.ts +75 -0
- package/dist/effect-signal.d.ts.map +1 -0
- package/dist/event-signal.d.ts +46 -0
- package/dist/event-signal.d.ts.map +1 -0
- package/dist/main.cjs +439 -33
- package/dist/main.cjs.map +4 -4
- package/dist/main.d.ts +6 -2
- package/dist/main.d.ts.map +1 -1
- package/dist/main.mjs +434 -31
- package/dist/main.mjs.map +4 -4
- package/dist/signal-base.d.ts +90 -0
- package/dist/signal-base.d.ts.map +1 -0
- package/dist/state-signal.d.ts +83 -0
- package/dist/state-signal.d.ts.map +1 -0
- package/dist/type.d.ts +240 -0
- package/dist/type.d.ts.map +1 -0
- package/package.json +11 -9
- package/src/computed-signal.test.js +150 -0
- package/src/effect-signal.test.js +150 -0
- package/src/event-signal.test.js +194 -0
- package/src/state-signal.test.js +236 -0
- package/dist/logger.d.ts +0 -2
- package/dist/logger.d.ts.map +0 -1
- package/dist/signal.d.ts +0 -16
- package/dist/signal.d.ts.map +0 -1
- package/dist/trigger.d.ts +0 -16
- package/dist/trigger.d.ts.map +0 -1
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"signal-base.d.ts","sourceRoot":"","sources":["../src/signal-base.ts"],"names":[],"mappings":"AAEA,OAAO,KAAK,EAAC,SAAS,EAAE,gBAAgB,EAAE,eAAe,EAAE,gBAAgB,EAAE,YAAY,EAAC,MAAM,WAAW,CAAC;AAC5G,OAAO,KAAK,EAAC,YAAY,EAAC,MAAM,gBAAgB,CAAC;AAKjD;;;;;;;;GAQG;AACH,8BAAsB,UAAU,CAAC,CAAC;IAChC;;OAEG;IACH,SAAgB,QAAQ,EAAE,MAAM,CAAC;IAEjC,SAAS,CAAC,QAAQ,CAAC,OAAO,EAAE,YAAY,CAAC;IAEzC;;;OAGG;IACH,SAAS,CAAC,QAAQ,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC,CAAC,EAAE,CAAM;IAEnD,SAAS,CAAC,YAAY,UAAS;IAE/B;;;;OAIG;IACH,IAAW,WAAW,IAAI,OAAO,CAEhC;IAED;;;OAGG;gBACgB,MAAM,EAAE,YAAY;IAIvC;;;;OAIG;IACH,SAAS,CAAC,eAAe,CAAC,QAAQ,EAAE,SAAS,CAAC,CAAC,CAAC,GAAG,IAAI;IAYvD;;;;;;;;OAQG;IACI,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe;IAoB5F;;;;;;;;OAQG;IACH,SAAS,CAAC,OAAO,CAAC,KAAK,EAAE,CAAC,GAAG,IAAI;IAkCjC;;;;;;;;;;;;OAYG;IACI,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC;IAY9B;;;;;;OAMG;IACI,OAAO,IAAI,IAAI;IAMtB;;;;OAIG;IACH,SAAS,CAAC,eAAe,QAAO,IAAI,CAKlC;CACH"}
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { SignalBase } from './signal-base.js';
|
|
2
|
+
import type { StateSignalConfig, ListenerCallback, SubscribeOptions, SubscribeResult, IReadonlySignal } from './type.js';
|
|
3
|
+
/**
|
|
4
|
+
* A stateful signal that holds a value and notifies listeners when the value changes.
|
|
5
|
+
*
|
|
6
|
+
* `StateSignal` is the core of the signal library, representing a piece of mutable state.
|
|
7
|
+
* It always has a value, and new subscribers immediately receive the current value by default.
|
|
8
|
+
*
|
|
9
|
+
* @template T The type of the state it holds.
|
|
10
|
+
* @implements {IReadonlySignal<T>}
|
|
11
|
+
*
|
|
12
|
+
* @example
|
|
13
|
+
* // Create a new state signal with an initial value.
|
|
14
|
+
* const counter = new StateSignal<number>({
|
|
15
|
+
* signalId: 'counter-signal',
|
|
16
|
+
* initialValue: 0,
|
|
17
|
+
* });
|
|
18
|
+
*
|
|
19
|
+
* // Get the current value.
|
|
20
|
+
* console.log(counter.value); // Outputs: 0
|
|
21
|
+
*
|
|
22
|
+
* // Subscribe to changes.
|
|
23
|
+
* const subscription = counter.subscribe(newValue => {
|
|
24
|
+
* console.log(`Counter changed to: ${newValue}`);
|
|
25
|
+
* });
|
|
26
|
+
*
|
|
27
|
+
* // Set a new value, which triggers the notification.
|
|
28
|
+
* counter.set(1); // Outputs: "Counter changed to: 1"
|
|
29
|
+
*
|
|
30
|
+
* // Unsubscribe when no longer needed.
|
|
31
|
+
* subscription.unsubscribe();
|
|
32
|
+
*/
|
|
33
|
+
export declare class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T> {
|
|
34
|
+
private value__;
|
|
35
|
+
protected logger_: import("@alwatr/logger").AlwatrLogger;
|
|
36
|
+
/**
|
|
37
|
+
* Initializes a new `StateSignal`.
|
|
38
|
+
* @param config The configuration for the state signal, including `signalId` and `initialValue`.
|
|
39
|
+
*/
|
|
40
|
+
constructor(config: StateSignalConfig<T>);
|
|
41
|
+
/**
|
|
42
|
+
* Retrieves the current value of the signal.
|
|
43
|
+
*
|
|
44
|
+
* @returns The current value.
|
|
45
|
+
*
|
|
46
|
+
* @example
|
|
47
|
+
* console.log(mySignal.value);
|
|
48
|
+
*/
|
|
49
|
+
get value(): T;
|
|
50
|
+
/**
|
|
51
|
+
* Updates the signal's value and notifies all active listeners.
|
|
52
|
+
*
|
|
53
|
+
* The notification is scheduled as a microtask, which means the update is deferred
|
|
54
|
+
* slightly to batch multiple synchronous changes.
|
|
55
|
+
*
|
|
56
|
+
* @param newValue The new value to set.
|
|
57
|
+
*
|
|
58
|
+
* @example
|
|
59
|
+
* // For primitive types
|
|
60
|
+
* mySignal.set(42);
|
|
61
|
+
*
|
|
62
|
+
* // For object types, it's best practice to set an immutable new object.
|
|
63
|
+
* mySignal.set({ ...mySignal.value, property: 'new-value' });
|
|
64
|
+
*/
|
|
65
|
+
set(newValue: T): void;
|
|
66
|
+
/**
|
|
67
|
+
* Subscribes a listener to this signal.
|
|
68
|
+
*
|
|
69
|
+
* By default, the listener is immediately called with the signal's current value (`receivePrevious: true`).
|
|
70
|
+
* This behavior can be customized via the `options` parameter.
|
|
71
|
+
*
|
|
72
|
+
* @param callback The function to be called when the signal's value changes.
|
|
73
|
+
* @param options Subscription options, including `receivePrevious` and `once`.
|
|
74
|
+
* @returns An object with an `unsubscribe` method to remove the listener.
|
|
75
|
+
*/
|
|
76
|
+
subscribe(callback: ListenerCallback<T>, options?: SubscribeOptions): SubscribeResult;
|
|
77
|
+
/**
|
|
78
|
+
* Destroys the signal, clearing its value and all listeners.
|
|
79
|
+
* This is crucial for memory management to prevent leaks.
|
|
80
|
+
*/
|
|
81
|
+
destroy(): void;
|
|
82
|
+
}
|
|
83
|
+
//# sourceMappingURL=state-signal.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"state-signal.d.ts","sourceRoot":"","sources":["../src/state-signal.ts"],"names":[],"mappings":"AAGA,OAAO,EAAC,UAAU,EAAC,MAAM,kBAAkB,CAAC;AAE5C,OAAO,KAAK,EAAC,iBAAiB,EAAE,gBAAgB,EAAE,gBAAgB,EAAE,eAAe,EAAE,eAAe,EAAC,MAAM,WAAW,CAAC;AAEvH;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GA6BG;AACH,qBAAa,WAAW,CAAC,CAAC,CAAE,SAAQ,UAAU,CAAC,CAAC,CAAE,YAAW,eAAe,CAAC,CAAC,CAAC;IAC7E,OAAO,CAAC,OAAO,CAAI;IACnB,SAAS,CAAC,OAAO,wCAAkD;IAEnE;;;OAGG;gBACgB,MAAM,EAAE,iBAAiB,CAAC,CAAC,CAAC;IAM/C;;;;;;;OAOG;IACH,IAAW,KAAK,IAAI,CAAC,CAGpB;IAED;;;;;;;;;;;;;;OAcG;IACI,GAAG,CAAC,QAAQ,EAAE,CAAC,GAAG,IAAI;IAe7B;;;;;;;;;OASG;IACa,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,GAAE,gBAAqB,GAAG,eAAe;IA4BzG;;;OAGG;IACa,OAAO,IAAI,IAAI;CAKhC"}
|
package/dist/type.d.ts
ADDED
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @package @alwatr/signal
|
|
3
|
+
*
|
|
4
|
+
* The callback function signature for a signal listener. It's a function that receives a value of type `T`
|
|
5
|
+
* and returns `void` or `Promise<void>`.
|
|
6
|
+
*
|
|
7
|
+
* @template T The type of the value that the signal holds or dispatches.
|
|
8
|
+
*/
|
|
9
|
+
export type ListenerCallback<T> = (value: T) => Awaitable<void>;
|
|
10
|
+
/**
|
|
11
|
+
* Options for fine-tuning the behavior of a subscription to a signal.
|
|
12
|
+
*/
|
|
13
|
+
export interface SubscribeOptions {
|
|
14
|
+
/**
|
|
15
|
+
* If `true`, the listener will be called only once and then automatically unsubscribed.
|
|
16
|
+
* This is useful for scenarios where you only need to react to the next change.
|
|
17
|
+
*
|
|
18
|
+
* @default false
|
|
19
|
+
*
|
|
20
|
+
* @example
|
|
21
|
+
* // The listener will be removed after the first click.
|
|
22
|
+
* onUserClick.subscribe(() => console.log('User clicked!'), { once: true });
|
|
23
|
+
*/
|
|
24
|
+
once?: boolean;
|
|
25
|
+
/**
|
|
26
|
+
* If `true`, the listener will be placed at the beginning of the notification queue and will be called before
|
|
27
|
+
* other, non-priority listeners.
|
|
28
|
+
*
|
|
29
|
+
* @default false
|
|
30
|
+
*
|
|
31
|
+
* @example
|
|
32
|
+
* // This listener will run before the others.
|
|
33
|
+
* mySignal.subscribe(() => console.log('High-priority action'), { priority: true });
|
|
34
|
+
*/
|
|
35
|
+
priority?: boolean;
|
|
36
|
+
/**
|
|
37
|
+
* **For `StateSignal` only.** If `true` (the default), the listener will be called immediately with the
|
|
38
|
+
* signal's current value upon subscription. Set to `false` if you only want to be notified of *future* changes.
|
|
39
|
+
*
|
|
40
|
+
* @default true
|
|
41
|
+
*
|
|
42
|
+
* @example
|
|
43
|
+
* const counter = new StateSignal({initialValue: 10});
|
|
44
|
+
*
|
|
45
|
+
* // This will log "Current value: 10" immediately.
|
|
46
|
+
* counter.subscribe(value => console.log(`Current value: ${value}`));
|
|
47
|
+
*
|
|
48
|
+
* // This will *not* log immediately, only when the counter is next updated.
|
|
49
|
+
* counter.subscribe(value => console.log(`New value: ${value}`), { receivePrevious: false });
|
|
50
|
+
*/
|
|
51
|
+
receivePrevious?: boolean;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* The object returned from a `subscribe` call, which contains the `unsubscribe` method.
|
|
55
|
+
* This allows for easy removal of the subscription when it's no longer needed.
|
|
56
|
+
*/
|
|
57
|
+
export interface SubscribeResult {
|
|
58
|
+
/**
|
|
59
|
+
* A function that, when called, removes the listener from the signal, preventing future notifications.
|
|
60
|
+
* It's crucial to call this to avoid memory leaks when a component or listener is destroyed.
|
|
61
|
+
*
|
|
62
|
+
* @example
|
|
63
|
+
* const subscription = mySignal.subscribe(value => console.log(value));
|
|
64
|
+
* // ... later ...
|
|
65
|
+
* subscription.unsubscribe(); // The listener is now removed.
|
|
66
|
+
*/
|
|
67
|
+
unsubscribe: () => void;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Internal representation of an observer, containing the listener's callback and its subscription options.
|
|
71
|
+
* @internal
|
|
72
|
+
*/
|
|
73
|
+
export interface Observer_<T> {
|
|
74
|
+
callback: ListenerCallback<T>;
|
|
75
|
+
options?: SubscribeOptions;
|
|
76
|
+
}
|
|
77
|
+
/**
|
|
78
|
+
* Basic configuration for creating any signal.
|
|
79
|
+
*/
|
|
80
|
+
export interface SignalConfig {
|
|
81
|
+
/**
|
|
82
|
+
* A unique identifier for the signal. This is crucial for debugging, logging, and differentiating signals,
|
|
83
|
+
* especially in large applications.
|
|
84
|
+
*
|
|
85
|
+
* @example
|
|
86
|
+
* 'user-profile-signal'
|
|
87
|
+
* 'app-theme-signal'
|
|
88
|
+
*/
|
|
89
|
+
readonly signalId: string;
|
|
90
|
+
}
|
|
91
|
+
/**
|
|
92
|
+
* Configuration specifically for creating a `StateSignal`.
|
|
93
|
+
* @template T The type of the state held by the signal.
|
|
94
|
+
*/
|
|
95
|
+
export interface StateSignalConfig<T> extends SignalConfig {
|
|
96
|
+
/**
|
|
97
|
+
* The initial value of the `StateSignal`. A `StateSignal` must always have a value.
|
|
98
|
+
*/
|
|
99
|
+
readonly initialValue: T;
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Represents a signal that can be read from but not written to.
|
|
103
|
+
* Both `StateSignal` and `ComputedSignal` implement this interface, allowing them to be used
|
|
104
|
+
* as dependencies in other signals without exposing their `set` or `dispatch` methods.
|
|
105
|
+
*
|
|
106
|
+
* @template T The type of the signal's value.
|
|
107
|
+
*/
|
|
108
|
+
export interface IReadonlySignal<T> {
|
|
109
|
+
/**
|
|
110
|
+
* The current value of the signal.
|
|
111
|
+
*/
|
|
112
|
+
readonly value: T;
|
|
113
|
+
/**
|
|
114
|
+
* Indicates whether the signal has been destroyed.
|
|
115
|
+
* A destroyed signal cannot be used and will throw an error if interacted with.
|
|
116
|
+
* @returns `true` if the signal is destroyed, `false` otherwise.
|
|
117
|
+
*/
|
|
118
|
+
readonly isDestroyed: boolean;
|
|
119
|
+
/**
|
|
120
|
+
* Subscribes a listener to this signal.
|
|
121
|
+
*
|
|
122
|
+
* @param callback The function to be called when the signal's value changes.
|
|
123
|
+
* @param options Optional settings for the subscription.
|
|
124
|
+
* @returns An object with an `unsubscribe` method for cleanup.
|
|
125
|
+
*/
|
|
126
|
+
subscribe(callback: ListenerCallback<T>, options?: SubscribeOptions): SubscribeResult;
|
|
127
|
+
/**
|
|
128
|
+
* Returns a Promise that resolves with the next value dispatched by the signal.
|
|
129
|
+
* This provides an elegant way to wait for a single, future event using `async/await`.
|
|
130
|
+
*
|
|
131
|
+
* @returns A Promise that resolves with the next dispatched value.
|
|
132
|
+
*
|
|
133
|
+
* @example
|
|
134
|
+
* async function onButtonClick() {
|
|
135
|
+
* console.log('Waiting for the next signal...');
|
|
136
|
+
* const nextValue = await mySignal.untilNext();
|
|
137
|
+
* console.log('Signal received:', nextValue);
|
|
138
|
+
* }
|
|
139
|
+
*/
|
|
140
|
+
untilNext(): Promise<T>;
|
|
141
|
+
/**
|
|
142
|
+
* Destroys the signal, clearing all its listeners and making it inactive.
|
|
143
|
+
*
|
|
144
|
+
* After destruction, any interaction with the signal (like `subscribe` or `untilNext`)
|
|
145
|
+
* will throw an error. This is crucial for preventing memory leaks by allowing
|
|
146
|
+
* garbage collection of the signal and its observers.
|
|
147
|
+
*/
|
|
148
|
+
destroy(): void;
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* A list of `IReadonlySignal` instances that a computed or effect signal depends on.
|
|
152
|
+
* This ensures that dependencies can be read from but not modified by the dependent signal.
|
|
153
|
+
*/
|
|
154
|
+
export type DependencyList = readonly IReadonlySignal<unknown>[];
|
|
155
|
+
/**
|
|
156
|
+
* Configuration for creating a `ComputedSignal`.
|
|
157
|
+
* @template T The type of the value computed by the signal.
|
|
158
|
+
*/
|
|
159
|
+
export interface ComputedSignalConfig<T> extends SignalConfig {
|
|
160
|
+
/**
|
|
161
|
+
* An array of dependency signals (`StateSignal` or other `ComputedSignal` instances).
|
|
162
|
+
* The `ComputedSignal` will automatically re-evaluate its value whenever any of these dependencies change.
|
|
163
|
+
*/
|
|
164
|
+
deps: DependencyList;
|
|
165
|
+
/**
|
|
166
|
+
* The function that computes the signal's value.
|
|
167
|
+
* It is executed once initially and then again whenever a dependency changes.
|
|
168
|
+
* This function should be pure and not have side effects.
|
|
169
|
+
*
|
|
170
|
+
* @example
|
|
171
|
+
* // A computed signal that derives a boolean from a number.
|
|
172
|
+
* const counter = new StateSignal({initialValue: 0});
|
|
173
|
+
* const isEven = new ComputedSignal({
|
|
174
|
+
* deps: [counter],
|
|
175
|
+
* get: () => counter.value % 2 === 0,
|
|
176
|
+
* });
|
|
177
|
+
*/
|
|
178
|
+
get: () => T;
|
|
179
|
+
}
|
|
180
|
+
/**
|
|
181
|
+
* The public interface for a `ComputedSignal`. It is a read-only signal
|
|
182
|
+
* that also includes a `destroy` method for essential lifecycle management.
|
|
183
|
+
*
|
|
184
|
+
* @template T The type of the computed value.
|
|
185
|
+
*/
|
|
186
|
+
export interface IComputedSignal<T> extends IReadonlySignal<T> {
|
|
187
|
+
/**
|
|
188
|
+
* Disconnects the `ComputedSignal` from its dependencies.
|
|
189
|
+
* This must be called to prevent memory leaks when the signal is no longer needed,
|
|
190
|
+
* as it stops the automatic re-evaluation.
|
|
191
|
+
*/
|
|
192
|
+
destroy: () => void;
|
|
193
|
+
}
|
|
194
|
+
/**
|
|
195
|
+
* Configuration for creating an `EffectSignal`.
|
|
196
|
+
*/
|
|
197
|
+
export interface EffectSignalConfig {
|
|
198
|
+
/**
|
|
199
|
+
* An array of dependency signals (`StateSignal` or `ComputedSignal` instances).
|
|
200
|
+
* The effect's `run` function will be executed whenever any of these signals change.
|
|
201
|
+
*/
|
|
202
|
+
readonly deps: DependencyList;
|
|
203
|
+
/**
|
|
204
|
+
* The function to execute as the side-effect (e.g., logging, DOM updates, network requests).
|
|
205
|
+
* It can be synchronous or asynchronous.
|
|
206
|
+
*
|
|
207
|
+
* @example
|
|
208
|
+
* // An effect that logs the counter's value to the console.
|
|
209
|
+
* const counter = new StateSignal({initialValue: 0});
|
|
210
|
+
* new EffectSignal({
|
|
211
|
+
* deps: [counter],
|
|
212
|
+
* run: () => console.log(`The counter is now: ${counter.value}`),
|
|
213
|
+
* });
|
|
214
|
+
*/
|
|
215
|
+
run: () => Awaitable<void>;
|
|
216
|
+
/**
|
|
217
|
+
* If `true`, the effect's `run` function will be executed once immediately upon initialization.
|
|
218
|
+
*
|
|
219
|
+
* @default false
|
|
220
|
+
*/
|
|
221
|
+
runImmediately?: boolean;
|
|
222
|
+
}
|
|
223
|
+
/**
|
|
224
|
+
* The public interface for an `EffectSignal`, which provides a `destroy` method for cleanup.
|
|
225
|
+
*/
|
|
226
|
+
export interface IEffectSignal {
|
|
227
|
+
/**
|
|
228
|
+
* Permanently disposes of the effect, unsubscribing from all dependencies
|
|
229
|
+
* and stopping any future executions. This is crucial for preventing memory leaks
|
|
230
|
+
* and unwanted side effects from running.
|
|
231
|
+
*/
|
|
232
|
+
destroy: () => void;
|
|
233
|
+
/**
|
|
234
|
+
* Indicates whether the signal has been destroyed.
|
|
235
|
+
* A destroyed signal cannot be used and will throw an error if interacted with.
|
|
236
|
+
* @returns `true` if the signal is destroyed, `false` otherwise.
|
|
237
|
+
*/
|
|
238
|
+
readonly isDestroyed: boolean;
|
|
239
|
+
}
|
|
240
|
+
//# sourceMappingURL=type.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAEA;;;;;;;GAOG;AACH,MAAM,MAAM,gBAAgB,CAAC,CAAC,IAAI,CAAC,KAAK,EAAE,CAAC,KAAK,SAAS,CAAC,IAAI,CAAC,CAAC;AAEhE;;GAEG;AACH,MAAM,WAAW,gBAAgB;IAC/B;;;;;;;;;OASG;IACH,IAAI,CAAC,EAAE,OAAO,CAAC;IAEf;;;;;;;;;OASG;IACH,QAAQ,CAAC,EAAE,OAAO,CAAC;IAEnB;;;;;;;;;;;;;;OAcG;IACH,eAAe,CAAC,EAAE,OAAO,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,eAAe;IAC9B;;;;;;;;OAQG;IACH,WAAW,EAAE,MAAM,IAAI,CAAC;CACzB;AAED;;;GAGG;AACH,MAAM,WAAW,SAAS,CAAC,CAAC;IAC1B,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,CAAC;IAC9B,OAAO,CAAC,EAAE,gBAAgB,CAAC;CAC5B;AAED;;GAEG;AACH,MAAM,WAAW,YAAY;IAC3B;;;;;;;OAOG;IACH,QAAQ,CAAC,QAAQ,EAAE,MAAM,CAAC;CAC3B;AAED;;;GAGG;AACH,MAAM,WAAW,iBAAiB,CAAC,CAAC,CAAE,SAAQ,YAAY;IACxD;;OAEG;IACH,QAAQ,CAAC,YAAY,EAAE,CAAC,CAAC;CAC1B;AAED;;;;;;GAMG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC;IAChC;;OAEG;IACH,QAAQ,CAAC,KAAK,EAAE,CAAC,CAAC;IAElB;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;IAE9B;;;;;;OAMG;IACH,SAAS,CAAC,QAAQ,EAAE,gBAAgB,CAAC,CAAC,CAAC,EAAE,OAAO,CAAC,EAAE,gBAAgB,GAAG,eAAe,CAAC;IAEtF;;;;;;;;;;;;OAYG;IACH,SAAS,IAAI,OAAO,CAAC,CAAC,CAAC,CAAC;IAExB;;;;;;OAMG;IACH,OAAO,IAAI,IAAI,CAAC;CACjB;AAED;;;GAGG;AACH,MAAM,MAAM,cAAc,GAAG,SAAS,eAAe,CAAC,OAAO,CAAC,EAAE,CAAC;AAEjE;;;GAGG;AACH,MAAM,WAAW,oBAAoB,CAAC,CAAC,CAAE,SAAQ,YAAY;IAC3D;;;OAGG;IACH,IAAI,EAAE,cAAc,CAAC;IAErB;;;;;;;;;;;;OAYG;IACH,GAAG,EAAE,MAAM,CAAC,CAAC;CACd;AAED;;;;;GAKG;AACH,MAAM,WAAW,eAAe,CAAC,CAAC,CAAE,SAAQ,eAAe,CAAC,CAAC,CAAC;IAC5D;;;;OAIG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;CACrB;AAED;;GAEG;AACH,MAAM,WAAW,kBAAkB;IACjC;;;OAGG;IACH,QAAQ,CAAC,IAAI,EAAE,cAAc,CAAC;IAE9B;;;;;;;;;;;OAWG;IACH,GAAG,EAAE,MAAM,SAAS,CAAC,IAAI,CAAC,CAAC;IAE3B;;;;OAIG;IACH,cAAc,CAAC,EAAE,OAAO,CAAC;CAC1B;AAED;;GAEG;AACH,MAAM,WAAW,aAAa;IAC5B;;;;OAIG;IACH,OAAO,EAAE,MAAM,IAAI,CAAC;IAEpB;;;;OAIG;IACH,QAAQ,CAAC,WAAW,EAAE,OAAO,CAAC;CAC/B"}
|
package/package.json
CHANGED
|
@@ -1,18 +1,20 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/signal",
|
|
3
|
-
"description": "
|
|
4
|
-
"version": "
|
|
3
|
+
"description": "Alwatr Signal is a powerful, lightweight, and modern reactive programming library. It is inspired by the best concepts from major reactive libraries but engineered to be faster and more efficient than all of them. It provides a robust and elegant way to manage application state through a system of signals, offering fine-grained reactivity, predictability, and excellent performance.",
|
|
4
|
+
"version": "5.0.0",
|
|
5
5
|
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com>",
|
|
6
6
|
"bugs": "https://github.com/Alwatr/flux/issues",
|
|
7
7
|
"dependencies": {
|
|
8
|
-
"@alwatr/
|
|
9
|
-
"@alwatr/
|
|
8
|
+
"@alwatr/delay": "^6.0.1",
|
|
9
|
+
"@alwatr/logger": "^5.5.10",
|
|
10
|
+
"@alwatr/package-tracer": "^5.5.10"
|
|
10
11
|
},
|
|
11
12
|
"devDependencies": {
|
|
12
|
-
"@alwatr/nano-build": "^6.1.
|
|
13
|
-
"@alwatr/prettier-config": "^5.0.
|
|
14
|
-
"@alwatr/tsconfig-base": "^6.0.
|
|
15
|
-
"@alwatr/type-helper": "^6.0.
|
|
13
|
+
"@alwatr/nano-build": "^6.1.1",
|
|
14
|
+
"@alwatr/prettier-config": "^5.0.3",
|
|
15
|
+
"@alwatr/tsconfig-base": "^6.0.1",
|
|
16
|
+
"@alwatr/type-helper": "^6.0.1",
|
|
17
|
+
"@jest/globals": "^30.1.2",
|
|
16
18
|
"@types/node": "^22.18.1",
|
|
17
19
|
"jest": "^30.1.3",
|
|
18
20
|
"typescript": "^5.9.2"
|
|
@@ -66,5 +68,5 @@
|
|
|
66
68
|
},
|
|
67
69
|
"type": "module",
|
|
68
70
|
"types": "./dist/main.d.ts",
|
|
69
|
-
"gitHead": "
|
|
71
|
+
"gitHead": "395a61b801b670ce58084a19f5ade96bb8338c5f"
|
|
70
72
|
}
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {describe, beforeEach, afterEach, it, expect, jest} from '@jest/globals';
|
|
2
|
+
import {ComputedSignal, StateSignal} from '@alwatr/signal';
|
|
3
|
+
import {delay} from '@alwatr/delay';
|
|
4
|
+
|
|
5
|
+
describe('ComputedSignal', () => {
|
|
6
|
+
/** @type {StateSignal<number>} */
|
|
7
|
+
let dep1;
|
|
8
|
+
/** @type {StateSignal<number>} */
|
|
9
|
+
let dep2;
|
|
10
|
+
/** @type {ComputedSignal<number>} */
|
|
11
|
+
let signal;
|
|
12
|
+
const signalId = 'test-computed-signal';
|
|
13
|
+
|
|
14
|
+
beforeEach(() => {
|
|
15
|
+
dep1 = new StateSignal({signalId: 'dep1', initialValue: 1});
|
|
16
|
+
dep2 = new StateSignal({signalId: 'dep2', initialValue: 2});
|
|
17
|
+
signal = new ComputedSignal({
|
|
18
|
+
signalId,
|
|
19
|
+
deps: [dep1, dep2],
|
|
20
|
+
get: () => dep1.value + dep2.value,
|
|
21
|
+
});
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
afterEach(() => {
|
|
25
|
+
signal.destroy();
|
|
26
|
+
dep1.destroy();
|
|
27
|
+
dep2.destroy();
|
|
28
|
+
});
|
|
29
|
+
|
|
30
|
+
it('should be defined and have the correct signalId and initial value', () => {
|
|
31
|
+
expect(ComputedSignal).toBeDefined();
|
|
32
|
+
expect(signal).toBeInstanceOf(ComputedSignal);
|
|
33
|
+
expect(signal.signalId).toBe(signalId);
|
|
34
|
+
expect(signal.value).toBe(3); // 1 + 2
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
it('should compute value from dependencies', async () => {
|
|
38
|
+
expect(signal.value).toBe(3);
|
|
39
|
+
dep1.set(5);
|
|
40
|
+
await signal.untilNext();
|
|
41
|
+
expect(signal.value).toBe(7); // 5 + 2
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
it('should notify subscribers when computed value changes', async () => {
|
|
45
|
+
const callback = jest.fn();
|
|
46
|
+
signal.subscribe(callback, {receivePrevious: false});
|
|
47
|
+
dep1.set(10);
|
|
48
|
+
await signal.untilNext();
|
|
49
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
50
|
+
expect(callback).toHaveBeenCalledWith(12); // 10 + 2
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should not notify if computed value does not change', async () => {
|
|
54
|
+
const callback = jest.fn();
|
|
55
|
+
signal.subscribe(callback, {receivePrevious: false});
|
|
56
|
+
dep1.set(1); // Same as initial
|
|
57
|
+
await delay.by(5);
|
|
58
|
+
expect(callback).not.toHaveBeenCalled();
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
it('should compute once if multiple dependencies change', async () => {
|
|
62
|
+
const callback = jest.fn();
|
|
63
|
+
signal.subscribe(callback, {receivePrevious: false});
|
|
64
|
+
dep1.set(3);
|
|
65
|
+
await delay.nextMicrotask();
|
|
66
|
+
dep2.set(4);
|
|
67
|
+
await signal.untilNext();
|
|
68
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
69
|
+
expect(callback).toHaveBeenCalledWith(7); // 3 + 4
|
|
70
|
+
});
|
|
71
|
+
|
|
72
|
+
it('should notify multiple subscribers', async () => {
|
|
73
|
+
const callback1 = jest.fn();
|
|
74
|
+
const callback2 = jest.fn();
|
|
75
|
+
signal.subscribe(callback1, {receivePrevious: false});
|
|
76
|
+
signal.subscribe(callback2, {receivePrevious: false});
|
|
77
|
+
dep2.set(5);
|
|
78
|
+
await signal.untilNext();
|
|
79
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
80
|
+
expect(callback1).toHaveBeenCalledWith(6); // 1 + 5
|
|
81
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
82
|
+
expect(callback2).toHaveBeenCalledWith(6);
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
it('should not notify unsubscribed listeners', async () => {
|
|
86
|
+
const callback = jest.fn();
|
|
87
|
+
const subscription = signal.subscribe(callback, {receivePrevious: false});
|
|
88
|
+
subscription.unsubscribe();
|
|
89
|
+
dep1.set(10);
|
|
90
|
+
await signal.untilNext();
|
|
91
|
+
expect(callback).not.toHaveBeenCalled();
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
it('should handle subscriptions with the "once" option', async () => {
|
|
95
|
+
const callback = jest.fn();
|
|
96
|
+
signal.subscribe(callback, {once: true, receivePrevious: false});
|
|
97
|
+
dep1.set(10);
|
|
98
|
+
await signal.untilNext();
|
|
99
|
+
expect(callback).toHaveBeenCalledTimes(1);
|
|
100
|
+
expect(callback).toHaveBeenCalledWith(12);
|
|
101
|
+
dep2.set(20);
|
|
102
|
+
await signal.untilNext();
|
|
103
|
+
expect(callback).toHaveBeenCalledTimes(1); // Should not be called again
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should resolve untilNext() with the next computed value', async () => {
|
|
107
|
+
const untilNextPromise = signal.untilNext();
|
|
108
|
+
dep1.set(5);
|
|
109
|
+
await expect(untilNextPromise).resolves.toBe(7);
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle no dependencies', () => {
|
|
113
|
+
const noDepSignal = new ComputedSignal({
|
|
114
|
+
signalId: 'no-dep',
|
|
115
|
+
deps: [],
|
|
116
|
+
get: () => 42,
|
|
117
|
+
});
|
|
118
|
+
expect(noDepSignal.value).toBe(42);
|
|
119
|
+
noDepSignal.destroy();
|
|
120
|
+
});
|
|
121
|
+
|
|
122
|
+
it('should continue notifying other subscribers if one callback throws an error', async () => {
|
|
123
|
+
const callback1 = jest.fn(() => {
|
|
124
|
+
throw new Error('Test error');
|
|
125
|
+
});
|
|
126
|
+
const callback2 = jest.fn();
|
|
127
|
+
signal.subscribe(callback1, {receivePrevious: false});
|
|
128
|
+
signal.subscribe(callback2, {receivePrevious: false});
|
|
129
|
+
dep1.set(10);
|
|
130
|
+
await signal.untilNext();
|
|
131
|
+
expect(callback1).toHaveBeenCalledTimes(1);
|
|
132
|
+
expect(callback2).toHaveBeenCalledTimes(1);
|
|
133
|
+
});
|
|
134
|
+
|
|
135
|
+
describe('destroyed signal', () => {
|
|
136
|
+
it('should throw error when accessing value after destroy', () => {
|
|
137
|
+
signal.destroy();
|
|
138
|
+
expect(() => signal.value).toThrow();
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
it('should not notify after destroy', async () => {
|
|
142
|
+
const callback = jest.fn();
|
|
143
|
+
signal.subscribe(callback, {receivePrevious: false});
|
|
144
|
+
signal.destroy();
|
|
145
|
+
dep1.set(10);
|
|
146
|
+
await delay.by(5);
|
|
147
|
+
expect(callback).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
import {describe, beforeEach, afterEach, it, expect, jest} from '@jest/globals';
|
|
2
|
+
import {EffectSignal, StateSignal} from '@alwatr/signal';
|
|
3
|
+
import {delay} from '@alwatr/delay';
|
|
4
|
+
|
|
5
|
+
describe('EffectSignal', () => {
|
|
6
|
+
/** @type {StateSignal<number>} */
|
|
7
|
+
let depSignal;
|
|
8
|
+
/** @type {EffectSignal} */
|
|
9
|
+
let effectSignal;
|
|
10
|
+
|
|
11
|
+
beforeEach(() => {
|
|
12
|
+
depSignal = new StateSignal({signalId: 'dep', initialValue: 0});
|
|
13
|
+
});
|
|
14
|
+
|
|
15
|
+
afterEach(() => {
|
|
16
|
+
if (effectSignal && !effectSignal.isDestroyed) {
|
|
17
|
+
effectSignal.destroy();
|
|
18
|
+
}
|
|
19
|
+
depSignal.destroy();
|
|
20
|
+
});
|
|
21
|
+
|
|
22
|
+
it('should be defined', () => {
|
|
23
|
+
const runFn = jest.fn();
|
|
24
|
+
effectSignal = new EffectSignal({
|
|
25
|
+
deps: [depSignal],
|
|
26
|
+
run: runFn,
|
|
27
|
+
});
|
|
28
|
+
expect(EffectSignal).toBeDefined();
|
|
29
|
+
expect(effectSignal).toBeInstanceOf(EffectSignal);
|
|
30
|
+
});
|
|
31
|
+
|
|
32
|
+
it('should run the effect immediately if runImmediately is true', async () => {
|
|
33
|
+
const runFn = jest.fn();
|
|
34
|
+
effectSignal = new EffectSignal({
|
|
35
|
+
deps: [depSignal],
|
|
36
|
+
run: runFn,
|
|
37
|
+
runImmediately: true,
|
|
38
|
+
});
|
|
39
|
+
await delay.by(5);
|
|
40
|
+
expect(runFn).toHaveBeenCalledTimes(1);
|
|
41
|
+
});
|
|
42
|
+
|
|
43
|
+
it('should not run the effect immediately if runImmediately is false or undefined', async () => {
|
|
44
|
+
const runFn = jest.fn();
|
|
45
|
+
effectSignal = new EffectSignal({
|
|
46
|
+
deps: [depSignal],
|
|
47
|
+
run: runFn,
|
|
48
|
+
});
|
|
49
|
+
await delay.by(5);
|
|
50
|
+
expect(runFn).not.toHaveBeenCalled();
|
|
51
|
+
});
|
|
52
|
+
|
|
53
|
+
it('should run the effect when a dependency changes', async () => {
|
|
54
|
+
const runFn = jest.fn();
|
|
55
|
+
effectSignal = new EffectSignal({
|
|
56
|
+
deps: [depSignal],
|
|
57
|
+
run: runFn,
|
|
58
|
+
});
|
|
59
|
+
await delay.by(5);
|
|
60
|
+
expect(runFn).not.toHaveBeenCalled();
|
|
61
|
+
depSignal.set(1);
|
|
62
|
+
await delay.by(5);
|
|
63
|
+
expect(runFn).toHaveBeenCalledTimes(1);
|
|
64
|
+
});
|
|
65
|
+
|
|
66
|
+
it('should run the effect for each dependency change', async () => {
|
|
67
|
+
const runFn = jest.fn();
|
|
68
|
+
effectSignal = new EffectSignal({
|
|
69
|
+
deps: [depSignal],
|
|
70
|
+
run: runFn,
|
|
71
|
+
});
|
|
72
|
+
depSignal.set(1);
|
|
73
|
+
await delay.by(5);
|
|
74
|
+
|
|
75
|
+
depSignal.set(2);
|
|
76
|
+
await delay.by(5);
|
|
77
|
+
|
|
78
|
+
expect(runFn).toHaveBeenCalledTimes(2);
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
it('should handle multiple dependencies', async () => {
|
|
82
|
+
const depSignal2 = new StateSignal({signalId: 'dep2', initialValue: 'a'});
|
|
83
|
+
const runFn = jest.fn();
|
|
84
|
+
effectSignal = new EffectSignal({
|
|
85
|
+
deps: [depSignal, depSignal2],
|
|
86
|
+
run: runFn,
|
|
87
|
+
});
|
|
88
|
+
depSignal.set(1);
|
|
89
|
+
await delay.by(5);
|
|
90
|
+
|
|
91
|
+
expect(runFn).toHaveBeenCalledTimes(1);
|
|
92
|
+
depSignal2.set('b');
|
|
93
|
+
await delay.by(5);
|
|
94
|
+
|
|
95
|
+
expect(runFn).toHaveBeenCalledTimes(2);
|
|
96
|
+
depSignal2.destroy();
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
it('should not run the effect after destroy', async () => {
|
|
100
|
+
const runFn = jest.fn();
|
|
101
|
+
effectSignal = new EffectSignal({
|
|
102
|
+
deps: [depSignal],
|
|
103
|
+
run: runFn,
|
|
104
|
+
});
|
|
105
|
+
effectSignal.destroy();
|
|
106
|
+
depSignal.set(1);
|
|
107
|
+
await delay.by(5);
|
|
108
|
+
|
|
109
|
+
expect(runFn).not.toHaveBeenCalled();
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
it('should handle async run functions', async () => {
|
|
113
|
+
const runFn = jest.fn().mockResolvedValue(undefined);
|
|
114
|
+
effectSignal = new EffectSignal({
|
|
115
|
+
deps: [depSignal],
|
|
116
|
+
run: runFn,
|
|
117
|
+
});
|
|
118
|
+
depSignal.set(1);
|
|
119
|
+
await delay.by(5);
|
|
120
|
+
|
|
121
|
+
expect(runFn).toHaveBeenCalledTimes(1);
|
|
122
|
+
});
|
|
123
|
+
|
|
124
|
+
it('should not run if dependencies do not change', async () => {
|
|
125
|
+
const runFn = jest.fn();
|
|
126
|
+
effectSignal = new EffectSignal({
|
|
127
|
+
deps: [depSignal],
|
|
128
|
+
run: runFn,
|
|
129
|
+
});
|
|
130
|
+
depSignal.set(0); // Same value
|
|
131
|
+
await delay.by(5);
|
|
132
|
+
|
|
133
|
+
expect(runFn).not.toHaveBeenCalled();
|
|
134
|
+
});
|
|
135
|
+
|
|
136
|
+
describe('destroyed signal', () => {
|
|
137
|
+
it('should not run effect after destroy', async () => {
|
|
138
|
+
const runFn = jest.fn();
|
|
139
|
+
effectSignal = new EffectSignal({
|
|
140
|
+
deps: [depSignal],
|
|
141
|
+
run: runFn,
|
|
142
|
+
});
|
|
143
|
+
effectSignal.destroy();
|
|
144
|
+
depSignal.set(1);
|
|
145
|
+
await delay.by(5);
|
|
146
|
+
|
|
147
|
+
expect(runFn).not.toHaveBeenCalled();
|
|
148
|
+
});
|
|
149
|
+
});
|
|
150
|
+
});
|