Skip to content

Commit 798b336

Browse files
committed
Make autoreload survive laptop suspend
Also simplify the implementation for softening the grey curtain when autoreload is in progress (only applies to Shiny for Python)
1 parent bef6b4b commit 798b336

File tree

11 files changed

+96
-65
lines changed

11 files changed

+96
-65
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -96,7 +96,7 @@ pre.shiny-text-output {
9696
pointer-events: none;
9797
}
9898

99-
.autoreload:not(.autoreload-closed) #shiny-disconnected-overlay {
99+
#shiny-disconnected-overlay.reloading {
100100
opacity: 0;
101101
animation: fadeIn 250ms forwards;
102102
animation-delay: 1s;

srcts/extras/shiny-autoreload.ts

Lines changed: 69 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,56 +1,84 @@
11
/* eslint-disable unicorn/filename-case */
2-
const protocol = (window.location.protocol === "https:") ? "wss:" : "ws:";
2+
3+
const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
34
// Add trailing slash to path, if necessary, before appending "autoreload"
4-
const defaultPath = window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
5+
const defaultPath =
6+
window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
57
const defaultUrl = `${protocol}//${window.location.host}${defaultPath}`;
68

79
// By default, use the defaultUrl. But if there's a data-ws-url attribute on our
810
// <script> tag, use that instead.
9-
const wsUrl = document.currentScript.dataset.wsUrl || defaultUrl;
10-
const ws = new WebSocket(wsUrl);
11-
12-
enum Status {
13-
// Trying to reach the autoreload server
14-
Connecting = "connecting",
15-
// Connected to the autoreload server, just waiting for it to tell us to
16-
// reload
17-
Ready = "ready",
18-
// The server has asked us to reload and we're in the process of doing so
19-
Reloading = "reloading",
20-
// The connection to the server was broken. TODO: should we try reconnecting?
21-
Closed = "closed",
22-
}
11+
const wsUrl = document.currentScript?.dataset?.wsUrl || defaultUrl;
2312

24-
// Put a class on <html> indicating the current state of the autoreload channel
25-
function setAutoreloadStatus(status: Status) {
26-
for (const s of Object.values(Status)) {
27-
if (status === s) {
28-
document.documentElement.classList.add(`autoreload-${s}`);
29-
} else {
30-
document.documentElement.classList.remove(`autoreload-${s}`);
31-
}
32-
}
33-
}
13+
/**
14+
* Connects to an autoreload URL and waits for the server to tell us what to do.
15+
*
16+
* @param url The ws:// or wss:// URL to connect to.
17+
* @returns true if the server requests a reload, or false if the connection was
18+
* successfully established but then closed without the server requesting a
19+
* reload
20+
* @throws A nondescript error if the connection fails to be established.
21+
*/
22+
async function autoreload(url: string): Promise<boolean> {
23+
const ws = new WebSocket(url);
24+
25+
let success = false;
3426

35-
// Also unconditionally add the "autoreload" class to <html>, so it's easy to
36-
// tell whether autoreload is even enabled
37-
document.documentElement.classList.add("autoreload");
27+
return new Promise((resolve, reject) => {
28+
ws.onopen = () => {
29+
success = true;
30+
};
3831

39-
setAutoreloadStatus(Status.Connecting);
32+
ws.onerror = (err) => {
33+
reject(err);
34+
};
4035

41-
ws.onopen = function(event) {
42-
setAutoreloadStatus(Status.Ready);
43-
};
36+
ws.onclose = () => {
37+
if (!success) {
38+
reject(new Error("WebSocket connection failed"));
39+
} else {
40+
resolve(false);
41+
}
42+
};
4443

45-
ws.onclose = function(event) {
46-
setAutoreloadStatus(Status.Closed);
47-
};
44+
ws.onmessage = function (event) {
45+
if (event.data === "autoreload") {
46+
resolve(true);
47+
}
48+
};
49+
});
50+
}
51+
52+
async function sleep(ms: number) {
53+
return new Promise((resolve) => setTimeout(resolve, ms));
54+
}
4855

49-
ws.onmessage = function (event) {
50-
if (event.data === "autoreload") {
51-
window.location.reload();
52-
setAutoreloadStatus(Status.Reloading);
56+
async function initialize() {
57+
while (true) {
58+
try {
59+
if (await autoreload(wsUrl)) {
60+
window.location.reload();
61+
return;
62+
}
63+
} catch (err) {
64+
// It's possible for the autoreload() call to throw. If it does, that
65+
// means we tried but failed to connect to the autoreload socket. This
66+
// probably means that the entire `shiny run --reload` process was
67+
// restarted. As of today, the autoreload websocket port number is
68+
// randomly chosen for each `shiny run --reload` process, so it's
69+
// impossible for us to recover.
70+
console.debug("Giving up on autoreload");
71+
return;
72+
}
73+
// If we get here, the connection to the autoreload server was
74+
// successful but then got broken. Wait for a second, and then
75+
// try to re-establish the connection.
76+
await sleep(1000);
5377
}
54-
};
78+
}
79+
80+
initialize().catch((err) => {
81+
console.error(err);
82+
});
5583

5684
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)