| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281 |
- define [], () ->
- ONEHOUR = 1000 * 60 * 60
- class ConnectionManager
- disconnectAfterMs: ONEHOUR * 24
- lastUserAction : new Date()
- constructor: (@ide, @$scope) ->
- if !io?
- console.error "Socket.io javascript not loaded. Please check that the real-time service is running and accessible."
- @ide.socket =
- on: () ->
- @$scope.$apply () =>
- @$scope.state.error = "Could not connect to websocket server :("
- return
- setInterval(() =>
- @disconnectIfInactive()
- , ONEHOUR)
- # trigger a reconnect immediately if network comes back online
- window.addEventListener 'online', =>
- sl_console.log "[online] browser notified online"
- if !@connected
- @tryReconnectWithRateLimit({force:true})
- @userIsLeavingPage = false
- window.addEventListener 'beforeunload', =>
- @userIsLeavingPage = true
- return # Don't return true or it will show a pop up
- @connected = false
- @userIsInactive = false
- @gracefullyReconnecting = false
- @$scope.connection =
- reconnecting: false
- # If we need to force everyone to reload the editor
- forced_disconnect: false
- inactive_disconnect: false
- @$scope.tryReconnectNow = () =>
- # user manually requested reconnection via "Try now" button
- @tryReconnectWithRateLimit({force:true})
- @$scope.$on 'cursor:editor:update', () =>
- @lastUserAction = new Date() # time of last edit
- if !@connected
- # user is editing, try to reconnect
- @tryReconnectWithRateLimit()
- document.querySelector('body').addEventListener 'click', (e) =>
- if !@connected and e.target.id != 'try-reconnect-now-button'
- # user is editing, try to reconnect
- @tryReconnectWithRateLimit()
- @ide.socket = io.connect null,
- reconnect: false
- 'connect timeout': 30 * 1000
- "force new connection": true
- # The "connect" event is the first event we get back. It only
- # indicates that the websocket is connected, we still need to
- # pass authentication to join a project.
- @ide.socket.on "connect", () =>
- sl_console.log "[socket.io connect] Connected"
- # The next event we should get is an authentication response
- # from the server, either "connectionAccepted" or
- # "connectionRejected".
- @ide.socket.on 'connectionAccepted', (message) =>
- sl_console.log "[socket.io connectionAccepted] allowed to connect"
- @connected = true
- @gracefullyReconnecting = false
- @ide.pushEvent("connected")
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
- @$scope.connection.inactive_disconnect = false
- if @$scope.state.loading
- @$scope.state.load_progress = 70
- # we have passed authentication so we can now join the project
- setTimeout(() =>
- @joinProject()
- , 100)
- @ide.socket.on 'connectionRejected', (err) =>
- sl_console.log "[socket.io connectionRejected] session not valid or other connection error"
- # we have failed authentication, usually due to an invalid session cookie
- return @reportConnectionError(err)
- # Alternatively the attempt to connect can fail completely, so
- # we never get into the "connect" state.
- @ide.socket.on "connect_failed", () =>
- @connected = false
- @$scope.$apply () =>
- @$scope.state.error = "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue."
- # We can get a "disconnect" event at any point after the
- # "connect" event.
- @ide.socket.on 'disconnect', () =>
- sl_console.log "[socket.io disconnect] Disconnected"
- @connected = false
- @ide.pushEvent("disconnected")
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
- if !@$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
- @startAutoReconnectCountdown()
- # Site administrators can send the forceDisconnect event to all users
- @ide.socket.on 'forceDisconnect', (message) =>
- @$scope.$apply () =>
- @$scope.permissions.write = false
- @$scope.connection.forced_disconnect = true
- @ide.socket.disconnect()
- @ide.showGenericMessageModal("Please Refresh", """
- We're performing maintenance on ShareLaTeX and you need to refresh the editor.
- Sorry for any inconvenience.
- The editor will refresh in automatically in 10 seconds.
- """)
- setTimeout () ->
- location.reload()
- , 10 * 1000
- @ide.socket.on "reconnectGracefully", () =>
- sl_console.log "Reconnect gracefully"
- @reconnectGracefully()
- # Error reporting, which can reload the page if appropriate
- reportConnectionError: (err) ->
- sl_console.log "[socket.io] reporting connection error"
- if err?.message == "not authorized" or err?.message == "invalid session"
- window.location = "/login?redir=#{encodeURI(window.location.pathname)}"
- else
- @ide.socket.disconnect()
- @ide.showGenericMessageModal("Something went wrong connecting", """
- Something went wrong connecting to your project. Please refresh if this continues to happen.
- """)
- joinProject: () ->
- sl_console.log "[joinProject] joining..."
- # Note: if the "joinProject" message doesn't reach the server
- # (e.g. if we are in a disconnected state at this point) the
- # callback will never be executed
- data = {
- project_id: @ide.project_id
- }
- if window.anonymousAccessToken
- data.anonymousAccessToken = window.anonymousAccessToken
- @ide.socket.emit 'joinProject', data, (err, project, permissionsLevel, protocolVersion) =>
- if err? or !project?
- return @reportConnectionError(err)
- if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion
- location.reload(true)
- @$scope.$apply () =>
- @$scope.protocolVersion = protocolVersion
- @$scope.project = project
- @$scope.permissionsLevel = permissionsLevel
- @$scope.state.load_progress = 100
- @$scope.state.loading = false
- @$scope.$broadcast "project:joined"
- reconnectImmediately: () ->
- @disconnect()
- @tryReconnect()
- disconnect: () ->
- sl_console.log "[socket.io] disconnecting client"
- @ide.socket.disconnect()
- startAutoReconnectCountdown: () ->
- sl_console.log "[ConnectionManager] starting autoreconnect countdown"
- twoMinutes = 2 * 60 * 1000
- if @lastUserAction? and new Date() - @lastUserAction > twoMinutes
- # between 1 minute and 3 minutes
- countdown = 60 + Math.floor(Math.random() * 120)
- else
- countdown = 3 + Math.floor(Math.random() * 7)
- if @userIsLeavingPage #user will have pressed refresh or back etc
- return
- @$scope.$apply () =>
- @$scope.connection.reconnecting = false
- @$scope.connection.reconnection_countdown = countdown
- setTimeout(=>
- if !@connected
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
- , 200)
- cancelReconnect: () ->
- # clear timeout and set to null so we know there is no countdown running
- if @timeoutId?
- sl_console.log "[ConnectionManager] cancelling existing reconnect timer"
- clearTimeout @timeoutId
- @timeoutId = null
- decreaseCountdown: () ->
- @timeoutId = null
- return if !@$scope.connection.reconnection_countdown?
- sl_console.log "[ConnectionManager] decreasing countdown", @$scope.connection.reconnection_countdown
- @$scope.$apply () =>
- @$scope.connection.reconnection_countdown--
- if @$scope.connection.reconnection_countdown <= 0
- @$scope.$apply () =>
- @tryReconnect()
- else
- @timeoutId = setTimeout (=> @decreaseCountdown()), 1000
- tryReconnect: () ->
- sl_console.log "[ConnectionManager] tryReconnect"
- @cancelReconnect()
- delete @$scope.connection.reconnection_countdown
- return if @connected
- @$scope.connection.reconnecting = true
- # use socket.io connect() here to make a single attempt, the
- # reconnect() method makes multiple attempts
- @ide.socket.socket.connect()
- # record the time of the last attempt to connect
- @lastConnectionAttempt = new Date()
- setTimeout (=> @startAutoReconnectCountdown() if !@connected), 2000
- MIN_RETRY_INTERVAL: 1000 # ms, rate limit on reconnects for user clicking "try now"
- BACKGROUND_RETRY_INTERVAL : 5 * 1000 # ms, rate limit on reconnects for other user activity (e.g. cursor moves)
- tryReconnectWithRateLimit: (options) ->
- # bail out if the reconnect is already in progress
- return if @$scope.connection?.reconnecting
- # bail out if we are going to reconnect soon anyway
- reconnectingSoon = @$scope.connection?.reconnection_countdown? and @$scope.connection.reconnection_countdown <= 5
- clickedTryNow = options?.force # user requested reconnection
- return if reconnectingSoon and not clickedTryNow
- # bail out if we tried reconnecting recently
- allowedInterval = if clickedTryNow then @MIN_RETRY_INTERVAL else @BACKGROUND_RETRY_INTERVAL
- return if @lastConnectionAttempt? and new Date() - @lastConnectionAttempt < allowedInterval
- @tryReconnect()
- disconnectIfInactive: ()->
- @userIsInactive = (new Date() - @lastUserAction) > @disconnectAfterMs
- if @userIsInactive and @connected
- @disconnect()
- @$scope.$apply () =>
- @$scope.connection.inactive_disconnect = true
- RECONNECT_GRACEFULLY_RETRY_INTERVAL: 5000 # ms
- MAX_RECONNECT_GRACEFULLY_INTERVAL: 60 * 5 * 1000 # 5 minutes
- reconnectGracefully: () ->
- @reconnectGracefullyStarted ?= new Date()
- userIsInactive = (new Date() - @lastUserAction) > @RECONNECT_GRACEFULLY_RETRY_INTERVAL
- maxIntervalReached = (new Date() - @reconnectGracefullyStarted) > @MAX_RECONNECT_GRACEFULLY_INTERVAL
- if userIsInactive or maxIntervalReached
- sl_console.log "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
- @_reconnectGracefullyNow()
- else
- sl_console.log "[reconnectGracefully] User is working, will try again in 5 seconds"
- setTimeout () =>
- @reconnectGracefully()
- , @RECONNECT_GRACEFULLY_RETRY_INTERVAL
- _reconnectGracefullyNow: () ->
- @gracefullyReconnecting = true
- @reconnectGracefullyStarted = null
- # Clear cookie so we don't go to the same backend server
- $.cookie("SERVERID", "", { expires: -1, path: "/" })
- @reconnectImmediately()
|