ConnectionManager.js 25 KB

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