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

81 lines
3.3 KiB
CoffeeScript
Raw Normal View History

2014-02-12 10:40:42 +00:00
ShareJsModel = require "./sharejs/server/model"
ShareJsDB = require "./ShareJsDB"
logger = require "logger-sharelatex"
Settings = require('settings-sharelatex')
Keys = require "./UpdateKeys"
2014-02-12 10:40:42 +00:00
{EventEmitter} = require "events"
util = require "util"
RealTimeRedisManager = require "./RealTimeRedisManager"
2019-04-08 12:43:24 +00:00
crypto = require "crypto"
metrics = require('./Metrics')
Errors = require("./Errors")
2014-02-12 10:40:42 +00:00
ShareJsModel:: = {}
util.inherits ShareJsModel, EventEmitter
2017-01-17 10:45:10 +00:00
MAX_AGE_OF_OP = 80
2014-02-12 10:40:42 +00:00
module.exports = ShareJsUpdateManager =
2016-11-28 10:14:42 +00:00
getNewShareJsModel: (project_id, doc_id, lines, version) ->
db = new ShareJsDB(project_id, doc_id, lines, version)
2017-01-17 10:45:10 +00:00
model = new ShareJsModel(db, maxDocLength: Settings.max_doc_length, maximumAge: MAX_AGE_OF_OP)
model.db = db
return model
2014-02-12 10:40:42 +00:00
2016-11-28 10:14:42 +00:00
applyUpdate: (project_id, doc_id, update, lines, version, callback = (error, updatedDocLines) ->) ->
logger.log project_id: project_id, doc_id: doc_id, update: update, "applying sharejs updates"
2014-02-12 10:40:42 +00:00
jobs = []
# record the update version before it is modified
incomingUpdateVersion = update.v
2014-02-12 10:40:42 +00:00
# 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).
2016-11-28 10:14:42 +00:00
model = @getNewShareJsModel(project_id, doc_id, lines, version)
2014-02-12 10:40:42 +00:00
@_listenForOps(model)
doc_key = Keys.combineProjectIdAndDocId(project_id, doc_id)
model.applyOp doc_key, update, (error) ->
2014-02-12 10:40:42 +00:00
if error?
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: project_id, doc_id: doc_id, error: error, "applied update"
2014-02-12 10:40:42 +00:00
model.getSnapshot doc_key, (error, data) =>
return callback(error) if error?
2019-04-08 12:43:24 +00:00
# only check hash when present and no other updates have been applied
if update.hash? and incomingUpdateVersion == version
2019-04-08 12:43:24 +00:00
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
docLines = data.snapshot.split(/\r\n|\n|\r/)
callback(null, docLines, data.v, model.db.appliedOps[doc_key] or [])
2014-02-12 10:40:42 +00:00
_listenForOps: (model) ->
model.on "applyOp", (doc_key, opData) ->
[project_id, doc_id] = Keys.splitProjectIdAndDocId(doc_key)
ShareJsUpdateManager._sendOp(project_id, doc_id, opData)
2016-11-28 10:14:42 +00:00
_sendOp: (project_id, doc_id, op) ->
RealTimeRedisManager.sendData {project_id, doc_id, op}
2014-02-12 10:40:42 +00:00
2019-04-08 12:43:24 +00:00
_computeHash: (content) ->
return crypto.createHash('sha1')
.update("blob " + content.length + "\x00")
.update(content, 'utf8')
.digest('hex')