UpdatesManager.js 27 KB

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