UpdatesManager.js 27 KB

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