143 lines
4.1 KiB
JavaScript
143 lines
4.1 KiB
JavaScript
|
/* eslint-disable no-restricted-globals */
|
||
|
|
||
|
import { logger } from '@rails/actioncable';
|
||
|
|
||
|
// This is based on https://github.com/rails/rails/blob/5a477890c809d4a17dc0dede43c6b8cef81d8175/actioncable/app/javascript/action_cable/connection_monitor.js
|
||
|
// so that we can take advantage of the improved reconnection logic. We can remove this once we upgrade @rails/actioncable to a version that includes this.
|
||
|
|
||
|
// Responsible for ensuring the cable connection is in good health by validating the heartbeat pings sent from the server, and attempting
|
||
|
// revival reconnections if things go astray. Internal class, not intended for direct user manipulation.
|
||
|
|
||
|
const now = () => new Date().getTime();
|
||
|
|
||
|
const secondsSince = (time) => (now() - time) / 1000;
|
||
|
class ConnectionMonitor {
|
||
|
constructor(connection) {
|
||
|
this.visibilityDidChange = this.visibilityDidChange.bind(this);
|
||
|
this.connection = connection;
|
||
|
this.reconnectAttempts = 0;
|
||
|
}
|
||
|
|
||
|
start() {
|
||
|
if (!this.isRunning()) {
|
||
|
this.startedAt = now();
|
||
|
delete this.stoppedAt;
|
||
|
this.startPolling();
|
||
|
addEventListener('visibilitychange', this.visibilityDidChange);
|
||
|
logger.log(
|
||
|
`ConnectionMonitor started. stale threshold = ${this.constructor.staleThreshold} s`,
|
||
|
);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
stop() {
|
||
|
if (this.isRunning()) {
|
||
|
this.stoppedAt = now();
|
||
|
this.stopPolling();
|
||
|
removeEventListener('visibilitychange', this.visibilityDidChange);
|
||
|
logger.log('ConnectionMonitor stopped');
|
||
|
}
|
||
|
}
|
||
|
|
||
|
isRunning() {
|
||
|
return this.startedAt && !this.stoppedAt;
|
||
|
}
|
||
|
|
||
|
recordPing() {
|
||
|
this.pingedAt = now();
|
||
|
}
|
||
|
|
||
|
recordConnect() {
|
||
|
this.reconnectAttempts = 0;
|
||
|
this.recordPing();
|
||
|
delete this.disconnectedAt;
|
||
|
logger.log('ConnectionMonitor recorded connect');
|
||
|
}
|
||
|
|
||
|
recordDisconnect() {
|
||
|
this.disconnectedAt = now();
|
||
|
logger.log('ConnectionMonitor recorded disconnect');
|
||
|
}
|
||
|
|
||
|
// Private
|
||
|
|
||
|
startPolling() {
|
||
|
this.stopPolling();
|
||
|
this.poll();
|
||
|
}
|
||
|
|
||
|
stopPolling() {
|
||
|
clearTimeout(this.pollTimeout);
|
||
|
}
|
||
|
|
||
|
poll() {
|
||
|
this.pollTimeout = setTimeout(() => {
|
||
|
this.reconnectIfStale();
|
||
|
this.poll();
|
||
|
}, this.getPollInterval());
|
||
|
}
|
||
|
|
||
|
getPollInterval() {
|
||
|
const { staleThreshold, reconnectionBackoffRate } = this.constructor;
|
||
|
const backoff = (1 + reconnectionBackoffRate) ** Math.min(this.reconnectAttempts, 10);
|
||
|
const jitterMax = this.reconnectAttempts === 0 ? 1.0 : reconnectionBackoffRate;
|
||
|
const jitter = jitterMax * Math.random();
|
||
|
return staleThreshold * 1000 * backoff * (1 + jitter);
|
||
|
}
|
||
|
|
||
|
reconnectIfStale() {
|
||
|
if (this.connectionIsStale()) {
|
||
|
logger.log(
|
||
|
`ConnectionMonitor detected stale connection. reconnectAttempts = ${
|
||
|
this.reconnectAttempts
|
||
|
}, time stale = ${secondsSince(this.refreshedAt)} s, stale threshold = ${
|
||
|
this.constructor.staleThreshold
|
||
|
} s`,
|
||
|
);
|
||
|
this.reconnectAttempts += 1;
|
||
|
if (this.disconnectedRecently()) {
|
||
|
logger.log(
|
||
|
`ConnectionMonitor skipping reopening recent disconnect. time disconnected = ${secondsSince(
|
||
|
this.disconnectedAt,
|
||
|
)} s`,
|
||
|
);
|
||
|
} else {
|
||
|
logger.log('ConnectionMonitor reopening');
|
||
|
this.connection.reopen();
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
get refreshedAt() {
|
||
|
return this.pingedAt ? this.pingedAt : this.startedAt;
|
||
|
}
|
||
|
|
||
|
connectionIsStale() {
|
||
|
return secondsSince(this.refreshedAt) > this.constructor.staleThreshold;
|
||
|
}
|
||
|
|
||
|
disconnectedRecently() {
|
||
|
return (
|
||
|
this.disconnectedAt && secondsSince(this.disconnectedAt) < this.constructor.staleThreshold
|
||
|
);
|
||
|
}
|
||
|
|
||
|
visibilityDidChange() {
|
||
|
if (document.visibilityState === 'visible') {
|
||
|
setTimeout(() => {
|
||
|
if (this.connectionIsStale() || !this.connection.isOpen()) {
|
||
|
logger.log(
|
||
|
`ConnectionMonitor reopening stale connection on visibilitychange. visibilityState = ${document.visibilityState}`,
|
||
|
);
|
||
|
this.connection.reopen();
|
||
|
}
|
||
|
}, 200);
|
||
|
}
|
||
|
}
|
||
|
}
|
||
|
|
||
|
ConnectionMonitor.staleThreshold = 6; // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)
|
||
|
ConnectionMonitor.reconnectionBackoffRate = 0.15;
|
||
|
|
||
|
export default ConnectionMonitor;
|