overleaf/services/document-updater/app/coffee/ShareJsUpdateManager.js

103 lines
4 KiB
JavaScript
Raw Normal View History

/*
* 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
ShareJsModel.prototype = {};
util.inherits(ShareJsModel, EventEmitter);
2014-02-12 05:40:42 -05:00
const MAX_AGE_OF_OP = 80;
2017-01-17 05:45:10 -05: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
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
_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);
});
},
_sendOp(project_id, doc_id, op) {
return RealTimeRedisManager.sendData({project_id, doc_id, op});
},
2014-02-12 05:40:42 -05:00
_computeHash(content) {
2019-04-08 08:43:24 -04:00
return crypto.createHash('sha1')
.update("blob " + content.length + "\x00")
.update(content, 'utf8')
.digest('hex');
}
});
2019-04-08 08:43:24 -04:00