@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
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import {createLogger, type AlwatrLogger} from '@alwatr/logger';
|
|
2
|
+
import {StateSignal} from './state-signal.js';
|
|
3
|
+
import type {
|
|
4
|
+
IReadonlySignal,
|
|
5
|
+
DerivedSignalConfig,
|
|
6
|
+
ListenerCallback,
|
|
7
|
+
SubscribeOptions,
|
|
8
|
+
SubscribeResult,
|
|
9
|
+
} from '../type.js';
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Ultra-performance read-only signal mapping exactly 1-to-1 over a single upstream source.
|
|
13
|
+
*
|
|
14
|
+
* COMPOSITION DESIGN PATTERN (HAS-A):
|
|
15
|
+
* Instead of extending the heavyweight Base class and duplicating tracking structures, it wraps
|
|
16
|
+
* an internal StateSignal instance. It features a "Cold Awakening Lifecycle": it consumes exactly
|
|
17
|
+
* ZERO stream overhead from the source until it receives its own first consumer subscription.
|
|
18
|
+
* If all consumers disconnect, it goes back to sleep (hibernation phase) to save performance.
|
|
19
|
+
*
|
|
20
|
+
* @template S The type of the source signal state.
|
|
21
|
+
* @template T The type of the derived/projected signal state.
|
|
22
|
+
*/
|
|
23
|
+
export class DerivedSignal<S, T> implements IReadonlySignal<T> {
|
|
24
|
+
/** The unique identifier for this signal instance, useful for debugging and tracing. */
|
|
25
|
+
public readonly name: string;
|
|
26
|
+
|
|
27
|
+
/** Scoped logger for tracking derived operations. */
|
|
28
|
+
protected readonly logger_: AlwatrLogger;
|
|
29
|
+
|
|
30
|
+
/** Wrapped internal state carrier - Enforcing COMPOSITION over inheritance, allocated lazily */
|
|
31
|
+
protected internalSignal_?: StateSignal<T> | null;
|
|
32
|
+
|
|
33
|
+
/** Subscription handle to the upstream source signal, active only when awake. */
|
|
34
|
+
private sourceSubscription__?: SubscribeResult;
|
|
35
|
+
|
|
36
|
+
/** Number of active standard/priority listeners currently subscribed to this signal. */
|
|
37
|
+
private activeConsumerCount__ = 0;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Creates a new DerivedSignal instance.
|
|
41
|
+
*
|
|
42
|
+
* @param config_ Configuration options including name, source, and projector.
|
|
43
|
+
*/
|
|
44
|
+
constructor(protected config_: DerivedSignalConfig<S, T>) {
|
|
45
|
+
this.name = this.config_.name;
|
|
46
|
+
this.logger_ = createLogger(`derived_signal:${this.name}`);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
untilNext(): Promise<T> {
|
|
50
|
+
this.logger_.logMethod?.('untilNext');
|
|
51
|
+
this.checkDestroyed__();
|
|
52
|
+
return new Promise<T>((resolve) => {
|
|
53
|
+
this.subscribe(
|
|
54
|
+
(value) => {
|
|
55
|
+
resolve(value);
|
|
56
|
+
},
|
|
57
|
+
{receivePrevious: false, once: true},
|
|
58
|
+
);
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Retrieves the current value of the derived signal.
|
|
64
|
+
*
|
|
65
|
+
* If there are no active subscribers (cold state), it re-computes dynamically
|
|
66
|
+
* on demand to ensure strict data freshness.
|
|
67
|
+
*
|
|
68
|
+
* @returns The current projected value.
|
|
69
|
+
*/
|
|
70
|
+
public get(): T {
|
|
71
|
+
this.logger_.logMethod?.('get');
|
|
72
|
+
this.checkDestroyed__();
|
|
73
|
+
if (this.activeConsumerCount__ === 0) {
|
|
74
|
+
return this.config_.projector(this.config_.source.get());
|
|
75
|
+
}
|
|
76
|
+
return this.internalSignal_!.get();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Indicates whether the signal has been destroyed.
|
|
81
|
+
*/
|
|
82
|
+
public get isDestroyed(): boolean {
|
|
83
|
+
return this.config_ === null;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
/**
|
|
87
|
+
* Subscribes a listener to updates of this derived signal.
|
|
88
|
+
*
|
|
89
|
+
* In case of first subscription, it triggers the "Cold Awakening Lifecycle"
|
|
90
|
+
* to subscribe to the source signal.
|
|
91
|
+
*
|
|
92
|
+
* @param callback Subscription callback function.
|
|
93
|
+
* @param options Subscription configurations.
|
|
94
|
+
* @returns Unsubscribe handle object.
|
|
95
|
+
*/
|
|
96
|
+
public subscribe(callback: ListenerCallback<T>, options?: SubscribeOptions): SubscribeResult {
|
|
97
|
+
this.logger_.logMethod?.('subscribe');
|
|
98
|
+
this.checkDestroyed__();
|
|
99
|
+
this.activeConsumerCount__++;
|
|
100
|
+
|
|
101
|
+
// Wake-up phase: if this is the first active consumer, dynamically clamp to the upstream core source
|
|
102
|
+
if (this.activeConsumerCount__ === 1) {
|
|
103
|
+
this.logger_.logMethod?.('wakeUp_');
|
|
104
|
+
this.internalSignal_ = new StateSignal<T>({
|
|
105
|
+
name: `derived-internal:${this.name}`,
|
|
106
|
+
initialValue: this.config_.projector(this.config_.source.get()),
|
|
107
|
+
});
|
|
108
|
+
this.sourceSubscription__ = this.config_.source.subscribe(
|
|
109
|
+
(newValue) => {
|
|
110
|
+
this.internalSignal_!.set(this.config_.projector(newValue));
|
|
111
|
+
},
|
|
112
|
+
{receivePrevious: false},
|
|
113
|
+
);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
const sub = this.internalSignal_!.subscribe(callback, options);
|
|
117
|
+
|
|
118
|
+
return {
|
|
119
|
+
unsubscribe: () => {
|
|
120
|
+
this.logger_.logMethod?.('unsubscribe');
|
|
121
|
+
|
|
122
|
+
sub.unsubscribe();
|
|
123
|
+
this.activeConsumerCount__--;
|
|
124
|
+
|
|
125
|
+
// Hibernation phase: unlink tracking dependencies when view elements clear out to preserve processing cycles
|
|
126
|
+
if (this.activeConsumerCount__ === 0 && this.sourceSubscription__) {
|
|
127
|
+
this.logger_.logMethod?.('sleepCleanup_');
|
|
128
|
+
this.sourceSubscription__.unsubscribe();
|
|
129
|
+
this.sourceSubscription__ = undefined;
|
|
130
|
+
this.internalSignal_?.destroy();
|
|
131
|
+
this.internalSignal_ = undefined;
|
|
132
|
+
}
|
|
133
|
+
},
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Destroys the derived signal and unsubscribes from the source signal if currently awake.
|
|
139
|
+
*/
|
|
140
|
+
public destroy(): void {
|
|
141
|
+
this.logger_.logMethod?.('destroy');
|
|
142
|
+
if (this.isDestroyed) return;
|
|
143
|
+
|
|
144
|
+
if (this.sourceSubscription__) {
|
|
145
|
+
this.sourceSubscription__.unsubscribe();
|
|
146
|
+
this.sourceSubscription__ = undefined;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
this.internalSignal_?.destroy();
|
|
150
|
+
this.config_.onDestroy?.();
|
|
151
|
+
this.config_ = null as unknown as DerivedSignalConfig<S, T>;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
/**
|
|
155
|
+
* Checks if the signal has been destroyed.
|
|
156
|
+
*
|
|
157
|
+
* @private
|
|
158
|
+
* @throws {Error} If destroyed.
|
|
159
|
+
*/
|
|
160
|
+
private checkDestroyed__(): void {
|
|
161
|
+
this.logger_.logMethod?.('checkDestroyed__');
|
|
162
|
+
if (this.isDestroyed) {
|
|
163
|
+
throw new Error(`Cannot interact with a destroyed signal (id: ${this.name})`);
|
|
164
|
+
}
|
|
165
|
+
}
|
|
166
|
+
}
|
|
@@ -1,7 +1,6 @@
|
|
|
1
|
-
import {
|
|
2
|
-
import {
|
|
3
|
-
|
|
4
|
-
import type { EffectSignalConfig, IEffectSignal, SubscribeResult } from '../type.js';
|
|
1
|
+
import {delay} from '@alwatr/delay';
|
|
2
|
+
import {createLogger, type AlwatrLogger} from '@alwatr/logger';
|
|
3
|
+
import type {EffectSignalConfig, IEffectSignal, SubscribeResult} from '../type.js';
|
|
5
4
|
|
|
6
5
|
/**
|
|
7
6
|
* Manages a side-effect that runs in response to changes in dependency signals.
|
|
@@ -50,24 +49,30 @@ export class EffectSignal implements IEffectSignal {
|
|
|
50
49
|
|
|
51
50
|
/**
|
|
52
51
|
* The logger instance for this signal.
|
|
52
|
+
*
|
|
53
53
|
* @protected
|
|
54
54
|
*/
|
|
55
55
|
protected readonly logger_: AlwatrLogger;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* A list of subscriptions to dependency signals.
|
|
59
|
+
* Used to unsubscribe from dependencies when this signal is destroyed.
|
|
60
|
+
*
|
|
59
61
|
* @private
|
|
60
62
|
*/
|
|
61
63
|
private readonly dependencySubscriptions__: SubscribeResult[] = [];
|
|
62
64
|
|
|
63
65
|
/**
|
|
64
66
|
* A flag to prevent concurrent executions of the effect.
|
|
67
|
+
* Avoids scheduling multiple runs within the same event loop.
|
|
68
|
+
*
|
|
65
69
|
* @private
|
|
66
70
|
*/
|
|
67
71
|
private isRunning__ = false;
|
|
68
72
|
|
|
69
73
|
/**
|
|
70
74
|
* A flag indicating whether the effect has been destroyed.
|
|
75
|
+
*
|
|
71
76
|
* @private
|
|
72
77
|
*/
|
|
73
78
|
private isDestroyed__ = false;
|
|
@@ -82,6 +87,12 @@ export class EffectSignal implements IEffectSignal {
|
|
|
82
87
|
return this.isDestroyed__;
|
|
83
88
|
}
|
|
84
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Creates a new EffectSignal instance.
|
|
92
|
+
* Subscribes to all dependency signals to listen for updates.
|
|
93
|
+
*
|
|
94
|
+
* @param config_ Configuration options including dependencies, side-effect runner callback, and immediate execution flag.
|
|
95
|
+
*/
|
|
85
96
|
constructor(protected config_: EffectSignalConfig) {
|
|
86
97
|
this.name = config_.name ?? `[${config_.deps.map((dep) => dep.name).join(', ')}]`;
|
|
87
98
|
this.logger_ = createLogger(`effect-signal:${this.name}`);
|
|
@@ -92,8 +103,8 @@ export class EffectSignal implements IEffectSignal {
|
|
|
92
103
|
// Subscribe to all dependencies. We don't need the previous value,
|
|
93
104
|
// as the `runImmediately` option controls the initial execution.
|
|
94
105
|
for (const signal of config_.deps) {
|
|
95
|
-
this.logger_.logStep?.('constructor', 'subscribing_to_dependency', {
|
|
96
|
-
this.dependencySubscriptions__.push(signal.subscribe(this.scheduleExecution_, {
|
|
106
|
+
this.logger_.logStep?.('constructor', 'subscribing_to_dependency', {signal: signal.name});
|
|
107
|
+
this.dependencySubscriptions__.push(signal.subscribe(this.scheduleExecution_, {receivePrevious: false}));
|
|
97
108
|
}
|
|
98
109
|
|
|
99
110
|
// Run the effect immediately if requested.
|
|
@@ -107,10 +118,12 @@ export class EffectSignal implements IEffectSignal {
|
|
|
107
118
|
/**
|
|
108
119
|
* Schedules the execution of the effect's `run` function.
|
|
109
120
|
*
|
|
110
|
-
* This method batches updates using a macrotask (`delay.
|
|
121
|
+
* This method batches updates using a macrotask (`delay.nextMicrotask`) to ensure the
|
|
111
122
|
* `run` function executes only once per event loop tick, even if multiple
|
|
112
123
|
* dependencies change simultaneously.
|
|
124
|
+
*
|
|
113
125
|
* @protected
|
|
126
|
+
* @returns A promise that resolves when the execution schedules or runs.
|
|
114
127
|
*/
|
|
115
128
|
protected async scheduleExecution_(): Promise<void> {
|
|
116
129
|
this.logger_.logMethod?.('scheduleExecution_');
|
|
@@ -129,7 +142,7 @@ export class EffectSignal implements IEffectSignal {
|
|
|
129
142
|
|
|
130
143
|
try {
|
|
131
144
|
// Wait for the next macrotask to batch simultaneous updates.
|
|
132
|
-
await delay.
|
|
145
|
+
await delay.nextMicrotask();
|
|
133
146
|
if (this.isDestroyed__) {
|
|
134
147
|
this.logger_.incident?.('scheduleExecution_', 'destroyed_during_delay');
|
|
135
148
|
this.isRunning__ = false;
|
|
@@ -137,9 +150,8 @@ export class EffectSignal implements IEffectSignal {
|
|
|
137
150
|
}
|
|
138
151
|
|
|
139
152
|
this.logger_.logStep?.('scheduleExecution_', 'executing_effect');
|
|
140
|
-
|
|
141
|
-
}
|
|
142
|
-
catch (err) {
|
|
153
|
+
this.config_.run();
|
|
154
|
+
} catch (err) {
|
|
143
155
|
this.logger_.error('scheduleExecution_', 'effect_failed', err);
|
|
144
156
|
}
|
|
145
157
|
|
package/src/core/event-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 {IBaseSignal, SignalConfig} from '../type.js';
|
|
7
5
|
|
|
8
6
|
/**
|
|
@@ -35,27 +33,34 @@ import type {IBaseSignal, SignalConfig} from '../type.js';
|
|
|
35
33
|
export class EventSignal<T = void> extends SignalBase<T> implements IBaseSignal<T> {
|
|
36
34
|
/**
|
|
37
35
|
* The logger instance for this signal.
|
|
36
|
+
*
|
|
38
37
|
* @protected
|
|
39
38
|
*/
|
|
40
39
|
protected logger_: AlwatrLogger;
|
|
41
40
|
|
|
41
|
+
/**
|
|
42
|
+
* Creates a new EventSignal instance.
|
|
43
|
+
*
|
|
44
|
+
* @param config Configuration options including the unique event name and custom cleanup hooks.
|
|
45
|
+
*/
|
|
42
46
|
constructor(config: SignalConfig) {
|
|
43
47
|
super(config);
|
|
44
|
-
this.logger_ = createLogger(`
|
|
48
|
+
this.logger_ = createLogger(`event_signal:${this.name}`);
|
|
45
49
|
this.logger_.logMethod?.('constructor');
|
|
46
50
|
}
|
|
47
51
|
|
|
48
52
|
/**
|
|
49
|
-
* Dispatches an event with
|
|
50
|
-
*
|
|
51
|
-
*
|
|
53
|
+
* Dispatches an event with the specified payload to all active listeners.
|
|
54
|
+
*
|
|
55
|
+
* To prevent blocking of the main thread and ensure consistent execution order,
|
|
56
|
+
* the notification execution is scheduled as a microtask using `queueMicrotask`.
|
|
52
57
|
*
|
|
53
|
-
* @param payload The data to send with the event.
|
|
58
|
+
* @param payload The data payload to send with the event.
|
|
54
59
|
*/
|
|
55
60
|
public dispatch(payload: T): void {
|
|
56
61
|
this.logger_.logMethodArgs?.('dispatch', {payload});
|
|
57
62
|
this.checkDestroyed_();
|
|
58
63
|
// Dispatch as a microtask to ensure consistent, non-blocking behavior.
|
|
59
|
-
|
|
64
|
+
queueMicrotask(() => this.notify_(payload));
|
|
60
65
|
}
|
|
61
66
|
}
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {createDebouncer} from '@alwatr/debounce';
|
|
2
2
|
import {createLocalStorageProvider} from '@alwatr/local-storage';
|
|
3
|
-
|
|
4
3
|
import {StateSignal} from './state-signal.js';
|
|
5
|
-
|
|
6
4
|
import type {PersistentStateSignalConfig} from '../type.js';
|
|
7
5
|
import type {LocalStorageProvider} from '@alwatr/local-storage';
|
|
8
6
|
|
|
@@ -50,12 +48,16 @@ import type {LocalStorageProvider} from '@alwatr/local-storage';
|
|
|
50
48
|
export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
51
49
|
/**
|
|
52
50
|
* The underlying storage provider instance.
|
|
51
|
+
* Handles read, write, and schema version management under the hood.
|
|
52
|
+
*
|
|
53
53
|
* @private
|
|
54
54
|
*/
|
|
55
55
|
private readonly storageProvider__: LocalStorageProvider<T>;
|
|
56
56
|
|
|
57
57
|
/**
|
|
58
58
|
* Debouncer to limit how often we write to localStorage.
|
|
59
|
+
* Reduces performance overhead from excessive disk writes.
|
|
60
|
+
*
|
|
59
61
|
* @private
|
|
60
62
|
*/
|
|
61
63
|
private readonly storageDebouncer__;
|
|
@@ -64,12 +66,15 @@ export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
|
64
66
|
* The subscription to the signal's own changes to sync with storage.
|
|
65
67
|
* We subscribe to our own signal. When the value is set from anywhere,
|
|
66
68
|
* this listener will trigger and write it to localStorage.
|
|
69
|
+
*
|
|
67
70
|
* @private
|
|
68
71
|
*/
|
|
69
72
|
private readonly storageSyncSubscription__;
|
|
70
73
|
|
|
71
74
|
/**
|
|
72
75
|
* Listener for the browser's pagehide events to flush pending saves.
|
|
76
|
+
* Ensures that pending changes are saved before the page unloading.
|
|
77
|
+
*
|
|
73
78
|
* @private
|
|
74
79
|
*/
|
|
75
80
|
private readonly windowPageHideListener_ = (): void => {
|
|
@@ -78,6 +83,8 @@ export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
|
78
83
|
|
|
79
84
|
/**
|
|
80
85
|
* Listener for the browser's pageshow events to sync from storage when restored from BFCache.
|
|
86
|
+
* Refreshes the in-memory value if retrieved from the Back/Forward Cache.
|
|
87
|
+
*
|
|
81
88
|
* @private
|
|
82
89
|
*/
|
|
83
90
|
private readonly windowPageShowListener_ = (event: PageTransitionEvent): void => {
|
|
@@ -90,6 +97,13 @@ export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
|
90
97
|
}
|
|
91
98
|
};
|
|
92
99
|
|
|
100
|
+
/**
|
|
101
|
+
* Creates a new PersistentStateSignal instance.
|
|
102
|
+
* Restores initial value from storage if it exists, otherwise uses default initialValue.
|
|
103
|
+
* Sets up window page visibility and BFCache listeners to guarantee write flushes.
|
|
104
|
+
*
|
|
105
|
+
* @param config Configuration options including storage keys, debounce delays, schema, parse, and stringify overrides.
|
|
106
|
+
*/
|
|
93
107
|
constructor(config: PersistentStateSignalConfig<T>) {
|
|
94
108
|
const {
|
|
95
109
|
name,
|
|
@@ -137,7 +151,10 @@ export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
|
137
151
|
|
|
138
152
|
/**
|
|
139
153
|
* Syncs the new value to storage.
|
|
140
|
-
*
|
|
154
|
+
* Invoked automatically by the debouncer.
|
|
155
|
+
*
|
|
156
|
+
* @param newValue The new value to write to storage.
|
|
157
|
+
* @private
|
|
141
158
|
*/
|
|
142
159
|
private syncStorage__(newValue: T): void {
|
|
143
160
|
this.logger_.logMethodArgs?.('syncStorage__', newValue);
|
|
@@ -156,7 +173,7 @@ export class PersistentStateSignal<T> extends StateSignal<T> {
|
|
|
156
173
|
}
|
|
157
174
|
|
|
158
175
|
/**
|
|
159
|
-
* Overrides the destroy method to also clean up the storage sync subscription.
|
|
176
|
+
* Overrides the destroy method to also clean up the storage sync subscription and event listeners.
|
|
160
177
|
*/
|
|
161
178
|
public override destroy(): void {
|
|
162
179
|
this.logger_.logMethod?.('destroy');
|
|
@@ -1,8 +1,6 @@
|
|
|
1
1
|
import {createDebouncer} from '@alwatr/debounce';
|
|
2
2
|
import {createSessionStorageProvider} from '@alwatr/session-storage';
|
|
3
|
-
|
|
4
3
|
import {StateSignal} from './state-signal.js';
|
|
5
|
-
|
|
6
4
|
import type {SessionStateSignalConfig} from '../type.js';
|
|
7
5
|
import type {SessionStorageProvider} from '@alwatr/session-storage';
|
|
8
6
|
|
|
@@ -44,7 +42,7 @@ import type {SessionStorageProvider} from '@alwatr/session-storage';
|
|
|
44
42
|
*
|
|
45
43
|
* // Example 2: Custom state type with parse and stringify
|
|
46
44
|
* const dateSignal = new SessionStateSignal<Date>({
|
|
47
|
-
* name: '
|
|
45
|
+
* name: 'timestamp-signal',
|
|
48
46
|
* initialValue: new Date(),
|
|
49
47
|
* parse: (str: string) => new Date(str),
|
|
50
48
|
* stringify: (date: Date) => date.toISOString(),
|
|
@@ -62,24 +60,31 @@ import type {SessionStorageProvider} from '@alwatr/session-storage';
|
|
|
62
60
|
export class SessionStateSignal<T> extends StateSignal<T> {
|
|
63
61
|
/**
|
|
64
62
|
* The underlying session storage provider instance.
|
|
63
|
+
*
|
|
65
64
|
* @private
|
|
66
65
|
*/
|
|
67
66
|
private readonly storageProvider__: SessionStorageProvider<T>;
|
|
68
67
|
|
|
69
68
|
/**
|
|
70
69
|
* Debouncer to limit how often we write to sessionStorage.
|
|
70
|
+
* Helps reduce synchronous overhead from session storage updates.
|
|
71
|
+
*
|
|
71
72
|
* @private
|
|
72
73
|
*/
|
|
73
74
|
private readonly storageDebouncer__;
|
|
74
75
|
|
|
75
76
|
/**
|
|
76
77
|
* Subscription to the signal's own changes for sessionStorage sync.
|
|
78
|
+
* Writes the value to sessionStorage after a debounce delay.
|
|
79
|
+
*
|
|
77
80
|
* @private
|
|
78
81
|
*/
|
|
79
82
|
private readonly storageSyncSubscription__;
|
|
80
83
|
|
|
81
84
|
/**
|
|
82
85
|
* Listener for the browser's pagehide events to flush pending saves.
|
|
86
|
+
* Ensures pending state changes are flushed to sessionStorage before leaving the page.
|
|
87
|
+
*
|
|
83
88
|
* @private
|
|
84
89
|
*/
|
|
85
90
|
private readonly windowPageHideListener__ = (): void => {
|
|
@@ -88,6 +93,8 @@ export class SessionStateSignal<T> extends StateSignal<T> {
|
|
|
88
93
|
|
|
89
94
|
/**
|
|
90
95
|
* Listener for the browser's pageshow events to sync from storage when restored from BFCache.
|
|
96
|
+
* Ensures in-memory sync when restored from the back-forward cache.
|
|
97
|
+
*
|
|
91
98
|
* @private
|
|
92
99
|
*/
|
|
93
100
|
private readonly windowPageShowListener__ = (event: PageTransitionEvent): void => {
|
|
@@ -100,6 +107,13 @@ export class SessionStateSignal<T> extends StateSignal<T> {
|
|
|
100
107
|
}
|
|
101
108
|
};
|
|
102
109
|
|
|
110
|
+
/**
|
|
111
|
+
* Creates a new SessionStateSignal instance.
|
|
112
|
+
* Loads initial value from sessionStorage if found, otherwise uses config's initialValue.
|
|
113
|
+
* Sets up page hide/show lifecycle handlers.
|
|
114
|
+
*
|
|
115
|
+
* @param config Configuration options including storageKeys, custom parse/stringify, and debounce delay.
|
|
116
|
+
*/
|
|
103
117
|
constructor(config: SessionStateSignalConfig<T>) {
|
|
104
118
|
const {name, storageKey = name, saveDebounceDelay = 1000, initialValue, onDestroy, parse, stringify} = config;
|
|
105
119
|
|
|
@@ -140,6 +154,7 @@ export class SessionStateSignal<T> extends StateSignal<T> {
|
|
|
140
154
|
* Called automatically by the debouncer after each state change.
|
|
141
155
|
*
|
|
142
156
|
* @param newValue The new value to sync.
|
|
157
|
+
* @private
|
|
143
158
|
*/
|
|
144
159
|
private syncStorage__(newValue: T): void {
|
|
145
160
|
this.logger_.logMethodArgs?.('syncStorage__', newValue);
|
|
@@ -165,7 +180,7 @@ export class SessionStateSignal<T> extends StateSignal<T> {
|
|
|
165
180
|
}
|
|
166
181
|
|
|
167
182
|
/**
|
|
168
|
-
* Destroys the signal, flushing pending writes and cleaning up all resources.
|
|
183
|
+
* Destroys the signal, flushing pending writes and cleaning up all resources and events.
|
|
169
184
|
*
|
|
170
185
|
* Always call this when the signal is no longer needed (e.g., on component unmount)
|
|
171
186
|
* to prevent memory leaks.
|