diff options
Diffstat (limited to 'app/assets/javascripts/actioncable_connection_monitor.js')
-rw-r--r-- | app/assets/javascripts/actioncable_connection_monitor.js | 142 |
1 files changed, 142 insertions, 0 deletions
diff --git a/app/assets/javascripts/actioncable_connection_monitor.js b/app/assets/javascripts/actioncable_connection_monitor.js new file mode 100644 index 00000000000..fc4e436c7fb --- /dev/null +++ b/app/assets/javascripts/actioncable_connection_monitor.js @@ -0,0 +1,142 @@ +/* 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; |