2020-05-06 06:09:15 -04:00
|
|
|
|
/* 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.
|
2020-05-06 06:08:21 -04:00
|
|
|
|
/*
|
|
|
|
|
* decaffeinate suggestions:
|
|
|
|
|
* DS101: Remove unnecessary use of Array.from
|
|
|
|
|
* DS102: Remove unnecessary code created because of implicit returns
|
|
|
|
|
* DS201: Simplify complex destructure assignments
|
|
|
|
|
* DS205: Consider reworking code to avoid use of IIFEs
|
|
|
|
|
* DS207: Consider shorter variations of null checks
|
|
|
|
|
* Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
|
|
|
|
|
*/
|
2020-05-06 06:09:33 -04:00
|
|
|
|
let UpdateManager
|
|
|
|
|
const LockManager = require('./LockManager')
|
|
|
|
|
const RedisManager = require('./RedisManager')
|
|
|
|
|
const RealTimeRedisManager = require('./RealTimeRedisManager')
|
|
|
|
|
const ShareJsUpdateManager = require('./ShareJsUpdateManager')
|
|
|
|
|
const HistoryManager = require('./HistoryManager')
|
2021-07-12 12:47:15 -04:00
|
|
|
|
const Settings = require('@overleaf/settings')
|
2020-05-06 06:09:33 -04:00
|
|
|
|
const _ = require('lodash')
|
|
|
|
|
const async = require('async')
|
|
|
|
|
const logger = require('logger-sharelatex')
|
|
|
|
|
const Metrics = require('./Metrics')
|
|
|
|
|
const Errors = require('./Errors')
|
|
|
|
|
const DocumentManager = require('./DocumentManager')
|
|
|
|
|
const RangesManager = require('./RangesManager')
|
|
|
|
|
const SnapshotManager = require('./SnapshotManager')
|
|
|
|
|
const Profiler = require('./Profiler')
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
module.exports = UpdateManager = {
|
|
|
|
|
processOutstandingUpdates(project_id, doc_id, callback) {
|
|
|
|
|
if (callback == null) {
|
|
|
|
|
callback = function (error) {}
|
|
|
|
|
}
|
|
|
|
|
const timer = new Metrics.Timer('updateManager.processOutstandingUpdates')
|
2021-07-13 07:04:42 -04:00
|
|
|
|
return UpdateManager.fetchAndApplyUpdates(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
function (error) {
|
|
|
|
|
timer.done()
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
return callback()
|
2020-05-06 06:09:33 -04:00
|
|
|
|
}
|
2021-07-13 07:04:42 -04:00
|
|
|
|
)
|
2020-05-06 06:09:33 -04:00
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
processOutstandingUpdatesWithLock(project_id, doc_id, callback) {
|
|
|
|
|
if (callback == null) {
|
|
|
|
|
callback = function (error) {}
|
|
|
|
|
}
|
|
|
|
|
const profile = new Profiler('processOutstandingUpdatesWithLock', {
|
|
|
|
|
project_id,
|
2021-07-13 07:04:42 -04:00
|
|
|
|
doc_id,
|
2020-05-06 06:09:33 -04:00
|
|
|
|
})
|
|
|
|
|
return LockManager.tryLock(doc_id, (error, gotLock, lockValue) => {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
if (!gotLock) {
|
|
|
|
|
return callback()
|
|
|
|
|
}
|
|
|
|
|
profile.log('tryLock')
|
|
|
|
|
return UpdateManager.processOutstandingUpdates(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
function (error) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return UpdateManager._handleErrorInsideLock(
|
|
|
|
|
doc_id,
|
|
|
|
|
lockValue,
|
|
|
|
|
error,
|
|
|
|
|
callback
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
profile.log('processOutstandingUpdates')
|
2021-07-13 07:04:42 -04:00
|
|
|
|
return LockManager.releaseLock(doc_id, lockValue, error => {
|
2020-05-06 06:09:33 -04:00
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
profile.log('releaseLock').end()
|
|
|
|
|
return UpdateManager.continueProcessingUpdatesWithLock(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
callback
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
continueProcessingUpdatesWithLock(project_id, doc_id, callback) {
|
|
|
|
|
if (callback == null) {
|
|
|
|
|
callback = function (error) {}
|
|
|
|
|
}
|
|
|
|
|
return RealTimeRedisManager.getUpdatesLength(doc_id, (error, length) => {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
if (length > 0) {
|
|
|
|
|
return UpdateManager.processOutstandingUpdatesWithLock(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
callback
|
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
return callback()
|
|
|
|
|
}
|
|
|
|
|
})
|
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
fetchAndApplyUpdates(project_id, doc_id, callback) {
|
|
|
|
|
if (callback == null) {
|
|
|
|
|
callback = function (error) {}
|
|
|
|
|
}
|
|
|
|
|
const profile = new Profiler('fetchAndApplyUpdates', { project_id, doc_id })
|
|
|
|
|
return RealTimeRedisManager.getPendingUpdatesForDoc(
|
|
|
|
|
doc_id,
|
|
|
|
|
(error, updates) => {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
logger.log(
|
|
|
|
|
{ project_id, doc_id, count: updates.length },
|
|
|
|
|
'processing updates'
|
|
|
|
|
)
|
|
|
|
|
if (updates.length === 0) {
|
|
|
|
|
return callback()
|
|
|
|
|
}
|
|
|
|
|
profile.log('getPendingUpdatesForDoc')
|
|
|
|
|
const doUpdate = (update, cb) =>
|
|
|
|
|
UpdateManager.applyUpdate(project_id, doc_id, update, function (err) {
|
|
|
|
|
profile.log('applyUpdate')
|
|
|
|
|
return cb(err)
|
|
|
|
|
})
|
|
|
|
|
const finalCallback = function (err) {
|
|
|
|
|
profile.log('async done').end()
|
|
|
|
|
return callback(err)
|
|
|
|
|
}
|
|
|
|
|
return async.eachSeries(updates, doUpdate, finalCallback)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
applyUpdate(project_id, doc_id, update, _callback) {
|
|
|
|
|
if (_callback == null) {
|
|
|
|
|
_callback = function (error) {}
|
|
|
|
|
}
|
|
|
|
|
const callback = function (error) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
RealTimeRedisManager.sendData({
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
2021-07-13 07:04:42 -04:00
|
|
|
|
error: error.message || error,
|
2020-05-06 06:09:33 -04:00
|
|
|
|
})
|
|
|
|
|
profile.log('sendData')
|
|
|
|
|
}
|
|
|
|
|
profile.end()
|
|
|
|
|
return _callback(error)
|
|
|
|
|
}
|
2017-05-19 11:00:16 -04:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
var profile = new Profiler('applyUpdate', { project_id, doc_id })
|
|
|
|
|
UpdateManager._sanitizeUpdate(update)
|
|
|
|
|
profile.log('sanitizeUpdate')
|
2021-07-13 07:04:42 -04:00
|
|
|
|
return DocumentManager.getDoc(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
function (error, lines, version, ranges, pathname, projectHistoryId) {
|
|
|
|
|
profile.log('getDoc')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
if (lines == null || version == null) {
|
|
|
|
|
return callback(
|
|
|
|
|
new Errors.NotFoundError(`document not found: ${doc_id}`)
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
const previousVersion = version
|
|
|
|
|
return ShareJsUpdateManager.applyUpdate(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
update,
|
|
|
|
|
lines,
|
|
|
|
|
version,
|
|
|
|
|
function (error, updatedDocLines, version, appliedOps) {
|
|
|
|
|
profile.log('sharejs.applyUpdate')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
return RangesManager.applyUpdate(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
ranges,
|
|
|
|
|
appliedOps,
|
|
|
|
|
updatedDocLines,
|
|
|
|
|
function (error, new_ranges, ranges_were_collapsed) {
|
|
|
|
|
UpdateManager._addProjectHistoryMetadataToOps(
|
|
|
|
|
appliedOps,
|
|
|
|
|
pathname,
|
|
|
|
|
projectHistoryId,
|
|
|
|
|
lines
|
|
|
|
|
)
|
|
|
|
|
profile.log('RangesManager.applyUpdate')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
return RedisManager.updateDocument(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
updatedDocLines,
|
|
|
|
|
version,
|
|
|
|
|
appliedOps,
|
|
|
|
|
new_ranges,
|
|
|
|
|
update.meta,
|
|
|
|
|
function (error, doc_ops_length, project_ops_length) {
|
|
|
|
|
profile.log('RedisManager.updateDocument')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
return HistoryManager.recordAndFlushHistoryOps(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
appliedOps,
|
|
|
|
|
doc_ops_length,
|
|
|
|
|
project_ops_length,
|
|
|
|
|
function (error) {
|
|
|
|
|
profile.log('recordAndFlushHistoryOps')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
if (ranges_were_collapsed) {
|
|
|
|
|
logger.log(
|
|
|
|
|
{
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
previousVersion,
|
|
|
|
|
lines,
|
|
|
|
|
ranges,
|
|
|
|
|
update,
|
|
|
|
|
},
|
|
|
|
|
'update collapsed some ranges, snapshotting previous content'
|
|
|
|
|
)
|
|
|
|
|
// Do this last, since it's a mongo call, and so potentially longest running
|
|
|
|
|
// If it overruns the lock, it's ok, since all of our redis work is done
|
|
|
|
|
return SnapshotManager.recordSnapshot(
|
2020-05-06 06:09:33 -04:00
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
previousVersion,
|
2021-07-13 07:04:42 -04:00
|
|
|
|
pathname,
|
2020-05-06 06:09:33 -04:00
|
|
|
|
lines,
|
|
|
|
|
ranges,
|
2021-07-13 07:04:42 -04:00
|
|
|
|
function (error) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
logger.error(
|
|
|
|
|
{
|
|
|
|
|
err: error,
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
version,
|
|
|
|
|
lines,
|
|
|
|
|
ranges,
|
|
|
|
|
},
|
|
|
|
|
'error recording snapshot'
|
|
|
|
|
)
|
|
|
|
|
return callback(error)
|
|
|
|
|
} else {
|
|
|
|
|
return callback()
|
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
}
|
2021-07-13 07:04:42 -04:00
|
|
|
|
)
|
|
|
|
|
} else {
|
|
|
|
|
return callback()
|
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
}
|
2021-07-13 07:04:42 -04:00
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
)
|
2020-05-06 06:09:33 -04:00
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
lockUpdatesAndDo(method, project_id, doc_id, ...rest) {
|
|
|
|
|
const adjustedLength = Math.max(rest.length, 1)
|
|
|
|
|
const args = rest.slice(0, adjustedLength - 1)
|
|
|
|
|
const callback = rest[adjustedLength - 1]
|
|
|
|
|
const profile = new Profiler('lockUpdatesAndDo', { project_id, doc_id })
|
|
|
|
|
return LockManager.getLock(doc_id, function (error, lockValue) {
|
|
|
|
|
profile.log('getLock')
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
return UpdateManager.processOutstandingUpdates(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
function (error) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return UpdateManager._handleErrorInsideLock(
|
|
|
|
|
doc_id,
|
|
|
|
|
lockValue,
|
|
|
|
|
error,
|
|
|
|
|
callback
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
profile.log('processOutstandingUpdates')
|
2021-07-13 07:04:42 -04:00
|
|
|
|
return method(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id,
|
|
|
|
|
...Array.from(args),
|
|
|
|
|
function (error, ...response_args) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return UpdateManager._handleErrorInsideLock(
|
|
|
|
|
doc_id,
|
|
|
|
|
lockValue,
|
|
|
|
|
error,
|
|
|
|
|
callback
|
|
|
|
|
)
|
|
|
|
|
}
|
|
|
|
|
profile.log('method')
|
|
|
|
|
return LockManager.releaseLock(
|
2020-05-06 06:09:33 -04:00
|
|
|
|
doc_id,
|
|
|
|
|
lockValue,
|
2021-07-13 07:04:42 -04:00
|
|
|
|
function (error) {
|
|
|
|
|
if (error != null) {
|
|
|
|
|
return callback(error)
|
|
|
|
|
}
|
|
|
|
|
profile.log('releaseLock').end()
|
|
|
|
|
callback(null, ...Array.from(response_args))
|
|
|
|
|
// We held the lock for a while so updates might have queued up
|
|
|
|
|
return UpdateManager.continueProcessingUpdatesWithLock(
|
|
|
|
|
project_id,
|
|
|
|
|
doc_id
|
|
|
|
|
)
|
|
|
|
|
}
|
2020-05-06 06:09:33 -04:00
|
|
|
|
)
|
|
|
|
|
}
|
2021-07-13 07:04:42 -04:00
|
|
|
|
)
|
2020-05-06 06:09:33 -04:00
|
|
|
|
}
|
|
|
|
|
)
|
|
|
|
|
})
|
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
_handleErrorInsideLock(doc_id, lockValue, original_error, callback) {
|
|
|
|
|
if (callback == null) {
|
|
|
|
|
callback = function (error) {}
|
|
|
|
|
}
|
2021-07-13 07:04:42 -04:00
|
|
|
|
return LockManager.releaseLock(doc_id, lockValue, lock_error =>
|
2020-05-06 06:09:33 -04:00
|
|
|
|
callback(original_error)
|
|
|
|
|
)
|
|
|
|
|
},
|
2017-09-29 07:57:27 -04:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
_sanitizeUpdate(update) {
|
|
|
|
|
// In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
|
|
|
|
|
//
|
|
|
|
|
// From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
|
|
|
|
|
// "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
|
|
|
|
|
// for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
|
|
|
|
|
// and one Low Surrogate. A single surrogate code point will never be assigned a character.""
|
|
|
|
|
//
|
|
|
|
|
// The main offender seems to be \uD835 as a stand alone character, which would be the first
|
|
|
|
|
// 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
|
|
|
|
|
// Something must be going on client side that is screwing up the encoding and splitting the
|
|
|
|
|
// two 16-bit characters so that \uD835 is standalone.
|
|
|
|
|
for (const op of Array.from(update.op || [])) {
|
|
|
|
|
if (op.i != null) {
|
|
|
|
|
// Replace high and low surrogate characters with 'replacement character' (\uFFFD)
|
|
|
|
|
op.i = op.i.replace(/[\uD800-\uDFFF]/g, '\uFFFD')
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return update
|
|
|
|
|
},
|
2014-02-12 05:40:42 -05:00
|
|
|
|
|
2020-05-06 06:09:33 -04:00
|
|
|
|
_addProjectHistoryMetadataToOps(updates, pathname, projectHistoryId, lines) {
|
|
|
|
|
let doc_length = _.reduce(lines, (chars, line) => chars + line.length, 0)
|
|
|
|
|
doc_length += lines.length - 1 // count newline characters
|
|
|
|
|
return updates.forEach(function (update) {
|
|
|
|
|
update.projectHistoryId = projectHistoryId
|
|
|
|
|
if (!update.meta) {
|
|
|
|
|
update.meta = {}
|
|
|
|
|
}
|
|
|
|
|
update.meta.pathname = pathname
|
|
|
|
|
update.meta.doc_length = doc_length
|
|
|
|
|
// Each update may contain multiple ops, i.e.
|
|
|
|
|
// [{
|
|
|
|
|
// ops: [{i: "foo", p: 4}, {d: "bar", p:8}]
|
|
|
|
|
// }, {
|
|
|
|
|
// ops: [{d: "baz", p: 40}, {i: "qux", p:8}]
|
|
|
|
|
// }]
|
|
|
|
|
// We want to include the doc_length at the start of each update,
|
|
|
|
|
// before it's ops are applied. However, we need to track any
|
|
|
|
|
// changes to it for the next update.
|
|
|
|
|
return (() => {
|
|
|
|
|
const result = []
|
|
|
|
|
for (const op of Array.from(update.op)) {
|
|
|
|
|
if (op.i != null) {
|
|
|
|
|
doc_length += op.i.length
|
|
|
|
|
}
|
|
|
|
|
if (op.d != null) {
|
|
|
|
|
result.push((doc_length -= op.d.length))
|
|
|
|
|
} else {
|
|
|
|
|
result.push(undefined)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return result
|
|
|
|
|
})()
|
|
|
|
|
})
|
2021-07-13 07:04:42 -04:00
|
|
|
|
},
|
2020-05-06 06:09:33 -04:00
|
|
|
|
}
|