DiffManager.js 6.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240
  1. import logger from '@overleaf/logger'
  2. import OError from '@overleaf/o-error'
  3. import async from 'async'
  4. import * as DiffGenerator from './DiffGenerator.js'
  5. import * as FileTreeDiffGenerator from './FileTreeDiffGenerator.js'
  6. import * as UpdatesProcessor from './UpdatesProcessor.js'
  7. import * as HistoryStoreManager from './HistoryStoreManager.js'
  8. import * as WebApiManager from './WebApiManager.js'
  9. import * as ChunkTranslator from './ChunkTranslator.js'
  10. import * as Errors from './Errors.js'
  11. let MAX_CHUNK_REQUESTS = 10
  12. /**
  13. * Container for functions that need to be mocked in tests
  14. *
  15. * TODO: Rewrite tests in terms of exported functions only
  16. */
  17. export const _mocks = {}
  18. export function getDiff(projectId, pathname, fromVersion, toVersion, callback) {
  19. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  20. if (error) {
  21. return callback(OError.tag(error))
  22. }
  23. _getProjectUpdatesBetweenVersions(
  24. projectId,
  25. pathname,
  26. fromVersion,
  27. toVersion,
  28. (error, result) => {
  29. if (error) {
  30. return callback(OError.tag(error))
  31. }
  32. const { binary, initialContent, updates } = result
  33. let diff
  34. if (binary) {
  35. diff = { binary: true }
  36. } else {
  37. try {
  38. diff = DiffGenerator.buildDiff(initialContent, updates)
  39. } catch (err) {
  40. return callback(
  41. OError.tag(err, 'failed to build diff', {
  42. projectId,
  43. pathname,
  44. fromVersion,
  45. toVersion,
  46. })
  47. )
  48. }
  49. }
  50. callback(null, diff)
  51. }
  52. )
  53. })
  54. }
  55. export function getFileTreeDiff(projectId, fromVersion, toVersion, callback) {
  56. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  57. if (error) {
  58. return callback(OError.tag(error))
  59. }
  60. _getChunksAsSingleChunk(
  61. projectId,
  62. fromVersion,
  63. toVersion,
  64. (error, chunk) => {
  65. let diff
  66. if (error) {
  67. return callback(OError.tag(error))
  68. }
  69. try {
  70. diff = FileTreeDiffGenerator.buildDiff(chunk, fromVersion, toVersion)
  71. } catch (error1) {
  72. error = error1
  73. if (error instanceof Errors.InconsistentChunkError) {
  74. return callback(error)
  75. } else {
  76. return callback(OError.tag(error))
  77. }
  78. }
  79. callback(null, diff)
  80. }
  81. )
  82. })
  83. }
  84. export function _getChunksAsSingleChunk(
  85. projectId,
  86. fromVersion,
  87. toVersion,
  88. callback
  89. ) {
  90. logger.debug(
  91. { projectId, fromVersion, toVersion },
  92. '[_getChunksAsSingleChunk] getting chunks'
  93. )
  94. _getChunks(projectId, fromVersion, toVersion, (error, chunks) => {
  95. if (error) {
  96. return callback(OError.tag(error))
  97. }
  98. logger.debug(
  99. { projectId, fromVersion, toVersion, chunks },
  100. '[_getChunksAsSingleChunk] got chunks'
  101. )
  102. const chunk = _concatChunks(chunks)
  103. callback(null, chunk)
  104. })
  105. }
  106. _mocks._getProjectUpdatesBetweenVersions = (
  107. projectId,
  108. pathname,
  109. fromVersion,
  110. toVersion,
  111. callback
  112. ) => {
  113. _getChunksAsSingleChunk(projectId, fromVersion, toVersion, (error, chunk) => {
  114. if (error) {
  115. return callback(OError.tag(error))
  116. }
  117. logger.debug(
  118. { projectId, pathname, fromVersion, toVersion, chunk },
  119. '[_getProjectUpdatesBetweenVersions] concatted chunk'
  120. )
  121. ChunkTranslator.convertToDiffUpdates(
  122. projectId,
  123. chunk,
  124. pathname,
  125. fromVersion,
  126. toVersion,
  127. callback
  128. )
  129. })
  130. }
  131. export function _getProjectUpdatesBetweenVersions(...args) {
  132. _mocks._getProjectUpdatesBetweenVersions(...args)
  133. }
  134. _mocks._getChunks = (projectId, fromVersion, toVersion, callback) => {
  135. let chunksRequested = 0
  136. let lastChunkStartVersion = toVersion
  137. const chunks = []
  138. function shouldRequestAnotherChunk(cb) {
  139. const stillUnderChunkLimit = chunksRequested < MAX_CHUNK_REQUESTS
  140. const stillNeedVersions = fromVersion < lastChunkStartVersion
  141. const stillSaneStartVersion = lastChunkStartVersion > 0
  142. logger.debug(
  143. {
  144. projectId,
  145. stillUnderChunkLimit,
  146. stillNeedVersions,
  147. stillSaneStartVersion,
  148. fromVersion,
  149. lastChunkStartVersion,
  150. chunksRequested,
  151. },
  152. '[_getChunks.shouldRequestAnotherChunk]'
  153. )
  154. return cb(
  155. null,
  156. stillUnderChunkLimit && stillNeedVersions && stillSaneStartVersion
  157. )
  158. }
  159. function getNextChunk(cb) {
  160. logger.debug(
  161. {
  162. projectId,
  163. lastChunkStartVersion,
  164. },
  165. '[_getChunks.getNextChunk]'
  166. )
  167. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  168. if (error) {
  169. return cb(OError.tag(error))
  170. }
  171. HistoryStoreManager.getChunkAtVersion(
  172. projectId,
  173. historyId,
  174. lastChunkStartVersion,
  175. (error, chunk) => {
  176. if (error) {
  177. return cb(OError.tag(error))
  178. }
  179. lastChunkStartVersion = chunk.chunk.startVersion
  180. chunksRequested += 1
  181. chunks.push(chunk)
  182. cb()
  183. }
  184. )
  185. })
  186. }
  187. getNextChunk(error => {
  188. if (error) {
  189. return callback(OError.tag(error))
  190. }
  191. async.whilst(shouldRequestAnotherChunk, getNextChunk, error => {
  192. if (error) {
  193. return callback(error)
  194. }
  195. if (chunksRequested >= MAX_CHUNK_REQUESTS) {
  196. error = new Errors.BadRequestError('Diff spans too many chunks')
  197. callback(error)
  198. } else {
  199. callback(null, chunks)
  200. }
  201. })
  202. })
  203. }
  204. export function _getChunks(...args) {
  205. _mocks._getChunks(...args)
  206. }
  207. _mocks._concatChunks = chunks => {
  208. chunks.reverse()
  209. const chunk = chunks[0]
  210. // We will append all of the changes from the later
  211. // chunks onto the first one, to form one 'big' chunk.
  212. for (const nextChunk of chunks.slice(1)) {
  213. chunk.chunk.history.changes = chunk.chunk.history.changes.concat(
  214. nextChunk.chunk.history.changes
  215. )
  216. }
  217. return chunk
  218. }
  219. function _concatChunks(...args) {
  220. return _mocks._concatChunks(...args)
  221. }
  222. // for tests
  223. export function setMaxChunkRequests(value) {
  224. MAX_CHUNK_REQUESTS = value
  225. }