| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158 |
- // @ts-check
- const Profiler = require('./Profiler')
- const DocumentManager = require('./DocumentManager')
- const Errors = require('./Errors')
- const RedisManager = require('./RedisManager')
- const {
- EditOperationBuilder,
- StringFileData,
- EditOperationTransformer,
- } = require('overleaf-editor-core')
- const Metrics = require('./Metrics')
- const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
- const HistoryManager = require('./HistoryManager')
- const RealTimeRedisManager = require('./RealTimeRedisManager')
- /**
- * @typedef {import("./types").Update} Update
- * @typedef {import("./types").HistoryOTEditOperationUpdate} HistoryOTEditOperationUpdate
- */
- /**
- * @param {Update} update
- * @return {update is HistoryOTEditOperationUpdate}
- */
- function isHistoryOTEditOperationUpdate(update) {
- return (
- update &&
- 'doc' in update &&
- 'op' in update &&
- 'v' in update &&
- Array.isArray(update.op) &&
- EditOperationBuilder.isValid(update.op[0])
- )
- }
- /**
- * Try to apply an update to the given document
- *
- * @param {string} projectId
- * @param {string} docId
- * @param {HistoryOTEditOperationUpdate} update
- * @param {Profiler} profiler
- */
- async function tryApplyUpdate(projectId, docId, update, profiler) {
- let { lines, version, pathname, type } =
- await DocumentManager.promises.getDoc(projectId, docId)
- profiler.log('getDoc')
- if (lines == null || version == null) {
- throw new Errors.NotFoundError(`document not found: ${docId}`)
- }
- if (type !== 'history-ot') {
- throw new Errors.OTTypeMismatchError(type, 'history-ot')
- }
- let op = EditOperationBuilder.fromJSON(update.op[0])
- if (version !== update.v) {
- const transformUpdates = await RedisManager.promises.getPreviousDocOps(
- docId,
- update.v,
- version
- )
- for (const transformUpdate of transformUpdates) {
- if (!isHistoryOTEditOperationUpdate(transformUpdate)) {
- throw new Errors.OTTypeMismatchError('sharejs-text-ot', 'history-ot')
- }
- if (
- transformUpdate.meta.source &&
- update.dupIfSource?.includes(transformUpdate.meta.source)
- ) {
- update.dup = true
- break
- }
- const other = EditOperationBuilder.fromJSON(transformUpdate.op[0])
- op = EditOperationTransformer.transform(op, other)[0]
- }
- update.op = [op.toJSON()]
- }
- if (!update.dup) {
- const file = StringFileData.fromRaw(lines)
- file.edit(op)
- version += 1
- update.meta.ts = Date.now()
- await RedisManager.promises.updateDocument(
- projectId,
- docId,
- file.toRaw(),
- version,
- [update],
- {},
- update.meta
- )
- Metrics.inc('history-queue', 1, { status: 'project-history' })
- try {
- const projectOpsLength =
- await ProjectHistoryRedisManager.promises.queueOps(projectId, [
- JSON.stringify({
- ...update,
- meta: {
- ...update.meta,
- pathname,
- },
- }),
- ])
- HistoryManager.recordAndFlushHistoryOps(
- projectId,
- [update],
- projectOpsLength
- )
- profiler.log('recordAndFlushHistoryOps')
- } catch (err) {
- // The full project history can re-sync a project in case
- // updates went missing.
- // Just record the error here and acknowledge the write-op.
- Metrics.inc('history-queue-error')
- }
- }
- RealTimeRedisManager.sendData({
- project_id: projectId,
- doc_id: docId,
- op: update,
- })
- }
- /**
- * Apply an update to the given document
- *
- * @param {string} projectId
- * @param {string} docId
- * @param {HistoryOTEditOperationUpdate} update
- */
- async function applyUpdate(projectId, docId, update) {
- const profiler = new Profiler('applyUpdate', {
- project_id: projectId,
- doc_id: docId,
- type: 'history-ot',
- })
- try {
- await tryApplyUpdate(projectId, docId, update, profiler)
- } catch (error) {
- RealTimeRedisManager.sendData({
- project_id: projectId,
- doc_id: docId,
- error: error instanceof Error ? error.message : error,
- })
- profiler.log('sendData')
- throw error
- } finally {
- profiler.end()
- }
- }
- module.exports = { isHistoryOTEditOperationUpdate, applyUpdate }
|