Merge branch 'joe/feature/undedupe-inputs'

This commit is contained in:
Joe Cheng
2018-04-18 19:33:37 -07:00
10 changed files with 136 additions and 87 deletions

View File

@@ -13,6 +13,8 @@ This is a significant release for Shiny, with a major new feature that was nearl
* Support for asynchronous operations! Built-in render functions that expected a certain kind of object to be yielded from their `expr`, now generally can handle a promise for that kind of object. Reactive expressions and observers are now promise-aware as well. ([#1932](https://github.com/rstudio/shiny/pull/1932))
* Introduced two changes to the (undocumented but widely used) JavaScript function `Shiny.onInputChange(name, value)`. First, we changed the function name to `Shiny.setInputValue` (but don't worry--the old function name will continue to work). Second, until now, all calls to `Shiny.onInputChange(inputId, value)` have been "deduplicated"; that is, anytime an input is set to the same value it already has, the set is ignored. With Shiny v1.1, you can now add an options object as the third parameter: `Shiny.setInputValue("name", value, {priority: "event"})`. When the priority option is set to `"event"`, Shiny will always send the value and trigger reactivity, whether it is a duplicate or not. This closes [#928](https://github.com/rstudio/shiny/issues/928), which was the most upvoted open issue by far! Thanks, @daattali. ([#2018](https://github.com/rstudio/shiny/pull/2018))
### Minor new features and improvements
* Addressed [#1978](https://github.com/rstudio/shiny/issues/1978): `shiny:value` is now triggered when duplicate output data is received from the server. (Thanks, @andrewsali! [#1999](https://github.com/rstudio/shiny/pull/1999))

View File

@@ -278,8 +278,9 @@ ReactiveValues <- R6Class(
.allValuesDeps = 'Dependents',
# Dependents for all values
.valuesDeps = 'Dependents',
.dedupe = logical(0),
initialize = function() {
initialize = function(dedupe = TRUE) {
.label <<- paste('reactiveValues',
p_randomInt(1000, 10000),
sep="")
@@ -289,6 +290,7 @@ ReactiveValues <- R6Class(
.namesDeps <<- Dependents$new()
.allValuesDeps <<- Dependents$new()
.valuesDeps <<- Dependents$new()
.dedupe <<- dedupe
},
get = function(key) {
@@ -317,7 +319,7 @@ ReactiveValues <- R6Class(
hidden <- substr(key, 1, 1) == "."
if (exists(key, envir=.values, inherits=FALSE)) {
if (identical(.values[[key]], value)) {
if (.dedupe && identical(.values[[key]], value)) {
return(invisible())
}
}

View File

@@ -706,8 +706,8 @@ ShinySession <- R6Class(
private$flushCallbacks <- Callbacks$new()
private$flushedCallbacks <- Callbacks$new()
private$inputReceivedCallbacks <- Callbacks$new()
private$.input <- ReactiveValues$new()
private$.clientData <- ReactiveValues$new()
private$.input <- ReactiveValues$new(dedupe = FALSE)
private$.clientData <- ReactiveValues$new(dedupe = TRUE)
private$timingRecorder <- ShinyServerTimingRecorder$new()
self$progressStack <- Stack$new()
self$files <- Map$new()

View File

@@ -762,26 +762,34 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastChanceCallback = [];
};
(function () {
this.setInput = function (name, value) {
var self = this;
this.setInput = function (name, value, opts) {
this.pendingData[name] = value;
if (!this.timerId && !this.reentrant) {
this.timerId = setTimeout(function () {
self.reentrant = true;
try {
$.each(self.lastChanceCallback, function (i, callback) {
callback();
});
self.timerId = null;
var currentData = self.pendingData;
self.pendingData = {};
self.shinyapp.sendInput(currentData);
} finally {
self.reentrant = false;
}
}, 0);
if (!this.reentrant) {
if (opts.priority === "event") {
this.$sendNow();
} else if (!this.timerId) {
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
}
}
};
this.$sendNow = function () {
if (this.reentrant) {
console.trace("Unexpected reentrancy in InputBatchSender!");
}
this.reentrant = true;
try {
this.timerId = null;
$.each(this.lastChanceCallback, function (i, callback) {
callback();
});
var currentData = this.pendingData;
this.pendingData = {};
this.shinyapp.sendInput(currentData);
} finally {
this.reentrant = false;
}
};
}).call(InputBatchSender.prototype);
@@ -791,11 +799,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.lastSentValues = this.reset(initialValues);
};
(function () {
this.setInput = function (name, value) {
// Note that opts is not passed to setInput at this stage of the input
// decorator stack. If in the future this setInput keeps track of opts, it
// would be best not to store the `el`, because that could prevent it from
// being GC'd.
this.setInput = function (name, value, opts) {
var _splitInputNameType = splitInputNameType(name);
var inputName = _splitInputNameType.name;
@@ -803,11 +807,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var jsonValue = JSON.stringify(value);
if (this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
if (opts.priority !== "event" && this.lastSentValues[inputName] && this.lastSentValues[inputName].jsonValue === jsonValue && this.lastSentValues[inputName].inputType === inputType) {
return;
}
this.lastSentValues[inputName] = { jsonValue: jsonValue, inputType: inputType };
this.target.setInput(name, value);
this.target.setInput(name, value, opts);
};
this.reset = function () {
var values = arguments.length > 0 && arguments[0] !== undefined ? arguments[0] : {};
@@ -850,6 +854,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
evt.value = value;
evt.binding = opts.binding;
evt.el = opts.el;
evt.priority = opts.priority;
$(document).trigger(evt);
@@ -857,9 +862,9 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
name = evt.name;
if (evt.inputType !== '') name += ':' + evt.inputType;
// opts aren't passed along to lower levels in the input decorator
// Most opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value);
this.target.setInput(name, evt.value, { priority: opts.priority });
}
};
}).call(InputEventDecorator.prototype);
@@ -872,7 +877,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
this.setInput = function (name, value, opts) {
this.$ensureInit(name);
if (opts.immediate) this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
if (opts.priority !== "deferred") this.inputRatePolicies[name].immediateCall(name, value, opts);else this.inputRatePolicies[name].normalCall(name, value, opts);
};
this.setRatePolicy = function (name, mode, millis) {
if (mode === 'direct') {
@@ -924,11 +929,25 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// Merge opts with defaults, and return a new object.
function addDefaultInputOpts(opts) {
return $.extend({
immediate: false,
opts = $.extend({
priority: "immediate",
binding: null,
el: null
}, opts);
if (opts && typeof opts.priority !== "undefined") {
switch (opts.priority) {
case "deferred":
case "immediate":
case "event":
break;
default:
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
}
}
return opts;
}
function splitInputNameType(name) {
@@ -2968,7 +2987,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
return function (e) {
if (e === null) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
return;
}
@@ -2976,7 +2995,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// If outside of plotting region
if (!coordmap.isInPanel(offset)) {
if (nullOutside) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
return;
}
if (clip) return;
@@ -2997,8 +3016,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
coords.range = panel.range;
coords.log = panel.log;
coords[".nonce"] = Math.random();
exports.onInputChange(inputId, coords);
exports.setInputValue(inputId, coords, { priority: "event" });
};
};
};
@@ -3185,7 +3203,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
@@ -3212,7 +3230,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
coords.outputId = outputId;
// Send data to server
exports.onInputChange(inputId, coords);
exports.setInputValue(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
@@ -3846,7 +3864,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
};
exports.resetBrush = function (brushId) {
exports.onInputChange(brushId, null);
exports.setInputValue(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});
@@ -6072,7 +6090,7 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
inputs = new InputValidateDecorator(inputs);
exports.onInputChange = function (name, value, opts) {
exports.setInputValue = exports.onInputChange = function (name, value, opts) {
opts = addDefaultInputOpts(opts);
inputs.setInput(name, value, opts);
};
@@ -6086,7 +6104,11 @@ function _defineProperty(obj, key, value) { if (key in obj) { Object.definePrope
var type = binding.getType(el);
if (type) id = id + ":" + type;
var opts = { immediate: !allowDeferred, binding: binding, el: el };
var opts = {
priority: allowDeferred ? "deferred" : "immediate",
binding: binding,
el: el
};
inputs.setInput(id, value, opts);
}
}

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@@ -101,7 +101,7 @@ function initShiny() {
inputs = new InputValidateDecorator(inputs);
exports.onInputChange = function(name, value, opts) {
exports.setInputValue = exports.onInputChange = function(name, value, opts) {
opts = addDefaultInputOpts(opts);
inputs.setInput(name, value, opts);
};
@@ -116,7 +116,11 @@ function initShiny() {
if (type)
id = id + ":" + type;
let opts = { immediate: !allowDeferred, binding: binding, el: el };
let opts = {
priority: allowDeferred ? "deferred" : "immediate",
binding: binding,
el: el
};
inputs.setInput(id, value, opts);
}
}

View File

@@ -189,26 +189,34 @@ var InputBatchSender = function(shinyapp) {
this.lastChanceCallback = [];
};
(function() {
this.setInput = function(name, value) {
var self = this;
this.setInput = function(name, value, opts) {
this.pendingData[name] = value;
if (!this.timerId && !this.reentrant) {
this.timerId = setTimeout(function() {
self.reentrant = true;
try {
$.each(self.lastChanceCallback, function(i, callback) {
callback();
});
self.timerId = null;
var currentData = self.pendingData;
self.pendingData = {};
self.shinyapp.sendInput(currentData);
} finally {
self.reentrant = false;
}
}, 0);
if (!this.reentrant) {
if (opts.priority === "event") {
this.$sendNow();
} else if (!this.timerId) {
this.timerId = setTimeout(this.$sendNow.bind(this), 0);
}
}
};
this.$sendNow = function() {
if (this.reentrant) {
console.trace("Unexpected reentrancy in InputBatchSender!");
}
this.reentrant = true;
try {
this.timerId = null;
$.each(this.lastChanceCallback, (i, callback) => {
callback();
});
var currentData = this.pendingData;
this.pendingData = {};
this.shinyapp.sendInput(currentData);
} finally {
this.reentrant = false;
}
};
}).call(InputBatchSender.prototype);
@@ -219,21 +227,18 @@ var InputNoResendDecorator = function(target, initialValues) {
this.lastSentValues = this.reset(initialValues);
};
(function() {
this.setInput = function(name, value) {
// Note that opts is not passed to setInput at this stage of the input
// decorator stack. If in the future this setInput keeps track of opts, it
// would be best not to store the `el`, because that could prevent it from
// being GC'd.
this.setInput = function(name, value, opts) {
const { name: inputName, inputType: inputType } = splitInputNameType(name);
const jsonValue = JSON.stringify(value);
if (this.lastSentValues[inputName] &&
if (opts.priority !== "event" &&
this.lastSentValues[inputName] &&
this.lastSentValues[inputName].jsonValue === jsonValue &&
this.lastSentValues[inputName].inputType === inputType) {
return;
}
this.lastSentValues[inputName] = { jsonValue, inputType };
this.target.setInput(name, value);
this.target.setInput(name, value, opts);
};
this.reset = function(values = {}) {
// Given an object with flat name-value format:
@@ -271,6 +276,7 @@ var InputEventDecorator = function(target) {
evt.value = value;
evt.binding = opts.binding;
evt.el = opts.el;
evt.priority = opts.priority;
$(document).trigger(evt);
@@ -278,9 +284,9 @@ var InputEventDecorator = function(target) {
name = evt.name;
if (evt.inputType !== '') name += ':' + evt.inputType;
// opts aren't passed along to lower levels in the input decorator
// Most opts aren't passed along to lower levels in the input decorator
// stack.
this.target.setInput(name, evt.value);
this.target.setInput(name, evt.value, { priority: opts.priority });
}
};
}).call(InputEventDecorator.prototype);
@@ -294,7 +300,7 @@ var InputRateDecorator = function(target) {
this.setInput = function(name, value, opts) {
this.$ensureInit(name);
if (opts.immediate)
if (opts.priority !== "deferred")
this.inputRatePolicies[name].immediateCall(name, value, opts);
else
this.inputRatePolicies[name].normalCall(name, value, opts);
@@ -359,11 +365,25 @@ const InputValidateDecorator = function(target) {
// Merge opts with defaults, and return a new object.
function addDefaultInputOpts(opts) {
return $.extend({
immediate: false,
opts = $.extend({
priority: "immediate",
binding: null,
el: null
}, opts);
if (opts && typeof(opts.priority) !== "undefined") {
switch (opts.priority) {
case "deferred":
case "immediate":
case "event":
break;
default:
throw new Error("Unexpected input value mode: '" + opts.priority + "'");
}
}
return opts;
}

View File

@@ -436,7 +436,7 @@ imageutils.initCoordmap = function($el, coordmap) {
return function(e) {
if (e === null) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
return;
}
@@ -444,7 +444,7 @@ imageutils.initCoordmap = function($el, coordmap) {
// If outside of plotting region
if (!coordmap.isInPanel(offset)) {
if (nullOutside) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
return;
}
if (clip)
@@ -466,8 +466,7 @@ imageutils.initCoordmap = function($el, coordmap) {
coords.range = panel.range;
coords.log = panel.log;
coords[".nonce"] = Math.random();
exports.onInputChange(inputId, coords);
exports.setInputValue(inputId, coords, {priority: "event"});
};
};
};
@@ -662,7 +661,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
// We're in a new or reset state
if (isNaN(coords.xmin)) {
exports.onInputChange(inputId, null);
exports.setInputValue(inputId, null);
// Must tell other brushes to clear.
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: inputId, outputId: null
@@ -689,7 +688,7 @@ imageutils.createBrushHandler = function(inputId, $el, opts, coordmap, outputId)
coords.outputId = outputId;
// Send data to server
exports.onInputChange(inputId, coords);
exports.setInputValue(inputId, coords);
$el.data("mostRecentBrush", true);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", coords);
@@ -1373,7 +1372,7 @@ imageutils.createBrush = function($el, opts, coordmap, expandPixels) {
};
exports.resetBrush = function(brushId) {
exports.onInputChange(brushId, null);
exports.setInputValue(brushId, null);
imageOutputBinding.find(document).trigger("shiny-internal:brushed", {
brushId: brushId, outputId: null
});