ConnectedUsersManager.js 5.4 KB

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