File: bind-cache.R

package info (click to toggle)
r-cran-shiny 1.10.0%2Bdfsg-2
  • links: PTS, VCS
  • area: main
  • in suites: forky, sid, trixie
  • size: 10,948 kB
  • sloc: javascript: 39,934; sh: 28; makefile: 20
file content (774 lines) | stat: -rw-r--r-- 29,714 bytes parent folder | download
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
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
utils::globalVariables(".GenericCallEnv", add = TRUE)

#' Add caching with reactivity to an object
#'
#' @description
#'
#' `bindCache()` adds caching [reactive()] expressions and `render*` functions
#' (like [renderText()], [renderTable()], ...).
#'
#' Ordinary [reactive()] expressions automatically cache their _most recent_
#' value, which helps to  avoid redundant computation in downstream reactives.
#' `bindCache()` will cache all previous values (as long as they fit in the
#' cache) and they can be shared across user sessions. This allows
#' `bindCache()` to dramatically improve performance when used correctly.

#' @details
#'
#' `bindCache()` requires one or more expressions that are used to generate a
#' **cache key**, which is used to determine if a computation has occurred
#' before and hence can be retrieved from the cache. If you're familiar with the
#' concept of memoizing pure functions (e.g., the \pkg{memoise} package), you
#' can think of the cache key as the input(s) to a pure function. As such, one
#' should take care to make sure the use of `bindCache()` is _pure_ in the same
#' sense, namely:
#'
#' 1. For a given key, the return value is always the same.
#' 2. Evaluation has no side-effects.
#'
#' In the example here, the `bindCache()` key consists of `input$x` and
#' `input$y` combined, and the value is `input$x * input$y`. In this simple
#' example, for any given key, there is only one possible returned value.
#'
#' ```
#' r <- reactive({ input$x * input$y }) %>%
#'   bindCache(input$x, input$y)
#' ```
#'

#' The largest performance improvements occur when the cache key is fast to
#' compute and the reactive expression is slow to compute. To see if the value
#' should be computed, a cached reactive evaluates the key, and then serializes
#' and hashes the result. If the resulting hashed key is in the cache, then the
#' cached reactive simply retrieves the previously calculated value and returns
#' it; if not, then the value is computed and the result is stored in the cache
#' before being returned.
#'
#' To compute the cache key, `bindCache()` hashes the contents of `...`, so it's
#' best to avoid including large objects in a cache key since that can result in
#' slow hashing. It's also best to avoid reference objects like environments and
#' R6 objects, since the serialization of these objects may not capture relevant
#' changes.
#'
#' If you want to use a large object as part of a cache key, it may make sense
#' to do some sort of reduction on the data that still captures information
#' about whether a value can be retrieved from the cache. For example, if you
#' have a large data set with timestamps, it might make sense to extract the
#' most recent timestamp and return that. Then, instead of hashing the entire
#' data object, the cached reactive only needs to hash the timestamp.
#'
#' ```
#' r <- reactive({ compute(bigdata()) } %>%
#'   bindCache({ extract_most_recent_time(bigdata()) })
#' ```
#'
#' For computations that are very slow, it often makes sense to pair
#' [bindCache()] with [bindEvent()] so that no computation is performed until
#' the user explicitly requests it (for more, see the Details section of
#' [bindEvent()]).

#' @section Cache keys and reactivity:
#'
#'   Because the **value** expression (from the original [reactive()]) is
#'   cached, it is not necessarily re-executed when someone retrieves a value,
#'   and therefore it can't be used to decide what objects to take reactive
#'   dependencies on. Instead, the **key** is used to figure out which objects
#'   to take reactive dependencies on. In short, the key expression is reactive,
#'   and value expression is no longer reactive.
#'
#'   Here's an example of what not to do: if the key is `input$x` and the value
#'   expression is from `reactive({input$x + input$y})`, then the resulting
#'   cached reactive  will only take a reactive dependency on `input$x` -- it
#'   won't recompute `{input$x + input$y}` when just `input$y` changes.
#'   Moreover, the cache won't use `input$y` as part of the key, and so it could
#'   return incorrect values in the future when it retrieves values from the
#'   cache. (See the examples below for an example of this.)
#'
#'   A better cache key would be something like `input$x, input$y`. This does
#'   two things: it ensures that a reactive dependency is taken on both
#'   `input$x` and `input$y`, and it also makes sure that both values are
#'   represented in the cache key.
#'
#'   In general, `key` should use the same reactive inputs as `value`, but the
#'   computation should be simpler. If there are other (non-reactive) values
#'   that are consumed, such as external data sources, they should be used in
#'   the `key` as well. Note that if the `key` is large, it can make sense to do
#'   some sort of reduction on it so that the serialization and hashing of the
#'   cache key is not too expensive.
#'
#'   Remember that the key is _reactive_, so it is not re-executed every single
#'   time that someone accesses the cached reactive. It is only re-executed if
#'   it has been invalidated by one of the reactives it depends on. For
#'   example, suppose we have this cached reactive:
#'
#'   ```
#'   r <- reactive({ input$x * input$y }) %>%
#'     bindCache(input$x, input$y)
#'   ```
#'
#' In this case, the key expression is essentially `reactive(list(input$x,
#' input$y))` (there's a bit more to it, but that's a good enough
#' approximation). The first time `r()` is called, it executes the key, then
#' fails to find it in the cache, so it executes the value expression, `{
#' input$x + input$y }`. If `r()` is called again, then it does not need to
#' re-execute the key expression, because it has not been invalidated via a
#' change to `input$x` or `input$y`; it simply returns the previous value.
#' However, if `input$x` or `input$y` changes, then the reactive expression will
#' be invalidated, and the next time that someone calls `r()`, the key
#' expression will need to be re-executed.
#'
#' Note that if the cached reactive is passed to [bindEvent()], then the key
#' expression will no longer be reactive; instead, the event expression will be
#' reactive.
#'
#'
#' @section Cache scope:
#'
#'   By default, when `bindCache()` is used, it is scoped to the running
#'   application. That means that it shares a cache with all user sessions
#'   connected to the application (within the R process). This is done with the
#'   `cache` parameter's default value, `"app"`.
#'
#'   With an app-level cache scope, one user can benefit from the work done for
#'   another user's session. In most cases, this is the best way to get
#'   performance improvements from caching. However, in some cases, this could
#'   leak information between sessions. For example, if the cache key does not
#'   fully encompass the inputs used by the value, then data could leak between
#'   the sessions. Or if a user sees that a cached reactive returns its value
#'   very quickly, they may be able to infer that someone else has already used
#'   it with the same values.
#'
#'   It is also possible to scope the cache to the session, with
#'   `cache="session"`. This removes the risk of information leaking between
#'   sessions, but then one session cannot benefit from computations performed in
#'   another session.
#'
#'   It is possible to pass in caching objects directly to
#'   `bindCache()`. This can be useful if, for example, you want to use a
#'   particular type of cache with specific cached reactives, or if you want to
#'   use a [cachem::cache_disk()] that is shared across multiple processes and
#'   persists beyond the current R session.
#'
#'   To use different settings for an application-scoped cache, you can call
#'   [shinyOptions()] at the top of your app.R, server.R, or
#'   global.R. For example, this will create a cache with 500 MB of space
#'   instead of the default 200 MB:
#'
#'   ```
#'   shinyOptions(cache = cachem::cache_mem(max_size = 500e6))
#'   ```
#'
#'   To use different settings for a session-scoped cache, you can set
#'   `session$cache` at the top of your server function. By default, it will
#'   create a 200 MB memory cache for each session, but you can replace it with
#'   something different. To use the session-scoped cache, you must also call
#'   `bindCache()` with `cache="session"`. This will create a 100 MB cache for
#'   the session:
#'
#'   ```
#'   function(input, output, session) {
#'     session$cache <- cachem::cache_mem(max_size = 100e6)
#'     ...
#'   }
#'   ```
#'
#'   If you want to use a cache that is shared across multiple R processes, you
#'   can use a [cachem::cache_disk()]. You can create a application-level shared
#'   cache by putting this at the top of your app.R, server.R, or global.R:
#'
#'   ```
#'   shinyOptions(cache = cachem::cache_disk(file.path(dirname(tempdir()), "myapp-cache")))
#'   ```
#'
#'   This will create a subdirectory in your system temp directory named
#'   `myapp-cache` (replace `myapp-cache` with a unique name of
#'   your choosing). On most platforms, this directory will be removed when
#'   your system reboots. This cache will persist across multiple starts and
#'   stops of the R process, as long as you do not reboot.
#'
#'   To have the cache persist even across multiple reboots, you can create the
#'   cache in a location outside of the temp directory. For example, it could
#'   be a subdirectory of the application:
#'
#'   ```
#'   shinyOptions(cache = cachem::cache_disk("./myapp-cache"))
#'   ```
#'
#'   In this case, resetting the cache will have to be done manually, by deleting
#'   the directory.
#'
#'   You can also scope a cache to just one item, or selected items. To do that,
#'   create a [cachem::cache_mem()] or [cachem::cache_disk()], and pass it
#'   as the `cache` argument of `bindCache()`.
#'

#'
#' @section Computing cache keys:
#'
#'   The actual cache key that is used internally takes value from evaluating
#'   the key expression(s) (from the `...` arguments) and combines it with the
#'   (unevaluated) value expression.
#'
#'   This means that if there are two cached reactives which have the same
#'   result from evaluating the key, but different value expressions, then they
#'   will not need to worry about collisions.
#'
#'   However, if two cached reactives have identical key and value expressions
#'   expressions, they will share the cached values. This is useful when using
#'   `cache="app"`: there may be multiple user sessions which create separate
#'   cached reactive objects (because they are created from the same code in the
#'   server function, but the server function is executed once for each user
#'   session), and those cached reactive objects across sessions can share
#'   values in the cache.



#'
#' @section Async with cached reactives:
#'
#'   With a cached reactive expression, the key and/or value expression can be
#'   _asynchronous_. In other words, they can be promises --- not regular R
#'   promises, but rather objects provided by the
#'   \href{https://rstudio.github.io/promises/}{\pkg{promises}}  package, which
#'   are similar to promises in JavaScript. (See [promises::promise()] for more
#'   information.) You can also use [future::future()] objects to run code in a
#'   separate process or even on a remote machine.
#'
#'   If the value returns a promise, then anything that consumes the cached
#'   reactive must expect it to return a promise.
#'
#'   Similarly, if the key is a promise (in other words, if it is asynchronous),
#'   then the entire cached reactive must be asynchronous, since the key must be
#'   computed asynchronously before it knows whether to compute the value or the
#'   value is retrieved from the cache. Anything that consumes the cached
#'   reactive must therefore expect it to return a promise.
#'

#'
#' @section Developing render functions for caching:
#'
#'   If you've implemented your own `render*()` function, it may just work with
#'   `bindCache()`, but it is possible that you will need to make some
#'   modifications. These modifications involve helping `bindCache()` avoid
#'   cache collisions, dealing with internal state that may be set by the,
#'   `render` function, and modifying the data as it goes in and comes out of
#'   the cache.
#'
#'   You may need to provide a `cacheHint` to [createRenderFunction()] (or
#'   `htmlwidgets::shinyRenderWidget()`, if you've authored an htmlwidget) in
#'   order for `bindCache()` to correctly compute a cache key.
#'
#'   The potential problem is a cache collision. Consider the following:
#'
#'   ```
#'   output$x1 <- renderText({ input$x }) %>% bindCache(input$x)
#'   output$x2 <- renderText({ input$x * 2 }) %>% bindCache(input$x)
#'   ```
#'
#'   Both `output$x1` and `output$x2` use `input$x` as part of their cache key,
#'   but if it were the only thing used in the cache key, then the two outputs
#'   would have a cache collision, and they would have the same output. To avoid
#'   this, a _cache hint_ is automatically added when [renderText()] calls
#'   [createRenderFunction()]. The cache hint is used as part of the actual
#'   cache key, in addition to the one passed to `bindCache()` by the user. The
#'   cache hint can be viewed by calling the internal Shiny function
#'   `extractCacheHint()`:
#'
#'   ```
#'   r <- renderText({ input$x })
#'   shiny:::extractCacheHint(r)
#'   ```
#'
#'   This returns a nested list containing an item, `$origUserFunc$body`, which
#'   in this case is the expression which was passed to `renderText()`:
#'   `{ input$x }`. This (quoted)  expression is mixed into the actual cache
#'   key, and it is how `output$x1` does not have collisions with `output$x2`.
#'
#'   For most developers of render functions, nothing extra needs to be done;
#'   the automatic inference of the cache hint is sufficient. Again, you can
#'   check it by calling `shiny:::extractCacheHint()`, and by testing the
#'   render function for cache collisions in a real application.
#'
#'   In some cases, however, the automatic cache hint inference is not
#'   sufficient, and it is necessary to provide a cache hint. This is true
#'   for `renderPrint()`. Unlike `renderText()`, it wraps the user-provided
#'   expression in another function, before passing it to [createRenderFunction()]
#'   (instead of [createRenderFunction()]). Because the user code is wrapped in
#'   another function, `createRenderFunction()` is not able to automatically
#'   extract the user-provided code and use it in the cache key. Instead,
#'   `renderPrint` calls `createRenderFunction()`, it explicitly passes along a
#'   `cacheHint`, which includes a label and the original user expression.
#'
#'   In general, if you need to provide a `cacheHint`, it is best practice to
#'   provide a `label` id, the user's `expr`, as well as any other arguments
#'   that may influence the final value.
#'
#'   For \pkg{htmlwidgets}, it will try to automatically infer a cache hint;
#'   again, you can inspect the cache hint with `shiny:::extractCacheHint()` and
#'   also test it in an application. If you do need to explicitly provide a
#'   cache hint, pass it to `shinyRenderWidget`. For example:
#'
#'   ```
#'   renderMyWidget <- function(expr) {
#'     q <- rlang::enquo0(expr)
#'
#'     htmlwidgets::shinyRenderWidget(
#'       q,
#'       myWidgetOutput,
#'       quoted = TRUE,
#'       cacheHint = list(label = "myWidget", userQuo = q)
#'     )
#'   }
#'   ```
#'
#'   If your `render` function sets any internal state, you may find it useful
#'   in your call to [createRenderFunction()] to use
#'   the `cacheWriteHook` and/or `cacheReadHook` parameters. These hooks are
#'   functions that run just before the object is stored in the cache, and just
#'   after the object is retrieved from the cache. They can modify the data
#'   that is stored and retrieved; this can be useful if extra information needs
#'   to be stored in the cache. They can also be used to modify the state of the
#'   application; for example, it can call [createWebDependency()] to make
#'   JS/CSS resources available if the cached object is loaded in a different R
#'   process. (See the source of `htmlwidgets::shinyRenderWidget` for an example
#'   of this.)
#'
#' @section Uncacheable objects:
#'
#'   Some render functions cannot be cached, typically because they have side
#'   effects or modify some external state, and they must re-execute each time
#'   in order to work properly.
#'
#'   For developers of such code, they should call [createRenderFunction()] (or
#'   [markRenderFunction()]) with `cacheHint = FALSE`.
#'
#'
#' @section Caching with `renderPlot()`:
#'
#'   When `bindCache()` is used with `renderPlot()`, the `height` and `width`
#'   passed to the original `renderPlot()` are ignored. They are superseded by
#'   `sizePolicy` argument passed to `bindCache. The default is:
#'
#'   ```
#'   sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2)
#'   ```
#'
#' `sizePolicy` must be a function that takes a two-element numeric vector as
#' input, representing the width and height of the `<img>` element in the
#' browser window, and it must return a two-element numeric vector, representing
#' the pixel dimensions of the plot to generate. The purpose is to round the
#' actual pixel dimensions from the browser to some other dimensions, so that
#' this will not generate and cache images of every possible pixel dimension.
#' See [sizeGrowthRatio()] for more information on the default sizing policy.
#'
#' @param x The object to add caching to.
#' @param ... One or more expressions to use in the caching key.
#' @param cache The scope of the cache, or a cache object. This can be `"app"`
#'   (the default), `"session"`, or a cache object like a
#'   [cachem::cache_disk()]. See the Cache Scoping section for more information.
#'
#' @seealso [bindEvent()], [renderCachedPlot()] for caching plots.
#'
#' @examples
#' \dontrun{
#' rc <- bindCache(
#'   x = reactive({
#'     Sys.sleep(2)   # Pretend this is expensive
#'     input$x * 100
#'   }),
#'   input$x
#' )
#'
#' # Can make it prettier with the %>% operator
#' library(magrittr)
#'
#' rc <- reactive({
#'   Sys.sleep(2)
#'   input$x * 100
#' }) %>%
#'   bindCache(input$x)
#'
#' }
#'
#' ## Only run app examples in interactive R sessions
#' if (interactive()) {
#'
#' # Basic example
#' shinyApp(
#'   ui = fluidPage(
#'     sliderInput("x", "x", 1, 10, 5),
#'     sliderInput("y", "y", 1, 10, 5),
#'     div("x * y: "),
#'     verbatimTextOutput("txt")
#'   ),
#'   server = function(input, output) {
#'     r <- reactive({
#'       # The value expression is an _expensive_ computation
#'       message("Doing expensive computation...")
#'       Sys.sleep(2)
#'       input$x * input$y
#'     }) %>%
#'       bindCache(input$x, input$y)
#'
#'     output$txt <- renderText(r())
#'   }
#' )
#'
#'
#' # Caching renderText
#' shinyApp(
#'   ui = fluidPage(
#'     sliderInput("x", "x", 1, 10, 5),
#'     sliderInput("y", "y", 1, 10, 5),
#'     div("x * y: "),
#'     verbatimTextOutput("txt")
#'   ),
#'   server = function(input, output) {
#'     output$txt <- renderText({
#'       message("Doing expensive computation...")
#'       Sys.sleep(2)
#'       input$x * input$y
#'     }) %>%
#'       bindCache(input$x, input$y)
#'   }
#' )
#'
#'
#' # Demo of using events and caching with an actionButton
#' shinyApp(
#'   ui = fluidPage(
#'     sliderInput("x", "x", 1, 10, 5),
#'     sliderInput("y", "y", 1, 10, 5),
#'     actionButton("go", "Go"),
#'     div("x * y: "),
#'     verbatimTextOutput("txt")
#'   ),
#'   server = function(input, output) {
#'     r <- reactive({
#'       message("Doing expensive computation...")
#'       Sys.sleep(2)
#'       input$x * input$y
#'     }) %>%
#'       bindCache(input$x, input$y) %>%
#'       bindEvent(input$go)
#'       # The cached, eventified reactive takes a reactive dependency on
#'       # input$go, but doesn't use it for the cache key. It uses input$x and
#'       # input$y for the cache key, but doesn't take a reactive dependency on
#'       # them, because the reactive dependency is superseded by addEvent().
#'
#'     output$txt <- renderText(r())
#'   }
#' )
#'
#' }
#'
#' @export
bindCache <- function(x, ..., cache = "app") {
  force(cache)

  UseMethod("bindCache")
}

#' @export
bindCache.default <- function(x, ...) {
  stop("Don't know how to handle object with class ", paste(class(x), collapse = ", "))
}

#' @export
bindCache.reactiveExpr <- function(x, ..., cache = "app") {
  check_dots_unnamed()

  label <- exprToLabel(substitute(key), "cachedReactive")
  domain <- reactive_get_domain(x)

  # Convert the ... to a function that returns their evaluated values.
  keyFunc <- quos_to_func(enquos0(...))

  valueFunc <- reactive_get_value_func(x)
  # Hash cache hint now -- this will be added to the key later on, to reduce the
  # chance of key collisions with other cachedReactives.
  cacheHint <- rlang::hash(extractCacheHint(x))
  valueFunc <- wrapFunctionLabel(valueFunc, "cachedReactiveValueFunc", ..stacktraceon = TRUE)

  # Don't hold on to the reference for x, so that it can be GC'd
  rm(x)
  # Hacky workaround for issue with `%>%` preventing GC:
  # https://github.com/tidyverse/magrittr/issues/229
  if (exists(".GenericCallEnv") && exists(".", envir = .GenericCallEnv)) {
    rm(list = ".", envir = .GenericCallEnv)
  }


  res <- reactive(label = label, domain = domain, {
    cache <- resolve_cache_object(cache, domain)
    hybrid_chain(
      keyFunc(),
      generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook = identity, cacheWriteHook = identity)
    )
  })

  class(res) <- c("reactive.cache", class(res))
  res
}

#' @export
bindCache.shiny.render.function <- function(x, ..., cache = "app") {
  check_dots_unnamed()

  keyFunc <- quos_to_func(enquos0(...))

  cacheHint <- rlang::hash(extractCacheHint(x))

  cacheWriteHook <- attr(x, "cacheWriteHook", exact = TRUE) %||% identity
  cacheReadHook  <- attr(x, "cacheReadHook",  exact = TRUE) %||% identity

  valueFunc <- x

  renderFunc <- function(...) {
    domain <- getDefaultReactiveDomain()
    cache <- resolve_cache_object(cache, domain)

    hybrid_chain(
      keyFunc(),
      generateCacheFun(valueFunc, cache, cacheHint, cacheReadHook, cacheWriteHook, ...)
    )
  }

  renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
  class(renderFunc) <- c("shiny.render.function.cache", class(valueFunc))
  renderFunc
}

#' @export
bindCache.shiny.renderPlot <- function(x, ...,
  cache = "app",
  sizePolicy = sizeGrowthRatio(width = 400, height = 400, growthRate = 1.2))
{
  check_dots_unnamed()

  valueFunc <- x

  # Given the actual width/height of the image element in the browser, the
  # resize observer computes the width/height using sizePolicy() and pushes
  # those values into `fitWidth` and `fitHeight`. It's done this way so that the
  # `fitWidth` and `fitHeight` only change (and cause invalidations of the key
  # expression) when the rendered image size changes, and not every time the
  # browser's <img> tag changes size.
  #
  # If the key expression were invalidated every time the image element changed
  # size, even if the resulting key was the same (because `sizePolicy()` gave
  # the same output for a slightly different img element size), it would result
  # in getting the (same) image from the cache and sending it to the client
  # again. This resize observer prevents that.
  fitDims <- reactiveVal(NULL)
  resizeObserverCreated <- FALSE
  outputName <- NULL
  ensureResizeObserver <- function() {
    if (resizeObserverCreated)
      return()

    doResizeCheck <- function() {
      if (is.null(outputName)) {
        outputName <<- getCurrentOutputInfo()$name
      }
      session <- getDefaultReactiveDomain()

      width  <- session$clientData[[paste0('output_', outputName, '_width')]]  %||% 0
      height <- session$clientData[[paste0('output_', outputName, '_height')]] %||% 0

      rect <- sizePolicy(c(width, height))
      fitDims(list(width = rect[1], height = rect[2]))
    }

    # Run it once immediately, then set up the observer
    isolate(doResizeCheck())

    observe({
      doResizeCheck()
    })
    # TODO: Make sure this observer gets GC'd if output$foo is replaced.
    # Currently, if you reassign output$foo, the observer persists until the
    # session ends. This is generally bad programming practice and should be
    # rare, but still, we should try to clean up properly.

    resizeObserverCreated <<- TRUE
  }

  renderFunc <- function(...) {
    hybrid_chain(
      # Pass in fitDims so that so that the generated plot will be the
      # dimensions specified by the sizePolicy; otherwise the plot would be the
      # exact dimensions of the img element, which isn't what we want for cached
      # plots.
      valueFunc(..., get_dims = fitDims),
      function(img) {
        # Replace exact pixel dimensions; instead, the max-height and max-width
        # will be set to 100% from CSS.
        img$class <- "shiny-scalable"
        img$width  <- NULL
        img$height <- NULL

        img
      }
    )
  }

  renderFunc <- addAttributes(renderFunc, renderFunctionAttributes(valueFunc))
  class(renderFunc) <- class(valueFunc)

  bindCache.shiny.render.function(
    renderFunc,
    ...,
    {
      ensureResizeObserver()
      session <- getDefaultReactiveDomain()
      if (is.null(session) || is.null(fitDims())) {
        req(FALSE)
      }
      pixelratio <- session$clientData$pixelratio %||% 1

      list(fitDims(), pixelratio)
    },
    cache = cache
  )
}

#' @export
bindCache.reactive.cache <- function(x, ...) {
  stop("bindCache() has already been called on the object.")
}

#' @export
bindCache.shiny.render.function.cache <- bindCache.reactive.cache

#' @export
bindCache.reactive.event <- function(x, ...) {
  stop("Can't call bindCache() after calling bindEvent() on an object. Maybe you wanted to call bindEvent() after bindCache()?")
}

#' @export
bindCache.shiny.render.function.event <- bindCache.reactive.event

#' @export
bindCache.Observer <- function(x, ...) {
  stop("Can't bindCache an observer, because observers exist for the side efects, not for their return values.")
}

#' @export
bindCache.function <- function(x, ...) {
  stop(
    "Don't know how to add caching to a plain function. ",
    "If this is a render* function for Shiny, it may need to be updated. ",
    "Please see ?shiny::bindCache for more information."
  )
}

# Returns a function which should be passed as a step in to hybrid_chain(). The
# returned function takes a cache key as input and manages storing and retrieving
# values from the cache, as well as executing the valueFunc if needed.
generateCacheFun <- function(
  valueFunc,
  cache,
  cacheHint,
  cacheReadHook,
  cacheWriteHook,
  ...
) {
  function(cacheKeyResult) {
    key_str <- rlang::hash(list(cacheKeyResult, cacheHint))
    res <- cache$get(key_str)

    # Case 1: cache hit
    if (!is.key_missing(res)) {
      return(hybrid_chain(
        {
          # The first step is just to convert `res` to a promise or not, so
          # that hybrid_chain() knows to propagate the promise-ness.
          if (res$is_promise) promise_resolve(res)
          else                res
        },
        function(res) {
          if (res$error) {
            stop(res$value)
          }

          cacheReadHook(valueWithVisible(res))
        }
      ))
    }

    # Case 2: cache miss
    #
    # valueFunc() might return a promise, or an actual value. Normally we'd
    # use a hybrid_chain() for this, but in this case, we need to have
    # different behavior if it's a promise or not a promise -- the
    # information about whether or not it's a promise needs to be stored in
    # the cache. We need to handle both cases and record in the cache
    # whether it's a promise or not, so that any consumer of the
    # cachedReactive() will be given the correct kind of object (a promise
    # vs. an actual value) in the case of a future cache hit.
    p <- withCallingHandlers(
      withVisible(isolate(valueFunc(...))),
      error = function(e) {
        cache$set(key_str, list(
          is_promise = FALSE,
          value      = e,
          visible    = TRUE,
          error      = TRUE
        ))
      }
    )

    if (is.promising(p$value)) {
      p$value <- as.promise(p$value)
      p$value <- p$value$
        then(function(value) {
          res <- withVisible(value)
          cache$set(key_str, list(
            is_promise = TRUE,
            value      = cacheWriteHook(res$value),
            visible    = res$visible,
            error      = FALSE
          ))
          valueWithVisible(res)
        })$
        catch(function(e) {
          cache$set(key_str, list(
            is_promise = TRUE,
            value      = e,
            visible    = TRUE,
            error      = TRUE
          ))
          stop(e)
        })
      valueWithVisible(p)
    } else {
      # result is an ordinary value, not a promise.
      cache$set(key_str, list(
        is_promise = FALSE,
        value      = cacheWriteHook(p$value),
        visible    = p$visible,
        error      = FALSE
      ))
      return(valueWithVisible(p))
    }
  }
}

extractCacheHint <- function(func) {
  cacheHint <- attr(func, "cacheHint", exact = TRUE)

  if (is_false(cacheHint)) {
    stop(
      "Cannot call `bindCache()` on this object because it is marked as not cacheable.",
      call. = FALSE
    )
  }

  if (is.null(cacheHint)) {
    warning("No cacheHint found for this object. ",
            "Caching may not work properly.")
  }

  cacheHint
}