@alwatr/signal 9.26.0 → 9.29.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/README.md +197 -603
- package/dist/core/channel-signal.d.ts +12 -53
- package/dist/core/channel-signal.d.ts.map +1 -1
- package/dist/core/computed-signal.d.ts +19 -33
- package/dist/core/computed-signal.d.ts.map +1 -1
- package/dist/core/derived-signal.d.ts +71 -0
- package/dist/core/derived-signal.d.ts.map +1 -0
- package/dist/core/effect-signal.d.ts +15 -1
- package/dist/core/effect-signal.d.ts.map +1 -1
- package/dist/core/event-signal.d.ts +11 -4
- package/dist/core/event-signal.d.ts.map +1 -1
- package/dist/core/persistent-state-signal.d.ts +21 -2
- package/dist/core/persistent-state-signal.d.ts.map +1 -1
- package/dist/core/session-state-signal.d.ts +19 -2
- package/dist/core/session-state-signal.d.ts.map +1 -1
- package/dist/core/signal-base.d.ts +58 -38
- package/dist/core/signal-base.d.ts.map +1 -1
- package/dist/core/state-signal.d.ts +33 -14
- package/dist/core/state-signal.d.ts.map +1 -1
- package/dist/creators/channel.d.ts +1 -1
- package/dist/creators/channel.d.ts.map +1 -1
- package/dist/creators/derived.d.ts +31 -0
- package/dist/creators/derived.d.ts.map +1 -0
- package/dist/main.d.ts +2 -1
- package/dist/main.d.ts.map +1 -1
- package/dist/main.js +3 -3
- package/dist/main.js.map +16 -15
- package/dist/operators/debounce.d.ts +2 -3
- package/dist/operators/debounce.d.ts.map +1 -1
- package/dist/operators/filter.d.ts +14 -13
- package/dist/operators/filter.d.ts.map +1 -1
- package/dist/type.d.ts +68 -3
- package/dist/type.d.ts.map +1 -1
- package/package.json +6 -6
- package/src/core/channel-signal.ts +25 -68
- package/src/core/computed-signal.ts +50 -74
- package/src/core/derived-signal.ts +166 -0
- package/src/core/effect-signal.ts +23 -11
- package/src/core/event-signal.ts +14 -9
- package/src/core/persistent-state-signal.ts +21 -4
- package/src/core/session-state-signal.ts +19 -4
- package/src/core/signal-base.ts +98 -61
- package/src/core/state-signal.ts +48 -29
- package/src/creators/channel.ts +1 -2
- package/src/creators/derived.ts +34 -0
- package/src/main.ts +2 -1
- package/src/operators/debounce.ts +13 -23
- package/src/operators/filter.ts +20 -26
- package/src/type.ts +71 -3
- package/dist/operators/map.d.ts +0 -36
- package/dist/operators/map.d.ts.map +0 -1
- package/src/operators/map.ts +0 -48
package/src/core/signal-base.ts
CHANGED
|
@@ -2,40 +2,55 @@ import type {Observer_, SubscribeOptions, SubscribeResult, ListenerCallback, Sig
|
|
|
2
2
|
import type {AlwatrLogger} from '@alwatr/logger';
|
|
3
3
|
|
|
4
4
|
/**
|
|
5
|
-
* An abstract base class for signal implementations.
|
|
6
|
-
* It provides the core functionality for managing subscriptions (observers).
|
|
5
|
+
* An abstract base class for all signal implementations in the `@alwatr/signal` package.
|
|
7
6
|
*
|
|
8
|
-
*
|
|
7
|
+
* It provides core subscription management capabilities, including priority observer queues,
|
|
8
|
+
* microtask/macrotask-friendly updates, type-safe unsubscribes, async promise resolution via `untilNext`,
|
|
9
|
+
* and safe destruction lifecycles to prevent memory leaks.
|
|
10
|
+
*
|
|
11
|
+
* @template T The type of data that the signal holds, dispatches, or streams.
|
|
9
12
|
*/
|
|
10
13
|
export abstract class SignalBase<T> {
|
|
11
14
|
/**
|
|
12
15
|
* The unique identifier for this signal instance.
|
|
13
|
-
*
|
|
16
|
+
* Highly useful for debugging, filtering logs, and tracing data flows.
|
|
14
17
|
*/
|
|
15
18
|
public readonly name: string;
|
|
16
19
|
|
|
17
20
|
/**
|
|
18
21
|
* The logger instance for this signal.
|
|
22
|
+
* Custom scoped logger based on the signal name and type.
|
|
23
|
+
*
|
|
19
24
|
* @protected
|
|
20
25
|
*/
|
|
21
26
|
protected abstract logger_: AlwatrLogger;
|
|
22
27
|
|
|
23
28
|
/**
|
|
24
|
-
*
|
|
29
|
+
* High-priority observers that are executed first during notifications.
|
|
30
|
+
* Allocated lazily upon the first priority subscription to guard heap memory.
|
|
31
|
+
*
|
|
32
|
+
* @protected
|
|
33
|
+
*/
|
|
34
|
+
protected priorityObservers_?: Set<Observer_<T>>;
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Standard-priority observers executed after priority observers.
|
|
38
|
+
* Allocated lazily upon the first standard subscription to guard heap memory.
|
|
39
|
+
*
|
|
25
40
|
* @protected
|
|
26
41
|
*/
|
|
27
|
-
protected
|
|
28
|
-
protected readonly observers_ = new Set<Observer_<T>>();
|
|
42
|
+
protected observers_?: Set<Observer_<T>>;
|
|
29
43
|
|
|
30
44
|
/**
|
|
31
|
-
*
|
|
45
|
+
* Internal flag representing whether the signal has been destroyed.
|
|
46
|
+
*
|
|
32
47
|
* @private
|
|
33
48
|
*/
|
|
34
49
|
private isDestroyed__ = false;
|
|
35
50
|
|
|
36
51
|
/**
|
|
37
52
|
* Indicates whether the signal has been destroyed.
|
|
38
|
-
* A destroyed signal cannot be
|
|
53
|
+
* A destroyed signal cannot be subscribed to or dispatched to.
|
|
39
54
|
*
|
|
40
55
|
* @returns `true` if the signal is destroyed, `false` otherwise.
|
|
41
56
|
*/
|
|
@@ -43,14 +58,19 @@ export abstract class SignalBase<T> {
|
|
|
43
58
|
return this.isDestroyed__;
|
|
44
59
|
}
|
|
45
60
|
|
|
61
|
+
/**
|
|
62
|
+
* Creates a new instance of the signal base.
|
|
63
|
+
*
|
|
64
|
+
* @param config_ Configuration options including the unique signal name and cleanup hooks.
|
|
65
|
+
*/
|
|
46
66
|
constructor(protected config_: SignalConfig) {
|
|
47
67
|
this.name = config_.name;
|
|
48
68
|
}
|
|
49
69
|
|
|
50
70
|
/**
|
|
51
|
-
* Removes a specific observer from the
|
|
71
|
+
* Removes a specific observer from both the standard and priority observer queues.
|
|
52
72
|
*
|
|
53
|
-
* @param observer The observer
|
|
73
|
+
* @param observer The observer wrapper object containing the callback and options to remove.
|
|
54
74
|
* @protected
|
|
55
75
|
*/
|
|
56
76
|
protected removeObserver_(observer: Observer_<T>): void {
|
|
@@ -61,18 +81,27 @@ export abstract class SignalBase<T> {
|
|
|
61
81
|
return;
|
|
62
82
|
}
|
|
63
83
|
|
|
64
|
-
|
|
65
|
-
|
|
84
|
+
if (observer.options?.priority) {
|
|
85
|
+
this.priorityObservers_?.delete(observer);
|
|
86
|
+
if (this.priorityObservers_?.size === 0) {
|
|
87
|
+
this.priorityObservers_ = undefined;
|
|
88
|
+
}
|
|
89
|
+
} else {
|
|
90
|
+
this.observers_?.delete(observer);
|
|
91
|
+
if (this.observers_?.size === 0) {
|
|
92
|
+
this.observers_ = undefined;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
66
95
|
}
|
|
67
96
|
|
|
68
97
|
/**
|
|
69
98
|
* Subscribes a listener function to this signal.
|
|
70
99
|
*
|
|
71
|
-
* The listener will be called whenever the signal
|
|
100
|
+
* The listener will be called whenever the signal notifies its observers.
|
|
72
101
|
*
|
|
73
|
-
* @param callback The function to
|
|
74
|
-
* @param options
|
|
75
|
-
* @returns
|
|
102
|
+
* @param callback The function to invoke when the signal dispatches a new value.
|
|
103
|
+
* @param options Custom options to control priority, immediate callback, or single execution.
|
|
104
|
+
* @returns An object with an `unsubscribe` method to remove the subscription.
|
|
76
105
|
*/
|
|
77
106
|
public subscribe(callback: ListenerCallback<T>, options?: SubscribeOptions): SubscribeResult {
|
|
78
107
|
this.logger_.logMethodArgs?.('subscribe.base', options);
|
|
@@ -81,24 +110,24 @@ export abstract class SignalBase<T> {
|
|
|
81
110
|
const observer: Observer_<T> = {callback, options};
|
|
82
111
|
|
|
83
112
|
if (options?.priority) {
|
|
113
|
+
this.priorityObservers_ ??= new Set();
|
|
84
114
|
this.priorityObservers_.add(observer);
|
|
85
115
|
} else {
|
|
116
|
+
this.observers_ ??= new Set();
|
|
86
117
|
this.observers_.add(observer);
|
|
87
118
|
}
|
|
88
119
|
|
|
89
|
-
//
|
|
120
|
+
// Return an unsubscribe handler as a closure to prevent memory leaks.
|
|
90
121
|
return {
|
|
91
122
|
unsubscribe: (): void => this.removeObserver_(observer),
|
|
92
123
|
};
|
|
93
124
|
}
|
|
94
125
|
|
|
95
126
|
/**
|
|
96
|
-
* Notifies all registered observers
|
|
97
|
-
*
|
|
98
|
-
* This method iterates through a snapshot of the current observers to prevent issues
|
|
99
|
-
* with subscriptions changing during notification (e.g., an observer unsubscribing itself).
|
|
127
|
+
* Notifies all registered priority and standard observers with the given value.
|
|
128
|
+
* Iterates synchronously over current queues.
|
|
100
129
|
*
|
|
101
|
-
* @param value The
|
|
130
|
+
* @param value The value to pass to each observer's callback.
|
|
102
131
|
* @protected
|
|
103
132
|
*/
|
|
104
133
|
protected notify_(value: T): void {
|
|
@@ -109,72 +138,78 @@ export abstract class SignalBase<T> {
|
|
|
109
138
|
return;
|
|
110
139
|
}
|
|
111
140
|
|
|
112
|
-
|
|
113
|
-
|
|
141
|
+
// Execute priority observers first
|
|
142
|
+
if (this.priorityObservers_?.size) {
|
|
143
|
+
for (const observer of this.priorityObservers_) {
|
|
144
|
+
this.executeObserver__(observer, value);
|
|
145
|
+
}
|
|
114
146
|
}
|
|
115
147
|
|
|
116
|
-
|
|
117
|
-
|
|
148
|
+
// Execute standard observers second
|
|
149
|
+
if (this.observers_?.size) {
|
|
150
|
+
for (const observer of this.observers_) {
|
|
151
|
+
this.executeObserver__(observer, value);
|
|
152
|
+
}
|
|
118
153
|
}
|
|
119
154
|
}
|
|
120
155
|
|
|
121
156
|
/**
|
|
122
|
-
* Executes a
|
|
157
|
+
* Executes a single observer's callback, handles auto-unsubscribing for `once` listeners,
|
|
158
|
+
* and wraps execution in a try-catch block to prevent observer exceptions from crashing the signal.
|
|
159
|
+
*
|
|
160
|
+
* @param observer The observer descriptor to execute.
|
|
161
|
+
* @param value The value to supply to the observer's callback.
|
|
162
|
+
* @private
|
|
123
163
|
*/
|
|
124
164
|
private executeObserver__(observer: Observer_<T>, value: T): void {
|
|
125
165
|
if (observer.options?.once) {
|
|
126
166
|
this.removeObserver_(observer);
|
|
127
167
|
}
|
|
128
168
|
try {
|
|
129
|
-
|
|
130
|
-
if (result instanceof Promise) {
|
|
131
|
-
result.catch((err) => this.logger_.error('notify_', 'async_callback_failed', err, {observer}));
|
|
132
|
-
}
|
|
169
|
+
observer.callback(value);
|
|
133
170
|
} catch (err) {
|
|
134
171
|
this.logger_.error('notify_', 'sync_callback_failed', err);
|
|
135
172
|
}
|
|
136
173
|
}
|
|
137
174
|
|
|
138
|
-
private pendingRejects__ = new Set<(reason?: any) => void>();
|
|
139
|
-
|
|
140
175
|
/**
|
|
141
|
-
*
|
|
142
|
-
*
|
|
176
|
+
* Holds the promise rejection functions of any pending `untilNext` invocations
|
|
177
|
+
* to reject them if the signal is destroyed.
|
|
143
178
|
*
|
|
144
|
-
* @
|
|
179
|
+
* @private
|
|
180
|
+
*/
|
|
181
|
+
private pendingRejects__?: Set<(reason?: any) => void>;
|
|
182
|
+
|
|
183
|
+
/**
|
|
184
|
+
* Returns a Promise that resolves with the next value/payload dispatched by the signal.
|
|
185
|
+
* Use this for async orchestration (e.g. `await signal.untilNext()`).
|
|
145
186
|
*
|
|
146
|
-
* @
|
|
147
|
-
* async function onButtonClick() {
|
|
148
|
-
* console.log('Waiting for the next signal...');
|
|
149
|
-
* const nextValue = await mySignal.untilNext();
|
|
150
|
-
* console.log('Signal received:', nextValue);
|
|
151
|
-
* }
|
|
187
|
+
* @returns A Promise that resolves with the next value dispatched by the signal.
|
|
152
188
|
*/
|
|
153
189
|
public untilNext(): Promise<T> {
|
|
154
190
|
this.logger_.logMethod?.('untilNext');
|
|
155
191
|
this.checkDestroyed_();
|
|
156
192
|
return new Promise((resolve, reject) => {
|
|
193
|
+
this.pendingRejects__ ??= new Set();
|
|
157
194
|
this.pendingRejects__.add(reject);
|
|
158
195
|
this.subscribe(
|
|
159
196
|
(value) => {
|
|
160
|
-
this.pendingRejects__
|
|
197
|
+
this.pendingRejects__?.delete(reject);
|
|
161
198
|
resolve(value);
|
|
162
199
|
},
|
|
163
200
|
{
|
|
164
201
|
once: true,
|
|
165
|
-
priority: true, //
|
|
166
|
-
receivePrevious: false, //
|
|
202
|
+
priority: true, // Internal promise resolution is prioritized over normal observers.
|
|
203
|
+
receivePrevious: false, // Wait only for the next value change.
|
|
167
204
|
},
|
|
168
205
|
);
|
|
169
206
|
});
|
|
170
207
|
}
|
|
171
208
|
|
|
172
209
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
176
|
-
* will throw an error. This is crucial for preventing memory leaks by allowing
|
|
177
|
-
* garbage collection of the signal and its observers.
|
|
210
|
+
* Permanently destroys the signal instance.
|
|
211
|
+
* Clears all observers, rejects pending `untilNext` promises with a 'signal_destroyed' error,
|
|
212
|
+
* invokes the optional `onDestroy` config hook, and breaks internal references to facilitate GC.
|
|
178
213
|
*/
|
|
179
214
|
public destroy(): void {
|
|
180
215
|
this.logger_.logMethod?.('destroy');
|
|
@@ -182,25 +217,27 @@ export abstract class SignalBase<T> {
|
|
|
182
217
|
this.logger_.incident?.('destroy_', 'double_destroy_attempt');
|
|
183
218
|
return;
|
|
184
219
|
}
|
|
185
|
-
this.isDestroyed__ = true;
|
|
186
|
-
|
|
187
|
-
|
|
220
|
+
this.isDestroyed__ = true;
|
|
221
|
+
|
|
222
|
+
// Reject all pending promises to prevent hang-ups.
|
|
223
|
+
if (this.pendingRejects__?.size) {
|
|
188
224
|
const error = new Error('signal_destroyed');
|
|
189
225
|
for (const reject of this.pendingRejects__) {
|
|
190
226
|
reject(error);
|
|
191
227
|
}
|
|
192
|
-
this.pendingRejects__.clear();
|
|
228
|
+
this.pendingRejects__.clear();
|
|
193
229
|
}
|
|
194
|
-
this.priorityObservers_
|
|
195
|
-
this.observers_
|
|
196
|
-
this.config_.onDestroy?.();
|
|
197
|
-
this.config_ = null as unknown as SignalConfig;
|
|
230
|
+
this.priorityObservers_?.clear();
|
|
231
|
+
this.observers_?.clear();
|
|
232
|
+
this.config_.onDestroy?.();
|
|
233
|
+
this.config_ = null as unknown as SignalConfig;
|
|
198
234
|
}
|
|
199
235
|
|
|
200
236
|
/**
|
|
201
|
-
*
|
|
202
|
-
*
|
|
237
|
+
* Checks if the signal has been destroyed. If so, throws an error and logs an accident.
|
|
238
|
+
*
|
|
203
239
|
* @protected
|
|
240
|
+
* @throws {Error} If the signal has been destroyed.
|
|
204
241
|
*/
|
|
205
242
|
protected checkDestroyed_(): void {
|
|
206
243
|
if (this.isDestroyed__) {
|
package/src/core/state-signal.ts
CHANGED
|
@@ -1,8 +1,6 @@
|
|
|
1
|
-
import {
|
|
1
|
+
import {queueMicrotask} from '@alwatr/delay';
|
|
2
2
|
import {createLogger, type AlwatrLogger} from '@alwatr/logger';
|
|
3
|
-
|
|
4
3
|
import {SignalBase} from './signal-base.js';
|
|
5
|
-
|
|
6
4
|
import type {StateSignalConfig, ListenerCallback, SubscribeOptions, SubscribeResult, IReadonlySignal} from '../type.js';
|
|
7
5
|
|
|
8
6
|
/**
|
|
@@ -41,34 +39,45 @@ import type {StateSignalConfig, ListenerCallback, SubscribeOptions, SubscribeRes
|
|
|
41
39
|
export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T> {
|
|
42
40
|
/**
|
|
43
41
|
* The current value of the signal.
|
|
42
|
+
*
|
|
44
43
|
* @private
|
|
45
44
|
*/
|
|
46
45
|
private value__: T;
|
|
47
46
|
|
|
48
47
|
/**
|
|
49
48
|
* The logger instance for this signal.
|
|
49
|
+
*
|
|
50
50
|
* @protected
|
|
51
51
|
*/
|
|
52
52
|
protected logger_: AlwatrLogger;
|
|
53
53
|
|
|
54
54
|
/**
|
|
55
55
|
* Indicates if a notification is already scheduled.
|
|
56
|
+
* Helps batch multiple synchronous `set` operations into a single microtask notification.
|
|
57
|
+
*
|
|
56
58
|
* @private
|
|
57
59
|
*/
|
|
58
60
|
private notifyPending__ = false;
|
|
59
61
|
|
|
60
62
|
/**
|
|
61
|
-
* The version of the last notification.
|
|
63
|
+
* The version of the last notification. Incrementing on every state update.
|
|
64
|
+
* Used to guard immediate subscriber execution when updates happen within the same tick.
|
|
65
|
+
*
|
|
62
66
|
* @private
|
|
63
67
|
*/
|
|
64
68
|
private notifyVersion__ = 0;
|
|
65
69
|
|
|
70
|
+
/**
|
|
71
|
+
* Creates a new StateSignal instance.
|
|
72
|
+
*
|
|
73
|
+
* @param config Configuration options including name, initialValue, and custom cleanup hooks.
|
|
74
|
+
*/
|
|
66
75
|
constructor(config: StateSignalConfig<T>) {
|
|
67
76
|
super({
|
|
68
77
|
name: config.name,
|
|
69
78
|
onDestroy: config.onDestroy,
|
|
70
79
|
});
|
|
71
|
-
this.logger_ = createLogger(`
|
|
80
|
+
this.logger_ = createLogger(`state_signal:${this.name}`);
|
|
72
81
|
this.value__ = config.initialValue;
|
|
73
82
|
this.logger_.logMethodArgs?.('constructor', {initialValue: this.value__});
|
|
74
83
|
}
|
|
@@ -77,6 +86,7 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
77
86
|
* Retrieves the current value of the signal.
|
|
78
87
|
*
|
|
79
88
|
* @returns The current value.
|
|
89
|
+
* @throws {Error} If the signal has been destroyed.
|
|
80
90
|
*
|
|
81
91
|
* @example
|
|
82
92
|
* console.log(mySignal.get());
|
|
@@ -87,10 +97,11 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
87
97
|
}
|
|
88
98
|
|
|
89
99
|
/**
|
|
90
|
-
* Updates the signal's value and
|
|
100
|
+
* Updates the signal's value and schedules notifications for all active listeners.
|
|
91
101
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
102
|
+
* Primitives are comparison-checked via `Object.is`. If unchanged, the update is ignored.
|
|
103
|
+
* The notification is scheduled as a microtask, allowing multiple synchronous updates
|
|
104
|
+
* to be batched and executed once.
|
|
94
105
|
*
|
|
95
106
|
* @param newValue The new value to set.
|
|
96
107
|
*
|
|
@@ -115,9 +126,10 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
115
126
|
}
|
|
116
127
|
|
|
117
128
|
/**
|
|
118
|
-
*
|
|
129
|
+
* Forcefully schedules a notification of the current value to all subscribers.
|
|
119
130
|
*
|
|
120
|
-
*
|
|
131
|
+
* Useful when mutating properties within object states directly without assigning a new reference.
|
|
132
|
+
* Notification is queued as a microtask for batching.
|
|
121
133
|
*/
|
|
122
134
|
public notifyChange(): void {
|
|
123
135
|
this.logger_.logMethod?.('notifyChange');
|
|
@@ -128,7 +140,7 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
128
140
|
this.notifyPending__ = true;
|
|
129
141
|
|
|
130
142
|
// Dispatch as a microtask to ensure consistent, non-blocking behavior.
|
|
131
|
-
|
|
143
|
+
queueMicrotask(() => {
|
|
132
144
|
this.notifyPending__ = false;
|
|
133
145
|
this.notify_(this.value__);
|
|
134
146
|
});
|
|
@@ -137,10 +149,9 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
137
149
|
/**
|
|
138
150
|
* Updates the signal's value based on its previous value.
|
|
139
151
|
*
|
|
140
|
-
*
|
|
141
|
-
* especially for objects or arrays, as it promotes an immutable update pattern.
|
|
152
|
+
* A functional update pattern that retrieves the current value, computes the next, and sets it.
|
|
142
153
|
*
|
|
143
|
-
* @param updater A function that receives the current value and returns the new value.
|
|
154
|
+
* @param updater A callback function that receives the current value and returns the new value.
|
|
144
155
|
*
|
|
145
156
|
* @example
|
|
146
157
|
* // For a counter
|
|
@@ -157,13 +168,13 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
157
168
|
}
|
|
158
169
|
|
|
159
170
|
/**
|
|
160
|
-
* Subscribes a listener to this signal.
|
|
171
|
+
* Subscribes a listener function to this state signal.
|
|
161
172
|
*
|
|
162
173
|
* By default, the listener is immediately called with the signal's current value (`receivePrevious: true`).
|
|
163
|
-
* This
|
|
174
|
+
* This immediate call is queued as a microtask to match the asynchronous flow of signals.
|
|
164
175
|
*
|
|
165
|
-
* @param callback The function to
|
|
166
|
-
* @param options
|
|
176
|
+
* @param callback The function to invoke when the state changes.
|
|
177
|
+
* @param options Custom options, such as `receivePrevious: false` to only listen to future updates.
|
|
167
178
|
* @returns An object with an `unsubscribe` method to remove the listener.
|
|
168
179
|
*/
|
|
169
180
|
public override subscribe(callback: ListenerCallback<T>, options: SubscribeOptions = {}): SubscribeResult {
|
|
@@ -176,30 +187,38 @@ export class StateSignal<T> extends SignalBase<T> implements IReadonlySignal<T>
|
|
|
176
187
|
if (this.notifyPending__) return result; // If a notification is already pending, the callback will be called with the latest value when the notification is processed.
|
|
177
188
|
|
|
178
189
|
const subscribeVersion = this.notifyVersion__;
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
.
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
190
|
+
|
|
191
|
+
queueMicrotask((): void => {
|
|
192
|
+
this.logger_.logStep?.('subscribe', 'immediate_callback');
|
|
193
|
+
if (this.notifyVersion__ !== subscribeVersion) return; // A notification occurred after subscribing, so skip the immediate callback.
|
|
194
|
+
if (options.once) {
|
|
195
|
+
result.unsubscribe();
|
|
196
|
+
}
|
|
197
|
+
try {
|
|
187
198
|
callback(this.value__);
|
|
188
|
-
})
|
|
189
|
-
|
|
199
|
+
} catch (err) {
|
|
200
|
+
this.logger_.error('subscribe', 'immediate_callback_failed', err);
|
|
201
|
+
}
|
|
202
|
+
});
|
|
190
203
|
|
|
191
204
|
return result;
|
|
192
205
|
}
|
|
193
206
|
|
|
194
207
|
/**
|
|
195
208
|
* Destroys the signal, clearing its value and all listeners.
|
|
196
|
-
*
|
|
209
|
+
* Breaks references for garbage collection.
|
|
197
210
|
*/
|
|
198
211
|
public override destroy(): void {
|
|
199
212
|
this.value__ = null as T; // Clear the value to allow for garbage collection.
|
|
200
213
|
super.destroy();
|
|
201
214
|
}
|
|
202
215
|
|
|
216
|
+
/**
|
|
217
|
+
* Returns this signal cast to the `IReadonlySignal<T>` interface.
|
|
218
|
+
* Limits access so external callers can only subscribe/read but not set/update state.
|
|
219
|
+
*
|
|
220
|
+
* @returns A readonly representation of this signal.
|
|
221
|
+
*/
|
|
203
222
|
public asReadonly(): IReadonlySignal<T> {
|
|
204
223
|
return this;
|
|
205
224
|
}
|
package/src/creators/channel.ts
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import {ChannelSignal} from '../core/channel-signal.js';
|
|
2
|
-
|
|
3
|
-
import type {ChannelSignalConfig} from '../core/channel-signal.js';
|
|
2
|
+
import type {ChannelSignalConfig} from '../type.js';
|
|
4
3
|
|
|
5
4
|
/**
|
|
6
5
|
* Creates a stateless multi-channel signal that acts as a typed message bus.
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
import {DerivedSignal} from '../core/derived-signal.js';
|
|
2
|
+
|
|
3
|
+
import type {DerivedSignalConfig} from '../type.js';
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Creates a read-only signal mapping exactly 1-to-1 over a single upstream source.
|
|
7
|
+
*
|
|
8
|
+
* `createDerivedSignal` is a utility function to instantiate `DerivedSignal` without using the `new` keyword.
|
|
9
|
+
* A derived signal wraps a source signal and maps its value to a new representation using a projector function.
|
|
10
|
+
* It uses the "Cold Awakening Lifecycle" pattern, which avoids subscribing to the source signal until the derived
|
|
11
|
+
* signal has at least one subscriber of its own, saving processing cycles.
|
|
12
|
+
*
|
|
13
|
+
* @template S The type of the source signal's state.
|
|
14
|
+
* @template T The type of the projected derived state.
|
|
15
|
+
*
|
|
16
|
+
* @param config Configuration options including name, source, and projector.
|
|
17
|
+
* @returns A new, readonly derived signal.
|
|
18
|
+
*
|
|
19
|
+
* @example
|
|
20
|
+
* ```typescript
|
|
21
|
+
* const countSignal = createStateSignal({ name: 'count', initialValue: 5 });
|
|
22
|
+
*
|
|
23
|
+
* const isEvenSignal = createDerivedSignal({
|
|
24
|
+
* name: 'is-even-signal',
|
|
25
|
+
* source: countSignal,
|
|
26
|
+
* projector: (count) => count % 2 === 0
|
|
27
|
+
* });
|
|
28
|
+
*
|
|
29
|
+
* console.log(isEvenSignal.get()); // false
|
|
30
|
+
* ```
|
|
31
|
+
*/
|
|
32
|
+
export function createDerivedSignal<S, T>(config: DerivedSignalConfig<S, T>): DerivedSignal<S, T> {
|
|
33
|
+
return new DerivedSignal(config);
|
|
34
|
+
}
|
package/src/main.ts
CHANGED
|
@@ -2,6 +2,7 @@ export * from './core/signal-base.js';
|
|
|
2
2
|
export * from './core/event-signal.js';
|
|
3
3
|
export * from './core/state-signal.js';
|
|
4
4
|
export * from './core/computed-signal.js';
|
|
5
|
+
export * from './core/derived-signal.js';
|
|
5
6
|
export * from './core/effect-signal.js';
|
|
6
7
|
export * from './core/persistent-state-signal.js';
|
|
7
8
|
export * from './core/session-state-signal.js';
|
|
@@ -10,6 +11,7 @@ export * from './core/channel-signal.js';
|
|
|
10
11
|
export * from './creators/event.js';
|
|
11
12
|
export * from './creators/state.js';
|
|
12
13
|
export * from './creators/computed.js';
|
|
14
|
+
export * from './creators/derived.js';
|
|
13
15
|
export * from './creators/effect.js';
|
|
14
16
|
export * from './creators/persistent-state.js';
|
|
15
17
|
export * from './creators/session-state.js';
|
|
@@ -17,6 +19,5 @@ export * from './creators/channel.js';
|
|
|
17
19
|
|
|
18
20
|
export * from './operators/debounce.js';
|
|
19
21
|
export * from './operators/filter.js';
|
|
20
|
-
export * from './operators/map.js';
|
|
21
22
|
|
|
22
23
|
export type * from './type.js';
|
|
@@ -1,9 +1,5 @@
|
|
|
1
1
|
import {createDebouncer} from '@alwatr/debounce';
|
|
2
|
-
|
|
3
2
|
import {StateSignal} from '../core/state-signal.js';
|
|
4
|
-
import {createComputedSignal} from '../creators/computed.js';
|
|
5
|
-
|
|
6
|
-
import type {ComputedSignal} from '../core/computed-signal.js';
|
|
7
3
|
import type {IReadonlySignal, DebounceSignalConfig} from '../type.js';
|
|
8
4
|
|
|
9
5
|
/**
|
|
@@ -20,7 +16,7 @@ import type {IReadonlySignal, DebounceSignalConfig} from '../type.js';
|
|
|
20
16
|
*
|
|
21
17
|
* @template T The type of the signal's value.
|
|
22
18
|
*
|
|
23
|
-
* @param {IReadonlySignal<T>}
|
|
19
|
+
* @param {IReadonlySignal<T>} source The original signal to debounce.
|
|
24
20
|
* It can be a `StateSignal`, `ComputedSignal`, or any signal implementing `IReadonlySignal`.
|
|
25
21
|
* @param {DebounceSignalConfig} config Configuration object for the debouncer,
|
|
26
22
|
* including `delay`, `leading`, and `trailing` options from `@alwatr/debounce`.
|
|
@@ -59,12 +55,18 @@ import type {IReadonlySignal, DebounceSignalConfig} from '../type.js';
|
|
|
59
55
|
* // debouncedSearch.destroy();
|
|
60
56
|
* ```
|
|
61
57
|
*/
|
|
62
|
-
export function createDebouncedSignal<T>(
|
|
63
|
-
const name = config.name ?? `${
|
|
58
|
+
export function createDebouncedSignal<T>(source: IReadonlySignal<T>, config: DebounceSignalConfig): IReadonlySignal<T> {
|
|
59
|
+
const name = config.name ?? `${source.name}_debounced`;
|
|
64
60
|
|
|
65
61
|
const internalSignal = new StateSignal<T>({
|
|
66
|
-
name
|
|
67
|
-
initialValue:
|
|
62
|
+
name,
|
|
63
|
+
initialValue: source.get(),
|
|
64
|
+
onDestroy() {
|
|
65
|
+
subscription.unsubscribe();
|
|
66
|
+
debouncer.cancel();
|
|
67
|
+
config.onDestroy?.();
|
|
68
|
+
config = null as unknown as DebounceSignalConfig;
|
|
69
|
+
},
|
|
68
70
|
});
|
|
69
71
|
|
|
70
72
|
const debouncer = createDebouncer({
|
|
@@ -73,19 +75,7 @@ export function createDebouncedSignal<T>(sourceSignal: IReadonlySignal<T>, confi
|
|
|
73
75
|
func: internalSignal.set,
|
|
74
76
|
});
|
|
75
77
|
|
|
76
|
-
const subscription =
|
|
78
|
+
const subscription = source.subscribe(debouncer.trigger, {receivePrevious: false});
|
|
77
79
|
|
|
78
|
-
return
|
|
79
|
-
name: name,
|
|
80
|
-
deps: [internalSignal],
|
|
81
|
-
get: () => internalSignal.get(),
|
|
82
|
-
onDestroy: () => {
|
|
83
|
-
if (internalSignal.isDestroyed) return;
|
|
84
|
-
subscription.unsubscribe();
|
|
85
|
-
debouncer.cancel();
|
|
86
|
-
internalSignal.destroy();
|
|
87
|
-
config.onDestroy?.();
|
|
88
|
-
config = null as unknown as DebounceSignalConfig;
|
|
89
|
-
},
|
|
90
|
-
});
|
|
80
|
+
return internalSignal;
|
|
91
81
|
}
|