2018-11-05 05:06:39 -05:00
|
|
|
// TODO: This file was created by bulk-decaffeinate.
|
|
|
|
// Fix any style issues and re-enable lint.
|
|
|
|
/*
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
* DS206: Consider reworking classes to avoid initClass
|
|
|
|
* DS207: Consider shorter variations of null checks
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
*/
|
|
|
|
|
2019-10-14 09:17:30 -04:00
|
|
|
/* global io */
|
2019-09-10 10:31:08 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
import SocketIoShim from './SocketIoShim'
|
2020-06-05 06:15:16 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
let ConnectionManager
|
|
|
|
const ONEHOUR = 1000 * 60 * 60
|
2020-06-30 08:04:31 -04:00
|
|
|
let transport = 'handshake'
|
2019-09-10 10:31:08 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
export default (ConnectionManager = (function() {
|
|
|
|
ConnectionManager = class ConnectionManager {
|
|
|
|
static initClass() {
|
|
|
|
this.prototype.disconnectAfterMs = ONEHOUR * 24
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.prototype.lastUserAction = new Date()
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.prototype.MIN_RETRY_INTERVAL = 1000 // ms, rate limit on reconnects for user clicking "try now"
|
|
|
|
this.prototype.BACKGROUND_RETRY_INTERVAL = 5 * 1000
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.prototype.JOIN_PROJECT_RETRY_INTERVAL = 5000
|
|
|
|
this.prototype.JOIN_PROJECT_MAX_RETRY_INTERVAL = 60000
|
|
|
|
this.prototype.RECONNECT_GRACEFULLY_RETRY_INTERVAL = 5000 // ms
|
|
|
|
this.prototype.MAX_RECONNECT_GRACEFULLY_INTERVAL = 45 * 1000
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-06-30 08:04:31 -04:00
|
|
|
constructor(ide, $scope, eventTracking) {
|
2020-05-19 05:02:56 -04:00
|
|
|
this.ide = ide
|
|
|
|
this.$scope = $scope
|
|
|
|
this.wsUrl = ide.wsUrl || null // websocket url (if defined)
|
|
|
|
if (typeof io === 'undefined' || io === null) {
|
|
|
|
if (this.wsUrl && !window.location.href.match(/ws=fallback/)) {
|
|
|
|
// if we tried to boot from a custom real-time backend and failed,
|
|
|
|
// try reloading and falling back to the siteUrl
|
|
|
|
window.location = window.location.href + '?ws=fallback'
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
console.error(
|
|
|
|
'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
|
|
|
|
)
|
|
|
|
this.ide.socket = SocketIoShim.stub()
|
|
|
|
this.$scope.$apply(() => {
|
|
|
|
return (this.$scope.state.error =
|
|
|
|
'Could not connect to websocket server :(')
|
2018-11-05 05:06:39 -05:00
|
|
|
})
|
2020-05-19 05:02:56 -04:00
|
|
|
return
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
setInterval(() => {
|
|
|
|
return this.disconnectIfInactive()
|
|
|
|
}, ONEHOUR)
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// trigger a reconnect immediately if network comes back online
|
|
|
|
window.addEventListener('online', () => {
|
|
|
|
sl_console.log('[online] browser notified online')
|
|
|
|
if (!this.connected) {
|
2018-11-05 05:06:39 -05:00
|
|
|
return this.tryReconnectWithRateLimit({ force: true })
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
this.userIsLeavingPage = false
|
|
|
|
window.addEventListener('beforeunload', () => {
|
|
|
|
this.userIsLeavingPage = true
|
|
|
|
}) // Don't return true or it will show a pop up
|
|
|
|
|
|
|
|
this.connected = false
|
|
|
|
this.userIsInactive = false
|
|
|
|
this.gracefullyReconnecting = false
|
|
|
|
|
|
|
|
this.joinProjectRetryInterval = this.JOIN_PROJECT_RETRY_INTERVAL
|
|
|
|
|
|
|
|
this.$scope.connection = {
|
|
|
|
debug: sl_debugging,
|
|
|
|
reconnecting: false,
|
|
|
|
stillReconnecting: false,
|
|
|
|
// If we need to force everyone to reload the editor
|
|
|
|
forced_disconnect: false,
|
|
|
|
inactive_disconnect: false,
|
|
|
|
jobId: 0
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.$scope.tryReconnectNow = () => {
|
|
|
|
// user manually requested reconnection via "Try now" button
|
|
|
|
return this.tryReconnectWithRateLimit({ force: true })
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.$scope.$on('cursor:editor:update', () => {
|
|
|
|
this.lastUserAction = new Date() // time of last edit
|
|
|
|
if (!this.connected) {
|
|
|
|
// user is editing, try to reconnect
|
|
|
|
return this.tryReconnectWithRateLimit()
|
2020-04-02 09:48:00 -04:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
})
|
2019-02-13 04:02:10 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
document.querySelector('body').addEventListener('click', e => {
|
|
|
|
if (!this.connected && e.target.id !== 'try-reconnect-now-button') {
|
|
|
|
// user is editing, try to reconnect
|
|
|
|
return this.tryReconnectWithRateLimit()
|
2019-02-14 09:21:33 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
})
|
|
|
|
|
|
|
|
// initial connection attempt
|
|
|
|
this.updateConnectionManagerState('connecting')
|
|
|
|
let parsedURL
|
|
|
|
try {
|
|
|
|
parsedURL = new URL(this.wsUrl || '/socket.io', window.location)
|
|
|
|
} catch (e) {
|
|
|
|
// hello IE11
|
|
|
|
if (
|
|
|
|
this.wsUrl &&
|
|
|
|
this.wsUrl.indexOf('/') !== 0 &&
|
|
|
|
!window.location.href.match(/ws=fallback/)
|
|
|
|
) {
|
|
|
|
// do not even try parsing the wsUrl, use the fallback
|
|
|
|
window.location = window.location.href + '?ws=fallback'
|
|
|
|
}
|
|
|
|
parsedURL = {
|
|
|
|
origin: null,
|
|
|
|
pathname: this.wsUrl || '/socket.io'
|
|
|
|
}
|
|
|
|
}
|
|
|
|
this.ide.socket = SocketIoShim.connect(
|
|
|
|
parsedURL.origin,
|
|
|
|
{
|
|
|
|
resource: parsedURL.pathname.slice(1),
|
|
|
|
reconnect: false,
|
|
|
|
'connect timeout': 30 * 1000,
|
|
|
|
'force new connection': true
|
|
|
|
}
|
|
|
|
)
|
2020-06-30 08:04:31 -04:00
|
|
|
const socket = this.ide.socket
|
|
|
|
socket.on('error', reportTransportError)
|
|
|
|
socket.on('connect_failed', reportTransportError)
|
|
|
|
socket.on('connecting', name => {
|
|
|
|
transport = name
|
|
|
|
socket.removeListener('error', reportTransportError)
|
|
|
|
socket.on('error', reportTransportError)
|
|
|
|
})
|
|
|
|
socket.on('connectionAccepted', () => {
|
|
|
|
// we have reached the real-time layer. ignore errors from now on.
|
|
|
|
socket.removeListener('error', reportTransportError)
|
|
|
|
socket.removeListener('connect_failed', reportTransportError)
|
|
|
|
})
|
|
|
|
function reportTransportError(error) {
|
|
|
|
socket.removeListener('error', reportTransportError)
|
|
|
|
socket.removeListener('connect_failed', reportTransportError)
|
|
|
|
if (typeof error === 'undefined') {
|
|
|
|
error = 'undefined'
|
|
|
|
} else if (typeof error !== 'string') {
|
|
|
|
try {
|
|
|
|
error = JSON.stringify(error)
|
|
|
|
} catch (e) {
|
|
|
|
error = `JSON_serialize_failed: ${e.message}`
|
|
|
|
}
|
|
|
|
}
|
|
|
|
if (transport === 'websocket' && error === '{"isTrusted":true}') {
|
|
|
|
error = 'hidden'
|
|
|
|
} else if (error.indexOf('<title>500') !== -1) {
|
|
|
|
error = '500'
|
|
|
|
} else if (error.indexOf('<title>502') !== -1) {
|
|
|
|
error = '502'
|
|
|
|
} else if (error.indexOf('<title>503') !== -1) {
|
|
|
|
error = '503'
|
|
|
|
} else if (error.length > 97) {
|
|
|
|
error = error.slice(0, 97) + '...'
|
|
|
|
}
|
|
|
|
eventTracking.sendMBSampled('socket-io-error', { transport, error })
|
|
|
|
}
|
|
|
|
this.ide.socket.reportTransportError = reportTransportError
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// handle network-level websocket errors (e.g. failed dns lookups)
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-06-05 06:15:16 -04:00
|
|
|
let connectionAttempt = 1
|
2020-05-19 05:02:56 -04:00
|
|
|
let connectionErrorHandler = err => {
|
2020-06-05 06:15:16 -04:00
|
|
|
if (
|
|
|
|
window.wsRetryHandshake &&
|
|
|
|
connectionAttempt++ < window.wsRetryHandshake
|
|
|
|
) {
|
|
|
|
return setTimeout(() => this.ide.socket.socket.connect(), 100)
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
this.updateConnectionManagerState('error')
|
|
|
|
sl_console.log('socket.io error', err)
|
|
|
|
if (this.wsUrl && !window.location.href.match(/ws=fallback/)) {
|
|
|
|
// if we tried to load a custom websocket location and failed
|
|
|
|
// try reloading and falling back to the siteUrl
|
|
|
|
window.location = window.location.href + '?ws=fallback'
|
|
|
|
} else {
|
|
|
|
this.connected = false
|
|
|
|
return this.$scope.$apply(() => {
|
|
|
|
return (this.$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.")
|
2018-11-05 05:06:39 -05:00
|
|
|
})
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
|
|
|
}
|
|
|
|
this.ide.socket.on('error', connectionErrorHandler)
|
|
|
|
|
|
|
|
// 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.
|
|
|
|
|
|
|
|
this.ide.socket.on('connect', () => {
|
|
|
|
// state should be 'connecting'...
|
|
|
|
// remove connection error handler when connected, avoid unwanted fallbacks
|
|
|
|
this.ide.socket.removeListener('error', connectionErrorHandler)
|
|
|
|
sl_console.log('[socket.io connect] Connected')
|
|
|
|
this.updateConnectionManagerState('authenticating')
|
|
|
|
})
|
|
|
|
|
|
|
|
// The next event we should get is an authentication response
|
|
|
|
// from the server, either "connectionAccepted" or
|
|
|
|
// "connectionRejected".
|
|
|
|
|
2020-06-08 04:29:36 -04:00
|
|
|
this.ide.socket.on('connectionAccepted', (_, publicId) => {
|
|
|
|
this.ide.socket.publicId = publicId || this.ide.socket.socket.sessionid
|
2020-05-19 05:02:56 -04:00
|
|
|
// state should be 'authenticating'...
|
|
|
|
sl_console.log('[socket.io connectionAccepted] allowed to connect')
|
|
|
|
this.connected = true
|
|
|
|
this.gracefullyReconnecting = false
|
|
|
|
this.ide.pushEvent('connected')
|
|
|
|
this.updateConnectionManagerState('joining')
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.$scope.$apply(() => {
|
|
|
|
if (this.$scope.state.loading) {
|
|
|
|
return (this.$scope.state.load_progress = 70)
|
2019-08-14 11:04:04 -04:00
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
})
|
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// we have passed authentication so we can now join the project
|
|
|
|
let connectionJobId = this.$scope.connection.jobId
|
|
|
|
setTimeout(() => {
|
|
|
|
this.joinProject(connectionJobId)
|
|
|
|
}, 100)
|
|
|
|
})
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.ide.socket.on('connectionRejected', err => {
|
|
|
|
// state should be 'authenticating'...
|
|
|
|
sl_console.log(
|
|
|
|
'[socket.io connectionRejected] session not valid or other connection error'
|
|
|
|
)
|
|
|
|
// real time sends a 'retry' message if the process was shutting down
|
|
|
|
if (err && err.message === 'retry') {
|
|
|
|
return this.tryReconnectWithRateLimit()
|
|
|
|
}
|
|
|
|
// we have failed authentication, usually due to an invalid session cookie
|
|
|
|
return this.reportConnectionError(err)
|
|
|
|
})
|
|
|
|
|
|
|
|
// Alternatively the attempt to connect can fail completely, so
|
|
|
|
// we never get into the "connect" state.
|
|
|
|
|
|
|
|
this.ide.socket.on('connect_failed', () => {
|
|
|
|
this.updateConnectionManagerState('error')
|
|
|
|
this.connected = false
|
|
|
|
return this.$scope.$apply(() => {
|
|
|
|
return (this.$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.")
|
2018-11-05 05:06:39 -05:00
|
|
|
})
|
2020-05-19 05:02:56 -04:00
|
|
|
})
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// We can get a "disconnect" event at any point after the
|
|
|
|
// "connect" event.
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.ide.socket.on('disconnect', () => {
|
|
|
|
sl_console.log('[socket.io disconnect] Disconnected')
|
|
|
|
this.connected = false
|
|
|
|
this.ide.pushEvent('disconnected')
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
if (!this.$scope.connection.state.match(/^waiting/)) {
|
|
|
|
if (
|
|
|
|
!this.$scope.connection.forced_disconnect &&
|
|
|
|
!this.userIsInactive
|
|
|
|
) {
|
|
|
|
this.startAutoReconnectCountdown()
|
|
|
|
} else {
|
|
|
|
this.updateConnectionManagerState('inactive')
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
|
|
|
})
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// Site administrators can send the forceDisconnect event to all users
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.ide.socket.on('forceDisconnect', message => {
|
|
|
|
this.updateConnectionManagerState('inactive')
|
|
|
|
this.$scope.$apply(() => {
|
|
|
|
this.$scope.permissions.write = false
|
|
|
|
return (this.$scope.connection.forced_disconnect = true)
|
|
|
|
})
|
|
|
|
this.ide.socket.disconnect()
|
|
|
|
this.ide.showGenericMessageModal(
|
|
|
|
'Please Refresh',
|
|
|
|
`\
|
2019-07-18 09:23:38 -04:00
|
|
|
We're performing maintenance on Overleaf and you need to refresh the editor.
|
2018-11-05 05:06:39 -05:00
|
|
|
Sorry for any inconvenience.
|
|
|
|
The editor will refresh in automatically in 10 seconds.\
|
|
|
|
`
|
2020-05-19 05:02:56 -04:00
|
|
|
)
|
|
|
|
return setTimeout(() => location.reload(), 10 * 1000)
|
|
|
|
})
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.ide.socket.on('reconnectGracefully', () => {
|
|
|
|
sl_console.log('Reconnect gracefully')
|
|
|
|
this.reconnectGracefully()
|
|
|
|
})
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
updateConnectionManagerState(state) {
|
|
|
|
this.$scope.$apply(() => {
|
|
|
|
this.$scope.connection.jobId += 1
|
|
|
|
let jobId = this.$scope.connection.jobId
|
|
|
|
sl_console.log(
|
|
|
|
`[updateConnectionManagerState ${jobId}] from ${
|
|
|
|
this.$scope.connection.state
|
|
|
|
} to ${state}`
|
|
|
|
)
|
|
|
|
this.$scope.connection.state = state
|
|
|
|
|
|
|
|
this.$scope.connection.reconnecting = false
|
|
|
|
this.$scope.connection.stillReconnecting = false
|
|
|
|
this.$scope.connection.inactive_disconnect = false
|
|
|
|
this.$scope.connection.joining = false
|
|
|
|
this.$scope.connection.reconnection_countdown = null
|
|
|
|
|
|
|
|
if (state === 'connecting') {
|
|
|
|
// initial connection
|
|
|
|
} else if (state === 'reconnecting') {
|
|
|
|
// reconnection after a connection has failed
|
|
|
|
this.stopReconnectCountdownTimer()
|
|
|
|
this.$scope.connection.reconnecting = true
|
|
|
|
// if reconnecting takes more than 1s (it doesn't, usually) show the
|
|
|
|
// 'reconnecting...' warning
|
|
|
|
setTimeout(() => {
|
|
|
|
if (
|
|
|
|
this.$scope.connection.reconnecting &&
|
|
|
|
this.$scope.connection.jobId === jobId
|
|
|
|
) {
|
|
|
|
this.$scope.connection.stillReconnecting = true
|
|
|
|
}
|
|
|
|
}, 1000)
|
|
|
|
} else if (state === 'reconnectFailed') {
|
|
|
|
// reconnect attempt failed
|
|
|
|
} else if (state === 'authenticating') {
|
|
|
|
// socket connection has been established, trying to authenticate
|
|
|
|
} else if (state === 'joining') {
|
|
|
|
// authenticated, joining project
|
|
|
|
this.$scope.connection.joining = true
|
|
|
|
} else if (state === 'ready') {
|
|
|
|
// project has been joined
|
|
|
|
} else if (state === 'waitingCountdown') {
|
|
|
|
// disconnected and waiting to reconnect via the countdown timer
|
|
|
|
this.stopReconnectCountdownTimer()
|
|
|
|
} else if (state === 'waitingGracefully') {
|
|
|
|
// disconnected and waiting to reconnect gracefully
|
|
|
|
this.stopReconnectCountdownTimer()
|
|
|
|
} else if (state === 'inactive') {
|
|
|
|
// disconnected and not trying to reconnect (inactive)
|
|
|
|
} else if (state === 'error') {
|
|
|
|
// something is wrong
|
|
|
|
} else {
|
2019-09-10 10:31:08 -04:00
|
|
|
sl_console.log(
|
2020-05-19 05:02:56 -04:00
|
|
|
`[WARN] [updateConnectionManagerState ${jobId}] got unrecognised state ${state}`
|
2019-09-10 10:31:08 -04:00
|
|
|
)
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
})
|
|
|
|
}
|
2019-09-10 10:31:08 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
expectConnectionManagerState(state, jobId) {
|
|
|
|
if (
|
|
|
|
this.$scope.connection.state === state &&
|
|
|
|
(!jobId || jobId === this.$scope.connection.jobId)
|
|
|
|
) {
|
|
|
|
return true
|
2019-09-10 10:31:08 -04:00
|
|
|
}
|
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
sl_console.log(
|
|
|
|
`[WARN] [state mismatch] expected state ${state}${
|
|
|
|
jobId ? '/' + jobId : ''
|
|
|
|
} when in ${this.$scope.connection.state}/${
|
|
|
|
this.$scope.connection.jobId
|
|
|
|
}`
|
|
|
|
)
|
|
|
|
return false
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
// Error reporting, which can reload the page if appropriate
|
|
|
|
|
|
|
|
reportConnectionError(err) {
|
|
|
|
sl_console.log('[socket.io] reporting connection error')
|
|
|
|
this.updateConnectionManagerState('error')
|
|
|
|
if (
|
|
|
|
(err != null ? err.message : undefined) === 'not authorized' ||
|
|
|
|
(err != null ? err.message : undefined) === 'invalid session'
|
|
|
|
) {
|
|
|
|
return (window.location = `/login?redir=${encodeURI(
|
|
|
|
window.location.pathname
|
|
|
|
)}`)
|
|
|
|
} else {
|
|
|
|
this.ide.socket.disconnect()
|
|
|
|
return this.ide.showGenericMessageModal(
|
|
|
|
'Something went wrong connecting',
|
|
|
|
`\
|
2018-11-05 05:06:39 -05:00
|
|
|
Something went wrong connecting to your project. Please refresh if this continues to happen.\
|
|
|
|
`
|
2020-05-19 05:02:56 -04:00
|
|
|
)
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
joinProject(connectionId) {
|
|
|
|
sl_console.log(`[joinProject ${connectionId}] 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
|
|
|
|
if (!this.expectConnectionManagerState('joining', connectionId)) {
|
|
|
|
sl_console.log(
|
|
|
|
`[joinProject ${connectionId}] aborting with stale connection`
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
const data = {
|
|
|
|
project_id: this.ide.project_id
|
|
|
|
}
|
|
|
|
if (window.anonymousAccessToken) {
|
|
|
|
data.anonymousAccessToken = window.anonymousAccessToken
|
|
|
|
}
|
|
|
|
this.ide.socket.emit(
|
|
|
|
'joinProject',
|
|
|
|
data,
|
|
|
|
(err, project, permissionsLevel, protocolVersion) => {
|
|
|
|
if (err != null || project == null) {
|
|
|
|
if (err.code === 'TooManyRequests') {
|
|
|
|
sl_console.log(
|
|
|
|
`[joinProject ${connectionId}] retrying: ${err.message}`
|
|
|
|
)
|
|
|
|
setTimeout(
|
|
|
|
() => this.joinProject(connectionId),
|
|
|
|
this.joinProjectRetryInterval
|
|
|
|
)
|
|
|
|
if (
|
|
|
|
this.joinProjectRetryInterval <
|
|
|
|
this.JOIN_PROJECT_MAX_RETRY_INTERVAL
|
|
|
|
) {
|
|
|
|
this.joinProjectRetryInterval += this.JOIN_PROJECT_RETRY_INTERVAL
|
2019-09-10 10:31:08 -04:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
return
|
|
|
|
} else {
|
|
|
|
return this.reportConnectionError(err)
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.joinProjectRetryInterval = this.JOIN_PROJECT_RETRY_INTERVAL
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
if (
|
|
|
|
this.$scope.protocolVersion != null &&
|
|
|
|
this.$scope.protocolVersion !== protocolVersion
|
|
|
|
) {
|
|
|
|
location.reload(true)
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.$scope.$apply(() => {
|
|
|
|
this.updateConnectionManagerState('ready')
|
|
|
|
this.$scope.protocolVersion = protocolVersion
|
|
|
|
this.$scope.project = project
|
|
|
|
this.$scope.permissionsLevel = permissionsLevel
|
|
|
|
this.$scope.state.load_progress = 100
|
|
|
|
this.$scope.state.loading = false
|
|
|
|
this.$scope.$broadcast('project:joined')
|
|
|
|
})
|
2019-08-14 11:04:04 -04:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
)
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
reconnectImmediately() {
|
|
|
|
this.disconnect()
|
|
|
|
return this.tryReconnect()
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
disconnect() {
|
|
|
|
if (this.ide.socket.socket && !this.ide.socket.socket.connected) {
|
|
|
|
sl_console.log(
|
|
|
|
'[socket.io] skipping disconnect because socket.io has not connected'
|
|
|
|
)
|
|
|
|
return
|
|
|
|
}
|
|
|
|
sl_console.log('[socket.io] disconnecting client')
|
|
|
|
return this.ide.socket.disconnect()
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
startAutoReconnectCountdown() {
|
|
|
|
this.updateConnectionManagerState('waitingCountdown')
|
|
|
|
let connectionId = this.$scope.connection.jobId
|
|
|
|
let countdown
|
|
|
|
sl_console.log('[ConnectionManager] starting autoreconnect countdown')
|
|
|
|
const twoMinutes = 2 * 60 * 1000
|
|
|
|
if (
|
|
|
|
this.lastUserAction != null &&
|
|
|
|
new Date() - this.lastUserAction > twoMinutes
|
|
|
|
) {
|
|
|
|
// between 1 minute and 3 minutes
|
|
|
|
countdown = 60 + Math.floor(Math.random() * 120)
|
|
|
|
} else {
|
|
|
|
countdown = 3 + Math.floor(Math.random() * 7)
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
if (this.userIsLeavingPage) {
|
|
|
|
// user will have pressed refresh or back etc
|
|
|
|
return
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
this.$scope.$apply(() => {
|
|
|
|
this.$scope.connection.reconnecting = false
|
|
|
|
this.$scope.connection.stillReconnecting = false
|
|
|
|
this.$scope.connection.joining = false
|
|
|
|
this.$scope.connection.reconnection_countdown = countdown
|
|
|
|
})
|
|
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
if (!this.connected && !this.countdownTimeoutId) {
|
|
|
|
this.countdownTimeoutId = setTimeout(
|
|
|
|
() => this.decreaseCountdown(connectionId),
|
|
|
|
1000
|
2018-11-05 05:06:39 -05:00
|
|
|
)
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}, 200)
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
stopReconnectCountdownTimer() {
|
|
|
|
// clear timeout and set to null so we know there is no countdown running
|
|
|
|
if (this.countdownTimeoutId != null) {
|
|
|
|
sl_console.log(
|
|
|
|
'[ConnectionManager] cancelling existing reconnect timer'
|
|
|
|
)
|
|
|
|
clearTimeout(this.countdownTimeoutId)
|
2019-09-10 10:31:08 -04:00
|
|
|
this.countdownTimeoutId = null
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
|
|
|
}
|
2019-09-10 10:31:08 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
decreaseCountdown(connectionId) {
|
|
|
|
this.countdownTimeoutId = null
|
|
|
|
if (this.$scope.connection.reconnection_countdown == null) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
if (
|
|
|
|
!this.expectConnectionManagerState('waitingCountdown', connectionId)
|
|
|
|
) {
|
2018-11-05 05:06:39 -05:00
|
|
|
sl_console.log(
|
2020-05-19 05:02:56 -04:00
|
|
|
`[ConnectionManager] Aborting stale countdown ${connectionId}`
|
2018-11-05 05:06:39 -05:00
|
|
|
)
|
2020-05-19 05:02:56 -04:00
|
|
|
return
|
|
|
|
}
|
|
|
|
|
|
|
|
sl_console.log(
|
|
|
|
'[ConnectionManager] decreasing countdown',
|
|
|
|
this.$scope.connection.reconnection_countdown
|
|
|
|
)
|
|
|
|
this.$scope.$apply(() => {
|
|
|
|
this.$scope.connection.reconnection_countdown--
|
|
|
|
})
|
|
|
|
|
|
|
|
if (this.$scope.connection.reconnection_countdown <= 0) {
|
|
|
|
this.$scope.connection.reconnecting = false
|
2018-11-05 05:06:39 -05:00
|
|
|
this.$scope.$apply(() => {
|
2020-05-19 05:02:56 -04:00
|
|
|
this.tryReconnect()
|
2018-11-05 05:06:39 -05:00
|
|
|
})
|
2020-05-19 05:02:56 -04:00
|
|
|
} else {
|
|
|
|
this.countdownTimeoutId = setTimeout(
|
|
|
|
() => this.decreaseCountdown(connectionId),
|
|
|
|
1000
|
|
|
|
)
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
tryReconnect() {
|
|
|
|
sl_console.log('[ConnectionManager] tryReconnect')
|
|
|
|
if (this.connected || this.$scope.connection.reconnecting) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
this.updateConnectionManagerState('reconnecting')
|
|
|
|
sl_console.log('[ConnectionManager] Starting new connection')
|
2020-03-30 06:19:48 -04:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
const removeHandler = () => {
|
|
|
|
this.ide.socket.removeListener('error', handleFailure)
|
|
|
|
this.ide.socket.removeListener('connect', handleSuccess)
|
|
|
|
}
|
|
|
|
const handleFailure = () => {
|
|
|
|
sl_console.log('[ConnectionManager] tryReconnect: failed')
|
|
|
|
removeHandler()
|
|
|
|
this.updateConnectionManagerState('reconnectFailed')
|
|
|
|
this.tryReconnectWithRateLimit({ force: true })
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
const handleSuccess = () => {
|
|
|
|
sl_console.log('[ConnectionManager] tryReconnect: success')
|
|
|
|
removeHandler()
|
|
|
|
}
|
|
|
|
this.ide.socket.on('error', handleFailure)
|
|
|
|
this.ide.socket.on('connect', handleSuccess)
|
2020-06-30 08:04:31 -04:00
|
|
|
const reportTransportError = this.ide.socket.reportTransportError
|
|
|
|
this.ide.socket.on('error', reportTransportError)
|
|
|
|
this.ide.socket.on('connect_failed', reportTransportError)
|
|
|
|
transport = 'handshake'
|
2020-05-19 05:02:56 -04:00
|
|
|
|
|
|
|
// use socket.io connect() here to make a single attempt, the
|
|
|
|
// reconnect() method makes multiple attempts
|
|
|
|
this.ide.socket.socket.connect()
|
|
|
|
// record the time of the last attempt to connect
|
|
|
|
this.lastConnectionAttempt = new Date()
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
tryReconnectWithRateLimit(options) {
|
|
|
|
// bail out if the reconnect is already in progress
|
|
|
|
if (this.$scope.connection.reconnecting || this.connected) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// bail out if we are going to reconnect soon anyway
|
|
|
|
const reconnectingSoon =
|
|
|
|
this.$scope.connection.reconnection_countdown != null &&
|
|
|
|
this.$scope.connection.reconnection_countdown <= 5
|
|
|
|
const clickedTryNow = options != null ? options.force : undefined // user requested reconnection
|
|
|
|
if (reconnectingSoon && !clickedTryNow) {
|
|
|
|
return
|
|
|
|
}
|
|
|
|
// bail out if we tried reconnecting recently
|
|
|
|
const allowedInterval = clickedTryNow
|
|
|
|
? this.MIN_RETRY_INTERVAL
|
|
|
|
: this.BACKGROUND_RETRY_INTERVAL
|
|
|
|
if (
|
|
|
|
this.lastConnectionAttempt != null &&
|
|
|
|
new Date() - this.lastConnectionAttempt < allowedInterval
|
|
|
|
) {
|
|
|
|
if (this.$scope.connection.state !== 'waitingCountdown') {
|
|
|
|
this.startAutoReconnectCountdown()
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
return
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
this.tryReconnect()
|
|
|
|
}
|
2018-11-05 05:06:39 -05:00
|
|
|
|
2020-05-19 05:02:56 -04:00
|
|
|
disconnectIfInactive() {
|
|
|
|
this.userIsInactive =
|
|
|
|
new Date() - this.lastUserAction > this.disconnectAfterMs
|
|
|
|
if (this.userIsInactive && this.connected) {
|
|
|
|
this.disconnect()
|
|
|
|
return this.$scope.$apply(() => {
|
|
|
|
return (this.$scope.connection.inactive_disconnect = true)
|
|
|
|
}) // 5 minutes
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
}
|
|
|
|
reconnectGracefully(force) {
|
|
|
|
if (this.reconnectGracefullyStarted == null) {
|
|
|
|
this.reconnectGracefullyStarted = new Date()
|
|
|
|
} else {
|
|
|
|
if (!force) {
|
2018-11-05 05:06:39 -05:00
|
|
|
sl_console.log(
|
2020-05-19 05:02:56 -04:00
|
|
|
'[reconnectGracefully] reconnection is already in process, so skipping'
|
2018-11-05 05:06:39 -05:00
|
|
|
)
|
2020-05-19 05:02:56 -04:00
|
|
|
return
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
const userIsInactive =
|
|
|
|
new Date() - this.lastUserAction >
|
|
|
|
this.RECONNECT_GRACEFULLY_RETRY_INTERVAL
|
|
|
|
const maxIntervalReached =
|
|
|
|
new Date() - this.reconnectGracefullyStarted >
|
|
|
|
this.MAX_RECONNECT_GRACEFULLY_INTERVAL
|
|
|
|
if (userIsInactive || maxIntervalReached) {
|
|
|
|
sl_console.log(
|
|
|
|
"[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
|
|
|
|
)
|
|
|
|
this._reconnectGracefullyNow()
|
|
|
|
} else {
|
|
|
|
sl_console.log(
|
|
|
|
'[reconnectGracefully] User is working, will try again in 5 seconds'
|
|
|
|
)
|
|
|
|
this.updateConnectionManagerState('waitingGracefully')
|
|
|
|
setTimeout(() => {
|
|
|
|
this.reconnectGracefully(true)
|
|
|
|
}, this.RECONNECT_GRACEFULLY_RETRY_INTERVAL)
|
2018-11-05 05:06:39 -05:00
|
|
|
}
|
|
|
|
}
|
2020-05-19 05:02:56 -04:00
|
|
|
|
|
|
|
_reconnectGracefullyNow() {
|
|
|
|
this.gracefullyReconnecting = true
|
|
|
|
this.reconnectGracefullyStarted = null
|
|
|
|
// Clear cookie so we don't go to the same backend server
|
|
|
|
$.cookie('SERVERID', '', { expires: -1, path: '/' })
|
|
|
|
return this.reconnectImmediately()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
ConnectionManager.initClass()
|
|
|
|
return ConnectionManager
|
|
|
|
})())
|