@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.
Files changed (52) hide show
  1. package/README.md +197 -603
  2. package/dist/core/channel-signal.d.ts +12 -53
  3. package/dist/core/channel-signal.d.ts.map +1 -1
  4. package/dist/core/computed-signal.d.ts +19 -33
  5. package/dist/core/computed-signal.d.ts.map +1 -1
  6. package/dist/core/derived-signal.d.ts +71 -0
  7. package/dist/core/derived-signal.d.ts.map +1 -0
  8. package/dist/core/effect-signal.d.ts +15 -1
  9. package/dist/core/effect-signal.d.ts.map +1 -1
  10. package/dist/core/event-signal.d.ts +11 -4
  11. package/dist/core/event-signal.d.ts.map +1 -1
  12. package/dist/core/persistent-state-signal.d.ts +21 -2
  13. package/dist/core/persistent-state-signal.d.ts.map +1 -1
  14. package/dist/core/session-state-signal.d.ts +19 -2
  15. package/dist/core/session-state-signal.d.ts.map +1 -1
  16. package/dist/core/signal-base.d.ts +58 -38
  17. package/dist/core/signal-base.d.ts.map +1 -1
  18. package/dist/core/state-signal.d.ts +33 -14
  19. package/dist/core/state-signal.d.ts.map +1 -1
  20. package/dist/creators/channel.d.ts +1 -1
  21. package/dist/creators/channel.d.ts.map +1 -1
  22. package/dist/creators/derived.d.ts +31 -0
  23. package/dist/creators/derived.d.ts.map +1 -0
  24. package/dist/main.d.ts +2 -1
  25. package/dist/main.d.ts.map +1 -1
  26. package/dist/main.js +3 -3
  27. package/dist/main.js.map +16 -15
  28. package/dist/operators/debounce.d.ts +2 -3
  29. package/dist/operators/debounce.d.ts.map +1 -1
  30. package/dist/operators/filter.d.ts +14 -13
  31. package/dist/operators/filter.d.ts.map +1 -1
  32. package/dist/type.d.ts +68 -3
  33. package/dist/type.d.ts.map +1 -1
  34. package/package.json +6 -6
  35. package/src/core/channel-signal.ts +25 -68
  36. package/src/core/computed-signal.ts +50 -74
  37. package/src/core/derived-signal.ts +166 -0
  38. package/src/core/effect-signal.ts +23 -11
  39. package/src/core/event-signal.ts +14 -9
  40. package/src/core/persistent-state-signal.ts +21 -4
  41. package/src/core/session-state-signal.ts +19 -4
  42. package/src/core/signal-base.ts +98 -61
  43. package/src/core/state-signal.ts +48 -29
  44. package/src/creators/channel.ts +1 -2
  45. package/src/creators/derived.ts +34 -0
  46. package/src/main.ts +2 -1
  47. package/src/operators/debounce.ts +13 -23
  48. package/src/operators/filter.ts +20 -26
  49. package/src/type.ts +71 -3
  50. package/dist/operators/map.d.ts +0 -36
  51. package/dist/operators/map.d.ts.map +0 -1
  52. package/src/operators/map.ts +0 -48
@@ -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
- * @template T The type of data that the signal holds or dispatches.
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
- * Useful for debugging and logging.
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
- * The list of observers (listeners) subscribed to this signal.
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 readonly priorityObservers_ = new Set<Observer_<T>>();
28
- protected readonly observers_ = new Set<Observer_<T>>();
42
+ protected observers_?: Set<Observer_<T>>;
29
43
 
30
44
  /**
31
- * A flag indicating whether the signal has been destroyed.
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 used and will throw an error if interacted with.
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 observers list.
71
+ * Removes a specific observer from both the standard and priority observer queues.
52
72
  *
53
- * @param observer The observer instance to remove.
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
- this.priorityObservers_.delete(observer);
65
- this.observers_.delete(observer);
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 is notified (e.g., when `dispatch` or `set` is called).
100
+ * The listener will be called whenever the signal notifies its observers.
72
101
  *
73
- * @param callback The function to be called when the signal is dispatched.
74
- * @param options Subscription options to customize the behavior (e.g., `once`, `priority`).
75
- * @returns A `SubscribeResult` object with an `unsubscribe` method to remove the listener.
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
- // The returned unsubscribe function is a closure that calls the internal removal method.
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 about a new value.
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 new value to notify observers about.
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
- for (const observer of this.priorityObservers_) {
113
- this.executeObserver__(observer, value);
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
- for (const observer of this.observers_) {
117
- this.executeObserver__(observer, value);
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 given observer's callback with the provided value, handling both synchronous and asynchronous callbacks.
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
- const result = observer.callback(value);
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
- * Returns a Promise that resolves with the next value dispatched by the signal.
142
- * This provides an elegant way to wait for a single, future event using `async/await`.
176
+ * Holds the promise rejection functions of any pending `untilNext` invocations
177
+ * to reject them if the signal is destroyed.
143
178
  *
144
- * @returns A Promise that resolves with the next dispatched value.
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
- * @example
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__.delete(reject);
197
+ this.pendingRejects__?.delete(reject);
161
198
  resolve(value);
162
199
  },
163
200
  {
164
201
  once: true,
165
- priority: true, // Resolve the promise before other listeners are called.
166
- receivePrevious: false, // We only want the *next* value, not the current one.
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
- * Destroys the signal, clearing all its listeners and making it inactive.
174
- *
175
- * After destruction, any interaction with the signal (like `subscribe` or `untilNext`)
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; // Mark the signal as destroyed.
186
- // Reject all pending promises.
187
- if (this.pendingRejects__.size) {
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(); // Clear all pending rejects.
228
+ this.pendingRejects__.clear();
193
229
  }
194
- this.priorityObservers_.clear(); // Clear all priority observers.
195
- this.observers_.clear(); // Clear all normal observers.
196
- this.config_.onDestroy?.(); // Call the optional onDestroy callback.
197
- this.config_ = null as unknown as SignalConfig; // Help GC by breaking references.
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
- * Throws an error if the signal has been destroyed.
202
- * This is a safeguard to prevent interaction with a defunct signal.
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__) {
@@ -1,8 +1,6 @@
1
- import {delay} from '@alwatr/delay';
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(`state-signal:${this.name}`);
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 notifies all active listeners.
100
+ * Updates the signal's value and schedules notifications for all active listeners.
91
101
  *
92
- * The notification is scheduled as a microtask, which means the update is deferred
93
- * slightly to batch multiple synchronous changes.
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
- * Notifies all listeners about the current value, even if it hasn't changed.
129
+ * Forcefully schedules a notification of the current value to all subscribers.
119
130
  *
120
- * This method is useful when you change the value instance directly (e.g., mutating an object) and want to inform listeners about the change.
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
- delay.nextMicrotask().then(() => {
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
- * This method is particularly useful for state transitions that depend on the current value,
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 behavior can be customized via the `options` parameter.
174
+ * This immediate call is queued as a microtask to match the asynchronous flow of signals.
164
175
  *
165
- * @param callback The function to be called when the signal's value changes.
166
- * @param options Subscription options, including `receivePrevious` and `once`.
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
- delay
180
- .nextMicrotask()
181
- .then((): void => {
182
- this.logger_.logStep?.('subscribe', 'immediate_callback');
183
- if (this.notifyVersion__ !== subscribeVersion) return; // A notification occurred after subscribing, so skip the immediate callback.
184
- if (options.once) {
185
- result.unsubscribe();
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
- .catch((err) => this.logger_.error('subscribe', 'immediate_callback_failed', err));
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
- * This is crucial for memory management to prevent leaks.
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
  }
@@ -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>} sourceSignal The original signal to debounce.
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>(sourceSignal: IReadonlySignal<T>, config: DebounceSignalConfig): ComputedSignal<T> {
63
- const name = config.name ?? `${sourceSignal.name}-debounced`;
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: `${name}-internal`,
67
- initialValue: sourceSignal.get(),
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 = sourceSignal.subscribe(debouncer.trigger, {receivePrevious: false});
78
+ const subscription = source.subscribe(debouncer.trigger, {receivePrevious: false});
77
79
 
78
- return createComputedSignal({
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
  }