mirror of
https://github.com/overleaf/overleaf.git
synced 2024-12-03 20:59:49 -05:00
340 lines
15 KiB
JavaScript
340 lines
15 KiB
JavaScript
/*
|
|
* decaffeinate suggestions:
|
|
* DS101: Remove unnecessary use of Array.from
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
* DS207: Consider shorter variations of null checks
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
*/
|
|
let DocumentManager;
|
|
const RedisManager = require("./RedisManager");
|
|
const ProjectHistoryRedisManager = require("./ProjectHistoryRedisManager");
|
|
const PersistenceManager = require("./PersistenceManager");
|
|
const DiffCodec = require("./DiffCodec");
|
|
const logger = require("logger-sharelatex");
|
|
const Metrics = require("./Metrics");
|
|
const HistoryManager = require("./HistoryManager");
|
|
const RealTimeRedisManager = require("./RealTimeRedisManager");
|
|
const Errors = require("./Errors");
|
|
const RangesManager = require("./RangesManager");
|
|
const async = require("async");
|
|
|
|
const MAX_UNFLUSHED_AGE = 300 * 1000; // 5 mins, document should be flushed to mongo this time after a change
|
|
|
|
module.exports = (DocumentManager = {
|
|
getDoc(project_id, doc_id, _callback) {
|
|
if (_callback == null) { _callback = function(error, lines, version, ranges, pathname, projectHistoryId, unflushedTime, alreadyLoaded) {}; }
|
|
const timer = new Metrics.Timer("docManager.getDoc");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return RedisManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId, unflushedTime) {
|
|
if (error != null) { return callback(error); }
|
|
if ((lines == null) || (version == null)) {
|
|
logger.log({project_id, doc_id}, "doc not in redis so getting from persistence API");
|
|
return PersistenceManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId, projectHistoryType) {
|
|
if (error != null) { return callback(error); }
|
|
logger.log({project_id, doc_id, lines, version, pathname, projectHistoryId, projectHistoryType}, "got doc from persistence API");
|
|
return RedisManager.putDocInMemory(project_id, doc_id, lines, version, ranges, pathname, projectHistoryId, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return RedisManager.setHistoryType(doc_id, projectHistoryType, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback(null, lines, version, ranges || {}, pathname, projectHistoryId, null, false);
|
|
});
|
|
});
|
|
});
|
|
} else {
|
|
return callback(null, lines, version, ranges, pathname, projectHistoryId, unflushedTime, true);
|
|
}
|
|
});
|
|
},
|
|
|
|
getDocAndRecentOps(project_id, doc_id, fromVersion, _callback) {
|
|
if (_callback == null) { _callback = function(error, lines, version, ops, ranges, pathname, projectHistoryId) {}; }
|
|
const timer = new Metrics.Timer("docManager.getDocAndRecentOps");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return DocumentManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId) {
|
|
if (error != null) { return callback(error); }
|
|
if (fromVersion === -1) {
|
|
return callback(null, lines, version, [], ranges, pathname, projectHistoryId);
|
|
} else {
|
|
return RedisManager.getPreviousDocOps(doc_id, fromVersion, version, function(error, ops) {
|
|
if (error != null) { return callback(error); }
|
|
return callback(null, lines, version, ops, ranges, pathname, projectHistoryId);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
setDoc(project_id, doc_id, newLines, source, user_id, undoing, _callback) {
|
|
if (_callback == null) { _callback = function(error) {}; }
|
|
const timer = new Metrics.Timer("docManager.setDoc");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
if ((newLines == null)) {
|
|
return callback(new Error("No lines were provided to setDoc"));
|
|
}
|
|
|
|
const UpdateManager = require("./UpdateManager");
|
|
return DocumentManager.getDoc(project_id, doc_id, function(error, oldLines, version, ranges, pathname, projectHistoryId, unflushedTime, alreadyLoaded) {
|
|
if (error != null) { return callback(error); }
|
|
|
|
if ((oldLines != null) && (oldLines.length > 0) && (oldLines[0].text != null)) {
|
|
logger.log({doc_id, project_id, oldLines, newLines}, "document is JSON so not updating");
|
|
return callback(null);
|
|
}
|
|
|
|
logger.log({doc_id, project_id, oldLines, newLines}, "setting a document via http");
|
|
return DiffCodec.diffAsShareJsOp(oldLines, newLines, function(error, op) {
|
|
if (error != null) { return callback(error); }
|
|
if (undoing) {
|
|
for (let o of Array.from(op || [])) {
|
|
o.u = true;
|
|
} // Turn on undo flag for each op for track changes
|
|
}
|
|
const update = {
|
|
doc: doc_id,
|
|
op,
|
|
v: version,
|
|
meta: {
|
|
type: "external",
|
|
source,
|
|
user_id
|
|
}
|
|
};
|
|
return UpdateManager.applyUpdate(project_id, doc_id, update, function(error) {
|
|
if (error != null) { 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) {
|
|
return DocumentManager.flushDocIfLoaded(project_id, doc_id, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback(null);
|
|
});
|
|
} else {
|
|
return DocumentManager.flushAndDeleteDoc(project_id, doc_id, {}, function(error) {
|
|
// There is no harm in flushing project history if the previous
|
|
// call failed and sometimes it is required
|
|
HistoryManager.flushProjectChangesAsync(project_id);
|
|
|
|
if (error != null) { return callback(error); }
|
|
return callback(null);
|
|
});
|
|
}
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
flushDocIfLoaded(project_id, doc_id, _callback) {
|
|
if (_callback == null) { _callback = function(error) {}; }
|
|
const timer = new Metrics.Timer("docManager.flushDocIfLoaded");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
return RedisManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId, unflushedTime, lastUpdatedAt, lastUpdatedBy) {
|
|
if (error != null) { return callback(error); }
|
|
if ((lines == null) || (version == null)) {
|
|
logger.log({project_id, doc_id}, "doc is not loaded so not flushing");
|
|
return callback(null); // TODO: return a flag to bail out, as we go on to remove doc from memory?
|
|
} else {
|
|
logger.log({project_id, doc_id, version}, "flushing doc");
|
|
return PersistenceManager.setDoc(project_id, doc_id, lines, version, ranges, lastUpdatedAt, lastUpdatedBy, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return RedisManager.clearUnflushedTime(doc_id, callback);
|
|
});
|
|
}
|
|
});
|
|
},
|
|
|
|
flushAndDeleteDoc(project_id, doc_id, options, _callback) {
|
|
const timer = new Metrics.Timer("docManager.flushAndDeleteDoc");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return DocumentManager.flushDocIfLoaded(project_id, doc_id, function(error) {
|
|
if (error != null) {
|
|
if (options.ignoreFlushErrors) {
|
|
logger.warn({project_id, doc_id, err: error}, "ignoring flush error while deleting document");
|
|
} else {
|
|
return callback(error);
|
|
}
|
|
}
|
|
|
|
// Flush in the background since it requires a http request
|
|
HistoryManager.flushDocChangesAsync(project_id, doc_id);
|
|
|
|
return RedisManager.removeDocFromMemory(project_id, doc_id, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback(null);
|
|
});
|
|
});
|
|
},
|
|
|
|
acceptChanges(project_id, doc_id, change_ids, _callback) {
|
|
if (change_ids == null) { change_ids = []; }
|
|
if (_callback == null) { _callback = function(error) {}; }
|
|
const timer = new Metrics.Timer("docManager.acceptChanges");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return DocumentManager.getDoc(project_id, doc_id, function(error, lines, version, ranges) {
|
|
if (error != null) { return callback(error); }
|
|
if ((lines == null) || (version == null)) {
|
|
return callback(new Errors.NotFoundError(`document not found: ${doc_id}`));
|
|
}
|
|
return RangesManager.acceptChanges(change_ids, ranges, function(error, new_ranges) {
|
|
if (error != null) { return callback(error); }
|
|
return RedisManager.updateDocument(project_id, doc_id, lines, version, [], new_ranges, {}, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
deleteComment(project_id, doc_id, comment_id, _callback) {
|
|
if (_callback == null) { _callback = function(error) {}; }
|
|
const timer = new Metrics.Timer("docManager.deleteComment");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return DocumentManager.getDoc(project_id, doc_id, function(error, lines, version, ranges) {
|
|
if (error != null) { return callback(error); }
|
|
if ((lines == null) || (version == null)) {
|
|
return callback(new Errors.NotFoundError(`document not found: ${doc_id}`));
|
|
}
|
|
return RangesManager.deleteComment(comment_id, ranges, function(error, new_ranges) {
|
|
if (error != null) { return callback(error); }
|
|
return RedisManager.updateDocument(project_id, doc_id, lines, version, [], new_ranges, {}, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback();
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
renameDoc(project_id, doc_id, user_id, update, projectHistoryId, _callback) {
|
|
if (_callback == null) { _callback = function(error) {}; }
|
|
const timer = new Metrics.Timer("docManager.updateProject");
|
|
const callback = function(...args) {
|
|
timer.done();
|
|
return _callback(...Array.from(args || []));
|
|
};
|
|
|
|
return RedisManager.renameDoc(project_id, doc_id, user_id, update, projectHistoryId, callback);
|
|
},
|
|
|
|
getDocAndFlushIfOld(project_id, doc_id, callback) {
|
|
if (callback == null) { callback = function(error, doc) {}; }
|
|
return DocumentManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId, unflushedTime, alreadyLoaded) {
|
|
if (error != null) { 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)) {
|
|
return DocumentManager.flushDocIfLoaded(project_id, doc_id, function(error) {
|
|
if (error != null) { return callback(error); }
|
|
return callback(null, lines, version);
|
|
});
|
|
} else {
|
|
return callback(null, lines, version);
|
|
}
|
|
});
|
|
},
|
|
|
|
resyncDocContents(project_id, doc_id, callback) {
|
|
logger.log({project_id, doc_id}, "start resyncing doc contents");
|
|
return RedisManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId) {
|
|
if (error != null) { return callback(error); }
|
|
|
|
if ((lines == null) || (version == null)) {
|
|
logger.log({project_id, doc_id}, "resyncing doc contents - not found in redis - retrieving from web");
|
|
return PersistenceManager.getDoc(project_id, doc_id, function(error, lines, version, ranges, pathname, projectHistoryId) {
|
|
if (error != null) {
|
|
logger.error({project_id, doc_id, getDocError: error}, "resyncing doc contents - error retrieving from web");
|
|
return callback(error);
|
|
}
|
|
return ProjectHistoryRedisManager.queueResyncDocContent(project_id, projectHistoryId, doc_id, lines, version, pathname, callback);
|
|
});
|
|
} else {
|
|
logger.log({project_id, doc_id}, "resyncing doc contents - doc in redis - will queue in redis");
|
|
return ProjectHistoryRedisManager.queueResyncDocContent(project_id, projectHistoryId, doc_id, lines, version, pathname, callback);
|
|
}
|
|
});
|
|
},
|
|
|
|
getDocWithLock(project_id, doc_id, callback) {
|
|
if (callback == null) { callback = function(error, lines, version) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.getDoc, project_id, doc_id, callback);
|
|
},
|
|
|
|
getDocAndRecentOpsWithLock(project_id, doc_id, fromVersion, callback) {
|
|
if (callback == null) { callback = function(error, lines, version, ops, ranges, pathname, projectHistoryId) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.getDocAndRecentOps, project_id, doc_id, fromVersion, callback);
|
|
},
|
|
|
|
getDocAndFlushIfOldWithLock(project_id, doc_id, callback) {
|
|
if (callback == null) { callback = function(error, doc) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.getDocAndFlushIfOld, project_id, doc_id, callback);
|
|
},
|
|
|
|
setDocWithLock(project_id, doc_id, lines, source, user_id, undoing, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.setDoc, project_id, doc_id, lines, source, user_id, undoing, callback);
|
|
},
|
|
|
|
flushDocIfLoadedWithLock(project_id, doc_id, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.flushDocIfLoaded, project_id, doc_id, callback);
|
|
},
|
|
|
|
flushAndDeleteDocWithLock(project_id, doc_id, options, callback) {
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.flushAndDeleteDoc, project_id, doc_id, options, callback);
|
|
},
|
|
|
|
acceptChangesWithLock(project_id, doc_id, change_ids, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.acceptChanges, project_id, doc_id, change_ids, callback);
|
|
},
|
|
|
|
deleteCommentWithLock(project_id, doc_id, thread_id, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.deleteComment, project_id, doc_id, thread_id, callback);
|
|
},
|
|
|
|
renameDocWithLock(project_id, doc_id, user_id, update, projectHistoryId, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.renameDoc, project_id, doc_id, user_id, update, projectHistoryId, callback);
|
|
},
|
|
|
|
resyncDocContentsWithLock(project_id, doc_id, callback) {
|
|
if (callback == null) { callback = function(error) {}; }
|
|
const UpdateManager = require("./UpdateManager");
|
|
return UpdateManager.lockUpdatesAndDo(DocumentManager.resyncDocContents, project_id, doc_id, callback);
|
|
}
|
|
});
|