ShareJsUpdateManager.coffee 3.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475
  1. ShareJsModel = require "./sharejs/server/model"
  2. ShareJsDB = require "./ShareJsDB"
  3. logger = require "logger-sharelatex"
  4. Settings = require('settings-sharelatex')
  5. Keys = require "./UpdateKeys"
  6. {EventEmitter} = require "events"
  7. util = require "util"
  8. RealTimeRedisManager = require "./RealTimeRedisManager"
  9. crypto = require "crypto"
  10. metrics = require('./Metrics')
  11. Errors = require("./Errors")
  12. ShareJsModel:: = {}
  13. util.inherits ShareJsModel, EventEmitter
  14. MAX_AGE_OF_OP = 80
  15. module.exports = ShareJsUpdateManager =
  16. getNewShareJsModel: (project_id, doc_id, lines, version) ->
  17. db = new ShareJsDB(project_id, doc_id, lines, version)
  18. model = new ShareJsModel(db, maxDocLength: Settings.max_doc_length, maximumAge: MAX_AGE_OF_OP)
  19. model.db = db
  20. return model
  21. applyUpdate: (project_id, doc_id, update, lines, version, callback = (error, updatedDocLines) ->) ->
  22. logger.log project_id: project_id, doc_id: doc_id, update: update, "applying sharejs updates"
  23. jobs = []
  24. # record the update version before it is modified
  25. incomingUpdateVersion = update.version
  26. # We could use a global model for all docs, but we're hitting issues with the
  27. # internal state of ShareJS not being accessible for clearing caches, and
  28. # getting stuck due to queued callbacks (line 260 of sharejs/server/model.coffee)
  29. # This adds a small but hopefully acceptable overhead (~12ms per 1000 updates on
  30. # my 2009 MBP).
  31. model = @getNewShareJsModel(project_id, doc_id, lines, version)
  32. @_listenForOps(model)
  33. doc_key = Keys.combineProjectIdAndDocId(project_id, doc_id)
  34. model.applyOp doc_key, update, (error) ->
  35. if error?
  36. if error == "Op already submitted"
  37. metrics.inc "sharejs.already-submitted"
  38. logger.warn {project_id, doc_id, update}, "op has already been submitted"
  39. update.dup = true
  40. ShareJsUpdateManager._sendOp(project_id, doc_id, update)
  41. else if /^Delete component/.test(error)
  42. metrics.inc "sharejs.delete-mismatch"
  43. logger.warn {project_id, doc_id, update, shareJsErr: error}, "sharejs delete does not match"
  44. error = new Errors.DeleteMismatchError("Delete component does not match")
  45. return callback(error)
  46. else
  47. return callback(error)
  48. logger.log project_id: project_id, doc_id: doc_id, error: error, "applied update"
  49. model.getSnapshot doc_key, (error, data) =>
  50. return callback(error) if error?
  51. # only check hash when present and no other updates have been applied
  52. if update.hash? and incomingUpdateVersion == version
  53. ourHash = ShareJsUpdateManager._computeHash(data.snapshot)
  54. return callback(new Error("Invalid hash")) if ourHash != update.hash
  55. docLines = data.snapshot.split(/\r\n|\n|\r/)
  56. callback(null, docLines, data.v, model.db.appliedOps[doc_key] or [])
  57. _listenForOps: (model) ->
  58. model.on "applyOp", (doc_key, opData) ->
  59. [project_id, doc_id] = Keys.splitProjectIdAndDocId(doc_key)
  60. ShareJsUpdateManager._sendOp(project_id, doc_id, opData)
  61. _sendOp: (project_id, doc_id, op) ->
  62. RealTimeRedisManager.sendData {project_id, doc_id, op}
  63. _computeHash: (content) ->
  64. return crypto.createHash('sha1')
  65. .update("blob " + content.length + "\x00")
  66. .update(content, 'utf8')
  67. .digest('hex')