The StateChangeNotifier
class provides a type-safe mechanism for awaiting state transitions.
It functions similarly to a condition variable, allowing asynchronous tasks to pause until a state change occurs. Consumers can await any transition or specify target states of interest.
This utility is ideal for coordinating asynchronous flows that rely on state progression - such as initialization routines, lifecycle management, or event-driven logic.
- State Management 🔄: Provides methods to get and set the current state, as well as to await state changes using modern
async/await
syntax. Asynchronous tasks can pause until a state transition occurs, either to any new state or to specific target states of interest. The optionalICustomStateChangeRequest
argument of thewaitForChange
method allows specifying the desired state(s) to wait for - either in an inclusive manner ("one of the following states") or exclusive ("any state except the following"). - Comprehensive documentation 📚: Fully documented, enabling IDEs to provide intelligent tooltips for an enhanced development experience.
- Thoroughly Tested 🧪: Backed by extensive unit tests, to ensure reliability in production.
- Zero Runtime Dependencies 🕊️: Only development dependencies are included.
- ES2020 Compatibility: The project targets ES2020 for modern JavaScript support.
- Full TypeScript Support: Designed for seamless TypeScript integration.
The StateChangeNotifier
class provides the following methods:
- state getter: Retrieves the current state.
- state setter: Updates the current state. If the new state differs from the previous one, all pending state change awaiters are resolved - either unconditionally or if their custom request criteria are satisfied.
- waitForChange: Awaits a state change. Optionally, a filtering request can be provided to wait only for relevant state transitions.
- rejectAllStateChangeAwaiters: Rejects all current
waitForChange
awaiters with the provided error. This method can be used for error propagation or teardown procedures, allowing all awaiting consumers to be notified of failure or cancellation.
If needed, refer to the code documentation for a more comprehensive description of each method.
In systems where a component can be started and stopped multiple times, each transition often requires an asynchronous process - such as opening or closing network connections, initializing resources, or flushing buffers.
StateChangeNotifier
can coordinate these state transitions safely. For instance, if a consumer attempts to start()
the service while it is in the process of stopping, the notifier ensures the consumer waits until the stop process is complete before initiating the start sequence. This prevents race conditions, ensures consistent state, and reduces the need for manual state checks.
import {
ICustomStateChangeRequest,
StateChangeNotifier
} from 'state-change-notifier';
type LifecycleState =
| 'INACTIVE'
| 'STARTING'
| 'ACTIVE'
| 'STOPPING';
class ToggleableComponent {
// Initialized with the initial state.
private readonly _stateNotifier = new StateChangeNotifier<LifecycleState>('INACTIVE');
public async start(): Promise<void> {
if (this._stateNotifier.state === 'ACTIVE') {
return; // Already active - no action needed.
}
if (this._stateNotifier.state === 'STARTING') {
// Wait for the ongoing initialization to complete.
return this._stateNotifier.waitForChange();
}
if (this._stateNotifier.state === 'STOPPING') {
// Gracefully wait for teardown to complete *before* reinitializing.
// This avoids conflicting operations like opening and closing
// the same network connection simultaneously.
await this._stateNotifier.waitForChange();
}
// We are now in the 'INACTIVE' state.
this._stateNotifier.state = 'STARTING';
try {
await this._establishConnection();
} catch (err) {
this._stateNotifier.rejectAllStateChangeAwaiters(err);
this._stateNotifier.state = 'INACTIVE';
throw err;
}
this._stateNotifier.state = 'ACTIVE';
}
public async stop(): Promise<void> {
if (this._stateNotifier.state === 'INACTIVE') {
return; // Already inactive - no action needed.
}
if (this._stateNotifier.state === 'STOPPING') {
// Wait for the ongoing teardown to complete.
return this._stateNotifier.waitForChange();
}
if (this._stateNotifier.state === 'STARTING') {
// Gracefully wait for initialization to finish before stopping.
// This avoids conflicting operations like opening and closing
// the same network connection simultaneously.
await this._stateNotifier.waitForChange();
}
// We are now in the 'ACTIVE' state.
this._stateNotifier.state = 'STOPPING';
try {
await this._closeConnection();
} catch (err) {
this._stateNotifier.rejectAllStateChangeAwaiters(err);
// Note: in real-world scenarios, a failed teardown may warrant
// a dedicated 'FAILED' or 'ERROR' state to represent uncertainty.
// For simplicity, we revert to 'ACTIVE'.
this._stateNotifier.state = 'ACTIVE';
throw err;
}
this._stateNotifier.state = 'INACTIVE';
}
private _establishConnection(): Promise<void> {
// Connects to an external resource (e.g., a database or socket).
}
private _closeConnection(): Promise<void> {
// Gracefully closes the connection to an external resource.
}
}