Evnty
0-dependency, high-performance, reactive event handling library optimized for both browser and Node.js environments. This library introduces a robust and type-safe abstraction for handling events, reducing boilerplate and increasing code maintainability.
Table of Contents
Motivation
In traditional event handling in TypeScript, events are often represented as strings, and there's no easy way to apply functional transformations like filtering or mapping directly on the event data. This approach lacks type safety, and chaining operations require additional boilerplate, making the code verbose and less maintainable.
The proposed library introduces a robust Event
abstraction that encapsulates event data and provides a suite of functional methods like map
, filter
, reduce
, debounce
, etc., allowing for a more declarative and type-safe approach to event handling. This design facilitates method chaining and composition, making the code more readable and maintainable. For instance, it allows developers to create new events by transforming or filtering existing ones, thus promoting code reusability and modularity.
Features
- Modern: Supports Promises and module systems ESM and CommonJS
- Zero Dependencies: Utilizes native features for optimal performance.
- Full TypeScript Support: Ensures type safety and improves developer experience.
- Functional Programming Techniques: Offers map, filter, reduce, expand, and more for event handling.
- Flexible Environment Support: Works seamlessly in both the browser and Node.js, including service workers.
- Performance Optimized: Competes with and exceeds other well-known libraries like EventEmitter3 and EventEmitter2 in performance benchmarks.
Platform Support
| | | | | |
---|
Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ | Latest ✔ |
Installing
Using pnpm:
pnpm add @diotoborg/fugiat-doloremque
Using yarn:
yarn add @diotoborg/fugiat-doloremque
Using npm:
npm install @diotoborg/fugiat-doloremque
Usage
createEvent
Creates a new event instance.
import { createEvent, Event } from '@diotoborg/fugiat-doloremque';
type ClickEvent = { x: number; y: number; button: number };
const clickEvent = createEvent<ClickEvent>();
type KeyPressEvent = { key: string };
const keyPressEvent = createEvent<KeyPressEvent>();
type ChangeEvent = { target: HTMLInputElement };
const changeEvent = createEvent<ChangeEvent>();
document.querySelector('input').addEventListener('change', changeEvent);
filter
Returns a new event that will only be triggered once the provided filter function returns true
.
Supports predicate type guard function and filter function as a more concise variant.
import { Predicate } from '@diotoborg/fugiat-doloremque';
type SpacePressEvent = KeyPressEvent & { key: 'Space' };
type LeftClickEvent = ClickEvent & { button: 1 };
const spacePressPredicate: Predicate<KeyPressEvent, SpacePressEvent> = (keyPressEvent): keyPressEvent is SpacePressEvent => keyPressEvent.key === 'Space';
const spacePressPredicatedEvent = keyPressEvent.filter(spacePredicate);
const spacePressFilteredEvent = keyPressEvent.filter<SpacePressEvent>(({ key }) => key === 'Space');
const leftClickPredicate: Predicate<ClickEvent, LeftClickEvent> = (mouseClickEvent): mouseClickEvent is LeftClickEvent => mouseClickEvent.button === 1;
const leftClickPredicatedEvent = clickEvent.filter(leftClickPredicate);
const leftClickFilteredEvent = keyPressEvent.filter<LeftClickEvent>(({ button }) => button === 1);
map
Returns a new event that maps the values of this event using the provided mapper function.
const point = { x: 100, y: 100 };
const distanceClickEvent = clickEvent.map((click) => (click.x - point.x) ** 2 + (click.y - point.y) ** 2);
const lowerCaseKeyPressEvent = keyPressEvent.map(({ key }) => key.toLowerCase());
const trimmedChangeEvent = changeEvent.map((event) => event.target.value.trim());
reduce
Returns a new event that reduces the emitted values using the provided reducer function.
const sumEvent = numberEvent.reduce((a, b) => a + b, 0);
sumEvent.on((sum) => console.log(sum));
await sumEvent(1);
await sumEvent(2);
await sumEvent(3);
expand
Transforms each event's data into multiple events using an expander function. The expander function takes
the original event's data and returns an array of new data elements, each of which will be emitted individually
by the returned Event
instance. This method is useful for scenarios where an event's data can naturally
be expanded into multiple, separate pieces of data which should each trigger further processing independently.
const sentenceEvent = new Event<string>();
const wordEvent = sentenceEvent.expand(sentence => sentence.split(' '));
wordEvent.on(word => console.log('Word:', word));
await sentenceEvent('Hello world');
orchestrate
Creates a new event that emits values based on a conductor event. The orchestrated event will emit the last value
captured from the original event each time the conductor event is triggered. This method is useful for synchronizing
events, where the emission of one event controls the timing of another.
const rightClickPositionEvent = mouseMoveEvent.orchestrate(mouseRightClickEvent);
const tickEvent = new Event<void>();
const dataEvent = new Event<string>();
const synchronizedEvent = dataEvent.orchestrate(tickEvent);
synchronizedEvent.on(data => console.log('Data on tick:', data));
await dataEvent('Hello');
await dataEvent('World!');
await tickEvent();
debounce
Creates a debounced event that delays triggering until after a specified interval has elapsed
following the last time it was invoked. This method is particularly useful for limiting the rate
at which a function is executed. Common use cases include handling rapid user inputs, window resizing,
or scroll events.
const searchEvent = changeEvent.debounce(100);
searchEvent.value.on(text => searchAPI(text));
searchEvent.error.on(error => console.error(error));
await searchEvent('t');
await searchEvent('te');
await searchEvent('tex');
await searchEvent('text');
batch
Aggregates multiple event emissions into batches and emits the batched events either at specified
time intervals or when the batch reaches a predefined size. This method is useful for grouping
a high volume of events into manageable chunks, such as logging or processing data in bulk.
const sendData = changeEvent.batch(100, 5);
sendData.value.on(data => console.log(data));
sendData.error.on(error => console.error(error));
await sendData('data1');
await sendData('data2');
await sendData('data3');
await sendData('data4');
generator
Transforms an event into an AsyncIterable that yields values as they are emitted by the event. This allows for the consumption
of event data using async iteration mechanisms. The iterator generated will yield all emitted values until the event
signals it should no longer be active.
const numberEvent = new Event<number>();
const numberIterable = numberEvent.generator();
await numberEvent(1);
await numberEvent(2);
await numberEvent(3);
for await (const num of numberIterable) {
console.log('Number:', num);
}
queue
Creates a queue from the event, where each emitted value is sequentially processed. The returned object allows popping elements
from the queue, ensuring that elements are handled one at a time. This method is ideal for scenarios where order and sequential processing are critical.
const taskEvent = new Event<string>();
const taskQueue = taskEvent.queue();
await taskEvent('Task 1');
await taskEvent('Task 2');
console.log('Processing:', await taskQueue.pop());
console.log('Processing:', await taskQueue.pop());
merge
Merges multiple events into a single event. This function takes any number of Event
instances
and returns a new Event
that triggers whenever any of the input events trigger. The parameters
and results of the merged event are derived from the input events, providing a flexible way to
handle multiple sources of events in a unified manner.
import { merge } from '@diotoborg/fugiat-doloremque';
const mouseEvent = createEvent<MouseEvent>();
const keyboardEvent = createEvent<KeyboardEvent>();
const inputEvent = merge(mouseEvent, keyboardEvent);
inputEvent.on(event => console.log('Input event:', event));
createInterval
Creates an event that triggers at a specified interval.
import { createInterval } from '@diotoborg/fugiat-doloremque';
const everySecondEvent = createInterval(1000);
Examples
import createEvent, { Event } from '@diotoborg/fugiat-doloremque';
type Click = { button: string };
const clickEvent = createEvent<Click>();
const handleClick = ({ button }: Click) => console.log('Clicked button is', button);
const unsubscribeClick = clickEvent.on(handleClick);
type KeyPress = { key: string };
const keyPressEvent = createEvent<KeyPress>();
const handleKeyPress = ({ key }: KeyPress) => console.log('Key pressed', key);
const unsubscribeKeyPress = keyPressEvent.on(handleKeyPress);
type Input = Click | KeyPress;
const handleInput = (input: Input) => console.log('Input', input);;
const inputEvent = Event.merge(clickEvent, keyPressEvent);
inputEvent.on(handleInput);
const handleLeftClick = () => console.log('Left button is clicked');
const leftClickEvent = clickEvent.filter(({ button }) => button === 'left');
leftClickEvent.on(handleLeftClick);
setTimeout(keyPressEvent, 1000, { key: 'Enter' });
await keyPressEvent.first(({ key }) => key === 'Enter').onceAsync();
keyPressEvent({ key: 'W' });
keyPressEvent({ key: 'A' });
keyPressEvent({ key: 'S' });
keyPressEvent({ key: 'D' });
clickEvent({ button: 'right' });
clickEvent({ button: 'left' });
clickEvent({ button: 'middle' });
unsubscribeClick();
leftClickEvent.off(handleLeftClick);
unsubscribeKeyPress.after(() => keyPressEvent
.first(({ key }) => key === 'Esc')
.onceAsync()
);
keyPressEvent({ key: 'Esc' });
const messageEvent = createEvent();
const messagesBatchEvent = messageEvent.debounce(100);
const messageEvent = createEvent();
const messagesBatchEvent = messageEvent.batch(100);
Migration
From v1 to v2
A breaking change has been made: events no longer accept a list of arguments. Now, each event accepts a single argument, so simply wrap your arguments in an object. This decision was taken to leverage the benefits of predicate type guards.
License
License Apache-2.0
Copyright (c) 2021-present Ivan Zakharchanka