HistoryOTUpdateManager.js 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163
  1. // @ts-check
  2. const Profiler = require('./Profiler')
  3. const DocumentManager = require('./DocumentManager')
  4. const Errors = require('./Errors')
  5. const RedisManager = require('./RedisManager')
  6. const {
  7. EditOperationBuilder,
  8. StringFileData,
  9. EditOperationTransformer,
  10. } = require('overleaf-editor-core')
  11. const Metrics = require('./Metrics')
  12. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  13. const HistoryManager = require('./HistoryManager')
  14. const RealTimeRedisManager = require('./RealTimeRedisManager')
  15. /**
  16. * @typedef {import("./types").Update} Update
  17. * @typedef {import("./types").HistoryOTEditOperationUpdate} HistoryOTEditOperationUpdate
  18. */
  19. /**
  20. * @param {Update} update
  21. * @return {update is HistoryOTEditOperationUpdate}
  22. */
  23. function isHistoryOTEditOperationUpdate(update) {
  24. return (
  25. update &&
  26. 'doc' in update &&
  27. 'op' in update &&
  28. 'v' in update &&
  29. Array.isArray(update.op) &&
  30. EditOperationBuilder.isValid(update.op[0])
  31. )
  32. }
  33. /**
  34. * Try to apply an update to the given document
  35. *
  36. * @param {string} projectId
  37. * @param {string} docId
  38. * @param {HistoryOTEditOperationUpdate} update
  39. * @param {Profiler} profiler
  40. */
  41. async function tryApplyUpdate(projectId, docId, update, profiler) {
  42. let { lines, version, pathname, type } =
  43. await DocumentManager.promises.getDoc(projectId, docId)
  44. profiler.log('getDoc')
  45. if (lines == null || version == null) {
  46. throw new Errors.NotFoundError(`document not found: ${docId}`)
  47. }
  48. if (type !== 'history-ot') {
  49. throw new Errors.OTTypeMismatchError(type, 'history-ot')
  50. }
  51. let op = EditOperationBuilder.fromJSON(update.op[0])
  52. if (version !== update.v) {
  53. const transformUpdates = await RedisManager.promises.getPreviousDocOps(
  54. docId,
  55. update.v,
  56. version
  57. )
  58. for (const transformUpdate of transformUpdates) {
  59. if (!isHistoryOTEditOperationUpdate(transformUpdate)) {
  60. throw new Errors.OTTypeMismatchError('sharejs-text-ot', 'history-ot')
  61. }
  62. if (
  63. transformUpdate.meta.source &&
  64. update.dupIfSource?.includes(transformUpdate.meta.source)
  65. ) {
  66. update.dup = true
  67. break
  68. }
  69. const other = EditOperationBuilder.fromJSON(transformUpdate.op[0])
  70. op = EditOperationTransformer.transform(op, other)[0]
  71. }
  72. update.op = [op.toJSON()]
  73. }
  74. if (!update.dup) {
  75. const file = StringFileData.fromRaw(lines)
  76. file.edit(op)
  77. version += 1
  78. update.meta.ts = Date.now()
  79. await RedisManager.promises.updateDocument(
  80. projectId,
  81. docId,
  82. file.toRaw(),
  83. version,
  84. [update],
  85. {},
  86. update.meta
  87. )
  88. Metrics.inc('history-queue', 1, { status: 'project-history' })
  89. try {
  90. const projectOpsLength =
  91. await ProjectHistoryRedisManager.promises.queueOps(projectId, [
  92. JSON.stringify({
  93. ...update,
  94. meta: {
  95. ...update.meta,
  96. pathname,
  97. },
  98. }),
  99. ])
  100. HistoryManager.recordAndFlushHistoryOps(
  101. projectId,
  102. [update],
  103. projectOpsLength
  104. )
  105. profiler.log('recordAndFlushHistoryOps')
  106. } catch (err) {
  107. // The full project history can re-sync a project in case
  108. // updates went missing.
  109. // Just record the error here and acknowledge the write-op.
  110. Metrics.inc('history-queue-error')
  111. }
  112. await RedisManager.promises.recordProjectNotificationTimestamp(
  113. projectId,
  114. update.meta.ts
  115. )
  116. profiler.log('recordProjectNotificationTimestamp')
  117. }
  118. RealTimeRedisManager.sendData({
  119. project_id: projectId,
  120. doc_id: docId,
  121. op: update,
  122. })
  123. }
  124. /**
  125. * Apply an update to the given document
  126. *
  127. * @param {string} projectId
  128. * @param {string} docId
  129. * @param {HistoryOTEditOperationUpdate} update
  130. */
  131. async function applyUpdate(projectId, docId, update) {
  132. const profiler = new Profiler('applyUpdate', {
  133. project_id: projectId,
  134. doc_id: docId,
  135. type: 'history-ot',
  136. })
  137. try {
  138. await tryApplyUpdate(projectId, docId, update, profiler)
  139. } catch (error) {
  140. RealTimeRedisManager.sendData({
  141. project_id: projectId,
  142. doc_id: docId,
  143. error: error instanceof Error ? error.message : error,
  144. })
  145. profiler.log('sendData')
  146. throw error
  147. } finally {
  148. profiler.end()
  149. }
  150. }
  151. module.exports = { isHistoryOTEditOperationUpdate, applyUpdate }