overleaf/services/web/public/coffee/ide/connection/ConnectionManager.coffee

279 lines
9.8 KiB
CoffeeScript
Raw Normal View History

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
2014-06-24 11:33:36 -04:00
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
2016-10-31 12:26:08 -04:00
@$scope.connection =
2014-06-24 11:33:36 -04:00
reconnecting: false
# If we need to force everyone to reload the editor
forced_disconnect: false
inactive_disconnect: false
2014-06-24 11:33:36 -04:00
@$scope.tryReconnectNow = () =>
# user manually requested reconnection via "Try now" button
@tryReconnectWithRateLimit({force:true})
2014-06-24 11:33:36 -04:00
@$scope.$on 'cursor:editor:update', () =>
2016-10-31 12:19:07 -04:00
@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()
2014-07-09 15:32:03 -04:00
2015-03-20 15:08:35 -04:00
@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"
2014-06-24 11:33:36 -04:00
@connected = true
@gracefullyReconnecting = false
2014-06-24 11:33:36 -04:00
@ide.pushEvent("connected")
@$scope.$apply () =>
2014-06-24 11:33:36 -04:00
@$scope.connection.reconnecting = false
@$scope.connection.inactive_disconnect = false
2014-06-24 11:33:36 -04:00
if @$scope.state.loading
2014-07-10 08:41:54 -04:00
@$scope.state.load_progress = 70
2014-06-24 11:33:36 -04:00
# we have passed authentication so we can now join the project
2014-06-24 11:33:36 -04:00
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 () =>
2016-11-04 06:25:08 -04:00
@$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.
2014-06-24 11:33:36 -04:00
@ide.socket.on 'disconnect', () =>
sl_console.log "[socket.io disconnect] Disconnected"
2014-06-24 11:33:36 -04:00
@connected = false
@ide.pushEvent("disconnected")
@$scope.$apply () =>
@$scope.connection.reconnecting = false
if !$scope.connection.forced_disconnect and !@userIsInactive and !@gracefullyReconnecting
2014-06-24 11:33:36 -04:00
@startAutoReconnectCountdown()
# Site administrators can send the forceDisconnect event to all users
2014-06-24 11:33:36 -04:00
@ide.socket.on 'forceDisconnect', (message) =>
@$scope.$apply () =>
@$scope.permissions.write = false
2014-06-24 11:33:36 -04:00
@$scope.connection.forced_disconnect = true
2014-07-24 11:39:32 -04:00
@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 is this continues to happen.
""")
2014-06-24 11:33:36 -04:00
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
2014-06-24 11:33:36 -04:00
@ide.socket.emit 'joinProject', {
project_id: @ide.project_id
}, (err, project, permissionsLevel, protocolVersion) =>
if err?
return @reportConnectionError(err)
2014-06-24 11:33:36 -04:00
if @$scope.protocolVersion? and @$scope.protocolVersion != protocolVersion
location.reload(true)
2014-06-24 11:33:36 -04:00
@$scope.$apply () =>
@$scope.protocolVersion = protocolVersion
@$scope.project = project
@$scope.permissionsLevel = permissionsLevel
2014-06-24 11:33:36 -04:00
@$scope.state.load_progress = 100
@$scope.state.loading = false
@$scope.$broadcast "project:joined"
2014-06-24 10:31:44 -04:00
reconnectImmediately: () ->
@disconnect()
2014-06-24 11:33:36 -04:00
@tryReconnect()
2014-06-24 10:31:44 -04:00
disconnect: () ->
sl_console.log "[socket.io] disconnecting client"
2014-06-24 11:33:36 -04:00
@ide.socket.disconnect()
startAutoReconnectCountdown: () ->
sl_console.log "[ConnectionManager] starting autoreconnect countdown"
twoMinutes = 2 * 60 * 1000
2016-10-31 12:19:07 -04:00
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
2014-06-24 11:33:36 -04:00
@$scope.$apply () =>
2014-07-02 11:41:29 -04:00
@$scope.connection.reconnecting = false
@$scope.connection.reconnection_countdown = countdown
2014-06-24 11:33:36 -04:00
setTimeout(=>
if !@connected
@timeoutId = setTimeout (=> @decreaseCountdown()), 1000
, 200)
cancelReconnect: () ->
2016-10-31 11:31:32 -04:00
# 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
2014-06-24 11:33:36 -04:00
decreaseCountdown: () ->
2016-10-31 11:31:32 -04:00
@timeoutId = null
return if !@$scope.connection.reconnection_countdown?
2016-10-31 11:31:32 -04:00
sl_console.log "[ConnectionManager] decreasing countdown", @$scope.connection.reconnection_countdown
2014-06-24 11:33:36 -04:00
@$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"
2014-06-24 11:33:36 -04:00
@cancelReconnect()
delete @$scope.connection.reconnection_countdown
2014-07-17 08:44:50 -04:00
return if @connected
2014-06-24 11:33:36 -04:00
@$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
2014-06-24 11:33:36 -04:00
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
2016-10-31 12:26:08 -04:00
_reconnectGracefullyNow: () ->
@gracefullyReconnecting = true
@reconnectGracefullyStarted = null
# Clear cookie so we don't go to the same backend server
$.cookie("SERVERID", "", { expires: -1, path: "/" })
@reconnectImmediately()