Files
shiny/R/reactives.R
Joe Cheng 108dd4ff24 Add invalidateLater API call
Provides a simpler mechanism for doing time-based invalidation of reactive functions.
2012-07-13 02:29:03 -07:00

233 lines
5.5 KiB
R

Values <- setRefClass(
'Values',
fields = list(
.values = 'environment',
.dependencies = 'environment'
),
methods = list(
initialize = function() {
.values <<- new.env(parent=emptyenv())
.dependencies <<- new.env(parent=emptyenv())
},
get = function(key) {
ctx <- .getReactiveEnvironment()$currentContext()
dep.key <- paste(key, ':', ctx$id, sep='')
if (!exists(dep.key, where=.dependencies, inherits=F)) {
assign(dep.key, ctx, pos=.dependencies, inherits=F)
ctx$onInvalidate(function() {
rm(list=dep.key, pos=.dependencies, inherits=F)
})
}
if (!exists(key, where=.values, inherits=F))
NULL
else
base::get(key, pos=.values, inherits=F)
},
set = function(key, value) {
if (exists(key, where=.values, inherits=F)) {
if (identical(base::get(key, pos=.values, inherits=F), value)) {
return(invisible())
}
}
assign(key, value, pos=.values, inherits=F)
dep.keys <- objects(
pos=.dependencies,
pattern=paste('^\\Q', key, ':', '\\E', '\\d+$', sep='')
)
lapply(
mget(dep.keys, envir=.dependencies),
function(ctx) {
ctx$invalidate()
NULL
}
)
invisible()
},
mset = function(lst) {
lapply(names(lst),
function(name) {
.self$set(name, lst[[name]])
})
}
)
)
`[.Values` <- function(values, name) {
values$get(name)
}
`[<-.Values` <- function(values, name, value) {
values$set(name, value)
return(values)
}
.createValuesReader <- function(values) {
acc <- list(impl=values)
class(acc) <- 'reactvaluesreader'
return(acc)
}
#' @S3method $ reactvaluesreader
`$.reactvaluesreader` <- function(x, name) {
x[['impl']]$get(name)
}
Observable <- setRefClass(
'Observable',
fields = c(
'.func', # function
'.dependencies', # Map
'.initialized', # logical
'.value' # any
),
methods = list(
initialize = function(func) {
.func <<- func
.dependencies <<- Map$new()
.initialized <<- F
},
getValue = function() {
if (!.initialized) {
.initialized <<- T
.self$.updateValue()
}
ctx <- .getReactiveEnvironment()$currentContext()
if (!.dependencies$containsKey(ctx$id)) {
.dependencies$set(ctx$id, ctx)
ctx$onInvalidate(function() {
.dependencies$remove(ctx$id)
})
}
return(.value)
},
.updateValue = function() {
old.value <- .value
ctx <- Context$new()
ctx$onInvalidate(function() {
.self$.updateValue()
})
ctx$run(function() {
.value <<- .func()
})
if (!identical(old.value, .value)) {
lapply(
.dependencies$values(),
function(dep.ctx) {
dep.ctx$invalidate()
NULL
}
)
}
}
)
)
#' Wraps a normal function to create a reactive function.
#'
#' A reactive function is a function that knows its result will change over
#' time.
#'
#'
#'
#' Reactive values are values that can change over time.
#'
#' Reactive functions are functions that can read reactive values and call other
#' reactive functions. Whenever a reactive value changes, any reactive functions
#' that depended on it will re-execute.
#'
#' @param x The value or function to make reactive.
#'
#' @export
reactive <- function(x) {
UseMethod("reactive")
}
#' @S3method reactive function
reactive.function <- function(x) {
return(Observable$new(x)$getValue)
}
#' @S3method reactive default
reactive.default <- function(x) {
stop("Don't know how to make this value reactive!")
}
Observer <- setRefClass(
'Observer',
fields = list(
.func = 'function'
),
methods = list(
initialize = function(func) {
.func <<- func
.self$run()
},
run = function() {
ctx <- Context$new()
ctx$onInvalidate(function() {
run()
})
ctx$run(.func)
}
)
)
#' Creates a reactive timer with the given interval.
#' @param intervalMs Interval to fire, in milliseconds
#' @export
reactiveTimer <- function(intervalMs=1000) {
dependencies <- Map$new()
timerCallbacks$schedule(intervalMs, function() {
timerCallbacks$schedule(intervalMs, sys.function())
lapply(
dependencies$values(),
function(dep.ctx) {
dep.ctx$invalidate()
NULL
})
})
return(function() {
ctx <- .getReactiveEnvironment()$currentContext()
if (!dependencies$containsKey(ctx$id)) {
dependencies$set(ctx$id, ctx)
ctx$onInvalidate(function() {
dependencies$remove(ctx$id)
})
}
})
}
#' Schedules the current reactive context to be invalidated in the given number
#' of milliseconds.
#' @param millis Approximate milliseconds to wait before invalidating the
#' current reactive context.
#' @export
invalidateLater <- function(millis) {
ctx <- .getReactiveEnvironment()$currentContext()
timerCallbacks$schedule(millis, function() {
ctx$invalidate()
})
}
.test <- function () {
values <- Values$new()
obs <- Observer$new(function() {print(values$get('foo'))})
flushReact()
values$set('foo', 'bar')
flushReact()
values$set('a', 100)
values$set('b', 250)
observable <- Observable$new(function() {
values$get('a') + values$get('b')
})
obs2 <- Observer$new(function() {print(paste0('a+b: ', observable$getValue()))})
flushReact()
values$set('b', 300)
flushReact()
values$mset(list(a = 10, b = 20))
flushReact()
}