Skip to content

Various EventEmitter event replacements with synchronous, a-synchronous, and queued events. Events are members. Usable with JavaScript and TypeScript.

License

Notifications You must be signed in to change notification settings

rogierschouten/ts-events

Repository files navigation

NPM version license

NPM NPM

ts-events

A library for sending spontaneous events similar to Qt signal/slot or C# events. It replaces EventEmitter, and instead makes each event into a member which is its own little emitter. Implemented in TypeScript (typings file included) and usable with JavaScript as well.

You may also want to check out https://github.com/garronej/ts-evt which is a library with a similar approach.

TL;DR

Synchronous events:

import {SyncEvent} from 'ts-events';

const evtChange = new SyncEvent<string>();
evtChange.attach(function(s) {
    console.log(s);
});
evtChange.post('hi!');
// at this point, 'hi!' was already printed on the console

A-synchronous events:

import {AsyncEvent} from 'ts-events';

const evtChange = new AsyncEvent<string>();
evtChange.attach(function(s) {
    console.log(s);
});
evtChange.post('hi!');
// 'hi!' will be printed to the console in the next Node.JS cycle

Queued events for fine-grained control:

import {QueuedEvent} from 'ts-events';
import * as tsEvents from 'ts-events';

const evtChange = new QueuedEvent<string>();
evtChange.attach(function(s) {
    console.log(s);
});
evtChange.post('hi!');
// the event is still in a global queue

tsEvents.flush();
// now, 'hi!' has been written to the console

Different ways of attaching:

// attach a function
evtChange.attach(function(s) {
    console.log(s);
});

// attach a function bound to an object
evtChange.attach(this, this.onChange);

// directly attach another event
evtChange.attach(this.evtChange);

// like EventEmitter.once(), add a handler which is automatically detached after being called
evtChange.once(function(s) {
    console.log(s);
});

Versatile events, let the subscriber choose:

import {AnyEvent} from 'ts-events';
import * as tsEvents from 'ts-events';

const evtChange = new AnyEvent<string>();
evtChange.attachSync(function(s) {
    console.log(s + ' this is synchronous.');
});
evtChange.attachAsync(function(s) {
    console.log(s + ' this is a-synchronous.');
});
evtChange.attachQueued(function(s) {
    console.log(s + ' this is queued.');
});
evtChange.post('hi!');
tsEvents.flush(); // only needed for queued

evtChange.onceAsync(function(s) {
    console.log(s + ' after this event, I will be detached and print no more');
});
// similar functions onceSync() and onceQueued() exist.

Features

  • Each event is a member, and its own little event emitter. Because of this, you have a place for comments to document them. And adding handlers is no longer on string basis.
  • For TypeScript users: made in TypeScript and type-safe. Typings are in ts-events.d.ts
  • Synchronous, a-synchronous and queued events
  • For a-synchronous events, you decide whether to use setImmediate(), setTimeout(, 0) or process.nextTick()
  • Recursion-safe: sending events from event handlers is possible, endless loops are detected
  • Attaching and detaching event handlers has clear semantics
  • Attach handlers bound to a certain object, i.e. no need for .bind(this)
  • Detach one handler, all handlers, or all handlers bound to a certain object
  • Decide on sync/a-sync/queued either in the publisher or in the subscriber

Installation

Node.JS

Install using: npm install ts-events or yarn install ts-events.

Then require the module in your code:

// JavaScript
var tc = require("ts-events");

// TypeScript
import * as tsEvents from "ts-events";

Browser

There are two options:

  • Browserify your Node.JS code
  • Use one of the ready-made UMD-wrapped browser bundles: ts-events.js or ts-events.min.js. You can find an example of ts-events and RequireJS in the examples directory

Usage

Event types

ts-events supports three event types: Synchronous, A-synchronous and Queued. Here is a comparison:

Event Type Handler Invocation Condensable?
Synchronous directly, within the call to post() no
A-synchronous in the next Node.JS cycle yes
Queued when you flush the queue manually yes

In the table above, 'condensable' means that you can choose to condense multiple sent events into one: e.g. for an a-synchronous event, you can opt that if it is sent more than once in a Node.JS cycle, the event handlers are invoked only once.

There is a fourth event called AnyEvent, which can act as a Sync/Async/Queued event depending on how you attach listeners.

Synchronous Events

If you want EventEmitter-style events, then use SyncEvent. The handlers of SyncEvents are called directly when you emit the event.

import {SyncEvent} from 'ts-events';

const myEvent = new SyncEvent();

myEvent.attach(function(s) {
    console.log(s);
});

myEvent.post('hi!');
// at this point, 'hi!' was already printed on the console

Typically you use events as members in a class, instead of extending EventEmitter:

import {SyncEvent} from 'ts-events';

export class Counter {
    /**
     * This event is called whenever the counter changes
     * @param n The counter value
     */
    public evtChanged: SyncEvent<number> = new SyncEvent<number>();

    /**
     * The counter value
     */
    private _n = 0;

    public inc(): void {
        this._n++;
        this.evtChanged.post(this._n);
    }
};

const ctr = new Counter();

// Attach a handler to the event
// Do this instead of ctr.on('changed', ...)
ctr.evtChanged.attach((n: number): void => {
    console.log('The counter changed to: ' + n.toString(10));
});

ctr.inc();
// Here, the event handler is already called and you see a log line on the console

As you can see, each event is its own little emitter.

Recursion protection

Suppose that the handler for an event - directly or indirectly - causes the same event to be sent. For synchronous events, this would mean an infinite loop. SyncEvents have protection built-in: if a handler causes the same event to get posted 10 times recursively, an error is thrown. You can change or disable this behaviour with the static variable SyncEvent.MAX_RECURSION_DEPTH. Set it to undefined or null to disable or to a number greater than 0 to trigger the error sooner or later.

A-synchronous events

Synchronous events (like Node.JS EventEmitter events) have the nasty habit of invoking handlers when they don't expect it. Therefore we also have a-synchronous events: when you post an a-synchronous event, the handlers are called in the next Node.JS cycle. To use, simply use AsyncEvent instead of SyncEvent in the example above.

By default, AsyncEvent uses setImmediate() to defer a call to the next Node.JS cycle. You can change that by calling the static function AsyncEvent.setScheduler().

import {AsyncEvent} from 'ts-events';

// Replace the default setImmediate() call by a setTimeout(, 0) call
AsyncEvent.setScheduler(function(callback) {
    setTimeout(callback, 0);
})

Queued events

For fine-grained control, use a QueuedEvent instead of an AsyncEvent. All queued events remain in one queue until you flush it.

import {QueuedEvent} from 'ts-events';
import * as tsEvents from 'ts-events';

export class Counter {
    /**
     * This event is called whenever the counter changes
     * @param n The counter value
     */
    public evtChanged: QueuedEvent<number> = new QueuedEvent<number>();

    /**
     * The counter value
     */
    private _n = 0;

    public inc(): void {
        this._n++;
        this.evtChanged.post(this._n);
    }
};

const ctr = new Counter();

// Attach a handler to the event
// Do this instead of ctr.on('changed', ...)
ctr.evtChanged.attach((n: number): void => {
    console.log('The counter changed to: ' + n.toString(10));
});

ctr.inc();
// Here, the event handler is not called yet

// Flush the event queue
tsEvents.flush();
// Here, the handler is called

Creating your own event queues

You can put different events in different queues. By default, all events go into one global queue. To assign a specific queue to an event, do this:

import {QueuedEvent, EventQueue} from 'ts-events';

const myQueue = new EventQueue();
const myEvent = new QueuedEvent({ queue: myQueue });
myEvent.post('hi!');

// flush only my own queue
myQueue.flush();

flushOnce() vs flush()

Event queues have two flush functions:

  • flushOnce() calls all the events that are in the queue at the time of the call.
  • flush() keeps clearing the queue until it remains empty, i.e. events added by event handlers are also called.

The flush() function has a safeguard: by default, if it needs more than 10 iterations to clear the queue, it throws an error saying there is an endless recursion going on. You can give it a different limit if you like. Simply call e.g. flush(100) to set the limit to 100.

evtFilled and evtDrained

Event queues have two synchronous events themselves that fire when the queue becomes empty (evtDrained) or non-empty (evtFilled). The Filled event only occurs when an event is added to an empty queue OUTSIDE of a flush operation. The Drained event occurs at the end of a flush operation if the queue is flushed empty. To check whether the queue is empty, use the empty() method.

Condensing events

For a-synchronous events and for queued events, you can opt to condense multiple post() calls into one. If multiple post() calls happen before the handlers are called, the handlers are invoked only once, with the argument from the last post() call.

import {AsyncEvent} from 'ts-events';

// create a condensed event
const myEvent = new AsyncEvent<string>({ condensed: true });
myEvent.attach(function(s) {
    console.log(s);
});
myEvent.post('hi!');
myEvent.post('bye!');

// after a cycle, only 'bye!' is logged to the console

Binding to objects

There is no need to use .bind(this) when attaching a handler. Simply call myEvent.attach(this, myFunc);

Attaching and Detaching

There are clear semantics for the effect of attach() and detach(). These semantics were chosen to prevent surprises, however there is no reason why we should not support different semantics in the future. Please submit an issue if you need a different implementation.

  • Attaching a handler to an event guarantees that the handler is called only for events posted after the call to attach(). Events that are already underway will not invoke the handler.
  • Detaching a handler from an event guarantees that it is not called anymore, even if there are events still queued.
  • You can use the once() method to attach a handler that is automatically removed when it is called.

Attaching has the following forms:

const obj = {};
const handler = function() {
};
const myEvent = new AsyncEvent<string>();
const myOtherEvent = new AsyncEvent<string>();

myEvent.attach(handler); // will call handler with this === myEvent
myEvent.attach(obj, handler); // will call handler with this === obj
myEvent.attach(myOtherEvent); // will post myOtherEvent

Detaching has the following forms:

const obj = {};
const handler = function() {
};
const myEvent = new AsyncEvent<string>();
const myOtherEvent = new AsyncEvent<string>();

myEvent.detach(handler); // detaches all instances of the given handler
myEvent.detach(obj); // detaches all handlers bound to the given object
myEvent.detach(obj, handler); // detaches only the given handler bound to the given object
myEvent.detach(myOtherEvent); // detaches only myOtherEvent
myEvent.detach(); // detaches all handlers


// returned detacher function
const detacher = myEvent.attach(handler);
detacher(); // detachers `handler`

Note that when you attach an AsyncEvent to another AsyncEvent, the handlers of both events are called in the very next cycle, i.e. it does not take 2 cycles to call all handlers. This is 'decoupled enough' for most purposes and reduces latency.

Error events

The old EventEmitter treats 'error' events different from events with other names. If you emit them at a time when there are no listeners attached, then an error is thrown. You can get the same behaviour by using an ErrorSyncEvent, ErrorAsyncEvent or ErrorQueuedEvent.

const myEvent = new ErrorSyncEvent();

// this throws: 'error event posted while no listeners attached. Error: foo'
myEvent.post(new Error('foo'));

myEvent.attach((e: Error): void => {});

// this simply calls the event handler with the given error
myEvent.post(new Error('foo'));

AnyEvent

The AnyEvent class lets you choose between sync/async/queued in the attach() function. For instance:

import {AnyEvent, EventType} from 'ts-events';
import * as tsEvents from 'ts-events';

const evtChange = new AnyEvent();
evtChange.attach(function(s) {
    console.log(s + ' this is synchronous.');
});
evtChange.attach(EventType.Sync, function(s) {
    console.log(s + ' this is synchronous.');
});
evtChange.attach(EventType.Async, function(s) {
    console.log(s + ' this is a-synchronous.');
});
evtChange.attach(EventType.Queued, function(s) {
    console.log(s + ' this is queued.');
});

evtChange.once(function(s) {
    console.log(s + ' this is synchronous and will be called only once.');
});
evtChange.once(EventType.Sync, function(s) {
    console.log(s + ' this is synchronous and will be called only once.');
});
evtChange.once(EventType.Async, function(s) {
    console.log(s + ' this is a-synchronous and will be called only once.');
});
evtChange.once(EventType.Queued, function(s) {
    console.log(s + ' this is queued and will be called only once.');
});

// convenience functions:
evtChange.attachSync(function(s) {
    console.log(s + ' this is conveniently synchronous.');
});
evtChange.attachAsync(function(s) {
    console.log(s + ' this is conveniently a-synchronous and condensed.');
}, { condensed: true });
evtChange.attachAsync(function(s) {
    console.log(s + ' this is conveniently a-synchronous and not condensed.');
});
evtChange.attachQueued(function(s) {
    console.log(s + ' this is conveniently queued.');
});

// convenience functions:
evtChange.onceSync(function(s) {
    console.log(s + ' this is conveniently synchronous and will be called only once.');
});
evtChange.onceAsync(function(s) {
    console.log(s + ' this is conveniently a-synchronous and condensed and will be called only once.');
}, { condensed: true });
evtChange.onceAsync(function(s) {
    console.log(s + ' this is conveniently a-synchronous and not condensed and will be called only once.');
});
evtChange.onceQueued(function(s) {
    console.log(s + ' this is conveniently queued and will be called only once.');
});

evtChange.post('hi!');
tsEvents.flush(); // only needed for queued

No arguments

A TypeScript annoyance: when you create an event with a void argument, TypeScript forces you to pass 'undefined' to post(). To overcome this, we added VoidSyncEvent, VoidAsyncEvent and VoidQueuedEvent classes.

const myEvent = new SyncEvent<void>();

// annoying: have to pass undefined to post() to make it compile
myEvent.post(undefined)

// Solution:
const myEvent = new VoidSyncEvent();
myEvent.post(); // no need to pass 'undefined'

Listening to the listeners

Each type of event has a member evtListenersChanged: VoidSyncEvent to notify you when someone attaches or detaches event handlers.

Changelog

v3.4.1 (2022-02-04)

  • security updates

v3.4.0 (2020-02-07)

  • Add evtListenersChanged event to all types of events
  • Update dependencies

v3.3.1 (2019-06-04)

  • Remove .git directory from published module

v3.3.0 (2019-06-02)

  • Return a detacher function from attach() and once() methods.

v3.2.1 (2019-02-01)

  • Update dependencies to resolve vulnerabilities reported by npm audit.

v3.2.0 (2017-03-31)

  • Added once(), onceSync(), onceAsync() and onceQueued() methods to attach a handler that is automatically removed when called.

v3.1.5 (2017-01-11)

  • Moved node typings from dependencies to devDependencies

v3.1.4 (2016-11-26)

  • Use new @types typings
  • Upgrade NPM packages
  • Fix new TSLint errors

v3.1.3 (2016-10-28)

  • Inlined sourcemaps since the .map files were not published to npm

v3.1.2 (2016-10-27)

  • Inlined sourcemaps since the .map files were not published to npm

v3.1.1 (2016-02-27)

  • Removed dependency on util and assert

v3.1.0 (2016-02-27)

  • Add UMD browser bundles

v3.0.1

  • Bugfix in published NPM module

v3.0.0

  • Update whole module to 2016 standards (thanks to Tomasz Ciborski)
  • Ensure that ts-events works in browsers as well without setting another a-sync scheduler
  • Add generic way of attaching to an AnyEvent
  • Add evtFirstAttached and evtLastDetached events to AnyEvent

v2.4.0

  • Revert releases 2.0.1 * 2.3.0 because they don't work

v2.3.0 (2016-02-20)

  • Add evtFirstAttached and evtLastDetached events to AnyEvent

v2.2.0 (2016-02-20)

  • Add generic way of attaching to an AnyEvent

v2.1.1 (2016-02-20)

  • Ensure that ts-events works in browsers as well without setting another a-sync scheduler

v2.1.0 (2016-02-20)

  • Update whole module to 2016 standards (thanks to Tomasz Ciborski)

v2.0.0

  • Breaking change: removed AnyEvent#attach() and replaced it with attachSync() to force users of AnyEvents to think about how to attach.

v1.1.0 (2015-05-25):

  • Add events to the EventQueue to detect when it becomes empty/non-empty to facilitate intelligent flushing.
  • Add a new type of event called AnyEvent where the choice of sync/async/queued is left to the subscriber rather than the publisher.

v1.0.0 (2015-04-30):

  • Ready for production use.

v0.0.6 (2015-04-29):

  • Performance improvements

v0.0.5 (2015-04-29):

  • Fix NPM warning about package.json repository field

v0.0.4 (2015-04-29):

  • Fix missing ts-events.d.ts in published module

v0.0.3 (2015-04-28):

  • Feature: allow to attach any event to any other event directly
  • Feature: allow to disable recursion protection mechanism for SyncEvents
  • Breaking change: renamed flushEmpty() to flush()
  • Documentation updates
  • Various build system improvements

v0.0.2 (2015-04-27):

  • Documentation update

v0.0.1 (2015-04-27):

  • Initial version

License

Copyright � 2015 Rogier Schouten github@workingcode.ninja ISC (see LICENSE file in repository root).

About

Various EventEmitter event replacements with synchronous, a-synchronous, and queued events. Events are members. Usable with JavaScript and TypeScript.

Resources

License

Stars

Watchers

Forks

Packages

No packages published

Contributors 4

  •  
  •  
  •  
  •