Skip to content

Commit 1f23f37

Browse files
committed
Allow outputs to stay in progress mode after flush
Adds a req(FALSE, cancelOutput="progress") which behaves similarly to cancelOutput=TRUE, but also keeps the output in .recalculating state even across flush cycles. This is called "persistent progress" and an output can leave this state when it is invalidated again and doesn't call req(FALSE, cancelOutput="progress") during that flush cycle. This will be useful for implementing long-running tasks that don't hold up the flush cycle, leaving sessions responsive to do other tasks.
1 parent 59b1c46 commit 1f23f37

File tree

9 files changed

+133
-52
lines changed

9 files changed

+133
-52
lines changed

R/shiny.R

Lines changed: 39 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -1149,6 +1149,8 @@ ShinySession <- R6Class(
11491149
structure(list(), class = "try-error", condition = cond)
11501150
} else if (inherits(cond, "shiny.output.cancel")) {
11511151
structure(list(), class = "cancel-output")
1152+
} else if (inherits(cond, "shiny.output.progress")) {
1153+
structure(list(), class = "progress-output")
11521154
} else if (cnd_inherits(cond, "shiny.silent.error")) {
11531155
# The error condition might have been chained by
11541156
# foreign code, e.g. dplyr. Find the original error.
@@ -1177,6 +1179,33 @@ ShinySession <- R6Class(
11771179
# client knows that progress is over.
11781180
self$requestFlush()
11791181

1182+
if (inherits(value, "progress-output")) {
1183+
# This is the case where an output needs to compute for longer
1184+
# than this reactive flush. We put the output into progress mode
1185+
# (i.e. adding .recalculating) with a special flag that means
1186+
# the progress indication should not be cleared until this
1187+
# specific output receives a new value or error.
1188+
self$showProgress(name, persistent=TRUE)
1189+
1190+
# It's conceivable that this output already ran successfully
1191+
# within this reactive flush, in which case we could either show
1192+
# the new output while simultaneously making it .recalculating;
1193+
# or we squelch the new output and make whatever output is in
1194+
# the client .recalculating. I (jcheng) decided on the latter as
1195+
# it seems more in keeping with what we do with these kinds of
1196+
# intermediate output values/errors in general, i.e. ignore them
1197+
# and wait until we have a final answer. (Also kind of feels
1198+
# like a bug in the app code if you routinely have outputs that
1199+
# are executing successfully, only to be invalidated again
1200+
# within the same reactive flush--use priority to fix that.)
1201+
private$invalidatedOutputErrors$remove(name)
1202+
private$invalidatedOutputValues$remove(name)
1203+
1204+
# It's important that we return so that the existing output in
1205+
# the client remains untouched.
1206+
return()
1207+
}
1208+
11801209
private$sendMessage(recalculating = list(
11811210
name = name, status = 'recalculated'
11821211
))
@@ -1309,23 +1338,27 @@ ShinySession <- R6Class(
13091338
private$startCycle()
13101339
}
13111340
},
1312-
showProgress = function(id) {
1341+
showProgress = function(id, persistent=FALSE) {
13131342
'Send a message to the client that recalculation of the output identified
13141343
by \\code{id} is in progress. There is currently no mechanism for
13151344
explicitly turning off progress for an output component; instead, all
1316-
progress is implicitly turned off when flushOutput is next called.'
1345+
progress is implicitly turned off when flushOutput is next called.
1346+
1347+
You can use persistent=TRUE if the progress for this output component
1348+
should stay on beyond the flushOutput (or any subsequent flushOutputs); in
1349+
that case, progress is only turned off (and the persistent flag cleared)
1350+
when the output component receives a value or error, or, if
1351+
showProgress(id, persistent=FALSE) is called and a subsequent flushOutput
1352+
occurs.'
13171353

13181354
# If app is already closed, be sure not to show progress, otherwise we
13191355
# will get an error because of the closed websocket
13201356
if (self$closed)
13211357
return()
13221358

1323-
if (id %in% private$progressKeys)
1324-
return()
1325-
13261359
private$progressKeys <- c(private$progressKeys, id)
13271360

1328-
self$sendProgress('binding', list(id = id))
1361+
self$sendProgress('binding', list(id = id, persistent = persistent))
13291362
},
13301363
sendProgress = function(type, message) {
13311364
private$sendMessage(

R/utils.R

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1115,7 +1115,10 @@ need <- function(expr, message = paste(label, "must be provided"), label) {
11151115
#' @param ... Values to check for truthiness.
11161116
#' @param cancelOutput If `TRUE` and an output is being evaluated, stop
11171117
#' processing as usual but instead of clearing the output, leave it in
1118-
#' whatever state it happens to be in.
1118+
#' whatever state it happens to be in. If `"progress"`, do the same as `TRUE`,
1119+
#' but also keep the output in recalculating state; this is intended for cases
1120+
#' when an in-progress calculation will not be completed in this reactive
1121+
#' flush cycle, but is still expected to provide a result in the future.
11191122
#' @return The first value that was passed in.
11201123
#' @export
11211124
#' @examples
@@ -1147,6 +1150,8 @@ req <- function(..., cancelOutput = FALSE) {
11471150
if (!isTruthy(item)) {
11481151
if (isTRUE(cancelOutput)) {
11491152
cancelOutput()
1153+
} else if (identical(cancelOutput, "progress")) {
1154+
reactiveStop(class = "shiny.output.progress")
11501155
} else {
11511156
reactiveStop(class = "validation")
11521157
}

inst/www/shared/shiny.js

Lines changed: 46 additions & 29 deletions
Original file line numberDiff line numberDiff line change
@@ -4869,6 +4869,20 @@
48694869
}
48704870
});
48714871

4872+
// node_modules/core-js/modules/es.set.constructor.js
4873+
var require_es_set_constructor = __commonJS({
4874+
"node_modules/core-js/modules/es.set.constructor.js": function() {
4875+
"use strict";
4876+
var collection = require_collection();
4877+
var collectionStrong = require_collection_strong();
4878+
collection("Set", function(init2) {
4879+
return function Set2() {
4880+
return init2(this, arguments.length ? arguments[0] : void 0);
4881+
};
4882+
}, collectionStrong);
4883+
}
4884+
});
4885+
48724886
// node_modules/core-js/internals/array-buffer-basic-detection.js
48734887
var require_array_buffer_basic_detection = __commonJS({
48744888
"node_modules/core-js/internals/array-buffer-basic-detection.js": function(exports, module) {
@@ -5559,20 +5573,6 @@
55595573
}
55605574
});
55615575

5562-
// node_modules/core-js/modules/es.set.constructor.js
5563-
var require_es_set_constructor = __commonJS({
5564-
"node_modules/core-js/modules/es.set.constructor.js": function() {
5565-
"use strict";
5566-
var collection = require_collection();
5567-
var collectionStrong = require_collection_strong();
5568-
collection("Set", function(init2) {
5569-
return function Set2() {
5570-
return init2(this, arguments.length ? arguments[0] : void 0);
5571-
};
5572-
}, collectionStrong);
5573-
}
5574-
});
5575-
55765576
// node_modules/core-js/internals/flatten-into-array.js
55775577
var require_flatten_into_array = __commonJS({
55785578
"node_modules/core-js/internals/flatten-into-array.js": function(exports, module) {
@@ -18882,6 +18882,12 @@
1888218882
return _bindAll3.apply(this, arguments);
1888318883
}
1888418884

18885+
// srcts/src/shiny/shinyapp.ts
18886+
var import_es_array_iterator49 = __toESM(require_es_array_iterator());
18887+
18888+
// node_modules/core-js/modules/es.set.js
18889+
require_es_set_constructor();
18890+
1888518891
// srcts/src/shiny/shinyapp.ts
1888618892
var import_es_regexp_exec15 = __toESM(require_es_regexp_exec());
1888718893
var import_es_json_stringify4 = __toESM(require_es_json_stringify());
@@ -18955,7 +18961,6 @@
1895518961
});
1895618962

1895718963
// srcts/src/shiny/shinyapp.ts
18958-
var import_es_array_iterator49 = __toESM(require_es_array_iterator());
1895918964
var import_jquery38 = __toESM(require_jquery());
1896018965

1896118966
// srcts/src/utils/asyncQueue.ts
@@ -19433,9 +19438,6 @@
1943319438
// node_modules/core-js/modules/es.weak-map.js
1943419439
require_es_weak_map_constructor();
1943519440

19436-
// node_modules/core-js/modules/es.set.js
19437-
require_es_set_constructor();
19438-
1943919441
// node_modules/core-js/modules/es.array.flat.js
1944019442
var $77 = require_export();
1944119443
var flattenIntoArray = require_flatten_into_array();
@@ -22823,6 +22825,7 @@
2282322825
_defineProperty20(this, "$inputValues", {});
2282422826
_defineProperty20(this, "$initialInput", null);
2282522827
_defineProperty20(this, "$bindings", {});
22828+
_defineProperty20(this, "$persistentProgress", /* @__PURE__ */ new Set());
2282622829
_defineProperty20(this, "$values", {});
2282722830
_defineProperty20(this, "$errors", {});
2282822831
_defineProperty20(this, "$conditionals", {});
@@ -22860,6 +22863,11 @@
2286022863
});
2286122864
if (binding2.showProgress)
2286222865
binding2.showProgress(true);
22866+
if (message.persistent) {
22867+
this.$persistentProgress.add(key);
22868+
} else {
22869+
this.$persistentProgress.delete(key);
22870+
}
2286322871
}
2286422872
},
2286522873
open: function() {
@@ -23459,38 +23467,45 @@
2345923467
}
2346023468
return _sendMessagesToHandlers;
2346123469
}()
23470+
}, {
23471+
key: "_clearProgress",
23472+
value: function _clearProgress() {
23473+
for (var name in this.$bindings) {
23474+
if (hasOwnProperty(this.$bindings, name) && !this.$persistentProgress.has(name)) {
23475+
this.$bindings[name].showProgress(false);
23476+
}
23477+
}
23478+
}
2346223479
}, {
2346323480
key: "_init",
2346423481
value: function _init() {
2346523482
var _this3 = this;
2346623483
addMessageHandler("values", /* @__PURE__ */ function() {
2346723484
var _ref3 = _asyncToGenerator13(/* @__PURE__ */ _regeneratorRuntime13().mark(function _callee8(message) {
23468-
var name, _key;
23485+
var _key;
2346923486
return _regeneratorRuntime13().wrap(function _callee8$(_context8) {
2347023487
while (1)
2347123488
switch (_context8.prev = _context8.next) {
2347223489
case 0:
23473-
for (name in _this3.$bindings) {
23474-
if (hasOwnProperty(_this3.$bindings, name))
23475-
_this3.$bindings[name].showProgress(false);
23476-
}
23490+
_this3._clearProgress();
2347723491
_context8.t0 = _regeneratorRuntime13().keys(message);
2347823492
case 2:
2347923493
if ((_context8.t1 = _context8.t0()).done) {
23480-
_context8.next = 9;
23494+
_context8.next = 10;
2348123495
break;
2348223496
}
2348323497
_key = _context8.t1.value;
2348423498
if (!hasOwnProperty(message, _key)) {
23485-
_context8.next = 7;
23499+
_context8.next = 8;
2348623500
break;
2348723501
}
23488-
_context8.next = 7;
23502+
_this3.$persistentProgress.delete(_key);
23503+
_context8.next = 8;
2348923504
return _this3.receiveOutput(_key, message[_key]);
23490-
case 7:
23505+
case 8:
2349123506
_context8.next = 2;
2349223507
break;
23493-
case 9:
23508+
case 10:
2349423509
case "end":
2349523510
return _context8.stop();
2349623511
}
@@ -23502,8 +23517,10 @@
2350223517
}());
2350323518
addMessageHandler("errors", function(message) {
2350423519
for (var _key2 in message) {
23505-
if (hasOwnProperty(message, _key2))
23520+
if (hasOwnProperty(message, _key2)) {
23521+
_this3.$persistentProgress.delete(_key2);
2350623522
_this3.receiveError(_key2, message[_key2]);
23523+
}
2350723524
}
2350823525
});
2350923526
addMessageHandler("inputMessages", /* @__PURE__ */ function() {

inst/www/shared/shiny.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/www/shared/shiny.min.js

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/www/shared/shiny.min.js.map

Lines changed: 4 additions & 4 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

man/req.Rd

Lines changed: 4 additions & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

srcts/src/shiny/shinyapp.ts

Lines changed: 26 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -130,6 +130,7 @@ class ShinyApp {
130130

131131
// Output bindings
132132
$bindings: { [key: string]: OutputBindingAdapter } = {};
133+
$persistentProgress: Set<string> = new Set();
133134

134135
// Cached values/errors
135136
$values: { [key: string]: any } = {};
@@ -688,19 +689,28 @@ class ShinyApp {
688689
}
689690
}
690691

692+
private _clearProgress() {
693+
for (const name in this.$bindings) {
694+
if (
695+
hasOwnProperty(this.$bindings, name) &&
696+
!this.$persistentProgress.has(name)
697+
) {
698+
this.$bindings[name].showProgress(false);
699+
}
700+
}
701+
}
702+
691703
private _init() {
692704
// Dev note:
693705
// * Use arrow functions to allow the Types to propagate.
694706
// * However, `_sendMessagesToHandlers()` will adjust the `this` context to the same _`this`_.
695707

696708
addMessageHandler("values", async (message: { [key: string]: any }) => {
697-
for (const name in this.$bindings) {
698-
if (hasOwnProperty(this.$bindings, name))
699-
this.$bindings[name].showProgress(false);
700-
}
709+
this._clearProgress();
701710

702711
for (const key in message) {
703712
if (hasOwnProperty(message, key)) {
713+
this.$persistentProgress.delete(key);
704714
await this.receiveOutput(key, message[key]);
705715
}
706716
}
@@ -710,8 +720,10 @@ class ShinyApp {
710720
"errors",
711721
(message: { [key: string]: ErrorsMessageValue }) => {
712722
for (const key in message) {
713-
if (hasOwnProperty(message, key))
723+
if (hasOwnProperty(message, key)) {
724+
this.$persistentProgress.delete(key);
714725
this.receiveError(key, message[key]);
726+
}
715727
}
716728
}
717729
);
@@ -1401,7 +1413,10 @@ class ShinyApp {
14011413

14021414
progressHandlers = {
14031415
// Progress for a particular object
1404-
binding: function (this: ShinyApp, message: { id: string }): void {
1416+
binding: function (
1417+
this: ShinyApp,
1418+
message: { id: string; persistent: boolean }
1419+
): void {
14051420
const key = message.id;
14061421
const binding = this.$bindings[key];
14071422

@@ -1413,6 +1428,11 @@ class ShinyApp {
14131428
name: key,
14141429
});
14151430
if (binding.showProgress) binding.showProgress(true);
1431+
if (message.persistent) {
1432+
this.$persistentProgress.add(key);
1433+
} else {
1434+
this.$persistentProgress.delete(key);
1435+
}
14161436
}
14171437
},
14181438

srcts/types/src/shiny/shinyapp.d.ts

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -30,6 +30,7 @@ declare class ShinyApp {
3030
$bindings: {
3131
[key: string]: OutputBindingAdapter;
3232
};
33+
$persistentProgress: Set<string>;
3334
$values: {
3435
[key: string]: any;
3536
};
@@ -74,10 +75,12 @@ declare class ShinyApp {
7475
$updateConditionals(): void;
7576
dispatchMessage(data: ArrayBufferLike | string): Promise<void>;
7677
private _sendMessagesToHandlers;
78+
private _clearProgress;
7779
private _init;
7880
progressHandlers: {
7981
binding: (this: ShinyApp, message: {
8082
id: string;
83+
persistent: boolean;
8184
}) => void;
8285
open: (message: {
8386
style: "notification" | "old";

0 commit comments

Comments
 (0)