Chapter 14. Realtime with WebSockets

This chapter covers

  • Understanding WebSockets
  • Using the Sails WebSocket client
  • Implementing a chat system in Brushfire
  • Incorporating resourceful pubsub into chat

Brushfire users want to communicate with each other about the videos they’re watching. We’ll satisfy this requirement by implementing a chat system where each video will have its own persistent chat room. Making the chat persistent, meaning storing the chats, will give users the flexibility to interact synchronously (at the same time) or asynchronously (leaving a question that can be answered later).

In this chapter, we’ll differentiate WebSocket events from the HTTP request/ response scheme we’ve used in prior chapters. We’ll show Sails virtual request integration that makes WebSockets work like regular ol’ requests but with access to the WebSocket via the req dictionary. With access to the user-agent’s WebSocket, we’ll introduce the concept of using rooms to organize those connected to the Sails WebSocket server and currently viewing a particular video. We’ll enable user-agents to chat using lower-level methods like sails.sockets.join() and sails.sockets.broadcast() on the backend and io.socket.on() on the frontend. Next, we’ll transition to Sails resourceful pubsub (RPS) methods, replacing sails.sockets.join() with Video.subscribe() and sails.sockets.broadcast() with Video.publish-Update() on the backend. Finally, we’ll take a closer look at using resourceful pubsub with blueprint CRUD actions, which have added features regarding WebSockets. If that seems like a lot of material, don’t worry. We’ll take each topic step by step with examples. Let’s get started.

14.1. Obtaining the example materials for this chapter

If you followed along in chapter 13 with an existing project, you can continue to use that project in this chapter. If you want to start from this chapter and move forward, clone the following repo: https://github.com/sailsinaction/brushfire-ch13-end. After cloning the repo, install the Node module dependencies via npm install. You’ll also want to add the local.js file you created in chapter 11. In Sublime, copy and paste your local.js file you created in chapter 13, or create a new file in brushfire/config/local.js, and add the following code.

Example 14.1. Adding to the local.js file
module.exports.blueprints = {
  shortcuts: true,
  prefix: '/bp',
};

module.exports.connections = {
  myPostgresqlServer: {
    adapter: 'sails-postgresql',
    host: 'localhost',
    database: 'brushfire'
  }
};

module.exports.mailgun =  {
  apiKey: 'ADD YOUR MAILGUN API KEY HERE',
  domain: 'ADD YOUR MAILGUN DOMAIN HERE',
  baseUrl: 'http://localhost:1337'
};

14.2. Understanding WebSockets

A WebSocket is an enhancement to HTTP and can run side by side with HTTP.

Note

By making the process of connecting to a WebSocket server similar to an HTTP request, a WebSocket server can listen for both HTTP requests and WebSocket connection requests on the same port.

Unlike HTTP, once a client connects to the WebSocket server and establishes a WebSocket id, the server can send events to listening clients with a matching id and vice versa. This is in contrast to HTTP, where the client must first make a request, which briefly opens a connection, before the server can respond and close it. Once the server responds, the connection is closed and the server can no longer initiate communication with the client until another request is received. Figure 14.1 illustrates this important difference between HTTP and WebSockets communication.

Figure 14.1. An HTTP request is necessary for a server to respond. The request creates an open connection between the client and server. Once the server responds, the connection is closed and another request is needed before the server can again respond to the client. With WebSockets, once a client makes a connection , the server can send a message to it , at any time so long as the connection remains open.

The Sails HTTP/WebSocket server is listening for both HTTP requests and upgraded WebSocket events.

Definition

Often you’ll encounter the terms events, messages, and notifications used interchangeably with WebSockets. For clarity, we’ll use the term event to specify the thing the Sails WebSocket server and client send to each other. Events can have names like chat, and their contents are referred to as the message.

After establishing a connection with the Sails server, the user-agent is given an id. Let’s build an example that will demonstrate these concepts.

14.2.1. Establishing a socket connection

Overall, Sails uses WebSocket.io for the lower-level WebSocket communication between the client and server.

Definition

Socket.io is a JavaScript library that enables access to WebSockets in Node.

By default, Sails creates a WebSocket server on the backend when the Sails server starts via sails lift. The Sails WebSocket client, located in brushfire/assets/js/ dependencies/sails.io.js, is a tiny client-side library on the frontend that’s bundled by default in new Sails apps.

Tip

You can, of course, choose not to use the library by deleting the file in the assets/ folder.

The Sails WebSocket client is a lightweight wrapper that sits on top of the WebSocket.io client, whose purpose is to simplify the sending and receiving of events from your Sails backend. You’ve connected to the WebSocket server via the client throughout the book. For example, restart Sails using sails lift and navigate your browser to localhost:1337 with the browser’s console window open. You should see a browser console log message indicating that a connection was established, as shown in figure 14.2.

Figure 14.2. Each time Sails responds with a server-rendered view, a new WebSocket connection is created.

This message is coming from the Sails WebSocket client and is automatic as long as you utilize the default sails.io.js. Now that you’ve established a connection between the client and the Sails WebSocket, you can build requests on the frontend and add methods to controller actions on the backend. This allows you to create and join rooms, send events to connected WebSockets, and more.

14.2.2. Virtual requests

Let’s quickly create a Sails app to solidify the distinction between an HTTP request and a Sails WebSocket virtual request. From the command line, create a new Sails application named WebSocketExample:

~ $ sails new WebSocketExample
info: Created a new Sails app `socketExample`!

After changing into the WebSocketExample folder, create a controller and name it example:

~/socketExample $ sails generate controller example
info: Created a new controller ("example") at api/controllers/ExampleController.js!

You’ll start on the backend by creating an action that can be accessed by both an HTTP request and a WebSocket virtual request.

Note

We use the qualifier virtual request because WebSockets don’t make plain HTTP requests. Instead, the Sails WebSocket client, a.k.a. sails.io.js, contains methods that allow you to access HTTP actions as if you were using AJAX. The Sails backend then takes care of transforming the WebSocket event into a virtual request that routes the request to the appropriate controller action with a req dictionary that contains a body, headers, and so on.

In Sublime, open WebSocketExample/api/controllers/ExampleController.js, and add the following helloWorld action.

Example 14.2. Adding an action that will be accessed by both HTTP and WebSockets

If you make an HTTP GET request to /example/helloWorld, the backend will respond with a dictionary that contains the key/value pair hello and world. If you use the Sails WebSocket client and make a Sails WebSocket virtual request to helloWorld, the backend will respond with a dictionary that also contains the WebSocket id. So, how do you access the Sails WebSocket client to use the virtual requests?

Sails provides a familiar AJAX-like interface for communicating with the Sails WebSocket server. Through the client you have access to CRUD methods like io.socket.get(), .post(), .put(), and .delete(). Let’s look at this in action. Make sure Sails is running via sails lift and make a GET request in Postman to localhost:1337/ example/helloWorld, similar to figure 14.3.

Figure 14.3. Making a GET request to localhost:1337/example/helloWorld returns the dictionary without the WebSocketId property and formatted as JSON.

The Postman HTTP request to the helloWorld action isn’t a WebSocket request and thus doesn’t have access to request’s WebSocketId. It responds solely with the JSON dictionary.

Now, you’ll make a WebSocket virtual request using the Sails WebSocket client. Because the homepage view loads the Sails WebSocket client via sails.io.js, you have access to its methods from the browser console. Navigate your browser to localhost:1337 with the console window open. From the browser console window, copy and paste the following code snippet on the command line.

Example 14.3. Making a WebSocket request using the Sails WebSocket client
io.socket.get('/example/helloWorld', function(resData, jwres){
  console.log(resData);
});

When the code is executed, the browser console should return something like figure 14.4.

Figure 14.4. Using io.socket.get() to make a request triggers a WebSocket virtual GET request that matches a blueprint action route and executes the helloWorld action that responds with the WebSocket id via the WebSocketId property.

Unlike the HTTP request, the Sails WebSocket virtual request yields the WebSocket id. The WebSocket id is used to differentiate between requesting clients. Sails abstracts away the need to worry about the WebSocket id the same way it does with HTTP by making it part of the req dictionary.

Figure 14.5 illustrates that the function signature of the Sails virtual get, post, put, and delete WebSocket methods is similar to an AJAX request.

Figure 14.5. The function signature of the get WebSocket method should look familiar to anyone who has used an AJAX equivalent method. The function takes the URL as the first argument and the callback as the second argument . Within the callback, you can get access to the response data as JSON and the JSON WebSocket Response dictionary .

The callback to the virtual methods contains two arguments: resData and jwres. The resData argument contains any data passed back from the request as JSON. Optionally, you can get access to the JSON WebSocket response (jwres), which contains a dictionary of response headers, the body, and the status code of the response similar to what you’d get with a standard HTTP request. Depending on the type of application, you can also get a reduction in the latency (elapsed time) between the request and response using WebSockets. But clearly the biggest advantage is you can now create features that allow the server to send events to clients without the client having to make a request. Let’s expand what you’ve learned and implement the chat features of Brushfire.

14.3. Implementing chat

Figure 14.6 illustrates the video player page locals, parameters, and endpoints that relate to chat.

Figure 14.6. Specific to chat, the video player page endpoint will pass seven locals to the view, four of which will be displayed: gravatarURL and username from the user model, message and created from the chat model. The view also contains a single outgoing parameter, message, sent within a request, Send chat.

The API Reference provides the seven locals that will be sent to the view. Table 14.1 describes each locals’ attribute.

Table 14.1. The locals dictionary: chats for the video player page

attribute

model attribute

Description

message message The text of the message a user can send to the video room.
sender sender The model association attribute that contains the id of the user that sent the chat.
video video The model association attribute that contains the id of the video that’s related to the chat.
id id The id of the record in the chat model.
created createdAt The date and time the chat record was created.
username N/A The username attribute of the user who created the chat. The username isn’t stored in the chat model, but through the sender model association attribute you can populate sender to get the username.
gravatarURL N/A The gravatarURL attribute of the user who created the chat. The gravatarURL isn’t stored in the chat model, but through the sender model association attribute you can populate sender to get the gravatarURL.

Notice that not all the locals will be stored in the chat model. Instead, you’ll take advantage of Sails’ associations to provide the necessary locals from other models. Figure 14.7 illustrates the relationships between the chat, user, and video models.

Figure 14.7. A user can have many chats, but a chat can have only one user. A video record can have many chats, but a chat can have only one video.

14.3.1. Creating a chat API

Your first step in implementing chat function in Brushfire is to create an API for the chat resource. From the root of the project on the command line, create a new chat API by typing

~ $ sails generate api chat
info: Created a new api!

Now, let’s configure the attributes of the new chat model and other related models. Figure 14.8 illustrates the association relationships you’ll need between the user, video, and chat models.

Figure 14.8. The model associations between the user, video and chat models. The user model has a chats association attribute configured as a collection with the chat model that uses via . The video model has a chats association attribute configured as a collection with the chat model that uses via . The chat model has a sender association attribute configured as a model with the user . The chat model also has a video association attribute configured as a model with the video .

In Sublime, open brushfire/api/models/Chat.js, and add the following attributes.

Example 14.4. The chat model attributes
...
module.exports = {

  attributes: {

    message: {
      type: 'string'
    },

    sender: {
      model: 'user'
    },

    video: {
      model: 'video'
    }
  }
};
...

Next, you’ll add the chats association attribute to the user model. This attribute, when populated, contains all of the chat records created by this user. In Sublime, open brushfire/api/models/User.js, and add the following attributes.

Example 14.5. The user model attributes
...
following: {
  collection: 'user',
  via: 'followers'
},

chats: {
  collection: 'chat',
  via: 'sender'
},
...

Finally, you’ll add a chats association attribute to the video model. This attribute, when populated, contains all of the chat records made about the video record. In Sublime, open brushfire/api/models/Video.js, and add the following attributes.

Example 14.6. The video model attributes
...
tutorialAssoc: {
  model: 'tutorial'
},

chats: {
  collection: 'chat',
  via: 'video'
}
...

Now that you have the models configured, let’s implement the video player page and, specifically, the showVideo action.

14.3.2. Adding chat to an existing page

The showVideo action is responsible for displaying the video player page with the appropriately formatted locals. You’re currently using an array of dictionaries that simulate chat records, FAKE_CHAT, as well as a dictionary, video, to simulate a video record. Let’s replace the simulated chat records with the real thing. In Sublime, open brushfire/api/controllers/PageController.js, and add the following code to the show-Video action to first find the video to play.

Example 14.7. Finding the video to play

In addition to finding the particular video, you populate the chats association attribute so that you have access to an array of chat dictionaries related to the video model. Next, you’ll iterate through each chat and format its properties based on the requirements in the Brushfire API Reference. In Sublime, add the following code.

Example 14.8. Iterate through the chats array and format each video’s chat record

Finally, you’ll respond based on the user’s authenticated state. If the user isn’t authenticated, respond with the me dictionary set to null, the found video information for the page, the tutorial id, and the chat information. In Sublime, add the response if the user isn’t authenticated, similar to the following listing.

Example 14.9. The response if the user isn't authenticated

If the user is authenticated, you’ll add the required information about the authenticated user to the me property as well as related chat information. In Sublime, add the response if the user is authenticated, similar to the next listing.

Example 14.10. The response if the user is authenticated

Now that you’re adding real chat records to the video player page, you need to be able to chat! You’ll start to implement that next.

14.3.3. Subscribing a socket to a room

Each chat is associated with a particular video and a particular user. When the rendered brushfire/views/show-video.ejs view loads, you display any existing chats associated with the video. You’ll need a way to send new chats created for the period between page refreshes. To accomplish this task, you’ll send an event as each new chat is created. But you don’t want to send the event to all connected WebSockets. Instead, you want to limit sending the event to those WebSockets that are currently on the video player page of a particular video record. You can accomplish this by creating a room unique to that video record, joining the WebSockets that are on this page to the room, and then sending an event to the room. But what room are we talking about?

Socket.io uses the metaphor of a room to group WebSockets together. From the backend, you can then broadcast messages to the room. You can also subscribe and unsubscribe WebSockets from the room. There’s no need to create a room explicitly. The first user who joins a room automatically creates the room in the process. So, by loading the show-video view, the frontend Angular controller makes a PUT request to /videos/2/join. This, in turn, triggers the joinChat action, which you’ll implement now. In Sublime, open brushfire/api/controllers/VideoController.js, and add the following code to the joinChat action.

Example 14.11. Joining the video room

The sails.sockets.join() method is one of the low-level methods Sails provides that allows realtime communication with the frontend. The method has two required arguments: a WebSocket and a roomName. The WebSocket refers to the client WebSocket that made the request, and the roomName is the name of the room to join. You’ll pass the req dictionary as the first argument, and use a combination of the word video with the id of the video record to create a unique room name for each video as the second argument. Now that you’ve joined the WebSocket to the video room, let’s set up a mechanism for sending a chat.

14.3.4. Sending a chat notification

You want to send the contents of the input field of the chat form in the show-video view to the backend send-chat endpoint. When the user clicks the Send button, the chat action is triggered. In Sublime, open brushfire/api/controllers/VideoController.js, and add the following code to the chat action.

Example 14.12. Saving the chat and then broadcasting it to the video room

Initially, the chat record is created. Next, you’ll query for the currently authenticated user using the userId found in the session. You’ll use a combination of the results of your created chat record and user query as the required Brushfire API reference values in the chat event that you’ll send to the room. To accomplish this, you send the event using another Sails low-level WebSocket method: sails.sockets.broadcast(). This method can be executed with three different function signatures, as shown in figure 14.9.

Figure 14.9. The broadcast method can be executed using the roomName, eventName, and data as arguments . The method can also be executed without the eventName , in which case the default event name is used. In addition, the currently requesting WebSocket can be omitted by adding the WebSocketToOmit argument, req. Finally, all four arguments can be used.

If an eventName isn’t provided, a default event name of message is used. You want the requesting WebSocket to be included in those WebSockets that will receive the event, so you’ll leave out the WebSocketToOmit argument. The final step is adding an event listener on the frontend configured for the chat event.

14.3.5. Adding a chat event listener to the frontend

Although we’re concentrating on the backend, it’s important to review what’s being incorporated into the frontend regarding WebSocket requests. Figure 14.10 illustrates an overview of the significant exchanges between the frontend and backend as it relates to chat.

Figure 14.10. The significant interactions between the frontend and backend relating to chat

When the user-agent clicks a video, the showVideo action is triggered , which finds and displays any existing chats using Video.findOne() and populates the chats to the view on the backend. When the rendered view is displayed on the browser, the user-agent connects to the Sails WebSocket server automatically via a link to sails.io.js in the show-video view. The Angular controller also joins the newly created WebSocket to a unique video room. When a user clicks the Send button on the chat form , the backend broadcasts the chat message to any listening WebSockets in the video room . The listening WebSocket can hear the broadcasted event because of an event listener in the showVideoPageController.js controller file, which is displayed in the following listing.

Example 14.13. The chat event listener

When a chat event is sent to the client, an event handler is triggered and the newly created chat is added to the chats array on the Angular $scope, which updates the UI displaying the chat, similar to figure 14.11.

Figure 14.11. The chat event listener displays the chat event message contents.

Our old friend Chad has an additional request for the chat. He wants a user to know when another user is typing in the message field of the chat form. You’ll implement this last piece of chat functionality in the next section.

14.4. Sending typing and stoppedTyping notifications

Figure 14.12 provides an overview of the typing message feature you’re going to implement.

Figure 14.12. The steps the frontend and backend must fulfill in order to display a message when a user is typing in the message field of the chat window

When a user is typing in the message field of the chat window , you want the frontend to make a PUT request to /videos/:id/typing . That request matches a route that triggers the typing action in the video controller, which broadcasts a typing event. An event handler listens for the typing event , which triggers the display of the typing animation . Reviewing the API Reference reveals that this feature will require the following on the backend:

  • A typing action that broadcasts a typing event to all WebSocket members of the video room
  • A stoppedTyping action that broadcasts a stopTyping event to all WebSocket members of the video room
  • A route connecting a PUT request to /videos/:id/typing to the typing action
  • A route connecting a PUT request to /videos/:id/stoppedTyping to the stopped-Typing action

On the frontend, you’ll need to generate two requests when the user starts typing in the message input field and when the user stops typing, as well as event handlers to listen for the typing and stoppedTyping events. Let’s get busy.

14.4.1. Listening for different kinds of notifications

First, let’s add the two requests for the typing and stopped-typing states. Remember, you’re now dealing with the frontend and not the backend API. In Sublime, open brushfire/assets/js/controllers/showVideoPageController.js, and add the whenTyping and whenNotTyping methods shown here.

Example 14.14. Adding the whenTyping and whenNotTyping methods
  ...
  $scope.whenTyping = function (event) {

    io.socket.request({
      url: '/videos/'+$scope.fromUrlVideoId+'/typing',
      method: 'put'
    }, function (data, JWR){
        // If something went wrong, handle the error.
        if (JWR.statusCode !== 200) {
          console.error(JWR);
          return;
        }
    });
  };

  $scope.whenNotTyping = function (event) {

    io.socket.request({
      url: '/videos/'+$scope.fromUrlVideoId+'/stoppedTyping',
      method: 'put'
    }, function (data, JWR){
        // If something went wrong, handle the error.
        if (JWR.statusCode !== 200) {
          console.error(JWR);
          return;
        }
    });
  };
}]);

You use the io.socket.request() method, which provides you with lower-level access to the request headers, parameters, method, and URL of the request. Both methods are triggered within the show-video view in brushfire/views/show-view.ejs using a combination of ng-blur, ng-focus, and ng-keypress. Now let’s add the event handlers that will listen for the typing and stoppedTyping events. Back in Sublime, open brushfire/assets/js/controllers/ showVideoPageController.js, and add the event handlers for the typing and stoppedTyping events shown in the next listing.

Example 14.15. Adding the event handlers for typing and stoppedTyping events
...
io.socket.on('typing', function (e) {
  console.log('typing!', e);

  $scope.usernameTyping = e.username;
  $scope.typing = true;

  $scope.$apply();
});

io.socket.on('stoppedTyping', function (e) {
  console.log('stoppedTyping!', e);

  $scope.typing = false;
  $scope.$apply();
});
...

14.4.2. Excluding the sender from a broadcast

Now that you have the frontend implemented, let’s start with backend implementation of the two routes that will trigger the typing and stoppedTyping actions. In Sublime, open brushfire/config/routes.js, and add the two routes shown here.

Example 14.16. Adding two routes that trigger the typing and stoppedTyping actions
...
'PUT /videos/:id/join': 'VideoController.joinChat',
'PUT /videos/:id/typing': 'VideoController.typing',
'PUT /videos/:id/stoppedTyping': 'VideoController.stoppedTyping',
...

Next, let’s implement the typing action. In Sublime, open brushfire/api/controllers/ VideoController.js, and add the typing action as follows.

Example 14.17. Broadcasting a typing event excluding the sender’s WebSocket

Earlier, you used the sails.sockets.broadcast() method without sending an optional WebSocket to be omitted. It’s not necessary for the user who’s typing in the chat window to receive the typing event. You’ll pass the requesting WebSocket as the WebSocket to be omitted from receiving the event. Last, you’ll implement the action that will stop the animation. The stoppedTyping action doesn’t require a query to the user model because you’re not displaying a message that a particular user stopped typing. In Sublime, open brushfire/api/controllers/VideoController.js, and add the stopTyping action shown in the following listing.

Example 14.18. Broadcasting a stoppedTyping event excluding the sender’s WebSocket
stoppedTyping: function(req, res) {
  if (!req.isSocket) {
    return res.badRequest();
  }

  sails.sockets.broadcast('video'+req.param('id'),
    'stoppedTyping', (req.isSocket ? req : undefined) );

  return res.ok();
}

Let’s see this in action. Restart Sails via sails lift, and navigate two different authenticated browsers, one in regular mode and one in incognito mode, to the same video. You can use the users that were created by the bootstrap file, nikolatesla and sails-inaction, both of which have the password abc123. Use ShiftIt to line up the browsers side by side. Browse to the same video, for example, http://localhost:1337/tutorials/1/videos/1/show. Begin typing in one of the chat windows, and you should see something similar to figure 14.13.

Note

We used an incognito browser, which creates a browser with a different session cookie than the regular browser. If we had used two regular browsers, the same session cookie would be used and, therefore, both browsers would access the same authenticated user.

Figure 14.13. When two user-agents are on the same video and one begins to type a chat message , the other user-agent has a typing message displayed .

Typing in the message input field triggers a PUT request to /videos/:id/typing, which triggers the typing action. Within this action, you broadcast a typing event to any WebSocket ids that have joined the video room ('video' + video id) and have an event handler listening for a typing event.

14.4.3. Other useful methods in sails.sockets

Brushfire doesn’t require using all of the backend low-level WebSocket methods, so let’s take a quick look at the remaining methods:

  • .addRoomMembersToRooms(sourceRoom, destRooms, cb) subscribes all members of a room to one or more additional rooms.
  • sails.sockets.blast(data) sends a message/event to all WebSockets connected to the Sails WebSocket server regardless of what rooms they’ve joined.
  • sails.sockets.leave(socket, roomName) unsubscribes a WebSocket from a room.
  • sails.sockets.leaveAll(roomName, cb) unsubscribes all members of a room from that room and every other room they’re currently subscribed to.
  • .removeRoomMembersFromRooms() unsubscribes all members of a room from one or more other rooms.

For a complete reference to each method, head over to http://mng.bz/2jH2.

14.5. Understanding resourceful pubsub

In addition to the WebSocket client and low-level backend WebSocket methods, Sails provides an automation layer called resourceful pubsub. Each model, also referred to as a resource, is automatically equipped with methods for joining connected WebSockets to event notifications when records are created, updated, and/or deleted. In Brushfire, you want each video record to have its own unique room such as video1. Currently, you’re creating the room name by concatenating the word video with the id of each record. When the browser user-agent’s WebSocket accesses a particular video player page, you join the WebSocket to the room with the concatenated name.

Resourceful pubsub methods create and maintain the room names for you. RPS rooms also add an additional layer of detail. In addition to using a model record’s id as the basis for a room name, pubsub rooms are generated for create, update, and delete actions, as well as additions to and removals from associations. For example, you have the fidelity of joining a requesting WebSocket to a room that limits events to the updates of that record.

Note

These RPS methods follow a pattern referred to as the publish/subscription model. If a WebSocket is subscribed to a resource, then events are published to the subscribing WebSockets.

In the previous section, you used three backend low-level WebSocket methods:

  • sails.sockets.getId(req) returns the WebSocket id found on the req dictionary-.
  • sails.sockets.join(req, roomName) joins the WebSocket to a room.
  • sails.sockets.broadcast(roomName, data) sends an event to a room.

RPS provides additional backend methods that extend their low-level counterparts to a specific resource—the model. There are ten pubsub methods in total. Four of the methods replace and enhance the low-level .join() method:

  • .subscribe() joins a requesting WebSocket to the update, destroy, add, and remove rooms of one or more record(s) of a model. Behind the scenes, Sails takes care of creating and managing these room names. For example, a room named sails_model_user_33: update is the update action room for a user record with an id of 33. You don’t have to remember the room names. Instead, you pass various pubsub methods, including .subscribe(), the id of a record or records you want to join the room.
  • .unsubscribe() removes a requesting WebSocket from the room of one or more record(s) of a model.
  • .watch() joins a requesting WebSocket to a model’s class room whose name is derived behind the scenes by concatenating the word create with the model name, like sails_model_create_user. This is a room that will watch for new records of a particular model.
  • .unwatch() removes a requesting WebSocket from the model’s class room.

The remaining six pubsub methods replace and enhance the low-level .broadcast() method that sends events to WebSockets:

  • .publishCreate() sends an event to subscribers of the model class room like sails_model_create_video.
  • .publishUpdate() sends an event to subscribers of the update instance room like sails_model_video_44:update.
  • .publishDestroy() sends an event to subscribers of the destroy instance room like sails_model_video_44:destroy.
  • .publishAdd() sends an event to subscribers of the add instance room like sails_model_video_44:add:chats.
  • .publishRemove() sends an event to subscribers of the remove instance room like sails_model_video_44:add:chats.
  • .message() sends an event to all WebSockets that are listening for an event based on the model name like sails_model_video_44:message.

14.5.1. Using .subscribe()

The .subscribe() pubsub method subscribes the requesting client WebSocket to one or more database records. In our example, we have a single video record for each video player page. The Video.subscribe() method will automatically create video update and destroy rooms, as well as rooms affecting the chats association, adding and removing associated records and then joining the requesting WebSocket to those rooms. In the next section, we’ll explore methods that send events to these rooms. For now, in Sublime, open brushfire/api/controllers/VideoController.js, and change the joinChat action similar to the following listing.

Example 14.19. Adding the .subscribe() method with .join() in the joinChat action

By using Video.subscribe(), you’re not responsible for managing room names for the chat messages. Instead, you’ll refer to the id of the record you want to subscribe a WebSocket to or to send an event to. Next, let’s incorporate pubsub methods to send events to these rooms.

14.5.2. Using .publishUpdate()

The requesting WebSockets are now joined to unique rooms whose names reflect the id of the record of the video model as well as the type of action being performed: create, update, destroy, and so on. Now, you need a way of sending events to the appropriate rooms. Video.publishUpdate() accepts an id or array of ids of video records as the first argument in its method signature. It then uses that information to broadcast an update event to the update room of those records.

Tip

Ironically, the .publishUpdate() method doesn’t update any model records. Instead, it provides a mechanism for you to broadcast the fact that an update event has occurred within a controller action to those WebSockets that are subscribed to the model room and have event listeners configured to listen for a particular event.

In Sublime, open brushfire/api/controllers/VideoController.js, and substitute sails.sockets.broadcast() in the chat action with Video.publishUpdate() similar to the following.

Example 14.20. Substituting .broadcast() with .publishUpdate() in the chat action

Now, when a user sends a chat, a video event is broadcast to all WebSockets subscribed to that particular video record. Finally, let’s change the event name of your event listener from chat to video.

14.5.3. RPS methods and event names

The pubsub publish methods .publishUpdate(), .publishCreate(), and .publishDestroy() share the same event name derived from the model. In this case, the event name is video. You currently have the event listener configured for a chat event. So let’s change it to video. In Sublime, open brushfire/assets/js/controllers/show-Video-PageController.js, and change the event listener event name to video, as shown here.

Example 14.21. Updating the event listener to the video event

The listener will now be triggered when a video event is sent. Also, the contents of the event will have a data property similar to the following.

Example 14.22. A typical video event
{
  data: {
    created: "just now",
    gravatarURL:
     "http://www.gravatar.com/avatar/c06112bbecd8a290a00441bf181a24d3?",
    message: "How are you?",
    username: "nikola-tesla",
  },
  id: 6,
  verb: "updated"
}

Even though you use the same event name for each method, you can differentiate between event types by checking the verb property, in this case, updated.

For Brushfire chat, you need to transform some of the values before they’re sent as part of an event to the frontend. Therefore, you use the publish methods in your custom actions. There are cases, however, when you don’t need a custom controller action and can instead rely on blueprint actions. In these cases, you can use built-in pubsub features of blueprint actions to automate the process of managing WebSockets.

14.6. Understanding how the blueprint API uses RPS methods

Eight blueprint actions are available to each controller unless overwritten by a custom action using the same name:

  • find
  • findOne
  • create
  • update
  • destroy
  • add
  • remove
  • populate

In chapter 4, we introduced some of these actions and how they can be triggered by blueprint routes or custom routes to provide prebuilt CRUD operations to controllers. What we haven’t discussed is that these actions also contain built-in RPS features.

14.6.1. The find and findOne actions

The blueprint find and findOne actions automatically subscribe requesting WebSockets to various pubsub rooms of existing records of a model. The blueprint find action automatically subscribes the requesting WebSocket to any records that match the results of a find query, including any records that are part of an association. For example, let’s say you want to track the position of each user’s cursor in the browser. You’ll first create a cursor model. You’ll then use the Sails WebSocket client to make a virtual GET request to /cursor. This virtual request will trigger the blueprint find action executing Cursor.subscribe(), which will automatically subscribe the requesting WebSocket to various rooms. Cursor.subscribe() will join the requesting WebSocket to the update, destroy, add, and remove rooms for each record found by the find query and generated by the first WebSocket joining the room. In addition, the find action will execute Cursor.watch(). Cursor.watch() joins the requesting WebSocket to a room that combines the model name with the word create: sails_ model_create_cursor. Adding the requesting WebSocket to the various rooms will enable the pubsub publish methods to automatically send events within the blueprint create, update, destroy, add, remove, and populate actions. Unlike the blueprint find action, the findOne action subscribes the requesting client to the record rooms—update, destroy, add, and remove—but not to the create class room.

14.6.2. The create action

After the blueprint create action creates a record or records, it subscribes the requesting WebSocket to the created record or records. It then subscribes all of the WebSockets that are members of the model’s create room to the newly created record or records. Finally, by using .publishCreate(), the blueprint create action will send an event to the subscribers of the model’s create room. For example, sails_model_create_cursor would be the create room for cursor model. A typical event from the blueprint create action’s automatic use of the .publishCreate() method will look similar to this.

Example 14.23. A typical event from the .publishCreate() method

14.6.3. The update action

After the blueprint update action updates a record, it subscribes the requesting WebSocket to the updated record. Using .publishUpdate(), the blueprint update action sends an event to the subscribers of the updated record’s update room. For example, sails_model_cursor_24:update would be the update room for a cursor record with an id of 24. A typical event from the blueprint update action’s automatic use of the .publishUpdate() method will look similar to the following.

Example 14.24. A typical event from the .publishUpdate() method

14.6.4. The destroy action

After the blueprint destroy action destroys a record, it executes .publishDestroy(), sending an event to the subscribers of the deleted record’s delete room. For example, sails_model_cursor_24:destroy would be the destroy room for a cursor record with the id of 24. The action then unsubscribes the requesting WebSocket and any existing subscribers from the deleted record’s update, destroy, add, and remove rooms. A typical event from the blueprint destroy action’s automatic use of the .publishDestroy() method will look similar to this.

Example 14.25. A typical event from the .publishDestroy() method

14.6.5. The populate action

After the blueprint populate action populates an association, it subscribes the requesting WebSocket to the record or records returned after populating the association. Using this blueprint method doesn’t produce any events.

14.6.6. The add action

After the blueprint add action adds the associated record, it executes .publishAdd(), sending an event to the subscribers of the add room. For example, sails_model_video _32:add:cursors would be the add room for the cursor record with the id of 32. A typical event from the blueprint add action’s automatic use of the .publishAdd() method will look similar to the following.

Example 14.26. A typical event from the .publishAdd() method

14.6.7. The remove action

After the blueprint remove action removes the associated record, it executes .publish-Remove(), sending an event to the subscribers of the remove room. For example, sails_model_video_32:remove:cursors would be the remove room for the cursor record with the id of 32. A typical event from the blueprint remove action’s automatic use of the .publishRemove() method will look similar to this.

Example 14.27. A typical event from the .publishRemove() method

14.7. Summary

  • Once a WebSocket makes a connection with the WebSocket server, the server can send events to the connected WebSocket at will.
  • Sails provides virtual request methods similar to AJAX but with the addition of exposing the WebSocket id to the backend.
  • Sails provides resourceful pubsub, which automatically equips models with methods for joining connected WebSockets to event notifications when records are created, updated, and/or deleted.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset