EventEmitter is a TypeScript library that provides a simple way to have different parts of your application communicate with each other.
You can install it via NPM:
npm install @anephenix/event-emitter
I've been building some proof of concept applications using Svelte, which has been an ejoyable frontend library to code in. I can see why it is getting picked up by companies like Stack Overflow for building web applications.
One of the proof of concept applications is a very basic Digital Audio Workstation (DAW), or music production software as others may describe it. It is "Let's Make Sweet Music, and you can checkout the code at https://github.com/paulbjensen/lets-make-sweet-music.
In the app, I had built a way to trigger the playback of sounds using a virtual musical keyboard, which would then play audio sounds that I had recorded using a microphone (eventually these were replaced with a virtual synthesizer). I later realised that I could record the track notes in tracks with timestamps, to then enable the ability to play back the notes in a track. Later on I was then able to connect this to a piece of code that recorded the sounds and saved the audio to a ogg music file and downloaded it.
As the code started to grow, I wanted to find a way to keep the components well-contained in terms of what they did, and not have them linked together. How could I achieve that?
One idea that came to mind was to use an EventEmitter.
I could setup an instance of an EventEmitter class, pass it to all of the components that would use it, and then have a way for an event occurring in one component to then be acted on by other components that were interested in a particular event occurring. Similar to the Publish/Subscribe pattern used in Realtime web applications with WebSockets or backend systems with message queues.
Therefore I implemented a basic EventEmitter class in TypeScript similar to the one provided by Node.js.
I would first initiate the EventEmitter instance:
const eventEmitter = new EventEmitter();
Then I would pass the eventEmitter instance as a property to the Svelte component for it to then be able to trigger an event on.
eventEmitter.trigger('playSound', { note: 'B'});
Then another component (receiving the same eventEmitter
instance via $props()
) could then setup a function to react to any events with the name playSound
:
eventEmitter.on('playSound', (data) => {
soundBox.playSound(data.note);
});
This way, the components could remain well-contained and yet through the shared eventEmitter instance send data between them.
The second Svelte application I made was a little ball-based game called 3D garden. I found myself wanting to do the same thing - organise and structure the code into well-contained components (with the lofty goal that one day I could just drag and drop components into other projects as I desired - in theory Svelte components are designed to be self-contained).
The EventEmitter class file was copied over from the "Let's Make Sweet Music" project. It was then that I realised that perhaps the file should be a library instead.
One of the initial problems I encountered with the eventEmitter approach was ensuring that the spelling of the event names was correct. A small typo or a wrongly-remembered event name would end up with cases where events were triggered but not reacted to, and vice versa.
Initially I tried to solve this by passing an optional typedEvents
array property that specified a list of official event names, and would throw an error if there were any attempts to call an event name that was not present in that original list.
However, this felt like it handled the issue much further down the lifecycle - at runtime rather than at code linting/build time when it could be flagged.
A bit of chinwagging with ChatGPT and a better approach was found that enabled the ability to pass a Record of keys and functions to the EventEmitter
class that could apply a type to the event names, as well as to the data passed by the emit
, on
, and off
function calls.
I could define an EventMap
type in the code and pass it to the EventEmitter
class like so:
import type { Body } from "./types";
import EventEmitter from "@anephenix/event-emitter";
type EventMap = {
ballFellOff: () => void;
gameOver: ({winner}: {winner: Body | null}) => void;
playerAction: (action: string) => void;
playerPositionUpdate: (position: {
x: number;
y: number;
z: number
}) => void;
gameRestart: () => void;
};
const eventEmitter = new EventEmitter<EventMap>();
export default eventEmitter;
That way, when I write code that is calling emit
, on
, or off
with the event name and the data payload, it will check that they are correct.
If I wanted to log out all of the events being emitted, I could set the enableLogging
property to true
and it would log the event name and data to the console.
const eventEmitter = new EventEmitter();
// Set this flag to true to enable logging
eventEmitter.enableLogging = true;
The EventEmitter library provided a nice way to keep Svelte components well-contained and decoupled from each other, while still having a way to connect them so that they could trigger and react to events that would occur. The goal in the future is to start making reusable components that I can drop into other projects as a single file.
If you want to explore those features in detail, checkout the README on the GitHub repository.