evnty
v5.0.0
Published
Async-first, reactive event handling library for complex event flows in browser and Node.js
Maintainers
Readme
Evnty
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.
Table of Contents
Core Concepts
Evnty provides three complementary async primitives, each designed for specific patterns:
🔊 Event - Multi-Listener Broadcasting
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
📡 Signal - Promise-Based Coordination
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
📦 Sequence - Async Queue (FIFO)
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()
Key Differences
| | 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 |
Motivation
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
Features
- 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
Platform Support
|
|
|
|
|
|
|
| --------------------- | ----------------------- | ------------------------- | ----------------------- | --------------------- | ------------------- |
| Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
Installing
Using pnpm:
pnpm add evntyUsing yarn:
yarn add evntyUsing npm:
npm install evntyExamples
Event - Multi-Listener Pattern
import { 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);
}Signal - Async Coordination
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'));Sequence - Task Queue
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());Combining Primitives
// 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
License The MIT License Copyright (c) 2025 Ivan Zakharchanka
