@alwatr/signal 9.26.0 → 9.30.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/dist/type.d.ts
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import type { Awaitable } from '@alwatr/type-helper';
|
|
2
1
|
import type { DebouncerConfig } from '@alwatr/debounce';
|
|
3
2
|
import type { LocalStorageProviderConfig } from '@alwatr/local-storage';
|
|
4
3
|
import type { SessionStorageProviderConfig } from '@alwatr/session-storage';
|
|
@@ -10,7 +9,7 @@ import type { SessionStorageProviderConfig } from '@alwatr/session-storage';
|
|
|
10
9
|
*
|
|
11
10
|
* @template T The type of the value that the signal holds or dispatches.
|
|
12
11
|
*/
|
|
13
|
-
export type ListenerCallback<T> = (value: T) =>
|
|
12
|
+
export type ListenerCallback<T> = (value: T) => void;
|
|
14
13
|
/**
|
|
15
14
|
* Options for fine-tuning the behavior of a subscription to a signal.
|
|
16
15
|
*/
|
|
@@ -205,6 +204,21 @@ export interface ComputedSignalConfig<T> extends SignalConfig {
|
|
|
205
204
|
*/
|
|
206
205
|
get: () => T;
|
|
207
206
|
}
|
|
207
|
+
/**
|
|
208
|
+
* Configuration for creating a `DerivedSignal`.
|
|
209
|
+
* @template S The type of the source signal state.
|
|
210
|
+
* @template T The type of the projected derived state.
|
|
211
|
+
*/
|
|
212
|
+
export interface DerivedSignalConfig<S, T> extends SignalConfig {
|
|
213
|
+
/**
|
|
214
|
+
* The single upstream readonly source signal.
|
|
215
|
+
*/
|
|
216
|
+
readonly source: IReadonlySignal<S>;
|
|
217
|
+
/**
|
|
218
|
+
* Projection mapping function transforming source S to derived T.
|
|
219
|
+
*/
|
|
220
|
+
readonly projector: (value: S) => T;
|
|
221
|
+
}
|
|
208
222
|
/**
|
|
209
223
|
* Configuration for creating an `EffectSignal`.
|
|
210
224
|
*/
|
|
@@ -236,7 +250,7 @@ export interface EffectSignalConfig {
|
|
|
236
250
|
* run: () => console.log(`The counter is now: ${counter.get()}`),
|
|
237
251
|
* });
|
|
238
252
|
*/
|
|
239
|
-
run: () =>
|
|
253
|
+
run: () => void;
|
|
240
254
|
/**
|
|
241
255
|
* If `true`, the effect's `run` function will be executed once immediately upon initialization.
|
|
242
256
|
* @default false
|
|
@@ -373,4 +387,55 @@ export interface SessionStateSignalConfig<T> extends StateSignalConfig<T>, Sessi
|
|
|
373
387
|
*/
|
|
374
388
|
saveDebounceDelay?: number;
|
|
375
389
|
}
|
|
390
|
+
/**
|
|
391
|
+
* Determines whether the payload argument for a given channel message is
|
|
392
|
+
* required or optional, based solely on the declared type in `TMap`.
|
|
393
|
+
*
|
|
394
|
+
* - `void | undefined` → payload is optional (second arg may be omitted).
|
|
395
|
+
* - anything else → payload is **required** (omitting it is a compile error).
|
|
396
|
+
*
|
|
397
|
+
* This is used to build the rest-parameter tuple for `dispatch()` so that
|
|
398
|
+
* TypeScript enforces the correct call signature at every dispatch site.
|
|
399
|
+
*
|
|
400
|
+
* @template TMap A record mapping message names to their payload types.
|
|
401
|
+
* @template K The specific message name key.
|
|
402
|
+
*
|
|
403
|
+
* @example
|
|
404
|
+
* ```ts
|
|
405
|
+
* // ActionRecord: { 'logout': void; 'add-to-cart': {productId: number} }
|
|
406
|
+
* type A = DispatchArgs<ActionRecord, 'logout'>; // [name: 'logout', payload?: void]
|
|
407
|
+
* type B = DispatchArgs<ActionRecord, 'add-to-cart'>; // [name: 'add-to-cart', payload: {productId: number}]
|
|
408
|
+
* ```
|
|
409
|
+
*/
|
|
410
|
+
export type DispatchArgs<TMap extends object, K extends keyof TMap> = TMap[K] extends void | undefined ? [name: K, payload?: TMap[K]] : [name: K, payload: TMap[K]];
|
|
411
|
+
/**
|
|
412
|
+
* A single message dispatched through a `ChannelSignal`.
|
|
413
|
+
*
|
|
414
|
+
* `name` identifies the message type (e.g. `'open-drawer'`, `'add-to-cart'`).
|
|
415
|
+
* `payload` carries the associated data, whose type is determined by the generic `TMap` based on the `name`.
|
|
416
|
+
*
|
|
417
|
+
* @template TMap A record mapping message names to their payload types.
|
|
418
|
+
* @template K The specific message name key (inferred, not set manually).
|
|
419
|
+
*/
|
|
420
|
+
export type ChannelMessage<TMap extends object, K extends keyof TMap = keyof TMap> = {
|
|
421
|
+
name: K;
|
|
422
|
+
payload: TMap[K];
|
|
423
|
+
};
|
|
424
|
+
/**
|
|
425
|
+
* A typed handler for a specific named message on a `ChannelSignal`.
|
|
426
|
+
* Receives only the `payload` — the name is already known at subscription time.
|
|
427
|
+
*
|
|
428
|
+
* The payload type mirrors `DispatchArgs`: it is `TMap[K] | undefined` only
|
|
429
|
+
* when the declared type is `void | undefined`; otherwise it is exactly `TMap[K]`
|
|
430
|
+
* (non-optional) so handlers do not need unnecessary null-guards.
|
|
431
|
+
*
|
|
432
|
+
* @template TMap A record mapping message names to their payload types.
|
|
433
|
+
* @template K The specific message name key.
|
|
434
|
+
*/
|
|
435
|
+
export type ChannelHandler<TMap extends object, K extends keyof TMap = keyof TMap> = (payload: TMap[K]) => void;
|
|
436
|
+
/**
|
|
437
|
+
* Configuration for creating a `ChannelSignal`.
|
|
438
|
+
*/
|
|
439
|
+
export interface ChannelSignalConfig extends SignalConfig {
|
|
440
|
+
}
|
|
376
441
|
//# sourceMappingURL=type.d.ts.map
|
package/dist/type.d.ts.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAC,
|
|
1
|
+
{"version":3,"file":"type.d.ts","sourceRoot":"","sources":["../src/type.ts"],"names":[],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@alwatr/signal",
|
|
3
|
-
"version": "9.
|
|
3
|
+
"version": "9.30.0",
|
|
4
4
|
"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.",
|
|
5
5
|
"license": "MPL-2.0",
|
|
6
6
|
"author": "S. Ali Mihandoost <ali.mihandoost@gmail.com> (https://ali.mihandoost.com)",
|
|
@@ -22,10 +22,10 @@
|
|
|
22
22
|
"sideEffects": false,
|
|
23
23
|
"dependencies": {
|
|
24
24
|
"@alwatr/debounce": "9.26.0",
|
|
25
|
-
"@alwatr/delay": "9.
|
|
26
|
-
"@alwatr/local-storage": "9.
|
|
27
|
-
"@alwatr/logger": "9.
|
|
28
|
-
"@alwatr/session-storage": "9.
|
|
25
|
+
"@alwatr/delay": "9.30.0",
|
|
26
|
+
"@alwatr/local-storage": "9.29.0",
|
|
27
|
+
"@alwatr/logger": "9.29.0",
|
|
28
|
+
"@alwatr/session-storage": "9.29.0"
|
|
29
29
|
},
|
|
30
30
|
"devDependencies": {
|
|
31
31
|
"@alwatr/nano-build": "9.25.0",
|
|
@@ -66,5 +66,5 @@
|
|
|
66
66
|
"signal",
|
|
67
67
|
"typescript"
|
|
68
68
|
],
|
|
69
|
-
"gitHead": "
|
|
69
|
+
"gitHead": "9ef1681df17bbf7c991631d800a2d4261e54b843"
|
|
70
70
|
}
|
|
@@ -1,61 +1,15 @@
|
|
|
1
|
-
import
|
|
2
|
-
import {delay} from '@alwatr/delay';
|
|
1
|
+
import {queueMicrotask} from '@alwatr/delay';
|
|
3
2
|
import {createLogger, type AlwatrLogger} from '@alwatr/logger';
|
|
4
|
-
|
|
5
3
|
import {SignalBase} from './signal-base.js';
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
* - `void | undefined` → payload is optional (second arg may be omitted).
|
|
16
|
-
* - anything else → payload is **required** (omitting it is a compile error).
|
|
17
|
-
*
|
|
18
|
-
* This is used to build the rest-parameter tuple for `dispatch()` so that
|
|
19
|
-
* TypeScript enforces the correct call signature at every dispatch site.
|
|
20
|
-
*
|
|
21
|
-
* @template TMap A record mapping message names to their payload types.
|
|
22
|
-
* @template K The specific message name key.
|
|
23
|
-
*
|
|
24
|
-
* @example
|
|
25
|
-
* ```ts
|
|
26
|
-
* // ActionRecord: { 'logout': void; 'add-to-cart': {productId: number} }
|
|
27
|
-
* type A = DispatchArgs<ActionRecord, 'logout'>; // [name: 'logout', payload?: void]
|
|
28
|
-
* type B = DispatchArgs<ActionRecord, 'add-to-cart'>; // [name: 'add-to-cart', payload: {productId: number}]
|
|
29
|
-
* ```
|
|
30
|
-
*/
|
|
31
|
-
export type DispatchArgs<TMap extends object, K extends keyof TMap> =
|
|
32
|
-
TMap[K] extends void | undefined ? [name: K, payload?: TMap[K]] : [name: K, payload: TMap[K]];
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* A single message dispatched through a `ChannelSignal`.
|
|
36
|
-
*
|
|
37
|
-
* `name` identifies the message type (e.g. `'open-drawer'`, `'add-to-cart'`).
|
|
38
|
-
* `payload` carries the associated data, whose type is determined by the generic `TMap` based on the `name`.
|
|
39
|
-
*
|
|
40
|
-
* @template TMap A record mapping message names to their payload types.
|
|
41
|
-
* @template K The specific message name key (inferred, not set manually).
|
|
42
|
-
*/
|
|
43
|
-
export type ChannelMessage<TMap extends object, K extends keyof TMap = keyof TMap> = {name: K; payload: TMap[K]};
|
|
44
|
-
|
|
45
|
-
/**
|
|
46
|
-
* A typed handler for a specific named message on a `ChannelSignal`.
|
|
47
|
-
* Receives only the `payload` — the name is already known at subscription time.
|
|
48
|
-
*
|
|
49
|
-
* The payload type mirrors `DispatchArgs`: it is `TMap[K] | undefined` only
|
|
50
|
-
* when the declared type is `void | undefined`; otherwise it is exactly `TMap[K]`
|
|
51
|
-
* (non-optional) so handlers do not need unnecessary null-guards.
|
|
52
|
-
*
|
|
53
|
-
* @template TMap A record mapping message names to their payload types.
|
|
54
|
-
* @template K The specific message name key.
|
|
55
|
-
*/
|
|
56
|
-
export type ChannelHandler<TMap extends object, K extends keyof TMap = keyof TMap> = (
|
|
57
|
-
payload: TMap[K],
|
|
58
|
-
) => Awaitable<void>;
|
|
4
|
+
import type {
|
|
5
|
+
SubscribeOptions,
|
|
6
|
+
SubscribeResult,
|
|
7
|
+
ListenerCallback,
|
|
8
|
+
ChannelHandler,
|
|
9
|
+
ChannelMessage,
|
|
10
|
+
ChannelSignalConfig,
|
|
11
|
+
DispatchArgs,
|
|
12
|
+
} from '../type.js';
|
|
59
13
|
|
|
60
14
|
/**
|
|
61
15
|
* Internal handler type used inside `namedHandlers__`.
|
|
@@ -68,12 +22,7 @@ export type ChannelHandler<TMap extends object, K extends keyof TMap = keyof TMa
|
|
|
68
22
|
*
|
|
69
23
|
* @internal
|
|
70
24
|
*/
|
|
71
|
-
type InternalHandler = (payload: unknown) =>
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Configuration for creating a `ChannelSignal`.
|
|
75
|
-
*/
|
|
76
|
-
export interface ChannelSignalConfig extends SignalConfig {}
|
|
25
|
+
type InternalHandler = (payload: unknown) => void;
|
|
77
26
|
|
|
78
27
|
// ─── Class ────────────────────────────────────────────────────────────────────
|
|
79
28
|
|
|
@@ -124,6 +73,7 @@ export interface ChannelSignalConfig extends SignalConfig {}
|
|
|
124
73
|
export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessage<TMap>> {
|
|
125
74
|
/**
|
|
126
75
|
* The logger instance for this signal.
|
|
76
|
+
*
|
|
127
77
|
* @protected
|
|
128
78
|
*/
|
|
129
79
|
protected logger_: AlwatrLogger;
|
|
@@ -143,9 +93,14 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
143
93
|
*/
|
|
144
94
|
private readonly namedHandlers__ = new Map<keyof TMap, Set<{handler: InternalHandler; once: boolean}>>();
|
|
145
95
|
|
|
96
|
+
/**
|
|
97
|
+
* Creates a new ChannelSignal instance.
|
|
98
|
+
*
|
|
99
|
+
* @param config Configuration options including the unique channel name and cleanup hook.
|
|
100
|
+
*/
|
|
146
101
|
constructor(config: ChannelSignalConfig) {
|
|
147
102
|
super(config);
|
|
148
|
-
this.logger_ = createLogger(`
|
|
103
|
+
this.logger_ = createLogger(`channel_signal:${this.name}`);
|
|
149
104
|
this.logger_.logMethod?.('constructor');
|
|
150
105
|
}
|
|
151
106
|
|
|
@@ -172,6 +127,7 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
172
127
|
* channel.dispatch('logout', undefined); // ✅ also fine
|
|
173
128
|
* ```
|
|
174
129
|
*
|
|
130
|
+
* @template K The specific message name key.
|
|
175
131
|
* @param args Tuple of `[name, payload]` — payload optionality is enforced
|
|
176
132
|
* by `DispatchArgs<TMap, K>` based on the declared type.
|
|
177
133
|
*/
|
|
@@ -179,7 +135,7 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
179
135
|
const [name, payload] = args;
|
|
180
136
|
this.logger_.logMethodArgs?.('dispatch', {name, payload});
|
|
181
137
|
this.checkDestroyed_();
|
|
182
|
-
|
|
138
|
+
queueMicrotask(() => this.route__(name, payload));
|
|
183
139
|
}
|
|
184
140
|
|
|
185
141
|
/**
|
|
@@ -192,6 +148,7 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
192
148
|
* envelope) — since the name is already known at subscription time, passing
|
|
193
149
|
* it again would be redundant.
|
|
194
150
|
*
|
|
151
|
+
* @template K The specific message name key.
|
|
195
152
|
* @param name The message name to listen for.
|
|
196
153
|
* @param handler Callback invoked with the payload each time the named message
|
|
197
154
|
* is dispatched.
|
|
@@ -272,6 +229,9 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
272
229
|
* 2. Invokes each handler, removing `once` entries after their first call.
|
|
273
230
|
* 3. Notifies raw-stream subscribers via `SignalBase.notify_()`.
|
|
274
231
|
*
|
|
232
|
+
* @template K The specific message name key.
|
|
233
|
+
* @param name The message name to route.
|
|
234
|
+
* @param payload The payload associated with the message name.
|
|
275
235
|
* @private
|
|
276
236
|
*/
|
|
277
237
|
private route__<K extends keyof TMap>(name: K, payload: TMap[K] | undefined): void {
|
|
@@ -285,10 +245,7 @@ export class ChannelSignal<TMap extends object> extends SignalBase<ChannelMessag
|
|
|
285
245
|
if (handlerSet.size === 0) this.namedHandlers__.delete(name);
|
|
286
246
|
}
|
|
287
247
|
try {
|
|
288
|
-
|
|
289
|
-
if (result instanceof Promise) {
|
|
290
|
-
result.catch((err) => this.logger_.error('route__', 'async_named_handler_failed', err));
|
|
291
|
-
}
|
|
248
|
+
entry.handler(payload);
|
|
292
249
|
} catch (err) {
|
|
293
250
|
this.logger_.error('route__', 'sync_named_handler_failed', err);
|
|
294
251
|
}
|
|
@@ -1,9 +1,13 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
|
|
6
|
-
|
|
1
|
+
import {queueMicrotask} from '@alwatr/delay';
|
|
2
|
+
import {createLogger, type AlwatrLogger} from '@alwatr/logger';
|
|
3
|
+
import {StateSignal} from './state-signal.js';
|
|
4
|
+
import type {
|
|
5
|
+
ComputedSignalConfig,
|
|
6
|
+
IReadonlySignal,
|
|
7
|
+
SubscribeOptions,
|
|
8
|
+
SubscribeResult,
|
|
9
|
+
ListenerCallback,
|
|
10
|
+
} from '../type.js';
|
|
7
11
|
|
|
8
12
|
/**
|
|
9
13
|
* A read-only signal that derives its value from a set of dependency signals.
|
|
@@ -16,32 +20,6 @@ import type { ComputedSignalConfig, IReadonlySignal, SubscribeResult, SubscribeO
|
|
|
16
20
|
* needed to prevent memory leaks from its subscriptions to dependency signals.
|
|
17
21
|
*
|
|
18
22
|
* @template T The type of the computed value.
|
|
19
|
-
*
|
|
20
|
-
* @example
|
|
21
|
-
* // --- Create dependency signals ---
|
|
22
|
-
* const firstName = new StateSignal({ name: 'firstName', initialValue: 'John' });
|
|
23
|
-
* const lastName = new StateSignal({ name: 'lastName', initialValue: 'Doe' });
|
|
24
|
-
*
|
|
25
|
-
* // --- Create a computed signal ---
|
|
26
|
-
* const fullName = new ComputedSignal({
|
|
27
|
-
* name: 'fullName',
|
|
28
|
-
* deps: [firstName, lastName],
|
|
29
|
-
* get: () => `${firstName.get()} ${lastName.get()}`,
|
|
30
|
-
* });
|
|
31
|
-
*
|
|
32
|
-
* console.log(fullName.get()); // Outputs: "John Doe"
|
|
33
|
-
*
|
|
34
|
-
* // --- Subscribe to the computed value ---
|
|
35
|
-
* fullName.subscribe(newFullName => {
|
|
36
|
-
* console.log(`Name changed to: ${newFullName}`);
|
|
37
|
-
* });
|
|
38
|
-
*
|
|
39
|
-
* // --- Update a dependency ---
|
|
40
|
-
* lastName.set('Smith'); // Recalculates and logs: "Name changed to: John Smith"
|
|
41
|
-
* console.log(fullName.get()); // Outputs: "John Smith"
|
|
42
|
-
*
|
|
43
|
-
* // --- IMPORTANT: Clean up when done ---
|
|
44
|
-
* fullName.destroy();
|
|
45
23
|
*/
|
|
46
24
|
export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
47
25
|
/**
|
|
@@ -51,6 +29,7 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
51
29
|
|
|
52
30
|
/**
|
|
53
31
|
* The logger instance for this signal.
|
|
32
|
+
*
|
|
54
33
|
* @protected
|
|
55
34
|
*/
|
|
56
35
|
protected readonly logger_: AlwatrLogger;
|
|
@@ -58,39 +37,51 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
58
37
|
/**
|
|
59
38
|
* The internal `StateSignal` that holds the computed value.
|
|
60
39
|
* This is how the computed signal provides `.get()` and `.subscribe()` methods.
|
|
40
|
+
* Enforces COMPOSITION over inheritance.
|
|
41
|
+
*
|
|
61
42
|
* @protected
|
|
62
43
|
*/
|
|
63
44
|
protected readonly internalSignal_: StateSignal<T>;
|
|
64
45
|
|
|
65
46
|
/**
|
|
66
47
|
* A list of subscriptions to dependency signals.
|
|
48
|
+
* Used to unsubscribe from dependencies when this signal is destroyed.
|
|
49
|
+
*
|
|
67
50
|
* @private
|
|
68
51
|
*/
|
|
69
|
-
|
|
70
52
|
private readonly dependencySubscriptions__: SubscribeResult[] = [];
|
|
71
53
|
|
|
72
54
|
/**
|
|
73
55
|
* A flag to prevent concurrent recalculations.
|
|
56
|
+
* Avoids queuing multiple updates in the event loop.
|
|
57
|
+
*
|
|
74
58
|
* @private
|
|
75
59
|
*/
|
|
76
60
|
private isRecalculating__ = false;
|
|
77
61
|
|
|
62
|
+
/**
|
|
63
|
+
* Creates a new ComputedSignal instance.
|
|
64
|
+
* Subscribes to all dependency signals to trigger recalculations.
|
|
65
|
+
*
|
|
66
|
+
* @param config_ Configuration options including dependencies, evaluation getter, and cleanup hook.
|
|
67
|
+
*/
|
|
78
68
|
constructor(protected config_: ComputedSignalConfig<T>) {
|
|
79
69
|
this.name = config_.name;
|
|
80
|
-
this.logger_ = createLogger(`
|
|
70
|
+
this.logger_ = createLogger(`computed_signal:${this.name}`);
|
|
81
71
|
this.recalculate_ = this.recalculate_.bind(this);
|
|
82
72
|
|
|
83
73
|
this.logger_.logMethod?.('constructor');
|
|
84
74
|
|
|
85
75
|
this.internalSignal_ = new StateSignal<T>({
|
|
86
|
-
name: `
|
|
76
|
+
name: `compute_internal:${this.name}`,
|
|
87
77
|
initialValue: this.config_.get(),
|
|
88
78
|
});
|
|
89
79
|
|
|
90
80
|
// Subscribe to all dependencies to trigger recalculation on change.
|
|
91
|
-
for (
|
|
92
|
-
|
|
93
|
-
this.
|
|
81
|
+
for (let i = 0; i < this.config_.deps.length; i++) {
|
|
82
|
+
const signal = this.config_.deps[i];
|
|
83
|
+
this.logger_.logStep?.('constructor', 'subscribing_to_dependency', {signal: signal.name});
|
|
84
|
+
this.dependencySubscriptions__.push(signal.subscribe(this.recalculate_, {receivePrevious: false}));
|
|
94
85
|
}
|
|
95
86
|
}
|
|
96
87
|
|
|
@@ -108,6 +99,7 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
108
99
|
/**
|
|
109
100
|
* Indicates whether the computed signal has been destroyed.
|
|
110
101
|
* A destroyed signal cannot be used and will throw an error if interacted with.
|
|
102
|
+
*
|
|
111
103
|
* @returns `true` if the signal is destroyed, `false` otherwise.
|
|
112
104
|
*/
|
|
113
105
|
public get isDestroyed(): boolean {
|
|
@@ -122,7 +114,7 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
122
114
|
* @param options Subscription options.
|
|
123
115
|
* @returns A `SubscribeResult` object with an `unsubscribe` method.
|
|
124
116
|
*/
|
|
125
|
-
public subscribe(callback:
|
|
117
|
+
public subscribe(callback: ListenerCallback<T>, options?: SubscribeOptions): SubscribeResult {
|
|
126
118
|
return this.internalSignal_.subscribe(callback, options);
|
|
127
119
|
}
|
|
128
120
|
|
|
@@ -146,42 +138,30 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
146
138
|
*/
|
|
147
139
|
public destroy(): void {
|
|
148
140
|
this.logger_.logMethod?.('destroy');
|
|
149
|
-
/**
|
|
150
|
-
* If already destroyed, log an incident and return early.
|
|
151
|
-
*/
|
|
152
141
|
if (this.isDestroyed) {
|
|
153
142
|
this.logger_.incident?.('destroy', 'already_destroyed');
|
|
154
143
|
return;
|
|
155
144
|
}
|
|
156
145
|
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
subscription.unsubscribe();
|
|
146
|
+
for (let i = 0; i < this.dependencySubscriptions__.length; i++) {
|
|
147
|
+
this.dependencySubscriptions__[i].unsubscribe();
|
|
160
148
|
}
|
|
161
|
-
this.dependencySubscriptions__.length = 0;
|
|
149
|
+
this.dependencySubscriptions__.length = 0;
|
|
162
150
|
|
|
163
|
-
this.internalSignal_.destroy();
|
|
164
|
-
this.config_.onDestroy?.();
|
|
165
|
-
this.config_ = null as unknown as ComputedSignalConfig<T>;
|
|
151
|
+
this.internalSignal_.destroy();
|
|
152
|
+
this.config_.onDestroy?.();
|
|
153
|
+
this.config_ = null as unknown as ComputedSignalConfig<T>;
|
|
166
154
|
}
|
|
167
155
|
|
|
168
156
|
/**
|
|
169
|
-
*
|
|
157
|
+
* Recalculates the derived value.
|
|
158
|
+
* Centralized microtask batcher coordination avoids internal loop crashes.
|
|
170
159
|
*
|
|
171
|
-
* This method batches updates using a macrotask (`delay.nextMacrotask`) to ensure the
|
|
172
|
-
* `get` function runs only once per event loop tick, even if multiple dependencies
|
|
173
|
-
* change in the same synchronous block of code.
|
|
174
160
|
* @protected
|
|
175
161
|
*/
|
|
176
|
-
protected
|
|
162
|
+
protected recalculate_(): void {
|
|
177
163
|
this.logger_.logMethod?.('recalculate_');
|
|
178
164
|
|
|
179
|
-
if (this.internalSignal_.isDestroyed) {
|
|
180
|
-
// This check is important in case a dependency fires after this signal is destroyed.
|
|
181
|
-
this.logger_.incident?.('recalculate_', 'recalculate_on_destroyed_signal');
|
|
182
|
-
return;
|
|
183
|
-
}
|
|
184
|
-
|
|
185
165
|
if (this.isRecalculating__) {
|
|
186
166
|
// If a recalculation is already scheduled, do nothing.
|
|
187
167
|
this.logger_.logStep?.('recalculate_', 'skipping_recalculation_already_scheduled');
|
|
@@ -190,11 +170,7 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
190
170
|
|
|
191
171
|
this.isRecalculating__ = true;
|
|
192
172
|
|
|
193
|
-
|
|
194
|
-
// Wait for the next macrotask to start the recalculation.
|
|
195
|
-
// This batches all synchronous dependency updates in the current event loop.
|
|
196
|
-
await delay.nextMacrotask();
|
|
197
|
-
|
|
173
|
+
queueMicrotask(() => {
|
|
198
174
|
if (this.isDestroyed) {
|
|
199
175
|
this.logger_.incident?.('recalculate_', 'destroyed_during_delay');
|
|
200
176
|
this.isRecalculating__ = false;
|
|
@@ -202,15 +178,15 @@ export class ComputedSignal<T> implements IReadonlySignal<T> {
|
|
|
202
178
|
}
|
|
203
179
|
|
|
204
180
|
this.logger_.logStep?.('recalculate_', 'recalculating_value');
|
|
181
|
+
try {
|
|
182
|
+
// Set the new value on the internal signal, which will notify our subscribers.
|
|
183
|
+
this.internalSignal_.set(this.config_.get());
|
|
184
|
+
} catch (err) {
|
|
185
|
+
this.logger_.error('recalculate_', 'projection_evaluation_failed', err);
|
|
186
|
+
}
|
|
205
187
|
|
|
206
|
-
//
|
|
207
|
-
this.
|
|
208
|
-
}
|
|
209
|
-
catch (err) {
|
|
210
|
-
this.logger_.error('recalculate_', 'recalculation_failed', err);
|
|
211
|
-
}
|
|
212
|
-
|
|
213
|
-
// Allow the next recalculation to be scheduled.
|
|
214
|
-
this.isRecalculating__ = false;
|
|
188
|
+
// Allow the next recalculation to be scheduled.
|
|
189
|
+
this.isRecalculating__ = false;
|
|
190
|
+
});
|
|
215
191
|
}
|
|
216
192
|
}
|