11 R and JS

This rather technical chapter aims at untangling what are the main mechanisms behind a Shiny app, responsible for driving the R/JavaScript communication, which is quite frankly mind blowing.

This is a feature leveraging the httpuv package. The HTTP protocol is not very convenient to chain numerous requests since the connection is closed after each request. If you already built complex apps festooned with inputs and outputs, you may imagine the amount of exchanges necessary between R and JS, thereby making HTTP definitely not suitable. What we would need instead is a permanent connection, allowing bidirectional fluxes, that is, if R wants to inform JS about something or if JS wants to send information to R.

11.1 JSON: exhange data

Since R and JS a very different languages, we can’t just send R code to JS and conversely. We must find a common language to exchange data. Guess what? We’ll be using JSON! JSON stands for JavaScript Object Notation. JSON has the great advantage to be suitable for many languages, particularly R. It has the same structure as a JS object but represented as a character string, for instance:

my_json <- '
{
  "name": "David",
  "color": "purple",
  "planet": "Mars",
  "animals": [
     {
       "name": "Euclide",
       "type": "cat",
       "age": 7
     },
     {
       "name": "Sophocle",
       "type": "dog",
       "age": 2
     }
  ]
}
'

Now, let’s see how we may interact with JSON.

11.1.1 Process JSON from R

There are two situations:

  • Read data from a JSON and convert it to the appropriate R structure, like list().
  • Export data from a R structure and convert it into JSON, for later use in another language, for instance JS.

The most commonly utilized R package is jsonlite (Ooms 2020) which allows to read JSON with fronJSON and export to JSON with toJSON. Let’s try to read the above defined JSON:

library(jsonlite)
res <- fromJSON(my_json)
str(res)
## List of 4
##  $ name   : chr "David"
##  $ color  : chr "purple"
##  $ planet : chr "Mars"
##  $ animals:'data.frame': 2 obs. of  3 variables:
##   ..$ name: chr [1:2] "Euclide" "Sophocle"
##   ..$ type: chr [1:2] "cat" "dog"
##   ..$ age : int [1:2] 7 2

By default, this gives us a list. Interestingly, the nested array is converted into a dataframe. If you don’t like this behavior, you may pass the simplifyVector = FALSE options, giving nested lists:

fromJSON(my_json, simplifyVector = FALSE)
## $name
## [1] "David"
## 
## $color
## [1] "purple"
## 
## $planet
## [1] "Mars"
## 
## $animals
## $animals[[1]]
## $animals[[1]]$name
## [1] "Euclide"
## 
## $animals[[1]]$type
## [1] "cat"
## 
## $animals[[1]]$age
## [1] 7
## 
## 
## $animals[[2]]
## $animals[[2]]$name
## [1] "Sophocle"
## 
## $animals[[2]]$type
## [1] "dog"
## 
## $animals[[2]]$age
## [1] 2

Inversely, assume we have a R list() that we want to transmit to JS. We apply toJSON:

my_list <- list(
  name = "David",
  color = "purple",
  planet = "Mars"
)

toJSON(my_list, auto_unbox = TRUE, pretty = TRUE)
## {
##   "name": "David",
##   "color": "purple",
##   "planet": "Mars"
## }

Note the auto_unbox and pretty options that allow for a better rendering.

There are many more available options and we invit the reader to refer to the jsonlite documentation.

11.1.2 Process JSON from JS

Like R, JS has 2 methods to process JSON, that are provided the JSON class. We parse a JSON, that is converting it from character to JS object with:

JSON.parse(my_json)

Conversely, we convert a JS object to JSON leveraging JSON.stringify:

myObject = {
  "name": "David",
  "color": "purple",
  "planet": "Mars",
}
JSON.stringify(my_object)

Now that we have seen a convenient way to exchange data between two different languages, R and JS, we are going to explain how this communication is made possible. This involves web elements called websockets.

11.2 What is a websocket?

Before going further let’s define what is a websocket. It is an advanced technology allowing bidirectional communication between a (or multiple) client(s) and a server. For instance, a chat system may be built on top of a websocket.4 The server is generally created using Node.js libraries like ws and the client with JavaScript. In the R Shiny context, the server part is created from httpuv (Cheng and Chang 2021) and the client either with {websocket} (Chang et al. 2020) (see below) or directly from JavaScript, as described later:

library(httpuv)
# set the server
s <- startServer("127.0.0.1", 8080,
  list(
    onWSOpen = function(ws) {
      # The ws object is a WebSocket object
      cat("Server connection opened.\n")
      
      ws$onMessage(function(binary, message) {
        cat("Server received message:", message, "\n")
        ws$send("Hello client!")
      })
      ws$onClose(function() {
        cat("Server connection closed.\n")
      })
    }
  )
)

On the server side, startServer() also handles websockets. To proceed, the app list must contain an extra element, that is the onWSOpen function, defining all actions to perform after the connection is established. Those actions are listed in the httpuv WebSocket R6 class:

  • onMessage is invoked whenever a message is received on this connection.
  • onClose is invoked when the connection is closed.
  • send sends a message from the server (to the client).

On the client, we may use the {websocket} WebSocket class provided by the websocket package:

library(websocket)
# set the client
ws <- websocket::WebSocket$new("ws://127.0.0.1:8080/")
ws$onMessage(function(event) {
  cat("Client received message:", event$data, "\n")
})

# Wait for a moment before running next line
ws$send("Hello server!")

# Close client
ws$close()

We briefly describe the above code:

  • We create a new client socket instance, which triggers the server onWSOpen function, displaying the welcome message.
  • We set the client ws$onMessage event manager that will print the message sent by the server.
  • Then a message is sent from the client with ws$send, received on the server and sent back to the client, and so on. Figure 11.1 shows the main mechanisms.
  • The client connection is closed, which also closes the server connection.
Typical websocket flow between client and server.

FIGURE 11.1: Typical websocket flow between client and server.

Interestingly, multiple clients can connect to the same server. You may give it a try with the {OSUICode} side package:

library(OSUICode)
server <- websocket_server()
client_1 <- websocket_client()
client_2 <- websocket_client()
client_1$send("Hello from client 1")
client_2$send("Hello from client 2")
client_1$close()
client_2$send("Only client 2 is here")
client_2$close()
Sys.sleep(1)
server$stop()

whose output is shown below.

Server connection opened.
Server connection opened.
Server received message: Hello from client 1 
Client received message: Hello client! 
Server received message: Hello from client 2 
Client received message: Hello client! 
Server connection closed.
Server received message: Only client 2 is here 
Client received message: Hello client! 
Server connection closed.

Under the hood, whenever a client initiates a websocket connection, it actually sends an HTTP request to the server. This is called the handshake, utilizing the CONNECT HTTP method to establish a bridge between the HTTP server and the websocket server. If the server accepts, the returned HTTP code is 101, meaning that we switch protocole from HTTP to WS or WSS, as depicted by Figure 11.2.

HTTP upgrade to WS in a shiny app example.

FIGURE 11.2: HTTP upgrade to WS in a shiny app example.

11.2.1 Example

In practice, Shiny does not use {websocket}. As mentioned earlier, the client is directly built from JS. To better understand the whole process, we are going to design a simple web page containing an HTML range slider, pass its value from JS to R through the websocket, so that R can do a simple histogram. Moreover, R will also send a message to JS, thereby updating a gauge meter widget located in the HTML page.

To proceed, we need few elements:

  • The HTML page containing the slider, gauge and the JS logic to create the client websocket connection, process the slider value and update the gauge value.
  • An app composed of an httpuv powered HTTP server serving this HTML page as well as a websocket server to connect R and JS.

11.2.1.1 Create the app

As shown above, we use the startServer function, giving it a default port and host such that the url is 127.0.0.1:8080. The most important elements is the app that consists in an HTTP response and a server websocket. The websocket call back may be defined as below:

ws_handler <- function(ws) {
  # The ws object is a WebSocket object
  cat("New connection opened.\n")
  # Capture client messages
  ws$onMessage(function(binary, message) {
    # create plot
    input_message <- jsonlite::fromJSON(message)
    print(input_message)
    cat("Number of bins:", input_message$value, "\n")
    hist(rnorm(input_message$value))
    
    # update gauge widget
    output_message <- jsonlite::toJSON(
      list(
        val = sample(0:100, 1),
        message = "Thanks client! I updated the plot..."
      ),
      pretty = TRUE,
      auto_unbox = TRUE
    )
    ws$send(output_message)
    cat(output_message)
  })
  ws$onClose(function() {
    cat("Server connection closed.\n")
  })
}

The critical part is the onMessage call back which has to process the client message. As we’ll send a JSON (from the client), we leverage fromJSON() to properly treat the message. It is printed for debugging purposes and the value is injected inside a hist(rnorm()) function. The second task is to send a message to JS in order to update the gauge value. See it like an updateSlider() function for instance. We utilize toJSON() to send a random value to JS as well as a polite message.

The HTTP response is returned by the call function and is typically defined as follows:

http_response <- function(req) {
  list(
    status = 200L,
    headers = list(
      'Content-Type' = 'text/html'
    ),
    body = "Hello world!"
  )
}

It is a list composed of a status code, 200 being the OK HTTP status, some headers here indicating the content nature and the body which is what well be display when the client visits 127.0.0.1:8080. The app code is:

startServer(
  "127.0.0.1",
  8080,
  list(call = http_reponse, onWSOpen = ws_handler)
)

The next step is to replace the http_reponse$body by a real HTML page containing the client websocket handler, as well as the slider and gauge widgets.

11.2.1.2 Design the page content

The first task is to create the websocket client connection:

  • We initialize the socket connection with the WebSocket API. It is crucial that the host and port match the parameters provided during the websocket server initialization.
  • We create the event registry that is socket.onopen, socket.onmessage. Inside socket.onmessage, we have to process the message sent from R with JSON.parse that creates an object. Remember that we sent a list from R and are only interested in the val element.

Importantly, we must wait for all elements to be available in the DOM before starting any action. Therefore, we wrap the whole thing inside a document.addEventListener("DOMContentLoaded", ...).

document.addEventListener(
  "DOMContentLoaded", function(event) {
    // Capture gauge widget
    let gauge = document.getElementById("mygauge");
    // Initialize client socket connection
    let mySocket = new WebSocket("ws://<HOST>:<PORT>");
    mySocket.onopen = function (event) {
      // do things
    };
    // Handle server message
    mySocket.onmessage = function (event) {
      let data = JSON.parse(event.data);
      gauge.value = data.val;
    };
});

We eventually insert it inside the script tag of our basic HTML boilerplate, which also contains the gauge skeleton. min, max and value set the range, while low, high and optimum are responsible for the color (red, yellow and green, respectively):

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <script language="javascript">
      document.addEventListener(
        "DOMContentLoaded", function(event) {
          // Capture gauge widget
          let gauge = document.getElementById("mygauge");
          // Initialize client socket connection
          let mySocket = new WebSocket("ws://<HOST>:<PORT>");
          mySocket.onopen = function (event) {
            // do things
          };
          // Handle server message
          mySocket.onmessage = function (event) {
            let data = JSON.parse(event.data);
            gauge.value = data.val;
          };
      });
    </script>
    <title>Websocket Example</title>
  </head>
  <body>
    <label for="mygauge">Gauge:</label>
    <meter id="mygauge" min="0" max="100" low="33" high="66" 
    optimum="80" value="50"></meter>
  </body>
</html>

Once done, we have to take care of the range slider whose code is taken from the MDN resources:

<div>
  <input type="range" id="slider" name="volume" 
  min="0" max="100">
  <label for="slider" id ="sliderLabel">Value:</label>
</div>

It is a simple div containing an input tag as well as a label. The input tag has some attributes, notably the minimum and maximum value. The slider has to be inserted in the HTML boilerplate shown below:

<!DOCTYPE HTML>
<html lang="en">
  <head>
    <script language="javascript">
      document.addEventListener(
        "DOMContentLoaded", function(event) {
          // Capture gauge widget
          let gauge = document.getElementById("mygauge");
          // Initialize client socket connection
          let mySocket = new WebSocket("ws://<HOST>:<PORT>");
          mySocket.onopen = function (event) {
            // do things
          };
          // Handle server message
          mySocket.onmessage = function (event) {
            let data = JSON.parse(event.data);
            gauge.value = data.val;
          };
      });
    </script>
    <title>Websocket Example</title>
  </head>
  <body>
    <div>
      <input type="range" id="slider" name="volume" min="0" 
      max="100">
      <label for="slider" id ="sliderLabel"></label>
    </div>
    <br/>
    <label for="mygauge">Gauge:</label>
    <meter id="mygauge" min="0" max="100" low="33" high="66" 
    optimum="80" value="50"></meter>
  </body>
</html>

The slider behavior is entirely controlled with JS. We recover its value with document.getElementById and add it to the label inner HTML so as to know the current value. We also add an event listener to update the slider value each time the range is updated, either by drag or by keyboard action with oninput. It is best practice to convert the slider value to a number with parseInt, as the returned value defaults to a string. Finally, we send the value through the websocket, converting it to JSON so that we may process it from R with jsonlite (or any other relevant package):

let sliderWidget = document.getElementById("slider");
let label = document.getElementById("sliderLabel");
label.innerHTML = "Value:" + slider.value; // init
// on change
sliderWidget.oninput = function() {
  let val = parseInt(this.value);
  mySocket.send(
    JSON.stringify({
      value: val,
      message: "New value for you server!"
    })
  );
  label.innerHTML = "Value:" + val;
};

11.2.2 Test it!

For convenience, the whole code is provided by OSUICode::httpuv_app(). Run that function in the R console and browse to 127.0.0.1:8080 with Chrome. You should see the range slider as well as its current value. We suggest the reader to have R and Chrome side by side, to properly see all messages sent between R and JS. In Chrome, open the developer tools and navigate to the Network tab and select the websocket entry, as show Figure 11.3. From now, you may change the slider value. Notice the green arrow message appearing in the developer tools. This indicates a message sent by the client: here a JSON containing the slider value as well as a tiny message, to be polite with the server. In the R console, you may inspect the received message (it should be the same as the client!). R is instructed to create a new plot and, once done, sends a message back to the client (red arrow) to indicate that the plot is updated and a new value has been generated for the gauge.

Server client communication through websocket

FIGURE 11.3: Server client communication through websocket

11.3 Clients concurrency

Not shown in the above sections, httpuv_app() exposes a delay parameter that simulates a computationally intense task on the server:

ws$onMessage(function(binary, message) {
  message <- jsonlite::fromJSON(message)
  print(message)
  cat("Number of bins:", message$value, "\n")
  hist(rnorm(message$value))
  if (!is.null(delay)) Sys.sleep(delay)
  ws$send("Thanks client! I updated the plot.")
})

This is to simulate concurrency that could occur between multiple client. To test it, you may try to call my_app <- httpuv_app(5), open two browser tabs pointing to 127.0.0.1:8080, update the slider on the first client and update it on the second client. What happens? Why? This highlights one fundamental limitation about Shiny: as R is single threaded, clients have to queue to get an answer from the server.

Once done, don’t forget to close the server connection with my_app$stop()!

In practice, Shiny’s core is much more complex but hopefully, you should get a better understanding of the general idea! The reader must understand that when Shiny inputs/outputs are modified on the client by an end user, there are a lot of exchanges between R and JS, through the websocket. In the following, we briefly describe how Shiny leverages this technology, on both server and client side.

11.4 Shiny and websockets

In the previous section, we showed how R and JS can communicate through a httpuv powered websocket. Now let’s see what happens in the context of Shiny.

11.4.1 The Shiny session object

We won’t be able to go anywhere without giving some reminders about the Shiny session object. Why do we say object? session is actually an instance of the ShinySession R6 class. Importantly, the session is unique to a given user. It means that two different clients cannot share the same session. This is important since it contains all information about input, output and client data.

Upon calling ShinySession$new(), the initialization method takes one parameter, namely the websocket. As shown in the last section, the websocket allows bidirectional exchanges between R and JS. The session object exposes two methods to communicate with from R with JavaScript:

  • sendCustomMessage sends messages from R to JS. It calls the private sendMessage method which itself calls write. The message is sent only when the session is opened, through the websocket private$websocket$send(json). If the shiny.trace option is TRUE, a message showing the sent JSON is displayed, which is useful for debugging.
  • sendInputMessage is used to update inputs from the server. The message is stored in a message queue and ultimately sent through the websocket private$websocket$send(json).

The below code is extracted from the shiny.R file.

sendCustomMessage = function(type, message) {
  data <- list()
  data[[type]] <- message
  private$sendMessage(custom = data)
}

sendInputMessage = function(inputId, message) {
  data <- list(id = inputId, message = message)
  
  # Add to input message queue
  len <- length(private$inputMessageQueue)
  private$inputMessageQueue[[len + 1]] <- data
  # Needed so that Shiny knows to actually 
  # flush the input message queue
  self$requestFlush()
}


sendMessage = function(...) {
  # This function is a wrapper for $write
  msg <- list(...)
  if (anyUnnamed(msg)) {
    stop("All arguments to sendMessage must be named.")
  }
  private$write(toJSON(msg))
}


write = function(json) {
  if (self$closed){
    return()
  }
  traceOption <- getOption('shiny.trace', FALSE)
  if (isTRUE(traceOption) || traceOption == "send")
    message(
      'SEND ',
      gsub(
        '(?m)base64,[a-zA-Z0-9+/=]+', 
        '[base64 data]',
        json,
        perl=TRUE
      )
    )
  private$websocket$send(json)
}
# ...

No worry if it is not clear at the moment. We will discuss sendInputMessage and sendCustomMessage in the following chapters, respectively 12 and 15.

11.4.2 Server side

On the server, that is R, a websocket is initiated in the startApp function, leveraging the httpuv package. Websocket handlers are defined by shiny:::createAppHandlers:

ws = function(ws) {
  # many things
  
  shinysession <- ShinySession$new(ws)
  
  ws$onMessage(function(binary, msg) {
    # If unhandled errors occur, make sure they get 
    # properly logged
    withLogErrors(messageHandler(binary, msg))
  })
  
  ws$onClose(function() {
    shinysession$wsClosed()
    appsByToken$remove(shinysession$token)
    appsNeedingFlush$remove(shinysession$token)
  })
  return(TRUE)
}

Overall, handlers drive the server websocket behavior. When the Shiny session is initialized, a message is sent through the WS, providing the sessionId, workerId and user to the client (see Shiny.shinyapp.config and section 10.6.3):

private$sendMessage(
  config = list(
    workerId = workerId(),
    sessionId = self$token,
    user = self$user
  )
)

The workerId is not always used. In practice, it is relevant only in the context of solutions able to load balance clients across multiple workers, that is shinyapps.io, RStudio Connect and Shiny server pro.

ws$onMessage describes what should happen when the server receives an message from the client. It applies the messageHandler function that, in short:

  • Decodes the received message.
  • Processes the message. At initialization, the client send a message with an init method tag, which tells Shiny to manage input (manageInputs(msg$data, now = TRUE)) before running any observer (since input don’t have value yet). After initialization, client messages have the update tag, meaning that we wait for observers to run before.

Finally, when the server connection is closed, all client connections are also closed.

All those handlers are applied by handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE):

# see middleware.R
httpuvApp <- handlerManager$createHttpuvApp()

onWSOpen = function(ws) {
  return(wsHandlers$invoke(ws))
}

addWSHandler = function(wsHandler, key, tail = FALSE) {
  wsHandlers$add(wsHandler, key, tail)
}

11.4.3 Client side

On the JS side, the socket creation occurs in the shinyapps.js file:

var ws = new WebSocket(protocol + '//' + window.location.host + defaultPath);

through the WebSocket object. protocol is the chosen protocol, either ws or wss (if using https). window.location.host contains the host name and its port. Once the connection is opened, events are handled with the onopen event registry:

socket.onopen = function() {
  hasOpened = true;

  $(document).trigger({
    type: 'shiny:connected',
    socket: socket
  });

  self.onConnected(); // remove overlay

  socket.send(JSON.stringify({
    method: 'init',
    data: self.$initialInput
  }));

  while (self.$pendingMessages.length) {
    var msg = self.$pendingMessages.shift();
    socket.send(msg);
  }
}

The shiny:connected event is triggered, any disconnected overlay (the famous grayed out screen) is then removed from the DOM. Initial input values are sent to the server via the send method. The onmessage registry aims at handling messages received from the server:

socket.onmessage = function(e) {
  self.dispatchMessage(e.data);
};

It subsequently invokes the dispatchMessage method that sends message to all handlers (through _sendMessagesToHandlers), triggering the shiny:message event. Shiny has internal and custom provided handlers (understand user-defined) stored in separate arrays. Each time, a message type matches a given handler, it is treated. For instance, there is a dedicated internal handler for input messages, that bridges the gap between a given input and the corresponding input binding. This handler eventually triggers the inputBinding.receiveMessage method so that the input value is updated on the client. We discuss this in detail in the following section 13.2.

Finally the onclose method is called when the websocket connection is closed.

socket.onclose = function() {
  // These things are needed only if we've successfully opened the
  // websocket.
  if (hasOpened) {
    $(document).trigger({
      type: 'shiny:disconnected',
      socket: socket
    });

    self.$notifyDisconnected();
  }

  self.onDisconnected(); // Must be run before self.$removeSocket()
  self.$removeSocket();
}

If the connection was opened, the shiny:disconnected event is triggered. Then, the disconnect overlay is added to the DOM (grayed out screen) and the socket is removed.

Should any error occurs in the R code, the server sends the error through the websocket, which is captured by the client and displayed.

11.4.4 Debug websocket with Shiny

Let’s run the following app (see 11.4, left panel):

library(shiny)
shinyApp(
  ui = fluidPage(
    selectInput(
      "variable", 
      "Variable:",
      c("Cylinders" = "cyl",
        "Transmission" = "am",
        "Gears" = "gear")
    ),
    tableOutput("data")
  ),
  server = function(input, output) {
    output$data <- renderTable({
      mtcars[, c("mpg", input$variable), drop = FALSE]
    }, rownames = TRUE)
  }
)

After opening the HTML inspector, we select the network tab and search for websocket in the list. By choosing the message tab, you may inspect what R and JavaScript say to each others. As stated above, the first message sent contains initial input values. Then Shiny recalculates the table, notifies when the recalculation is done and becomes idle. The second message received from R is after updating the select input, which triggers the same event cycle.

Although complex, it is extremely useful to check whether the input / output communication is working properly. If not, we would see the error field identifying the issue.

Shiny.shinyapp.$socket.readyState returns the state of the socket connection. It should be 1 if your app is running. In some instances when the socket is closed, an error would be raised.

Shiny websocket

FIGURE 11.4: Shiny websocket

We see below that we can even bypass the UI element and update the input value directly via the websocket using Shiny.shinyapp.$sendMsg with the update method. This is captured on the server side which triggers the output recalculation. We’ll discuss more about this in the next section 12.

updateObsVal <- function(value) {
  sprintf(
    "Shiny.shinyapp.$sendMsg(JSON.stringify({
      method: 'update',
      data: {obs: %s}
    }));",
    value
  )
}

# below we shunt the slider input by sending message
# directly through the websocket

ui <- fluidPage(
  tags$button(
    "Update obs value",
    onclick = updateObsVal(4)
  ),
  sliderInput(
    "obs", 
    "Number of observations:",
    min = 0, 
    max = 1000, 
    value = 500
  ),
  plotOutput("distPlot")
)

server <- function(input, output, session) {
  output$distPlot <- renderPlot({
    hist(rnorm(input$obs))
  })
}

shinyApp(ui, server)

11.4.5 Summary

Below is a summary of the server and client websocket parts. The Shiny app shown in Figure 11.5 consists in an actionButton() and a sliderInput(). Clicking on the action button triggers an observeEvent() that fires updateSlideInput(). Under the hood, clicking on the action button sends a message from the client to the server. This message is processed and the corresponding input value is updated on the server, thereby invalidating any observer, reactive element. updateSlideInput() sends a message back to the client containing the id of the input to update. This message is received and processed by the onMessage event manager, which redirects the message to the related message handler, thereby updating the corresponding input element on the client. The underlying mechanisms are going to be detailed in the next part 12. You may imagine that when the slider is updated, it also sends a message to the server, triggering a cascade of reactions.

It let you imagine how many messages are exchanged for more complex apps!

Websocket allows communication between server and client.

FIGURE 11.5: Websocket allows communication between server and client.