UpdatesManager.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895
  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. * DS103: Rewrite code to no longer use __guard__
  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 fiveMinutes, UpdatesManager
  18. const MongoManager = require('./MongoManager')
  19. const PackManager = require('./PackManager')
  20. const RedisManager = require('./RedisManager')
  21. const UpdateCompressor = require('./UpdateCompressor')
  22. const LockManager = require('./LockManager')
  23. const WebApiManager = require('./WebApiManager')
  24. const UpdateTrimmer = require('./UpdateTrimmer')
  25. const logger = require('logger-sharelatex')
  26. const async = require('async')
  27. const _ = require('underscore')
  28. const Settings = require('@overleaf/settings')
  29. const keys = Settings.redis.lock.key_schema
  30. const util = require('util')
  31. module.exports = UpdatesManager = {
  32. compressAndSaveRawUpdates(
  33. project_id,
  34. doc_id,
  35. rawUpdates,
  36. temporary,
  37. callback
  38. ) {
  39. let i
  40. if (callback == null) {
  41. callback = function (error) {}
  42. }
  43. const { length } = rawUpdates
  44. if (length === 0) {
  45. return callback()
  46. }
  47. // check that ops are in the correct order
  48. for (i = 0; i < rawUpdates.length; i++) {
  49. const op = rawUpdates[i]
  50. if (i > 0) {
  51. const thisVersion = op != null ? op.v : undefined
  52. const prevVersion = __guard__(rawUpdates[i - 1], x => x.v)
  53. if (!(prevVersion < thisVersion)) {
  54. logger.error(
  55. {
  56. project_id,
  57. doc_id,
  58. rawUpdates,
  59. temporary,
  60. thisVersion,
  61. prevVersion,
  62. },
  63. 'op versions out of order'
  64. )
  65. }
  66. }
  67. }
  68. // FIXME: we no longer need the lastCompressedUpdate, so change functions not to need it
  69. // CORRECTION: we do use it to log the time in case of error
  70. return MongoManager.peekLastCompressedUpdate(
  71. doc_id,
  72. function (error, lastCompressedUpdate, lastVersion) {
  73. // lastCompressedUpdate is the most recent update in Mongo, and
  74. // lastVersion is its sharejs version number.
  75. //
  76. // The peekLastCompressedUpdate method may pass the update back
  77. // as 'null' (for example if the previous compressed update has
  78. // been archived). In this case it can still pass back the
  79. // lastVersion from the update to allow us to check consistency.
  80. let op
  81. if (error != null) {
  82. return callback(error)
  83. }
  84. // Ensure that raw updates start where lastVersion left off
  85. if (lastVersion != null) {
  86. const discardedUpdates = []
  87. rawUpdates = rawUpdates.slice(0)
  88. while (rawUpdates[0] != null && rawUpdates[0].v <= lastVersion) {
  89. discardedUpdates.push(rawUpdates.shift())
  90. }
  91. if (discardedUpdates.length) {
  92. logger.error(
  93. { project_id, doc_id, discardedUpdates, temporary, lastVersion },
  94. 'discarded updates already present'
  95. )
  96. }
  97. if (rawUpdates[0] != null && rawUpdates[0].v !== lastVersion + 1) {
  98. const ts = __guard__(
  99. lastCompressedUpdate != null
  100. ? lastCompressedUpdate.meta
  101. : undefined,
  102. x1 => x1.end_ts
  103. )
  104. const last_timestamp = ts != null ? new Date(ts) : 'unknown time'
  105. error = new Error(
  106. `Tried to apply raw op at version ${rawUpdates[0].v} to last compressed update with version ${lastVersion} from ${last_timestamp}`
  107. )
  108. logger.error(
  109. {
  110. err: error,
  111. doc_id,
  112. project_id,
  113. prev_end_ts: ts,
  114. temporary,
  115. lastCompressedUpdate,
  116. },
  117. 'inconsistent doc versions'
  118. )
  119. if (
  120. (Settings.trackchanges != null
  121. ? Settings.trackchanges.continueOnError
  122. : undefined) &&
  123. rawUpdates[0].v > lastVersion + 1
  124. ) {
  125. // we have lost some ops - continue to write into the database, we can't recover at this point
  126. lastCompressedUpdate = null
  127. } else {
  128. return callback(error)
  129. }
  130. }
  131. }
  132. if (rawUpdates.length === 0) {
  133. return callback()
  134. }
  135. // some old large ops in redis need to be rejected, they predate
  136. // the size limit that now prevents them going through the system
  137. const REJECT_LARGE_OP_SIZE = 4 * 1024 * 1024
  138. for (var rawUpdate of Array.from(rawUpdates)) {
  139. const opSizes = (() => {
  140. const result = []
  141. for (op of Array.from(
  142. (rawUpdate != null ? rawUpdate.op : undefined) || []
  143. )) {
  144. result.push(
  145. (op.i != null ? op.i.length : undefined) ||
  146. (op.d != null ? op.d.length : undefined)
  147. )
  148. }
  149. return result
  150. })()
  151. const size = _.max(opSizes)
  152. if (size > REJECT_LARGE_OP_SIZE) {
  153. error = new Error(
  154. `dropped op exceeding maximum allowed size of ${REJECT_LARGE_OP_SIZE}`
  155. )
  156. logger.error(
  157. { err: error, doc_id, project_id, size, rawUpdate },
  158. 'dropped op - too big'
  159. )
  160. rawUpdate.op = []
  161. }
  162. }
  163. const compressedUpdates = UpdateCompressor.compressRawUpdates(
  164. null,
  165. rawUpdates
  166. )
  167. return PackManager.insertCompressedUpdates(
  168. project_id,
  169. doc_id,
  170. lastCompressedUpdate,
  171. compressedUpdates,
  172. temporary,
  173. function (error, result) {
  174. if (error != null) {
  175. return callback(error)
  176. }
  177. if (result != null) {
  178. logger.log(
  179. {
  180. project_id,
  181. doc_id,
  182. orig_v:
  183. lastCompressedUpdate != null
  184. ? lastCompressedUpdate.v
  185. : undefined,
  186. new_v: result.v,
  187. },
  188. 'inserted updates into pack'
  189. )
  190. }
  191. return callback()
  192. }
  193. )
  194. }
  195. )
  196. },
  197. // Check whether the updates are temporary (per-project property)
  198. _prepareProjectForUpdates(project_id, callback) {
  199. if (callback == null) {
  200. callback = function (error, temporary) {}
  201. }
  202. return UpdateTrimmer.shouldTrimUpdates(
  203. project_id,
  204. function (error, temporary) {
  205. if (error != null) {
  206. return callback(error)
  207. }
  208. return callback(null, temporary)
  209. }
  210. )
  211. },
  212. // Check for project id on document history (per-document property)
  213. _prepareDocForUpdates(project_id, doc_id, callback) {
  214. if (callback == null) {
  215. callback = function (error) {}
  216. }
  217. return MongoManager.backportProjectId(project_id, doc_id, function (error) {
  218. if (error != null) {
  219. return callback(error)
  220. }
  221. return callback(null)
  222. })
  223. },
  224. // Apply updates for specific project/doc after preparing at project and doc level
  225. REDIS_READ_BATCH_SIZE: 100,
  226. processUncompressedUpdates(project_id, doc_id, temporary, callback) {
  227. // get the updates as strings from redis (so we can delete them after they are applied)
  228. if (callback == null) {
  229. callback = function (error) {}
  230. }
  231. return RedisManager.getOldestDocUpdates(
  232. doc_id,
  233. UpdatesManager.REDIS_READ_BATCH_SIZE,
  234. function (error, docUpdates) {
  235. if (error != null) {
  236. return callback(error)
  237. }
  238. const { length } = docUpdates
  239. // parse the redis strings into ShareJs updates
  240. return RedisManager.expandDocUpdates(
  241. docUpdates,
  242. function (error, rawUpdates) {
  243. if (error != null) {
  244. logger.err(
  245. { project_id, doc_id, docUpdates },
  246. 'failed to parse docUpdates'
  247. )
  248. return callback(error)
  249. }
  250. logger.log(
  251. { project_id, doc_id, rawUpdates },
  252. 'retrieved raw updates from redis'
  253. )
  254. return UpdatesManager.compressAndSaveRawUpdates(
  255. project_id,
  256. doc_id,
  257. rawUpdates,
  258. temporary,
  259. function (error) {
  260. if (error != null) {
  261. return callback(error)
  262. }
  263. logger.log(
  264. { project_id, doc_id },
  265. 'compressed and saved doc updates'
  266. )
  267. // delete the applied updates from redis
  268. return RedisManager.deleteAppliedDocUpdates(
  269. project_id,
  270. doc_id,
  271. docUpdates,
  272. function (error) {
  273. if (error != null) {
  274. return callback(error)
  275. }
  276. if (length === UpdatesManager.REDIS_READ_BATCH_SIZE) {
  277. // There might be more updates
  278. logger.log(
  279. { project_id, doc_id },
  280. 'continuing processing updates'
  281. )
  282. return setTimeout(
  283. () =>
  284. UpdatesManager.processUncompressedUpdates(
  285. project_id,
  286. doc_id,
  287. temporary,
  288. callback
  289. ),
  290. 0
  291. )
  292. } else {
  293. logger.log(
  294. { project_id, doc_id },
  295. 'all raw updates processed'
  296. )
  297. return callback()
  298. }
  299. }
  300. )
  301. }
  302. )
  303. }
  304. )
  305. }
  306. )
  307. },
  308. // Process updates for a doc when we flush it individually
  309. processUncompressedUpdatesWithLock(project_id, doc_id, callback) {
  310. if (callback == null) {
  311. callback = function (error) {}
  312. }
  313. return UpdatesManager._prepareProjectForUpdates(
  314. project_id,
  315. function (error, temporary) {
  316. if (error != null) {
  317. return callback(error)
  318. }
  319. return UpdatesManager._processUncompressedUpdatesForDocWithLock(
  320. project_id,
  321. doc_id,
  322. temporary,
  323. callback
  324. )
  325. }
  326. )
  327. },
  328. // Process updates for a doc when the whole project is flushed (internal method)
  329. _processUncompressedUpdatesForDocWithLock(
  330. project_id,
  331. doc_id,
  332. temporary,
  333. callback
  334. ) {
  335. if (callback == null) {
  336. callback = function (error) {}
  337. }
  338. return UpdatesManager._prepareDocForUpdates(
  339. project_id,
  340. doc_id,
  341. function (error) {
  342. if (error != null) {
  343. return callback(error)
  344. }
  345. return LockManager.runWithLock(
  346. keys.historyLock({ doc_id }),
  347. releaseLock =>
  348. UpdatesManager.processUncompressedUpdates(
  349. project_id,
  350. doc_id,
  351. temporary,
  352. releaseLock
  353. ),
  354. callback
  355. )
  356. }
  357. )
  358. },
  359. // Process all updates for a project, only check project-level information once
  360. processUncompressedUpdatesForProject(project_id, callback) {
  361. if (callback == null) {
  362. callback = function (error) {}
  363. }
  364. return RedisManager.getDocIdsWithHistoryOps(
  365. project_id,
  366. function (error, doc_ids) {
  367. if (error != null) {
  368. return callback(error)
  369. }
  370. return UpdatesManager._prepareProjectForUpdates(
  371. project_id,
  372. function (error, temporary) {
  373. const jobs = []
  374. for (const doc_id of Array.from(doc_ids)) {
  375. ;(doc_id =>
  376. jobs.push(cb =>
  377. UpdatesManager._processUncompressedUpdatesForDocWithLock(
  378. project_id,
  379. doc_id,
  380. temporary,
  381. cb
  382. )
  383. ))(doc_id)
  384. }
  385. return async.parallelLimit(jobs, 5, callback)
  386. }
  387. )
  388. }
  389. )
  390. },
  391. // flush all outstanding changes
  392. flushAll(limit, callback) {
  393. if (callback == null) {
  394. callback = function (error, result) {}
  395. }
  396. return RedisManager.getProjectIdsWithHistoryOps(function (
  397. error,
  398. project_ids
  399. ) {
  400. let project_id
  401. if (error != null) {
  402. return callback(error)
  403. }
  404. logger.log(
  405. {
  406. count: project_ids != null ? project_ids.length : undefined,
  407. project_ids,
  408. },
  409. 'found projects'
  410. )
  411. const jobs = []
  412. project_ids = _.shuffle(project_ids) // randomise to avoid hitting same projects each time
  413. const selectedProjects =
  414. limit < 0 ? project_ids : project_ids.slice(0, limit)
  415. for (project_id of Array.from(selectedProjects)) {
  416. ;(project_id =>
  417. jobs.push(cb =>
  418. UpdatesManager.processUncompressedUpdatesForProject(
  419. project_id,
  420. err => cb(null, { failed: err != null, project_id })
  421. )
  422. ))(project_id)
  423. }
  424. return async.series(jobs, function (error, result) {
  425. let x
  426. if (error != null) {
  427. return callback(error)
  428. }
  429. const failedProjects = (() => {
  430. const result1 = []
  431. for (x of Array.from(result)) {
  432. if (x.failed) {
  433. result1.push(x.project_id)
  434. }
  435. }
  436. return result1
  437. })()
  438. const succeededProjects = (() => {
  439. const result2 = []
  440. for (x of Array.from(result)) {
  441. if (!x.failed) {
  442. result2.push(x.project_id)
  443. }
  444. }
  445. return result2
  446. })()
  447. return callback(null, {
  448. failed: failedProjects,
  449. succeeded: succeededProjects,
  450. all: project_ids,
  451. })
  452. })
  453. })
  454. },
  455. getDanglingUpdates(callback) {
  456. if (callback == null) {
  457. callback = function (error, doc_ids) {}
  458. }
  459. return RedisManager.getAllDocIdsWithHistoryOps(function (
  460. error,
  461. all_doc_ids
  462. ) {
  463. if (error != null) {
  464. return callback(error)
  465. }
  466. return RedisManager.getProjectIdsWithHistoryOps(function (
  467. error,
  468. all_project_ids
  469. ) {
  470. if (error != null) {
  471. return callback(error)
  472. }
  473. // function to get doc_ids for each project
  474. const task = cb =>
  475. async.concatSeries(
  476. all_project_ids,
  477. RedisManager.getDocIdsWithHistoryOps,
  478. cb
  479. )
  480. // find the dangling doc ids
  481. return task(function (error, project_doc_ids) {
  482. const dangling_doc_ids = _.difference(all_doc_ids, project_doc_ids)
  483. logger.log(
  484. { all_doc_ids, all_project_ids, project_doc_ids, dangling_doc_ids },
  485. 'checking for dangling doc ids'
  486. )
  487. return callback(null, dangling_doc_ids)
  488. })
  489. })
  490. })
  491. },
  492. getDocUpdates(project_id, doc_id, options, callback) {
  493. if (options == null) {
  494. options = {}
  495. }
  496. if (callback == null) {
  497. callback = function (error, updates) {}
  498. }
  499. return UpdatesManager.processUncompressedUpdatesWithLock(
  500. project_id,
  501. doc_id,
  502. function (error) {
  503. if (error != null) {
  504. return callback(error)
  505. }
  506. // console.log "options", options
  507. return PackManager.getOpsByVersionRange(
  508. project_id,
  509. doc_id,
  510. options.from,
  511. options.to,
  512. function (error, updates) {
  513. if (error != null) {
  514. return callback(error)
  515. }
  516. return callback(null, updates)
  517. }
  518. )
  519. }
  520. )
  521. },
  522. getDocUpdatesWithUserInfo(project_id, doc_id, options, callback) {
  523. if (options == null) {
  524. options = {}
  525. }
  526. if (callback == null) {
  527. callback = function (error, updates) {}
  528. }
  529. return UpdatesManager.getDocUpdates(
  530. project_id,
  531. doc_id,
  532. options,
  533. function (error, updates) {
  534. if (error != null) {
  535. return callback(error)
  536. }
  537. return UpdatesManager.fillUserInfo(updates, function (error, updates) {
  538. if (error != null) {
  539. return callback(error)
  540. }
  541. return callback(null, updates)
  542. })
  543. }
  544. )
  545. },
  546. getSummarizedProjectUpdates(project_id, options, callback) {
  547. if (options == null) {
  548. options = {}
  549. }
  550. if (callback == null) {
  551. callback = function (error, updates) {}
  552. }
  553. if (!options.min_count) {
  554. options.min_count = 25
  555. }
  556. let summarizedUpdates = []
  557. const { before } = options
  558. let nextBeforeTimestamp = null
  559. return UpdatesManager.processUncompressedUpdatesForProject(
  560. project_id,
  561. function (error) {
  562. if (error != null) {
  563. return callback(error)
  564. }
  565. return PackManager.makeProjectIterator(
  566. project_id,
  567. before,
  568. function (err, iterator) {
  569. if (err != null) {
  570. return callback(err)
  571. }
  572. // repeatedly get updates and pass them through the summariser to get an final output with user info
  573. return async.whilst(
  574. () =>
  575. // console.log "checking iterator.done", iterator.done()
  576. summarizedUpdates.length < options.min_count &&
  577. !iterator.done(),
  578. cb =>
  579. iterator.next(function (err, partialUpdates) {
  580. if (err != null) {
  581. return callback(err)
  582. }
  583. // logger.log {partialUpdates}, 'got partialUpdates'
  584. if (partialUpdates.length === 0) {
  585. return cb()
  586. } // # FIXME should try to avoid this happening
  587. nextBeforeTimestamp =
  588. partialUpdates[partialUpdates.length - 1].meta.end_ts
  589. // add the updates to the summary list
  590. summarizedUpdates = UpdatesManager._summarizeUpdates(
  591. partialUpdates,
  592. summarizedUpdates
  593. )
  594. return cb()
  595. }),
  596. () =>
  597. // finally done all updates
  598. // console.log 'summarized Updates', summarizedUpdates
  599. UpdatesManager.fillSummarizedUserInfo(
  600. summarizedUpdates,
  601. function (err, results) {
  602. if (err != null) {
  603. return callback(err)
  604. }
  605. return callback(
  606. null,
  607. results,
  608. !iterator.done() ? nextBeforeTimestamp : undefined
  609. )
  610. }
  611. )
  612. )
  613. }
  614. )
  615. }
  616. )
  617. },
  618. exportProject(projectId, consumer) {
  619. // Flush anything before collecting updates.
  620. UpdatesManager.processUncompressedUpdatesForProject(projectId, err => {
  621. if (err) return consumer(err)
  622. // Fetch all the packs.
  623. const before = undefined
  624. PackManager.makeProjectIterator(projectId, before, (err, iterator) => {
  625. if (err) return consumer(err)
  626. const accumulatedUserIds = new Set()
  627. async.whilst(
  628. () => !iterator.done(),
  629. cb =>
  630. iterator.next((err, updatesFromASinglePack) => {
  631. if (err) return cb(err)
  632. if (updatesFromASinglePack.length === 0) {
  633. // This should not happen when `iterator.done() == false`.
  634. // Emitting an empty array would signal the consumer the final
  635. // call.
  636. return cb()
  637. }
  638. updatesFromASinglePack.forEach(update => {
  639. accumulatedUserIds.add(
  640. // Super defensive access on update details.
  641. String(update && update.meta && update.meta.user_id)
  642. )
  643. })
  644. // Emit updates and wait for the consumer.
  645. consumer(null, { updates: updatesFromASinglePack }, cb)
  646. }),
  647. err => {
  648. if (err) return consumer(err)
  649. // Adding undefined can happen for broken updates.
  650. accumulatedUserIds.delete('undefined')
  651. consumer(null, {
  652. updates: [],
  653. userIds: Array.from(accumulatedUserIds).sort(),
  654. })
  655. }
  656. )
  657. })
  658. })
  659. },
  660. fetchUserInfo(users, callback) {
  661. if (callback == null) {
  662. callback = function (error, fetchedUserInfo) {}
  663. }
  664. const jobs = []
  665. const fetchedUserInfo = {}
  666. for (const user_id in users) {
  667. ;(user_id =>
  668. jobs.push(callback =>
  669. WebApiManager.getUserInfo(user_id, function (error, userInfo) {
  670. if (error != null) {
  671. return callback(error)
  672. }
  673. fetchedUserInfo[user_id] = userInfo
  674. return callback()
  675. })
  676. ))(user_id)
  677. }
  678. return async.series(jobs, function (err) {
  679. if (err != null) {
  680. return callback(err)
  681. }
  682. return callback(null, fetchedUserInfo)
  683. })
  684. },
  685. fillUserInfo(updates, callback) {
  686. let update, user_id
  687. if (callback == null) {
  688. callback = function (error, updates) {}
  689. }
  690. const users = {}
  691. for (update of Array.from(updates)) {
  692. ;({ user_id } = update.meta)
  693. if (UpdatesManager._validUserId(user_id)) {
  694. users[user_id] = true
  695. }
  696. }
  697. return UpdatesManager.fetchUserInfo(
  698. users,
  699. function (error, fetchedUserInfo) {
  700. if (error != null) {
  701. return callback(error)
  702. }
  703. for (update of Array.from(updates)) {
  704. ;({ user_id } = update.meta)
  705. delete update.meta.user_id
  706. if (UpdatesManager._validUserId(user_id)) {
  707. update.meta.user = fetchedUserInfo[user_id]
  708. }
  709. }
  710. return callback(null, updates)
  711. }
  712. )
  713. },
  714. fillSummarizedUserInfo(updates, callback) {
  715. let update, user_id, user_ids
  716. if (callback == null) {
  717. callback = function (error, updates) {}
  718. }
  719. const users = {}
  720. for (update of Array.from(updates)) {
  721. user_ids = update.meta.user_ids || []
  722. for (user_id of Array.from(user_ids)) {
  723. if (UpdatesManager._validUserId(user_id)) {
  724. users[user_id] = true
  725. }
  726. }
  727. }
  728. return UpdatesManager.fetchUserInfo(
  729. users,
  730. function (error, fetchedUserInfo) {
  731. if (error != null) {
  732. return callback(error)
  733. }
  734. for (update of Array.from(updates)) {
  735. user_ids = update.meta.user_ids || []
  736. update.meta.users = []
  737. delete update.meta.user_ids
  738. for (user_id of Array.from(user_ids)) {
  739. if (UpdatesManager._validUserId(user_id)) {
  740. update.meta.users.push(fetchedUserInfo[user_id])
  741. } else {
  742. update.meta.users.push(null)
  743. }
  744. }
  745. }
  746. return callback(null, updates)
  747. }
  748. )
  749. },
  750. _validUserId(user_id) {
  751. if (user_id == null) {
  752. return false
  753. } else {
  754. return !!user_id.match(/^[a-f0-9]{24}$/)
  755. }
  756. },
  757. TIME_BETWEEN_DISTINCT_UPDATES: (fiveMinutes = 5 * 60 * 1000),
  758. SPLIT_ON_DELETE_SIZE: 16, // characters
  759. _summarizeUpdates(updates, existingSummarizedUpdates) {
  760. if (existingSummarizedUpdates == null) {
  761. existingSummarizedUpdates = []
  762. }
  763. const summarizedUpdates = existingSummarizedUpdates.slice()
  764. let previousUpdateWasBigDelete = false
  765. for (const update of Array.from(updates)) {
  766. var doc_id
  767. const earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  768. let shouldConcat = false
  769. // If a user inserts some text, then deletes a big chunk including that text,
  770. // the update we show might concat the insert and delete, and there will be no sign
  771. // of that insert having happened, or be able to restore to it (restoring after a big delete is common).
  772. // So, we split the summary on 'big' deletes. However, we've stepping backwards in time with
  773. // most recent changes considered first, so if this update is a big delete, we want to start
  774. // a new summarized update next timge, hence we monitor the previous update.
  775. if (previousUpdateWasBigDelete) {
  776. shouldConcat = false
  777. } else if (
  778. earliestUpdate &&
  779. earliestUpdate.meta.end_ts - update.meta.start_ts <
  780. this.TIME_BETWEEN_DISTINCT_UPDATES
  781. ) {
  782. // We're going backwards in time through the updates, so only combine if this update starts less than 5 minutes before
  783. // the end of current summarized block, so no block spans more than 5 minutes.
  784. shouldConcat = true
  785. }
  786. let isBigDelete = false
  787. for (const op of Array.from(update.op || [])) {
  788. if (op.d != null && op.d.length > this.SPLIT_ON_DELETE_SIZE) {
  789. isBigDelete = true
  790. }
  791. }
  792. previousUpdateWasBigDelete = isBigDelete
  793. if (shouldConcat) {
  794. // check if the user in this update is already present in the earliest update,
  795. // if not, add them to the users list of the earliest update
  796. earliestUpdate.meta.user_ids = _.union(earliestUpdate.meta.user_ids, [
  797. update.meta.user_id,
  798. ])
  799. doc_id = update.doc_id.toString()
  800. const doc = earliestUpdate.docs[doc_id]
  801. if (doc != null) {
  802. doc.fromV = Math.min(doc.fromV, update.v)
  803. doc.toV = Math.max(doc.toV, update.v)
  804. } else {
  805. earliestUpdate.docs[doc_id] = {
  806. fromV: update.v,
  807. toV: update.v,
  808. }
  809. }
  810. earliestUpdate.meta.start_ts = Math.min(
  811. earliestUpdate.meta.start_ts,
  812. update.meta.start_ts
  813. )
  814. earliestUpdate.meta.end_ts = Math.max(
  815. earliestUpdate.meta.end_ts,
  816. update.meta.end_ts
  817. )
  818. } else {
  819. const newUpdate = {
  820. meta: {
  821. user_ids: [],
  822. start_ts: update.meta.start_ts,
  823. end_ts: update.meta.end_ts,
  824. },
  825. docs: {},
  826. }
  827. newUpdate.docs[update.doc_id.toString()] = {
  828. fromV: update.v,
  829. toV: update.v,
  830. }
  831. newUpdate.meta.user_ids.push(update.meta.user_id)
  832. summarizedUpdates.push(newUpdate)
  833. }
  834. }
  835. return summarizedUpdates
  836. },
  837. }
  838. module.exports.promises = {
  839. processUncompressedUpdatesForProject: util.promisify(
  840. UpdatesManager.processUncompressedUpdatesForProject
  841. ),
  842. }
  843. function __guard__(value, transform) {
  844. return typeof value !== 'undefined' && value !== null
  845. ? transform(value)
  846. : undefined
  847. }