DiffManager.js 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229
  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. diff = DiffGenerator.buildDiff(initialContent, updates)
  38. }
  39. callback(null, diff)
  40. }
  41. )
  42. })
  43. }
  44. export function getFileTreeDiff(projectId, fromVersion, toVersion, callback) {
  45. UpdatesProcessor.processUpdatesForProject(projectId, error => {
  46. if (error) {
  47. return callback(OError.tag(error))
  48. }
  49. _getChunksAsSingleChunk(
  50. projectId,
  51. fromVersion,
  52. toVersion,
  53. (error, chunk) => {
  54. let diff
  55. if (error) {
  56. return callback(OError.tag(error))
  57. }
  58. try {
  59. diff = FileTreeDiffGenerator.buildDiff(chunk, fromVersion, toVersion)
  60. } catch (error1) {
  61. error = error1
  62. if (error instanceof Errors.InconsistentChunkError) {
  63. return callback(error)
  64. } else {
  65. throw OError.tag(error)
  66. }
  67. }
  68. callback(null, diff)
  69. }
  70. )
  71. })
  72. }
  73. export function _getChunksAsSingleChunk(
  74. projectId,
  75. fromVersion,
  76. toVersion,
  77. callback
  78. ) {
  79. logger.debug(
  80. { projectId, fromVersion, toVersion },
  81. '[_getChunksAsSingleChunk] getting chunks'
  82. )
  83. _getChunks(projectId, fromVersion, toVersion, (error, chunks) => {
  84. if (error) {
  85. return callback(OError.tag(error))
  86. }
  87. logger.debug(
  88. { projectId, fromVersion, toVersion, chunks },
  89. '[_getChunksAsSingleChunk] got chunks'
  90. )
  91. const chunk = _concatChunks(chunks)
  92. callback(null, chunk)
  93. })
  94. }
  95. _mocks._getProjectUpdatesBetweenVersions = (
  96. projectId,
  97. pathname,
  98. fromVersion,
  99. toVersion,
  100. callback
  101. ) => {
  102. _getChunksAsSingleChunk(projectId, fromVersion, toVersion, (error, chunk) => {
  103. if (error) {
  104. return callback(OError.tag(error))
  105. }
  106. logger.debug(
  107. { projectId, pathname, fromVersion, toVersion, chunk },
  108. '[_getProjectUpdatesBetweenVersions] concatted chunk'
  109. )
  110. ChunkTranslator.convertToDiffUpdates(
  111. projectId,
  112. chunk,
  113. pathname,
  114. fromVersion,
  115. toVersion,
  116. callback
  117. )
  118. })
  119. }
  120. export function _getProjectUpdatesBetweenVersions(...args) {
  121. _mocks._getProjectUpdatesBetweenVersions(...args)
  122. }
  123. _mocks._getChunks = (projectId, fromVersion, toVersion, callback) => {
  124. let chunksRequested = 0
  125. let lastChunkStartVersion = toVersion
  126. const chunks = []
  127. function shouldRequestAnotherChunk(cb) {
  128. const stillUnderChunkLimit = chunksRequested < MAX_CHUNK_REQUESTS
  129. const stillNeedVersions = fromVersion < lastChunkStartVersion
  130. const stillSaneStartVersion = lastChunkStartVersion > 0
  131. logger.debug(
  132. {
  133. projectId,
  134. stillUnderChunkLimit,
  135. stillNeedVersions,
  136. stillSaneStartVersion,
  137. fromVersion,
  138. lastChunkStartVersion,
  139. chunksRequested,
  140. },
  141. '[_getChunks.shouldRequestAnotherChunk]'
  142. )
  143. return cb(
  144. null,
  145. stillUnderChunkLimit && stillNeedVersions && stillSaneStartVersion
  146. )
  147. }
  148. function getNextChunk(cb) {
  149. logger.debug(
  150. {
  151. projectId,
  152. lastChunkStartVersion,
  153. },
  154. '[_getChunks.getNextChunk]'
  155. )
  156. WebApiManager.getHistoryId(projectId, (error, historyId) => {
  157. if (error) {
  158. return cb(OError.tag(error))
  159. }
  160. HistoryStoreManager.getChunkAtVersion(
  161. projectId,
  162. historyId,
  163. lastChunkStartVersion,
  164. (error, chunk) => {
  165. if (error) {
  166. return cb(OError.tag(error))
  167. }
  168. lastChunkStartVersion = chunk.chunk.startVersion
  169. chunksRequested += 1
  170. chunks.push(chunk)
  171. cb()
  172. }
  173. )
  174. })
  175. }
  176. getNextChunk(error => {
  177. if (error) {
  178. return callback(OError.tag(error))
  179. }
  180. async.whilst(shouldRequestAnotherChunk, getNextChunk, error => {
  181. if (error) {
  182. return callback(error)
  183. }
  184. if (chunksRequested >= MAX_CHUNK_REQUESTS) {
  185. error = new Errors.BadRequestError('Diff spans too many chunks')
  186. callback(error)
  187. } else {
  188. callback(null, chunks)
  189. }
  190. })
  191. })
  192. }
  193. export function _getChunks(...args) {
  194. _mocks._getChunks(...args)
  195. }
  196. _mocks._concatChunks = chunks => {
  197. chunks.reverse()
  198. const chunk = chunks[0]
  199. // We will append all of the changes from the later
  200. // chunks onto the first one, to form one 'big' chunk.
  201. for (const nextChunk of chunks.slice(1)) {
  202. chunk.chunk.history.changes = chunk.chunk.history.changes.concat(
  203. nextChunk.chunk.history.changes
  204. )
  205. }
  206. return chunk
  207. }
  208. function _concatChunks(...args) {
  209. return _mocks._concatChunks(...args)
  210. }
  211. // for tests
  212. export function setMaxChunkRequests(value) {
  213. MAX_CHUNK_REQUESTS = value
  214. }