DocUpdaterClient.js 5.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212
  1. let DocUpdaterClient
  2. const Settings = require('settings-sharelatex')
  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)
  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. flushDoc(projectId, docId, callback) {
  115. request.post(
  116. `http://localhost:3003/project/${projectId}/doc/${docId}/flush`,
  117. (error, res, body) => callback(error, res, body)
  118. )
  119. },
  120. setDocLines(projectId, docId, lines, source, userId, undoing, callback) {
  121. request.post(
  122. {
  123. url: `http://localhost:3003/project/${projectId}/doc/${docId}`,
  124. json: {
  125. lines,
  126. source,
  127. user_id: userId,
  128. undoing
  129. }
  130. },
  131. (error, res, body) => callback(error, res, body)
  132. )
  133. },
  134. deleteDoc(projectId, docId, callback) {
  135. request.del(
  136. `http://localhost:3003/project/${projectId}/doc/${docId}`,
  137. (error, res, body) => callback(error, res, body)
  138. )
  139. },
  140. flushProject(projectId, callback) {
  141. request.post(`http://localhost:3003/project/${projectId}/flush`, callback)
  142. },
  143. deleteProject(projectId, callback) {
  144. request.del(`http://localhost:3003/project/${projectId}`, callback)
  145. },
  146. deleteProjectOnShutdown(projectId, callback) {
  147. request.del(
  148. `http://localhost:3003/project/${projectId}?background=true&shutdown=true`,
  149. callback
  150. )
  151. },
  152. flushOldProjects(callback) {
  153. request.get(
  154. 'http://localhost:3003/flush_queued_projects?min_delete_age=1',
  155. callback
  156. )
  157. },
  158. acceptChange(projectId, docId, changeId, callback) {
  159. request.post(
  160. `http://localhost:3003/project/${projectId}/doc/${docId}/change/${changeId}/accept`,
  161. callback
  162. )
  163. },
  164. removeComment(projectId, docId, comment, callback) {
  165. request.del(
  166. `http://localhost:3003/project/${projectId}/doc/${docId}/comment/${comment}`,
  167. callback
  168. )
  169. },
  170. getProjectDocs(projectId, projectStateHash, callback) {
  171. request.get(
  172. `http://localhost:3003/project/${projectId}/doc?state=${projectStateHash}`,
  173. (error, res, body) => {
  174. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  175. body = JSON.parse(body)
  176. }
  177. callback(error, res, body)
  178. }
  179. )
  180. },
  181. sendProjectUpdate(projectId, userId, updates, version, callback) {
  182. request.post(
  183. {
  184. url: `http://localhost:3003/project/${projectId}`,
  185. json: { userId, updates, version }
  186. },
  187. (error, res, body) => callback(error, res, body)
  188. )
  189. }
  190. }