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

131 lines
4.5 KiB
JavaScript

/* eslint-disable
camelcase,
handle-callback-err,
no-unused-vars,
*/
// TODO: This file was created by bulk-decaffeinate.
// Fix any style issues and re-enable lint.
/*
* 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')
ShareJsModel.prototype = {}
util.inherits(ShareJsModel, EventEmitter)
const MAX_AGE_OF_OP = 80
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
},
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] || []
)
})
})
},
_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 })
},
_computeHash(content) {
return crypto
.createHash('sha1')
.update('blob ' + content.length + '\x00')
.update(content, 'utf8')
.digest('hex')
}
}