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.
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.
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' };
A WebSocket is an enhancement to HTTP and can run side by side with HTTP.
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.
The Sails HTTP/WebSocket server is listening for both HTTP requests and upgraded WebSocket events.
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.
Overall, Sails uses WebSocket.io for the lower-level WebSocket communication between the client and server.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Figure 14.6 illustrates the video player page locals, parameters, and endpoints that relate to chat.
The API Reference provides the seven locals that will be sent to the view. Table 14.1 describes each locals’ 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.
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.
In Sublime, open brushfire/api/models/Chat.js, and add the following 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.
... 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.
... 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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
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.
Figure 14.12 provides an overview of the typing message feature you’re going to implement.
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:
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.
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.
... $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.
... 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(); }); ...
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.
... '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.
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.
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.
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.
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.
Brushfire doesn’t require using all of the backend low-level WebSocket methods, so let’s take a quick look at the remaining methods:
For a complete reference to each method, head over to http://mng.bz/2jH2.
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.
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:
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:
The remaining six pubsub methods replace and enhance the low-level .broadcast() method that sends events to WebSockets:
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.
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.
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.
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.
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.
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.
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.
{ 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.
Eight blueprint actions are available to each controller unless overwritten by a custom action using the same name:
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.
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.
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.
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.
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.
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.
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.
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.