Path: blob/master/src/packages/frontend/app/monitor-connection.ts
1496 views
/*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";9import { alert_message } from "@cocalc/frontend/alerts";10import { redux } from "@cocalc/frontend/app-framework";11import { webapp_client } from "@cocalc/frontend/webapp-client";12import { minutes_ago } from "@cocalc/util/misc";13import { reuseInFlight } from "@cocalc/util/reuse-in-flight";14import { SITE_NAME } from "@cocalc/util/theme";15import { ConnectionStatus } from "./store";1617const DISCONNECTED_STATE_DELAY_MS = 10000;18const CONNECTING_STATE_DELAY_MS = 5000;1920import { isMobile } from "../feature";2122export function init_connection(): void {23const actions = redux.getActions("page");24const store = redux.getStore("page");2526const recent_disconnects: number[] = [];27function record_disconnect(): void {28recent_disconnects.push(+new Date());29if (recent_disconnects.length > 100) {30// do not waste memory by deleting oldest entry:31recent_disconnects.splice(0, 1);32}33}3435function num_recent_disconnects(minutes: number = 5): number {36// note the "+", since we work with ms since epoch.37const ago = +minutes_ago(minutes);38return recent_disconnects.filter((x) => x > ago).length;39}4041let reconnection_warning: null | number = null;4243// heartbeats are used to detect standby's (e.g. user closes their laptop).44// The reason to record more than one is to take rapid re-firing45// of the time after resume into account.46const heartbeats: number[] = [];47const heartbeat_N = 3;48const heartbeat_interval_min = 1;49const heartbeat_interval_ms = heartbeat_interval_min * 60 * 1000;50function record_heartbeat() {51heartbeats.push(+new Date());52if (heartbeats.length > heartbeat_N) {53heartbeats.slice(0, 1);54}55}56setInterval(record_heartbeat, heartbeat_interval_ms);5758// heuristic to detect recent wakeup from standby:59// second last heartbeat older than (N+1)x the interval60function recent_wakeup_from_standby(): boolean {61return (62heartbeats.length === heartbeat_N &&63+minutes_ago((heartbeat_N + 1) * heartbeat_interval_min) > heartbeats[0]64);65}6667let actual_status: ConnectionStatus = store.get("connection_status");68webapp_client.on("connected", () => {69actual_status = "connected";70actions.set_connection_status("connected", new Date());71});7273const handle_disconnected = reuseInFlight(async () => {74record_disconnect();75const date = new Date();76actions.set_ping(undefined, undefined);77if (store.get("connection_status") == "connected") {78await delay(DISCONNECTED_STATE_DELAY_MS);79}80if (actual_status == "disconnected") {81// still disconnected after waiting the delay82actions.set_connection_status("disconnected", date);83}84});8586webapp_client.on("disconnected", () => {87actual_status = "disconnected";88handle_disconnected();89});9091webapp_client.on("connecting", () => {92actual_status = "connecting";93handle_connecting();94});9596const handle_connecting = reuseInFlight(async () => {97const date = new Date();98if (store.get("connection_status") == "connected") {99await delay(CONNECTING_STATE_DELAY_MS);100}101if (actual_status == "connecting") {102// still connecting after waiting the delay103actions.set_connection_status("connecting", date);104}105106const attempt = webapp_client.conat_client.numConnectionAttempts;107async function reconnect(msg) {108// reset recent disconnects, and hope that after the reconnection the situation will be better109recent_disconnects.length = 0; // see https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript110reconnection_warning = +new Date();111console.log(112`ALERT: connection unstable, notification + attempting to fix it -- ${attempt} attempts and ${num_recent_disconnects()} disconnects`,113);114if (!recent_wakeup_from_standby()) {115alert_message(msg);116}117webapp_client.conat_client.reconnect();118// Wait a half second, then remove one extra reconnect added by the call in the above line.119await delay(500);120recent_disconnects.pop();121}122123console.log(124`attempt: ${attempt} and num_recent_disconnects: ${num_recent_disconnects()}`,125);126// NOTE: On mobile devices the websocket is disconnected every time one backgrounds127// the application. This normal and expected behavior, which does not indicate anything128// bad about the user's actual network connection. Thus displaying this error in the case129// of mobile is likely wrong. (It could also be right, of course.)130const EPHEMERAL_WEBSOCKETS = isMobile.any();131if (132!EPHEMERAL_WEBSOCKETS &&133(num_recent_disconnects() >= 2 || attempt >= 10)134) {135// this event fires several times, limit displaying the message and calling reconnect() too often136const SiteName =137redux.getStore("customize").get("site_name") ?? SITE_NAME;138if (139reconnection_warning === null ||140reconnection_warning < +minutes_ago(1)141) {142if (num_recent_disconnects() >= 7 || attempt >= 20) {143actions.set_connection_quality("bad");144reconnect({145type: "error",146timeout: 10,147message: `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).`,148});149} else if (attempt >= 10) {150actions.set_connection_quality("flaky");151reconnect({152type: "info",153timeout: 10,154message: `Your connection could be weak or the ${SiteName} service is temporarily unstable. Proceed with caution.`,155});156}157}158} else {159reconnection_warning = null;160actions.set_connection_quality("good");161}162});163164webapp_client.on("new_version", actions.set_new_version);165}166167168