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

ssignal

v1.6.0

Published

Lightweight zero-dependency reactive signal built on the native EventTarget API. Supports any value type, reactive Map mutations, updater functions, and AbortSignal integration.

Downloads

781

Readme

SSignal

npm version npm downloads bundle size License: MIT Node.js TypeScript GitHub stars GitHub issues

A lightweight, zero-dependency reactive signal built on top of the native EventTarget API. SSignal lets you observe value changes on any data type — including deep mutations on Map instances — without a framework, build plugin, or compiler transform.

Features

  • Simple APIvalue, subscribe, and an unsubscribe function. That's it.
  • Framework-agnostic — works in the browser, Node.js ≥ 18.7, and any runtime that supports EventTarget.
  • Reactive Map support — mutations via set(), delete(), and clear() automatically dispatch change events.
  • Updater functionssignal.value = (prev) => prev + 1 for safe derived updates.
  • Immediate mode{ immediate: true } fires the callback with the current value on subscribe.
  • One-time subscriptionsonce() listens for the next change only, then unsubscribes itself.
  • Computed signals — derive read-only signals from one or more sources with computed().
  • AbortSignal integration — cancel subscriptions with a standard AbortController.
  • TypeScript-first — fully typed, zero any in the public API.
  • Tree-shakeablesideEffects: false, ships ESM + CJS + UMD.

Installation

npm install ssignal

CDN (browser)

<script src="https://unpkg.com/ssignal@latest/lib/ssignal.umd.js"></script>

API

| Member | Description | | :----- | :---------- | | new SSignal(value: T) | Creates a signal. Map values are automatically wrapped in a reactive proxy. | | signal.value | Gets the current value. | | signal.value = newValue \| (prev: T) => T | Sets a new value. Accepts a direct value or an updater function. No event is fired when the value does not change. | | signal.subscribe(callback, options?) | Registers a listener called on every change. Returns an unsubscribe function. Options: { signal?: AbortSignal, immediate?: boolean }. | | signal.once(callback, options?) | Registers a listener called only on the next change, then unsubscribes automatically. Returns an unsubscribe function. Options: { signal?: AbortSignal }. | | computed(source, fn) | Creates a read-only ComputedSignal derived from one source. | | computed([...sources], fn) | Creates a read-only ComputedSignal derived from multiple sources. | | computed.dispose() | Removes all source subscriptions. Call when the signal is no longer needed. |

Events

| Event | Type | Description | | :---- | :--- | :---------- | | change | CustomEvent<T> | Fired when the value changes. The new value is available as event.detail. |

Usage examples

Plain function / vanilla JS

import SSignal from 'ssignal';

const counter = new SSignal(0);

const unsubscribe = counter.subscribe((value) => {
  console.log('counter changed:', value);
});

counter.value = 1;              // logs: counter changed: 1
counter.value = (n) => n + 1;  // logs: counter changed: 2
counter.value = 2;              // no log — same value, no event fired

unsubscribe();
counter.value = 99;             // no log — already unsubscribed

React component

import { useEffect, useState } from 'react';
import SSignal from 'ssignal';

// Create signals outside the component so they are shared across the app
export const themeSignal = new SSignal<'light' | 'dark'>('light');
export const cartSignal = new SSignal(new Map<string, number>());

// Generic hook to bind any SSignal to local state
function useSignal<T>(signal: SSignal<T>): T {
  const [value, setValue] = useState<T>(signal.value);

  useEffect(() => {
    const controller = new AbortController();
    // immediate: true keeps state in sync if the signal changes between
    // render and the effect running
    signal.subscribe((v) => setValue(v), { signal: controller.signal, immediate: true });
    return () => controller.abort();
  }, [signal]);

  return value;
}

export function ThemeToggle() {
  const theme = useSignal(themeSignal);

  return (
    <button onClick={() => themeSignal.value = theme === 'light' ? 'dark' : 'light'}>
      Current theme: {theme}
    </button>
  );
}

export function Cart() {
  const cart = useSignal(cartSignal);

  const addItem = (id: string) => {
    cartSignal.value.set(id, (cart.get(id) ?? 0) + 1);
  };

  return (
    <div>
      <p>Items in cart: {cart.size}</p>
      <button onClick={() => addItem('product-1')}>Add product</button>
    </div>
  );
}

Express backend

import express from 'express';
import SSignal from 'ssignal';

const app = express();

// Shared application state
const connectedClients = new SSignal(0);
const featureFlags = new SSignal(new Map<string, boolean>([
  ['new-checkout', false],
  ['dark-mode', true],
]));

// Log every time the client count changes
connectedClients.subscribe((count) => {
  console.log(`[${new Date().toISOString()}] Connected clients: ${count}`);
});

app.use((req, res, next) => {
  connectedClients.value = (n) => n + 1;
  res.on('finish', () => {
    connectedClients.value = (n) => n - 1;
  });
  next();
});

app.get('/flags', (req, res) => {
  res.json(Object.fromEntries(featureFlags.value));
});

app.patch('/flags/:name', express.json(), (req, res) => {
  const { name } = req.params;
  featureFlags.value.set(name, req.body.enabled);
  res.sendStatus(204);
});

app.listen(3000, () => console.log('Server running on port 3000'));

Reactive Map

import SSignal from 'ssignal';

const store = new SSignal(new Map<string, number>());

store.subscribe((map) => {
  console.log('store changed, size:', map.size);
});

store.value.set('a', 1);    // logs: store changed, size: 1
store.value.set('b', 2);    // logs: store changed, size: 2
store.value.delete('a');    // logs: store changed, size: 1
store.value.clear();        // logs: store changed, size: 0

Immediate mode

import SSignal from 'ssignal';

const user = new SSignal({ name: 'Ivan' });

// Fires immediately with current value, then on every change
user.subscribe((v) => console.log('user:', v.name), { immediate: true });
// logs: user: Ivan  ← fired synchronously on subscribe

user.value = { name: 'Junior' };
// logs: user: Junior

One-time subscription

import SSignal from 'ssignal';

type CheckoutState =
  | { status: 'idle' }
  | { status: 'processing'; orderId: string }
  | { status: 'paid'; orderId: string; receiptUrl: string }
  | { status: 'failed'; orderId: string; reason: string };

const checkout = new SSignal<CheckoutState>({ status: 'idle' });

function openCheckout(orderId: string) {
  const controller = new AbortController();

  checkout.once((state) => {
    if (state.status === 'paid') {
      window.location.assign(state.receiptUrl);
    }
  }, { signal: controller.signal });

  checkout.value = { status: 'processing', orderId };

  return {
    close: () => controller.abort(),
  };
}

const modal = openCheckout('order_123');

checkout.value = {
  status: 'paid',
  orderId: 'order_123',
  receiptUrl: '/receipts/order_123',
}; // redirects once

modal.close(); // no effect after the one-time listener has already fired

Computed signals

import SSignal, { computed } from 'ssignal';

// Single source
const price = new SSignal(100);
const withTax = computed(price, (p) => p * 1.21);

withTax.subscribe((v) => console.log('price with tax:', v));
// logs: price with tax: 121

price.value = 200;
// logs: price with tax: 242

// Multiple sources
const qty = new SSignal(3);
const total = computed([price, qty], ([p, q]) => p * q);

total.subscribe((v) => console.log('total:', v)); // logs: total: 600
qty.value = 5; // logs: total: 1000

// computed signals are read-only
total.value = 0; // throws TypeError

// clean up when no longer needed
total.dispose();

AbortController

import SSignal from 'ssignal';

const signal = new SSignal(0);
const controller = new AbortController();

signal.subscribe((v) => console.log(v), { signal: controller.signal });

signal.value = 1;    // logs: 1
controller.abort();
signal.value = 2;    // no log

Scripts

| Command | Description | | :------ | :---------- | | npm run build | Compile and bundle to lib/. | | npm test | Run the test suite. | | npm run test:coverage | Run tests with coverage report. |

Performance

SSignal handles 200,000 value updates notifying 10 simultaneous subscribers in under 500 ms.

Performance test report

License

MIT — see LICENSE.


Repository: github.com/ElJijuna/ssignal