WebsocketLoadBalancer.js 6.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217
  1. /* eslint-disable
  2. camelcase,
  3. */
  4. // TODO: This file was created by bulk-decaffeinate.
  5. // Fix any style issues and re-enable lint.
  6. /*
  7. * decaffeinate suggestions:
  8. * DS101: Remove unnecessary use of Array.from
  9. * DS102: Remove unnecessary code created because of implicit returns
  10. * DS205: Consider reworking code to avoid use of IIFEs
  11. * DS207: Consider shorter variations of null checks
  12. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  13. */
  14. let WebsocketLoadBalancer
  15. const Settings = require('settings-sharelatex')
  16. const logger = require('logger-sharelatex')
  17. const RedisClientManager = require('./RedisClientManager')
  18. const SafeJsonParse = require('./SafeJsonParse')
  19. const EventLogger = require('./EventLogger')
  20. const HealthCheckManager = require('./HealthCheckManager')
  21. const RoomManager = require('./RoomManager')
  22. const ChannelManager = require('./ChannelManager')
  23. const ConnectedUsersManager = require('./ConnectedUsersManager')
  24. const RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST = [
  25. 'connectionAccepted',
  26. 'otUpdateApplied',
  27. 'otUpdateError',
  28. 'joinDoc',
  29. 'reciveNewDoc',
  30. 'reciveNewFile',
  31. 'reciveNewFolder',
  32. 'removeEntity'
  33. ]
  34. module.exports = WebsocketLoadBalancer = {
  35. rclientPubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  36. rclientSubList: RedisClientManager.createClientList(Settings.redis.pubsub),
  37. emitToRoom(room_id, message, ...payload) {
  38. if (room_id == null) {
  39. logger.warn(
  40. { message, payload },
  41. 'no room_id provided, ignoring emitToRoom'
  42. )
  43. return
  44. }
  45. const data = JSON.stringify({
  46. room_id,
  47. message,
  48. payload
  49. })
  50. logger.log(
  51. { room_id, message, payload, length: data.length },
  52. 'emitting to room'
  53. )
  54. return Array.from(this.rclientPubList).map((rclientPub) =>
  55. ChannelManager.publish(rclientPub, 'editor-events', room_id, data)
  56. )
  57. },
  58. emitToAll(message, ...payload) {
  59. return this.emitToRoom('all', message, ...Array.from(payload))
  60. },
  61. listenForEditorEvents(io) {
  62. logger.log(
  63. { rclients: this.rclientPubList.length },
  64. 'publishing editor events'
  65. )
  66. logger.log(
  67. { rclients: this.rclientSubList.length },
  68. 'listening for editor events'
  69. )
  70. for (const rclientSub of Array.from(this.rclientSubList)) {
  71. rclientSub.subscribe('editor-events')
  72. rclientSub.on('message', function (channel, message) {
  73. if (Settings.debugEvents > 0) {
  74. EventLogger.debugEvent(channel, message)
  75. }
  76. return WebsocketLoadBalancer._processEditorEvent(io, channel, message)
  77. })
  78. }
  79. return this.handleRoomUpdates(this.rclientSubList)
  80. },
  81. handleRoomUpdates(rclientSubList) {
  82. const roomEvents = RoomManager.eventSource()
  83. roomEvents.on('project-active', function (project_id) {
  84. const subscribePromises = Array.from(rclientSubList).map((rclient) =>
  85. ChannelManager.subscribe(rclient, 'editor-events', project_id)
  86. )
  87. return RoomManager.emitOnCompletion(
  88. subscribePromises,
  89. `project-subscribed-${project_id}`
  90. )
  91. })
  92. return roomEvents.on('project-empty', (project_id) =>
  93. Array.from(rclientSubList).map((rclient) =>
  94. ChannelManager.unsubscribe(rclient, 'editor-events', project_id)
  95. )
  96. )
  97. },
  98. _processEditorEvent(io, channel, message) {
  99. return SafeJsonParse.parse(message, function (error, message) {
  100. let clientList
  101. let client
  102. if (error != null) {
  103. logger.error({ err: error, channel }, 'error parsing JSON')
  104. return
  105. }
  106. if (message.room_id === 'all') {
  107. return io.sockets.emit(message.message, ...Array.from(message.payload))
  108. } else if (
  109. message.message === 'clientTracking.refresh' &&
  110. message.room_id != null
  111. ) {
  112. clientList = io.sockets.clients(message.room_id)
  113. logger.log(
  114. {
  115. channel,
  116. message: message.message,
  117. room_id: message.room_id,
  118. message_id: message._id,
  119. socketIoClients: (() => {
  120. const result = []
  121. for (client of Array.from(clientList)) {
  122. result.push(client.id)
  123. }
  124. return result
  125. })()
  126. },
  127. 'refreshing client list'
  128. )
  129. return (() => {
  130. const result1 = []
  131. for (client of Array.from(clientList)) {
  132. result1.push(
  133. ConnectedUsersManager.refreshClient(
  134. message.room_id,
  135. client.publicId
  136. )
  137. )
  138. }
  139. return result1
  140. })()
  141. } else if (message.room_id != null) {
  142. if (message._id != null && Settings.checkEventOrder) {
  143. const status = EventLogger.checkEventOrder(
  144. 'editor-events',
  145. message._id,
  146. message
  147. )
  148. if (status === 'duplicate') {
  149. return // skip duplicate events
  150. }
  151. }
  152. const is_restricted_message = !Array.from(
  153. RESTRICTED_USER_MESSAGE_TYPE_PASS_LIST
  154. ).includes(message.message)
  155. // send messages only to unique clients (due to duplicate entries in io.sockets.clients)
  156. clientList = io.sockets
  157. .clients(message.room_id)
  158. .filter(
  159. (client) =>
  160. !(is_restricted_message && client.ol_context.is_restricted_user)
  161. )
  162. // avoid unnecessary work if no clients are connected
  163. if (clientList.length === 0) {
  164. return
  165. }
  166. logger.log(
  167. {
  168. channel,
  169. message: message.message,
  170. room_id: message.room_id,
  171. message_id: message._id,
  172. socketIoClients: (() => {
  173. const result2 = []
  174. for (client of Array.from(clientList)) {
  175. result2.push(client.id)
  176. }
  177. return result2
  178. })()
  179. },
  180. 'distributing event to clients'
  181. )
  182. const seen = {}
  183. return (() => {
  184. const result3 = []
  185. for (client of Array.from(clientList)) {
  186. if (!seen[client.id]) {
  187. seen[client.id] = true
  188. result3.push(
  189. client.emit(message.message, ...Array.from(message.payload))
  190. )
  191. } else {
  192. result3.push(undefined)
  193. }
  194. }
  195. return result3
  196. })()
  197. } else if (message.health_check != null) {
  198. logger.debug(
  199. { message },
  200. 'got health check message in editor events channel'
  201. )
  202. return HealthCheckManager.check(channel, message.key)
  203. }
  204. })
  205. }
  206. }