| 1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980 |
- ShareJsModel = require "./sharejs/server/model"
- ShareJsDB = require "./ShareJsDB"
- logger = require "logger-sharelatex"
- Settings = require('settings-sharelatex')
- Keys = require "./UpdateKeys"
- {EventEmitter} = require "events"
- util = require "util"
- RealTimeRedisManager = require "./RealTimeRedisManager"
- crypto = require "crypto"
- metrics = require('./Metrics')
- Errors = require("./Errors")
- ShareJsModel:: = {}
- util.inherits ShareJsModel, EventEmitter
- MAX_AGE_OF_OP = 80
- module.exports = ShareJsUpdateManager =
- getNewShareJsModel: (project_id, doc_id, lines, version) ->
- db = new ShareJsDB(project_id, doc_id, lines, version)
- 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 = (error, updatedDocLines) ->) ->
- logger.log project_id: project_id, doc_id: doc_id, update: update, "applying sharejs updates"
- jobs = []
- # record the update version before it is modified
- 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).
- model = @getNewShareJsModel(project_id, doc_id, lines, version)
- @_listenForOps(model)
- doc_key = Keys.combineProjectIdAndDocId(project_id, doc_id)
- model.applyOp doc_key, update, (error) ->
- 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"
- model.getSnapshot doc_key, (error, data) =>
- return callback(error) if error?
- # only check hash when present and no other updates have been applied
- if update.hash? and incomingUpdateVersion == version
- 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 [])
- _listenForOps: (model) ->
- model.on "applyOp", (doc_key, opData) ->
- [project_id, doc_id] = Keys.splitProjectIdAndDocId(doc_key)
- ShareJsUpdateManager._sendOp(project_id, doc_id, opData)
-
- _sendOp: (project_id, doc_id, op) ->
- RealTimeRedisManager.sendData {project_id, doc_id, op}
- _computeHash: (content) ->
- return crypto.createHash('sha1')
- .update("blob " + content.length + "\x00")
- .update(content, 'utf8')
- .digest('hex')
|