ConnectedUsersManager.js 5.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179
  1. /* eslint-disable
  2. camelcase,
  3. */
  4. const async = require('async')
  5. const Settings = require('@overleaf/settings')
  6. const logger = require('logger-sharelatex')
  7. const redis = require('@overleaf/redis-wrapper')
  8. const OError = require('@overleaf/o-error')
  9. const rclient = redis.createClient(Settings.redis.realtime)
  10. const Keys = Settings.redis.realtime.key_schema
  11. const ONE_HOUR_IN_S = 60 * 60
  12. const ONE_DAY_IN_S = ONE_HOUR_IN_S * 24
  13. const FOUR_DAYS_IN_S = ONE_DAY_IN_S * 4
  14. const USER_TIMEOUT_IN_S = ONE_HOUR_IN_S / 4
  15. const REFRESH_TIMEOUT_IN_S = 10 // only show clients which have responded to a refresh request in the last 10 seconds
  16. module.exports = {
  17. // Use the same method for when a user connects, and when a user sends a cursor
  18. // update. This way we don't care if the connected_user key has expired when
  19. // we receive a cursor update.
  20. updateUserPosition(project_id, client_id, user, cursorData, callback) {
  21. logger.debug(
  22. { project_id, client_id },
  23. 'marking user as joined or connected'
  24. )
  25. const multi = rclient.multi()
  26. multi.sadd(Keys.clientsInProject({ project_id }), client_id)
  27. multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S)
  28. multi.hset(
  29. Keys.connectedUser({ project_id, client_id }),
  30. 'last_updated_at',
  31. Date.now()
  32. )
  33. multi.hset(
  34. Keys.connectedUser({ project_id, client_id }),
  35. 'user_id',
  36. user._id
  37. )
  38. multi.hset(
  39. Keys.connectedUser({ project_id, client_id }),
  40. 'first_name',
  41. user.first_name || ''
  42. )
  43. multi.hset(
  44. Keys.connectedUser({ project_id, client_id }),
  45. 'last_name',
  46. user.last_name || ''
  47. )
  48. multi.hset(
  49. Keys.connectedUser({ project_id, client_id }),
  50. 'email',
  51. user.email || ''
  52. )
  53. if (cursorData) {
  54. multi.hset(
  55. Keys.connectedUser({ project_id, client_id }),
  56. 'cursorData',
  57. JSON.stringify(cursorData)
  58. )
  59. }
  60. multi.expire(
  61. Keys.connectedUser({ project_id, client_id }),
  62. USER_TIMEOUT_IN_S
  63. )
  64. multi.exec(function (err) {
  65. if (err) {
  66. err = new OError('problem marking user as connected').withCause(err)
  67. }
  68. callback(err)
  69. })
  70. },
  71. refreshClient(project_id, client_id) {
  72. logger.debug({ project_id, client_id }, 'refreshing connected client')
  73. const multi = rclient.multi()
  74. multi.hset(
  75. Keys.connectedUser({ project_id, client_id }),
  76. 'last_updated_at',
  77. Date.now()
  78. )
  79. multi.expire(
  80. Keys.connectedUser({ project_id, client_id }),
  81. USER_TIMEOUT_IN_S
  82. )
  83. multi.exec(function (err) {
  84. if (err) {
  85. logger.err(
  86. { err, project_id, client_id },
  87. 'problem refreshing connected client'
  88. )
  89. }
  90. })
  91. },
  92. markUserAsDisconnected(project_id, client_id, callback) {
  93. logger.debug({ project_id, client_id }, 'marking user as disconnected')
  94. const multi = rclient.multi()
  95. multi.srem(Keys.clientsInProject({ project_id }), client_id)
  96. multi.expire(Keys.clientsInProject({ project_id }), FOUR_DAYS_IN_S)
  97. multi.del(Keys.connectedUser({ project_id, client_id }))
  98. multi.exec(function (err) {
  99. if (err) {
  100. err = new OError('problem marking user as disconnected').withCause(err)
  101. }
  102. callback(err)
  103. })
  104. },
  105. _getConnectedUser(project_id, client_id, callback) {
  106. rclient.hgetall(
  107. Keys.connectedUser({ project_id, client_id }),
  108. function (err, result) {
  109. if (err) {
  110. err = new OError('problem fetching connected user details', {
  111. other_client_id: client_id,
  112. }).withCause(err)
  113. return callback(err)
  114. }
  115. if (!(result && result.user_id)) {
  116. result = {
  117. connected: false,
  118. client_id,
  119. }
  120. } else {
  121. result.connected = true
  122. result.client_id = client_id
  123. result.client_age =
  124. (Date.now() - parseInt(result.last_updated_at, 10)) / 1000
  125. if (result.cursorData) {
  126. try {
  127. result.cursorData = JSON.parse(result.cursorData)
  128. } catch (e) {
  129. OError.tag(e, 'error parsing cursorData JSON', {
  130. other_client_id: client_id,
  131. cursorData: result.cursorData,
  132. })
  133. return callback(e)
  134. }
  135. }
  136. }
  137. callback(err, result)
  138. }
  139. )
  140. },
  141. getConnectedUsers(project_id, callback) {
  142. const self = this
  143. rclient.smembers(
  144. Keys.clientsInProject({ project_id }),
  145. function (err, results) {
  146. if (err) {
  147. err = new OError('problem getting clients in project').withCause(err)
  148. return callback(err)
  149. }
  150. const jobs = results.map(
  151. client_id => cb => self._getConnectedUser(project_id, client_id, cb)
  152. )
  153. async.series(jobs, function (err, users) {
  154. if (err) {
  155. OError.tag(err, 'problem getting connected users')
  156. return callback(err)
  157. }
  158. users = users.filter(
  159. user =>
  160. user && user.connected && user.client_age < REFRESH_TIMEOUT_IN_S
  161. )
  162. callback(null, users)
  163. })
  164. }
  165. )
  166. },
  167. }