ConnectionManager.js 25 KB

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