aboutsummaryrefslogblamecommitdiffstats
path: root/actioncable/app/javascript/action_cable/connection_monitor.js
blob: f0e75ae137c4305e081ee8daaec26a3b246657fe (plain) (tree)
1
2
3
4
5
6
7
8
9
                             
 

                                                                                                                                         
 
                                      
 
                                                  
 
                                                                        
 





                                                                  
 





                                                                             
                                                                                          
     
   
 




                                                                                
                                             
     
   
 


                                            
 


                         
 



                              
                                                    
   
 

                               
                                                       
   
 
            
 



                      
 


                                  
 



                                         
     

                             
 
                     

                                                                      




                                                       
                                                                                                                                                                                                                                                                               

                                        
                                                                            
              
                                                 
                                

       
   
 


                                                                                                         
 


                                                                                                       
 



                                                                    
                                                                                                                                      
                                  
         
       
            
     

   
 
 

                                  

               
 
 
                                                                                                 
 
                                
import logger from "./logger"

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

const clamp = (number, min, max) => Math.max(min, Math.min(max, number))

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()
      document.addEventListener("visibilitychange", this.visibilityDidChange)
      logger.log(`ConnectionMonitor started. pollInterval = ${this.getPollInterval()} ms`)
    }
  }

  stop() {
    if (this.isRunning()) {
      this.stoppedAt = now()
      this.stopPolling()
      document.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 {min, max, multiplier} = this.constructor.pollInterval
    const interval = multiplier * Math.log(this.reconnectAttempts + 1)
    return Math.round(clamp(interval, min, max) * 1000)
  }

  reconnectIfStale() {
    if (this.connectionIsStale()) {
      logger.log(`ConnectionMonitor detected stale connection. reconnectAttempts = ${this.reconnectAttempts}, pollInterval = ${this.getPollInterval()} ms, time disconnected = ${secondsSince(this.disconnectedAt)} s, stale threshold = ${this.constructor.staleThreshold} s`)
      this.reconnectAttempts++
      if (this.disconnectedRecently()) {
        logger.log("ConnectionMonitor skipping reopening recent disconnect")
      } else {
        logger.log("ConnectionMonitor reopening")
        this.connection.reopen()
      }
    }
  }

  connectionIsStale() {
    return secondsSince(this.pingedAt ? this.pingedAt : this.startedAt) > 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. visbilityState = ${document.visibilityState}`)
          this.connection.reopen()
        }
      }
      , 200)
    }
  }

}

ConnectionMonitor.pollInterval = {
  min: 3,
  max: 30,
  multiplier: 5
}

ConnectionMonitor.staleThreshold = 6 // Server::Connections::BEAT_INTERVAL * 2 (missed two pings)

export default ConnectionMonitor