From 141c57ad1e9230722ea012d71df82ce1c8580c88 Mon Sep 17 00:00:00 2001
From: Joe Cheng
Date: Tue, 26 Jun 2012 17:53:28 -0700
Subject: [PATCH] Implement Shiny server in R
- Use websockets package to implement Shiny server in R
- NB: Current behavior is undefined if more than one client connects at the same time
- Added HTML and plot (actually image) binding types on the client
---
R/react.R | 29 +++++++-
R/shiny.R | 178 +++++++++++++++++++++++++++++++++++++++++++++++++
www/index.html | 6 +-
www/shiny.js | 29 +++++++-
4 files changed, 237 insertions(+), 5 deletions(-)
create mode 100644 R/shiny.R
diff --git a/R/react.R b/R/react.R
index 913331166..3504854fd 100644
--- a/R/react.R
+++ b/R/react.R
@@ -26,13 +26,15 @@ Map <- setRefClass(
},
set = function(key, value) {
assign(key, value, pos=.env, inherits=F)
+ return(value)
},
remove = function(key) {
- if (contains.key(key)) {
+ if (.self$contains.key(key)) {
+ result <- .self$get(key)
rm(list = key, pos=.env, inherits=F)
- return(T)
+ return(result)
}
- return(F)
+ return(NULL)
},
contains.key = function(key) {
exists(key, where=.env, inherits=F)
@@ -45,10 +47,23 @@ Map <- setRefClass(
},
clear = function() {
.env <<- new.env(parent=emptyenv())
+ invisible(NULL)
+ },
+ size = function() {
+ length(.env)
}
)
)
+as.list.Map <- function(map) {
+ sapply(map$keys(),
+ map$get,
+ simplify=F)
+}
+length.Map <- function(map) {
+ map$size()
+}
+
Context <- setRefClass(
'Context',
fields = list(
@@ -174,6 +189,12 @@ Values <- setRefClass(
}
)
invisible()
+ },
+ mset = function(lst) {
+ lapply(names(lst),
+ function(name) {
+ .self$set(name, lst[[name]])
+ })
}
)
)
@@ -277,4 +298,6 @@ test <- function () {
flush.react()
values$set('b', 300)
flush.react()
+ values$mset(list(a = 10, b = 20))
+ flush.react()
}
diff --git a/R/shiny.R b/R/shiny.R
new file mode 100644
index 000000000..444d9eddc
--- /dev/null
+++ b/R/shiny.R
@@ -0,0 +1,178 @@
+library(websockets)
+library(RJSONIO)
+library(caTools)
+library(xtable)
+
+ShinyApp <- setRefClass(
+ 'ShinyApp',
+ fields = list(
+ .websocket = 'list',
+ .outputs = 'Map',
+ .invalidated.output.values = 'Map',
+ session = 'Values'
+ ),
+ methods = list(
+ initialize = function(ws) {
+ .websocket <<- ws
+ .outputs <<- Map$new()
+ .invalidated.output.values <<- Map$new()
+ session <<- Values$new()
+ },
+ define.output = function(name, func) {
+ .outputs$set(name, func)
+ },
+ define.plot.output = function(name, func, ...) {
+ .outputs$set(name, function() {
+ png.file <- tempfile(fileext='.png')
+ png(filename=png.file, ...)
+ func()
+ dev.off()
+
+ bytes <- file.info(png.file)$size
+ if (is.na(bytes))
+ return(NULL)
+
+ b64 <- base64encode(readBin(png.file, 'raw', n=bytes))
+ return(paste("data:image/png;base64,", b64, sep=''))
+ })
+ },
+ instantiate.outputs = function() {
+ lapply(.outputs$keys(),
+ function(key) {
+ func <- .outputs$remove(key)
+ Observer$new(function() {
+ value <- func()
+ .invalidated.output.values$set(key, value)
+ })
+ })
+ },
+ flush.output = function() {
+ if (length(.invalidated.output.values) == 0)
+ return(invisible())
+
+ data <- .invalidated.output.values
+ .invalidated.output.values <<- Map$new()
+ cat(c("SEND", toJSON(as.list(data)), "\n"))
+ websocket_write(toJSON(as.list(data)), .websocket)
+ }
+ )
+)
+
+statics <- function(root) {
+ root <- normalizePath(root, mustWork=T)
+
+ return(function(ws, header) {
+ # TODO: Stop using websockets' internal methods
+ path <- header$RESOURCE
+
+ if (is.null(path))
+ return(websockets:::.http_400(ws))
+
+ if (path == '/')
+ path <- '/index.html'
+
+ abs.path <- file.path(root, path)
+
+ if (!file.exists(abs.path)) {
+ # TODO: This should be 404, not 400
+ return(websockets:::.http_400(ws))
+ }
+
+ abs.path <- normalizePath(abs.path, mustWork=T)
+
+ if (nchar(abs.path) <= nchar(root) + 1) {
+ return(websockets:::.http_400(ws))
+ }
+
+ if (substr(abs.path, 1, nchar(root)) != root ||
+ !(substr(abs.path, nchar(root)+1, nchar(root)+1) %in% c('/', '\\'))) {
+ return(websockets:::.http_400(ws))
+ }
+
+ ext <- tools::file_ext(abs.path)
+ content.type <- switch(ext,
+ html='text/html; charset=UTF-8',
+ htm='text/html; charset=UTF-8',
+ js='text/javascript',
+ css='text/css',
+ png='image/png',
+ jpg='image/jpeg',
+ jpeg='image/jpeg',
+ gif='image/gif',
+ 'application/octet-stream')
+ response.content <- readBin(abs.path, 'raw', n=file.info(abs.path)$size)
+ return(websockets:::.http_200(ws, content.type, response.content))
+ })
+}
+
+start.app <- function(port = 8101L) {
+
+ ws_env <- create_server(port=port, webpage=statics('./www'))
+
+ set_callback('established', function(WS, ...) {
+ shinyapp <<- ShinyApp$new(WS)
+
+ input <- Observable$new(function() {
+ str <- shinyapp$session$get('input1')
+ if (shinyapp$session$get('addnewline'))
+ str <- paste(str, "\n", sep='')
+ return(str)
+ })
+ input.df <- Observable$new(function() {
+ varname <- shinyapp$session$get('input1')
+ if (nchar(varname) > 0 && exists(varname, where=.GlobalEnv)) {
+ df <- get(varname, pos=.GlobalEnv)
+ if (is.data.frame(df)) {
+ return(df)
+ }
+ }
+ return(NULL)
+ })
+ shinyapp$define.output('md5_hash', function() {
+ digest(input$get.value(), algo='md5', serialize=F)
+ })
+ shinyapp$define.output('sha1_hash', function() {
+ digest(input$get.value(), algo='sha1', serialize=F)
+ })
+ shinyapp$define.output('table1', function() {
+ if (!is.null(input.df$get.value()))
+ print(xtable(input.df$get.value()), type='html')
+ })
+ shinyapp$define.plot.output('plot1', function() {
+ if (!is.null(input.df$get.value()))
+ plot(input.df$get.value())
+ }, width=800, height=600)
+
+ }, ws_env)
+
+ set_callback('closed', function(WS, ...) {
+ }, ws_env)
+
+ set_callback('receive', function(DATA, WS, ...) {
+ cat(c("RECV", rawToChar(DATA), "\n"))
+
+ if (identical(charToRaw("\003\xe9"), DATA))
+ return()
+
+ msg <- fromJSON(rawToChar(DATA), asText=T, simplify=F)
+ switch(
+ msg$method,
+ init = {
+ shinyapp$session$mset(msg$data)
+ flush.react()
+ shinyapp$instantiate.outputs()
+ },
+ update = {
+ shinyapp$session$mset(msg$data)
+ })
+ flush.react()
+ shinyapp$flush.output()
+ }, ws_env)
+
+ return(ws_env)
+}
+
+run.app <- function(ws_env) {
+ while (T)
+ service(server=ws_env)
+}
diff --git a/www/index.html b/www/index.html
index 3b3dd240c..aa0100435 100644
--- a/www/index.html
+++ b/www/index.html
@@ -9,7 +9,7 @@
-
+
Append newline
@@ -22,5 +22,9 @@
+
+
+
+