UpdatesManager.js 27 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896
  1. /* eslint-disable
  2. camelcase,
  3. no-unused-vars,
  4. */
  5. // TODO: This file was created by bulk-decaffeinate.
  6. // Fix any style issues and re-enable lint.
  7. /*
  8. * decaffeinate suggestions:
  9. * DS101: Remove unnecessary use of Array.from
  10. * DS102: Remove unnecessary code created because of implicit returns
  11. * DS103: Rewrite code to no longer use __guard__
  12. * DS205: Consider reworking code to avoid use of IIFEs
  13. * DS207: Consider shorter variations of null checks
  14. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  15. */
  16. let fiveMinutes, UpdatesManager
  17. const MongoManager = require('./MongoManager')
  18. const PackManager = require('./PackManager')
  19. const RedisManager = require('./RedisManager')
  20. const UpdateCompressor = require('./UpdateCompressor')
  21. const LockManager = require('./LockManager')
  22. const WebApiManager = require('./WebApiManager')
  23. const UpdateTrimmer = require('./UpdateTrimmer')
  24. const logger = require('@overleaf/logger')
  25. const async = require('async')
  26. const _ = require('underscore')
  27. const Settings = require('@overleaf/settings')
  28. const keys = Settings.redis.lock.key_schema
  29. const util = require('util')
  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 () {}
  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 (const 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.debug(
  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 () {}
  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 () {}
  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 () {}
  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.debug(
  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.debug(
  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.debug(
  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.debug(
  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 () {}
  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 () {}
  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 () {}
  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. if (error) return callback(error)
  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 () {}
  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.debug(
  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 () {}
  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. if (error) return callback(error)
  483. const dangling_doc_ids = _.difference(all_doc_ids, project_doc_ids)
  484. logger.debug(
  485. { all_doc_ids, all_project_ids, project_doc_ids, dangling_doc_ids },
  486. 'checking for dangling doc ids'
  487. )
  488. return callback(null, dangling_doc_ids)
  489. })
  490. })
  491. })
  492. },
  493. getDocUpdates(project_id, doc_id, options, callback) {
  494. if (options == null) {
  495. options = {}
  496. }
  497. if (callback == null) {
  498. callback = function () {}
  499. }
  500. return UpdatesManager.processUncompressedUpdatesWithLock(
  501. project_id,
  502. doc_id,
  503. function (error) {
  504. if (error != null) {
  505. return callback(error)
  506. }
  507. // console.log "options", options
  508. return PackManager.getOpsByVersionRange(
  509. project_id,
  510. doc_id,
  511. options.from,
  512. options.to,
  513. function (error, updates) {
  514. if (error != null) {
  515. return callback(error)
  516. }
  517. return callback(null, updates)
  518. }
  519. )
  520. }
  521. )
  522. },
  523. getDocUpdatesWithUserInfo(project_id, doc_id, options, callback) {
  524. if (options == null) {
  525. options = {}
  526. }
  527. if (callback == null) {
  528. callback = function () {}
  529. }
  530. return UpdatesManager.getDocUpdates(
  531. project_id,
  532. doc_id,
  533. options,
  534. function (error, updates) {
  535. if (error != null) {
  536. return callback(error)
  537. }
  538. return UpdatesManager.fillUserInfo(updates, function (error, updates) {
  539. if (error != null) {
  540. return callback(error)
  541. }
  542. return callback(null, updates)
  543. })
  544. }
  545. )
  546. },
  547. getSummarizedProjectUpdates(project_id, options, callback) {
  548. if (options == null) {
  549. options = {}
  550. }
  551. if (callback == null) {
  552. callback = function () {}
  553. }
  554. if (!options.min_count) {
  555. options.min_count = 25
  556. }
  557. let summarizedUpdates = []
  558. const { before } = options
  559. let nextBeforeTimestamp = null
  560. return UpdatesManager.processUncompressedUpdatesForProject(
  561. project_id,
  562. function (error) {
  563. if (error != null) {
  564. return callback(error)
  565. }
  566. return PackManager.makeProjectIterator(
  567. project_id,
  568. before,
  569. function (err, iterator) {
  570. if (err != null) {
  571. return callback(err)
  572. }
  573. // repeatedly get updates and pass them through the summariser to get an final output with user info
  574. return async.whilst(
  575. () =>
  576. // console.log "checking iterator.done", iterator.done()
  577. summarizedUpdates.length < options.min_count &&
  578. !iterator.done(),
  579. cb =>
  580. iterator.next(function (err, partialUpdates) {
  581. if (err != null) {
  582. return callback(err)
  583. }
  584. // logger.log {partialUpdates}, 'got partialUpdates'
  585. if (partialUpdates.length === 0) {
  586. return cb()
  587. } // # FIXME should try to avoid this happening
  588. nextBeforeTimestamp =
  589. partialUpdates[partialUpdates.length - 1].meta.end_ts
  590. // add the updates to the summary list
  591. summarizedUpdates = UpdatesManager._summarizeUpdates(
  592. partialUpdates,
  593. summarizedUpdates
  594. )
  595. return cb()
  596. }),
  597. () =>
  598. // finally done all updates
  599. // console.log 'summarized Updates', summarizedUpdates
  600. UpdatesManager.fillSummarizedUserInfo(
  601. summarizedUpdates,
  602. function (err, results) {
  603. if (err != null) {
  604. return callback(err)
  605. }
  606. return callback(
  607. null,
  608. results,
  609. !iterator.done() ? nextBeforeTimestamp : undefined
  610. )
  611. }
  612. )
  613. )
  614. }
  615. )
  616. }
  617. )
  618. },
  619. exportProject(projectId, consumer) {
  620. // Flush anything before collecting updates.
  621. UpdatesManager.processUncompressedUpdatesForProject(projectId, err => {
  622. if (err) return consumer(err)
  623. // Fetch all the packs.
  624. const before = undefined
  625. PackManager.makeProjectIterator(projectId, before, (err, iterator) => {
  626. if (err) return consumer(err)
  627. const accumulatedUserIds = new Set()
  628. async.whilst(
  629. () => !iterator.done(),
  630. cb =>
  631. iterator.next((err, updatesFromASinglePack) => {
  632. if (err) return cb(err)
  633. if (updatesFromASinglePack.length === 0) {
  634. // This should not happen when `iterator.done() == false`.
  635. // Emitting an empty array would signal the consumer the final
  636. // call.
  637. return cb()
  638. }
  639. updatesFromASinglePack.forEach(update => {
  640. accumulatedUserIds.add(
  641. // Super defensive access on update details.
  642. String(update && update.meta && update.meta.user_id)
  643. )
  644. })
  645. // Emit updates and wait for the consumer.
  646. consumer(null, { updates: updatesFromASinglePack }, cb)
  647. }),
  648. err => {
  649. if (err) return consumer(err)
  650. // Adding undefined can happen for broken updates.
  651. accumulatedUserIds.delete('undefined')
  652. consumer(null, {
  653. updates: [],
  654. userIds: Array.from(accumulatedUserIds).sort(),
  655. })
  656. }
  657. )
  658. })
  659. })
  660. },
  661. fetchUserInfo(users, callback) {
  662. if (callback == null) {
  663. callback = function () {}
  664. }
  665. const jobs = []
  666. const fetchedUserInfo = {}
  667. for (const user_id in users) {
  668. ;(user_id =>
  669. jobs.push(callback =>
  670. WebApiManager.getUserInfo(user_id, function (error, userInfo) {
  671. if (error != null) {
  672. return callback(error)
  673. }
  674. fetchedUserInfo[user_id] = userInfo
  675. return callback()
  676. })
  677. ))(user_id)
  678. }
  679. return async.series(jobs, function (err) {
  680. if (err != null) {
  681. return callback(err)
  682. }
  683. return callback(null, fetchedUserInfo)
  684. })
  685. },
  686. fillUserInfo(updates, callback) {
  687. let update, user_id
  688. if (callback == null) {
  689. callback = function () {}
  690. }
  691. const users = {}
  692. for (update of Array.from(updates)) {
  693. ;({ user_id } = update.meta)
  694. if (UpdatesManager._validUserId(user_id)) {
  695. users[user_id] = true
  696. }
  697. }
  698. return UpdatesManager.fetchUserInfo(
  699. users,
  700. function (error, fetchedUserInfo) {
  701. if (error != null) {
  702. return callback(error)
  703. }
  704. for (update of Array.from(updates)) {
  705. ;({ user_id } = update.meta)
  706. delete update.meta.user_id
  707. if (UpdatesManager._validUserId(user_id)) {
  708. update.meta.user = fetchedUserInfo[user_id]
  709. }
  710. }
  711. return callback(null, updates)
  712. }
  713. )
  714. },
  715. fillSummarizedUserInfo(updates, callback) {
  716. let update, user_id, user_ids
  717. if (callback == null) {
  718. callback = function () {}
  719. }
  720. const users = {}
  721. for (update of Array.from(updates)) {
  722. user_ids = update.meta.user_ids || []
  723. for (user_id of Array.from(user_ids)) {
  724. if (UpdatesManager._validUserId(user_id)) {
  725. users[user_id] = true
  726. }
  727. }
  728. }
  729. return UpdatesManager.fetchUserInfo(
  730. users,
  731. function (error, fetchedUserInfo) {
  732. if (error != null) {
  733. return callback(error)
  734. }
  735. for (update of Array.from(updates)) {
  736. user_ids = update.meta.user_ids || []
  737. update.meta.users = []
  738. delete update.meta.user_ids
  739. for (user_id of Array.from(user_ids)) {
  740. if (UpdatesManager._validUserId(user_id)) {
  741. update.meta.users.push(fetchedUserInfo[user_id])
  742. } else {
  743. update.meta.users.push(null)
  744. }
  745. }
  746. }
  747. return callback(null, updates)
  748. }
  749. )
  750. },
  751. _validUserId(user_id) {
  752. if (user_id == null) {
  753. return false
  754. } else {
  755. return !!user_id.match(/^[a-f0-9]{24}$/)
  756. }
  757. },
  758. TIME_BETWEEN_DISTINCT_UPDATES: (fiveMinutes = 5 * 60 * 1000),
  759. SPLIT_ON_DELETE_SIZE: 16, // characters
  760. _summarizeUpdates(updates, existingSummarizedUpdates) {
  761. if (existingSummarizedUpdates == null) {
  762. existingSummarizedUpdates = []
  763. }
  764. const summarizedUpdates = existingSummarizedUpdates.slice()
  765. let previousUpdateWasBigDelete = false
  766. for (const update of Array.from(updates)) {
  767. let doc_id
  768. const earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  769. let shouldConcat = false
  770. // If a user inserts some text, then deletes a big chunk including that text,
  771. // the update we show might concat the insert and delete, and there will be no sign
  772. // of that insert having happened, or be able to restore to it (restoring after a big delete is common).
  773. // So, we split the summary on 'big' deletes. However, we've stepping backwards in time with
  774. // most recent changes considered first, so if this update is a big delete, we want to start
  775. // a new summarized update next timge, hence we monitor the previous update.
  776. if (previousUpdateWasBigDelete) {
  777. shouldConcat = false
  778. } else if (
  779. earliestUpdate &&
  780. earliestUpdate.meta.end_ts - update.meta.start_ts <
  781. this.TIME_BETWEEN_DISTINCT_UPDATES
  782. ) {
  783. // We're going backwards in time through the updates, so only combine if this update starts less than 5 minutes before
  784. // the end of current summarized block, so no block spans more than 5 minutes.
  785. shouldConcat = true
  786. }
  787. let isBigDelete = false
  788. for (const op of Array.from(update.op || [])) {
  789. if (op.d != null && op.d.length > this.SPLIT_ON_DELETE_SIZE) {
  790. isBigDelete = true
  791. }
  792. }
  793. previousUpdateWasBigDelete = isBigDelete
  794. if (shouldConcat) {
  795. // check if the user in this update is already present in the earliest update,
  796. // if not, add them to the users list of the earliest update
  797. earliestUpdate.meta.user_ids = _.union(earliestUpdate.meta.user_ids, [
  798. update.meta.user_id,
  799. ])
  800. doc_id = update.doc_id.toString()
  801. const doc = earliestUpdate.docs[doc_id]
  802. if (doc != null) {
  803. doc.fromV = Math.min(doc.fromV, update.v)
  804. doc.toV = Math.max(doc.toV, update.v)
  805. } else {
  806. earliestUpdate.docs[doc_id] = {
  807. fromV: update.v,
  808. toV: update.v,
  809. }
  810. }
  811. earliestUpdate.meta.start_ts = Math.min(
  812. earliestUpdate.meta.start_ts,
  813. update.meta.start_ts
  814. )
  815. earliestUpdate.meta.end_ts = Math.max(
  816. earliestUpdate.meta.end_ts,
  817. update.meta.end_ts
  818. )
  819. } else {
  820. const newUpdate = {
  821. meta: {
  822. user_ids: [],
  823. start_ts: update.meta.start_ts,
  824. end_ts: update.meta.end_ts,
  825. },
  826. docs: {},
  827. }
  828. newUpdate.docs[update.doc_id.toString()] = {
  829. fromV: update.v,
  830. toV: update.v,
  831. }
  832. newUpdate.meta.user_ids.push(update.meta.user_id)
  833. summarizedUpdates.push(newUpdate)
  834. }
  835. }
  836. return summarizedUpdates
  837. },
  838. }
  839. module.exports.promises = {
  840. processUncompressedUpdatesForProject: util.promisify(
  841. UpdatesManager.processUncompressedUpdatesForProject
  842. ),
  843. }
  844. function __guard__(value, transform) {
  845. return typeof value !== 'undefined' && value !== null
  846. ? transform(value)
  847. : undefined
  848. }