ConnectionManager.js 24 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685
  1. // TODO: This file was created by bulk-decaffeinate.
  2. // Fix any style issues and re-enable lint.
  3. /*
  4. * decaffeinate suggestions:
  5. * DS102: Remove unnecessary code created because of implicit returns
  6. * DS206: Consider reworking classes to avoid initClass
  7. * DS207: Consider shorter variations of null checks
  8. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  9. */
  10. /* global io */
  11. import SocketIoShim from './SocketIoShim'
  12. let ConnectionManager
  13. const ONEHOUR = 1000 * 60 * 60
  14. export default ConnectionManager = (function () {
  15. ConnectionManager = class ConnectionManager {
  16. static initClass() {
  17. this.prototype.disconnectAfterMs = ONEHOUR * 24
  18. this.prototype.lastUserAction = new Date()
  19. this.prototype.MIN_RETRY_INTERVAL = 1000 // ms, rate limit on reconnects for user clicking "try now"
  20. this.prototype.BACKGROUND_RETRY_INTERVAL = 5 * 1000
  21. this.prototype.JOIN_PROJECT_RETRY_INTERVAL = 5000
  22. this.prototype.JOIN_PROJECT_MAX_RETRY_INTERVAL = 60000
  23. this.prototype.RECONNECT_GRACEFULLY_RETRY_INTERVAL = 5000 // ms
  24. this.prototype.MAX_RECONNECT_GRACEFULLY_INTERVAL = 45 * 1000
  25. }
  26. constructor(ide, $scope) {
  27. this.ide = ide
  28. this.$scope = $scope
  29. this.wsUrl = ide.wsUrl || null // websocket url (if defined)
  30. if (typeof io === 'undefined' || io === null) {
  31. if (this.wsUrl && !window.location.href.match(/ws=fallback/)) {
  32. // if we tried to boot from a custom real-time backend and failed,
  33. // try reloading and falling back to the siteUrl
  34. window.location = window.location.href + '?ws=fallback'
  35. }
  36. console.error(
  37. 'Socket.io javascript not loaded. Please check that the real-time service is running and accessible.'
  38. )
  39. this.ide.socket = SocketIoShim.stub()
  40. this.$scope.$apply(() => {
  41. return (this.$scope.state.error =
  42. 'Could not connect to websocket server :(')
  43. })
  44. return
  45. }
  46. setInterval(() => {
  47. return this.disconnectIfInactive()
  48. }, ONEHOUR)
  49. // trigger a reconnect immediately if network comes back online
  50. window.addEventListener('online', () => {
  51. sl_console.log('[online] browser notified online')
  52. if (!this.connected) {
  53. return this.tryReconnectWithRateLimit({ force: true })
  54. }
  55. })
  56. this.userIsLeavingPage = false
  57. window.addEventListener('beforeunload', () => {
  58. this.userIsLeavingPage = true
  59. }) // Don't return true or it will show a pop up
  60. this.connected = false
  61. this.userIsInactive = false
  62. this.gracefullyReconnecting = false
  63. this.shuttingDown = false
  64. this.joinProjectRetryInterval = this.JOIN_PROJECT_RETRY_INTERVAL
  65. this.$scope.connection = {
  66. debug: sl_debugging,
  67. reconnecting: false,
  68. stillReconnecting: false,
  69. // If we need to force everyone to reload the editor
  70. forced_disconnect: false,
  71. inactive_disconnect: false,
  72. jobId: 0,
  73. }
  74. this.$scope.tryReconnectNow = () => {
  75. // user manually requested reconnection via "Try now" button
  76. return this.tryReconnectWithRateLimit({ force: true })
  77. }
  78. this.$scope.$on('cursor:editor:update', () => {
  79. this.lastUserAction = new Date() // time of last edit
  80. if (!this.connected) {
  81. // user is editing, try to reconnect
  82. return this.tryReconnectWithRateLimit()
  83. }
  84. })
  85. document.querySelector('body').addEventListener('click', e => {
  86. if (
  87. !this.shuttingDown &&
  88. !this.connected &&
  89. e.target.id !== 'try-reconnect-now-button'
  90. ) {
  91. // user is editing, try to reconnect
  92. return this.tryReconnectWithRateLimit()
  93. }
  94. })
  95. // initial connection attempt
  96. this.updateConnectionManagerState('connecting')
  97. let parsedURL
  98. try {
  99. parsedURL = new URL(this.wsUrl || '/socket.io', window.location)
  100. } catch (e) {
  101. // hello IE11
  102. if (
  103. this.wsUrl &&
  104. this.wsUrl.indexOf('/') !== 0 &&
  105. !window.location.href.match(/ws=fallback/)
  106. ) {
  107. // do not even try parsing the wsUrl, use the fallback
  108. window.location = window.location.href + '?ws=fallback'
  109. }
  110. parsedURL = {
  111. origin: null,
  112. pathname: this.wsUrl || '/socket.io',
  113. }
  114. }
  115. this.ide.socket = SocketIoShim.connect(parsedURL.origin, {
  116. resource: parsedURL.pathname.slice(1),
  117. reconnect: false,
  118. 'connect timeout': 30 * 1000,
  119. 'force new connection': true,
  120. })
  121. // handle network-level websocket errors (e.g. failed dns lookups)
  122. let connectionAttempt = 1
  123. const connectionErrorHandler = err => {
  124. if (
  125. window.wsRetryHandshake &&
  126. connectionAttempt++ < window.wsRetryHandshake
  127. ) {
  128. return setTimeout(() => this.ide.socket.socket.connect(), 100)
  129. }
  130. this.updateConnectionManagerState('error')
  131. sl_console.log('socket.io error', err)
  132. if (this.wsUrl && !window.location.href.match(/ws=fallback/)) {
  133. // if we tried to load a custom websocket location and failed
  134. // try reloading and falling back to the siteUrl
  135. window.location = window.location.href + '?ws=fallback'
  136. } else {
  137. this.connected = false
  138. return this.$scope.$apply(() => {
  139. return (this.$scope.state.error =
  140. "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
  141. })
  142. }
  143. }
  144. this.ide.socket.on('error', connectionErrorHandler)
  145. // The "connect" event is the first event we get back. It only
  146. // indicates that the websocket is connected, we still need to
  147. // pass authentication to join a project.
  148. this.ide.socket.on('connect', () => {
  149. // state should be 'connecting'...
  150. // remove connection error handler when connected, avoid unwanted fallbacks
  151. this.ide.socket.removeListener('error', connectionErrorHandler)
  152. sl_console.log('[socket.io connect] Connected')
  153. this.updateConnectionManagerState('authenticating')
  154. })
  155. // The next event we should get is an authentication response
  156. // from the server, either "connectionAccepted" or
  157. // "connectionRejected".
  158. this.ide.socket.on('connectionAccepted', (_, publicId) => {
  159. this.ide.socket.publicId = publicId || this.ide.socket.socket.sessionid
  160. // state should be 'authenticating'...
  161. sl_console.log('[socket.io connectionAccepted] allowed to connect')
  162. this.connected = true
  163. this.gracefullyReconnecting = false
  164. this.ide.pushEvent('connected')
  165. this.updateConnectionManagerState('joining')
  166. this.$scope.$apply(() => {
  167. if (this.$scope.state.loading) {
  168. this.$scope.state.load_progress = 70
  169. }
  170. })
  171. // we have passed authentication so we can now join the project
  172. const connectionJobId = this.$scope.connection.jobId
  173. setTimeout(() => {
  174. this.joinProject(connectionJobId)
  175. }, 100)
  176. })
  177. this.ide.socket.on('connectionRejected', err => {
  178. // state should be 'authenticating'...
  179. sl_console.log(
  180. '[socket.io connectionRejected] session not valid or other connection error'
  181. )
  182. // real time sends a 'retry' message if the process was shutting down
  183. if (err && err.message === 'retry') {
  184. return this.tryReconnectWithRateLimit()
  185. }
  186. // we have failed authentication, usually due to an invalid session cookie
  187. return this.reportConnectionError(err)
  188. })
  189. // Alternatively the attempt to connect can fail completely, so
  190. // we never get into the "connect" state.
  191. this.ide.socket.on('connect_failed', () => {
  192. this.updateConnectionManagerState('error')
  193. this.connected = false
  194. return this.$scope.$apply(() => {
  195. return (this.$scope.state.error =
  196. "Unable to connect, please view the <u><a href='/learn/Kb/Connection_problems'>connection problems guide</a></u> to fix the issue.")
  197. })
  198. })
  199. // We can get a "disconnect" event at any point after the
  200. // "connect" event.
  201. this.ide.socket.on('disconnect', () => {
  202. sl_console.log('[socket.io disconnect] Disconnected')
  203. this.connected = false
  204. this.ide.pushEvent('disconnected')
  205. if (!this.$scope.connection.state.match(/^waiting/)) {
  206. if (
  207. !this.$scope.connection.forced_disconnect &&
  208. !this.userIsInactive &&
  209. !this.shuttingDown
  210. ) {
  211. this.startAutoReconnectCountdown()
  212. } else {
  213. this.updateConnectionManagerState('inactive')
  214. }
  215. }
  216. })
  217. // Site administrators can send the forceDisconnect event to all users
  218. this.ide.socket.on('forceDisconnect', (message, delay = 10) => {
  219. this.updateConnectionManagerState('inactive')
  220. this.shuttingDown = true // prevent reconnection attempts
  221. this.$scope.$apply(() => {
  222. this.$scope.permissions.write = false
  223. return (this.$scope.connection.forced_disconnect = true)
  224. })
  225. // flush changes before disconnecting
  226. this.ide.$scope.$broadcast('flush-changes')
  227. setTimeout(() => this.ide.socket.disconnect(), 1000)
  228. this.ide.showLockEditorMessageModal(
  229. 'Please wait',
  230. `\
  231. We're performing maintenance on Overleaf and you need to wait a moment.
  232. Sorry for any inconvenience.
  233. The editor will refresh automatically in ${delay} seconds.\
  234. `
  235. )
  236. return setTimeout(() => location.reload(), delay * 1000)
  237. })
  238. this.ide.socket.on('reconnectGracefully', () => {
  239. sl_console.log('Reconnect gracefully')
  240. this.reconnectGracefully()
  241. })
  242. this.ide.socket.on('unregisterServiceWorker', () => {
  243. sl_console.log('Unregister service worker')
  244. this.$scope.$broadcast('service-worker:unregister')
  245. })
  246. }
  247. updateConnectionManagerState(state) {
  248. this.$scope.$apply(() => {
  249. this.$scope.connection.jobId += 1
  250. const jobId = this.$scope.connection.jobId
  251. sl_console.log(
  252. `[updateConnectionManagerState ${jobId}] from ${this.$scope.connection.state} to ${state}`
  253. )
  254. this.$scope.connection.state = state
  255. this.$scope.connection.reconnecting = false
  256. this.$scope.connection.stillReconnecting = false
  257. this.$scope.connection.inactive_disconnect = false
  258. this.$scope.connection.joining = false
  259. this.$scope.connection.reconnection_countdown = null
  260. if (state === 'connecting') {
  261. // initial connection
  262. } else if (state === 'reconnecting') {
  263. // reconnection after a connection has failed
  264. this.stopReconnectCountdownTimer()
  265. this.$scope.connection.reconnecting = true
  266. // if reconnecting takes more than 1s (it doesn't, usually) show the
  267. // 'reconnecting...' warning
  268. setTimeout(() => {
  269. if (
  270. this.$scope.connection.reconnecting &&
  271. this.$scope.connection.jobId === jobId
  272. ) {
  273. this.$scope.connection.stillReconnecting = true
  274. }
  275. }, 1000)
  276. } else if (state === 'reconnectFailed') {
  277. // reconnect attempt failed
  278. } else if (state === 'authenticating') {
  279. // socket connection has been established, trying to authenticate
  280. } else if (state === 'joining') {
  281. // authenticated, joining project
  282. this.$scope.connection.joining = true
  283. } else if (state === 'ready') {
  284. // project has been joined
  285. } else if (state === 'waitingCountdown') {
  286. // disconnected and waiting to reconnect via the countdown timer
  287. this.stopReconnectCountdownTimer()
  288. } else if (state === 'waitingGracefully') {
  289. // disconnected and waiting to reconnect gracefully
  290. this.stopReconnectCountdownTimer()
  291. } else if (state === 'inactive') {
  292. // disconnected and not trying to reconnect (inactive)
  293. } else if (state === 'error') {
  294. // something is wrong
  295. } else {
  296. sl_console.log(
  297. `[WARN] [updateConnectionManagerState ${jobId}] got unrecognised state ${state}`
  298. )
  299. }
  300. })
  301. }
  302. expectConnectionManagerState(state, jobId) {
  303. if (
  304. this.$scope.connection.state === state &&
  305. (!jobId || jobId === this.$scope.connection.jobId)
  306. ) {
  307. return true
  308. }
  309. sl_console.log(
  310. `[WARN] [state mismatch] expected state ${state}${
  311. jobId ? '/' + jobId : ''
  312. } when in ${this.$scope.connection.state}/${
  313. this.$scope.connection.jobId
  314. }`
  315. )
  316. return false
  317. }
  318. // Error reporting, which can reload the page if appropriate
  319. reportConnectionError(err) {
  320. sl_console.log('[socket.io] reporting connection error')
  321. this.updateConnectionManagerState('error')
  322. if (
  323. (err != null ? err.message : undefined) === 'not authorized' ||
  324. (err != null ? err.message : undefined) === 'invalid session'
  325. ) {
  326. return (window.location = `/login?redir=${encodeURI(
  327. window.location.pathname
  328. )}`)
  329. } else {
  330. this.ide.socket.disconnect()
  331. return this.ide.showGenericMessageModal(
  332. 'Something went wrong connecting',
  333. `\
  334. Something went wrong connecting to your project. Please refresh if this continues to happen.\
  335. `
  336. )
  337. }
  338. }
  339. joinProject(connectionId) {
  340. sl_console.log(`[joinProject ${connectionId}] joining...`)
  341. // Note: if the "joinProject" message doesn't reach the server
  342. // (e.g. if we are in a disconnected state at this point) the
  343. // callback will never be executed
  344. if (!this.expectConnectionManagerState('joining', connectionId)) {
  345. sl_console.log(
  346. `[joinProject ${connectionId}] aborting with stale connection`
  347. )
  348. return
  349. }
  350. const data = {
  351. project_id: this.ide.project_id,
  352. }
  353. if (window.anonymousAccessToken) {
  354. data.anonymousAccessToken = window.anonymousAccessToken
  355. }
  356. this.ide.socket.emit(
  357. 'joinProject',
  358. data,
  359. (err, project, permissionsLevel, protocolVersion) => {
  360. if (err != null || project == null) {
  361. err = err || {}
  362. if (err.code === 'ProjectNotFound') {
  363. // A stale browser tab tried to join a deleted project.
  364. // Reloading the page will render a 404.
  365. this.ide
  366. .showGenericMessageModal(
  367. 'Project has been deleted',
  368. 'This project has been deleted by the owner.'
  369. )
  370. .result.then(() => location.reload(true))
  371. return
  372. }
  373. if (err.code === 'TooManyRequests') {
  374. sl_console.log(
  375. `[joinProject ${connectionId}] retrying: ${err.message}`
  376. )
  377. setTimeout(
  378. () => this.joinProject(connectionId),
  379. this.joinProjectRetryInterval
  380. )
  381. if (
  382. this.joinProjectRetryInterval <
  383. this.JOIN_PROJECT_MAX_RETRY_INTERVAL
  384. ) {
  385. this.joinProjectRetryInterval += this.JOIN_PROJECT_RETRY_INTERVAL
  386. }
  387. return
  388. } else {
  389. return this.reportConnectionError(err)
  390. }
  391. }
  392. this.joinProjectRetryInterval = this.JOIN_PROJECT_RETRY_INTERVAL
  393. if (
  394. this.$scope.protocolVersion != null &&
  395. this.$scope.protocolVersion !== protocolVersion
  396. ) {
  397. location.reload(true)
  398. }
  399. this.$scope.$apply(() => {
  400. this.updateConnectionManagerState('ready')
  401. this.$scope.protocolVersion = protocolVersion
  402. const defaultProjectAttributes = { rootDoc_id: null }
  403. this.$scope.project = { ...defaultProjectAttributes, ...project }
  404. this.$scope.permissionsLevel = permissionsLevel
  405. this.ide.loadingManager.socketLoaded()
  406. window.dispatchEvent(
  407. new CustomEvent('project:joined', { detail: this.$scope.project })
  408. )
  409. this.$scope.$broadcast('project:joined')
  410. })
  411. }
  412. )
  413. }
  414. reconnectImmediately() {
  415. this.disconnect()
  416. return this.tryReconnect()
  417. }
  418. disconnect(options) {
  419. if (options && options.permanent) {
  420. sl_console.log('[disconnect] shutting down ConnectionManager')
  421. this.updateConnectionManagerState('inactive')
  422. this.shuttingDown = true // prevent reconnection attempts
  423. } else if (this.ide.socket.socket && !this.ide.socket.socket.connected) {
  424. sl_console.log(
  425. '[socket.io] skipping disconnect because socket.io has not connected'
  426. )
  427. return
  428. }
  429. sl_console.log('[socket.io] disconnecting client')
  430. return this.ide.socket.disconnect()
  431. }
  432. startAutoReconnectCountdown() {
  433. this.updateConnectionManagerState('waitingCountdown')
  434. const connectionId = this.$scope.connection.jobId
  435. let countdown
  436. sl_console.log('[ConnectionManager] starting autoreconnect countdown')
  437. const twoMinutes = 2 * 60 * 1000
  438. if (
  439. this.lastUserAction != null &&
  440. new Date() - this.lastUserAction > twoMinutes
  441. ) {
  442. // between 1 minute and 3 minutes
  443. countdown = 60 + Math.floor(Math.random() * 120)
  444. } else {
  445. countdown = 3 + Math.floor(Math.random() * 7)
  446. }
  447. if (this.userIsLeavingPage) {
  448. // user will have pressed refresh or back etc
  449. return
  450. }
  451. this.$scope.$apply(() => {
  452. this.$scope.connection.reconnecting = false
  453. this.$scope.connection.stillReconnecting = false
  454. this.$scope.connection.joining = false
  455. this.$scope.connection.reconnection_countdown = countdown
  456. })
  457. setTimeout(() => {
  458. if (!this.connected && !this.countdownTimeoutId) {
  459. this.countdownTimeoutId = setTimeout(
  460. () => this.decreaseCountdown(connectionId),
  461. 1000
  462. )
  463. }
  464. }, 200)
  465. }
  466. stopReconnectCountdownTimer() {
  467. // clear timeout and set to null so we know there is no countdown running
  468. if (this.countdownTimeoutId != null) {
  469. sl_console.log(
  470. '[ConnectionManager] cancelling existing reconnect timer'
  471. )
  472. clearTimeout(this.countdownTimeoutId)
  473. this.countdownTimeoutId = null
  474. }
  475. }
  476. decreaseCountdown(connectionId) {
  477. this.countdownTimeoutId = null
  478. if (this.$scope.connection.reconnection_countdown == null) {
  479. return
  480. }
  481. if (
  482. !this.expectConnectionManagerState('waitingCountdown', connectionId)
  483. ) {
  484. sl_console.log(
  485. `[ConnectionManager] Aborting stale countdown ${connectionId}`
  486. )
  487. return
  488. }
  489. sl_console.log(
  490. '[ConnectionManager] decreasing countdown',
  491. this.$scope.connection.reconnection_countdown
  492. )
  493. this.$scope.$apply(() => {
  494. this.$scope.connection.reconnection_countdown--
  495. })
  496. if (this.$scope.connection.reconnection_countdown <= 0) {
  497. this.$scope.connection.reconnecting = false
  498. this.$scope.$apply(() => {
  499. this.tryReconnect()
  500. })
  501. } else {
  502. this.countdownTimeoutId = setTimeout(
  503. () => this.decreaseCountdown(connectionId),
  504. 1000
  505. )
  506. }
  507. }
  508. tryReconnect() {
  509. sl_console.log('[ConnectionManager] tryReconnect')
  510. if (
  511. this.connected ||
  512. this.shuttingDown ||
  513. this.$scope.connection.reconnecting
  514. ) {
  515. return
  516. }
  517. this.updateConnectionManagerState('reconnecting')
  518. sl_console.log('[ConnectionManager] Starting new connection')
  519. const removeHandler = () => {
  520. this.ide.socket.removeListener('error', handleFailure)
  521. this.ide.socket.removeListener('connect', handleSuccess)
  522. }
  523. const handleFailure = () => {
  524. sl_console.log('[ConnectionManager] tryReconnect: failed')
  525. removeHandler()
  526. this.updateConnectionManagerState('reconnectFailed')
  527. this.tryReconnectWithRateLimit({ force: true })
  528. }
  529. const handleSuccess = () => {
  530. sl_console.log('[ConnectionManager] tryReconnect: success')
  531. removeHandler()
  532. }
  533. this.ide.socket.on('error', handleFailure)
  534. this.ide.socket.on('connect', handleSuccess)
  535. // use socket.io connect() here to make a single attempt, the
  536. // reconnect() method makes multiple attempts
  537. this.ide.socket.socket.connect()
  538. // record the time of the last attempt to connect
  539. this.lastConnectionAttempt = new Date()
  540. }
  541. tryReconnectWithRateLimit(options) {
  542. // bail out if the reconnect is already in progress
  543. if (this.$scope.connection.reconnecting || this.connected) {
  544. return
  545. }
  546. // bail out if we are going to reconnect soon anyway
  547. const reconnectingSoon =
  548. this.$scope.connection.reconnection_countdown != null &&
  549. this.$scope.connection.reconnection_countdown <= 5
  550. const clickedTryNow = options != null ? options.force : undefined // user requested reconnection
  551. if (reconnectingSoon && !clickedTryNow) {
  552. return
  553. }
  554. // bail out if we tried reconnecting recently
  555. const allowedInterval = clickedTryNow
  556. ? this.MIN_RETRY_INTERVAL
  557. : this.BACKGROUND_RETRY_INTERVAL
  558. if (
  559. this.lastConnectionAttempt != null &&
  560. new Date() - this.lastConnectionAttempt < allowedInterval
  561. ) {
  562. if (this.$scope.connection.state !== 'waitingCountdown') {
  563. this.startAutoReconnectCountdown()
  564. }
  565. return
  566. }
  567. this.tryReconnect()
  568. }
  569. disconnectIfInactive() {
  570. this.userIsInactive =
  571. new Date() - this.lastUserAction > this.disconnectAfterMs
  572. if (this.userIsInactive && this.connected) {
  573. this.disconnect()
  574. return this.$scope.$apply(() => {
  575. return (this.$scope.connection.inactive_disconnect = true)
  576. }) // 5 minutes
  577. }
  578. }
  579. reconnectGracefully(force) {
  580. if (this.reconnectGracefullyStarted == null) {
  581. this.reconnectGracefullyStarted = new Date()
  582. } else {
  583. if (!force) {
  584. sl_console.log(
  585. '[reconnectGracefully] reconnection is already in process, so skipping'
  586. )
  587. return
  588. }
  589. }
  590. const userIsInactive =
  591. new Date() - this.lastUserAction >
  592. this.RECONNECT_GRACEFULLY_RETRY_INTERVAL
  593. const maxIntervalReached =
  594. new Date() - this.reconnectGracefullyStarted >
  595. this.MAX_RECONNECT_GRACEFULLY_INTERVAL
  596. if (userIsInactive || maxIntervalReached) {
  597. sl_console.log(
  598. "[reconnectGracefully] User didn't do anything for last 5 seconds, reconnecting"
  599. )
  600. this._reconnectGracefullyNow()
  601. } else {
  602. sl_console.log(
  603. '[reconnectGracefully] User is working, will try again in 5 seconds'
  604. )
  605. this.updateConnectionManagerState('waitingGracefully')
  606. setTimeout(() => {
  607. this.reconnectGracefully(true)
  608. }, this.RECONNECT_GRACEFULLY_RETRY_INTERVAL)
  609. }
  610. }
  611. _reconnectGracefullyNow() {
  612. this.gracefullyReconnecting = true
  613. this.reconnectGracefullyStarted = null
  614. // Clear cookie so we don't go to the same backend server
  615. $.cookie('SERVERID', '', { expires: -1, path: '/' })
  616. return this.reconnectImmediately()
  617. }
  618. }
  619. ConnectionManager.initClass()
  620. return ConnectionManager
  621. })()