Event manager that composes events effortlessly 🎵


Keywords
event manager, event, events, event composer, emitter, event-manager, tree-shakeable, side-effect-free, functional, pure, TypeScript, compose, typings, event emitter, subscriber, producer-consumer, Node, Browser, for node, for browser, TS, compose-events, event-handlers, event-management, event-signature, functional-programming, manager, meta-events, solid, solid-principles, type-safe
License
MIT
Install
npm install eventhoven@0.10.0-rc.0

Documentation

eventhoven

Compose events effortlessly 🎵

npm npm size

Table of Contents

What is this?

It's a simple type-safe event manager library for browser and node, less than 1KB (gzipped).

It provides a powerful set of tools for creating and composing event managers.
In other words, it manages event managers!

A main list of features includes (but not limited to):

  • Full tree-shaking
  • Functional-style API
  • Multiple event arguments
  • Event names can also be symbols (private events)
  • Versatile plugin system (using meta-events)
  • Fully type-safe - each event-map remembers its event names and type signature (no need for hacky enums)
  • All functions are curried and point-free, which makes them easier to use in functional environment (for example, with ramda and similar tools)
  • SOLID
    • SRP - every function does only one thing
    • OCP - HOFs allow to change certain behaviours without the need to rewrite code
    • LSP - all funcions are easily substitutable using dependency injection as long as they adhere to the same API
    • ISP - all data types are the least specific versions of them
    • DIP - API depends only on abstractions
  • Code-generation-friendly:
    Due to the SRP, all functions have a very limited number of ways of invocation.
    This allows to automatically generate efficient code (for example, CRUD events) for this library without concerns about its stability.
  • KISS and DRY

Something's missing or found a bug?
Feel free to create an issue! 😉


Disclaimer

and some ground principles

TypeScript

eventhoven's main concern is type-safety at every step, so all the code examples will be written in typescript.

It was written in a "type-first, implementation-later" way, and will be kept that way to ensure that runtime types always match the static ones.

Currying

"Why curry functions?" you may ask. Great question! It has many answers on the web already, but I'd recommend reading this and this.

eventhoven uses the concept of currying to elevate the abstraction and allow for a much wider and precise usage of it's API in return for sometimes writing )( instead of usual , , which is not too much of a price to pay for this feature.

It also allows eventhoven to be used effortlessly with other functional libraries like ramda and many others.

Not all eventhoven function are curried. Those, which are, however, will have the following disclaimer:

Note, that the function is curried, which means that it must be called partially

External state >>> Internal state

eventhoven doesn't store anything internally, it's a completely stateless, pure and side-effect-free library.
It only has side-effects from closures on an external state that a user provides.
So, there it is - no private fields, no hidden implementation details, no complications.
This allows for easier testing and simpler usage.

Thanks to this rule, eventhoven is a higher abstraction above other event-managers. A Higher-Order-Event-Manager, if you like.
That is, any other event manager's API can be built on top of what eventhoven gives you, providing a nearly endless set of possibilities.

OK, but why not %event-manager-package%?

eventhoven is not in direct comparison to other event managers. As stated in the main description - its main purpose is to compose events and event managers.

In production, it's a fairly typical scenario that multiple libraries with multiple event systems exist and function in the same project at the same time.
Front-end libraries do that all the time - vue, react, angular - all have own separate event systems - even from the DOM! eventhoven aims to provide a connecting bridge for different event managing strategies, by providing instruments for unifying the event management API.
In other words, it allows to unify event management.

It just so happens that it can do event management very efficiently too. 😉


Installation

npm:

npm i -S eventhoven

module: see importing

Currently, only installation through npm or script[type=module] is supported.
No single-file bundles just yet.

Importing

// TS-module (pure typescript),
// allows compilation settings to be set from the project config
import { emit, on, off } from 'eventhoven/src';

// ES-module (node, typescript)
import { emit, on, off } from 'eventhoven';

// ESNext (no polyfills for esnext)
import { emit, on, off } from 'eventhoven/dist/esnext';

// ES-module (browser)
import { emit, on, off } from 'https://unpkg.com/eventhoven/dist/es';

// Classic node commonjs
const { emit, on, off } = require('eventhoven/dist/js');

Simple usage examples

Example 1
// Essential imports
import { eventMap, emit, on, off } from 'eventhoven';

// Event-map declaration
const emojiEvents = eventMap({
  // key - event name,
  // function arguments - event arguments,
  // function body - default handler for the event
  // (leave emtpy if you need to just declare the event)
  '👩'(emoji: '👨' | '👩') {},
  '🌈'(emoji: '🦄' | '🌧') {},
  '🎌':(emoji: '👘' | '🍣' | '🚗', amount: number) {},
});

on(emojiEvents)('🎌')(
  (emoji, amount) => console.log(`Yay!, ${amount} of ${emoji}-s from japan!`)
);

on(emojiEvents)('🎌')(
  // Returning promises is also allowed (example API from http://www.sushicount.com/api)
  (emoji, amount) => fetch('http://api.sushicount.com/add-piece-of-sushi/')
    .then(_ => _.json())
    .then(resp => console.log(`Yay!, ${resp.pieces_of_sushi} of ${emoji}-s loaded from sushicount!`))
);

// It's possible to await execution of all event handlers too
await emit(emojiEvents)(
  // Autocomplete for event names here!
  '🎌'
)(
  // Autocomplete for arguments here!
  '🍣', 10
);
// Console output:
// => Yay!, 10 🍣-s from japan!
// => Yay!, 11 🍣-s loaded from sushicount!
Example 2
import { eventMap, emit, on, off } from 'eventhoven';

type Todo = { done: boolean; text: string; };

const todos: Todo[] = [];

// Event-map declaration
const todoEvents = eventMap({
  // key - event name,
  // function arguments - event arguments,
  // function body - default handler for the event
  // (leave emtpy if you need to just declare the event)
  'todo-added'(newTodo: Todo, todos: Todo[]) {
    // typically, a default handler is used
    // to compose events from other event managers here
  },
  'done-change'(todo: Todo, newDone: boolean) {},
  'text-change'(todo: Todo, newText: string) {},
});

const unsubFromAddTodo = on(todoEvents)('todo-added')(
  (todo, todos) => todos.push(todo)
);

// `addingTodos` is a promise that resolves
// when all event subscribers are done executing
const addingTodos = emit(todoEvents)('todo-added')(
  { done: false, text: 'new todo' },
  todos
);
// Now, `todos` contains the new todo

API

General exports are the following:

name type description
eventMap function Event-map factory
emit function Event emitter factory
subscribe function Event subscriber factory
subscribeToAll function Event subscriber factory for all events in a collection
on function Alias for subscribe
onAll function Alias for subscribeToAll
unsubscribe function Event unsubscriber factory
unsubscribeFromAll function Event unsubscriber factory
off function Alias for unsubscribe
offAll function Alias for unsubscribeFromAll
emitCollection function Creates a collection of event-emitters from an event-map
subscribeCollection function Creates a collection of event-subscribers from an event-map
unsubscribeCollection function Creates a collection of event-unsubscribers from an event-map
eventCollection function Creates a collection of the three previous collections from an event-map
wait function Waits for an event to be executed
harmonicWait function Same as wait but has an arity of 3, just as all the other event-handling functions
debug function Sets the debug mode (if enabled - logs all events to the console)
metaEvents object A meta-event-map. Can be used to subscribe to the internal eventhoven's events
emitMeta function A meta-event emitter. An emit function created for metaEvents

eventMap

Creates an event-map from event signatures.

Parameters:

name type description
events TEventSignatures a collection of event signatures

Returns: TEventMap

This function is the main "entry point" to the whole event management pipeline. It constructs a base storage for events and their handlers, which is then used by all of the other functions.

In other words, to start working with events in eventhoven you start by creating an event-map:

import { eventMap } from 'eventhoven';

// `keyboardEvents` should now be used for all event interactions
const keyboardEvents = eventMap({
  keyup(e: KeyboardEvent) {},
  keydown(e: KeyboardEvent) {},
  keypress(e: KeyboardEvent, modifier?: string) {
    // This is a default handler for the event,
    // it's always executed when the event is invoked
    console.log('modifier:', modifier);
  },
});

In this example, keys in keyboardEvents correspond to event names ('keyup', 'keydown', etc.) and values contain handler maps and amount of arguments for a given event.

Here, `keyboardEvents` is equal to the following object:
const keyboardEvents = {
  // Name of the event
  keyup: {
    // Amount of arguments for the event handlers
    arity: 1,

    // Collection of the event handlers
    handlers: new Map([
      // Notice the default event handler from the event-map
      (e: KeyboardEvent) => {},

      // Do we execute this event handler only once?
      false
    ])
  },
  keydown: {
    arity: 1,
    handlers: new Map([(e: KeyboardEvent) => {}, false])
  },
  keypress: {
    arity: 2,
    handlers: new Map([
      // Notice the default event handler from the event-map
      (e: KeyboardEvent, modifier?: string) => { console.log('modifier:', modifier); },
      false
    ])
  },
}

It's also possible to add new events to the event-map at runtime (by creating a new event-map 😁):

const inputEvents = {
  ...keyboardEvents,
  ...eventMap({
    'mouse-click'(e: MouseEvent) {},
  }),
}

// Still have type inference here!
emit(inputEvents)('mouse-click')

emit

Creates event emitters for an event-map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to emit events from
event PropertyKey An event name to emit for a given event-map (can be a symbol too)
...args any (contextual) Arguments for the specific event, spread

Returns: Promise<void> - a promise that is resolved when all event handlers have finished their execution


emitAll

Emits all events in an event map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to subscribe to.
eventArgs TEventParamsMap Parameters for all events in an event map.

Returns: Record<keyof M, Promise<void>> - a map for all events' emits promises (each will resolve upon all event handlers' resolution).


subscribe

Creates event subscribers for an event in an event-map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to get events from.
event PropertyKey An event name to subscribe to for a given event-map (can be a symbol too).
...handlers function[] Handlers to execute on the event, spread. If emtpy, no subscribing is done.

Returns: () => void - a function that unsubscribes the handler from the event

Alias: on

subscribeToAll

Subscribes handler(s) to all events in an event map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to subscribe to.
...handlers function[] Handlers to execute on the events, spread. If emtpy, no subscribing is done.

Returns: void

Alias: onAll


unsubscribe

Unsubscribes handlers from events of an event-map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to unsubscribe handlers from.
event PropertyKey An event name to unsub from for a given event-map (can be a symbol too).
...handlers function[] Handlers to unsubscribe from the event, spread. If empty - all currently subbed handlers will be unsubscribed.

Returns: void

Alias: off

unsubscribeFromAll

Unsubscribes handler(s) from all events in an event map.

Note, that the function is curried, which means that it must be called partially

Parameters:

name type description
eventMap TEventMap An event-map to unsubscribe from.
...handlers function[] Handlers to unsubscribe from the events, spread. If empty - all currently subbed handlers will be unsubscribed.

Returns: void

Alias: offAll


wait

Allows to wait for an event without the need for callbacks.

Note, that the function is curried, which means that it must be called partially

Basically, promise-style subscribe with the once flag.
It is a way to block execution flow until some event occurs.

Parameters:

name type description
eventMap TEventMap An event-map to wait events from.
event PropertyKey An event name to wait for in a given event-map (can be a symbol too).

Returns: Promise<Array<unknown>> (contextual) - a promise with array of parameters passed to the event.

Simple example
import { wait } from 'eventhoven';

const keydown = wait(keyboardEvents)('keydown');

//... some time later in async context

const [e] = await keydown; // Resolves upon the first 'keydown' event emit
console.log(e);
// => KeyboardEvent {}

harmonicWait

Same as wait, but returns a promise factory instead of a plain promise.

Note, that the function is curried, which means that it must be called partially

Useful due to having the same signature as emit, subscribe and unsubscribe, which allows for an easier composition of waiters.

Parameters:

name type description
eventMap TEventMap An event-map to wait events from.
event PropertyKey An event name to wait for in a given event-map (can be a symbol too).

Returns: () => Promise<Array<unknown>> (contextual) - a promise factory with array of parameters passed to the event.

Simple example
import { wait } from 'eventhoven';

// Function that initiates a waiter
const waitForKeydown = wait(keyboardEvents)('keydown');

//... some time later in async context

// Resolves upon the first 'keydown' event emit
// since the call of the `waitForKeydown`
const [e] = await waitForKeydown();
console.log(e);
// => KeyboardEvent {}

debug

Sets a debug mode.

Parameters:

name type description
enabled boolean Whether to enable the debug mode or disable it.
logEvent v0.4.0 function (optional) A custom logging function.

Returns: void

When debug mode is enabled, all emits, subscribes and unsubscribes are logged to the console in a following format (default):

HH:MM:SS [EVENT {event-name}]: {event-handler-or-params}

where {event-name} is the name of the event
and {event-handler-or-params} is the handler for the event (when subscribing or unsubscribing) or its params (when emitting).

Example:

debug(true);

emit(emojiEvents)('🎌')('🍣', 10);

// logs:
// 12:59:05 [EVENT EMIT 🎌]: 🍣, 10

Collections

eventhoven provides a way to group your event-managing needs using collections.

Parameters:

name type description
eventMap TEventMap An event-map to wait events from.

Return: A map of event names to the action for that event name.

Currently available collections are:

name action description
emitCollection emit Creates a an object, where each property is a function that emits a prescribed event
subscribeCollection subscribe Creates a an object, where each property is a function that subscribes to a prescribed event
unsubscribeCollection unsubscribe Creates a an object, where each property is a function that unsubscribes from a prescribed event
eventCollection all of the above Creates an object that contains all three collections in itself. Can be used to create a singleton that manages all events in an event-map.

Plugin API

It's also possible to write custom plugins for eventhoven thanks to meta-events!

Meta-events is a simple event-map with events for internal eventhoven actions, like emit.
One can subscribe to these events to execute some actions or emit these events to emulate them for the eventhoven.

The simplest possible plugin is already written for you - the debug plugin.
It can be used as an example for writing your own plugins for eventhoven!

Current list of all meta-events is as follows:

name emitted when
emit Any event is emitted, except itself.
subscribe Any event is subscribed to, except itself.
unsubscribe Any event is unsubscribed from, except itself.

Simple example:

import { metaEvents, on } from 'eventhoven';

on(metaEvents)('emit')(
  (eventMap, eventName, eventArgs) => console.log(`This handler will be executed when ANY event is emitted!`)
);

Contribute

First, fork the repo and clone it:

git clone https://github.com/%your-github-username%/eventhoven.git

Then:

npm install

Then:

npm run dev

Then propose a PR!
I'll be happy to review it!


Something's missing or found a bug?
Feel free to create an issue! 😉