Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.
Path: blob/master/src/packages/frontend/app/monitor-connection.ts
Views: 687
/*1* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.2* License: MS-RSL – see LICENSE.md for details3*/45// Monitor connection-related events from webapp_client and use them to set some6// state in the page store.78import { delay } from "awaiting";910import { alert_message } from "@cocalc/frontend/alerts";11import { redux } from "@cocalc/frontend/app-framework";12import { webapp_client } from "@cocalc/frontend/webapp-client";13import { minutes_ago } from "@cocalc/util/misc";14import { reuseInFlight } from "@cocalc/util/reuse-in-flight";15import { SITE_NAME } from "@cocalc/util/theme";16import { ConnectionStatus } from "./store";1718const DISCONNECTED_STATE_DELAY_MS = 5000;19const CONNECTING_STATE_DELAY_MS = 3000;2021import { isMobile } from "../feature";2223export function init_connection(): void {24const actions = redux.getActions("page");25const store = redux.getStore("page");2627const recent_disconnects: number[] = [];28function record_disconnect(): void {29recent_disconnects.push(+new Date());30if (recent_disconnects.length > 100) {31// do not waste memory by deleting oldest entry:32recent_disconnects.splice(0, 1);33}34}3536function num_recent_disconnects(minutes: number = 5): number {37// note the "+", since we work with ms since epoch.38const ago = +minutes_ago(minutes);39return recent_disconnects.filter((x) => x > ago).length;40}4142let reconnection_warning: null | number = null;4344// heartbeats are used to detect standby's (e.g. user closes their laptop).45// The reason to record more than one is to take rapid re-firing46// of the time after resume into account.47const heartbeats: number[] = [];48const heartbeat_N = 3;49const heartbeat_interval_min = 1;50const heartbeat_interval_ms = heartbeat_interval_min * 60 * 1000;51function record_heartbeat() {52heartbeats.push(+new Date());53if (heartbeats.length > heartbeat_N) {54heartbeats.slice(0, 1);55}56}57setInterval(record_heartbeat, heartbeat_interval_ms);5859// heuristic to detect recent wakeup from standby:60// second last heartbeat older than (N+1)x the interval61function recent_wakeup_from_standby(): boolean {62return (63heartbeats.length === heartbeat_N &&64+minutes_ago((heartbeat_N + 1) * heartbeat_interval_min) > heartbeats[0]65);66}6768let actual_status: ConnectionStatus = store.get("connection_status");69webapp_client.on("connected", () => {70actual_status = "connected";71actions.set_connection_status("connected", new Date());72});7374const handle_disconnected = reuseInFlight(async () => {75record_disconnect();76const date = new Date();77actions.set_ping(undefined, undefined);78if (store.get("connection_status") == "connected") {79await delay(DISCONNECTED_STATE_DELAY_MS);80}81if (actual_status == "disconnected") {82// still disconnected after waiting the delay83actions.set_connection_status("disconnected", date);84}85});8687webapp_client.on("disconnected", () => {88actual_status = "disconnected";89handle_disconnected();90});9192webapp_client.on("connecting", () => {93actual_status = "connecting";94handle_connecting();95});9697const handle_connecting = reuseInFlight(async () => {98const date = new Date();99if (store.get("connection_status") == "connected") {100await delay(CONNECTING_STATE_DELAY_MS);101}102if (actual_status == "connecting") {103// still connecting after waiting the delay104actions.set_connection_status("connecting", date);105}106107const attempt = webapp_client.hub_client.get_num_attempts();108async function reconnect(msg) {109// reset recent disconnects, and hope that after the reconnection the situation will be better110recent_disconnects.length = 0; // see https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript111reconnection_warning = +new Date();112console.log(113`ALERT: connection unstable, notification + attempting to fix it -- ${attempt} attempts and ${num_recent_disconnects()} disconnects`,114);115if (!recent_wakeup_from_standby()) {116alert_message(msg);117}118webapp_client.hub_client.fix_connection();119// Wait a half second, then remove one extra reconnect added by the call in the above line.120await delay(500);121recent_disconnects.pop();122}123124console.log(125`attempt: ${attempt} and num_recent_disconnects: ${num_recent_disconnects()}`,126);127// NOTE: On mobile devices the websocket is disconnected every time one backgrounds128// the application. This normal and expected behavior, which does not indicate anything129// bad about the user's actual network connection. Thus displaying this error in the case130// of mobile is likely wrong. (It could also be right, of course.)131const EPHEMERAL_WEBSOCKETS = isMobile.any();132if (133!EPHEMERAL_WEBSOCKETS &&134(num_recent_disconnects() >= 2 || attempt >= 10)135) {136// this event fires several times, limit displaying the message and calling reconnect() too often137const SiteName =138redux.getStore("customize").get("site_name") ?? SITE_NAME;139if (140reconnection_warning === null ||141reconnection_warning < +minutes_ago(1)142) {143if (num_recent_disconnects() >= 7 || attempt >= 20) {144actions.set_connection_quality("bad");145reconnect({146type: "error",147timeout: 10,148message: `Your connection is unstable or ${SiteName} is temporarily not available. You may need to refresh your browser or completely quit and restart it (see https://github.com/sagemathinc/cocalc/issues/6642).`,149});150} else if (attempt >= 10) {151actions.set_connection_quality("flaky");152reconnect({153type: "info",154timeout: 10,155message: `Your connection could be weak or the ${SiteName} service is temporarily unstable. Proceed with caution.`,156});157}158}159} else {160reconnection_warning = null;161actions.set_connection_quality("good");162}163});164165webapp_client.on("new_version", actions.set_new_version);166}167168169