DocUpdaterClient.js 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224
  1. let DocUpdaterClient
  2. const Settings = require('@overleaf/settings')
  3. const _ = require('lodash')
  4. const rclient = require('@overleaf/redis-wrapper').createClient(
  5. Settings.redis.documentupdater
  6. )
  7. const keys = Settings.redis.documentupdater.key_schema
  8. const request = require('request').defaults({ jar: false })
  9. const async = require('async')
  10. const rclientSub = require('@overleaf/redis-wrapper').createClient(
  11. Settings.redis.pubsub
  12. )
  13. rclientSub.subscribe('applied-ops')
  14. rclientSub.setMaxListeners(0)
  15. module.exports = DocUpdaterClient = {
  16. randomId() {
  17. let str = ''
  18. for (let i = 0; i < 24; i++) {
  19. str += Math.floor(Math.random() * 16).toString(16)
  20. }
  21. return str
  22. },
  23. subscribeToAppliedOps(callback) {
  24. rclientSub.on('message', callback)
  25. },
  26. _getPendingUpdateListKey() {
  27. const shard = _.random(0, Settings.dispatcherCount - 1)
  28. if (shard === 0) {
  29. return 'pending-updates-list'
  30. } else {
  31. return `pending-updates-list-${shard}`
  32. }
  33. },
  34. sendUpdate(projectId, docId, update, callback) {
  35. rclient.rpush(
  36. keys.pendingUpdates({ doc_id: docId }),
  37. JSON.stringify(update),
  38. error => {
  39. if (error) {
  40. return callback(error)
  41. }
  42. const docKey = `${projectId}:${docId}`
  43. rclient.sadd('DocsWithPendingUpdates', docKey, error => {
  44. if (error) {
  45. return callback(error)
  46. }
  47. rclient.rpush(
  48. DocUpdaterClient._getPendingUpdateListKey(),
  49. docKey,
  50. callback
  51. )
  52. })
  53. }
  54. )
  55. },
  56. sendUpdates(projectId, docId, updates, callback) {
  57. DocUpdaterClient.preloadDoc(projectId, docId, error => {
  58. if (error) {
  59. return callback(error)
  60. }
  61. const jobs = updates.map(update => callback => {
  62. DocUpdaterClient.sendUpdate(projectId, docId, update, callback)
  63. })
  64. async.series(jobs, err => {
  65. if (err) {
  66. return callback(err)
  67. }
  68. DocUpdaterClient.waitForPendingUpdates(projectId, docId, callback)
  69. })
  70. })
  71. },
  72. waitForPendingUpdates(projectId, docId, callback) {
  73. async.retry(
  74. { times: 30, interval: 100 },
  75. cb =>
  76. rclient.llen(keys.pendingUpdates({ doc_id: docId }), (err, length) => {
  77. if (err) {
  78. return cb(err)
  79. }
  80. if (length > 0) {
  81. cb(new Error('updates still pending'))
  82. } else {
  83. cb()
  84. }
  85. }),
  86. callback
  87. )
  88. },
  89. getDoc(projectId, docId, callback) {
  90. request.get(
  91. `http://localhost:3003/project/${projectId}/doc/${docId}`,
  92. (error, res, body) => {
  93. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  94. body = JSON.parse(body)
  95. }
  96. callback(error, res, body)
  97. }
  98. )
  99. },
  100. getDocAndRecentOps(projectId, docId, fromVersion, callback) {
  101. request.get(
  102. `http://localhost:3003/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`,
  103. (error, res, body) => {
  104. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  105. body = JSON.parse(body)
  106. }
  107. callback(error, res, body)
  108. }
  109. )
  110. },
  111. preloadDoc(projectId, docId, callback) {
  112. DocUpdaterClient.getDoc(projectId, docId, callback)
  113. },
  114. peekDoc(projectId, docId, callback) {
  115. request.get(
  116. `http://localhost:3003/project/${projectId}/doc/${docId}/peek`,
  117. (error, res, body) => {
  118. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  119. body = JSON.parse(body)
  120. }
  121. callback(error, res, body)
  122. }
  123. )
  124. },
  125. flushDoc(projectId, docId, callback) {
  126. request.post(
  127. `http://localhost:3003/project/${projectId}/doc/${docId}/flush`,
  128. (error, res, body) => callback(error, res, body)
  129. )
  130. },
  131. setDocLines(projectId, docId, lines, source, userId, undoing, callback) {
  132. request.post(
  133. {
  134. url: `http://localhost:3003/project/${projectId}/doc/${docId}`,
  135. json: {
  136. lines,
  137. source,
  138. user_id: userId,
  139. undoing,
  140. },
  141. },
  142. (error, res, body) => callback(error, res, body)
  143. )
  144. },
  145. deleteDoc(projectId, docId, callback) {
  146. request.del(
  147. `http://localhost:3003/project/${projectId}/doc/${docId}`,
  148. (error, res, body) => callback(error, res, body)
  149. )
  150. },
  151. flushProject(projectId, callback) {
  152. request.post(`http://localhost:3003/project/${projectId}/flush`, callback)
  153. },
  154. deleteProject(projectId, callback) {
  155. request.del(`http://localhost:3003/project/${projectId}`, callback)
  156. },
  157. deleteProjectOnShutdown(projectId, callback) {
  158. request.del(
  159. `http://localhost:3003/project/${projectId}?background=true&shutdown=true`,
  160. callback
  161. )
  162. },
  163. flushOldProjects(callback) {
  164. request.get(
  165. 'http://localhost:3003/flush_queued_projects?min_delete_age=1',
  166. callback
  167. )
  168. },
  169. acceptChange(projectId, docId, changeId, callback) {
  170. request.post(
  171. `http://localhost:3003/project/${projectId}/doc/${docId}/change/${changeId}/accept`,
  172. callback
  173. )
  174. },
  175. removeComment(projectId, docId, comment, callback) {
  176. request.del(
  177. `http://localhost:3003/project/${projectId}/doc/${docId}/comment/${comment}`,
  178. callback
  179. )
  180. },
  181. getProjectDocs(projectId, projectStateHash, callback) {
  182. request.get(
  183. `http://localhost:3003/project/${projectId}/doc?state=${projectStateHash}`,
  184. (error, res, body) => {
  185. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  186. body = JSON.parse(body)
  187. }
  188. callback(error, res, body)
  189. }
  190. )
  191. },
  192. sendProjectUpdate(projectId, userId, updates, version, callback) {
  193. request.post(
  194. {
  195. url: `http://localhost:3003/project/${projectId}`,
  196. json: { userId, updates, version },
  197. },
  198. (error, res, body) => callback(error, res, body)
  199. )
  200. },
  201. }