Async-first, reactive event handling library for complex event flows with three powerful primitives: Event (multi-listener broadcast), Signal (promise-like coordination), and Sequence (async queue). Built for both browser and Node.js with full TypeScript support.
Evnty provides three complementary async primitives, each designed for specific patterns:
Events allow multiple listeners to react to values. All registered listeners are called for each emission.
const clickEvent = createEvent<{ x: number, y: number }>();
// Multiple listeners can subscribe
clickEvent.on(({ x, y }) => console.log(`Click at ${x},${y}`));
clickEvent.on(({ x, y }) => updateUI(x, y));
// All listeners receive the value
clickEvent({ x: 100, y: 200 });Use Event when:
- Multiple components need to react to the same occurrence
- You need pub/sub or observer pattern
- Listeners should persist across multiple emissions
Signals are for coordinating async operations. When a value is sent, ALL waiting consumers receive it (broadcast).
const signal = new Signal<string>();
// Multiple consumers can wait
const promise1 = signal.next();
const promise2 = signal.next();
// Send value - all waiting consumers receive it
signal('data');
const [result1, result2] = await Promise.all([promise1, promise2]);
// result1 === 'data' && result2 === 'data'Use Signal when:
- You need one-time notifications
- Multiple async operations need the same trigger
- Implementing async coordination patterns
Sequences are FIFO queues for single-consumer scenarios. Values are consumed in order, with backpressure support.
const taskQueue = new Sequence<Task>();
// Producer adds tasks
taskQueue(task1);
taskQueue(task2);
taskQueue(task3);
// Single consumer processes in order
for await (const task of taskQueue) {
await processTask(task); // task1, then task2, then task3
}Use Sequence when:
- You need ordered processing (FIFO)
- Only one consumer should handle each value
- You want backpressure control with
reserve()
| Event | Signal | Sequence | |
|---|---|---|---|
| Consumers | Multiple persistent listeners | Multiple one-time receivers | Single consumer |
| Delivery | All listeners called | All waiting get same value | Each value consumed once |
| Pattern | Pub/Sub | Broadcast coordination | Queue/Stream |
| Persistence | Listeners stay registered | Resolves once per next() |
Values queued until consumed |
Traditional event handling in JavaScript/TypeScript has limitations:
- String-based event names lack type safety
- No built-in async coordination primitives
- Missing functional transformations for event streams
- Complex patterns require extensive boilerplate
Evnty solves these problems by providing:
- Type-safe events with full TypeScript inference
- Three specialized primitives for different async patterns
- Rich functional operators (map, filter, reduce, debounce, batch, etc.)
- Composable abstractions that work together seamlessly
- Async-First Design: Built from the ground up for asynchronous event handling with full Promise support
- Functional Programming: Rich set of operators including map, filter, reduce, debounce, batch, and expand for event stream transformations
- Type-Safe: Full TypeScript support with strong typing and inference throughout the event pipeline
- Async Iteration: Events can be consumed as async iterables using for-await-of loops
- Event Composition: Merge, combine, and transform multiple event streams into new events
- Minimal Dependencies: Lightweight with only essential dependencies for optimal bundle size
- Universal: Works seamlessly in both browser and Node.js environments, including service workers
| Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
Using pnpm:
pnpm add evntyUsing yarn:
yarn add evntyUsing npm:
npm install evntyimport { createEvent } from 'evnty';
// Create a typed event
const userEvent = createEvent<{ id: number, name: string }>();
// Multiple listeners
userEvent.on(user => console.log('Logger:', user));
userEvent.on(user => updateUI(user));
userEvent.on(user => saveToCache(user));
// Emit - all listeners are called
userEvent({ id: 1, name: 'Alice' });
// Functional transformations
const adminEvent = userEvent
.filter(user => user.id < 100)
.map(user => ({ ...user, role: 'admin' }));
// Async iteration
for await (const user of userEvent) {
console.log('User event:', user);
}import { Signal } from 'evnty';
// Coordinate multiple async operations
const dataSignal = new Signal<Buffer>();
// Multiple operations wait for the same data
async function processA() {
const data = await dataSignal.next();
// Process data in way A
}
async function processB() {
const data = await dataSignal.next();
// Process data in way B
}
// Start both processors
Promise.all([processA(), processB()]);
// Both receive the same data when it arrives
dataSignal(Buffer.from('shared data'));import { Sequence } from 'evnty';
// Create a task queue
const taskQueue = new Sequence<() => Promise<void>>();
// Single consumer processes tasks in order
(async () => {
for await (const task of taskQueue) {
await task();
console.log('Task completed');
}
})();
// Multiple producers add tasks
taskQueue(async () => fetchData());
taskQueue(async () => processData());
taskQueue(async () => saveResults());
// Backpressure control
await taskQueue.reserve(10); // Wait until queue has ≤10 items
taskQueue(async () => nonUrgentTask());// Event + Signal for request/response pattern
const requestEvent = createEvent<Request>();
const responseSignal = new Signal<Response>();
requestEvent.on(async (req) => {
const response = await handleRequest(req);
responseSignal(response);
});
// Event + Sequence for buffered processing
const dataEvent = createEvent<Data>();
const processQueue = new Sequence<Data>();
dataEvent.on(data => processQueue(data));
// Process with controlled concurrency
for await (const data of processQueue) {
await processWithRateLimit(data);
}License The MIT License Copyright (c) 2025 Ivan Zakharchanka