DocUpdaterClient.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
  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 { fetchJson, fetchNothing } = require('@overleaf/fetch-utils')
  9. const { setTimeout } = require('node:timers/promises')
  10. const rclientSub = require('@overleaf/redis-wrapper').createClient(
  11. Settings.redis.pubsub
  12. )
  13. rclientSub.subscribe('applied-ops')
  14. rclientSub.setMaxListeners(0)
  15. function getPendingUpdateListKey() {
  16. const shard = _.random(0, Settings.dispatcherCount - 1)
  17. if (shard === 0) {
  18. return 'pending-updates-list'
  19. } else {
  20. return `pending-updates-list-${shard}`
  21. }
  22. }
  23. module.exports = DocUpdaterClient = {
  24. randomId() {
  25. let str = ''
  26. for (let i = 0; i < 24; i++) {
  27. str += Math.floor(Math.random() * 16).toString(16)
  28. }
  29. return str
  30. },
  31. subscribeToAppliedOps(messageHandler) {
  32. rclientSub.on('message', messageHandler)
  33. },
  34. async sendUpdate(projectId, docId, update) {
  35. const docKey = `${projectId}:${docId}`
  36. await rclient.rpush(
  37. keys.pendingUpdates({ doc_id: docId }),
  38. JSON.stringify(update)
  39. )
  40. await rclient.sadd('DocsWithPendingUpdates', docKey)
  41. await rclient.rpush(getPendingUpdateListKey(), docKey)
  42. },
  43. async sendUpdates(projectId, docId, updates) {
  44. await DocUpdaterClient.preloadDoc(projectId, docId)
  45. for (const update of updates) {
  46. await DocUpdaterClient.sendUpdate(projectId, docId, update)
  47. }
  48. await DocUpdaterClient.waitForPendingUpdates(docId)
  49. },
  50. async waitForPendingUpdates(docId) {
  51. const maxRetries = 30
  52. const retryInterval = 100
  53. for (let attempt = 0; attempt < maxRetries; attempt++) {
  54. const length = await rclient.llen(keys.pendingUpdates({ doc_id: docId }))
  55. if (length === 0) {
  56. return // Success - no pending updates
  57. }
  58. if (attempt < maxRetries - 1) {
  59. await setTimeout(retryInterval)
  60. }
  61. }
  62. throw new Error('updates still pending after maximum retries')
  63. },
  64. async getDoc(projectId, docId) {
  65. return await fetchJson(
  66. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}`
  67. )
  68. },
  69. async getDocAndRecentOps(projectId, docId, fromVersion) {
  70. return await fetchJson(
  71. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}?fromVersion=${fromVersion}`
  72. )
  73. },
  74. async getProjectLastUpdatedAt(projectId) {
  75. return await fetchJson(
  76. `http://127.0.0.1:3003/project/${projectId}/last_updated_at`
  77. )
  78. },
  79. async preloadDoc(projectId, docId) {
  80. await DocUpdaterClient.getDoc(projectId, docId)
  81. },
  82. async peekDoc(projectId, docId) {
  83. return await fetchJson(
  84. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/peek`
  85. )
  86. },
  87. async flushDoc(projectId, docId) {
  88. return await fetchNothing(
  89. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/flush`,
  90. { method: 'POST' }
  91. )
  92. },
  93. async setDocLines(projectId, docId, lines, source, userId, undoing) {
  94. return await fetchJson(
  95. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}`,
  96. {
  97. method: 'POST',
  98. json: {
  99. lines,
  100. source,
  101. user_id: userId,
  102. undoing,
  103. },
  104. }
  105. )
  106. },
  107. async deleteDoc(projectId, docId) {
  108. return await fetchNothing(
  109. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}`,
  110. { method: 'DELETE' }
  111. )
  112. },
  113. async flushProject(projectId) {
  114. return await fetchNothing(
  115. `http://127.0.0.1:3003/project/${projectId}/flush`,
  116. {
  117. method: 'POST',
  118. }
  119. )
  120. },
  121. async deleteProject(projectId) {
  122. return await fetchNothing(`http://127.0.0.1:3003/project/${projectId}`, {
  123. method: 'DELETE',
  124. })
  125. },
  126. async deleteProjectOnShutdown(projectId) {
  127. return await fetchNothing(
  128. `http://127.0.0.1:3003/project/${projectId}?background=true&shutdown=true`,
  129. {
  130. method: 'DELETE',
  131. }
  132. )
  133. },
  134. async flushOldProjects() {
  135. await fetchNothing(
  136. 'http://127.0.0.1:3003/flush_queued_projects?min_delete_age=1'
  137. )
  138. },
  139. async acceptChange(projectId, docId, changeId) {
  140. await fetchNothing(
  141. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/change/${changeId}/accept`,
  142. { method: 'POST' }
  143. )
  144. },
  145. async acceptChanges(projectId, docId, changeIds) {
  146. await fetchNothing(
  147. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/change/accept`,
  148. {
  149. method: 'POST',
  150. json: { change_ids: changeIds },
  151. }
  152. )
  153. },
  154. async rejectChanges(projectId, docId, changeIds, userId) {
  155. return await fetchJson(
  156. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/change/reject`,
  157. {
  158. method: 'POST',
  159. json: { change_ids: changeIds, user_id: userId },
  160. }
  161. )
  162. },
  163. async removeComment(projectId, docId, comment) {
  164. await fetchNothing(
  165. `http://127.0.0.1:3003/project/${projectId}/doc/${docId}/comment/${comment}`,
  166. { method: 'DELETE' }
  167. )
  168. },
  169. async getProjectDocs(projectId, projectStateHash) {
  170. return await fetchJson(
  171. `http://127.0.0.1:3003/project/${projectId}/doc?state=${projectStateHash}`
  172. )
  173. },
  174. async sendProjectUpdate(projectId, userId, updates, version) {
  175. await fetchNothing(`http://127.0.0.1:3003/project/${projectId}`, {
  176. method: 'POST',
  177. json: { userId, updates, version },
  178. })
  179. },
  180. }