DocUpdaterClient.js 6.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238
  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://127.0.0.1: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://127.0.0.1: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. enableHistoryRangesSupport(docId, cb) {
  112. rclient.sadd(keys.historyRangesSupport(), docId, cb)
  113. },
  114. preloadDoc(projectId, docId, callback) {
  115. DocUpdaterClient.getDoc(projectId, docId, callback)
  116. },
  117. peekDoc(projectId, docId, callback) {
  118. request.get(
  119. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/peek`,
  120. (error, res, body) => {
  121. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  122. body = JSON.parse(body)
  123. }
  124. callback(error, res, body)
  125. }
  126. )
  127. },
  128. flushDoc(projectId, docId, callback) {
  129. request.post(
  130. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/flush`,
  131. (error, res, body) => callback(error, res, body)
  132. )
  133. },
  134. setDocLines(projectId, docId, lines, source, userId, undoing, callback) {
  135. request.post(
  136. {
  137. url: `http://127.0.0.1:3003/project/${projectId}/doc/${docId}`,
  138. json: {
  139. lines,
  140. source,
  141. user_id: userId,
  142. undoing,
  143. },
  144. },
  145. (error, res, body) => callback(error, res, body)
  146. )
  147. },
  148. deleteDoc(projectId, docId, callback) {
  149. request.del(
  150. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}`,
  151. (error, res, body) => callback(error, res, body)
  152. )
  153. },
  154. flushProject(projectId, callback) {
  155. request.post(`http://127.0.0.1:3003/project/${projectId}/flush`, callback)
  156. },
  157. deleteProject(projectId, callback) {
  158. request.del(`http://127.0.0.1:3003/project/${projectId}`, callback)
  159. },
  160. deleteProjectOnShutdown(projectId, callback) {
  161. request.del(
  162. `http://127.0.0.1:3003/project/${projectId}?background=true&shutdown=true`,
  163. callback
  164. )
  165. },
  166. flushOldProjects(callback) {
  167. request.get(
  168. 'http://127.0.0.1:3003/flush_queued_projects?min_delete_age=1',
  169. callback
  170. )
  171. },
  172. acceptChange(projectId, docId, changeId, callback) {
  173. request.post(
  174. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/change/${changeId}/accept`,
  175. callback
  176. )
  177. },
  178. acceptChanges(projectId, docId, changeIds, callback) {
  179. request.post(
  180. {
  181. url: `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/change/accept`,
  182. json: { change_ids: changeIds },
  183. },
  184. callback
  185. )
  186. },
  187. removeComment(projectId, docId, comment, callback) {
  188. request.del(
  189. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/comment/${comment}`,
  190. callback
  191. )
  192. },
  193. getProjectDocs(projectId, projectStateHash, callback) {
  194. request.get(
  195. `http://127.0.0.1:3003/project/${projectId}/doc?state=${projectStateHash}`,
  196. (error, res, body) => {
  197. if (body != null && res.statusCode >= 200 && res.statusCode < 300) {
  198. body = JSON.parse(body)
  199. }
  200. callback(error, res, body)
  201. }
  202. )
  203. },
  204. sendProjectUpdate(projectId, userId, updates, version, callback) {
  205. request.post(
  206. {
  207. url: `http://127.0.0.1:3003/project/${projectId}`,
  208. json: { userId, updates, version },
  209. },
  210. (error, res, body) => callback(error, res, body)
  211. )
  212. },
  213. }