Last updated on

Centralized webapps

In this lab (and the next one), you will build centralized webapps using many of the principles of software construction you have seen in the course so far. That term will be explained in more detail very soon, but you probably already use many such centralized webapps in your everyday life: ticket-reservation platforms, multiplayer online card games or board games, e-banking, Q&A forums, chat, etc. In particular, using a simple state machine model, you will implement the logic of a Rock-Paper-Scissors game in this lab.

This lab aims to exercise the following concepts and techniques:

Logistics

High-level overview

A webapp is a piece of software that runs over the web. (Our running example for this section will be a classroom booking platform for EPFL.) The clients of a webapp are the programs that expose its core functionality to the outside world. (For instance, a campus administrator’s portal where they can update the availability of a room is a client, as is the smartphone app where you can search for rooms on a given date.) Often, this system has some state (e.g. which dates a particular room is free/booked), and some information needs to be communicated between clients (if the campus admin removes a room for maintenance, it shouldn’t show up in your search results).

One way to structure the webapp is to have a server that manages all the state and performs actions only in response to client events. On receiving an event (e.g. book a room for Nov 22), the server can optionally modify the state, and optionally send a response to that client, or other messages to other clients too. Then clients can initiate further events. This is a centralized webapp because the state is centralized in the server.

Another way to structure the system would be to have no complex logic on the server, and to have clients communicate directly with one another, where each client would hold some part of the state. In that model, servers may be used to facilitate communication and queue messages, but don’t perform complex processing. (For example, when you send an SMS message to your friend, you do connect to a ‘server’ (the cell tower), but it only facilitates communication between your client (phone) and theirs. The server has no state and you and your friend maintain local copies of your message history.) This would be a decentralized webapp. What are the advantages and disadvantages of this design compared to a centralized one? (For example, who is affected if you lose your phone? Is it possible to fully delete a message?)

State machines

Suppose we have a system with some state that changes in response to events. We can construct a graph where every vertex corresponds to a possible state and every edge, or state transition, corresponds to an event. Then, the evolution of the system over time as it responds to events can be represented as a path in this graph, following the corresponding transitions from state to state. (The graph itself does not change at all.)

This representation of a stateful system as a graph that transitions (responds to events) from one vertex (state) to the next is called a state machine. State machines are one of the most useful concepts in computer science, because they let us reason about many systems that look different on the surface with the same mental tools.

As a first example, let’s try to model the software that decides when to open and shut the doors on the M2 metro line. Each bubble is a state and each arrow is an event.

metro first try

Hmm, but that’s not quite right. The doors don’t transition directly from open to shut: they try to shut, but if someone is standing in the way, they go back to being open, and try again. The metro does not start until they are actually shut. Also, they shouldn’t open whenever the metro stops: they should only open when the metro stops and is aligned with the external doors on the platform. We can try to represent these additional details in a different state machine:

metro better

We have incorporated more information into our state, and introduced more events, to better represent the real system. Having this sort of model lets us reason about the system in many different ways.

Try to come up with as detailed of a state machine for the M2 doors as possible. Think about safety of passengers, emergency protocols, malfunctioning equipment, the fact that there are multiple independent doors, etc. How many states do you need? How would you structure the actual implementation? How would you prove that it is correct with respect to the state machine?

For our second example, let’s try to model the classroom booking platform from before with a state machine. The state maintains, for each campus, for each of its rooms, for each date, whether or not the room is free on that date. “For each date” means that the state is infinitely large! Of course, the actual implementation does not have infinite memory, so we may choose to have a different representation of the state—for example, storing only the dates on which a room is booked, and assuming the rest are free—or to make a simplifying assumption—for example, no reservations can be made more than six months in advance. But an infinite theoretical state lets us model the system elegantly and reason about whether those implementation choices are valid.

An example state:

room reservation state

We can’t draw the infinitely many states, but we describe them and reason about them, because we know how the possible events change the state.

Again, we can ask whether our state and transitions capture everything we want about the system. How can a user cancel a reservation (ideally, only their own)? Does ‘unavailable due to maintenance’ need to be a separate status from ‘booked’, i.e. are there events that apply to one but not the other? How do we represent the server “holding” a reservation for 15 minutes to give the user time to fill out their personal information and information about the event, before finalizing the reservation? And so on.

Try to come up with a state machine for a Rock-Paper-Scissors game (3 rounds) with two clients (players). The state space is not infinite, but still very large, so you won’t be able to write/draw it out in full, but try to describe it as accurately as possible. Later, compare your state machine against the one suggested by the scaffold code for the first part of this lab.

Architecting webapps with state machines

Each webapp you will design and implement is a state machine. The state machine will be initialized with a fixed list of clients. Clients will initiate events and send them to the server. The server will receive these events and perform the corresponding state transitions, i.e. update the state. Only the server maintains any state.

Beyond that, a key aspect of your webapps will be that each user has access to some part of the state. (E.g., the campus admin has access to the booking status of all the rooms on that campus through their own reservation portal.) The clients have no state nor memory, so all that they know about the world at a given point in time is exactly the content of the last message they received from the server. We call this a view or projection of the state. Views are sent by the server to each client, and the clients render them with some UI (user interface). Finally, the client sets up the UI to translate the user’s actions (e.g. clicking the search button) into events that are sent to the server.

Many card games and board games can be modeled in this pattern. Can you think of some games where the view/projection is the same for all clients (players)? And others where it is different for each client? Once you have thought of a few examples, the Wikipedia article about perfect information is a good starting point to learn more about how this topic is studied in game theory.

Actions: render, pause, alert

We are going to extend our notion of a state machine to make it more convenient to model the sorts of apps we want to build. Instead of each event triggering a state transition, it will trigger a sequence of actions that are sent to all the clients. In technical terms, this is a labeled transition system.

There are three types of actions: renders, pauses, and alerts.

Pauses and alerts are dispatched as-is to all clients. Renders must be projected into views before being sent to clients.

These actions are performed by clients in order.

Why do we need these extensions?

For example, consider the game Uno. When a player plays the penultimate card in their hand, they must say “Uno!”. So the other players (clients) not only see the card that was just played (the view), but also hear “Uno!” (a side effect). In a webapp version of Uno, the server has to tell the clients to perform that side effect (in our model we might use an alert or a separate view).

This means that each event can trigger multiple actions, even multiple renders which correspond to state transitions. Why? Consider a countdown timer as an example. The first event from the client sets the countdown to, say, 3 seconds. The second event from the client starts the countdown. But then, the state machine transitions between four states—“3 seconds left”, “2”, “1”, “0”—without receiving any events in between. For each state, the client receives a side effect of waiting for 1 second, along with a view of the time left.

To summarize:

  1. A client sends the server an event.
  2. The server produces a sequence of actions:
    1. For each render action the server updates its state and sends each client a view of that new state.
    2. Other actions are sent as-is.
  3. Each client:
    1. Performs the actions in order. Side effects are executed and views are rendered with some UI.
    2. Sets up the UI to fire the next event based on the user’s actions.

Below is an illustration of client-server interactions for a simple classroom reservation platform.

illustration of room reservation webapp

another illustration of room reservation webapp

One last bit of terminology: the representation of the state (which exists only on the server side) and the way the server responds to events is collectively called the model. Hence, this structure for webapps is called the model-view design pattern.

Optional reading: for a different explanation of a similar design pattern, you can read about the Elm Architecture. It involves the Elm programming language but the ideas are broadly applicable.

Implementation overview

The previous section was purely about the design of centralized webapps. In this section, we’ll look a bit more concretely at how they are implemented.

Serialization and deserialization

So far, we haven’t looked too closely at how a client and server can actually communicate with each other. Values in a program—say, instances of a class in Scala—cannot directly be transmitted over the internet, because the format of internet communications is different from the internal format of values in a program’s memory. Thus, the server and its clients need to agree on how to translate between the two, called serialization (from program to internet) and deserialization (the reverse).

Your webapps will have to define their own serializers and deserializers. This week’s exercise set has a useful exercise.

Extensibility

In our webapp framework, every webapp defines:

The event and view types, as well as the serialization and deserialization functions, will be shared between the client code and the server code. The state and transition and projection functions will only live with the server. The view rendering will only live with the client.

Everything else is just the plumbing between these parts, and thus we have provided it to you as a library, cs214.webapp. You don’t have to understand how the library code works but you do have to understand its API and how to use it to build your own webapps.

But then, creating another app is just a matter of implementing the three representation types and functions! This is how you will extend your program with a Rock-Paper-Scissors app and some other app of your choice after implementing Rock-Paper-Scissors.

These apps are extensible in another way, too, because the same app can have different clients as long as they communicate with the server with the same events and views in the same serialized format. For example, this Rock Paper Scissors app comes with a regular HTML-based client where the user can click to play, but also as a REPL client where the user types in their moves. We could even have implemented a physical UI with buttons and lights. Similarly, the webapp doesn’t have to run over the web. As long as the server and its clients can communicate using the format they have agreed upon, they could be on a LAN, or even on the same machine, or there could even be just one client. Again, this is possible because they have a well-defined communication interface that doesn’t depend on the specifics of the plumbing that connects them.

Code distribution

In the top-level directory for this lab that you cloned, apps/**/rps contains the scaffold code for the Rock Paper Scissors app. This is a cross-platform project, which means that different parts of the project are compiled for different platforms; in our case, JavaScript (JS) for the client, and JVM as usual for the server. Some parts of the project are shared between the two. The directory structure is thus as follows:

This lab uses the cs214.webapp library, in addition to the Scala standard library and a couple other web-related libraries. The source code is available on GitLab if you want to read it—but you should not have to, to successfully complete this lab! The API type signatures and documentation should be enough. You are not expected to understand how the library works, but you are expected to learn how to use the library to build your own webapps. This is an essential software construction skill.

So far, all the code in CS-214 has been compiled for the JVM platform using the regular Scala compiler. JVM code can run in many different contexts, from SIM cards to dedicated Java microprocessors in automobiles. JVM code used to be able to run in browsers, too, in the form of Java applets, but other web technologies like JavaScript (no relation to Java) have since overtaken it. The current way to run Scala in a browser is instead by compiling it directly to JavaScript with Scala.js. Understanding how this works and how to use it is beyond the scope of this course!

The following tree summarizes the layout of the code:

.
├── apps/
│   ├── js/
│   │   └── src/main/scala/
│   │               └── apps/
│   │                   ├── rps/
│   │                   │   ├── HtmlUI.scala
│   │                   │   └── TextUI.scala
│   │                   └── MainJS.scala
│   ├── jvm/
│   │   └── src/
│   │       ├── main/
│   │       │   ├── scala/
│   │       │   │   └── apps/
│   │       │   │       ├── rps/
│   │       │   │       │   └── Logic.scala
│   │       │   │       └── MainJVM.scala
│   │       │   └── resources/www/static/
│   │       │       └── rps.png (app thumbnail)
│   │       └── test/
│   │           └── scala/
│   │               └── apps/
│   │                   └── rps/
|   |                       └── Tests.scala
│   └── shared/
│       └── src/main/scala/
│                   └── apps/
│                       └── rps/
│                           ├── Wire.scala
│                           └── types.scala
└── build.sbt

App Diagram

The following diagram depicts all the steps triggered by a client event. Each box represents a component and each arrow represents a communication. Components are annotated with their containing source file in italics. The annotation in parentheses is the internal format/representation: a piece of Scala code, an immutable Scala object in memory, a mutable var, or a serialized representation over the network.

Start following the steps at label “1” in the client.

diagram of a webapp's inter-components interactions

Implementation guide for Rock-Paper-Scissors

Note: build configuration

This lab uses a more complex build configuration than usual, which causes issues with completion and imports in VS Code when using the default IDE configuration. We have already included a VS Code configuration setting to remediate this in the project. However:

You will need to use sbt --client instead of sbt to start SBT on the command line. If nothing appears when you type commands into SBT, try sbt -Djline.terminal=none --client instead.

You can read more about this issue in the “Troubleshooting” section of the degrees lab.

State, event, and view types

Let’s start with the state, event, and view types. These will be in the file types.scala under shared/.

Your friend has been working on the UI already, and the UI needs to know about the event and view types, so they went ahead and implemented those:

enum Event:
  /** A player has chosen their hand. */
  case HandSelected(hand: Hand)

apps/shared/src/main/scala/apps/rps/types.scala

/** A view of the rock paper scissor's state for a specific client.
  *
  * The UI alternates between two views: selecting next hand and viewing the
  * results, attributing corresponding scores.
  *
  * @param phaseView
  *   A projection of the current phase of the game.
  * @param scoresView
  *   The score of each player.
  */
case class View(
    phaseView: PhaseView,
    scoresView: ScoresView
)

enum PhaseView:
  /** Players are selecting their next hand. */
  case SelectingHand(ready: Map[UserId, Boolean])

  /** Players are looking at each other's hand. */
  case ViewingHands(hands: Map[UserId, Hand])

type ScoresView = Map[UserId, Int]

apps/shared/src/main/scala/apps/rps/types.scala

(What is UserID? Your first interaction with the webapp library! Since user IDs are something that many apps will have to deal with, the library provides an implementation. It is imported at the top of the file.)

On the other hand, the UI does not care about the state, so the State type is not implemented. Implement that type.

Remember that you already came up with a state machine for Rock-Paper-Scissors in a previous section. Are those events and views the same as in types.scala? Can you use the same state type?

Serialization and deserialization

For this section, you will work in Wire.scala in the same directory as types.scala.

Events and views need to be serialized and deserialized for communication between the server and the clients. In addition, the automated tests don’t know the details of your representations, but need to check that the game is implemented correctly anyway. This is done via probes: methods that check specific internal properties of views and events without relying on the internal representations.

As in the contextual abstraction exercises, wire formats for standard types (Bool, Int, Option, etc.) are already defined in the library. Using them, fill in the missing pieces marked with ??? in Wire.scala.

If you’re unsure how to work with JSON values, check out the webapps exercise set.

State transitions and projections

You’ve defined the state, event, and view types, and the serialization and deserialization functions. Your friend wrote the rendering function that displays a view and sets up events to be fired on user actions. What’s left is the server-side code: the transition function that responds to events, and the projection function that determines what view each client receives.

This code is in Logic.scala under the jvm/ subdirectory.

  1. The server receives an event from a client as an instance of Event.
  2. The transition function takes as input that Event, as well as the UserID that originated it and the current State. It produces as output a Try[Seq[Action[State]]].
    1. The Try[…] allows for the possibility of returning a Failure(…) containing an exception in case of an illegal move from one player (see the library for an already defined custom exception). Otherwise, the function returns a Success(…).
    2. The Seq[…] means that a sequence of Actions can be returned.
    3. An Action[T] is either a Render of T, a Pause, or an Alert.
    4. In our case, T is State. A Render of a State results in the corresponding state transition on the server and the corresponding View being sent to the client.
  3. The project function takes as input a State and a UserID and produces as output the appropriate View. This is used by the webapp library when it handles a Render action as described above.

Furthermore, when the server first starts, the init function produces the initial State.

Implement the missing parts (???) in Logic.scala.

Try it!

Check your implementation by trying to play the game with the browser client.

  1. In the top-level directory (not the subproject directories!), run sbt --client.

    • If nothing appears when you type commands into SBT, try sbt -Djline.terminal=none --client instead.
  2. At the SBT prompt, use run to start the server on port 8080.

  3. In your browser, go to localhost:8080. This will start a client in that browser tab.

  4. Choose the “Rock-Paper-Scissors” app and enter two user IDs (e.g. me, myFriend).

  5. Choose a UI. Your friend has implemented two options:

    • Text-based UI: Moves are entered as text (“rock”, “paper” or “scissors”).
    • HTML-based UI: Moves are entered by clicking buttons.

    This is purely a client-side choice. The server-side logic is the same, regardless of which UI is chosen. Two players playing the same game don’t even need to use the same UI.

  6. Select an ID for yourself on the next screen.

  7. To play with a friend, send them the URL that is displayed on the final screen. You can also open this URL in a separate browser or tab to play with yourself.

Your friends need to be connected to the same local network (WiFi/LAN) as you.

You can also run the automated tests with test at the SBT prompt.

Every time you make any changes, to restart the webapp:

  1. Close all browser tabs where you have started the client.
  2. Stop the server by pressing Ctrl+C twice.
  3. Do the steps above to restart the server and client.

Next steps

Now that you know how to build webapps, you’re ready to start thinking about the capstone of CS-214: the unguided lab! To prepare, you can: