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