npm package discovery and stats viewer.

Discover Tips

  • General search

    [free text search, go nuts!]

  • Package details

    pkg:[package-name]

  • User packages

    @[username]

Sponsor

Optimize Toolset

I’ve always been into building performant and accessible sites, but lately I’ve been taking it extremely seriously. So much so that I’ve been building a tool to help me optimize and monitor the sites that I build to make sure that I’m making an attempt to offer the best experience to those who visit them. If you’re into performant, accessible and SEO friendly sites, you might like it too! You can check it out at Optimize Toolset.

About

Hi, 👋, I’m Ryan Hefner  and I built this site for me, and you! The goal of this site was to provide an easy way for me to check the stats on my npm packages, both for prioritizing issues and updates, and to give me a little kick in the pants to keep up on stuff.

As I was building it, I realized that I was actually using the tool to build the tool, and figured I might as well put this out there and hopefully others will find it to be a fast and useful way to search and browse npm packages as I have.

If you’re interested in other things I’m working on, follow me on Twitter or check out the open source projects I’ve been publishing on GitHub.

I am also working on a Twitter bot for this site to tweet the most popular, newest, random packages from npm. Please follow that account now and it will start sending out packages soon–ish.

Open Software & Tools

This site wouldn’t be possible without the immense generosity and tireless efforts from the people who make contributions to the world and share their work via open source initiatives. Thank you 🙏

© 2025 – Pkg Stats / Ryan Hefner

mach9-signals

v1.1.0

Published

A comprehensive, type-safe signal-slot communication system for TypeScript

Downloads

7

Readme

mach9-signals

CI npm version TypeScript License: MIT

A comprehensive, type-safe signal-slot communication system that combines the best practices from various signal implementations. Designed for modern TypeScript applications requiring robust inter-component communication.

✨ Features

  • 🔒 Type Safety: Full TypeScript support with generics for void, single, and multiple parameters
  • 🎯 Priority System: Control execution order with connection priorities
  • ⚡ Performance: Batch operations, freeze/unfreeze, and minimal overhead
  • 🧹 Memory Management: Automatic disposal integration with leak detection
  • 🔧 Advanced Features: One-time connections, return value aggregation, parameter validation
  • 📊 Monitoring: Built-in performance monitoring and debugging utilities
  • 🏗️ Flexible Architecture: Simple API for basic usage, advanced features when needed

📋 Table of Contents

📦 Installation

npm install mach9-signals
# or
yarn add mach9-signals
# or
pnpm add mach9-signals

🚀 Quick Start

import { createSignal } from "mach9-signals";

// Create a signal
const onUserLogin = createSignal<string>();

// Connect a listener
const connection = onUserLogin.connect((username) => {
  console.log(`Welcome, ${username}!`);
});

// Emit the signal
onUserLogin.emit("john.doe");

// Clean up
connection.detach();

🎯 Core Concepts

Signals

Signals are the core communication mechanism. They can carry no data (void), single values, or multiple parameters using tuple types.

import { createSignal, SignalPriority } from "@signals";

// Void signal (no data)
const onReady = createSignal();

// Single parameter
const onUserLogin = createSignal<string>();

// Multiple parameters (use tuple types)
const onFileOperation = createSignal<[string, "read" | "write", boolean]>();

// Complex payload objects
const onDataSync = createSignal<{
  timestamp: Date;
  recordCount: number;
  success: boolean;
}>();

Connections

Connections link callbacks to signals and provide granular control over execution.

// Basic connection
const connection = signal.connect(callback);

// With context binding (recommended)
const connection = signal.connect(callback, this);

// With options
const connection = signal.connectWithOptions(callback, {
  priority: SignalPriority.HIGH,
  once: true,
  context: this,
  active: true,
});

// One-time connection (auto-disconnects after first execution)
const connection = signal.connectOnce(callback, this);

// Priority-based connection
const connection = signal.connectWithPriority(callback, SignalPriority.HIGH, this);

Signal Results

Signal emissions return result objects with utilities for working with callback return values.

const onDataValidation = createSignal<string>();

// Add validation callbacks
onDataValidation.connect((data) => data.length > 0);
onDataValidation.connect((data) => /^[a-zA-Z0-9]+$/.test(data));

// Emit and process results
const result = onDataValidation.emit("testData");

console.log("All validations passed:", result.every(Boolean));
console.log(
  "Any validation failed:",
  result.some((r) => !r),
);
console.log(
  "First failure:",
  result.findResult((r) => !r),
);

📚 API Reference

Signal Class

Creation

import { createSignal, Signal, Signals } from "@signals";

// Factory function (recommended)
const signal = createSignal<DataType>();

// Constructor
const signal = new Signal<DataType>({ debugName: "MySignal" });

// Using Signals utility
const signal = Signals.create<DataType>({ debugName: "MySignal" });

Connection Methods

// Simple API (covers 80% of use cases)
connect(callback: SignalCallback<T>): ISignalConnection<T>
connect(callback: SignalCallback<T>, context: object): ISignalConnection<T>
disconnect(connection: ISignalConnection<T>): void
disconnectAll(): void

// Advanced API
connectWithOptions(callback: SignalCallback<T>, options: ConnectionOptions): ISignalConnection<T>
connectOnce(callback: SignalCallback<T>, context?: object): ISignalConnection<T>
connectWithPriority(callback: SignalCallback<T>, priority: number, context?: object): ISignalConnection<T>
hasConnection(callback: SignalCallback<T>, context?: object): boolean
getConnections(): ReadonlyArray<ISignalConnection<T>>

Emission Methods

emit(data: T): SignalResult<T>

Batch Operations

freeze(): void                    // Disable emission temporarily
unfreeze(): void                 // Re-enable emission
withFrozen<R>(fn: () => R): R    // Execute function with signal frozen

Lifecycle & State

setLifecycleHandlers(handlers: ConnectionLifecycle): void
readonly connectionCount: number
readonly isFrozen: boolean
readonly isDisposed: boolean
dispose(): void

SignalConnection Class

interface ISignalConnection<T> {
  readonly signal: ISignal<T>;
  readonly callback: SignalCallback<T>;
  readonly context?: object;
  readonly priority: number;
  readonly isOnce: boolean;
  active: boolean;

  detach(): void;
  execute(data: T): any;
  isBound(): boolean;
}

Disposal Integration

import { createDisposableSignal, CompositeDisposable, SignalManager } from "@signals";

// Disposable signals
const signal = createDisposableSignal<string>();
const disposable = new CompositeDisposable();

// Auto-cleanup connection
signal.connectTo(disposable, callback, this);

// Auto-cleanup entire signal
signal.addTo(disposable);

// Signal manager for bulk operations
const manager = new SignalManager();
const signal1 = manager.createSignal<string>();
const signal2 = manager.createSignal<number>();

// Dispose all at once
manager.dispose();

🔧 Advanced Patterns

Component Architecture

export class ComponentSystem {
  public readonly signals = {
    componentAdded: createSignal<Component>(),
    componentRemoved: createSignal<Component>(),
    componentUpdated: createSignal<[Component, string]>(),
  };

  private components = new Map<string, Component>();
  private disposable = new CompositeDisposable();

  addComponent(component: Component): void {
    this.components.set(component.id, component);

    // Chain component signals
    component.signals.updated.connectTo(this.disposable, (prop: string) =>
      this.signals.componentUpdated.emit([component, prop]),
    );

    this.signals.componentAdded.emit(component);
  }

  dispose(): void {
    this.disposable.dispose();
    // ... cleanup
  }
}

Settings System with Cascading

export class SettingsSystem {
  private manager = Signals.createManager();

  public readonly graphics = this.createSettingsGroup("graphics", {
    resolution: "1920x1080",
    quality: "high",
  });

  public readonly audio = this.createSettingsGroup("audio", {
    masterVolume: 0.8,
    sfxVolume: 0.7,
  });

  // Global change signal
  public readonly onChange = this.manager.createSignal<[string, string, any]>();

  // Batch update with frozen signals
  updateSettings(updates: Array<{ group: string; key: string; value: any }>): void {
    const batch = createBatch()
      .addSignal(this.graphics.onChange)
      .addSignal(this.audio.onChange)
      .addSignal(this.onChange);

    for (const update of updates) {
      batch.addOperation(() => {
        const group = this.getGroup(update.group);
        group.set(update.key, update.value);
      });
    }

    batch.execute(); // All signals frozen during updates
  }
}

State Machine

export class StateMachine<TState extends string, TEvent extends string> {
  public readonly onStateChange = createSignal<[TState, TState, TEvent]>();
  public readonly onTransition = createSignal<[TState, TEvent, TState]>();

  private currentState: TState;
  private transitions = new Map<string, TState>();

  trigger(event: TEvent): boolean {
    const key = `${this.currentState}:${event}`;
    const newState = this.transitions.get(key);

    if (!newState) return false;

    const oldState = this.currentState;
    this.currentState = newState;

    this.onTransition.emit([oldState, event, newState]);
    this.onStateChange.emit([oldState, newState, event]);

    return true;
  }
}

📊 Performance & Debugging

Performance Monitoring

import { SignalDebugger, SignalPerformanceMonitor } from "@signals";

// Enable global monitoring
Signals.enableMonitoring();

// Create debug-enabled signals
const signal = Signals.createDebug<string>("DataProcessing");

// Monitor performance
const monitor = SignalPerformanceMonitor.getInstance();
monitor.enable();

// Get performance stats
const stats = monitor.getStats("DataProcessing");
console.log("Average emit time:", stats.averageEmitTime);

// Report slow signals
const slowSignals = monitor.reportSlowSignals(5); // > 5ms threshold

Memory Leak Detection

import { SignalLeakDetector, SignalValidator } from "@signals";

const detector = SignalLeakDetector.getInstance();
detector.setMaxConnections(50);

// Track signal for leaks
detector.trackSignal(signal, "MySignal");

// Check for potential leaks
const leaks = detector.reportPotentialLeaks();
console.log("Potential leaks:", leaks);

// Validate signal health
const validation = SignalValidator.validateSignal(signal);
if (!validation.isValid) {
  console.warn("Signal issues:", validation.issues);
}

Debug Utilities

// Print comprehensive debug info
SignalDebugger.debugInfo(signal);

// Global monitoring
SignalDebugger.startGlobalMonitoring();

// Create debug signal with logging
const debugSignal = SignalDebugger.createDebugSignal<string>("TestSignal");

🏆 Best Practices

1. Always Use Context Binding

// ✅ Good - proper context binding
signal.connect(this.handleEvent, this);

// ❌ Bad - no context binding (memory leak risk)
signal.connect(this.handleEvent);

2. Implement Proper Cleanup

export class Component {
  private disposable = new CompositeDisposable();

  constructor() {
    // Use disposal integration
    signal.connectTo(this.disposable, this.handleEvent, this);
  }

  dispose(): void {
    this.disposable.dispose(); // Cleans up all connections
  }
}

3. Use Appropriate Signal Types

// ✅ Good - specific types
const onUserLogin = createSignal<string>();
const onFileOp = createSignal<[string, "read" | "write", boolean]>();

// ❌ Bad - any or overly broad types
const onEvent = createSignal<any>();

4. Leverage Batch Operations

// For multiple related changes
signal.withFrozen(() => {
  // Multiple operations that should be batched
  this.updateProperty1();
  this.updateProperty2();
  this.updateProperty3();
}); // Single emission after all changes

5. Use Descriptive Names

// ✅ Good - clear purpose
const onUserAuthenticated = createSignal<User>();
const onDataValidationFailed = createSignal<ValidationError[]>();

// ❌ Bad - unclear purpose
const onChange = createSignal<any>();
const signal1 = createSignal();

6. Handle Errors Gracefully

signal.connect((data) => {
  try {
    this.processData(data);
  } catch (error) {
    console.error("Error processing data:", error);
    this.errorSignal.emit(error);
  }
}, this);

🔄 Migration Guide

From Custom Event Systems

// Old event system
class EventEmitter {
  on(event: string, callback: Function): void;
  emit(event: string, data?: any): void;
  off(event: string, callback: Function): void;
}

// New signal system
class SignalSystem {
  readonly signals = {
    userLogin: createSignal<string>(),
    dataUpdated: createSignal<Data>(),
  };
}

From Observer Pattern

// Old observer pattern
interface Observer {
  update(data: any): void;
}

class Subject {
  private observers: Observer[] = [];
  attach(observer: Observer): void {}
  notify(data: any): void {}
}

// New signal system
const dataChanged = createSignal<Data>();
const connection = dataChanged.connect((data) => this.update(data), this);

📖 Examples

Basic Usage

import { createSignal } from "@signals";

// Simple void signal
const onReady = createSignal();
onReady.connect(() => console.log("Ready!"));
onReady.emit();

// Single parameter
const onMessage = createSignal<string>();
onMessage.connect((msg) => console.log(`Message: ${msg}`));
onMessage.emit("Hello, World!");

// Multiple parameters
const onFileOp = createSignal<[string, boolean]>();
onFileOp.connect((filename, success) => {
  console.log(`File ${filename}: ${success ? "OK" : "FAILED"}`);
});
onFileOp.emit(["document.txt", true]);

Priority and One-time Connections

const onShutdown = createSignal();

// High priority - critical cleanup first
onShutdown.connectWithPriority(() => {
  console.log("1. Saving critical data...");
}, SignalPriority.HIGH);

// Normal priority
onShutdown.connect(() => {
  console.log("2. Regular cleanup...");
});

// One-time connection
onShutdown.connectOnce(() => {
  console.log("3. First shutdown only");
});

onShutdown.emit(); // Executes in priority order
onShutdown.emit(); // One-time connection won't execute again

Real-world Component System

See examples/advanced-patterns.ts for comprehensive real-world examples including:

  • Component-based architecture with signal hubs
  • Settings systems with cascading changes
  • Event-driven state machines
  • Command systems with undo/redo
  • Game engine architecture
  • Performance monitoring examples

📈 Performance Characteristics

  • Connection overhead: ~0.1ms per connection
  • Emission overhead: ~0.05ms base + ~0.01ms per connection
  • Memory usage: ~100 bytes per connection
  • Type safety: Zero runtime overhead (compile-time only)

🤝 Contributing

  1. Fork the repository
  2. Create a feature branch
  3. Add tests for new functionality
  4. Ensure all tests pass
  5. Submit a pull request

📄 License

MIT License - see LICENSE for details.

🙏 Acknowledgments

This signal system incorporates best practices and lessons learned from:

  • Vectary Signal System: Type-safe design, freeze/unfreeze operations, connection monitoring
  • JS-Signals Library: Priority system, memorization, robust connection management
  • Qt Signals/Slots: Original signal-slot pattern concepts
  • Event-Kit: Disposal integration patterns

Made with ❤️ for robust TypeScript applications