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 🙏

© 2026 – Pkg Stats / Ryan Hefner

@apihive/signals

v1.0.7

Published

<p align="center"> <img src="./logo.png" alt="APIHive Signals logo" width="120" /> </p>

Readme

APIHive Signals (formerly Signals.ts) is a modern, TypeScript-first pub-sub messaging system that enables completely decoupled communication between components. Built for applications that need reliable, type-safe message passing without tight coupling.

Breaking change from v1.0.4 to v1.0.5: Signals now resolve to a single value instead of an array when only one listener is attached.

Why Signals?

Event systems in web applications are nearly always embedded in the visual components, which forces the propagation and handling of events to be tied to the visual hierarchy. This works for many use cases but it fails to provide the flexibility needed when dealing with cross cutting concerns. They also have some overhead because the events need to travel up and down the visual hierarchy to reach their targets.

Furthermore they lack asyncronous bidirectional communication capabilities, which is often needed in modern applications.

Signals act as decoupled intermediaries between components, enabling bidirectional communication without tight coupling.

Inspired by JS-Signals and AS3-Signals, but built from the ground up for modern TypeScript applications.

Feature Highlights

  • Type-safe messaging

    • Generic payload types with full IntelliSense support
    • Typed return values from listeners
    • Compile-time safety for message contracts
  • Advanced listener control

    • Priority-based execution order
    • One-time listeners with addOnce()
    • Suspend/resume individual listeners
    • Binding management with cleanup
  • Flexible propagation modes

    • all — Wait for all listeners (default)
    • any — Stop after first successful response
    • any-fail — Stop after first failure
    • none — Expect all listeners to fail
  • Async-first design

    • Promise-based dispatch with await
    • Mixed sync/async listener support
    • Sequential execution with proper error handling
  • Memory and lifecycle management

    • Signal memoization for late subscribers
    • Automatic cleanup and disposal
    • Signal suspension for temporary disabling

Installation

Using npm:

npm install @apihive/signals

Using yarn:

yarn add @apihive/signals

Using pnpm:

pnpm add @apihive/signals

Quick Start

Basic messaging

import { Signal } from '@apihive/signals';

// Create a signal
const userLoggedIn = new Signal<{ userId: string, username: string }>();

// Add a listener
userLoggedIn.add((user) => {
  console.log(`Welcome back, ${user.username}!`);
});

// Dispatch the signal
userLoggedIn.dispatch({ userId: '123', username: 'alice' });

Request-response pattern

const showConfirmDialog = new Signal<{ 
  message: string, 
  title?: string 
}, boolean>();

// Listener returns a response
showConfirmDialog.add(async ({ message, title = 'Confirm' }) => {
  return window.confirm(`${title}: ${message}`);
});

// Dispatcher awaits the response
const confirmed = await showConfirmDialog.dispatch({
  message: 'Delete this item?',
  title: 'Are you sure?'
});

if (confirmed) {
  // Proceed with deletion
}

Application lifecycle with memoization

// Signal that remembers its last dispatch
const appInitialized = new Signal<void>({ memoize: true });

// Bootstrap process
async function bootstrap() {
  await loadConfig();
  await connectDatabase();
  appInitialized.dispatch(); // Signal initialization complete
}

// Components can listen even after initialization
appInitialized.addOnce(() => {
  // This will fire immediately if app is already initialized
  startPeriodicTasks();
});

Real-World Examples

E-commerce Cart System

// Define your signals
const cartSignals = {
  itemAdded: new Signal<{ productId: string, quantity: number }>(),
  itemRemoved: new Signal<{ productId: string }>(),
  cartCleared: new Signal<void>(),
  checkoutStarted: new Signal<{ cartTotal: number }, boolean>()
};

// Cart component listens and updates UI
cartSignals.itemAdded.add(({ productId, quantity }) => {
  updateCartBadge();
  showNotification(`Added ${quantity}x ${productId} to cart`);
});

// Inventory component listens and updates stock
cartSignals.itemAdded.add(({ productId, quantity }) => {
  updateInventoryCount(productId, -quantity);
});

// Analytics component tracks events
cartSignals.itemAdded.add(({ productId, quantity }) => {
  analytics.track('cart_item_added', { productId, quantity });
});

// Checkout validation
cartSignals.checkoutStarted.add(async ({ cartTotal }) => {
  const hasValidPayment = await validatePaymentMethod();
  const hasStock = await validateInventory();
  return hasValidPayment && hasStock;
});

// Usage
cartSignals.itemAdded.dispatch({ productId: 'laptop-123', quantity: 1 });

const canProceed = await cartSignals.checkoutStarted.dispatch({ 
  cartTotal: 1299.99 
});

Game Event System

interface GameEvents {
  playerMoved: Signal<{ x: number, y: number, playerId: string }>;
  enemyDefeated: Signal<{ enemyType: string, experience: number }>;
  levelCompleted: Signal<{ level: number, score: number }>;
  gameOver: Signal<{ finalScore: number, reason: string }>;
}

const game: GameEvents = {
  playerMoved: new Signal(),
  enemyDefeated: new Signal(),
  levelCompleted: new Signal(),
  gameOver: new Signal({ memoize: true }) // Remember game over state
};

// Multiple systems can react independently
game.enemyDefeated.add(({ experience }) => {
  player.addExperience(experience);
});

game.enemyDefeated.add(({ enemyType }) => {
  achievements.checkEnemyKillAchievements(enemyType);
});

game.enemyDefeated.add(({ enemyType }) => {
  audioSystem.playSound(`${enemyType}_death`);
});

// Priority-based listeners (higher priority = earlier execution)
game.levelCompleted.add(() => {
  saveGameState(); // Critical - save first
}, null, 10);

game.levelCompleted.add(() => {
  showLevelCompleteAnimation(); // Visual feedback after save
}, null, 5);

Form Validation Pipeline

interface ValidationResult {
  isValid: boolean;
  errors: string[];
}

const formValidation = new Signal<
  { field: string, value: any }, 
  ValidationResult
>({ 
  propagate: 'any-fail', // Stop on first validation failure
  listenerSuccessTest: (result: ValidationResult) => result.isValid 
});

// Add validators with priority
formValidation.add(({ field, value }) => {
  // Required field validation (highest priority)
  if (!value || value.trim() === '') {
    return { isValid: false, errors: [`${field} is required`] };
  }
  return { isValid: true, errors: [] };
}, null, 10);

formValidation.add(({ field, value }) => {
  // Email format validation
  if (field === 'email' && !isValidEmail(value)) {
    return { isValid: false, errors: ['Invalid email format'] };
  }
  return { isValid: true, errors: [] };
}, null, 5);

// Usage
const result = await formValidation.dispatch({ 
  field: 'email', 
  value: '[email protected]' 
});

if (!result.isValid) {
  displayErrors(result.errors);
}

Advanced Configuration

Propagation Control

// Stop after first successful response
const findHandler = new Signal<string, boolean>({ 
  propagate: 'any',
  haltOnResolve: true 
});

// Stop after first failure
const validateAll = new Signal<any, boolean>({ 
  propagate: 'any-fail',
  haltOnResolve: true,
  listenerSuccessTest: (result) => result === true
});

// Wait for all listeners regardless of results
const notifyAll = new Signal<string>({ 
  propagate: 'all',
  haltOnResolve: false 
});

Listener Management

const signal = new Signal<string>();

// Add listener with binding control
const binding = signal.add((message) => {
  console.log(message);
}, null, 5); // priority 5

// Temporarily suspend
binding.suspend();
signal.dispatch('This will not be logged');

// Resume
binding.resume();
signal.dispatch('This will be logged');

// Permanently remove
binding.detach();
signal.dispatch('This will not be logged');

// Check if listener exists
const hasListener = signal.has(myFunction, myContext);

Signal Lifecycle

const signal = new Signal<string>({ memoize: true });

// Dispatch early
signal.dispatch('Early message');

// Late subscriber still receives the message
signal.addOnce((message) => {
  console.log(message); // Logs: "Early message"
});

// Clear memoized value
signal.forget();

// Suspend entire signal
signal.suspend();
signal.dispatch('Will be rejected'); // Promise rejection

signal.resume();
signal.dispatch('Will work'); // Normal dispatch

// Clean up
signal.dispose(); // Removes all listeners and clears memory

TypeScript Integration

Signals provides excellent TypeScript support with full type inference:

// Payload and return types are fully typed
const typedSignal = new Signal<
  { userId: string, action: 'login' | 'logout' },
  { success: boolean, timestamp: number }
>();

// TypeScript knows the parameter types
typedSignal.add((data) => {
  // data is typed as { userId: string, action: 'login' | 'logout' }
  return { 
    success: true, 
    timestamp: Date.now() 
  }; // Return type is enforced
});

// Dispatch is type-safe
const result = await typedSignal.dispatch({
  userId: '123',
  action: 'login' // Only 'login' or 'logout' allowed
});

// Result is typed as { success: boolean, timestamp: number }
console.log(result.timestamp);

Performance Considerations

  • Lightweight: Zero dependencies, minimal runtime overhead
  • Efficient: Listeners are sorted by priority only when needed
  • Memory-conscious: Automatic cleanup of one-time listeners
  • Async-optimized: Non-blocking execution with proper Promise handling

Migration Guide

From v1.0.4 to v1.0.5

The main breaking change is in return value handling:

// v1.0.4 - always returned array
const results = await signal.dispatch(); // Always array

// v1.0.5 - single value when one listener
const result = await signal.dispatch(); // Single value if one listener
const results = await signal.dispatch(); // Array if multiple listeners

Contributing

Contributions are welcome! Please feel free to submit issues, feature requests, or pull requests.

License

Apache 2.0 License

Running Tests

yarn install
yarn test