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

ts-signals

v0.1.0

Published

A typed signal/event system for TypeScript

Readme

ts-signals

A lightweight, strongly-typed signal / event system for TypeScript with mandatory context binding, one-time handlers, and grouped removal by owner.

Designed for OOP-style lifecycle-driven code, not for reactive streams or global event buses.

Installation

npm install ts-signals

Why ts-signals?

ts-signals exists to solve a specific problem:

Managing event subscriptions in object-oriented code without losing track of ownership, lifecycle, or cleanup.

Intended use cases

  • Game engines (PixiJS, Phaser, custom engines)
  • UI systems with explicit object lifecycles
  • Entity / component architectures
  • Systems where objects own their subscriptions

Explicit non-goals

This library is not intended to replace:

  • RxJS (no streams, operators, async composition)
  • Node.js EventEmitter (no string-based events)
  • Global pub/sub or message buses
  • React-style functional event handling

If you need functional, stateless, or reactive patterns — use a different tool.

Core design principles

  • Context is mandatory
    • Every handler is bound to an owner object
    • The same context is used as this and as a lifecycle key
  • Ownership over convenience
    • Subscriptions are grouped by owner, not anonymous callbacks
  • Synchronous and predictable
    • Handlers are executed synchronously, in subscription order
  • Explicit cleanup
    • No automatic garbage collection of handlers
    • You must remove handlers or contexts explicitly

This is a deliberate trade-off in favor of clarity and control.

Basic usage

import { Signal } from 'ts-signals';

class GameScene {
  onScore = new Signal<number>();

  setup(player: Player) {
    this.onScore.add(player.onScoreChanged, player);
  }

  teardown(player: Player) {
    // Remove one specific handler
    this.onScore.remove(player.onScoreChanged, player);

    // Or remove all handlers registered by player at once
    this.onScore.removeContext(player);
  }
}

One-time handler

const onReady = new Signal<void>();

onReady.addOnce(scene.init, scene);
onReady.emit(); // scene.init fires once, then removed
onReady.emit(); // no output

Unsubscribe via returned function

const off = signal.add(player.onDamage, player);

// later...
off(); // equivalent to signal.remove(player.onDamage, player)

Using the handler type

import { Signal, SignalHandler } from 'ts-signals';

const onScore = new Signal<number>();

const handler: SignalHandler<number> = function (this: Player, score: number) {
  console.log(`${this.name} scored: ${score}`);
};

onScore.add(handler, player);
onScore.emit(42);
onScore.remove(handler, player);

API

new Signal<T>()

Creates a new signal.

  • T — type of data passed to handlers
  • Defaults to void

signal.add(handler, context): () => void

Subscribe a handler.

  • handler — (data: T) => void
  • context — owner object

Used as:

  • this binding when calling the handler
  • grouping key for removeContext()
  • Returns an unsubscribe function

Throws if handler is not a function.

signal.addOnce(handler, context): () => void

Subscribe a handler that fires only once and is then removed.

Throws if handler is not a function.

signal.emit(data: T): void

Emit the signal.

  • Regular handlers fire first, followed by one-time handlers
  • Handlers are executed synchronously
  • Execution order:
    • insertion order per context
    • no guaranteed ordering across different contexts
  • Exceptions are not caught internally

signal.remove(handler, context): void

Remove a specific handler. The same context must be provided.

signal.removeContext(context): void

Remove all handlers registered under a context.

This is the primary cleanup mechanism and should be called when an object is destroyed.

signal.removeAll(): void

Remove all handlers (regular and one-time).

signal.has(handler, context): boolean

Returns true if the given handler is currently subscribed under the given context (regular or one-time).

signal.add(player.onDamage, player);
signal.has(player.onDamage, player); // true
signal.remove(player.onDamage, player);
signal.has(player.onDamage, player); // false

signal.hasContext(context): boolean

Returns true if there are any handlers (regular or one-time) registered under the given context.

signal.add(player.onDamage, player);
signal.hasContext(player); // true
signal.removeContext(player);
signal.hasContext(player); // false

signal.contexts(): Iterable<object>

Returns an iterable of all unique context objects that currently have at least one registered handler. The returned snapshot is not live.

signal.add(player.onDamage, player);
signal.add(enemy.onDamage, enemy);
for (const ctx of signal.contexts()) { ... } // player, enemy

signal.listenerCount(): number

Returns the total number of active handlers (regular + one-time). Useful for debugging and leak detection.

signal.listenerCountFor(context): number

Returns the number of handlers (regular + one-time) registered under a specific context.

signal.add(player.onDamage, player);
signal.addOnce(player.onReady, player);
signal.listenerCountFor(player); // 2

Memory & lifecycle model

ts-signals uses strong references to contexts. This enables:

  • explicit ownership
  • predictable cleanup
  • debugging and introspection

As a result, handlers are not automatically garbage-collected. Objects that subscribe to signals are expected to explicitly remove their handlers or call removeContext() as part of their lifecycle.

This mirrors the behavior of DOM events and other ownership-based systems and is a deliberate trade-off in favor of control and debuggability.

License

MIT