UpdateManager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417
  1. /* eslint-disable
  2. camelcase,
  3. handle-callback-err,
  4. no-unused-vars,
  5. */
  6. // TODO: This file was created by bulk-decaffeinate.
  7. // Fix any style issues and re-enable lint.
  8. /*
  9. * decaffeinate suggestions:
  10. * DS101: Remove unnecessary use of Array.from
  11. * DS102: Remove unnecessary code created because of implicit returns
  12. * DS201: Simplify complex destructure assignments
  13. * DS205: Consider reworking code to avoid use of IIFEs
  14. * DS207: Consider shorter variations of null checks
  15. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  16. */
  17. let UpdateManager
  18. const LockManager = require('./LockManager')
  19. const RedisManager = require('./RedisManager')
  20. const RealTimeRedisManager = require('./RealTimeRedisManager')
  21. const ShareJsUpdateManager = require('./ShareJsUpdateManager')
  22. const HistoryManager = require('./HistoryManager')
  23. const Settings = require('@overleaf/settings')
  24. const _ = require('lodash')
  25. const async = require('async')
  26. const logger = require('logger-sharelatex')
  27. const Metrics = require('./Metrics')
  28. const Errors = require('./Errors')
  29. const DocumentManager = require('./DocumentManager')
  30. const RangesManager = require('./RangesManager')
  31. const SnapshotManager = require('./SnapshotManager')
  32. const Profiler = require('./Profiler')
  33. module.exports = UpdateManager = {
  34. processOutstandingUpdates(project_id, doc_id, callback) {
  35. if (callback == null) {
  36. callback = function (error) {}
  37. }
  38. const timer = new Metrics.Timer('updateManager.processOutstandingUpdates')
  39. return UpdateManager.fetchAndApplyUpdates(
  40. project_id,
  41. doc_id,
  42. function (error) {
  43. timer.done()
  44. if (error != null) {
  45. return callback(error)
  46. }
  47. return callback()
  48. }
  49. )
  50. },
  51. processOutstandingUpdatesWithLock(project_id, doc_id, callback) {
  52. if (callback == null) {
  53. callback = function (error) {}
  54. }
  55. const profile = new Profiler('processOutstandingUpdatesWithLock', {
  56. project_id,
  57. doc_id,
  58. })
  59. return LockManager.tryLock(doc_id, (error, gotLock, lockValue) => {
  60. if (error != null) {
  61. return callback(error)
  62. }
  63. if (!gotLock) {
  64. return callback()
  65. }
  66. profile.log('tryLock')
  67. return UpdateManager.processOutstandingUpdates(
  68. project_id,
  69. doc_id,
  70. function (error) {
  71. if (error != null) {
  72. return UpdateManager._handleErrorInsideLock(
  73. doc_id,
  74. lockValue,
  75. error,
  76. callback
  77. )
  78. }
  79. profile.log('processOutstandingUpdates')
  80. return LockManager.releaseLock(doc_id, lockValue, error => {
  81. if (error != null) {
  82. return callback(error)
  83. }
  84. profile.log('releaseLock').end()
  85. return UpdateManager.continueProcessingUpdatesWithLock(
  86. project_id,
  87. doc_id,
  88. callback
  89. )
  90. })
  91. }
  92. )
  93. })
  94. },
  95. continueProcessingUpdatesWithLock(project_id, doc_id, callback) {
  96. if (callback == null) {
  97. callback = function (error) {}
  98. }
  99. return RealTimeRedisManager.getUpdatesLength(doc_id, (error, length) => {
  100. if (error != null) {
  101. return callback(error)
  102. }
  103. if (length > 0) {
  104. return UpdateManager.processOutstandingUpdatesWithLock(
  105. project_id,
  106. doc_id,
  107. callback
  108. )
  109. } else {
  110. return callback()
  111. }
  112. })
  113. },
  114. fetchAndApplyUpdates(project_id, doc_id, callback) {
  115. if (callback == null) {
  116. callback = function (error) {}
  117. }
  118. const profile = new Profiler('fetchAndApplyUpdates', { project_id, doc_id })
  119. return RealTimeRedisManager.getPendingUpdatesForDoc(
  120. doc_id,
  121. (error, updates) => {
  122. if (error != null) {
  123. return callback(error)
  124. }
  125. logger.debug(
  126. { project_id, doc_id, count: updates.length },
  127. 'processing updates'
  128. )
  129. if (updates.length === 0) {
  130. return callback()
  131. }
  132. profile.log('getPendingUpdatesForDoc')
  133. const doUpdate = (update, cb) =>
  134. UpdateManager.applyUpdate(project_id, doc_id, update, function (err) {
  135. profile.log('applyUpdate')
  136. return cb(err)
  137. })
  138. const finalCallback = function (err) {
  139. profile.log('async done').end()
  140. return callback(err)
  141. }
  142. return async.eachSeries(updates, doUpdate, finalCallback)
  143. }
  144. )
  145. },
  146. applyUpdate(project_id, doc_id, update, _callback) {
  147. if (_callback == null) {
  148. _callback = function (error) {}
  149. }
  150. const callback = function (error) {
  151. if (error != null) {
  152. RealTimeRedisManager.sendData({
  153. project_id,
  154. doc_id,
  155. error: error.message || error,
  156. })
  157. profile.log('sendData')
  158. }
  159. profile.end()
  160. return _callback(error)
  161. }
  162. var profile = new Profiler('applyUpdate', { project_id, doc_id })
  163. UpdateManager._sanitizeUpdate(update)
  164. profile.log('sanitizeUpdate')
  165. return DocumentManager.getDoc(
  166. project_id,
  167. doc_id,
  168. function (error, lines, version, ranges, pathname, projectHistoryId) {
  169. profile.log('getDoc')
  170. if (error != null) {
  171. return callback(error)
  172. }
  173. if (lines == null || version == null) {
  174. return callback(
  175. new Errors.NotFoundError(`document not found: ${doc_id}`)
  176. )
  177. }
  178. const previousVersion = version
  179. return ShareJsUpdateManager.applyUpdate(
  180. project_id,
  181. doc_id,
  182. update,
  183. lines,
  184. version,
  185. function (error, updatedDocLines, version, appliedOps) {
  186. profile.log('sharejs.applyUpdate')
  187. if (error != null) {
  188. return callback(error)
  189. }
  190. return RangesManager.applyUpdate(
  191. project_id,
  192. doc_id,
  193. ranges,
  194. appliedOps,
  195. updatedDocLines,
  196. function (error, new_ranges, ranges_were_collapsed) {
  197. UpdateManager._addProjectHistoryMetadataToOps(
  198. appliedOps,
  199. pathname,
  200. projectHistoryId,
  201. lines
  202. )
  203. profile.log('RangesManager.applyUpdate')
  204. if (error != null) {
  205. return callback(error)
  206. }
  207. return RedisManager.updateDocument(
  208. project_id,
  209. doc_id,
  210. updatedDocLines,
  211. version,
  212. appliedOps,
  213. new_ranges,
  214. update.meta,
  215. function (error, doc_ops_length, project_ops_length) {
  216. profile.log('RedisManager.updateDocument')
  217. if (error != null) {
  218. return callback(error)
  219. }
  220. return HistoryManager.recordAndFlushHistoryOps(
  221. project_id,
  222. doc_id,
  223. appliedOps,
  224. doc_ops_length,
  225. project_ops_length,
  226. function (error) {
  227. profile.log('recordAndFlushHistoryOps')
  228. if (error != null) {
  229. return callback(error)
  230. }
  231. if (ranges_were_collapsed) {
  232. logger.debug(
  233. {
  234. project_id,
  235. doc_id,
  236. previousVersion,
  237. lines,
  238. ranges,
  239. update,
  240. },
  241. 'update collapsed some ranges, snapshotting previous content'
  242. )
  243. // Do this last, since it's a mongo call, and so potentially longest running
  244. // If it overruns the lock, it's ok, since all of our redis work is done
  245. return SnapshotManager.recordSnapshot(
  246. project_id,
  247. doc_id,
  248. previousVersion,
  249. pathname,
  250. lines,
  251. ranges,
  252. function (error) {
  253. if (error != null) {
  254. logger.error(
  255. {
  256. err: error,
  257. project_id,
  258. doc_id,
  259. version,
  260. lines,
  261. ranges,
  262. },
  263. 'error recording snapshot'
  264. )
  265. return callback(error)
  266. } else {
  267. return callback()
  268. }
  269. }
  270. )
  271. } else {
  272. return callback()
  273. }
  274. }
  275. )
  276. }
  277. )
  278. }
  279. )
  280. }
  281. )
  282. }
  283. )
  284. },
  285. lockUpdatesAndDo(method, project_id, doc_id, ...rest) {
  286. const adjustedLength = Math.max(rest.length, 1)
  287. const args = rest.slice(0, adjustedLength - 1)
  288. const callback = rest[adjustedLength - 1]
  289. const profile = new Profiler('lockUpdatesAndDo', { project_id, doc_id })
  290. return LockManager.getLock(doc_id, function (error, lockValue) {
  291. profile.log('getLock')
  292. if (error != null) {
  293. return callback(error)
  294. }
  295. return UpdateManager.processOutstandingUpdates(
  296. project_id,
  297. doc_id,
  298. function (error) {
  299. if (error != null) {
  300. return UpdateManager._handleErrorInsideLock(
  301. doc_id,
  302. lockValue,
  303. error,
  304. callback
  305. )
  306. }
  307. profile.log('processOutstandingUpdates')
  308. return method(
  309. project_id,
  310. doc_id,
  311. ...Array.from(args),
  312. function (error, ...response_args) {
  313. if (error != null) {
  314. return UpdateManager._handleErrorInsideLock(
  315. doc_id,
  316. lockValue,
  317. error,
  318. callback
  319. )
  320. }
  321. profile.log('method')
  322. return LockManager.releaseLock(
  323. doc_id,
  324. lockValue,
  325. function (error) {
  326. if (error != null) {
  327. return callback(error)
  328. }
  329. profile.log('releaseLock').end()
  330. callback(null, ...Array.from(response_args))
  331. // We held the lock for a while so updates might have queued up
  332. return UpdateManager.continueProcessingUpdatesWithLock(
  333. project_id,
  334. doc_id
  335. )
  336. }
  337. )
  338. }
  339. )
  340. }
  341. )
  342. })
  343. },
  344. _handleErrorInsideLock(doc_id, lockValue, original_error, callback) {
  345. if (callback == null) {
  346. callback = function (error) {}
  347. }
  348. return LockManager.releaseLock(doc_id, lockValue, lock_error =>
  349. callback(original_error)
  350. )
  351. },
  352. _sanitizeUpdate(update) {
  353. // In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
  354. //
  355. // From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
  356. // "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
  357. // for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
  358. // and one Low Surrogate. A single surrogate code point will never be assigned a character.""
  359. //
  360. // The main offender seems to be \uD835 as a stand alone character, which would be the first
  361. // 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
  362. // Something must be going on client side that is screwing up the encoding and splitting the
  363. // two 16-bit characters so that \uD835 is standalone.
  364. for (const op of Array.from(update.op || [])) {
  365. if (op.i != null) {
  366. // Replace high and low surrogate characters with 'replacement character' (\uFFFD)
  367. op.i = op.i.replace(/[\uD800-\uDFFF]/g, '\uFFFD')
  368. }
  369. }
  370. return update
  371. },
  372. _addProjectHistoryMetadataToOps(updates, pathname, projectHistoryId, lines) {
  373. let doc_length = _.reduce(lines, (chars, line) => chars + line.length, 0)
  374. doc_length += lines.length - 1 // count newline characters
  375. return updates.forEach(function (update) {
  376. update.projectHistoryId = projectHistoryId
  377. if (!update.meta) {
  378. update.meta = {}
  379. }
  380. update.meta.pathname = pathname
  381. update.meta.doc_length = doc_length
  382. // Each update may contain multiple ops, i.e.
  383. // [{
  384. // ops: [{i: "foo", p: 4}, {d: "bar", p:8}]
  385. // }, {
  386. // ops: [{d: "baz", p: 40}, {i: "qux", p:8}]
  387. // }]
  388. // We want to include the doc_length at the start of each update,
  389. // before it's ops are applied. However, we need to track any
  390. // changes to it for the next update.
  391. return (() => {
  392. const result = []
  393. for (const op of Array.from(update.op)) {
  394. if (op.i != null) {
  395. doc_length += op.i.length
  396. }
  397. if (op.d != null) {
  398. result.push((doc_length -= op.d.length))
  399. } else {
  400. result.push(undefined)
  401. }
  402. }
  403. return result
  404. })()
  405. })
  406. },
  407. }