Chapter Three: Creating a Google Docs clone with WebSockets using Javalin

What You Will Learn

In this tutorial we will create a very simple realtime collaboration tool (like google docs). We will be using WebSockets for this, as WebSockets provides us with two-way communication over a one connection, meaning we won’t have to make additional HTTP requests to send and receive messages. A WebSocket connection stays open, greatly reducing latency (and complexity). Dependencies

Create a maven project with dependencies

We will be using Javalin for our web-server and WebSockets, and slf4j for logging:

pom.xml
 1 <dependencies>
 2     <dependency>
 3         <groupId>io.javalin</groupId>
 4         <artifactId>javalin</artifactId>
 5         <version>1.7.0</version>
 6     </dependency>
 7     <dependency>
 8         <groupId>org.slf4j</groupId>
 9         <artifactId>slf4j-simple</artifactId>
10         <version>1.7.25</version>
11     </dependency>
12 </dependencies>

The Java application

The Java application is pretty straightforward. We need:

  • a data class (Collab) containing the document and the collaborators
  • a map to keep track of document-ids and Collabs
  • websocket handlers for connect/message/close

We can get the entire server done in about 40 lines:

Websockets server example
 1 import io.javalin.Javalin;
 2 import io.javalin.embeddedserver.jetty.websocket.WsSessio\
 3 n;
 4 import java.util.Map;
 5 import java.util.concurrent.ConcurrentHashMap;
 6 
 7 public class Main {
 8 
 9     private static Map<String, Collab> collabs = new Conc\
10 urrentHashMap<>();
11 
12     public static void main(String[] args) {
13 
14         Javalin.create()
15             .port(7070)
16             .enableStaticFiles("/public")
17             .ws("/docs/:doc-id", ws -> {
18                 ws.onConnect(session -> {
19                     if (getCollab(session) == null) {
20                         createCollab(session);
21                     }
22                     getCollab(session).sessions.add(sessi\
23 on);
24                     session.send(getCollab(session).doc);
25                 });
26                 ws.onMessage((session, message) -> {
27                     getCollab(session).doc = message;
28                     getCollab(session).sessions.stream().\
29 filter(WsSession::isOpen).forEach(s -> {
30                         s.send(getCollab(session).doc);
31                     });
32                 });
33                 ws.onClose((session, status, message) -> {
34                     getCollab(session).sessions.remove(se\
35 ssion);
36                 });
37             })
38             .start();
39 
40     }
41 
42     private static Collab getCollab(WsSession session) {
43         return collabs.get(session.param("doc-id"));
44     }
45 
46     private static void createCollab(WsSession session) {
47         collabs.put(session.param("doc-id"), new Collab()\
48 );
49     }
50 
51 }

We also need to create a data object for holding our document and the people working on it:

The Collboration object
 1 import io.javalin.embeddedserver.jetty.websocket.WsSessio\
 2 n;
 3 import java.util.Set;
 4 import java.util.concurrent.ConcurrentHashMap;
 5 
 6 public class Collab {
 7     public String doc;
 8     public Set<WsSession> sessions;
 9 
10     public Collab() {
11         this.doc = "";
12         this.sessions = ConcurrentHashMap.newKeySet();
13     }
14 }

Building a JavaScript Client

In order to demonstrate that our application works, we can build a JavaScript client. We’ll keep the HTML very simple, we just need a heading and a text area:

1 <body>
2     <h1>Open the URL in another tab to start collaboratin\
3 g</h1>
4     <textarea placeholder="Type something ..."></textarea>
5 </body>

The JavaScript part could also be very simple, but we want some slightly advanced features:

  • When you open the page, the app should either connect to an existing document or generate a new document with a random id
  • When a WebSocket connection is closed, it should immediately be reestablished
  • When new text is received, the user caret (“text-cursor”) should remain in the same location (easily the most complicated part of the tutorial).
 1 window.onload = setupWebSocket;
 2 window.onhashchange = setupWebSocket;
 3 
 4 if (!window.location.hash) { // document-id not present i\
 5 n url
 6     const newDocumentId = Date.now().toString(36); // thi\
 7 s should be more random
 8     window.history.pushState(null, null, "#" + newDocumen\
 9 tId);
10 }
11 
12 function setupWebSocket() {
13     const textArea = document.querySelector("textarea");
14     const ws = new WebSocket(`ws://localhost:7070/docs/${\
15 window.location.hash.substr(1)}`);
16     textArea.onkeyup = () => ws.send(textArea.value);
17     ws.onmessage = msg => {
18     // place the caret in the correct position
19         const offset = msg.data.length - textArea.value.l\
20 ength;
21         const selection = {start: textArea.selectionStart\
22 , end: textArea.selectionEnd};
23         const startsSame = msg.data.startsWith(textArea.v\
24 alue.substring(0, selection.end));
25         const endsSame = msg.data.endsWith(textArea.value\
26 .substring(selection.start));
27         textArea.value = msg.data;
28         if (startsSame && !endsSame) {
29             textArea.setSelectionRange(selection.start, s\
30 election.end);
31         } else if (!startsSame && endsSame) {
32             textArea.setSelectionRange(selection.start + \
33 offset, selection.end + offset);
34         } else { // this is what google docs does...
35             textArea.setSelectionRange(selection.start, s\
36 election.end + offset);
37         }
38     };
39     ws.onclose = setupWebSocket; // should reconnect if c\
40 onnection is closed
41 }

And that’s it! Now try opening localhost:7070 in a couple of different browser windows (that you can see simultaneously) and collaborate with yourself. Conclusion

We have a working realtime collaboration app written in less than 100 lines of Java and JavaScript. It’s very basic though, some things to add could include:

  • Show who is currently editing the document
  • Persist the data in a database at periodic intervals
  • Replace the textarea with a rich text editor, such as quill
  • Replace the textarea with a code editor such as ace for collaborative programming
  • Improving the collaborative aspects with operational transformation

The use cases are not limited to text and documents though, you should use WebSockets for any project which requires a lot of interactions with low latency. Have fun!