ConnectionManager.js 24 KB

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