-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathshiny-intro.Rmd
724 lines (571 loc) · 31 KB
/
shiny-intro.Rmd
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
# Communicate between R and JS {#shiny-intro}
This 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. Understanding this is crucial if you aim at developing your very own Shiny input widgets.
This is a feature leveraging the [httpuv](https://github.com/rstudio/httpuv) package. The __HTTP__ protocol is not very convenient chaining numerous __requests__ since the connection is closed after each request, as previously stated in section \@ref(web-applications-http). 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.
## Introductory example
The Shiny app shown in Figure \@ref(fig:websocket-intro) consists of an `actionButton()` and a `sliderInput()`. Clicking on the action button triggers an `observeEvent()`, which subsequently fires `updateSlideInput()`. Under the hood, clicking on the action button sends a message from the client (JS) to the server (R). This message is processed and the corresponding input value is updated on the server R, 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 JS, thereby updating the corresponding input element on the client. You may imagine that when the slider is updated, it also sends a message back to the server, triggering a __cascade__ of reactions.
```{r websocket-intro, echo=FALSE, fig.cap='Websocket allows communication between server and client.', out.width='100%'}
knitr::include_graphics("images/survival-kit/websocket-intro.png")
```
Reading this will probably raise a burning question: how can two different languages like R and JS communicate?
Let's meet below to understand what are the mechanisms involved.
## JSON: exhange data
Since R and JS are 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 that it is suitable for many languages, particularly R. It has the same structure as a JS object but can be serialized as a character string, for instance:
```{r}
my_json <- '
{
"name": "David",
"color": "purple",
"planet": "Mars",
"animals": [
{
"name": "Euclide",
"type": "cat",
"age": 7
}
]
}
'
```
In the next section, we'll see how we may interact with JSON.
### 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}` [@R-jsonlite], which allows reading JSON with `fromJSON` and exporting to JSON with
`toJSON`. Let's try to read the above defined JSON:
```{r}
library(jsonlite)
res <- fromJSON(my_json)
str(res)
```
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:
```{r}
fromJSON(my_json, simplifyVector = FALSE)
```
Inversely, assume we have a R `list()` that we want to transmit to JS. We apply `toJSON`:
```{r}
my_list <- list(
name = "David",
color = "purple",
planet = "Mars"
)
toJSON(my_list)
toJSON(my_list, auto_unbox = TRUE, pretty = TRUE)
```
Note the `auto_unbox` (unbox atomic vectors of length 1) and `pretty` (adds indentation) options that allow for a better rendering. There are many more available options and we invite the reader to refer to the `{jsonlite}`
documentation.
Most of the time, you will pass more complex data structures like nested lists. For instance imagine you have to send user profile information containing a unique id, name and organization, the latter being a nested list with fields like id, name, site, ...:
```{r}
my_list <- list(
id = "01522",
name = "David",
organization = list(
id = "AWER12",
name = "RinteRface",
site = "Switzerland"
)
)
toJSON(my_list, auto_unbox = TRUE, pretty = TRUE)
```
### Process JSON from JS
Like R, JS has two methods to process JSON, which are provided by the `JSON` class.
We parse a JSON, that is converting it from character to JS object with:
```{r, echo=FALSE, results='asis'}
js_code <- "JSON.parse(my_json)"
code_chunk_custom(js_code, "js")
```
Conversely, we convert a JS object to JSON leveraging `JSON.stringify`:
```{r, echo=FALSE, results='asis'}
js_code <- 'myObject = {
"name": "David",
"color": "purple",
"planet": "Mars",
}
JSON.stringify(my_object)'
code_chunk_custom(js_code, "js")
```
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__.
## 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](https://dev.to/spukas/learn-websockets-by-building-simple-chat-app-dee) system may be built on top of a websocket [^chat-system]. The server is generally created using Node.js libraries like [ws](https://github.com/websockets/ws) and the client with JavaScript. In the R Shiny context, the server part is created from `{httpuv}` [@R-httpuv] and the client either with `{websocket}` [@R-websocket] (see below) or directly from JavaScript, as described later:
```{r, eval=FALSE}
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")
})
}
)
)
```
[^chat-system]: By default, each time a client connects to the server, a new connection is opened, thereby preventing this client from capturing others connections messages, also called single cast. For a chat, we use a [multi-cast](https://medium.com/the-quarter-espresso/multicast-websocket-nodejs-ff1f400ba2f7) strategy, that is forwarding one client's message to (all) other connected clients. `{httpuv}` does not provide such a feature since this would not make sense and would be harmful in the context of Shiny.
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](https://github.com/rstudio/websocket) package:
```{r, eval=FALSE}
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 \@ref(fig:websocket-basics) shows the main mechanisms.
- The client connection is closed, which also closes the server connection.
```{r websocket-basics, echo=FALSE, fig.cap='Typical websocket flow between client and server.', out.width='100%'}
knitr::include_graphics("images/survival-kit/websocket-basics.png")
```
Interestingly, multiple clients can connect to the same server.
You may give it a try with the `{OSUICode}` side package:
```{r, eval=FALSE}
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.
```{r, echo=FALSE, results='asis'}
tmp_code <- '#> 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.'
code_chunk_custom(tmp_code)
```
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 \@ref(fig:http-upgrade-to-ws).
```{r http-upgrade-to-ws, echo=FALSE, fig.cap='HTTP upgrade to WS in a Shiny app example.', out.width='75%', fig.align='center'}
knitr::include_graphics("images/survival-kit/http-upgrade-to-ws.png")
```
### Example {#from-R-to-JS}
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 and pass its value from JS to R through the websocket, so that R can produce 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 a 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.
#### Create the app
To start the server, we leverage the `startServer()` function that expects:
- A __host__, usually `127.0.0.1` if you work locally.
- A __port__, like `8080` (app may be accessed on `<HOST>:<PORT>` in your web browser).
- A list of functions describing the __app__, particularly:
- `call` expects the HTTP response.
- `onWSOpen` expects the websocket server.
In the following, we gradually explain how to design each part.
##### Handle the websocket server
The most important element is the app, which consists of a server websocket (R) and an HTTP response (HTML, JS). The websocket call back may be defined as follows. We first raise a message upon client connection:
```{r, eval=FALSE}
ws_handler <- function(ws) {
# The ws object is a WebSocket object
cat("New connection opened.\n")
}
```
The critical part is the `onMessage` callback, which has to process the client message.
```{r, eval=FALSE}
ws_handler <- function(ws) {
# The ws object is a WebSocket object
cat("New connection opened.\n")
ws$onMessage(function(binary, message) {
# server logic
}
}
```
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. Copy the below code inside the `ws$onMessage` handler.
```{r, eval=FALSE}
# capture client message
input_message <- jsonlite::fromJSON(message)
# debug
print(input_message)
cat("Number of bins:", input_message$value, "\n")
# create plot
hist(rnorm(input_message$value))
```
Still within `ws$onMessage`, we 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:
```{r, eval=FALSE}
# Send random value to JS
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)
# debug
cat(output_message)
```
We finally add the `onClose` callback to handle client disconnection:
```{r, eval=FALSE}
ws$onClose(function() {
cat("Server connection closed.\n")
})
```
The whole code may be found in the `{OSUICode}` package (see https://github.com/DivadNojnarg/outstanding-shiny-ui-code/blob/b95f656bce9de7600c05b5045a4e005f70c4f83d/R/websocket.R#L145).
##### Handle the HTTP response
The HTTP response is returned by the `call` function and is typically defined as follows:
```{r, eval=FALSE}
http_response <- function(req) {
list(
status = 200L,
headers = list(
'Content-Type' = 'text/html'
),
body = "Hello world!"
)
}
```
It returns a list composed of:
- A __status__ code, 200 being the OK HTTP [status](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status).
- Some __headers__ indicating the content nature.
- The __body__, which is what will be displayed upon client request.
To start the server, we leverage the `startServer()` function, giving it a default port `8080` and host such that the url is `127.0.0.1:8080`:
```{r, eval=FALSE}
startServer(
"127.0.0.1",
8080,
list(call = http_response, 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.
#### Design the page content
The first task consists of setting up the websocket client connection:
- We initialize the socket connection with the `WebSocket` [API](https://developer.mozilla.org/en-US/docs/Web/API/WebSocket). 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`, which creates an object. Remember that we sent a list from R and are only interested in the `val` element.
::: {.importantblock data-latex=""}
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", ...)`.
:::
```{r, echo=FALSE, results='asis'}
js_code <- "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;
};
});"
code_chunk_custom(js_code, "js")
```
We eventually insert it inside the `script` tag of our basic HTML boilerplate, which also contains the gauge
skeleton, borrowed from the MDN [resources](https://developer.mozilla.org/fr/docs/Web/HTML/Element/Meter). `min`, `max` and `value` set the range, while `low`, `high` and `optimum` are responsible for the color (red, yellow and green, respectively):
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE HTML>
<html lang="en">
<head>
<script language="javascript">
// ADD EVENT LISTENER HERE
</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>'
code_chunk_custom(html_code, "html")
```
Once done, we have to take care of the range slider, whose code is taken from the MDN [resources](https://developer.mozilla.org/en-US/docs/Web/HTML/Element/Input/range):
```{r, echo=FALSE, results='asis'}
html_code <- '<div>
<input type="range" id="slider" name="volume"
min="0" max="100">
<label for="slider" id ="sliderLabel">Value:</label>
</div>'
code_chunk_custom(html_code, "html")
```
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:
```{r, echo=FALSE, results='asis'}
html_code <- '<!DOCTYPE HTML>
<html lang="en">
<head>
<script language="javascript">
// ADD EVENT LISTENER HERE
</script>
<title>Websocket Example</title>
</head>
<body>
<!-- INSERT SLIDER HERE -->
<br/>
<label for="mygauge">Gauge:</label>
<meter id="mygauge" min="0" max="100" low="33" high="66"
optimum="80" value="50"></meter>
</body>
</html>'
code_chunk_custom(html_code, "html")
```
The slider behavior is entirely controlled by 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`](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/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):
```{r, echo=FALSE, results='asis'}
js_code <- "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, 10);
mySocket.send(
JSON.stringify({
value: val,
message: 'New value for you server!'
})
);
label.innerHTML = 'Value:' + val;
};"
code_chunk_custom(js_code, "js")
```
### 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 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 \@ref(fig:httpuv-websocket-demo). 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.
```{r httpuv-websocket-demo, echo=FALSE, fig.cap='Server-client communication through a websocket.', out.width='100%'}
knitr::include_graphics("images/survival-kit/httpuv-websocket-demo.png")
```
## Client concurrency
Not shown in the above sections, `httpuv_app()` exposes a delay parameter that simulates a computationally intense task on the server:
```{r, eval=FALSE}
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 clients. 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 in Shiny: as R is single-threaded, clients have to queue to get an answer from the server.
::: {.importantblock data-latex=""}
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 many exchanges between R and JS through the websocket. In the following, we briefly describe how Shiny leverages this technology, on both server-side and client-side.
## 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.
### The Shiny session object {#shiny-session}
We won't be able to go anywhere without giving some reminders about the Shiny [session](https://shiny.rstudio.com/reference/shiny/1.6.0/session.html) object. Why do we say object? __session__ is actually an instance of the [`ShinySession`](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/R/shiny.R#L338) __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 JavaScript from R:
- __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](https://shiny.rstudio.com/reference/shiny/0.14/shiny-options.html) 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)`.
Curious readers will look at the `shiny.R` [file](https://github.com/rstudio/shiny/blob/da6df5da9e4ab40e2ed0afa846d5b2d172b647c1/R/shiny.R#L1264).
We will discuss `sendInputMessage` and `sendCustomMessage` in Chapters \@ref(shiny-input-system) and \@ref(shiny-custom-handler).
### Server side
On the server, that is R, a websocket is initiated in the [startApp](https://github.com/rstudio/shiny/blob/da6df5da9e4ab40e2ed0afa846d5b2d172b647c1/R/server.R#L534) function, leveraging the `{httpuv}` package. Websocket handlers are [defined](https://github.com/rstudio/shiny/blob/da6df5da9e4ab40e2ed0afa846d5b2d172b647c1/R/server.R#L303) by `shiny:::createAppHandlers`:
```{r, eval=FALSE}
ws = function(ws) {
# ....; Extra code removed
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 \@ref(shiny-js-object)):
```{r, eval=FALSE}
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](https://www.shinyapps.io/), [RStudio Connect](https://www.rstudio.com/products/connect/) and [Shiny Server Pro](https://docs.rstudio.com/other/ssp/).
`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 sends a message with an `init` key,
which tells Shiny to manage `inputs` before running any observer (since `inputs` do not have values yet). After initialization, client messages have the `update` key, 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](https://github.com/rstudio/shiny/blob/da6df5da9e4ab40e2ed0afa846d5b2d172b647c1/R/server.R#L537) by `handlerManager$addWSHandler(appHandlers$ws, "/", tail = TRUE)`:
```{r, eval=FALSE}
# see middleware.R
httpuvApp <- handlerManager$createHttpuvApp()
onWSOpen = function(ws) {
return(wsHandlers$invoke(ws))
}
addWSHandler = function(wsHandler, key, tail = FALSE) {
wsHandlers$add(wsHandler, key, tail)
}
```
-->
### Client side
On the JS side, the socket creation occurs in the `shinyapps.js` [file](https://github.com/rstudio/shiny/blob/60db1e02b03d8e6fb146c9bb1bbfbce269231add/srcjs/shinyapp.js#L58):
```{r, echo=FALSE, results='asis'}
js_code <- "var ws = new WebSocket(
protocol +
'//' +
window.location.host +
defaultPath
);"
code_chunk_custom(js_code, "js")
```
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](https://developer.mozilla.org/fr/docs/Web/API/window/location).
Once the connection is opened, events are handled with the `onopen` event registry:
```{r, echo=FALSE, results='asis'}
js_code <- "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);
}
}"
code_chunk_custom(js_code, "js")
```
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:
```{r, echo=FALSE, results='asis'}
js_code <- "socket.onmessage = function(e) {
self.dispatchMessage(e.data);
};"
code_chunk_custom(js_code, "js")
```
It subsequently invokes the `dispatchMessage` method that sends a message to all handlers, triggering the `shiny:message` event. Shiny has internal and custom-provided handlers (read 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, which 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 section \@ref(update-input-lifecycle).
Finally the `onclose` method is called when the websocket connection is closed.
```{r, echo=FALSE, results='asis'}
js_code <- "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(); // Run before self.$removeSocket()
self.$removeSocket();
}"
code_chunk_custom(js_code, "js")
```
If the connection was opened, the `shiny:disconnected` event is triggered. Then, the disconnect overlay is added to the DOM (grayed-out), 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.
### Debug websocket with Shiny
Let's run the following app (see Figure \@ref(fig:shiny-websocket), left panel):
```{r, eval=FALSE}
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 and 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.
```{r shiny-websocket, echo=FALSE, fig.cap='Inspect content exchanged in the websocket within a Shiny app.', out.width='100%'}
knitr::include_graphics("images/survival-kit/shiny-websocket.png")
```
<!-- REMOVE AS PER BARRET SUGGESTION.
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 \@ref(shiny-input-system).
```{r, eval=FALSE}
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)
```
-->
It lets you imagine how many messages are exchanged for more complex apps.