Skip to content

Implement support for asynchronous loading in Dash for R #157

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Merged
merged 30 commits into from
Dec 22, 2019
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
0d7c2b1
add eager_loading parameter
Nov 1, 2019
ef59eeb
:pencil2: add fields for eager_loading
Nov 1, 2019
f36c77a
:name_badge: add buildFingerprint
Nov 7, 2019
a869c4f
:name_badge: add checkFingerprint
Nov 7, 2019
46157f7
temporarily disable percy
rpkyle Dec 3, 2019
bd3ae5e
:see_no_evil: use path instead of mypath
Dec 4, 2019
1bf6158
:truck: dep path resolution into fn for :camel:
Dec 5, 2019
fa4c4bd
:pencil2: Dash-friendly ext/filename helpers
Dec 5, 2019
f351b8b
use getDependencyPath, + :paw_prints:/Etag support
Dec 5, 2019
a4e6754
updates to support async
Dec 13, 2019
436632e
Merge branch 'dev' into 134-async
rpkyle Dec 15, 2019
bae3256
:see_no_evil: fix misplaced paren
Dec 15, 2019
c04b8b4
fix variable name and parens
Dec 15, 2019
fd65186
:see_no_evil: fix misplaced paren
Dec 15, 2019
9cc86cb
Merge branch '134-async' of github.com:plotly/dashR into 134-async
Dec 15, 2019
ac81b37
:hammer: refactor tag generation
Dec 17, 2019
0c77d32
:sparkles: properly support gz compression
Dec 17, 2019
cfced69
sanity checks for response size
Dec 17, 2019
72cb717
update remote refs
Dec 18, 2019
df5888a
:bug: post-async fixes for CSS handling
Dec 18, 2019
0370ec8
add tryCompress fn
Dec 19, 2019
2987a7a
:bug: dashLogger fails when self does not exist
Dec 19, 2019
63885ec
Update CHANGELOG.md
rpkyle Dec 19, 2019
938b304
Update CHANGELOG.md
rpkyle Dec 19, 2019
a0c6bb6
update changelog
Dec 22, 2019
bd6a2d7
fix commit hashes
Dec 22, 2019
eb9521f
update Dash help page
Dec 22, 2019
4bea651
update URL to docs
Dec 22, 2019
937715b
:bug: fix example
Dec 22, 2019
a385ede
fix example; load pkgs
Dec 22, 2019
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 3 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,8 +1,10 @@
# Change Log for Dash for R
All notable changes to this project will be documented in this file.

## Unreleased
## [0.2.0] - 2019-12-23
### Added
- Support for asynchronous/dynamic loading of dependencies, resource caching, and asset fingerprinting [#157](https://github.com/plotly/dashR/pull/157)
- Compression of text resources using `brotli`, `gzip`, or `deflate` [#157](https://github.com/plotly/dashR/pull/157)
- Support for adding `<meta>` tags to index [#142](https://github.com/plotly/dashR/pull/142)
- Hot reloading now supported in debug mode [#127](https://github.com/plotly/dashR/pull/127)
- Support for displaying Dash for R applications within RStudio's viewer pane when `use_viewer = TRUE`
Expand Down
16 changes: 8 additions & 8 deletions DESCRIPTION
Original file line number Diff line number Diff line change
@@ -1,19 +1,19 @@
Package: dash
Title: An Interface to the Dash Ecosystem for Authoring Reactive Web Applications
Version: 0.1.0
Version: 0.2.0
Authors@R: c(person("Chris", "Parmer", role = c("aut"), email = "[email protected]"), person("Ryan Patrick", "Kyle", role = c("aut", "cre"), comment = c(ORCID = "0000-0001-5829-9867"), email = "[email protected]"), person("Carson", "Sievert", role = c("aut"), comment = c(ORCID = "0000-0002-4958-2844")), person(family = "Plotly Technologies", role = "cph"))
Description: A framework for building analytical web applications, Dash offers a pleasant and productive development experience. No JavaScript required.
Depends:
R (>= 3.0.2)
Imports:
dashHtmlComponents (== 1.0.0),
dashCoreComponents (== 1.0.0),
dashTable (== 4.0.2),
dashHtmlComponents (== 1.0.2),
dashCoreComponents (== 1.6.0),
dashTable (== 4.5.1),
R6,
fiery (> 1.0.0),
routr (> 0.2.0),
plotly,
reqres,
reqres (>= 0.2.3),
jsonlite,
htmltools,
assertthat,
Expand All @@ -31,9 +31,9 @@ Collate:
'imports.R'
'print.R'
'internal.R'
Remotes: plotly/dash-html-components@17da1f4,
plotly/dash-core-components@cc1e654,
plotly/dash-table@042ad65
Remotes: plotly/dash-html-components@55c3884,
plotly/dash-core-components@c107e0f,
plotly/dash-table@3058bd5
License: MIT + file LICENSE
Encoding: UTF-8
LazyData: true
Expand Down
108 changes: 100 additions & 8 deletions R/dash.R
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,7 @@
#' server = fiery::Fire$new(),
#' assets_folder = 'assets',
#' assets_url_path = '/assets',
#' eager_loading = FALSE,
#' assets_ignore = '',
#' serve_locally = TRUE,
#' meta_tags = NULL,
Expand All @@ -30,6 +31,7 @@
#' .css files will be loaded immediately unless excluded by `assets_ignore`,
#' and other files such as images will be served if requested. Default is `assets`. \cr
#' `assets_url_path` \tab \tab Character. Specify the URL path for asset serving. Default is `assets`. \cr
#' `eager_loading` \tab \tab Logical. Controls whether asynchronous resources are prefetched (if `TRUE`) or loaded on-demand (if `FALSE`). \cr
#' `assets_ignore` \tab \tab Character. A regular expression, to match assets to omit from
#' immediate loading. Ignored files will still be served if specifically requested. You
#' cannot use this to prevent access to sensitive files. \cr
Expand Down Expand Up @@ -126,7 +128,10 @@
#'
#' @examples
#' \dontrun{
#' library(dashCoreComponents)
#' library(dashHtmlComponents)
#' library(dash)

#' app <- Dash$new()
#' app$layout(
#' dccInput(id = "inputID", value = "initial value", type = "text"),
Expand Down Expand Up @@ -160,13 +165,15 @@ Dash <- R6::R6Class(
server = fiery::Fire$new(),
assets_folder = 'assets',
assets_url_path = '/assets',
eager_loading = FALSE,
assets_ignore = '',
serve_locally = TRUE,
meta_tags = NULL,
routes_pathname_prefix = NULL,
requests_pathname_prefix = NULL,
external_scripts = NULL,
external_stylesheets = NULL,
compress = TRUE,
suppress_callback_exceptions = FALSE) {

# argument type checking
Expand All @@ -178,12 +185,14 @@ Dash <- R6::R6Class(
# save relevant args as private fields
private$name <- name
private$serve_locally <- serve_locally
private$eager_loading <- eager_loading
# remove leading and trailing slash(es) if present
private$assets_folder <- gsub("^/+|/+$", "", assets_folder)
# remove trailing slash in assets_url_path, if present
private$assets_url_path <- sub("/$", "", assets_url_path)
private$assets_ignore <- assets_ignore
private$suppress_callback_exceptions <- suppress_callback_exceptions
private$compress <- compress
private$app_root_path <- getAppPath()
private$app_launchtime <- as.integer(Sys.time())
private$meta_tags <- meta_tags
Expand Down Expand Up @@ -240,6 +249,9 @@ Dash <- R6::R6Class(
response$body <- to_JSON(lay, pretty = TRUE)
response$status <- 200L
response$type <- 'json'



TRUE
})

Expand All @@ -250,6 +262,7 @@ Dash <- R6::R6Class(
response$body <- to_JSON(list())
response$status <- 200L
response$type <- 'json'

return(FALSE)
}

Expand All @@ -265,6 +278,8 @@ Dash <- R6::R6Class(
response$body <- to_JSON(setNames(payload, NULL))
response$status <- 200L
response$type <- 'json'
if (private$compress)
response <- tryCompress(request, response)
TRUE
})

Expand Down Expand Up @@ -393,17 +408,33 @@ Dash <- R6::R6Class(
response$status <- 500L
private$stack_message <- NULL
}

if (private$compress)
response <- tryCompress(request, response)
TRUE
})

# This endpoint supports dynamic dependency loading
# during `_dash-update-component` -- for reference:
# https://github.com/plotly/dash/blob/1249ffbd051bfb5fdbe439612cbec7fa8fff5ab5/dash/dash.py#L488
# https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
#
# analogous to
# https://github.com/plotly/dash/blob/2d735aa250fc67b14dc8f6a337d15a16b7cbd6f8/dash/dash.py#L543-L551
dash_suite <- paste0(self$config$routes_pathname_prefix, "_dash-component-suites/:package_name/:filename")

route$add_handler("get", dash_suite, function(request, response, keys, ...) {
filename <- basename(file.path(keys$filename))

# checkFingerprint returns a list of length 2, the first element is
# the un-fingerprinted path, if a fingerprint is present (otherwise
# the original path is returned), while the second element indicates
# whether the original filename included a valid fingerprint (by
# Dash convention)
fingerprinting_metadata <- checkFingerprint(filename)

filename <- fingerprinting_metadata[[1]]
has_fingerprint <- fingerprinting_metadata[[2]] == TRUE

dep_list <- c(private$dependencies_internal,
private$dependencies,
private$dependencies_user)
Expand All @@ -413,7 +444,6 @@ Dash <- R6::R6Class(
clean_dependencies(dep_list)
)


# return warning if a dependency goes unmatched, since the page
# will probably fail to render properly anyway without it
if (length(dep_pkg$rpkg_path) == 0) {
Expand All @@ -424,16 +454,44 @@ Dash <- R6::R6Class(
response$body <- NULL
response$status <- 404L
} else {
# need to check for debug mode, don't cache, don't etag
# if debug mode is not active
dep_path <- system.file(dep_pkg$rpkg_path,
package = dep_pkg$rpkg_name)

response$body <- readLines(dep_path,
warn = FALSE,
encoding = "UTF-8")
response$status <- 200L

if (!private$debug && has_fingerprint) {
response$status <- 200L
response$set_header('Cache-Control',
sprintf('public, max-age=%s',
31536000) # 1 year
)
} else if (!private$debug && !has_fingerprint) {
modified <- as.character(as.integer(file.mtime(dep_path)))

response$set_header('ETag', modified)

request_etag <- request$headers[["If-None-Match"]]

if (!is.null(request_etag) && modified == request_etag) {
response$body <- NULL
response$status <- 304L
} else {
response$status <- 200L
}
} else {
response$status <- 200L
}

response$type <- get_mimetype(filename)
}

if (private$compress && length(response$body) > 0)
response <- tryCompress(request, response)

TRUE
})

Expand Down Expand Up @@ -476,14 +534,20 @@ Dash <- R6::R6Class(
response$body <- readLines(asset_path,
warn = FALSE,
encoding = "UTF-8")

if (private$compress && length(response$body) > 0) {
response <- tryCompress(request, response)
}
} else {
file_handle <- file(asset_path, "rb")
file_size <- file.size(asset_path)

response$body <- readBin(file_handle,
raw(),
file.size(asset_path))
file_size)
close(file_handle)
}

response$status <- 200L
}
TRUE
Expand All @@ -501,8 +565,13 @@ Dash <- R6::R6Class(
file.size(asset_path))
close(file_handle)

response$set_header('Cache-Control',
sprintf('public, max-age=%s',
'31536000')
)
response$type <- 'image/x-icon'
response$status <- 200L

TRUE
})

Expand All @@ -512,6 +581,9 @@ Dash <- R6::R6Class(
response$body <- private$.index
response$status <- 200L
response$type <- 'html'

if (private$compress)
response <- tryCompress(request, response)
TRUE
})

Expand Down Expand Up @@ -540,6 +612,7 @@ Dash <- R6::R6Class(
response$body <- to_JSON(resp)
response$status <- 200L
response$type <- 'json'

# reset the field for the next reloading operation
private$modified_since_reload <- list()
TRUE
Expand Down Expand Up @@ -830,13 +903,15 @@ Dash <- R6::R6Class(
# private fields defined on initiation
name = NULL,
serve_locally = NULL,
eager_loading = NULL,
meta_tags = NULL,
assets_folder = NULL,
assets_url_path = NULL,
assets_ignore = NULL,
routes_pathname_prefix = NULL,
requests_pathname_prefix = NULL,
suppress_callback_exceptions = NULL,
compress = NULL,
asset_map = NULL,
css = NULL,
scripts = NULL,
Expand Down Expand Up @@ -1188,7 +1263,24 @@ Dash <- R6::R6Class(
css_deps <- render_dependencies(css_deps,
local = private$serve_locally,
prefix=self$config$requests_pathname_prefix)


# ensure that no dependency has both async and dynamic set
if (any(
vapply(depsAll, function(dep)
length(intersect(c("dynamic", "async"), names(dep))) > 1,
logical(1)
)
)
)
stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE)

# remove dependencies which are dynamic from the script list
# to avoid placing them into the index
depsAll <- depsAll[!vapply(depsAll,
isDynamic,
logical(1),
eager_loading = private$eager_loading)]

# scripts go after dash-renderer dependencies (i.e., React),
# but before dash-renderer itself
scripts_deps <- compact(lapply(depsAll, function(dep) {
Expand Down
Loading