DocUpdaterClient.js 6.4 KB

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