Registry Pattern

The Registry Pattern is a lightweight utility for managing resource lifecycle and cleanup operations in StateWalker applications. It centralizes resource teardown by tracking cleanup functions and executing them in proper order, preventing memory leaks and ensuring graceful resource disposal.

Type Definition

/**
 * Creates a registry for managing cleanup functions with guaranteed execution order and error handling.
 * Provides centralized resource lifecycle management to prevent memory leaks and ensure proper cleanup.
 * 
 * @template E - Type of errors that can be thrown during cleanup (defaults to unknown)
 * @param onError - Optional error handler called when cleanup functions throw errors
 * @returns Tuple containing [register, cleanup] functions
 */
function newRegistry<E = unknown>(
  onError?: (error: E) => void,
): [
  /** 
   * Registers a cleanup function to be executed during cleanup phase.
   * Functions are executed in LIFO (Last In, First Out) order.
   * @param cleanup - Function to execute during cleanup (can be sync or async)
   * @returns Cleanup function to remove this registration (optional)
   */
  register: (cleanup?: () => void | Promise<void>) => () => Promise<void>,
  
  /** 
   * Executes all registered cleanup functions in reverse order.
   * Continues execution even if individual cleanup functions throw errors.
   * @returns Promise that resolves when all cleanup functions complete
   */
  cleanup: () => Promise<void>,
];

Key Features:

Usage Patterns

Basic resource management:

const [register, cleanup] = newRegistry();

// Register resources as they're created
const timer = setInterval(() => console.log('tick'), 1000);
register(() => clearInterval(timer));

const button = document.getElementById('myButton');
const handler = () => console.log('clicked');
button.addEventListener('click', handler);
register(() => button.removeEventListener('click', handler));

// Clean up all resources
await cleanup();

Database connection management:

class DatabaseManager {
  constructor() {
    this.connections = new Map();
    [this.register, this.cleanup] = newRegistry();
  }
  
  async createConnection(name, config) {
    const connection = await connectToDatabase(config);
    this.connections.set(name, connection);
    
    this.register(() => {
      console.log(`Closing connection: ${name}`);
      return connection.close();
    });
    
    return connection;
  }
  
  async shutdown() {
    await this.cleanup();
    this.connections.clear();
  }
}

Observable subscription management:

class EventProcessor {
  constructor() {
    [this.register, this.cleanup] = newRegistry();
  }
  
  subscribe(observable, handler) {
    const subscription = observable.subscribe(handler);
    
    this.register(() => {
      console.log('Unsubscribing from observable');
      subscription.unsubscribe();
    });
    
    return subscription;
  }
  
  async dispose() {
    await this.cleanup();
  }
}

Implementation

/**
 * Creates a new registry that allows registering and cleaning up resources or listeners.
 * 
 * The registry provides two functions:
 * - `register`: Registers a cleanup function that will be called when the registry is cleaned up.
 * - `cleanup`: Cleans up all registered resources in reverse order of their registration.
 * 
 * @typeParam E - The type of error that may be passed to the `onError` callback.
 * 
 * @param onError - A callback function to handle errors that occur during cleanup. 
 *                  Defaults to `console.error` if not provided.
 * 
 * @returns A tuple containing:
 * - `register`: A function to register a cleanup listener. It returns a function that can be called to clean up the specific registration.
 * - `cleanup`: A function to clean up all registered listeners in reverse order of their registration.
 * 
 * @example
 * ```typescript
 * const [register, cleanup] = newRegistry();
 * 
 * const unregister = register(async () => {
 *   console.log('Cleaning up resource');
 * });
 * 
 * await unregister(); // Cleans up the specific registration
 * 
 * await cleanup(); // Cleans up all remaining registrations
 * ```
 */
// See https://dots.statewalker.com/registry/index.html
export function newRegistry<E = unknown>(
  onError: (error: E) => void = console.error,
): [
  register: (callback?: () => void | Promise<void>) => () => Promise<void>,
  cleanup: () => Promise<void>,
] {
  const registrationsIndex: {
    [registrationId: number]: () => Promise<void>;
  } = {};
  let registrationId = 0;
  return [
    function register(
      listener?: () => void | Promise<void>,
    ): () => Promise<void> {
      const id = registrationId++;
      const cleanup = async () => {
        if (!(id in registrationsIndex)) {
          return;
        }
        delete registrationsIndex[id];
        try {
          await listener?.();
        } catch (e) {
          onError(e as E);
        }
      };
      registrationsIndex[id] = cleanup;
      return cleanup;
    },
    async function cleanup(): Promise<void> {
      await Promise.all(
        Object.values(registrationsIndex)
          .reverse()
          .map((registration) => registration()),
      );
    },
  ];
}

Common Use Cases

This pattern excels when you need to: