UpdateManager.js 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422
  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('@overleaf/logger')
  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', { sync: true })
  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. const incomingUpdateVersion = update.v
  180. return ShareJsUpdateManager.applyUpdate(
  181. project_id,
  182. doc_id,
  183. update,
  184. lines,
  185. version,
  186. function (error, updatedDocLines, version, appliedOps) {
  187. profile.log('sharejs.applyUpdate', {
  188. // only synchronous when the update applies directly to the
  189. // doc version, otherwise getPreviousDocOps is called.
  190. sync: incomingUpdateVersion === previousVersion,
  191. })
  192. if (error != null) {
  193. return callback(error)
  194. }
  195. return RangesManager.applyUpdate(
  196. project_id,
  197. doc_id,
  198. ranges,
  199. appliedOps,
  200. updatedDocLines,
  201. function (error, new_ranges, ranges_were_collapsed) {
  202. UpdateManager._addProjectHistoryMetadataToOps(
  203. appliedOps,
  204. pathname,
  205. projectHistoryId,
  206. lines
  207. )
  208. profile.log('RangesManager.applyUpdate', { sync: true })
  209. if (error != null) {
  210. return callback(error)
  211. }
  212. return RedisManager.updateDocument(
  213. project_id,
  214. doc_id,
  215. updatedDocLines,
  216. version,
  217. appliedOps,
  218. new_ranges,
  219. update.meta,
  220. function (error, doc_ops_length, project_ops_length) {
  221. profile.log('RedisManager.updateDocument')
  222. if (error != null) {
  223. return callback(error)
  224. }
  225. return HistoryManager.recordAndFlushHistoryOps(
  226. project_id,
  227. doc_id,
  228. appliedOps,
  229. doc_ops_length,
  230. project_ops_length,
  231. function (error) {
  232. profile.log('recordAndFlushHistoryOps')
  233. if (error != null) {
  234. return callback(error)
  235. }
  236. if (ranges_were_collapsed) {
  237. logger.debug(
  238. {
  239. project_id,
  240. doc_id,
  241. previousVersion,
  242. lines,
  243. ranges,
  244. update,
  245. },
  246. 'update collapsed some ranges, snapshotting previous content'
  247. )
  248. // Do this last, since it's a mongo call, and so potentially longest running
  249. // If it overruns the lock, it's ok, since all of our redis work is done
  250. return SnapshotManager.recordSnapshot(
  251. project_id,
  252. doc_id,
  253. previousVersion,
  254. pathname,
  255. lines,
  256. ranges,
  257. function (error) {
  258. if (error != null) {
  259. logger.error(
  260. {
  261. err: error,
  262. project_id,
  263. doc_id,
  264. version,
  265. lines,
  266. ranges,
  267. },
  268. 'error recording snapshot'
  269. )
  270. return callback(error)
  271. } else {
  272. return callback()
  273. }
  274. }
  275. )
  276. } else {
  277. return callback()
  278. }
  279. }
  280. )
  281. }
  282. )
  283. }
  284. )
  285. }
  286. )
  287. }
  288. )
  289. },
  290. lockUpdatesAndDo(method, project_id, doc_id, ...rest) {
  291. const adjustedLength = Math.max(rest.length, 1)
  292. const args = rest.slice(0, adjustedLength - 1)
  293. const callback = rest[adjustedLength - 1]
  294. const profile = new Profiler('lockUpdatesAndDo', { project_id, doc_id })
  295. return LockManager.getLock(doc_id, function (error, lockValue) {
  296. profile.log('getLock')
  297. if (error != null) {
  298. return callback(error)
  299. }
  300. return UpdateManager.processOutstandingUpdates(
  301. project_id,
  302. doc_id,
  303. function (error) {
  304. if (error != null) {
  305. return UpdateManager._handleErrorInsideLock(
  306. doc_id,
  307. lockValue,
  308. error,
  309. callback
  310. )
  311. }
  312. profile.log('processOutstandingUpdates')
  313. return method(
  314. project_id,
  315. doc_id,
  316. ...Array.from(args),
  317. function (error, ...response_args) {
  318. if (error != null) {
  319. return UpdateManager._handleErrorInsideLock(
  320. doc_id,
  321. lockValue,
  322. error,
  323. callback
  324. )
  325. }
  326. profile.log('method')
  327. return LockManager.releaseLock(
  328. doc_id,
  329. lockValue,
  330. function (error) {
  331. if (error != null) {
  332. return callback(error)
  333. }
  334. profile.log('releaseLock').end()
  335. callback(null, ...Array.from(response_args))
  336. // We held the lock for a while so updates might have queued up
  337. return UpdateManager.continueProcessingUpdatesWithLock(
  338. project_id,
  339. doc_id
  340. )
  341. }
  342. )
  343. }
  344. )
  345. }
  346. )
  347. })
  348. },
  349. _handleErrorInsideLock(doc_id, lockValue, original_error, callback) {
  350. if (callback == null) {
  351. callback = function (error) {}
  352. }
  353. return LockManager.releaseLock(doc_id, lockValue, lock_error =>
  354. callback(original_error)
  355. )
  356. },
  357. _sanitizeUpdate(update) {
  358. // In Javascript, characters are 16-bits wide. It does not understand surrogates as characters.
  359. //
  360. // From Wikipedia (http://en.wikipedia.org/wiki/Plane_(Unicode)#Basic_Multilingual_Plane):
  361. // "The High Surrogates (U+D800–U+DBFF) and Low Surrogate (U+DC00–U+DFFF) codes are reserved
  362. // for encoding non-BMP characters in UTF-16 by using a pair of 16-bit codes: one High Surrogate
  363. // and one Low Surrogate. A single surrogate code point will never be assigned a character.""
  364. //
  365. // The main offender seems to be \uD835 as a stand alone character, which would be the first
  366. // 16-bit character of a blackboard bold character (http://www.fileformat.info/info/unicode/char/1d400/index.htm).
  367. // Something must be going on client side that is screwing up the encoding and splitting the
  368. // two 16-bit characters so that \uD835 is standalone.
  369. for (const op of Array.from(update.op || [])) {
  370. if (op.i != null) {
  371. // Replace high and low surrogate characters with 'replacement character' (\uFFFD)
  372. op.i = op.i.replace(/[\uD800-\uDFFF]/g, '\uFFFD')
  373. }
  374. }
  375. return update
  376. },
  377. _addProjectHistoryMetadataToOps(updates, pathname, projectHistoryId, lines) {
  378. let doc_length = _.reduce(lines, (chars, line) => chars + line.length, 0)
  379. doc_length += lines.length - 1 // count newline characters
  380. return updates.forEach(function (update) {
  381. update.projectHistoryId = projectHistoryId
  382. if (!update.meta) {
  383. update.meta = {}
  384. }
  385. update.meta.pathname = pathname
  386. update.meta.doc_length = doc_length
  387. // Each update may contain multiple ops, i.e.
  388. // [{
  389. // ops: [{i: "foo", p: 4}, {d: "bar", p:8}]
  390. // }, {
  391. // ops: [{d: "baz", p: 40}, {i: "qux", p:8}]
  392. // }]
  393. // We want to include the doc_length at the start of each update,
  394. // before it's ops are applied. However, we need to track any
  395. // changes to it for the next update.
  396. return (() => {
  397. const result = []
  398. for (const op of Array.from(update.op)) {
  399. if (op.i != null) {
  400. doc_length += op.i.length
  401. }
  402. if (op.d != null) {
  403. result.push((doc_length -= op.d.length))
  404. } else {
  405. result.push(undefined)
  406. }
  407. }
  408. return result
  409. })()
  410. })
  411. },
  412. }