Skip to content

Commit

Permalink
Merge pull request #3915 from rstudio/autoreload-indication
Browse files Browse the repository at this point in the history
Soften visually jarring greyout when autoreloading
  • Loading branch information
jcheng5 authored Oct 18, 2023
2 parents 80ab088 + 7f59f93 commit be6f671
Show file tree
Hide file tree
Showing 11 changed files with 114 additions and 35 deletions.
3 changes: 2 additions & 1 deletion inst/www/shared/shiny-autoreload.js

Large diffs are not rendered by default.

8 changes: 4 additions & 4 deletions inst/www/shared/shiny-autoreload.js.map

Large diffs are not rendered by default.

10 changes: 6 additions & 4 deletions inst/www/shared/shiny.js
Original file line number Diff line number Diff line change
Expand Up @@ -17905,15 +17905,16 @@
}, _callee2);
})));
};
socket.onclose = function() {
socket.onclose = function(e) {
var restarting = e.code === 1012;
if (hasOpened) {
(0, import_jquery38.default)(document).trigger({
type: "shiny:disconnected",
socket: socket
});
_this.$notifyDisconnected();
}
_this.onDisconnected();
_this.onDisconnected(restarting);
_this.$removeSocket();
};
return socket;
Expand Down Expand Up @@ -17993,10 +17994,11 @@
}, {
key: "onDisconnected",
value: function onDisconnected() {
var $overlay = (0, import_jquery38.default)("#shiny-disconnected-overlay");
if ($overlay.length === 0) {
var reloading = arguments.length > 0 && arguments[0] !== void 0 ? arguments[0] : false;
if ((0, import_jquery38.default)("#shiny-disconnected-overlay").length === 0) {
(0, import_jquery38.default)(document.body).append('<div id="shiny-disconnected-overlay"></div>');
}
(0, import_jquery38.default)("#shiny-disconnected-overlay").toggleClass("reloading", reloading);
if (this.$allowReconnect === true && this.$socket.allowReconnect === true || this.$allowReconnect === "force") {
var delay = this.reconnectDelay.next();
showReconnectDialog(delay);
Expand Down
4 changes: 2 additions & 2 deletions inst/www/shared/shiny.js.map

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion inst/www/shared/shiny.min.css

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion inst/www/shared/shiny.min.js

Large diffs are not rendered by default.

6 changes: 3 additions & 3 deletions inst/www/shared/shiny.min.js.map

Large diffs are not rendered by default.

16 changes: 12 additions & 4 deletions inst/www/shared/shiny_scss/shiny.scss
Original file line number Diff line number Diff line change
Expand Up @@ -88,17 +88,25 @@ pre.shiny-text-output {

#shiny-disconnected-overlay {
position: fixed;
top: 0;
bottom: 0;
left: 0;
right: 0;
inset: 0;
background-color: $shiny-disconnected-bg;
opacity: 0.5;
overflow: hidden;
z-index: 99998;
pointer-events: none;
}

html.autoreload-enabled #shiny-disconnected-overlay.reloading {
opacity: 0;
animation: fadeIn 250ms forwards;
animation-delay: 1s;
}
@keyframes fadeIn {
to {
opacity: 0.1;
}
}

.table.shiny-table {
@include table-padding($left: 12px, $right: 12px);
}
Expand Down
84 changes: 76 additions & 8 deletions srcts/extras/shiny-autoreload.ts
Original file line number Diff line number Diff line change
@@ -1,18 +1,86 @@
/* eslint-disable unicorn/filename-case */
const protocol = (window.location.protocol === "https:") ? "wss:" : "ws:";

document.documentElement.classList.add("autoreload-enabled");

const protocol = window.location.protocol === "https:" ? "wss:" : "ws:";
// Add trailing slash to path, if necessary, before appending "autoreload"
const defaultPath = window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
const defaultPath =
window.location.pathname.replace(/\/?$/, "/") + "autoreload/";
const defaultUrl = `${protocol}//${window.location.host}${defaultPath}`;

// By default, use the defaultUrl. But if there's a data-ws-url attribute on our
// <script> tag, use that instead.
const wsUrl = document.currentScript.dataset.wsUrl || defaultUrl;
const ws = new WebSocket(wsUrl);
const wsUrl = document.currentScript?.dataset?.wsUrl || defaultUrl;

/**
* Connects to an autoreload URL and waits for the server to tell us what to do.
*
* @param url The ws:// or wss:// URL to connect to.
* @returns true if the server requests a reload, or false if the connection was
* successfully established but then closed without the server requesting a
* reload
* @throws A nondescript error if the connection fails to be established.
*/
async function autoreload(url: string): Promise<boolean> {
const ws = new WebSocket(url);

let success = false;

return new Promise((resolve, reject) => {
ws.onopen = () => {
success = true;
};

ws.onmessage = function (event) {
if (event.data === "autoreload") {
window.location.reload();
ws.onerror = (err) => {
reject(err);
};

ws.onclose = () => {
if (!success) {
reject(new Error("WebSocket connection failed"));
} else {
resolve(false);
}
};

ws.onmessage = function (event) {
if (event.data === "autoreload") {
resolve(true);
}
};
});
}

async function sleep(ms: number) {
return new Promise((resolve) => setTimeout(resolve, ms));
}

async function initialize() {
while (true) {
try {
if (await autoreload(wsUrl)) {
window.location.reload();
return;
}
} catch (err) {
// It's possible for the autoreload() call to throw. If it does, that
// means we tried but failed to connect to the autoreload socket. This
// probably means that the entire `shiny run --reload` process was
// restarted. As of today, the autoreload websocket port number is
// randomly chosen for each `shiny run --reload` process, so it's
// impossible for us to recover.
console.debug("Giving up on autoreload");
return;
}
// If we get here, the connection to the autoreload server was
// successful but then got broken. Wait for a second, and then
// try to re-establish the connection.
await sleep(1000);
}
};
}

initialize().catch((err) => {
console.error(err);
});

export {};
12 changes: 6 additions & 6 deletions srcts/src/shiny/shinyapp.ts
Original file line number Diff line number Diff line change
Expand Up @@ -244,7 +244,8 @@ class ShinyApp {
};
// Called when a successfully-opened websocket is closed, or when an
// attempt to open a connection fails.
socket.onclose = () => {
socket.onclose = (e) => {
const restarting = e.code === 1012; // Uvicorn sets this code when autoreloading
// These things are needed only if we've successfully opened the
// websocket.
if (hasOpened) {
Expand All @@ -257,7 +258,7 @@ class ShinyApp {
this.$notifyDisconnected();
}

this.onDisconnected(); // Must be run before this.$removeSocket()
this.onDisconnected(restarting); // Must be run before this.$removeSocket()
this.$removeSocket();
};
return socket;
Expand Down Expand Up @@ -333,13 +334,12 @@ class ShinyApp {
};
})();

onDisconnected(): void {
onDisconnected(reloading = false): void {
// Add gray-out overlay, if not already present
const $overlay = $("#shiny-disconnected-overlay");

if ($overlay.length === 0) {
if ($("#shiny-disconnected-overlay").length === 0) {
$(document.body).append('<div id="shiny-disconnected-overlay"></div>');
}
$("#shiny-disconnected-overlay").toggleClass("reloading", reloading);

// To try a reconnect, both the app (this.$allowReconnect) and the
// server (this.$socket.allowReconnect) must allow reconnections, or
Expand Down
2 changes: 1 addition & 1 deletion srcts/types/src/shiny/shinyapp.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -61,7 +61,7 @@ declare class ShinyApp {
next: () => number;
reset: () => void;
};
onDisconnected(): void;
onDisconnected(reloading?: boolean): void;
onConnected(): void;
makeRequest(method: string, args: unknown[], onSuccess: OnSuccessRequest, onError: OnErrorRequest, blobs: Array<ArrayBuffer | Blob | string> | undefined): void;
$sendMsg(msg: MessageValue): void;
Expand Down

0 comments on commit be6f671

Please sign in to comment.