@cyberwebdev/nanosignals
v1.1.0
Published
Ultra-lightweight signal system inspired by Godot for JavaScript
Downloads
35
Maintainers
Readme
🔔 NanoSignals
An ultra-lightweight signal system inspired by Godot for JavaScript. Zero dependencies, modular and simple.
✨ Why NanoSignals?
- 🪶 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
📦 Installation
npm install @cyberwebdev/nanosignalsOr with a CDN:
import { Signal } from "https://esm.sh/@cyberwebdev/nanosignals";🚀 Quick Start
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!📚 API
new Signal(options?)
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);
},
});signal.connect(callback, context?)
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 callbacksignal.once(callback, context?)
Connects 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)signal.emit(...args)
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 });signal.disconnect(callback, context?)
Disconnects a specific callback.
signal.disconnect(myCallback);
signal.disconnect(this.handleSignal, this);signal.clear()
Disconnects all listeners.
signal.clear();signal.pause() / signal.resume()
Pause and resume signal emissions.
signal.pause();
signal.emit("ignored"); // Will not call any listeners
signal.resume();
signal.emit("processed"); // Will call all listenerssignal.setDebug(enabled)
Enable or disable debug mode dynamically.
signal.setDebug(true); // Enable debug logging
signal.setDebug(false); // Disable debug loggingsignal.getStats() / signal.printStats()
Get 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 tableProperties
signal.listenerCount; // Number of connected listeners
signal.isPaused; // Whether the signal is paused📘 TypeScript Support
NanoSignals includes full TypeScript type definitions with generics!
Typed Signals
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 errorGeneric Signals
// 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]>();With Options
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);🎯 Use Cases
Observer Pattern without Coupling
// 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 = "";
});Component Communication
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}`;
}
}Automatic Cleanup
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());
}
}Game Loop with Pause
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"Debug Mode for Development
// 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 │
// └─────────────────┴────────┘🆚 Comparison
| 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 |
🔧 Advanced Features
Error Handling
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!Custom Error Handler
const signal = new Signal({
catchErrors: true,
errorHandler: (error, callback, args) => {
// Send to your error tracking service
sendToSentry(error);
console.log("Error handled:", error.message);
},
});Performance Mode
Disable error catching for maximum performance in production:
const signal = new Signal({ catchErrors: false });
// Slightly faster, but errors will stop execution🤝 Contributing
Contributions are welcome! Feel free to open an issue or pull request on GitHub.
📄 License
MIT © CyberWebDev
Inspired by Godot Engine's signal system 🎮
