overleaf/services/document-updater/app/coffee/ShareJsUpdateManager.coffee
James Allen 2589e2d417 Gracefully return when an op has already been submitted
It is not a fatal error if an op has already been submitted. We just
need to send an ack back to the client that submitted it and continue.
If we detect a duplicate op, set dup: true on the op and pass it back
to real-time for distributing. The dup: true flag will ensure it only
gets acknowledged to the submitting client, not everyone.
2015-11-19 10:54:28 +00:00

75 lines
2.5 KiB
CoffeeScript

ShareJsModel = require "./sharejs/server/model"
ShareJsDB = require "./ShareJsDB"
async = require "async"
logger = require "logger-sharelatex"
Settings = require('settings-sharelatex')
Keys = require "./RedisKeyBuilder"
{EventEmitter} = require "events"
util = require "util"
redis = require("redis-sharelatex")
rclient = redis.createClient(Settings.redis.web)
ShareJsModel:: = {}
util.inherits ShareJsModel, EventEmitter
module.exports = ShareJsUpdateManager =
getNewShareJsModel: () -> new ShareJsModel(ShareJsDB, maxDocLength: Settings.max_doc_length)
applyUpdates: (project_id, doc_id, updates, callback = (error, updatedDocLines) ->) ->
logger.log project_id: project_id, doc_id: doc_id, updates: updates, "applying sharejs updates"
jobs = []
# 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()
@_listenForOps(model)
doc_key = Keys.combineProjectIdAndDocId(project_id, doc_id)
for update in updates
do (update) =>
jobs.push (callback) =>
model.applyOp doc_key, update, (error) ->
if error == "Op already submitted"
logger.warn {project_id, doc_id, update}, "op has already been submitted"
update.dup = true
ShareJsUpdateManager._sendOp(project_id, doc_id, update)
callback()
else
callback(error)
async.series jobs, (error) =>
logger.log project_id: project_id, doc_id: doc_id, error: error, "applied updates"
if error?
@_sendError(project_id, doc_id, error)
return callback(error)
model.getSnapshot doc_key, (error, data) =>
if error?
@_sendError(project_id, doc_id, error)
return callback(error)
docLines = data.snapshot.split(/\r\n|\n|\r/)
callback(null, docLines, data.v)
_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, opData) ->
data =
project_id: project_id
doc_id: doc_id
op: opData
data = JSON.stringify data
rclient.publish "applied-ops", data
_sendError: (project_id, doc_id, error) ->
data = JSON.stringify
project_id: project_id
doc_id: doc_id
error: error.message || error
rclient.publish "applied-ops", data