Skip to content

A lightweight utility for awaiting state transitions in a type-safe, asynchronous manner - ideal for coordinating state-dependent workflows such as lifecycle management or idempotent initialization. Supports custom state change requests (inclusive or exclusive) and allows globally rejecting all awaiting consumers when needed.

License

Notifications You must be signed in to change notification settings

ori88c/state-change-notifier

Folders and files

NameName
Last commit message
Last commit date

Latest commit

 

History

1 Commit
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 

Repository files navigation

state-change-notifier

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.

Table of Contents

✨ Key Features

  • 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 optional ICustomStateChangeRequest argument of the waitForChange 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.

🌐 API

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.

⏯️ Use Case Example: Lifecycle Management of a Toggleable Component

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.
  }
}

📜 License

Apache 2.0

About

A lightweight utility for awaiting state transitions in a type-safe, asynchronous manner - ideal for coordinating state-dependent workflows such as lifecycle management or idempotent initialization. Supports custom state change requests (inclusive or exclusive) and allows globally rejecting all awaiting consumers when needed.

Topics

Resources

License

Stars

Watchers

Forks

Packages

No packages published