ConnectionManager.js 25 KB

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