UpdatesManager.js 24 KB

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