Events are an intuitive way to model the execution flows of applications when several modules, each one with complex lifecycles, need to synchronize with each other. This is especially true if the application involves frequent user-initiated inputs.
The application I maintain at work falls exactly in this space (it is a backend system for managing interactive conversational bots). Yes, you could happily use a package for that (here a good one), but I always prefer to understand how things work under the hood.
For this reason, in this article I’ll present you the implementation of a very simple event system for TypeScript.
Simple Synchronous Signals
At the very basic, an Event is just a simple object to which someone can subscribe and receive a signal when the event is triggered. We can imagine a class with three basic methods:
on
to subscribe to the event/signal.off
to unsubscribetrigger
used to trigger the event (and notify the subscribers).
I will call this class Signal
(I will not use Event
because Event
frequently name-clash in TypeScript).
|
|
Very simple. Does it work? It is easy to check. Let’s create a class Dog
with BarkEvent
event.
|
|
Now, let’s create a class to receive the event. For instance:
|
|
And finally let’s run everything:
|
|
A slightly improved example
That’s cool. But there is a problem.
|
|
Ops. Now the listener can make the dog bark! This is a clear violation of what an event subscriber can do. Luckily, we can easily fix this.
First, let’s create a public interface for our signal. This will only include the methods that can be used by the listeners. No triggers.
|
|
Now we can edit our Signal
class. We will implement this class and provide an expose
method:
|
|
And update the event on Dog
in this way:
|
|
Because, expose
only returns an ISignal
, the listener would be unable to trigger the event.1
Now the EvilDogListner
will not compile, and we will get the error: Property 'trigger' does not exist on type 'ISignal<Dog, string>'.
Simple Asynchronous Signals
The Signal
we implemented before can handle correctly synchronous events: when the event is triggered, we run the subscribed function, and it immediately returns. Blocking the execution until every event handler is executed. But what if we want to subscribe an async
function? After all, TypeScript and JavaScript are heavily asynchronous and event handlers may do a lot of asynchronous stuff (e.g., doing API requests, updating databases and so on).
The upgrade is simple. We will change the handler function signature from (source: S, data: T) => void
to (source: S, data: T) => Promise<void>
.
|
|
The trigger
method is now async
and, when called, it will immediately return without waiting for the completion of the several handlers.
In some cases, we may want to trigger an event and wait for all the subscribers to complete like in the synchronous case. For this reason, we will also add a triggerAwait
version of trigger
.
Asynchronous Signals with Binding
I will now show you a variation of AsyncSignal
that is slightly more robust (depending on the use cases). With the previous version, in fact, it is possible to double-subscribe to an event.
A better solution is to not attach a handler as a function, instead, let’s use an object containing more information on the binding.
|
|
We can put there all the info we need to tune the binding as we need. In a simple version, we can just add an identifier for the listener so that we can guarantee to have just one handler for each listener (this is useful if the application you are working with can create a lot of dynamic listeners).
The extended implementation is easy. In the example below I changed on/off
with bind/unbind
, just to have a clearer distinction, but you can keep on/off
if you prefer. Also, in the implementation I used lodash
to simplify the contains
function, but you can avoid that with some extra code.
|
|
Conclusion
I hope this is useful for someone. I know there is probably a better solution for that, but this simple event system is serving me well so far. If you think it can be improved in any way, just let me know!
All the code examples in this page are released under the MIT license. You can find the final example at this link.
Photo by Will Araya on Unsplash
Of course, they could cast it to
Signal
… but this will stop not self-damaging coders. ↩︎