Introduction
MDN Web Doc defines WebSockets:
WebSocket is a protocol that allows for a persistent TCP connection between server and client so they can exchange data at any time.
https://developer.mozilla.org/en-US/docs/Glossary/WebSockets
But what is TCP? TCP is the Transmission Control Protocol and is a protocol used in the transport layer of the Internet Protocol Suite. To understand this better, I should briefly explain the internet protocol suite. The internet protocol suite is a model of data transmission over the internet. The most popular model (RFC 1122) consist of the 4 layers:
- Application Layer – communicates directly with application, e.g. the browser and server use Hypertext Transport Protocol (HTTP) at the application layer. It assumes that all the data has arrived at the proper destination.
- Transport layer – is responsible for assuring that the data has reached the destination. TCP is a popular protocol for the transport layer.
- Internet layer – routes the data across the networks. It defines addressing and the network. The most popular protocol at this level is the Internet Protocol (IPv4 or IPv6). The internet layer uses the IP address to route the data packets.
- Link layer – also called the Data Link layer assures that the data packets remain intact while being transmitted from one node/router to the next node on the network
A popular analogy for how the internet protocol suite transmits the data across the network is that it is like skins of an onion with the application layer deep in the onion and the link layer the outer skin of the onion. As the data packets transmit from router to router the link layer headers are read and modified for the next router. Likewise as the data packets move from one network to the next, the internet layer reads and modifies the internet headers.
Another important aspect of the internet protocol suite is that TCP is used in the transport layer, the layer below the application layer using HTTP. So WebSockets are stepping down from the application layer to the transport layer. An interesting contraction is that two required HTTP headers for designing a WebSocket connection refers to “Upgrade”:
GET /chat HTTP/1.1 Host: example.com:8000 Upgrade: websocket Connection: Upgrade Sec-WebSocket-Key: dGhlIHNhbXBsZSBub25jZQ== Sec-WebSocket-Version: 13
There are three profound implications for the developer on stepping down from HTTP to TCP:
- Freedom from the constraint of HTTP
- Responsibility for correctly interpreting the data
- Need for another protocol
The freedom from the constraints of HTTP and the responsibility for correctly interpreting the data are related. Recall that HTTP is based on the request-response model for interpreting the data. The browser makes requests to the server, and the server responds to the browser. All interactions between the browser and server must be initiated by the browser. The server cannot preempt the browser by sending data to the browser without a request. This makes some applications such as chatting difficult to implement. TCP does not have not have this restriction. The model for TCP is like a pipe or channel. Once the connection is made data can flow in either direction at any time. This model makes implementing a chat application easy, but it also removes structure and can introduce chaos. Applications more complex than chatting will have to make sense of what the data means at any given time. This lecture will give one technique for interpreting the data transmitted through the websocket.
A web developer does not want to have to establish and manage the connection performing actions such as subscribing and acknowledging messages. These low level operations should be managed by a protocol built for servers and browsers on top of TCP. Fortunately, such a protocol exist, the Simple Text Oriented Messaging Protocol (STOMP):
STOMP is a specification that needs to be implemented by both the server and the client/browser languages. Fortunate Spring 4 implemented a stomp server in 2013.
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket
and Grails has a plugin using Spring’s WebSocket implementation:
https://github.com/zyro23/grails-spring-websocket/tree/2.4.x
Note I have referenced the plugin version for Grails version 3.3.11. For both the Spring implementation and Grails implementation, the STOMP implementation for the client’s/browser’s language is StompJS.
http://jmesnil.net/stomp-websocket/doc/
StompJS only implements the protocol and expects that the connection/socket is made. In addition the older browsers require a polyfill, so SockJS is used to provide a WebSocket object.
https://github.com/sockjs/sockjs-client
Enough terminology and introduction of technology. Let us make something.
Simple Chat Application
Before starting construction, you should introduce yourself to the Spring Websocket Grails Plugin, by reading the README.md
https://github.com/zyro23/grails-spring-websocket/tree/2.4.x
The README has enough information to make a “hello, ${world}
” application. You may want to attempt constructing it. Note: be sure to use version 2.4 of the plugin with Grails version 3.3.11.
Make the Grails App
In your workspace directory, make the directory
chat/
Open IntelliJ IDEA, and make a new project using Application Forage. Be sure to specify
- Project SDK: 1.8 (java version 1.8.0_???)
- Project Type: Application
- Grails Version: 3.3.11
- Profile: web
- Features: you can keep the default or unclick all the boxes.
After clicking “Next”, in the next panel, point the project location to your chat/ directory. Click “Finish.” Let the gradle do its work and build the project. Test the build by clicking the run icon and observe the index at localhost:8080.
After the build, open build.gradle, and add to the dependence section:
compile "org.grails.plugins:grails-spring-websocket:2.4.1"
Rebuild and run the app. Check that the app still works.
Add ChatController
Make the Chat controller in the controller. Replace the content of ChatController.groovy with:
package chat import org.springframework.messaging.handler.annotation.MessageMapping import org.springframework.messaging.handler.annotation.SendTo import groovy.json.JsonBuilder class ChatController { def index() { } /** * Accepts incoming chat messages sent by browsers and routes them * to the 'chat' topic that all browser clients are subscribed to. **/ @MessageMapping("/chat") @SendTo("/topic/chat") protected String chat(String chatMsg) { /** * Use Groovy JsonBuilder to convert a dynamically-defined * data structure to JSON. **/ def builder = new JsonBuilder() builder { message(chatMsg) timestamp(new Date().getTime()) } builder.toString() } }
We will use the index method/action for the view. The chat method is the websocket. The @MessageMapping
annotation specifies the channel for incoming messages. Messages sent to “/app/chat” will be received by this method. The @SentTo
annotation specifies that outgoing messages will be routed to “/topic/chat”. Tradition uses either the prefix “/topic” or “/queue” for routing messages to the browser. The plugin configures all incoming messages through “/app”. The method is protected so that it is not directly visible to the browser.
The method builds a JOSN and adds the date to the outgoing message’s JSON.
Add the View
In “views/chat”, make index gsp. Replace the content with:
<%@ page contentType="text/html;charset=UTF-8" %> <html> <head> <title>Chat</title> <asset:javascript src="application" /> <asset:javascript src="spring-websocket" /> <script type="text/javascript"> $(function() { var socket = new SockJS("${createLink(uri: '/stomp')}"); var client = Stomp.over(socket); client.connect({}, function() { client.subscribe("/topic/chat", function(message) { var chatMsg = JSON.parse(message.body); var time = '<strong>' + new Date(chatMsg.timestamp).toLocaleTimeString() + '</strong>'; $("#chatDiv").append(time + ': ' + chatMsg.message + '<br/>'); }); }); $("#sendButton").click(function () { client.send("/app/chat", {}, JSON.stringify($("#chatMessage").val())); }); }); </script> </head> <body> <section> <h2>Chat</h2> <input id="chatMessage" title="Enter chat message"/> <button id="sendButton">Send</button> <div id="chatDiv"></div> </section> </body> </html>
The head of the page uses the asset pipe to load “/assets/javascripts/application.js” which loads jQuery, and then loads “spring-websockets.js” provided by the plugin. The spring-websocket module contains stockJS and stomp-websocket libraries.
The script for the page uses JQuery to run after the page loads. It first creates the WebSocket object and client:
var socket = new SockJS("${createLink(uri: '/stomp')}"); var client = Stomp.over(socket);
The client connects to the socket using the connect option with headers, but without headers.
client.connect({}, function() { client.subscribe("/topic/chat", function(message) { ... }); });
This is confusing, but using the empty headers is used to avoid having to use login and password. The callback is called after the connection is made. This is the typical location to subscribe because a subscription should not be made before the connection is established. If the client subscribes before the connection is established, the subscription will fail rather silently. Actually the stomp.js will log to the console:
connected to server undefined
You can have the client subscribe outside the connect callback, if you are sure that the connection has already been made. This is typically done by having a user action initiate the subscription. Also note the client can subscribe to multiple channels.
The callback for the subscription describes what should occur after receiving a message from the channel.
client.subscribe("/topic/chat", function(message) { var chatMsg = JSON.parse(message.body); var time = '<strong>'+ new Date(chatMsg.timestamp).toLocaleTimeString() + '</strong>'; $("#chatDiv").append(time + ': ' + chatMsg.message + '<br/>'); });
In this case, the JSON message is parsed and formatted. JQuery appends the formatted message to the chatDiv.
Sending messages is setup by JQuery by defining the onclick message for the “send” button.
$("#sendButton").click(function () { client.send("/app/chat", {}, JSON.stringify($("#chatMessage").val())); });
It gets the value of the chatMessage
input text box and uses the client.send
to route the JSON.stringfy
message to “/app/chat”. Note you need to use JSON.stringfy
or the send will silently fail. The empty object, {}, are for additional headers.
That is it.
Run and Try Chatting
Rerun the app, and click on “chat.ChatControler” to view Chat’s index view. Try entering text and clicking “Send”. See the message below. Note this message has been sent to the server, where the time is added, and broadcasted to all clients subscribed to “/topic/chat” channel.
Try opening a second browser window, browse to
Send a message from this window. You’ll notice that it appears in both widows.
Play with the app. Find its weakness.
Simple Premortem App
Although the chat app demonstrated the mechanics of setting up a websocket, subscribing and sending messages, it did not demonstrate how a more complex app will interpret the data. This section will design a simple Premortem app to demonstrate interpreting the data sent through the websocket. Although I will explain the implementation of a simple premortem app, the main goal of this section is to demonstrate my process for developing a complex app using websockets.
Premortem Problem
The first step of making an app is defining and analyzing the problem. I like to begin with a general statement of the problem or app. The brief description of the problem or app should be one or two sentences and not mention any implementation details.
Brief Premortem App Description
Premortem is a process or procedure for a facilitator to lead a team through brianstorm reasons for potential failures of a plan followed by brainstorming solutions for the reasons of failures.
Users
My first step of analyzing the app is to identify the users, their roles and characteristics. The Brief Premortem App Description names two user types:
- Facilitator – leads the team through the procedure
- Team – a group of users brainstorming reasons (for failures) and solutions.
Interaction Design/Workflow
My second step is to write the interaction design or workflow for the app. This is a big step and may take several meetings with clients or users and observing the current workflow. Frequently, there is more than one workflow.
Because the premortem process is rather long, I prefer to step back and list the phases for the premortem process.
Complete list Premortem phases:
- Initiating phase
- Gathering and sharing names phase
- Two minutes write reasons phase
- Reason collection:
- Posting Reason phase
- Weighting Reasons phase
- Grouping Reasons phase
- Two minutes write solutions phase
- Solution collection:
- Posting solutions phase
- Weighting solutions phase
- Grouping solutions phase
- Concluding
To organize the phases, I have collected two sets of phases into collections, so there are a total of 11 phases. The list of phases is not a workflow. Each phase has a workflow. But we can already deduce some aspects of the workflow or interaction design.
- The workflow is very linear, i.e. each phase follows another phase sequentially.
- The two collections of phases, reason and solution collections, follow similar sequential phases.
This complete list of phases is too long for this tutorial, so I will simplify it. In fact, if I were to develop the complete app, I would develop it increatmently by adding phases. At the time of writing this tutorial, the initiating and concluding phases were vaguely described by the client, and also I think that they are uncoupled from the core phases of the premortem process, collecting reasons and solutions.
The goal of simplifying the workflow or in this case the list of phases is to create a list that describes the essential aspect of the app and can be quickly implemented so to represent a base version to build on.
The simplified premortem phases:
- Gathering and sharing names phase
- Two minutes write reasons phase
- Posting Reason phase
- Two minutes write solutions phase
- Posting solutions phase
This is a manageable list of phases, so we can attempt to write a workflow for the simple premortem process.
The simple premortem workflow
- Facilitator post message request for names
- Server broadcast message to post names
- For each team member:
- Team member post name
- Server broadcast name to team and facilitator
- Browser adds name to list
- Facilitator post message for the start of writing reasons
- Server broadcast message start writing reasons
- Each team member write reasons on a piece of paper
- Facilitator post message that the 2 minutes over
- Server broadcast message 2 minutes over
- Until all unique reasons are listed:
- Facilitator post message requesting a reason from a team member
- Server broadcast request for a reason from a team member
- Team member post reason
- Server broadcast reason
- Browser adds reason to list
- Facilitator post message for the start of writing solutions
- Server broadcast message start of writing solutions
- Each team member write solutions on a piece of paper
- Facilitator post message that 2 minutes over
- Server broadcasts message 2 minutes over
- Until all unique solutions are listed:
- Facilitator post message requesting a solution from a team member
- Server broadcast request for a solution from a team member
- Team member post solution
- Server broadcast solution
- Browser adds solution to list
The workflow is rather detailed and long. This is why we needed to simplify the list of phases. Also note that each item in the workflow represents either a control statement or an actor acting. Actors include the server besides the facilitator and team members. Consequently, the workflow hints at the implementation.
The workflow clearly identifies the similarity between the steps. Notice that step 9 with its substeps and step 15 with its substeps are very similar with only “solutions” exchanged for “reasons”. Also step 3 with its substeps is similar to both step 9 and 15. All three steps constructing a list.
Also notice that the role of facilitator and server is always the same. The facilitator posts a message and the server always broadcasts the message. It is this aspect of the app, the server broadcasting the facilitator’s messages, which requires the use of websockets.
Data
My design process varies, but is always iterative. At this point, I either like to identify the data or the views. In this case, I’ll start with the data.
There are three list:
- names-list
- reasons-list
- solutions-list
The list items for all three lists are strings
There is a message which is a string posted by the facilitator and broadcasted by the server.
We can also imagine that the app will be organized using a state variable, phase, with states:
- NAMES
- REASONS
- SOLUTIONS
Views
We quickly realized that the views should be a single page because users will want to continuously see the names while communicating, and the team will want to view the list of reasons while writing and posting solutions. In addition the message from the facilitator should always be prominently visible. So an example view for a team member might look like:
Names:
- Bill
- Sam
- Robert
Reasons:
- Not enough time.
- Workflow too complicated
- Phases not clear
Solutions:
- Start early with priority list
- Simplify workflow
- Sequentially add list to views
Message from facilitate: Thank you
[input text … ] [Send button]
As hinted in this example, the app can make the phase apparent by making the lists visible as the facilitator moves from one phase to the next.
The facilitator view can look very similar, but the facilitator will need another widget to move through the phases.
[Current phase field] [Next Phase button]
UML
We are almost ready to implement the app, but I found it helpful to draw a UML diagram for the communication between team members, facilitator and server. I think that this step is appropriate for a web app using WebSockets.
The UML represents the flow of data through the channels as the premortem process progresses through the phases. The channels are represented by the horizontal lines and labelled by the channel name. The lines also depict the direction of flow, what actor initiator data flow and who receives the data. We can see that the facilitator is always posting on the phase and message channels. The phase channel is used to communicate the current phase. Team members only post on the list channel.
Implementation
I’ll not led you through the construction of the app, rather you can clone the app at
https://github.com/rpastel/Simple-Premortem
The app was created using application with options
- Project SDK: 1.8 (java version 1.8.0_???)
- Project Type: Application
- Grails Version: 3.3.11
- Profile: web
- Features: all the boxes unclick
The files modified or added to the base web app are:
- build.gradle
- grails-app/
- conf/application.yml
- websockets/simplepremortem/
- ListWebSocket.groovy
- MessageWebSocket.groovy
- PhaseWebSocket.groovy
- controllers/simplepremortem/
- FacilitatorController.groovy
- TeamController.groovy
- views/
- facilitator/index.gsp
- team/index.gsp
- layouts/app.gsp
- _list.gsp
- _message.gsp
- _phase.gsp
- assests/javascript/
- application.js.es6
- facilitator.js
- team.js
The order is the approximate order that the files were created.
build.gradle
To the dependence section In build.gradle, I added:
// Add for web sockets compile "org.grails.plugins:grails-spring-websocket:2.4.1"
application.yml
To the bottom of application.yml, I added:
server: contextPath: '/simplepremortem'
This was done to find out what modifications would be required for the server context. In actuality, this was the last modification that I made.
websockets/
The plugin permits making a websockets/ directory for the WebSocket artifacts instead of using a controller. I thought that this was a good idea because I view websockets as different from controllers. In particular, websockets do not have views. But, I’m not convinced that this is really necessary or a good idea.
I created WebSockets:
- ListWebSocket.groovy
- MessageWebSocket.groovy
- PhaseWebSocket.groovy
The websockets look very similar. Each has a method for the channels. They are:
- /topic/list for outgoing and /app/list for incoming
- /topic/message for outgoing and /app/message for incoming
- /topic/phase for outgoing and app/phase for incoming
Note that the websockets must be suffixed with WebSocket. Otherwise Spring will not discover them. Also you can only have one channel in each WebSocket and the name of the method for the channel should match the prefix for the WebSocket. For example PhaseWebScoket.groovy should have a method named phase(String message). I tried making a single PremortemWebSocket.groovy with three methods: list, message and phase. This confused Spring. Apparently, Spring would make a bean with listWebSocket but then found a conflict with PremortemWebSocket.list.
Note that I could not get working println and logging in the WebSocket files. This was true even if the websocket method is in a Controller. But I was successful to get println working in the simpler examples.
controllers/
I created two Grails Controllers:
- FacilitatorController.groovy
- TeamController.groovy
These are the default controller created by grails create-controller with only an empty index method. The only role for the controller is to provide end points for the views.
views/
I created two gsp files:
- facilitator/index.gsp
- team/index.gsp
The two views are very similar, so I used Grails layout and templates to construct the views. Open layouts/app.gsp to see what the two views have in common. In the head, both views use the asset pipeline to link spring-websockets.js library which is provided by the plugin.
In the body, both views use the lists and message templates.
<div class="container"> <g:render template="/lists" /> <br/> <g:render template="/message" /> <g:layoutBody/> </div>
Open /views/_lists.gsp, to see the lists template. The list template has three major div for the three lists:
<div id="names-section">
with a<ul id="names-list"> </ul>
<div id="reasons-section">
with a<ul id="names-list"> </ul>
<div id="solutions-section">
with a<ul id="solutions-list"> </ul>
The <div id="????-section">
will be used for hiding the list, and the <ul id="????-list>
is used for appending messages from the list channel on to the list.
Open the /views/_message.gsp to see the message template:
<p id="message-sent"></p> <input id="message" /> <button id="send">Send</button>
The template specifics a paragraph for posting the message, <p id="message-sent">
, and input text box for writing a message to send, <input id="message">
, and finally a “Send” button for posting the message. The Facilitator will use will use the “message” input to send messages through phase channel to the “message-sent” paragraph, while Team will use the “message” input will use the “message” input to send messages through the list channel to one of the lists in the _list.gsp template.
Open the team/index.gps to view to see how the Team view differs from the Facilitator view. In the head is a script tag.
<!-- Load the JS code for team view --> <asset:javascript src="team" />
The script uses asset pipeline to load and run a short script when the page loads. The script initializes the script for the Team view which is defined in application.js.
Look at facilitator/index.gsp again to see how Facilitator view differs. It too uses asset pipeline to load a short JavaScript for the facilitator:
<!-- Load the JS code for facilitator view --> <asset:javascript src="facilitator" />
In the body, the facilitator view has the additional phase template. Open /views/_phase.gsp to see the html code:
<input id="phase"/> <button id="next">Next Phase</button>
The id="next"
button is used for progressing the phase. It uses the input id="phase"
to show the current phase.
team.js and facilitator.js
The scripts for the individual views, team and facilitator, are very similar. For team.js:
// Required for WS JavaScript function //= require application $(function(){ console.log("In team.js running") var app = WS($) app.initializeTeam() })
and for facililator.js:
// Required for WS JavaScript function //= require application $(function(){ console.log("In facilitator.js running") var app = WS($) app.initializeFacilitator() })
They both use asset-pipeline to load the application.js code and then run the corresponding initializing JS code for the view.
application.js.es6
The brains of the app is the JavaScript code in application.js.es6. Note the “.es6” suffix and recall that Grails web profile creates a application.js file. I added the “.es6” suffix to the profile generated application.js because the code will use es6 features, and the “.es6” suffix will alert assets-pipeline to transpile the JavasScript. Without the “.es6” suffix assets-pipeline’s compilation will fail and deployment to a production server will fail.
At the top most level the WS function is organized into code that is shared by both views and returns an object with two methods:
- initializeFacilitator
- initializeTeam
We’ll review these methods after describing the code that is in common.
At the top of the common code:
/* * serverContext is the server contextPath specified in application.yml * Interesting that only the stomp path needs to be corrected. */ const serverContext = '/simplepremortem' /* * Intial Phase settings */ let _phase = null const hideAllPhases = () => { $("#names-section").hide() $("#reasons-section").hide() $("#solutions-section").hide() } hideAllPhases() /* * Create web socket and client */ const _socket = new SockJS( serverContext + '/stomp' ) const _client = Stomp.over( _socket )
The variable _phase
is the important state variable. It is initialized to null. Then the views are initialized by hiding all the lists. The _socket
and _client
are created. Note that adding a server context path only affects the URL for the socket and not the URL for the different channels or topics. This is interesting, and I suspect the reason is that Spring WebSocket uses only one socket for all messages and internally routes the messages to the different channels or topics.
A convenient connect function is defined that both connects the client and subscripes the client to the channels in Stomp.connect callback.
// Convenient connect and subscribe function const connect = (subscriptions ) => { _client.connect({}, () => { /* * Generally, subscribing need to be in the connect callback * to insure that the connection has been made first and * not run into JS async problems. */ console.log('In connect callback') for ( const i in subscriptions ) { console.log('subscription[i]: ', subscriptions[i]) _client.subscribe(subscriptions[i].channel, subscriptions[i].handler) } }) }
The client must be connected before subscribing to channels, so generally subscription is made in the callback. It is possible to subscribe outside the callback, but you must be careful that the connection has already been made, typically by using a user action.
The handlers for the messages passed through the three channels are defined:
- handleList
- handlePhase
- handleMessage
The handlers are the heart of WS. They manage the view through the three phases using a switch on _phase
. The handleList
just routes the messages sent to the list channel to the proper list for the phase.
const handleList = ( message ) => { console.log('In handleList, message: ', message) const msg = JSON.parse(message.body) if( msg.length ) { let listId switch( _phase ){ case "NAMES": listId = '#names-list' break case "REASONS": listId = '#reasons-list' break case "SOLUTIONS": listId = '#solutions-list' break default: console.log('In switch default. Should not get here.') } $(listId).append('<li>' + msg + '</li>') } else { console.log('no message body') } } // end handleList
The handler handlePhase listens to the phase channel.
const handlePhase = ( message ) =>{ console.log('In handlePhase, message: ', message) _phase = JSON.parse(message.body) switch (_phase) { case "END": case "SOLUTIONS": $("#solutions-section").show() case "REASONS": $("#reasons-section").show() case "NAMES": $("#names-section").show() break case "INITIAL": hideAllPhases() break default: _phase = null hideAllPhases() } } // end handlePhase
The Facilitator view sends messages to the phase channel which specifies the phase. Notice that the switch in handlePhase has cases in reverse order without the “break” statement. The missing “break” is deliberate. If the current phase is “SOLUTIONS” then JQuery will show the solution-section and percolate down to show also the reasons-section and names-section.
The handler handMessage manages sent by the Facilitator and simply replaces the message in the message in the message-sent p tag.
const handleMessage = ( message ) => { console.log('In handleMessage, message: ', message) const msg = JSON.parse(message.body) $('#message-sent').text(msg) } // end handleMessage
Another convenient function, initializeSend, is defined for sending messages to channels.
const initializeSend = (channel) => { // Send button sends message to the list channel $("#send").click( () => { send(channel, $("#message").val()) $("#message").val("") }) // So enter keypress clicks the send button $("#message").keypress( (event) => { if (event.which == 13) { // 13 is keycode for enter $("#send").click() return false } }) } // end initializeSend
It is used twice. The initializeFacilitator uses initializeSend to register a listener to the send button that reads the message input and sends it over the message channel. The method initializeTeam uses initializeSend to register the listener to send messages over the phase channel.
The initializeFacilitator method in the returned object method uses the convenient connect function to subscribe to all three channels and to initialize the send button.
It then defines the phases in an array and registers a listener to Facilitator’s next button. The listener just increments the phaseIndex and sends the phase to the phase channel.
initializeFacilitator: () => { console.log('In initializeFacilitator') // connect('topic/list', handleList) const subscriptions = [ { channel: '/topic/list', handler: handleList }, { channel: '/topic/phase', handler: handlePhase }, { channel: '/topic/message', handler: handleMessage } ] connect( subscriptions ) initializeSend('/topic/message') /* * Code for moving through phases. Iterates through an * array of phase names and sends the phase name on the * phase channel. This is code unique to the Facilitator. */ const phases = ['INITIAL', 'NAMES', 'REASONS', 'SOLUTIONS', 'END'] let phaseIndex = 0 $("#phase").val( phases[ phaseIndex ] ) $("#next").click(() => { if ( phaseIndex < phases.length -1 ){ phaseIndex++ $("#phase").val( phases[ phaseIndex ] ) send('/topic/phase', phases[ phaseIndex ]) } }) }, // end initializeFacilitator
The initializeTeam return method only needs to connect the client, subscribe to the three channels and initialize the send button to send messages to the list channel.
Run App
Run the simplemortem app if you have done so already. Use one browser window to view the Facilitator and another window to view Team. Note all the Fallictor progresses through the phases and the Team sends messages to the proper list.
Play enough with the app to learn how to break it.
When you make code changes, you may need to do a hard refresh in the browser to assure that it is not running the cached code.
Further Work
Much work remains to make a complete Premortem app. The styling can be much improved. Also several phases should be added. Because the app is very dependent on the phase, which is a state machine, it might be appropriate to use a JavaScript state machine library such as XState or Javascript State Machine, or to use a state management library such as Redux.
The “two minute write” phase is not explicitly in the app partly because I assumed that the facilitator could implement these phases by sending messages to the Team, but it might be nice to add additional widgets like a green and red light to indicate the beginning of the “two minute write” phase and to indicate the end of the write phase.
More challenging is adding the phases for weighting reasons or solutions and grouping reasons or solutions. I believe both require making the message sent by the Team more complex for the reasons and solutions lists. The simpler phase to implement is the weight phase.
Weighting
Although the process for weighting can vary, the weighting can be represented by an integer for the list item. So the data in the message will be represented by:
{ id: 123-123456-123456-123, text: "A reason for failure is too black", weight: 3 }
We need an id for the list item so that it can be found in the list. You could possibly use the string in the text attribute, but an Universally Unique Identifier (UUID) is a safer implementation.
https://github.com/uuidjs/uuid#readme
One technique for weighting is by voting. To implement the process, in the list, checkboxes appear next to each list item which enable users to check the item. When the item is checked, the weight for the item is incremented and sent to list channels. A number can appear next to the item indicating the weight. Note that this implementation requires that team members vote sequentially and not simultaneously.
Grouping
Grouping the items in list implies grouping list items and possibly naming the group. This is far more difficult to implement. Consider progressing from a random list of reasons to hierarchically list:
Meaning we want to change this list:
Reasons:
- Not enough time for designing
- Not enough designers
- Not enough time for implementing
- In sufficient severs
to this hierarchical list:
Reasons:
- Time
- Not enough time for designing
- Not enough time for implementing
- Resources
- Not enough designers
- Insufficient servers
To implement this we need to manipulate the entire list. Consequently, messages passed through the list channel must be the entire list.
[ {groupId: 15, name: "A Group of Reasons", items: [ {itemId: 1, text: "some reason", weight: 1}, {itemId: 2, text: "another reason, weight: 1}, ] }, {groupId: 16, name: "An Important Group", items: [ {itemId: 3, text: "a great reason", weight: 3}, {itemId: 4, text: "yet another reason, weight: 1}, … ] }, … { goupId: "uncatogrized", items: [ {itemId: 8, text: "a reason not categorized", weight: 1}, {itemId: 9, text: "a reason", weight: 1}, … ] } ]
During the grouping phase, the list can have check boxes and now a “group” button. The workflow could be that a team member selects items to group by checking checkboxes, clicking the “group” button and entering the group name in a modal.
A note: the structure sent through list channel does not be the same for all phases of the app. For example, during the posting phase, the message can be a plain text string, while during the weighting phase, the message can be the JSON with id, text and weight limit, and finally during the categorizing phase, the message can be a JSON of hierarchal lists.
Notice that the does make use of a database. All the data is kept by the clients. If premortem should be saved in database then there are several options depending on the need of the application. If only the final premortem should be saved then the Facilitator can have a button to send the JSON of reasons and solutions to a controller action that saves them to a database. If intermediate results of the phase should be saved the WebSockets could save the messages to the database before relaying the message through the channels.
References
General
https://developer.mozilla.org/en-US/docs/Glossary/WebSockets https://en.wikipedia.org/wiki/Internet_protocol_suite https://developer.mozilla.org/en-US/docs/Web/API/WebSockets_API/Writing_WebSocket_servers
Stomp
http://stomp.github.io/ http://jmesnil.net/stomp-websocket/doc/
SockJS
https://github.com/sockjs/sockjs-client
Grails Plugin
https://github.com/zyro23/grails-spring-websocket/tree/2.4.x https://github.com/zyro23/grails-spring-websocket
Grails 3 Example – I highly recommend this blog.
https://objectpartners.com/2015/06/10/websockets-in-grails-3-0/ https://github.com/mike-plummer/Grails_WebSockets
Spring WebSockets Documentation
https://docs.spring.io/spring-framework/docs/current/reference/html/web.html#websocket
Simple Premortem Repository
https://github.com/rpastel/Simple-Premortem
UUID
https://github.com/uuidjs/uuid#readme
Asset-pipeline
http://www.asset-pipeline.com/manual/index.html