Skip to content

Core: The Event Bus

Every event a player emits, from play to a plugin’s own custom events, flows through one class: EventEmitter.

What it is

EventEmitter<E> is a small, dependency-free pub/sub implementation, generic over an event map E that types each event name to its payload shape. Both NMVideoPlayer and NMMusicPlayer extend it directly, so every player instance already has on, once, off, emit, hasListeners, and listenerCount before a single kit mixin is composed onto it.

It’s a plain Map<event, Set<handler>> under the hood, not the DOM’s EventTarget, on purpose: a Set lets off(event, fn) remove a listener by reference without matching options, and that composes cleanly with once() in a way removeEventListener doesn’t. emit() snapshots the listener set before iterating, so a listener that calls off() on itself mid-dispatch doesn’t corrupt the current pass, and it catches exceptions from individual listeners so one bad handler can’t abort the rest of the chain.

The firehose

on('all', fn) registers a listener called for every emit, with (eventName, data) as its arguments. This is what player.on('all', console.log) and every debug overlay in the trio is built on. off('all') with no function removes every listener across every event, which is what dispose() calls to guarantee a disposed player leaks nothing.

Using it in a player library

You won’t usually construct an EventEmitter yourself when building on the kit: your player class extends it, and its typed event map is whatever interface you declare (the kit’s own BaseEventMap, extended with your own events, is the normal starting point). The example below builds a standalone bus with a small custom event map, so the mechanics are visible without a full player around them.

TypeScript
/**
* `EventEmitter` is the typed pub/sub primitive every kit mixin, plugin, and
* player class is built on. `on` / `once` / `off` / `emit` are strongly typed
* against an event map you supply as a generic; `on('all', fn)` is the
* untyped firehose every debugging overlay and `player.on('all', console.log)`
* call relies on. This example builds a standalone bus with no player
* involved — the same class both `NMVideoPlayer` and `NMMusicPlayer` extend.
*/

import { EventEmitter } from '@nomercy-entertainment/nomercy-player-core';

interface DemoEvents {
greeting: { name: string };
}

const bus = new EventEmitter<DemoEvents>();

bus.on('greeting', ({ name }) => console.log(`hello, ${name}`));

// once() self-removes after its first call — no manual off() needed to clean up.
bus.once('greeting', ({ name }) => console.log(`(once) hi, ${name}`));

// 'all' is the untyped firehose: called for every emit with (eventName, data).
bus.on('all', (event, data) => console.log('firehose:', event, data));

bus.emit('greeting', { name: 'Ada' });
bus.emit('greeting', { name: 'Grace' }); // the once() listener already fired and detached

console.log(bus.hasListeners('greeting')); // true — the typed listener is still registered
console.log(bus.listenerCount()); // 2: the typed listener + the firehose listener

bus.off('greeting');
console.log(bus.hasListeners('greeting')); // false

The cancellable variant

Plain emit() is fire-and-forget: it can’t be cancelled, delayed, or mutated by a listener. Every before* event (beforePlay, beforeSeek, beforeMutation, and the equivalent on a Plugin’s own custom events) runs through a different dispatcher built on top of this same listener registry, one that gives listeners a mutable, preventable payload. That’s the subject of the next page.

Next steps

  • Transport Contract: the cancellable before* dispatch pattern, demonstrated on play() / pause() / seek().