An ultra-lightweight signal system inspired by Godot for JavaScript. Zero dependencies, modular and simple.
- ๐ชถ Ultra-lightweight: Less than 2 KB minified
- ๐ฏ Simple: Intuitive API with just a few methods
- ๐ Modular: Zero dependencies, ES6 modules
- ๐ฎ Godot-inspired: If you know Godot, you already know how to use it
- ๐ Zero config: Works everywhere (Node, Browser, Deno, Bun)
- ๐ก๏ธ Type-safe: Full TypeScript support with generics
- ๐ Debug mode: Built-in debugging tools for development
- โธ๏ธ Pausable: Pause and resume signal emissions
- ๐ฏ Error handling: Prevents one failing listener from breaking others
npm install @cyberwebdev/nanosignalsOr with a CDN:
import { Signal } from "https://esm.sh/@cyberwebdev/nanosignals";import { Signal } from "@cyberwebdev/nanosignals";
class Player {
constructor() {
this.health = 100;
this.onDamaged = new Signal();
this.onDeath = new Signal();
}
takeDamage(amount) {
this.health -= amount;
this.onDamaged.emit(amount, this.health);
if (this.health <= 0) {
this.onDeath.emit();
}
}
}
// Connect to signals
const player = new Player();
player.onDamaged.connect((amount, health) => {
console.log(`-${amount} HP | Health: ${health}`);
});
player.onDeath.connect(() => {
console.log("Game Over!");
});
player.takeDamage(30); // -30 HP | Health: 70
player.takeDamage(80); // -80 HP | Health: -10
// Game Over!Creates a new signal with optional configuration.
// Default configuration
const signal = new Signal();
// With options
const signal = new Signal({
debug: false, // Enable debug logging
catchErrors: true, // Catch errors in listeners
errorHandler: (error, callback, args) => {
// Custom error handler
console.error("Custom handler:", error);
},
});Connects a function to the signal. Returns a disconnect function.
// Simple callback
const disconnect = signal.connect(() => console.log("Signal received!"));
// With context (to preserve 'this')
signal.connect(this.handleSignal, this);
// Auto-disconnect
const disconnect = signal.connect(callback);
disconnect(); // Disconnects the callbackConnects a callback that will only be called once, then automatically disconnected.
signal.once(() => {
console.log("This will only run once");
});
signal.emit(); // "This will only run once"
signal.emit(); // (nothing happens)Emits the signal with optional arguments. Does nothing if the signal is paused.
signal.emit();
signal.emit(42);
signal.emit("data", { x: 10, y: 20 });Disconnects a specific callback.
signal.disconnect(myCallback);
signal.disconnect(this.handleSignal, this);Disconnects all listeners.
signal.clear();Pause and resume signal emissions.
signal.pause();
signal.emit("ignored"); // Will not call any listeners
signal.resume();
signal.emit("processed"); // Will call all listenersEnable or disable debug mode dynamically.
signal.setDebug(true); // Enable debug logging
signal.setDebug(false); // Disable debug loggingGet or print signal statistics (only available in debug mode).
const stats = signal.getStats();
// {
// listenerCount: 3,
// emitCount: 10,
// lastEmitArgs: ['hello', 42],
// lastEmitTime: Date,
// isPaused: false,
// catchErrors: true
// }
signal.printStats(); // Prints stats to console as a tablesignal.listenerCount; // Number of connected listeners
signal.isPaused; // Whether the signal is pausedNanoSignals includes full TypeScript type definitions with generics!
import { Signal } from "@cyberwebdev/nanosignals";
// Signal with specific argument types
const onScoreChanged = new Signal<[score: number]>();
onScoreChanged.connect((score) => {
// TypeScript knows 'score' is a number
console.log(score.toFixed(2));
});
onScoreChanged.emit(42); // โ
OK
onScoreChanged.emit("42"); // โ TypeScript error// Signal with multiple typed arguments
const onDamaged = new Signal<[amount: number, health: number]>();
// Signal with no arguments
const onReady = new Signal<[]>();
// Signal with complex types
interface User {
id: number;
name: string;
}
const onUserLogin = new Signal<[user: User]>();import { Signal, SignalOptions } from "@cyberwebdev/nanosignals";
const options: SignalOptions = {
debug: true,
catchErrors: true,
errorHandler: (error, callback, args) => {
console.error("Error:", error);
},
};
const signal = new Signal<[string, number]>(options);// events.js
import { Signal } from "@cyberwebdev/nanosignals";
export const userLoggedIn = new Signal();
export const userLoggedOut = new Signal();// auth.js
import { userLoggedIn, userLoggedOut } from "./events.js";
function login(username) {
// ... login logic
userLoggedIn.emit(username);
}
function logout() {
// ... logout logic
userLoggedOut.emit();
}// ui.js
import { userLoggedIn, userLoggedOut } from "./events.js";
userLoggedIn.connect((username) => {
document.querySelector(".welcome").textContent = `Hello ${username}`;
});
userLoggedOut.connect(() => {
document.querySelector(".welcome").textContent = "";
});class Game {
constructor() {
this.onScoreChanged = new Signal();
this.score = 0;
}
addPoints(points) {
this.score += points;
this.onScoreChanged.emit(this.score);
}
}
class ScoreDisplay {
constructor(game) {
game.onScoreChanged.connect(this.update, this);
}
update(score) {
this.element.textContent = `Score: ${score}`;
}
}class Component {
constructor(emitter) {
this.disconnectors = [];
// Store disconnect functions
this.disconnectors.push(
emitter.onUpdate.connect(this.handleUpdate, this),
emitter.onDestroy.connect(this.handleDestroy, this),
);
}
destroy() {
// Automatically disconnect everything
this.disconnectors.forEach((disconnect) => disconnect());
}
}class Game {
constructor() {
this.onUpdate = new Signal();
this.isPaused = false;
}
pause() {
this.isPaused = true;
this.onUpdate.pause();
}
resume() {
this.isPaused = false;
this.onUpdate.resume();
}
update(deltaTime) {
// Will only emit if not paused
this.onUpdate.emit(deltaTime);
}
}
const game = new Game();
game.onUpdate.connect((dt) => {
console.log("Game updating:", dt);
});
game.update(0.016); // "Game updating: 0.016"
game.pause();
game.update(0.016); // (nothing happens)
game.resume();
game.update(0.016); // "Game updating: 0.016"// Development
const signal = new Signal({ debug: true });
signal.connect(() => console.log("Listener 1"));
signal.connect(() => console.log("Listener 2"));
signal.emit("test");
// [NanoSignals Debug] Emitting signal (#1)
// Arguments: ['test']
// Listeners to notify: 2
// [NanoSignals Debug] Calling listener 1/2
// Listener 1
// [NanoSignals Debug] Calling listener 2/2
// Listener 2
// [NanoSignals Debug] Signal emission completed
signal.printStats();
// โโโโโโโโโโโโโโโโโโโฌโโโโโโโโโ
// โ listenerCount โ 2 โ
// โ emitCount โ 1 โ
// โ isPaused โ false โ
// โโโโโโโโโโโโโโโโโโโดโโโโโโโโโ| Feature | NanoSignals | EventEmitter (Node) | Custom Events (DOM) |
|---|---|---|---|
| Size | < 4 KB | ~10 KB | Built-in |
| Dependencies | 0 | 0 | 0 |
| Simple API | โ | โ | โ |
| Auto-disconnect | โ | โ | โ |
| Context (this) | โ | โ | โ |
| TypeScript | โ | โ | โ |
| Error Handling | โ | โ | โ |
| Pause/Resume | โ | โ | โ |
| Debug Mode | โ | โ | โ |
| Once Method | โ | โ | โ |
| Browser/Node/Deno | โ | Node only | Browser only |
By default, NanoSignals catches errors in listeners to prevent one failing listener from breaking others:
const signal = new Signal({ catchErrors: true });
signal.connect(() => {
console.log("Listener 1");
});
signal.connect(() => {
throw new Error("Oops!");
});
signal.connect(() => {
console.log("Listener 3 still runs!");
});
signal.emit();
// Listener 1
// [NanoSignals] Error in listener: Error: Oops!
// Listener 3 still runs!const signal = new Signal({
catchErrors: true,
errorHandler: (error, callback, args) => {
// Send to your error tracking service
sendToSentry(error);
console.log("Error handled:", error.message);
},
});Disable error catching for maximum performance in production:
const signal = new Signal({ catchErrors: false });
// Slightly faster, but errors will stop executionContributions are welcome! Feel free to open an issue or pull request on GitHub.
MIT ยฉ CyberWebDev
Inspired by Godot Engine's signal system ๐ฎ