Skip to content

Commit e852995

Browse files
authored
Support for asynchronous loading/compression in Dash for R (#157)
* add eager_loading parameter * 📛 add buildFingerprint * 📛 add checkFingerprint * use getDependencyPath, + 🐾/Etag support * updates to support async * ✨ properly support gz compression * 🐛 post-async fixes for CSS handling
1 parent 1d5ee2d commit e852995

File tree

6 files changed

+252
-48
lines changed

6 files changed

+252
-48
lines changed

CHANGELOG.md

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
# Change Log for Dash for R
22
All notable changes to this project will be documented in this file.
33

4-
## Unreleased
4+
## [0.2.0] - 2019-12-23
55
### Added
6+
- Support for asynchronous/dynamic loading of dependencies, resource caching, and asset fingerprinting [#157](https://github.com/plotly/dashR/pull/157)
7+
- Compression of text resources using `brotli`, `gzip`, or `deflate` [#157](https://github.com/plotly/dashR/pull/157)
68
- Support for adding `<meta>` tags to index [#142](https://github.com/plotly/dashR/pull/142)
79
- Hot reloading now supported in debug mode [#127](https://github.com/plotly/dashR/pull/127)
810
- Support for displaying Dash for R applications within RStudio's viewer pane when `use_viewer = TRUE`

DESCRIPTION

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,19 @@
11
Package: dash
22
Title: An Interface to the Dash Ecosystem for Authoring Reactive Web Applications
3-
Version: 0.1.0
3+
Version: 0.2.0
44
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"))
55
Description: A framework for building analytical web applications, Dash offers a pleasant and productive development experience. No JavaScript required.
66
Depends:
77
R (>= 3.0.2)
88
Imports:
9-
dashHtmlComponents (== 1.0.0),
10-
dashCoreComponents (== 1.0.0),
11-
dashTable (== 4.0.2),
9+
dashHtmlComponents (== 1.0.2),
10+
dashCoreComponents (== 1.6.0),
11+
dashTable (== 4.5.1),
1212
R6,
1313
fiery (> 1.0.0),
1414
routr (> 0.2.0),
1515
plotly,
16-
reqres,
16+
reqres (>= 0.2.3),
1717
jsonlite,
1818
htmltools,
1919
assertthat,
@@ -31,9 +31,9 @@ Collate:
3131
'imports.R'
3232
'print.R'
3333
'internal.R'
34-
Remotes: plotly/dash-html-components@17da1f4,
35-
plotly/dash-core-components@cc1e654,
36-
plotly/dash-table@042ad65
34+
Remotes: plotly/dash-html-components@55c3884,
35+
plotly/dash-core-components@c107e0f,
36+
plotly/dash-table@3058bd5
3737
License: MIT + file LICENSE
3838
Encoding: UTF-8
3939
LazyData: true

R/dash.R

Lines changed: 100 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
#' server = fiery::Fire$new(),
1010
#' assets_folder = 'assets',
1111
#' assets_url_path = '/assets',
12+
#' eager_loading = FALSE,
1213
#' assets_ignore = '',
1314
#' serve_locally = TRUE,
1415
#' meta_tags = NULL,
@@ -30,6 +31,7 @@
3031
#' .css files will be loaded immediately unless excluded by `assets_ignore`,
3132
#' and other files such as images will be served if requested. Default is `assets`. \cr
3233
#' `assets_url_path` \tab \tab Character. Specify the URL path for asset serving. Default is `assets`. \cr
34+
#' `eager_loading` \tab \tab Logical. Controls whether asynchronous resources are prefetched (if `TRUE`) or loaded on-demand (if `FALSE`). \cr
3335
#' `assets_ignore` \tab \tab Character. A regular expression, to match assets to omit from
3436
#' immediate loading. Ignored files will still be served if specifically requested. You
3537
#' cannot use this to prevent access to sensitive files. \cr
@@ -126,7 +128,10 @@
126128
#'
127129
#' @examples
128130
#' \dontrun{
131+
#' library(dashCoreComponents)
132+
#' library(dashHtmlComponents)
129133
#' library(dash)
134+
130135
#' app <- Dash$new()
131136
#' app$layout(
132137
#' dccInput(id = "inputID", value = "initial value", type = "text"),
@@ -160,13 +165,15 @@ Dash <- R6::R6Class(
160165
server = fiery::Fire$new(),
161166
assets_folder = 'assets',
162167
assets_url_path = '/assets',
168+
eager_loading = FALSE,
163169
assets_ignore = '',
164170
serve_locally = TRUE,
165171
meta_tags = NULL,
166172
routes_pathname_prefix = NULL,
167173
requests_pathname_prefix = NULL,
168174
external_scripts = NULL,
169175
external_stylesheets = NULL,
176+
compress = TRUE,
170177
suppress_callback_exceptions = FALSE) {
171178

172179
# argument type checking
@@ -178,12 +185,14 @@ Dash <- R6::R6Class(
178185
# save relevant args as private fields
179186
private$name <- name
180187
private$serve_locally <- serve_locally
188+
private$eager_loading <- eager_loading
181189
# remove leading and trailing slash(es) if present
182190
private$assets_folder <- gsub("^/+|/+$", "", assets_folder)
183191
# remove trailing slash in assets_url_path, if present
184192
private$assets_url_path <- sub("/$", "", assets_url_path)
185193
private$assets_ignore <- assets_ignore
186194
private$suppress_callback_exceptions <- suppress_callback_exceptions
195+
private$compress <- compress
187196
private$app_root_path <- getAppPath()
188197
private$app_launchtime <- as.integer(Sys.time())
189198
private$meta_tags <- meta_tags
@@ -240,6 +249,9 @@ Dash <- R6::R6Class(
240249
response$body <- to_JSON(lay, pretty = TRUE)
241250
response$status <- 200L
242251
response$type <- 'json'
252+
253+
254+
243255
TRUE
244256
})
245257

@@ -250,6 +262,7 @@ Dash <- R6::R6Class(
250262
response$body <- to_JSON(list())
251263
response$status <- 200L
252264
response$type <- 'json'
265+
253266
return(FALSE)
254267
}
255268

@@ -265,6 +278,8 @@ Dash <- R6::R6Class(
265278
response$body <- to_JSON(setNames(payload, NULL))
266279
response$status <- 200L
267280
response$type <- 'json'
281+
if (private$compress)
282+
response <- tryCompress(request, response)
268283
TRUE
269284
})
270285

@@ -393,17 +408,33 @@ Dash <- R6::R6Class(
393408
response$status <- 500L
394409
private$stack_message <- NULL
395410
}
411+
412+
if (private$compress)
413+
response <- tryCompress(request, response)
396414
TRUE
397415
})
398416

399417
# This endpoint supports dynamic dependency loading
400418
# during `_dash-update-component` -- for reference:
401-
# https://github.com/plotly/dash/blob/1249ffbd051bfb5fdbe439612cbec7fa8fff5ab5/dash/dash.py#L488
402419
# https://docs.python.org/3/library/pkgutil.html#pkgutil.get_data
420+
#
421+
# analogous to
422+
# https://github.com/plotly/dash/blob/2d735aa250fc67b14dc8f6a337d15a16b7cbd6f8/dash/dash.py#L543-L551
403423
dash_suite <- paste0(self$config$routes_pathname_prefix, "_dash-component-suites/:package_name/:filename")
404-
424+
405425
route$add_handler("get", dash_suite, function(request, response, keys, ...) {
406426
filename <- basename(file.path(keys$filename))
427+
428+
# checkFingerprint returns a list of length 2, the first element is
429+
# the un-fingerprinted path, if a fingerprint is present (otherwise
430+
# the original path is returned), while the second element indicates
431+
# whether the original filename included a valid fingerprint (by
432+
# Dash convention)
433+
fingerprinting_metadata <- checkFingerprint(filename)
434+
435+
filename <- fingerprinting_metadata[[1]]
436+
has_fingerprint <- fingerprinting_metadata[[2]] == TRUE
437+
407438
dep_list <- c(private$dependencies_internal,
408439
private$dependencies,
409440
private$dependencies_user)
@@ -413,7 +444,6 @@ Dash <- R6::R6Class(
413444
clean_dependencies(dep_list)
414445
)
415446

416-
417447
# return warning if a dependency goes unmatched, since the page
418448
# will probably fail to render properly anyway without it
419449
if (length(dep_pkg$rpkg_path) == 0) {
@@ -424,16 +454,44 @@ Dash <- R6::R6Class(
424454
response$body <- NULL
425455
response$status <- 404L
426456
} else {
457+
# need to check for debug mode, don't cache, don't etag
458+
# if debug mode is not active
427459
dep_path <- system.file(dep_pkg$rpkg_path,
428460
package = dep_pkg$rpkg_name)
429-
461+
430462
response$body <- readLines(dep_path,
431463
warn = FALSE,
432464
encoding = "UTF-8")
433-
response$status <- 200L
465+
466+
if (!private$debug && has_fingerprint) {
467+
response$status <- 200L
468+
response$set_header('Cache-Control',
469+
sprintf('public, max-age=%s',
470+
31536000) # 1 year
471+
)
472+
} else if (!private$debug && !has_fingerprint) {
473+
modified <- as.character(as.integer(file.mtime(dep_path)))
474+
475+
response$set_header('ETag', modified)
476+
477+
request_etag <- request$headers[["If-None-Match"]]
478+
479+
if (!is.null(request_etag) && modified == request_etag) {
480+
response$body <- NULL
481+
response$status <- 304L
482+
} else {
483+
response$status <- 200L
484+
}
485+
} else {
486+
response$status <- 200L
487+
}
488+
434489
response$type <- get_mimetype(filename)
435490
}
436491

492+
if (private$compress && length(response$body) > 0)
493+
response <- tryCompress(request, response)
494+
437495
TRUE
438496
})
439497

@@ -476,14 +534,20 @@ Dash <- R6::R6Class(
476534
response$body <- readLines(asset_path,
477535
warn = FALSE,
478536
encoding = "UTF-8")
537+
538+
if (private$compress && length(response$body) > 0) {
539+
response <- tryCompress(request, response)
540+
}
479541
} else {
480542
file_handle <- file(asset_path, "rb")
543+
file_size <- file.size(asset_path)
544+
481545
response$body <- readBin(file_handle,
482546
raw(),
483-
file.size(asset_path))
547+
file_size)
484548
close(file_handle)
485549
}
486-
550+
487551
response$status <- 200L
488552
}
489553
TRUE
@@ -501,8 +565,13 @@ Dash <- R6::R6Class(
501565
file.size(asset_path))
502566
close(file_handle)
503567

568+
response$set_header('Cache-Control',
569+
sprintf('public, max-age=%s',
570+
'31536000')
571+
)
504572
response$type <- 'image/x-icon'
505573
response$status <- 200L
574+
506575
TRUE
507576
})
508577

@@ -512,6 +581,9 @@ Dash <- R6::R6Class(
512581
response$body <- private$.index
513582
response$status <- 200L
514583
response$type <- 'html'
584+
585+
if (private$compress)
586+
response <- tryCompress(request, response)
515587
TRUE
516588
})
517589

@@ -540,6 +612,7 @@ Dash <- R6::R6Class(
540612
response$body <- to_JSON(resp)
541613
response$status <- 200L
542614
response$type <- 'json'
615+
543616
# reset the field for the next reloading operation
544617
private$modified_since_reload <- list()
545618
TRUE
@@ -830,13 +903,15 @@ Dash <- R6::R6Class(
830903
# private fields defined on initiation
831904
name = NULL,
832905
serve_locally = NULL,
906+
eager_loading = NULL,
833907
meta_tags = NULL,
834908
assets_folder = NULL,
835909
assets_url_path = NULL,
836910
assets_ignore = NULL,
837911
routes_pathname_prefix = NULL,
838912
requests_pathname_prefix = NULL,
839913
suppress_callback_exceptions = NULL,
914+
compress = NULL,
840915
asset_map = NULL,
841916
css = NULL,
842917
scripts = NULL,
@@ -1188,7 +1263,24 @@ Dash <- R6::R6Class(
11881263
css_deps <- render_dependencies(css_deps,
11891264
local = private$serve_locally,
11901265
prefix=self$config$requests_pathname_prefix)
1191-
1266+
1267+
# ensure that no dependency has both async and dynamic set
1268+
if (any(
1269+
vapply(depsAll, function(dep)
1270+
length(intersect(c("dynamic", "async"), names(dep))) > 1,
1271+
logical(1)
1272+
)
1273+
)
1274+
)
1275+
stop("Can't have both 'dynamic' and 'async' in a Dash dependency; please correct and reload.", call. = FALSE)
1276+
1277+
# remove dependencies which are dynamic from the script list
1278+
# to avoid placing them into the index
1279+
depsAll <- depsAll[!vapply(depsAll,
1280+
isDynamic,
1281+
logical(1),
1282+
eager_loading = private$eager_loading)]
1283+
11921284
# scripts go after dash-renderer dependencies (i.e., React),
11931285
# but before dash-renderer itself
11941286
scripts_deps <- compact(lapply(depsAll, function(dep) {

0 commit comments

Comments
 (0)