2020-05-06 06:08:21 -04:00
|
|
|
/*
|
|
|
|
* 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 ShareJsUpdateManager;
|
|
|
|
const ShareJsModel = require("./sharejs/server/model");
|
|
|
|
const ShareJsDB = require("./ShareJsDB");
|
|
|
|
const logger = require("logger-sharelatex");
|
|
|
|
const Settings = require('settings-sharelatex');
|
|
|
|
const Keys = require("./UpdateKeys");
|
|
|
|
const {EventEmitter} = require("events");
|
|
|
|
const util = require("util");
|
|
|
|
const RealTimeRedisManager = require("./RealTimeRedisManager");
|
|
|
|
const crypto = require("crypto");
|
|
|
|
const metrics = require('./Metrics');
|
|
|
|
const Errors = require("./Errors");
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
ShareJsModel.prototype = {};
|
|
|
|
util.inherits(ShareJsModel, EventEmitter);
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
const MAX_AGE_OF_OP = 80;
|
2017-01-17 05:45:10 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
module.exports = (ShareJsUpdateManager = {
|
|
|
|
getNewShareJsModel(project_id, doc_id, lines, version) {
|
|
|
|
const db = new ShareJsDB(project_id, doc_id, lines, version);
|
|
|
|
const model = new ShareJsModel(db, {maxDocLength: Settings.max_doc_length, maximumAge: MAX_AGE_OF_OP});
|
|
|
|
model.db = db;
|
|
|
|
return model;
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
applyUpdate(project_id, doc_id, update, lines, version, callback) {
|
|
|
|
if (callback == null) { callback = function(error, updatedDocLines) {}; }
|
|
|
|
logger.log({project_id, doc_id, update}, "applying sharejs updates");
|
|
|
|
const jobs = [];
|
|
|
|
// record the update version before it is modified
|
|
|
|
const incomingUpdateVersion = update.v;
|
|
|
|
// We could use a global model for all docs, but we're hitting issues with the
|
|
|
|
// internal state of ShareJS not being accessible for clearing caches, and
|
|
|
|
// getting stuck due to queued callbacks (line 260 of sharejs/server/model.coffee)
|
|
|
|
// This adds a small but hopefully acceptable overhead (~12ms per 1000 updates on
|
|
|
|
// my 2009 MBP).
|
|
|
|
const model = this.getNewShareJsModel(project_id, doc_id, lines, version);
|
|
|
|
this._listenForOps(model);
|
|
|
|
const doc_key = Keys.combineProjectIdAndDocId(project_id, doc_id);
|
|
|
|
return model.applyOp(doc_key, update, function(error) {
|
|
|
|
if (error != null) {
|
|
|
|
if (error === "Op already submitted") {
|
|
|
|
metrics.inc("sharejs.already-submitted");
|
|
|
|
logger.warn({project_id, doc_id, update}, "op has already been submitted");
|
|
|
|
update.dup = true;
|
|
|
|
ShareJsUpdateManager._sendOp(project_id, doc_id, update);
|
|
|
|
} else if (/^Delete component/.test(error)) {
|
|
|
|
metrics.inc("sharejs.delete-mismatch");
|
|
|
|
logger.warn({project_id, doc_id, update, shareJsErr: error}, "sharejs delete does not match");
|
|
|
|
error = new Errors.DeleteMismatchError("Delete component does not match");
|
|
|
|
return callback(error);
|
|
|
|
} else {
|
|
|
|
metrics.inc("sharejs.other-error");
|
|
|
|
return callback(error);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
logger.log({project_id, doc_id, error}, "applied update");
|
|
|
|
return model.getSnapshot(doc_key, (error, data) => {
|
|
|
|
if (error != null) { return callback(error); }
|
|
|
|
// only check hash when present and no other updates have been applied
|
|
|
|
if ((update.hash != null) && (incomingUpdateVersion === version)) {
|
|
|
|
const ourHash = ShareJsUpdateManager._computeHash(data.snapshot);
|
|
|
|
if (ourHash !== update.hash) {
|
|
|
|
metrics.inc("sharejs.hash-fail");
|
|
|
|
return callback(new Error("Invalid hash"));
|
|
|
|
} else {
|
|
|
|
metrics.inc("sharejs.hash-pass", 0.001);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
const docLines = data.snapshot.split(/\r\n|\n|\r/);
|
|
|
|
return callback(null, docLines, data.v, model.db.appliedOps[doc_key] || []);
|
|
|
|
});
|
|
|
|
});
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
_listenForOps(model) {
|
|
|
|
return model.on("applyOp", function(doc_key, opData) {
|
|
|
|
const [project_id, doc_id] = Array.from(Keys.splitProjectIdAndDocId(doc_key));
|
|
|
|
return ShareJsUpdateManager._sendOp(project_id, doc_id, opData);
|
|
|
|
});
|
|
|
|
},
|
2015-11-19 05:54:28 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
_sendOp(project_id, doc_id, op) {
|
|
|
|
return RealTimeRedisManager.sendData({project_id, doc_id, op});
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
2020-05-06 06:08:21 -04:00
|
|
|
_computeHash(content) {
|
2019-04-08 08:43:24 -04:00
|
|
|
return crypto.createHash('sha1')
|
|
|
|
.update("blob " + content.length + "\x00")
|
|
|
|
.update(content, 'utf8')
|
2020-05-06 06:08:21 -04:00
|
|
|
.digest('hex');
|
|
|
|
}
|
|
|
|
});
|
2019-04-08 08:43:24 -04:00
|
|
|
|