Skip to content

Latest commit

 

History

History
835 lines (666 loc) · 31.5 KB

hello_chat.adoc

File metadata and controls

835 lines (666 loc) · 31.5 KB

Hello, chat

Real-time web applications are notoriously difficult to develop. They require a complex and error-prone infrastructure to handle communications between client and server, and often from server to server, in addition to deep security checks against specific attacks that may prove quite subtle.

Opa makes real-time web simple. In this chapter, we will see how to program a complete web chat application in Opa — in only 20 lines of code, and without compromising security. Along the way, we will introduce the basic concepts of the Opa language, but also user interface manipulation, data structure manipulation, embedding of external resources, as well as the first building bricks of concurrency and distribution.

Overview

Let us start with a picture of the web chat we will develop in this chapter:

result

This web application offers one chatroom. Users connecting to the web application with their browser automatically join this chatroom and can immediately start discussing in real-time. On the picture, we have two users, using regular web browsers. For the sake of simplicity, in this application, we choose the name of users randomly.

If you are curious, this is the full source code of the application:

link:hello_chat.opa[role=include]

Run

In this listing, we define the communication infrastructure for the chatroom, the user interface, and finally, the main application. In the rest of the chapter, we will walk you through all the concepts and constructions introduced.

A bit of style

Before exposing the real machinery, let’s care from the start how the apllication should look like, with this single import line:

import stdlib.themes.bootstrap

This automatically brings Bootstrap CSS from Twitter to your application, so you can use the predefined classes that will just look nice.

Setting up communications

A chat is all about communicating messages between users. This means that we need to decide of what type of messages we wish to transmit, as follows:

type message = {author: string; text: string}

This extract determines that each message is composed of two informations: an author (which is a string, in other words, some text) and a text (also a string).

Tip
About types

Types are the shape of data manipulated by an application. Opa uses types to perform checks on your application, including sanity checks (e.g. you are not confusing a length and a color) and security checks (e.g. a malicious user is not attempting to insert a malicious program inside a web page or to trick the database into confusing informations). Opa also uses types to perform a number of optimizations.

In most cases, Opa can work even if you do not provide any type information, thanks to a mechanism of type inference. However, in this book, for documentation purposes, we will put types even in a few places where they are not needed.

We say that type message is a record with two fields, author and text. We will see in a few minutes how to manipulate a message.

At this stage, we have a complete (albeit quite useless) application. Should you wish to check that your code is correct, you can compile it easily. Save your code as a file hello_chat.opa, open a terminal and enter

Compiling Hello, Chat
opa hello_chat.opa

Opa will take a few seconds to analyze your application, check that everything is in order and produce an executable file. We do not really need that file yet, not until it actually does something. Opa will inform you that you have no server in your application — at this stage, your application is not really useful — but that is ok, we will add the server shortly.

So far, we have defined message. Now, it is time to use it for communications. For this purpose, we should define a network. Networks are a unit of communication between browsers or between servers. As you will see, communications are one of the many domains where Opa shines. To define one, let us write:

Tip
Networks

A network is a real-time web construction used to broadcast messages from one source to many observers. Networks are used not only for chats, but also for system event handling or for user interface event handling.

Networks themselves are built upon a unique and extremely powerful paradigm of distributed session, which we will detail in a further chapter.

room = Network.cloud("room"): Network.network(message)

This extract defines a cloud network called room and initially empty. As everything in Opa, this network has a type. The type of this network is Network.network(message), marking that this is a network used to transmit informations with type message. We will see later a few other manners of creating networks for slightly different uses.

And that is it. With these two lines, we have set up our communication infrastructure — yes, really. We are now ready to add the user interface.

Defining the user interface ~~~~~~~~~

To define user interfaces, Opa uses a simple HTML-like notation for the structure, regular CSS for appearance and more Opa code for interactions. There are also a few higher-level constructions which we will introduce later, but HTML and CSS are more than sufficient for the following few chapters.

For starters, consider a possible skeleton for the user interface:

Skeleton of the user interface (incomplete)
<div class="topbar"><div class="fill"><div class="container"><div id=#logo /></div></div></div>
<div id=#conversation class="container"></div>
<div id=#footer><div class="container">
  <input id=#entry class="xlarge"/>
  <div class="btn primary" >Post</div>
</div></div>

If you are familiar with HTML, you will recognize easily that this skeleton defines a few boxes (or <div>), with some names (or id) and some classes, as well as a text input zone (or <input>) called entry. We will use these names to add interactions and style. If you are not familiar with HTML, it might be a good idea to grab a good HTML reference and check up the tags as you see them.

Actually, for convenience, and because it fits with the rest of the library, we will put this user interface inside a function, as follows:

Skeleton of the user interface factorized as a function (still incomplete)
start() =
(
  <div class="topbar"><div class="fill"><div class="container"><div id=#logo /></div></div></div>
  <div id=#conversation class="container"></div>
  <div id=#footer><div class="container">
    <input id=#entry class="xlarge"/>
    <div class="btn primary" >Post</div>
  </div></div>
)

This extract defines a function called start. This function takes no argument and produces a HTML-like content. As everything in Opa, start has a type. Its type is \-> xhtml .

Functions are bits of the program that represent a treatment that can be triggered as many times as needed (including zero). Functions that can have distinct behaviors, take arguments and all functions produce a result. Triggering the treatment is called calling or invoking the function.

Functions are used pervasively in Opa. A function with type t1, t2, t3 -> u takes 3 arguments, with respective types t1, t2 and t3 and produces a result with type u. A function with type \-> u takes no arguments and produces a result with type u.

There are several ways of defining functions. You can either write f(x1, x2, x3) = some_production or f = x1, x2, x3 \-> some_production, this is absolutely equivalent. Similarly, for a function with no argument, you can either write f() = some_production or f = \-> some_production. To call a function f expecting three arguments, you will need to write f(arg1, arg2, arg3). Similarly, for a function expecting no argument, you will write f().

Warning
Function syntax

In f(x1, x2, x3) = some_production, as well as in f(arg1, arg2, arg3), there is no space between f and (. Adding a space changes the meaning of the extract and would cause an error during compilation.

At this stage, we can already go a bit further and invent an author name, as follows:

Skeleton of the user interface with an arbitrary name (still incomplete)
start() =
(
  author = Random.string(8)
  <div class="topbar"><div class="fill"><div class="container"><div id=#logo /></div></div></div>
  <div id=#conversation class="container"></div>
  <div id=#footer><div class="container">
    <input id=#entry class="xlarge"/>
    <div class="btn primary" >Post</div>
  </div></div>
)

This defines a value called author, with a name composed of 8 random characters.

With this, we have placed everything on screen and we have already taken a few additional steps. That is enough for the user interface for the moment, we should get started with interactivity.

Sending and receiving

We are developing a chat application, so we want the following interactions:

  1. at start-up, the application should join the room;

  2. whenever a message is broadcasted to the room, we should display it;

  3. whenever the user presses return or clicks on the button, a message should be broadcasted to the room.

For these purposes, let us define a few auxiliary functions.

Broadcasting a message to the room
broadcast(author) =
(
   text    = Dom.get_value(#entry)
   message = {author=author text=text}
   do Network.broadcast(message, room)
   Dom.clear_value(#entry)
)

This defines a function broadcast, with one argument author and performing the following operations:

  • read the text entered by the user inside the input called entry, call this text text;

  • create a record with two fields author and text, in which the value of field author is author (the argument to the function) and the value field text is text (the value just read from the input), call this record message;

  • call Opa’s network broadcasting function to broadcast message to network room;

  • clear the contents of the input.

As you can start to see, network-related functions are all prefixed by Network. and user-interface related functions are all prefixed by Dom.. Keep this in mind, this will come in handy whenever you develop with Opa. Also note that our record corresponds to type message, as defined earlier. Otherwise, the Opa compiler would complain that there is something suspicious: indeed, we have defined our network to propagate messages of type message, attempting to send a message that does not fit would be an error.

Tip
About Dom

If you are familiar with web applications, you certainly know about the DOM already. Otherwise, it is sufficient to know that DOM, or Document Object Model, denotes the manipulation of the contents of a web page once that page is displayed in the browser.

Speaking of types, it is generally a good idea to know the type of functions. Function broadcast has type string \-> void, meaning that it takes an argument with type string and produces a value with type void. Also, writing {author=author text=text} is a bit painful, so we added a syntactic sugar for this. We could have written just as well:

Broadcasting a message to the room (variant)
broadcast(author: string): void =
(
   text    = Dom.get_value(#entry)
   message = {~author ~text}
   do Network.broadcast(message, room)
   Dom.clear_value(#entry)
)

Type void is an alias for the empty record, i.e. the record with no fields. It is commonly used for functions whose result is meaningless.

This takes care of sending a message to the network. Let us now define the symmetric function that we want to be called whenever the network propagates a message:

Updating the user interface when a message is received
user_update(x: message) =
(
  line =  <div class="row line">
            <div class="span1 columns userpic" />
            <div class="span2 columns user">{x.author}:</div>
            <div class="span13 columns message">{x.text}
            </div>
         </div>
  do Dom.transform([ #conversation +<- line ])
  Dom.scroll_to_bottom(#conversation)
)

The role of this function is to display a message just received to the screen. This function first produces a few items of user interface, using the same HTML-like syntax as above, and calls these items line. It then calls the Dom.transform function to add the contents of line at the end of box conversation we have defined earlier. And finally, it scrolls to the bottom of the box, to ensure that the user can always read the most recent items of the conversation.

If you look more closely at the HTML-like syntax, you may notice that the contents inside curly brackets are probably not HTML. Indeed, these curly brackets are called inserts and they mark the fact that we are inserting not text "x.author", but a text representation of the value of x.author, i.e. the value of field author of record x.

Tip
About inserts

Opa provides inserts to insert expressions inside HTML, inside strings and in a few other places that we will introduce as we meet them.

This mechanism of inserts is used both to ensure that the correct information is displayed and to ensure that this information is sanitized if needs be. It is powerful, simple and extensible.

In the extract, we have also seen function Dom.transform, whose role is to perform a (possibly complex) transformation on the screen, here by inserting the contents of line at the end of conversation. Other operators than +\<- can be used to provide insertion at start, replacement, etc. Actually, this function takes a list of transformations — something materialized in the syntax by the square brackets.

We are now ready to connect interactions.

Connecting interactions

Let us connect broadcast to our button and our input. This changes function start as follows:

Skeleton of the user interface connected to broadcast (still incomplete)
start() =
(
  author = Random.string(8)
  <div class="topbar"><div class="fill"><div class="container"><div id=#logo /></div></div></div>
  <div id=#conversation class="container"></div>
  <div id=#footer><div class="container">
    <input id=#entry class="xlarge" onnewline={_ -> broadcast(author)}/>
    <div class="btn primary" onclick={_ -> broadcast(author)}>Post</div>
  </div></div>
)

We have just added event handlers to entry and our button. Both call function broadcast, respectively when the user presses return on the text input and when the user clicks on the button. As you can notice, we find again the curly brackets.

Tip
About event handlers

An event handler is a function whose call is triggered not by the application but by the user herself. Typical event handlers react to user clicking (the event is called click), pressing return (event newline), moving the mouse (event mousemove) or the user loading the page (event ready).

Event handlers are always connected to HTML-like user interface elements. An event handler always has type Dom.event \-> void.

You can find more informations about event handlers in the Opa API documentation by searching entry Dom.event .

We will add one last event handler to our interface, to effectively join the network when the user loads the page, as follows:

Skeleton of the user interface now connected to everything (final version)
start() =
(
  author = Random.string(8)
  <div class="topbar"><div class="fill"><div class="container"><div id=#logo /></div></div></div>
  <div id=#conversation class="container" onready={_ -> Network.add_callback(user_update, room)}></div>
  <div id=#footer><div class="container">
    <input id=#entry class="xlarge" onnewline={_ -> broadcast(author)}/>
    <div class="btn primary" onclick={_ -> broadcast(author)}>Post</div>
  </div></div>
)

This event handler is triggered when the page is (fully) loaded and connects function user_update to our network.

And that is it! The user interface is complete and connected to all features. Now, we just need to add the server and make things a little nicer.

Opa has a special value name \_, pronounced "I don’t care". It is reserved for values or arguments that you are not going to use, to avoid clutter. You will frequently see it in event handlers, as it is relatively rare to need details on the event, at least in this book.

Bundling, building, launching ~~~~~~~~~~~ Every Opa application needs a server, to determine what is accessible from the web, so let us define one:

Tip
About servers

In Opa, every web application is defined by one or more server. A server is an entry point for the web, which offers to users a set of resources, such as web pages, stylesheets, images, sounds, etc.

The server (first version)
server = Server.one_page_bundle("Chat", [], [], start)

This extract defines a server, using Server.one_page_bundle, one of the many server constructors provided by Opa. This specific constructor takes four arguments:

  • the name of the application — users see it as the title in a browser tab or window;

  • a list of additional static files to provide, in particular images or stylesheets — more on this later, for the moment, an empty list will be sufficient;

  • a list of style informations to apply — more on this later, for the moment, an empty list will be sufficient;

  • a function used to initialize and display the user interface — here, our function start.

Tip
About server

The value server is special. It does not have to be defined, although an application without a server is generally not really useful, but if it is defined it must have type service. You can also define more than one server in one application, generally to handle distinct subdomains and/or paths. We will cover this later.

You can find more information on the definition of a server in the documentation of the standard library, by searching for Server.

Well, we officially have a complete application. Time to test it!

We have seen compilation already:

Compiling Hello, Chat
opa hello_chat.opa

Barring any error, Opa will inform you that compilation has succeeded and will produce a file called hello_chat.exe. This file contains everything you need, so you can now launch it, as follows:

Running Hello, Chat
./hello_chat.exe

Congratulations, your application is launched. Let us visit it.

Tip
About hello_chat.exe

The opa compiler produces self-sufficient executable applications. The application contains everything is requires, including:

  • webpages (HTML, CSS, JavaScript);

  • any resource included with @static_resource_directory;

  • the embedded web server itself;

  • the distributed database management system;

  • the initial content of the database;

  • security checks added automatically by the compiler;

  • the distributed communication infrastructure;

  • the default configuration for the application;

  • whatever is needed to get the various components to communicate.

In other words, to execute an application, you only need to launch this executable, without having to deploy, configure or otherwise administer any third-party component.

Tip
About 8080

By default, Opa applications are launched on port 8080. To launch them on a different port, use command-line argument --port. For some ports, you will need administrative rights.

As you can see, it works, but it is not very nice yet:

result without css

Perhaps it is time to add some style.

Adding some style

In Opa, all styling is done with stylesheets defined in the CSS language. This tutorial is not about CSS, so if you feel rusty, it is probably a good idea to keep a good reference at hand.

You have two possibilities for adding style information. You can either do it inside your Opa source file or as an external file. For this example, we will use an external file with the following contents:

Contents of file resources/css.css
link:resources/css.css[role=include]

Create a directory called resources and save this file as resources/css.css. It might be a good idea to add a few images to the mix, matching the names given in this stylesheet (opa-logo.png, user.png, btn_or.png) also in directory resources.

Now, we will want to instruct our server to access the directory and to use our stylesheet, by rewriting our server definition as follows:

The server (final version)
server = Server.one_page_bundle("Chat",
   [@static_resource_directory("resources")], ["resources/css.css"], start)

In this extract, we have instructed the Opa compiler to embed the files of directory resources, offer them to the browser, and use our CSS file as the main style for the application.

Tip
About embedding

In Opa, the preferred way to handle external files is to embed them in the executable. This is faster, more secure and easier to deploy than accessing the file system.

To embed a directory, use directive @static_resource_directory.

Tip
About directives

In Opa, a directive is an instruction given to the compiler. By opposition to functions, which are executed once the application is launched, directives are executed while the application is being built. Directives always start with character @.

While we are adding directives, let us take this opportunity to inform the compiler it does not have to protect the chatroom from clients, as follows:

@publish room = Network.cloud("room"): Network.network(message)

This will considerably improve the speed of the chat.

We are done, by the way. Not only is our application is now complete, it also looks nice:

result

As a summary, let us recapitulate the source file:

The complete application
link:hello_chat.opa[role=include]

Run

All this in 20 effective lines of code (without the CSS). Note that, in this final version, we have removed some needless parentheses, which were useful mostly for explanations, and documented the code.

Questions

Where is the room?

Good question: we have created a network called room and we haven’t given any location information, so where exactly is it? On the server? On some client? In the database?

As room is shared between all users, it is, of course, on the server, but the best answer is that, generally, you do not need to know. Opa handles such concerns as deciding what goes to the server or to the client. We will see in a further chapter exactly how Opa has extracted this information from your source code.

Where are my headers?

If you are accustomed to web applications, you probably wonder about the absence of headers, to define for instance the title, favicon, stylesheets or html version. In Opa, all these concerns are handled at higher level. You have already seen one way of connecting a page to a stylesheet and giving it a title. As for deciding which html version to use, Opa handles this behind-the-scenes.

To do or not to do?

The source code presented above uses construction do something, which may be mysterious for you at this stage.

There is a very good reason for this construction: in Opa, every function definition (and more generally every value not at toplevel) ends with one value, which is the result of the function — conversely, once we have reached the first value, we have the result of the function, so the function is complete. And, in Opa, everything is a value except value definitions and do actions.

For instance, let us consider the following extract:

broadcast(author) =
(
   text    = Dom.get_value(#entry)
   message = {author=author text=text}
   do Network.broadcast(message, room)
   Dom.clear_value(#entry)
)

This is the definition of a value called broadcast. This value is a function whose production is the following value:

(
   text    = Dom.get_value(#entry)
   message = {author=author text=text}
   do Network.broadcast(message, room)
   Dom.clear_value(#entry)
)

or, equivalently,

text    = Dom.get_value(#entry)
message = {author=author text=text}
do Network.broadcast(message, room)
Dom.clear_value(#entry)

This value is composed of two value definitions, one do action and a simpler value, the result of the function.

If we had omitted this do, our function definition would have been understood as:

broadcast(author) =
(
   text    = Dom.get_value(#entry)
   message = {author=author text=text}
   Network.broadcast(message, room)   //This is the result

   Dom.clear_value(#entry)            //This is a syntax error
)
Tip
do vs. return

If you are familiar with languages using the return construction, you can think of do as the opposite of return. In Opa, we spend more time returning results than calling functions without using the result, so this is mirrored in the syntax.

The rule to remember is that you need a do if you are computing a value but you are not defining any value, and it is also not your result.

To type or not to type?

As mentioned earlier, Opa is designed so that, most of the time, you do not need to provide type information manually. However, in some cases, if you do not provide type information, the Opa compiler will raise a value restriction error and reject the code. Database definitions and value restricted definitions are the (only) cases in which you need to provide type information for reasons other than optimization, documentation or stronger checks.

For more information on the theoretical definition of a value restriction error, we invite you to consult the reference chapters of this book. For this chapter, it is sufficient to say that value restriction is both a safety and a security measure, that alerts you that there is not enough type information on a specific value to successfully guard this value against subtle misuses or subtle attacks. The Opa compiler detects this possible safety or security hole and rejects the code, until you provide more precise type information.

This only ever happens to toplevel values (i.e. values that are defined outside of any function), so sometimes, you will need to provide type information for such values. Since it is also a good documentation practice, this is not a real loss. If you look at the source code of Opa’s standard library, you will notice that the Opa team strives to always provide such information, although it is often not necessary, for the sake of documentation.

Exercises

Time to see if this tutorial taught you something! Here are a few exercises that will have you expand and customize the web chat.

Customizing the display

Customize the chat so that

  • the text box appears on top;

  • each new message is added at the top, rather than at the bottom.

You will need to use operator -\<- instead of \<-+ to add at start instead of at end.

Saying "hello"

  • Customize the chat so that, at startup, at the start of #conversation, it displays the following message to the current user:

Hello, you are user 8dh335

(replace 8dh335 by the value of author, of course).

  • Customize the chat so that, at startup, it displays the following message to all users:

User 8dh335 has joined the room
  • Combine both: customize the chat so that the user sees

Hello, you are user 8dh335

and other users see

User 8dh335 has joined the room
Tip
About comparison

To compare two values, use operator == or, equivalently, function \`==` (with the backquotes). When comparing x == y (or \`==`(x,y)), x and y must have the same type. The result of a comparison is a boolean. We write that the type of function `==` is 'a,'a \-> bool.

Tip
About booleans

In Opa, booleans are values {true = void} and {false = void}, or, more concisely but equivalently, \{true\} and \{false\}. You can check whether boolean b is true or false by using if b then ... else ... or, equivalently, match b with \{true\} -> ... | \{false\} -> ....

Distinguishing messages between users

Customize the chat so that your messages are distinguised from messages by other users: your messages should be displayed with one icon and everybody else’s messages should be displayed with the default icon.

User customization

  • Let users choose their own user name.

  • Let users choose their own icon. You can let them enter a URI, for instance.

Caution
More about xhtml

For security reasons, values with type xhtml cannot be transmitted from a client to another one. So you will have to find another way of sending one user’s icon to all other users.

Security

As mentioned, values with type xhtml cannot be transmitted from a client to another one. Why?

And more

And now, an open exercise: turn this chat in the best chat application available on the web. Oh, and do not forget to show your app to the community!