CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutSign UpSign In
sagemathinc

Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place.

GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/frontend/app/monitor-connection.ts
Views: 687
1
/*
2
* This file is part of CoCalc: Copyright © 2020 Sagemath, Inc.
3
* License: MS-RSL – see LICENSE.md for details
4
*/
5
6
// Monitor connection-related events from webapp_client and use them to set some
7
// state in the page store.
8
9
import { delay } from "awaiting";
10
11
import { alert_message } from "@cocalc/frontend/alerts";
12
import { redux } from "@cocalc/frontend/app-framework";
13
import { webapp_client } from "@cocalc/frontend/webapp-client";
14
import { minutes_ago } from "@cocalc/util/misc";
15
import { reuseInFlight } from "@cocalc/util/reuse-in-flight";
16
import { SITE_NAME } from "@cocalc/util/theme";
17
import { ConnectionStatus } from "./store";
18
19
const DISCONNECTED_STATE_DELAY_MS = 5000;
20
const CONNECTING_STATE_DELAY_MS = 3000;
21
22
import { isMobile } from "../feature";
23
24
export function init_connection(): void {
25
const actions = redux.getActions("page");
26
const store = redux.getStore("page");
27
28
const recent_disconnects: number[] = [];
29
function record_disconnect(): void {
30
recent_disconnects.push(+new Date());
31
if (recent_disconnects.length > 100) {
32
// do not waste memory by deleting oldest entry:
33
recent_disconnects.splice(0, 1);
34
}
35
}
36
37
function num_recent_disconnects(minutes: number = 5): number {
38
// note the "+", since we work with ms since epoch.
39
const ago = +minutes_ago(minutes);
40
return recent_disconnects.filter((x) => x > ago).length;
41
}
42
43
let reconnection_warning: null | number = null;
44
45
// heartbeats are used to detect standby's (e.g. user closes their laptop).
46
// The reason to record more than one is to take rapid re-firing
47
// of the time after resume into account.
48
const heartbeats: number[] = [];
49
const heartbeat_N = 3;
50
const heartbeat_interval_min = 1;
51
const heartbeat_interval_ms = heartbeat_interval_min * 60 * 1000;
52
function record_heartbeat() {
53
heartbeats.push(+new Date());
54
if (heartbeats.length > heartbeat_N) {
55
heartbeats.slice(0, 1);
56
}
57
}
58
setInterval(record_heartbeat, heartbeat_interval_ms);
59
60
// heuristic to detect recent wakeup from standby:
61
// second last heartbeat older than (N+1)x the interval
62
function recent_wakeup_from_standby(): boolean {
63
return (
64
heartbeats.length === heartbeat_N &&
65
+minutes_ago((heartbeat_N + 1) * heartbeat_interval_min) > heartbeats[0]
66
);
67
}
68
69
let actual_status: ConnectionStatus = store.get("connection_status");
70
webapp_client.on("connected", () => {
71
actual_status = "connected";
72
actions.set_connection_status("connected", new Date());
73
});
74
75
const handle_disconnected = reuseInFlight(async () => {
76
record_disconnect();
77
const date = new Date();
78
actions.set_ping(undefined, undefined);
79
if (store.get("connection_status") == "connected") {
80
await delay(DISCONNECTED_STATE_DELAY_MS);
81
}
82
if (actual_status == "disconnected") {
83
// still disconnected after waiting the delay
84
actions.set_connection_status("disconnected", date);
85
}
86
});
87
88
webapp_client.on("disconnected", () => {
89
actual_status = "disconnected";
90
handle_disconnected();
91
});
92
93
webapp_client.on("connecting", () => {
94
actual_status = "connecting";
95
handle_connecting();
96
});
97
98
const handle_connecting = reuseInFlight(async () => {
99
const date = new Date();
100
if (store.get("connection_status") == "connected") {
101
await delay(CONNECTING_STATE_DELAY_MS);
102
}
103
if (actual_status == "connecting") {
104
// still connecting after waiting the delay
105
actions.set_connection_status("connecting", date);
106
}
107
108
const attempt = webapp_client.hub_client.get_num_attempts();
109
async function reconnect(msg) {
110
// reset recent disconnects, and hope that after the reconnection the situation will be better
111
recent_disconnects.length = 0; // see https://stackoverflow.com/questions/1232040/how-do-i-empty-an-array-in-javascript
112
reconnection_warning = +new Date();
113
console.log(
114
`ALERT: connection unstable, notification + attempting to fix it -- ${attempt} attempts and ${num_recent_disconnects()} disconnects`,
115
);
116
if (!recent_wakeup_from_standby()) {
117
alert_message(msg);
118
}
119
webapp_client.hub_client.fix_connection();
120
// Wait a half second, then remove one extra reconnect added by the call in the above line.
121
await delay(500);
122
recent_disconnects.pop();
123
}
124
125
console.log(
126
`attempt: ${attempt} and num_recent_disconnects: ${num_recent_disconnects()}`,
127
);
128
// NOTE: On mobile devices the websocket is disconnected every time one backgrounds
129
// the application. This normal and expected behavior, which does not indicate anything
130
// bad about the user's actual network connection. Thus displaying this error in the case
131
// of mobile is likely wrong. (It could also be right, of course.)
132
const EPHEMERAL_WEBSOCKETS = isMobile.any();
133
if (
134
!EPHEMERAL_WEBSOCKETS &&
135
(num_recent_disconnects() >= 2 || attempt >= 10)
136
) {
137
// this event fires several times, limit displaying the message and calling reconnect() too often
138
const SiteName =
139
redux.getStore("customize").get("site_name") ?? SITE_NAME;
140
if (
141
reconnection_warning === null ||
142
reconnection_warning < +minutes_ago(1)
143
) {
144
if (num_recent_disconnects() >= 7 || attempt >= 20) {
145
actions.set_connection_quality("bad");
146
reconnect({
147
type: "error",
148
timeout: 10,
149
message: `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).`,
150
});
151
} else if (attempt >= 10) {
152
actions.set_connection_quality("flaky");
153
reconnect({
154
type: "info",
155
timeout: 10,
156
message: `Your connection could be weak or the ${SiteName} service is temporarily unstable. Proceed with caution.`,
157
});
158
}
159
}
160
} else {
161
reconnection_warning = null;
162
actions.set_connection_quality("good");
163
}
164
});
165
166
webapp_client.on("new_version", actions.set_new_version);
167
}
168
169