@spearwolf/eventize
v4.0.1
Published
![npm (scoped)](https://img.shields.io/npm/v/%40spearwolf/eventize) ![GitHub Workflow Status (with event)](https://img.shields.io/github/actions/workflow/status/spearwolf/eventize/main.yml) ![GitHub](https://img.shields.io/github/license/spearwolf/eventiz
Downloads
157
Readme
@spearwolf/eventize
Introduction 👀
A tiny and clever framework for synchronous event-driven programming in Javascript.
Yes, you read that right: the event emitters here call the subscribers synchronously and not asynchronously like in node.js events for example.
This is perfectly reasonable: sometimes you want to have control over when something happens, e.g. when your code runs inside an animation frame. Or you might want to free resources immediately and instantly.
FEATURES
- :rocket: smart api with focus on developer experience
- :sparkles: wildcards &❗priorities❕
- includes typescript types (well, actually it is written in typescript) :tada:
- supports all major browsers and node.js environments, targeting
ES2022
- very small footprint ~3k gzip'd
- no runtime dependencies
- Apache 2.0 licence
⚙️ Installation
All you need to do is install the package:
$ npm i @spearwolf/eventize
The package exports the library in esm format (using import
and export
syntax) and also in commonjs format (using require
).
It is compiled with ES2022
as target, so there are no downgrades to older javascript syntax and features.
The typescript type definitions are also included in the package.
| 🔎 Since version 3.0.0 there is also a CHANGELOG
📖 Getting Started
The underlying concept is simple: certain types of objects (called "emitters") emit named events that cause function "listeners" to be called.
Emitter
🔎 Emitter is a synonym for an eventized object, which in turn is a synonym for an object instance that has the eventize superpowers attached to it! In this documentation we also use ε as a variable name to indicate that it is an eventized object.
Any object can become an emitter; to do so, the object must be upgraded:
import {eventize} from '@spearwolf/eventize'
// !!! THIS IS THE RECOMMENDED AND MOST DIRECT APPROACH TO CREATE AN EVENTIZED OBJECT !!!
const eventizedObj = eventize(obj)
🔎 If you don't want to specify an object, just leave it out and
{}
will be created for you:const ε = eventize()
or, if you are more familiar with class-based objects, you can use
import {Eventize} from '@spearwolf/eventize'
class Foo extends Eventize {}
const ε = new Foo()
// ε is now an object with eventize superpowers 🚀
For typescript, the following composition over inheritance variant has also worked well:
import {eventize, type Eventize} from '@spearwolf/eventize'
export interface Foo extends Eventize {}
export class Foo {
constructor() {
eventize.inject(this);
}
}
Since version 4.0.0 there is the functional eventize API, so it is now possible to use eventize()
in the constructor without any additions:
import {eventize} from '@spearwolf/eventize'
export class Foo {
constructor() {
eventize(this);
}
}
Listener or Subscriptions
Any function can be used as a listener. However, you can also use an object that defines methods with the exact name of the given event.
// ε is an eventized object
ε.on('foo', (bar) => {
console.log('I am a listener function and you called me with bar=', bar)
})
ε.on('foo', {
foo(bar, plah) {
console.log('I am a method and you called me with bar=', bar, 'and plah=', plah)
}
})
ε.on({
foo(bar, plah) {
console.log('foo ->', {bar, plah})
},
bar() {
console.log('hej')
}
})
Named Events
An emitter can emit any event name; parameters are optional
ε.emit('bar')
// => "hej"
ε.emit('foo', 123, 456)
// => "I am a listener function and you called me with bar= 123"
// => "I am a method and you called me with bar= 123 and plah= 456"
// => "foo -> {bar: 123, plah: 456}"
If an emitter emits an event to which no listeners are attached, nothing happens.
🔎 an event name can be either a string or a symbol
📚 API
How to emitter
EventizedObject vs. EventizeApi
To give an object the eventize superpowers, it needs to be initialized once. for this purpose, there is the eventize
function. The result is an EventizedObject
.
To use the eventize API, the functions are available as named exports. The API currently includes the following functions:
| function | description | |--------|-------------| | on | subscribe to events | | once | subscribe to the next event only | | onceAsync | the async version of subscribe only to the next event | | emit | dispatch an event | | emitAsync | dispatch an event and wait for any promises returned by subscribers | | off | unsubscribe | | retain | hold the last event until it is received by a subscriber | | retainClear | clear the last event |
Example
import {eventize, on, emit} from '@spearwolf/eventize';
const obj = eventize();
on(obj, 'foo', () => console.log('foo called'));
emit(obj, 'foo'); // => call foo subscriber
EventizeApi
If the Eventize
base class or eventize.inject()
is used instead of eventize()
, an eventized object is also returned, but here additionally with the EventizeApi attached as as methods:
import {Eventize, on, emit} from '@spearwolf/eventize';
class Foo extends Eventize {}
const obj = new Foo();
obj.on('foo', () => console.log('foo called'));
obj.emit('foo'); // => call foo subscriber
emit(obj, 'foo'); // => call foo subscriber
EventizedObject vs EventizeApi Overview Matrix
There are several ways to convert any object into an emitter / eventized object.
| Method | is EventizedObject | has EventizeApi injected |
|--------|--------------------|--------------------------|
| eventize(obj)
| ✅ | ❌ |
| eventize.inject(obj)
| ✅ | ✅ |
| class extends Eventize {}
| ✅ | ✅ |
eventize
The easiest way to create an eventized object is to use the eventize
function. The result is an object with eventize superpowers, which can be accessed using the eventize API functions:
eventize( myObj ) // => myObj
eventize.inject
Alternatively, it is possible to create an eventized object which has the complete eventize api injected as methods at the same time
eventize.inject( myObj ) // => myObj
Returns the same object, with the eventize api attached, by modifying the original object.
Class-based inheritance
The class-based approach is essentially the same as the extend method, but differs in how it is used:
import {Eventize} from '@spearwolf/eventize'
class Foo extends Eventize {
// constructor() {
// super()
// }
}
Class-based, without inheritance
If you want to create an emitter class-based, but not via inheritance, you can also use the eventize method in the constructor, here as a typescript example:
import {eventize, Eventize} from '@spearwolf/eventize'
interface Foo extends Eventize {}
class Foo {
constructor() {
eventize.inject(this)
}
}
Eventize API
Each emitter / eventized object provides an API for subscribing, unsubscribing and emitting events. This API is called the eventize API (because "emitter eventize API" is a bit too long and cumbersome).
| method | description |
|--------|-------------|
| on( .. )
| subscribe to events |
| once( .. )
| subscribe only to the next event |
| onceAsync( .. )
| the async version of subscribe only to the next event |
| off( .. )
| unsubscribe |
| retain( .. )
| hold the last event until it is received by a subscriber |
| retainClear( .. )
| clear the last event |
| emit( .. )
| dispatch an event |
| emitAsync( .. )
| dispatch an event and waits for all promises returned by the subscribers |
All methods can be used in the functional variant:
on(obj, ...)
emit(obj, ...)
// ..
the objects that have been injected with the eventize api also offer the api as methods:
obj.on(...)
obj.emit(...)
// ..
These API methods are described in detail below:
How to listen
on
on(ε, .. )
ε.on( .. )
The simplest and most direct way is to use a function to subscribe to an event:
import {eventize} from '@spearwolf/eventize'
const ε = eventize()
// short version
ε.on('foo', (a, b) => {
console.log('foo ->', {a, b});
});
// extended version
const unsubscribe = ε.on('foo', (a, b) => {
console.log('foo ->', {a, b});
});
The listener function is called when the named event is emitted. The parameters of the listener function are optional and will be filled with the event parameters later (if there are any).
The return value of on()
is always the inverse of the call — the unsubscription of the listener.
Wildcards
If you want to respond to all events, not just a specific named event, you can use the catch-em-all wildcard event *
:
ε.on('*', (...args) => console.log('an event occured, args=', ...args))
If you wish, you can simply omit the wildcard event:
ε.on((...args) => console.log('an event occured, args=', ...args))
Multiple event names
Instead of using a wildcard, you can specify multiple event names:
ε.on(['foo', 'bar'], (...args) => console.log('foo or bar occured, args=', ...args))
Priorities
Sometimes you also want to control the order in which the listeners are called.
By default, the listeners are called in the order in which they are subscribed — in their priority group; a priority group is defined by a number, where the default priority group is 0
and large numbers take precedence over small ones.
ε.on('foo', () => console.log("I don't care when I'm called"))
ε.on('foo', -999, () => console.log("I want to be the last in line"))
ε.on(Number.MAX_VALUE, () => console.log("I will be the first"))
ε.emit('foo')
// => "I will be the first"
// => "I don't care when I'm called"
// => "I want to be the last in line"
Listener objects
You can also use a listener object instead of a function:
ε.on('foo', {
foo(...args) {
console.log('foo called with args=', ...args)
}
})
This is quite useful in conjunction with wildcards:
const Init = Symbol('init') // yes, symbols are used here as event names
const Render = Symbol('render')
const Dispose = Symbol('dispose')
ε.on({
[Init]() {
// initialize
}
[Render]() {
// show something
}
[Dispose]() {
// dispose resources
}
})
.. or multiple event names:
ε.on(['init', 'dispose'], {
init() {
// initialize
}
goWild() {
// will probably not be called
}
dispose()) {
// dispose resources
}
})
Of course, this also works with priorities:
ε.on(1000, {
foo() {
console.log('foo!')
}
bar() {
console.log('bar!')
}
})
As a last option, it is also possible to pass the listener method as a name or function to be called in addition to the listener object.
Named listener object method
ε.on('hello', 'say', {
say(hello) {
console.log('hello', hello)
}
})
ε.emit('hello', 'world')
// => "hello world"
Listener function with explicit context
ε.on(
'hello',
function() {
console.log('hello', this.receiver)
}, {
receiver: 'world'
});
ε.emit('hello')
// => "hello world"
Complete on() method signature overview
Finally, here is an overview of all possible call signatures of the .on( .. )
method:
.on( eventName*, [ priority, ] listenerFunc [, listenerObject] )
.on( eventName*, [ priority, ] listenerFuncName, listenerObject )
.on( eventName*, [ priority, ] listenerObject )
Additional shortcuts for the wildcard *
syntax:
.on( [ priority, ] listenerFunc [, listenerObject] )
.on( [ priority, ] listenerObject )
Legend
| argument | type |
|----------|------|
| eventName*
| eventName or eventName[] |
| eventName
| string or symbol |
| listenerFunc
| function |
| listenerFuncName
| string or symbol |
| listenerObject
| object |
once
once(ε, .. )
ε.once( .. )
once()
does exactly the same as on()
, with the difference that the listener is automatically unsubscribed after being called, so the listener method is called exactly once. No more and no less – there is really nothing more to say about once.
| 🔎 if called with multiple event names, the first called event wins
ε.once('hi', () => console.log('hello'))
ε.emit('hi')
// => "hello"
ε.emit('hi')
// => (nothing happens here)
onceAsync
onceAsync(ε, eventName | eventName[] )
ε.onceAsync( eventName | eventName[] )
since v3.3.*
This creates a promise that will be fulfilled if one of the given events is emitted.
// at this point please do nothing, just wait
await ε.onceAsync('loaded')
// a little later, somewhere else in the program
ε.emit('loaded')
off
off(ε, .. )
ε.off( .. )
The art of unsubscribing
At the beginning we learned that each call to on()
returns an unsubscribe function. You can think of this as on()
creating a link to the event listener.
When this unsubscribe function is called, the link is removed.
So far, so good. Now let's say we write code that should respond to a dynamically generated event name with a particular method, e.g:
const queue = eventize()
class Greeter {
listenTo(name) {
queue.on(name, 'sayHello', this)
}
sayHello() {
// do what must be done
}
}
const greeter = new Greeter()
greeter.listenTo('suzuka')
greeter.listenTo('yui')
greeter.listenTo('moa')
To silence our greeter, we would have to call the unsubscribe function returned by on()
for every call to listenTo()
. Quite inconvenient. This is where off()
comes in. With off()
we can specifically disable one or more previously established links. In this case this would be
queue.off(greeter)
... this will cancel all subscriptions from queue
to greeter
!
All kinds of .off()
parameters in the summary
.off()
supports a number of variants, saving you from caching unsubscribe functions:
| .off()
parameter | description |
|-|-|
| ε.off(function)
| unsubscribe by function |
| ε.off(function, object)
| unsubscribe by function and object context |
| ε.off(eventName)
| unsubscribe by event name |
| ε.off(object)
| unsubscribe by object |
| ε.off()
| unsubscribe all listeners attached to ε |
🔎 For those with unanswered questions, we recommend a look at the detailed test cases ./src/off.spec.ts
getSubscriptionCount()
A small helper function that returns the number of subscriptions to the object. Very useful for tests, for example.
import {getSubscriptionCount} from '@spearwolf/eventize';
getSubscriptionCount(ε) // => number of active subscriptions
How to emit events
emit
emit(ε, .. )
ε.emit( .. )
Creating an event is fairly simple and straightforward:
ε.emit('foo', 'bar', 666)
That's it. No return value. All subscribed event listeners are immediately invoked.
The first argument is the name of the event. This can be a string or a symbol. All other parameters are optional and will be passed to the listener.
If you want to send multiple events at once - with the same parameters - you can simply pass an array of event names as the first parameter:
ε.emit(['foo', 'bar'], 'plah', 666)
emitAsync
emitAsync(ε, .. )
ε.emitAsync( .. )
since v3.1.*
const results = await ε.emitAsync('load');
Emits an event and waits for all promises returned by the subscribers.
Unlike the normal emit()
, here it is taken into account whether the subscribers return something.
If so, then all results are treated as promises and only when all have been resolved are the results
returned as an array.
Anything that is not null
or undefined
is considered a return value.
If there are no return values, then simply undefined
is returned.
All arguments that are allowed in emit()
are supported.
retain
retain(ε, eventName | eventName[] )
ε.retain( eventName | eventName[] )
Emit the last event to new subscribers
ε.retain('foo')
With retain
the last transmitted event is stored. Any new listener will get the last event, even if it was sent before they subscribed.
NOTE: This behaviour is similar to the
new ReplaySubject(1)
of rxjs. But somehow the method nameretain
seemed more appropriate here.
retainClear
retainClear(ε, eventName | eventName[] )
ε.retainClear( eventName | eventName[] )
Clear the last event
since v3.3.*
ε.retainClear('foo')
With retainClear()
the retain mode for the event is kept, but if there is already an event that is stored, it will now be cleared.