SnapshotManager.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149
  1. // @ts-check
  2. import { callbackify } from 'util'
  3. import Core from 'overleaf-editor-core'
  4. import { Readable as StringStream } from 'stream'
  5. import OError from '@overleaf/o-error'
  6. import * as HistoryStoreManager from './HistoryStoreManager.js'
  7. import * as WebApiManager from './WebApiManager.js'
  8. import * as Errors from './Errors.js'
  9. /**
  10. * @typedef {import('stream').Readable} ReadableStream
  11. * @typedef {import('overleaf-editor-core').Snapshot} Snapshot
  12. */
  13. StringStream.prototype._read = function () {}
  14. const MAX_REQUESTS = 4 // maximum number of parallel requests to v1 history service
  15. /**
  16. *
  17. * @param {string} projectId
  18. * @param {number} version
  19. * @param {string} pathname
  20. */
  21. async function getFileSnapshotStream(projectId, version, pathname) {
  22. const snapshot = await _getSnapshotAtVersion(projectId, version)
  23. const file = snapshot.getFile(pathname)
  24. if (file == null) {
  25. throw new Errors.NotFoundError(`${pathname} not found`, {
  26. projectId,
  27. version,
  28. pathname,
  29. })
  30. }
  31. const historyId = await WebApiManager.promises.getHistoryId(projectId)
  32. if (file.isEditable()) {
  33. await file.load('eager', HistoryStoreManager.getBlobStore(historyId))
  34. const stream = new StringStream()
  35. stream.push(file.getContent({ filterTrackedDeletes: true }))
  36. stream.push(null)
  37. return stream
  38. } else {
  39. return await HistoryStoreManager.promises.getProjectBlobStream(
  40. historyId,
  41. file.getHash()
  42. )
  43. }
  44. }
  45. // Returns project snapshot containing the document content for files with
  46. // text operations in the relevant chunk, and hashes for unmodified/binary
  47. // files. Used by git bridge to get the state of the project.
  48. async function getProjectSnapshot(projectId, version) {
  49. const snapshot = await _getSnapshotAtVersion(projectId, version)
  50. const historyId = await WebApiManager.promises.getHistoryId(projectId)
  51. await _loadFilesLimit(
  52. snapshot,
  53. 'eager',
  54. HistoryStoreManager.getBlobStore(historyId)
  55. )
  56. return {
  57. projectId,
  58. files: snapshot.getFileMap().map(file => {
  59. if (!file) {
  60. return null
  61. }
  62. const content = file.getContent({
  63. filterTrackedDeletes: true,
  64. })
  65. if (content === null) {
  66. return { data: { hash: file.getHash() } }
  67. }
  68. return { data: { content } }
  69. }),
  70. }
  71. }
  72. /**
  73. *
  74. * @param {string} projectId
  75. * @param {number} version
  76. */
  77. async function _getSnapshotAtVersion(projectId, version) {
  78. const historyId = await WebApiManager.promises.getHistoryId(projectId)
  79. const data = await HistoryStoreManager.promises.getChunkAtVersion(
  80. projectId,
  81. historyId,
  82. version
  83. )
  84. const chunk = Core.Chunk.fromRaw(data.chunk)
  85. const snapshot = chunk.getSnapshot()
  86. const changes = chunk.getChanges().slice(0, version - chunk.getStartVersion())
  87. snapshot.applyAll(changes)
  88. return snapshot
  89. }
  90. async function getLatestSnapshot(projectId, historyId) {
  91. const data = await HistoryStoreManager.promises.getMostRecentChunk(
  92. projectId,
  93. historyId
  94. )
  95. if (data == null || data.chunk == null) {
  96. throw new OError('undefined chunk')
  97. }
  98. // apply all the changes in the chunk to get the current snapshot
  99. const chunk = Core.Chunk.fromRaw(data.chunk)
  100. const snapshot = chunk.getSnapshot()
  101. const changes = chunk.getChanges()
  102. snapshot.applyAll(changes)
  103. const snapshotFiles = await snapshot.loadFiles(
  104. 'lazy',
  105. HistoryStoreManager.getBlobStore(historyId)
  106. )
  107. return snapshotFiles
  108. }
  109. async function _loadFilesLimit(snapshot, kind, blobStore) {
  110. await snapshot.fileMap.mapAsync(async file => {
  111. // only load changed files or files with tracked changes, others can be
  112. // dereferenced from their blobs (this method is only used by the git
  113. // bridge which understands how to load blobs).
  114. if (!file.isEditable() || (file.getHash() && !file.getRangesHash())) {
  115. return
  116. }
  117. await file.load(kind, blobStore)
  118. }, MAX_REQUESTS)
  119. }
  120. // EXPORTS
  121. const getFileSnapshotStreamCb = callbackify(getFileSnapshotStream)
  122. const getProjectSnapshotCb = callbackify(getProjectSnapshot)
  123. const getLatestSnapshotCb = callbackify(getLatestSnapshot)
  124. export {
  125. getFileSnapshotStreamCb as getFileSnapshotStream,
  126. getProjectSnapshotCb as getProjectSnapshot,
  127. getLatestSnapshotCb as getLatestSnapshot,
  128. }
  129. export const promises = {
  130. getFileSnapshotStream,
  131. getProjectSnapshot,
  132. getLatestSnapshot,
  133. }