queue_changes.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. // @ts-check
  2. 'use strict'
  3. const redisBackend = require('./chunk_store/redis')
  4. const { BlobStore } = require('./blob_store')
  5. const chunkStore = require('./chunk_store')
  6. const core = require('overleaf-editor-core')
  7. const Chunk = core.Chunk
  8. /**
  9. * Queues an incoming set of changes after validating them against the current snapshot.
  10. *
  11. * @async
  12. * @function queueChanges
  13. * @param {string} projectId - The project to queue changes for.
  14. * @param {Array<Object>} changesToQueue - An array of change objects to be applied and queued.
  15. * @param {number} endVersion - The expected version of the project before these changes are applied.
  16. * This is used for optimistic concurrency control.
  17. * @param {Object} [opts] - Additional options for queuing changes.
  18. * @throws {Chunk.ConflictingEndVersion} If the provided `endVersion` does not match the
  19. * current version of the project.
  20. * @returns {Promise<any>} A promise that resolves with the status returned by the
  21. * `redisBackend.queueChanges` operation.
  22. */
  23. async function queueChanges(projectId, changesToQueue, endVersion, opts) {
  24. const result = await redisBackend.getHeadSnapshot(projectId)
  25. let currentSnapshot = null
  26. let currentVersion = null
  27. if (result) {
  28. // If we have a snapshot in redis, we can use it to check the current state
  29. // of the project and apply changes to it.
  30. currentSnapshot = result.snapshot
  31. currentVersion = result.version
  32. } else {
  33. // Otherwise, load the latest chunk from the chunk store.
  34. const latestChunk = await chunkStore.loadLatest(projectId, {
  35. persistedOnly: true,
  36. })
  37. // Throw an error if no latest chunk is found, indicating the project has not been initialised.
  38. if (!latestChunk) {
  39. throw new Chunk.NotFoundError(projectId)
  40. }
  41. currentSnapshot = latestChunk.getSnapshot()
  42. currentSnapshot.applyAll(latestChunk.getChanges())
  43. currentVersion = latestChunk.getEndVersion()
  44. }
  45. // Ensure the endVersion matches the current version of the project.
  46. if (endVersion !== currentVersion) {
  47. throw new Chunk.ConflictingEndVersion(endVersion, currentVersion)
  48. }
  49. // Compute the new hollow snapshot to be saved to redis.
  50. const hollowSnapshot = currentSnapshot
  51. const blobStore = new BlobStore(projectId)
  52. await hollowSnapshot.loadFiles('hollow', blobStore)
  53. // Clone the changes to avoid modifying the original ones when computing the hollow snapshot.
  54. const hollowChanges = changesToQueue.map(change => change.clone())
  55. for (const change of hollowChanges) {
  56. await change.loadFiles('hollow', blobStore)
  57. }
  58. hollowSnapshot.applyAll(hollowChanges, { strict: true })
  59. const baseVersion = currentVersion
  60. const status = await redisBackend.queueChanges(
  61. projectId,
  62. hollowSnapshot,
  63. baseVersion,
  64. changesToQueue,
  65. opts
  66. )
  67. return status
  68. }
  69. module.exports = queueChanges