| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774 |
- const { promisifyAll } = require('@overleaf/promise-utils')
- const RedisManager = require('./RedisManager')
- const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
- const PersistenceManager = require('./PersistenceManager')
- const DiffCodec = require('./DiffCodec')
- const logger = require('@overleaf/logger')
- const Metrics = require('./Metrics')
- const HistoryManager = require('./HistoryManager')
- const Errors = require('./Errors')
- const RangesManager = require('./RangesManager')
- const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change
- const DocumentManager = {
- getDoc(projectId, docId, _callback) {
- const timer = new Metrics.Timer('docManager.getDoc')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- RedisManager.getDoc(
- projectId,
- docId,
- (
- error,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- lastUpdatedAt,
- lastUpdatedBy,
- historyRangesSupport
- ) => {
- if (error) {
- return callback(error)
- }
- if (lines == null || version == null) {
- logger.debug(
- { projectId, docId },
- 'doc not in redis so getting from persistence API'
- )
- PersistenceManager.getDoc(
- projectId,
- docId,
- (
- error,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- historyRangesSupport
- ) => {
- if (error) {
- return callback(error)
- }
- logger.debug(
- {
- projectId,
- docId,
- lines,
- version,
- pathname,
- projectHistoryId,
- historyRangesSupport,
- },
- 'got doc from persistence API'
- )
- RedisManager.putDocInMemory(
- projectId,
- docId,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- historyRangesSupport,
- error => {
- if (error) {
- return callback(error)
- }
- callback(
- null,
- lines,
- version,
- ranges || {},
- pathname,
- projectHistoryId,
- null,
- false,
- historyRangesSupport
- )
- }
- )
- }
- )
- } else {
- callback(
- null,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- true,
- historyRangesSupport
- )
- }
- }
- )
- },
- getDocAndRecentOps(projectId, docId, fromVersion, _callback) {
- const timer = new Metrics.Timer('docManager.getDocAndRecentOps')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- DocumentManager.getDoc(
- projectId,
- docId,
- (error, lines, version, ranges, pathname, projectHistoryId) => {
- if (error) {
- return callback(error)
- }
- if (fromVersion === -1) {
- callback(null, lines, version, [], ranges, pathname, projectHistoryId)
- } else {
- RedisManager.getPreviousDocOps(
- docId,
- fromVersion,
- version,
- (error, ops) => {
- if (error) {
- return callback(error)
- }
- callback(
- null,
- lines,
- version,
- ops,
- ranges,
- pathname,
- projectHistoryId
- )
- }
- )
- }
- }
- )
- },
- setDoc(projectId, docId, newLines, source, userId, undoing, _callback) {
- const timer = new Metrics.Timer('docManager.setDoc')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- if (newLines == null) {
- return callback(new Error('No lines were provided to setDoc'))
- }
- const UpdateManager = require('./UpdateManager')
- DocumentManager.getDoc(
- projectId,
- docId,
- (
- error,
- oldLines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- alreadyLoaded
- ) => {
- if (error) {
- return callback(error)
- }
- if (
- oldLines != null &&
- oldLines.length > 0 &&
- oldLines[0].text != null
- ) {
- logger.debug(
- { docId, projectId, oldLines, newLines },
- 'document is JSON so not updating'
- )
- return callback(null)
- }
- logger.debug(
- { docId, projectId, oldLines, newLines },
- 'setting a document via http'
- )
- const op = DiffCodec.diffAsShareJsOp(oldLines, newLines)
- if (undoing) {
- for (const o of op || []) {
- o.u = true
- } // Turn on undo flag for each op for track changes
- }
- const update = {
- doc: docId,
- op,
- v: version,
- meta: {
- type: 'external',
- source,
- user_id: userId,
- },
- }
- // Keep track of external updates, whether they are for live documents
- // (flush) or unloaded documents (evict), and whether the update is a no-op.
- Metrics.inc('external-update', 1, {
- status: op.length > 0 ? 'diff' : 'noop',
- method: alreadyLoaded ? 'flush' : 'evict',
- path: source,
- })
- const applyUpdateIfNeeded = cb => {
- if (op.length === 0) {
- // Do not notify the frontend about a noop update.
- // We still want to execute the callback code below
- // to evict the doc if we loaded it into redis for
- // this update, otherwise the doc would never be
- // removed from redis.
- return cb(null)
- }
- UpdateManager.applyUpdate(projectId, docId, update, cb)
- }
- applyUpdateIfNeeded(error => {
- if (error) {
- return callback(error)
- }
- // If the document was loaded already, then someone has it open
- // in a project, and the usual flushing mechanism will happen.
- // Otherwise we should remove it immediately since nothing else
- // is using it.
- if (alreadyLoaded) {
- DocumentManager.flushDocIfLoaded(
- projectId,
- docId,
- (error, result) => {
- if (error) {
- return callback(error)
- }
- callback(null, result)
- }
- )
- } else {
- DocumentManager.flushAndDeleteDoc(
- projectId,
- docId,
- {},
- (error, result) => {
- // There is no harm in flushing project history if the previous
- // call failed and sometimes it is required
- HistoryManager.flushProjectChangesAsync(projectId)
- if (error) {
- return callback(error)
- }
- callback(null, result)
- }
- )
- }
- })
- }
- )
- },
- flushDocIfLoaded(projectId, docId, _callback) {
- const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- RedisManager.getDoc(
- projectId,
- docId,
- (
- error,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- lastUpdatedAt,
- lastUpdatedBy
- ) => {
- if (error) {
- return callback(error)
- }
- if (lines == null || version == null) {
- Metrics.inc('flush-doc-if-loaded', 1, { status: 'not-loaded' })
- logger.debug(
- { projectId, docId },
- 'doc is not loaded so not flushing'
- )
- // TODO: return a flag to bail out, as we go on to remove doc from memory?
- callback(null)
- } else if (unflushedTime == null) {
- Metrics.inc('flush-doc-if-loaded', 1, { status: 'unmodified' })
- logger.debug(
- { projectId, docId },
- 'doc is not modified so not flushing'
- )
- callback(null)
- } else {
- logger.debug({ projectId, docId, version }, 'flushing doc')
- Metrics.inc('flush-doc-if-loaded', 1, { status: 'modified' })
- PersistenceManager.setDoc(
- projectId,
- docId,
- lines,
- version,
- ranges,
- lastUpdatedAt,
- lastUpdatedBy,
- (error, result) => {
- if (error) {
- return callback(error)
- }
- RedisManager.clearUnflushedTime(docId, err => {
- if (err) {
- return callback(err)
- }
- callback(null, result)
- })
- }
- )
- }
- }
- )
- },
- flushAndDeleteDoc(projectId, docId, options, _callback) {
- const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- DocumentManager.flushDocIfLoaded(projectId, docId, (error, result) => {
- if (error) {
- if (options.ignoreFlushErrors) {
- logger.warn(
- { projectId, docId, err: error },
- 'ignoring flush error while deleting document'
- )
- } else {
- return callback(error)
- }
- }
- RedisManager.removeDocFromMemory(projectId, docId, error => {
- if (error) {
- return callback(error)
- }
- callback(null, result)
- })
- })
- },
- acceptChanges(projectId, docId, changeIds, _callback) {
- if (changeIds == null) {
- changeIds = []
- }
- const timer = new Metrics.Timer('docManager.acceptChanges')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- DocumentManager.getDoc(
- projectId,
- docId,
- (error, lines, version, ranges) => {
- if (error) {
- return callback(error)
- }
- if (lines == null || version == null) {
- return callback(
- new Errors.NotFoundError(`document not found: ${docId}`)
- )
- }
- let newRanges
- try {
- newRanges = RangesManager.acceptChanges(changeIds, ranges)
- } catch (err) {
- return callback(err)
- }
- RedisManager.updateDocument(
- projectId,
- docId,
- lines,
- version,
- [],
- newRanges,
- {},
- error => {
- if (error) {
- return callback(error)
- }
- callback()
- }
- )
- }
- )
- },
- deleteComment(projectId, docId, commentId, userId, _callback) {
- const timer = new Metrics.Timer('docManager.deleteComment')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- DocumentManager.getDoc(
- projectId,
- docId,
- (
- error,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- alreadyLoaded,
- historyRangesSupport
- ) => {
- if (error) {
- return callback(error)
- }
- if (lines == null || version == null) {
- return callback(
- new Errors.NotFoundError(`document not found: ${docId}`)
- )
- }
- let newRanges
- try {
- newRanges = RangesManager.deleteComment(commentId, ranges)
- } catch (err) {
- return callback(err)
- }
- RedisManager.updateDocument(
- projectId,
- docId,
- lines,
- version,
- [],
- newRanges,
- {},
- error => {
- if (error) {
- return callback(error)
- }
- if (historyRangesSupport) {
- ProjectHistoryRedisManager.queueOps(
- projectId,
- JSON.stringify({
- pathname,
- deleteComment: commentId,
- meta: {
- ts: new Date(),
- user_id: userId,
- },
- }),
- error => {
- if (error) {
- return callback(error)
- }
- callback()
- }
- )
- } else {
- callback()
- }
- }
- )
- }
- )
- },
- renameDoc(projectId, docId, userId, update, projectHistoryId, _callback) {
- const timer = new Metrics.Timer('docManager.updateProject')
- const callback = (...args) => {
- timer.done()
- _callback(...args)
- }
- RedisManager.renameDoc(
- projectId,
- docId,
- userId,
- update,
- projectHistoryId,
- callback
- )
- },
- getDocAndFlushIfOld(projectId, docId, callback) {
- DocumentManager.getDoc(
- projectId,
- docId,
- (
- error,
- lines,
- version,
- ranges,
- pathname,
- projectHistoryId,
- unflushedTime,
- alreadyLoaded
- ) => {
- if (error) {
- return callback(error)
- }
- // if doc was already loaded see if it needs to be flushed
- if (
- alreadyLoaded &&
- unflushedTime != null &&
- Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
- ) {
- DocumentManager.flushDocIfLoaded(projectId, docId, error => {
- if (error) {
- return callback(error)
- }
- callback(null, lines, version)
- })
- } else {
- callback(null, lines, version)
- }
- }
- )
- },
- resyncDocContents(projectId, docId, path, callback) {
- logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
- RedisManager.getDoc(
- projectId,
- docId,
- (error, lines, version, ranges, pathname, projectHistoryId) => {
- if (error) {
- return callback(error)
- }
- // To avoid issues where the same docId appears with different paths,
- // we use the path from the resyncProjectStructure update. If we used
- // the path from the getDoc call to web then the two occurences of the
- // docId would map to the same path, and this would be rejected by
- // project-history as an unexpected resyncDocContent update.
- if (lines == null || version == null) {
- logger.debug(
- { projectId, docId },
- 'resyncing doc contents - not found in redis - retrieving from web'
- )
- PersistenceManager.getDoc(
- projectId,
- docId,
- { peek: true },
- (error, lines, version, ranges, pathname, projectHistoryId) => {
- if (error) {
- logger.error(
- { projectId, docId, getDocError: error },
- 'resyncing doc contents - error retrieving from web'
- )
- return callback(error)
- }
- ProjectHistoryRedisManager.queueResyncDocContent(
- projectId,
- projectHistoryId,
- docId,
- lines,
- version,
- path, // use the path from the resyncProjectStructure update
- callback
- )
- }
- )
- } else {
- logger.debug(
- { projectId, docId },
- 'resyncing doc contents - doc in redis - will queue in redis'
- )
- ProjectHistoryRedisManager.queueResyncDocContent(
- projectId,
- projectHistoryId,
- docId,
- lines,
- version,
- path, // use the path from the resyncProjectStructure update
- callback
- )
- }
- }
- )
- },
- getDocWithLock(projectId, docId, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.getDoc,
- projectId,
- docId,
- callback
- )
- },
- getDocAndRecentOpsWithLock(projectId, docId, fromVersion, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.getDocAndRecentOps,
- projectId,
- docId,
- fromVersion,
- callback
- )
- },
- getDocAndFlushIfOldWithLock(projectId, docId, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.getDocAndFlushIfOld,
- projectId,
- docId,
- callback
- )
- },
- setDocWithLock(projectId, docId, lines, source, userId, undoing, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.setDoc,
- projectId,
- docId,
- lines,
- source,
- userId,
- undoing,
- callback
- )
- },
- flushDocIfLoadedWithLock(projectId, docId, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.flushDocIfLoaded,
- projectId,
- docId,
- callback
- )
- },
- flushAndDeleteDocWithLock(projectId, docId, options, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.flushAndDeleteDoc,
- projectId,
- docId,
- options,
- callback
- )
- },
- acceptChangesWithLock(projectId, docId, changeIds, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.acceptChanges,
- projectId,
- docId,
- changeIds,
- callback
- )
- },
- deleteCommentWithLock(projectId, docId, threadId, userId, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.deleteComment,
- projectId,
- docId,
- threadId,
- userId,
- callback
- )
- },
- renameDocWithLock(
- projectId,
- docId,
- userId,
- update,
- projectHistoryId,
- callback
- ) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.renameDoc,
- projectId,
- docId,
- userId,
- update,
- projectHistoryId,
- callback
- )
- },
- resyncDocContentsWithLock(projectId, docId, path, callback) {
- const UpdateManager = require('./UpdateManager')
- UpdateManager.lockUpdatesAndDo(
- DocumentManager.resyncDocContents,
- projectId,
- docId,
- path,
- callback
- )
- },
- }
- module.exports = DocumentManager
- module.exports.promises = promisifyAll(DocumentManager, {
- multiResult: {
- getDoc: [
- 'lines',
- 'version',
- 'ranges',
- 'pathname',
- 'projectHistoryId',
- 'unflushedTime',
- 'alreadyLoaded',
- 'historyRangesSupport',
- ],
- getDocWithLock: [
- 'lines',
- 'version',
- 'ranges',
- 'pathname',
- 'projectHistoryId',
- 'unflushedTime',
- 'alreadyLoaded',
- 'historyRangesSupport',
- ],
- getDocAndFlushIfOld: ['lines', 'version'],
- getDocAndFlushIfOldWithLock: ['lines', 'version'],
- getDocAndRecentOps: [
- 'lines',
- 'version',
- 'ops',
- 'ranges',
- 'pathname',
- 'projectHistoryId',
- ],
- getDocAndRecentOpsWithLock: [
- 'lines',
- 'version',
- 'ops',
- 'ranges',
- 'pathname',
- 'projectHistoryId',
- ],
- },
- })
|