#' Web Application Framework for R #' #' Shiny makes it incredibly easy to build interactive web applications with R. #' Automatic "reactive" binding between inputs and outputs and extensive #' pre-built widgets make it possible to build beautiful, responsive, and #' powerful applications with minimal effort. #' #' The Shiny tutorial at \url{http://rstudio.github.com/shiny/tutorial} explains #' the framework in depth, walks you through building a simple application, and #' includes extensive annotated examples. #' #' @name shiny-package #' @aliases shiny #' @docType package #' @import httpuv caTools RJSONIO xtable digest methods NULL createUniqueId <- function(bytes) { # TODO: Use a method that isn't affected by the R seed paste(as.character(as.raw(floor(runif(bytes, min=1, max=255)))), collapse='') } #' @include utils.R ShinySession <- setRefClass( 'ShinySession', fields = list( .websocket = 'ANY', .invalidatedOutputValues = 'Map', .invalidatedOutputErrors = 'Map', .inputMessageQueue = 'list', # A list of inputMessages to send when flushed .outputs = 'list', # Keeps track of all the output observer objects .outputOptions = 'list', # Options for each of the output observer objects .progressKeys = 'character', .fileUploadContext = 'FileUploadContext', .input = 'ReactiveValues', # Internal object for normal input sent from client .clientData = 'ReactiveValues', # Internal object for other data sent from the client .closedCallbacks = 'Callbacks', .flushCallbacks = 'Callbacks', .flushedCallbacks = 'Callbacks', input = 'reactivevalues', # Externally-usable S3 wrapper object for .input output = 'ANY', # Externally-usable S3 wrapper object for .outputs clientData = 'reactivevalues', # Externally-usable S3 wrapper object for .clientData token = 'character', # Used to identify this instance in URLs files = 'Map', # For keeping track of files sent to client downloads = 'Map', closed = 'logical', session = 'environment', # Object for the server app to access session stuff .workerId = 'character', singletons = 'character' # Tracks singleton HTML fragments sent to the page ), methods = list( initialize = function(websocket, workerId) { .websocket <<- websocket .workerId <<- workerId .invalidatedOutputValues <<- Map$new() .invalidatedOutputErrors <<- Map$new() .inputMessageQueue <<- list() .progressKeys <<- character(0) closed <<- FALSE # TODO: Put file upload context in user/app-specific dir if possible .fileUploadContext <<- FileUploadContext$new() .input <<- ReactiveValues$new() .clientData <<- ReactiveValues$new() input <<- .createReactiveValues(.input, readonly=TRUE) .setLabel(input, 'input') clientData <<- .createReactiveValues(.clientData, readonly=TRUE) .setLabel(clientData, 'clientData') output <<- .createOutputWriter(.self) token <<- createUniqueId(16) .outputs <<- list() .outputOptions <<- list() session <<- new.env(parent=emptyenv()) session$clientData <<- clientData session$sendCustomMessage <<- .self$.sendCustomMessage session$sendInputMessage <<- .self$.sendInputMessage session$onSessionEnded <<- .self$onSessionEnded session$onFlush <<- .self$onFlush session$onFlushed <<- .self$onFlushed session$isClosed <<- .self$isClosed session$input <<- .self$input session$output <<- .self$output session$.impl <<- .self if (!is.null(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS)) { try({ creds <- fromJSON(websocket$request$HTTP_SHINY_SERVER_CREDENTIALS) session$user <<- creds$user session$groups <<- creds$groups }, silent=FALSE) } # session$request should throw an error if httpuv doesn't have # websocket$request, but don't throw it until a caller actually # tries to access session$request delayedAssign('request', websocket$request, assign.env = session) .write(toJSON(list(config = list( workerId = .workerId, sessionId = token )))) }, onSessionEnded = function(callback) { "Registers the given callback to be invoked when the session is closed (i.e. the connection to the client has been severed). The return value is a function which unregisters the callback. If multiple callbacks are registered, the order in which they are invoked is not guaranteed." return(.closedCallbacks$register(callback)) }, close = function() { closed <<- TRUE for (output in .outputs) { output$suspend() } .closedCallbacks$invoke(onError=function(e) { warning(simpleWarning( paste("An error occurred in an onSessionEnded handler:", e$message), e$call )) }) flushReact() lapply(appsByToken$values(), function(shinysession) { shinysession$flushOutput() NULL }) }, isClosed = function() { return(closed) }, defineOutput = function(name, func, label) { "Binds an output generating function to this name. The function can either take no parameters, or have named parameters for \\code{name} and \\code{shinysession} (in the future this list may expand, so it is a good idea to also include \\code{...} in your function signature)." # jcheng 08/31/2012: User submitted an example of a dynamically calculated # name not working unless name was eagerly evaluated. Yikes! force(name) # If overwriting an output object, suspend the previous copy of it if (!is.null(.outputs[[name]])) { .outputs[[name]]$suspend() } if (is.function(func)) { if (length(formals(func)) != 0) { orig <- func func <- function() { orig(name=name, shinysession=.self) } } # Preserve source reference and file information when formatting the # label for display in the reactive graph srcref <- attr(label, "srcref") srcfile <- attr(label, "srcfile") label <- sprintf('output$%s <- %s', name, paste(label, collapse='\n')) attr(label, "srcref") <- srcref attr(label, "srcfile") <- srcfile obs <- observe({ value <- try(shinyCallingHandlers(func()), silent=FALSE) .invalidatedOutputErrors$remove(name) .invalidatedOutputValues$remove(name) if (inherits(value, 'try-error')) { cond <- attr(value, 'condition') .invalidatedOutputErrors$set( name, list(message=cond$message, call=capture.output(print(cond$call)))) } else .invalidatedOutputValues$set(name, value) }, suspended=.shouldSuspend(name), label=label) obs$onInvalidate(function() { showProgress(name) }) .outputs[[name]] <<- obs if (is.null(.outputOptions[[name]])) .outputOptions[[name]] <<- list() } else { stop(paste("Unexpected", class(func), "output for", name)) } }, flushOutput = function() { .flushCallbacks$invoke() on.exit(.flushedCallbacks$invoke()) if (length(.progressKeys) == 0 && length(.invalidatedOutputValues) == 0 && length(.invalidatedOutputErrors) == 0 && length(.inputMessageQueue) == 0) { return(invisible()) } .progressKeys <<- character(0) values <- .invalidatedOutputValues .invalidatedOutputValues <<- Map$new() errors <- .invalidatedOutputErrors .invalidatedOutputErrors <<- Map$new() inputMessages <- .inputMessageQueue .inputMessageQueue <<- list() json <- toJSON(list(errors=as.list(errors), values=as.list(values), inputMessages=inputMessages)) .write(json) }, showProgress = function(id) { 'Send a message to the client that recalculation of the output identified by \\code{id} is in progress. There is currently no mechanism for explicitly turning off progress for an output component; instead, all progress is implicitly turned off when flushOutput is next called.' # If app is already closed, be sure not to show progress, otherwise we # will get an error because of the closed websocket if (closed) return() if (id %in% .progressKeys) return() .progressKeys <<- c(.progressKeys, id) json <- toJSON(list(progress=list(id))) .write(json) }, dispatch = function(msg) { method <- paste('@', msg$method, sep='') # we must use $ instead of [[ here at the moment; see # https://github.com/rstudio/shiny/issues/274 func <- try(do.call(`$`, list(.self, method)), silent=TRUE) if (inherits(func, 'try-error')) { .sendErrorResponse(msg, paste('Unknown method', msg$method)) } value <- try(do.call(func, as.list(append(msg$args, msg$blobs))), silent=TRUE) if (inherits(value, 'try-error')) { .sendErrorResponse(msg, conditionMessage(attr(value, 'condition'))) } else { .sendResponse(msg, value) } }, .sendResponse = function(requestMsg, value) { if (is.null(requestMsg$tag)) { warning("Tried to send response for untagged message; method: ", requestMsg$method) return() } .write(toJSON(list(response=list(tag=requestMsg$tag, value=value)))) }, .sendErrorResponse = function(requestMsg, error) { if (is.null(requestMsg$tag)) return() .write(toJSON(list(response=list(tag=requestMsg$tag, error=error)))) }, .sendCustomMessage = function(type, message) { data <- list() data[[type]] <- message .write(toJSON(list(custom=data))) }, .sendInputMessage = function(inputId, message) { data <- list(id = inputId, message = message) # Add to input message queue .inputMessageQueue[[length(.inputMessageQueue) + 1]] <<- data }, onFlush = function(func, once = TRUE) { if (!isTRUE(once)) { return(.flushCallbacks$register(func)) } else { dereg <- .flushCallbacks$register(function() { dereg() func() }) return(dereg) } }, onFlushed = function(func, once = TRUE) { if (!isTRUE(once)) { return(.flushedCallbacks$register(func)) } else { dereg <- .flushedCallbacks$register(function() { dereg() func() }) return(dereg) } }, .write = function(json) { if (closed){ return() } if (getOption('shiny.trace', FALSE)) message('SEND ', gsub('(?m)base64,[a-zA-Z0-9+/=]+','[base64 data]',json,perl=TRUE)) if (getOption('shiny.transcode.json', TRUE)) json <- iconv(json, to='UTF-8') .websocket$send(json) }, # Public RPC methods `@uploadInit` = function(fileInfos) { maxSize <- getOption('shiny.maxRequestSize', 5 * 1024 * 1024) fileInfos <- lapply(fileInfos, function(fi) { if (is.null(fi$type)) fi$type <- getContentType(tools::file_ext(fi$name)) fi }) sizes <- sapply(fileInfos, function(fi){ fi$size }) if (maxSize > 0 && any(sizes > maxSize)) { stop("Maximum upload size exceeded") } jobId <- .fileUploadContext$createUploadOperation(fileInfos) return(list(jobId=jobId, uploadUrl=paste('session', token, 'upload', paste(jobId, "?w=", .workerId,sep=""), sep='/'))) }, `@uploadEnd` = function(jobId, inputId) { fileData <- .fileUploadContext$getUploadOperation(jobId)$finish() .input$set(inputId, fileData) invisible() }, # Provides a mechanism for handling direct HTTP requests that are posted # to the session (rather than going through the websocket) handleRequest = function(req) { # TODO: Turn off caching for the response subpath <- req$PATH_INFO matches <- regmatches(subpath, regexec("^/([a-z]+)/([^?]*)", subpath, ignore.case=TRUE))[[1]] if (length(matches) == 0) return(httpResponse(400, 'text/html', '