/* 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;