Understanding Events in Node.js

Understanding Streams in Node.js

Node.js is an Event-Driven architecture, when Node starts a server it initializes its memory by declaring variables and functions, and then it waits for the events to occur. this is the nature of Node it gets into the main loop that listens for events and then invokes a callback function when one of the events is fired.


Events in the Browser

Javascript was designed in a different way than other languages, So now when you are reading this article, Javascript is waiting for you to interact with the page, Clicking on the navigator, clicking on some button, or scrolling down. It is ready to react to your actions and start waiting again, anything you do triggers an Event that Event is nothing but an object with data and then that object is sent to Javascript, Javascript then puts all Events in the queue "The Event Queue",

Event Queue is A queue stores a series of events from the application in first-in, first-out order. Sending a notification enqueues the event/message and returns.

Event Queue concept

Javascript is event-based because it reacts to events, it maintains a list of events that happen, loops over the events, and calls the function that is connected to that event.

You can check here what events you can listen to it in Javascript on the browser.

There are some concepts when you begin to interact to Javascript events.

  1. event handler: this is a function that runs when an event fires.

  2. event listener: is a function that initiates a predefined process if a specific event occurs, So, an event listener listens for action, and when this action triggers, it will call the event handler that provided for the event. in other words, an event listener watches for an event on an element, you can add an event handler to any event from the website above.

in the example below I used the addEventListener to initiate sayHello() function to the button below, So, if I clicked on the button it will fire the sayHello() function

index.html
Copy
1 ...
2  <button id="btn">Click Me!</button>
3  ...
4  
main.js
Copy
1 
2    function sayHello(){
3        alert("Hello There!!")
4    }
5
6    // here I am selecting the button element from the DOM.
7    const button = document.querySelector("#btn");
8
9    // I am attaching sayHello function with the event "click" on that button
10    // so if anyone clicks on the button the event listener will call
11    // sayHello function
12    button.addEventListener("click",sayHello);
13
14

You can try it out here.

That was how events are workking on Javascript on the browser, but what about events in Node.js environment.

Events in Node.js.

Unlike the browser that has a DOM tree, Javascript can select a node and start to listening on what events have been made on that element, Node.js doesn't work that way, Node doesn't have a DOM tree, Althogh Node is an Event-Driven Archetecture.

Node uses The EventEmitter module for its event-driven system.

The EventEmitter is a module that facilitates the creation of publisher-subscriber pattern(The Observer pattern), Many of Node’s built-in modules inherit from EventEmitter to achieve the Event-Driven architecture, i.e. HTTP request, response, and streams implement the EventEmitter module so they can provide a way to emit and listen to the event.

but before using the EventEmitter class to emit events or listen to the events, I would like to show you how the Observer Pattern works.

Observer Pattern is the pattern Node.js uses for its EventEmitter module, so if you understand the observer pattern well, you will face no problems understanding the EventEmitter internal working mechanism.

The Observer pattern.

We can subscribe certain objects called the observers, to another object called the observable. Whenever an event emits the Observable notifies all its observers.

So an Observable object contains four parts:

  1. observers: a list of objects that subscribe to a specific event, and will be notified when that event occurs.
  2. subscribe: a method that takes an observer or several observers, to add them to the observer's list.
  3. unsubscribe: a method that takes an observer or several observers, to remove them from the observer's list.
  4. notify: a method that calls all observers whenever a specific event occurs.

So to demonstrate these concepts let's build a small project:

Suppose we have an eCommerce website that consists of other two entities Marketing and Shipment, in our website, we have a payment, and whenever a user bought something we want to notify The Marketing and Shipment entities.

So to break things up:

  • Marketing and Shipment are the Observers.
  • Payment is the Observable that will contain all the above four parts.

Here how I strcutured my foldres.

Observer pattern folder strucure.
  • events: contains the payment class that is resposible for dealing with the user, so if the user bought something, the payment class has an instance from the payment observable and it will call the notify method from that instance.
  • observers: contains the marketing and shipment entities that subscribe for an event "the user bought something" these classes have an update method, this update method will be called when a user bought something.
  • observable: contains the payment observable class.
src/observables/PaymentObservable.js
Copy
1
2export default class PaymentObservable {
3    // private set to holds the observers.
4    #observers = new Set();
5
6    notify(data) {
7      this.#observers.forEach((observer) => {
8        observer.update(data);
9      });
10    }
11
12    subscribe(observable) {
13      this.#observers.add(observable);
14    }
15
16    unsubscribe(observable) {
17      this.#observers.delete(observable);
18    }
19
20}
21
src/observers/marketing.js
Copy
1
2export default class Marketing {
3  update({ id, username }) {
4    console.log("id: ", id, "[Marketing] will send welcome to ", username);
5  }
6}
7
src/observers/shipment.js
Copy
1
2export default class Shipment {
3  update({ id, username }) {
4    console.log("id: ", id, "[Shipment] will send welcome to ",username);
5  }
6}
7
src/events/payment.js
Copy
1
2export default class Payment {
3  constructor(subject) {
4    this.paymentSubject = subject;
5  }
6  creditCard(paymentData) {
7    console.log("payment occur from ", paymentData.username);
8    this.paymentSubject.notify(paymentData);
9  }
10}
11

Now all components are ready to connect them with each other.

src/index.js
Copy
1
2import Payment from "./events/payment.js";
3import Shipment from "./observers/shipment.js";
4import Marketing from "./observers/marketing.js";
5import PaymentOservable from "./observables/PaymentObservable.js";
6
7// creating an instance from PaymentOservable.
8const observable = new PaymentOservable();
9
10// creating an instance from Marketing observer.
11// and pass it as observer to the payment PaymentOservable instance
12const marketingObserver = new Marketing();
13observable.subscribe(marketingObserver);
14
15const shipmentObserver = new Shipment();
16observable.subscribe(shipmentObserver);
17
18// now we create an instance from the payment.
19// and pass the observable to it, this pattern is knowen as dependency injection
20const payment = new Payment(observable);
21
22// so when any user bought something, we call the payment method creditCard.
23// which in turn calls the notify method from the observable instance we pass to it above
24// which also calls the update method on all it's observers.
25payment.creditCard({ id: 1, username: "Islam" });
26
27// so at anytime if we want to unsubscribe an observer for any reason.
28// we call the unsubscribe method from the PaymentOservable instance
29// and pass the observer to id
30observable.unsubscribe(marketingObserver);
31
32// after removing marketing from the listeners we got different output
33payment.creditCard({ id: 2, username: "Mohamed" });
34

If we run the code above we get this output

Terminal
Copy
➜  observer-pattern node src/index.js
payment occur from Islam
id:  1 [Marketing] will send welcome to  Islam
id:  1 [Shipment] will send welcome to  Islam
payment occur from Mohamed
id:  2 [Shipment] will send welcome to  Mohamed

Very easy to understand right!

Now let us define the EventEmitter module and how to use it.

The EventEmitter

Node.js has already implemented the observer pattern for us through the EventEmitter class.

The EventEmitter class is very simple in it is nature, it allows us to register one or more functions as listeners on a specific event, and when that event or fire emits, these functions will be invoked.

Creating an EventEmitter

To create an event emitter, we need first to create an instance from the EventEmitter class that is exported from the events module.

Copy
1import { EventEmitter } from 'events';
2const emitter = new EventEmitter();

If you inspected the emitter instance you see several methods in it.

the important method you are gonna be using are:

  • emitter.on(eventName, listener): Adds the listener function to the end of the listeners array for the event named eventName. No checks are made to see if the listener has already been added. Multiple calls passing the same combination of eventName and listener will result in the listener being added, and called, multiple times.
  • emitter.emit(eventName[, ...args]): Synchronously calls each of the listeners registered for the event named eventName, in the order they were registered, passing the supplied arguments to each.
  • emitter.once(eventName, listener): Adds a one-time listener function for the event named eventName. The next time eventName is triggered, this listener is removed and then invoked.
  • emitter.removeListener(eventName, listener): Removes the specified listener from the listener array for the event named eventName.

You can see the rest of the methods in the Official Node Docs here.

Using The emitter

index.js
Copy
1
2import { EventEmitter } from "events";
3
4const emitter = new EventEmitter();
5
6const marketing = (data) =>
7console.log("[marketing]: User with id", data.id, "has bought something");
8
9const shipment = (data) =>
10console.log("[Shipment]: User with id", data.id, "has bought something");
11
12const sayHelloOnec = () => console.log("i am only invoked once!");
13
14// here i registered shipment and marketing functions to the event "buy"
15// these two function will be called when the event "buy" fired.
16
17emitter.on("buy", shipment);
18emitter.on("buy", marketing);
19
20// registered sayHelloOnec to event "buy"
21// but the different here that sayHelloOnec will be called only one time.
22// when the "buy" event fired, sayHelloOnec will be removed and then invoked.
23emitter.once("buy", sayHelloOnec);
24
25const user1 = { id: 1, username: "Islam" };
26const user2 = { id: 2, username: "Ahmed" };
27
28// emitting the "buy" event with the user1 object as argument.
29// hence all the listeners will be invoked with this argument.
30emitter.emit("buy", user1);
31
32// removing shipment from the listeners array for the event "buy"
33emitter.removeListener("buy", shipment);
34
35emitter.emit("buy", user2);
Terminal
Copy
➜ node src/index.js
[Shipment]: User with id 1 has bought something
[marketing]: User with id 1 has bought something
i am only invoked once!
[marketing]: User with id 2 has bought something

The EventEmitter with the standards modules.

Remember when I mentioned above that a lot of Node's modules implement the EventEmitter class, such as http request, response, and streams.

In this section, I will inspect this behavior in the http module.

http modules enable Node's applications to transfer data over the Hyper Text Transfer Protocol (HTTP).

In Node.js if you are building a web server, in one way or another you need to use createServer function

server.js
Copy
1import http from "http";
2
3const PORT = process.env.PORT | 8080;
4
5const handleRequest = (request, resonse) => {
6// make some action here
7};
8
9const server = http.createServer(handleRequest);
10
11server.listen(PORT);

In the code above we passed the handleRequest to the createServer functions. For any incoming request, Node will call the handleRequest function with some other objects(request and response) to handle that incoming request.

The most important thing we care about is the returned Server object that is returned from the createServer function.

The Server Object implements the EventEmitter class. and the code above is nothing but shorthand for creating a server, what actually happens is that Node creates the Server object and then adds the handleRequest function as a listener for the request event.

server.js
Copy
1
2...
3
4const handleRequest = (request, response) => {
5// make some action here
6};
7
8const server = http.createServer();
9
10server.on("request", handleRequest)
11...
12

Also, the request and response objects implement the EventEmitter class.

For example, if you sent a POST request to the Node server.

So my server will recognize that you are sending data to me, and Node will read it as a stream of data, this data come in streams(chunk by chunk), and when every chunk is read, Node will notify me of the event "data" to store it somewhere, and when it finishes reading all data it will fire an event called "end" on the request object, so now I have all the data you send to me.

The reason for that approach because the request object implements ReadableStream, interface This stream can be listened to or piped elsewhere just like any other stream. We can grab the data right out of the stream by listening to the stream's 'data' and 'end' events.

server.js
Copy
1
2// body is array of Buffers
3let body = [];
4
5request.on('data', (chunk) => {
6// the chunk is a buffer
7body.push(chunk);
8}).on('end', () => {
9// when Node finishes reading the stream it will notify us with by firing "end" event
10body = Buffer.concat(body).toString();
11});

This may seem overkilling for anyone to do when getting data from the body, luckily there are some libraries to do the heavy lifting for us and provide the data from the body normally, such as concat-stream.

Thanks for reading this article. I hope you enjoyed it