Skip to content

Commit be6f671

Browse files
authored
Merge pull request #3915 from rstudio/autoreload-indication
Soften visually jarring greyout when autoreloading
2 parents 80ab088 + 7f59f93 commit be6f671

File tree

11 files changed

+114
-35
lines changed

11 files changed

+114
-35
lines changed

inst/www/shared/shiny-autoreload.js

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

inst/www/shared/shiny-autoreload.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.js

Lines changed: 6 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -17905,15 +17905,16 @@
1790517905
}, _callee2);
1790617906
})));
1790717907
};
17908-
socket.onclose = function() {
17908+
socket.onclose = function(e) {
17909+
var restarting = e.code === 1012;
1790917910
if (hasOpened) {
1791017911
(0, import_jquery38.default)(document).trigger({
1791117912
type: "shiny:disconnected",
1791217913
socket: socket
1791317914
});
1791417915
_this.$notifyDisconnected();
1791517916
}
17916-
_this.onDisconnected();
17917+
_this.onDisconnected(restarting);
1791717918
_this.$removeSocket();
1791817919
};
1791917920
return socket;
@@ -17993,10 +17994,11 @@
1799317994
}, {
1799417995
key: "onDisconnected",
1799517996
value: function onDisconnected() {
17996-
var $overlay = (0, import_jquery38.default)("#shiny-disconnected-overlay");
17997-
if ($overlay.length === 0) {
17997+
var reloading = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : false;
17998+
if ((0, import_jquery38.default)("#shiny-disconnected-overlay").length === 0) {
1799817999
(0, import_jquery38.default)(document.body).append('<div id="shiny-disconnected-overlay"></div>');
1799918000
}
18001+
(0, import_jquery38.default)("#shiny-disconnected-overlay").toggleClass("reloading", reloading);
1800018002
if (this.$allowReconnect === true && this.$socket.allowReconnect === true || this.$allowReconnect === "force") {
1800118003
var delay = this.reconnectDelay.next();
1800218004
showReconnectDialog(delay);

inst/www/shared/shiny.js.map

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

inst/www/shared/shiny.min.css

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

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: 3 additions & 3 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

inst/www/shared/shiny_scss/shiny.scss

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -88,17 +88,25 @@ pre.shiny-text-output {
8888

8989
#shiny-disconnected-overlay {
9090
position: fixed;
91-
top: 0;
92-
bottom: 0;
93-
left: 0;
94-
right: 0;
91+
inset: 0;
9592
background-color: $shiny-disconnected-bg;
9693
opacity: 0.5;
9794
overflow: hidden;
9895
z-index: 99998;
9996
pointer-events: none;
10097
}
10198

99+
html.autoreload-enabled #shiny-disconnected-overlay.reloading {
100+
opacity: 0;
101+
animation: fadeIn 250ms forwards;
102+
animation-delay: 1s;
103+
}
104+
@keyframes fadeIn {
105+
to {
106+
opacity: 0.1;
107+
}
108+
}
109+
102110
.table.shiny-table {
103111
@include table-padding($left: 12px, $right: 12px);
104112
}

srcts/extras/shiny-autoreload.ts

Lines changed: 76 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,18 +1,86 @@
11
/* eslint-disable unicorn/filename-case */
2-
const protocol = (window.location.protocol === "https:") ? "wss:" : "ws:";
2+
3+
document.documentElement.classList.add("autoreload-enabled");
4+
5+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
36
// Add trailing slash to path, if necessary, before appending "autoreload"
4-
const defaultPath = window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
7+
const defaultPath =
8+
window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
59
const defaultUrl = `${protocol}//${window.location.host}${defaultPath}`;
610

711
// By default, use the defaultUrl. But if there's a data-ws-url attribute on our
812
// <script> tag, use that instead.
9-
const wsUrl = document.currentScript.dataset.wsUrl || defaultUrl;
10-
const ws = new WebSocket(wsUrl);
13+
const wsUrl = document.currentScript?.dataset?.wsUrl || defaultUrl;
14+
15+
/**
16+
* Connects to an autoreload URL and waits for the server to tell us what to do.
17+
*
18+
* @param url The ws:// or wss:// URL to connect to.
19+
* @returns true if the server requests a reload, or false if the connection was
20+
* successfully established but then closed without the server requesting a
21+
* reload
22+
* @throws A nondescript error if the connection fails to be established.
23+
*/
24+
async function autoreload(url: string): Promise<boolean> {
25+
const ws = new WebSocket(url);
26+
27+
let success = false;
28+
29+
return new Promise((resolve, reject) => {
30+
ws.onopen = () => {
31+
success = true;
32+
};
1133

12-
ws.onmessage = function (event) {
13-
if (event.data === "autoreload") {
14-
window.location.reload();
34+
ws.onerror = (err) => {
35+
reject(err);
36+
};
37+
38+
ws.onclose = () => {
39+
if (!success) {
40+
reject(new Error("WebSocket connection failed"));
41+
} else {
42+
resolve(false);
43+
}
44+
};
45+
46+
ws.onmessage = function (event) {
47+
if (event.data === "autoreload") {
48+
resolve(true);
49+
}
50+
};
51+
});
52+
}
53+
54+
async function sleep(ms: number) {
55+
return new Promise((resolve) => setTimeout(resolve, ms));
56+
}
57+
58+
async function initialize() {
59+
while (true) {
60+
try {
61+
if (await autoreload(wsUrl)) {
62+
window.location.reload();
63+
return;
64+
}
65+
} catch (err) {
66+
// It's possible for the autoreload() call to throw. If it does, that
67+
// means we tried but failed to connect to the autoreload socket. This
68+
// probably means that the entire `shiny run --reload` process was
69+
// restarted. As of today, the autoreload websocket port number is
70+
// randomly chosen for each `shiny run --reload` process, so it's
71+
// impossible for us to recover.
72+
console.debug("Giving up on autoreload");
73+
return;
74+
}
75+
// If we get here, the connection to the autoreload server was
76+
// successful but then got broken. Wait for a second, and then
77+
// try to re-establish the connection.
78+
await sleep(1000);
1579
}
16-
};
80+
}
81+
82+
initialize().catch((err) => {
83+
console.error(err);
84+
});
1785

1886
export {};

srcts/src/shiny/shinyapp.ts

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -244,7 +244,8 @@ class ShinyApp {
244244
};
245245
// Called when a successfully-opened websocket is closed, or when an
246246
// attempt to open a connection fails.
247-
socket.onclose = () => {
247+
socket.onclose = (e) => {
248+
const restarting = e.code === 1012; // Uvicorn sets this code when autoreloading
248249
// These things are needed only if we've successfully opened the
249250
// websocket.
250251
if (hasOpened) {
@@ -257,7 +258,7 @@ class ShinyApp {
257258
this.$notifyDisconnected();
258259
}
259260

260-
this.onDisconnected(); // Must be run before this.$removeSocket()
261+
this.onDisconnected(restarting); // Must be run before this.$removeSocket()
261262
this.$removeSocket();
262263
};
263264
return socket;
@@ -333,13 +334,12 @@ class ShinyApp {
333334
};
334335
})();
335336

336-
onDisconnected(): void {
337+
onDisconnected(reloading = false): void {
337338
// Add gray-out overlay, if not already present
338-
const $overlay = $("#shiny-disconnected-overlay");
339-
340-
if ($overlay.length === 0) {
339+
if ($("#shiny-disconnected-overlay").length === 0) {
341340
$(document.body).append('<div id="shiny-disconnected-overlay"></div>');
342341
}
342+
$("#shiny-disconnected-overlay").toggleClass("reloading", reloading);
343343

344344
// To try a reconnect, both the app (this.$allowReconnect) and the
345345
// server (this.$socket.allowReconnect) must allow reconnections, or

0 commit comments

Comments
 (0)