UpdatesManager.js 26 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883
  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. exportProject(projectId, consumer) {
  613. // Flush anything before collecting updates.
  614. UpdatesManager.processUncompressedUpdatesForProject(projectId, (err) => {
  615. if (err) return consumer(err)
  616. // Fetch all the packs.
  617. const before = undefined
  618. PackManager.makeProjectIterator(projectId, before, (err, iterator) => {
  619. if (err) return consumer(err)
  620. const accumulatedUserIds = new Set()
  621. async.whilst(
  622. () => !iterator.done(),
  623. (cb) =>
  624. iterator.next((err, updatesFromASinglePack) => {
  625. if (err) return cb(err)
  626. if (updatesFromASinglePack.length === 0) {
  627. // This should not happen when `iterator.done() == false`.
  628. // Emitting an empty array would signal the consumer the final
  629. // call.
  630. return cb()
  631. }
  632. updatesFromASinglePack.forEach((update) => {
  633. accumulatedUserIds.add(
  634. // Super defensive access on update details.
  635. String(update && update.meta && update.meta.user_id)
  636. )
  637. })
  638. // Emit updates and wait for the consumer.
  639. consumer(null, { updates: updatesFromASinglePack }, cb)
  640. }),
  641. (err) => {
  642. if (err) return consumer(err)
  643. // Adding undefined can happen for broken updates.
  644. accumulatedUserIds.delete('undefined')
  645. consumer(null, {
  646. updates: [],
  647. userIds: Array.from(accumulatedUserIds).sort()
  648. })
  649. }
  650. )
  651. })
  652. })
  653. },
  654. fetchUserInfo(users, callback) {
  655. if (callback == null) {
  656. callback = function (error, fetchedUserInfo) {}
  657. }
  658. const jobs = []
  659. const fetchedUserInfo = {}
  660. for (const user_id in users) {
  661. ;((user_id) =>
  662. jobs.push((callback) =>
  663. WebApiManager.getUserInfo(user_id, function (error, userInfo) {
  664. if (error != null) {
  665. return callback(error)
  666. }
  667. fetchedUserInfo[user_id] = userInfo
  668. return callback()
  669. })
  670. ))(user_id)
  671. }
  672. return async.series(jobs, function (err) {
  673. if (err != null) {
  674. return callback(err)
  675. }
  676. return callback(null, fetchedUserInfo)
  677. })
  678. },
  679. fillUserInfo(updates, callback) {
  680. let update, user_id
  681. if (callback == null) {
  682. callback = function (error, updates) {}
  683. }
  684. const users = {}
  685. for (update of Array.from(updates)) {
  686. ;({ user_id } = update.meta)
  687. if (UpdatesManager._validUserId(user_id)) {
  688. users[user_id] = true
  689. }
  690. }
  691. return UpdatesManager.fetchUserInfo(users, function (
  692. error,
  693. fetchedUserInfo
  694. ) {
  695. if (error != null) {
  696. return callback(error)
  697. }
  698. for (update of Array.from(updates)) {
  699. ;({ user_id } = update.meta)
  700. delete update.meta.user_id
  701. if (UpdatesManager._validUserId(user_id)) {
  702. update.meta.user = fetchedUserInfo[user_id]
  703. }
  704. }
  705. return callback(null, updates)
  706. })
  707. },
  708. fillSummarizedUserInfo(updates, callback) {
  709. let update, user_id, user_ids
  710. if (callback == null) {
  711. callback = function (error, updates) {}
  712. }
  713. const users = {}
  714. for (update of Array.from(updates)) {
  715. user_ids = update.meta.user_ids || []
  716. for (user_id of Array.from(user_ids)) {
  717. if (UpdatesManager._validUserId(user_id)) {
  718. users[user_id] = true
  719. }
  720. }
  721. }
  722. return UpdatesManager.fetchUserInfo(users, function (
  723. error,
  724. fetchedUserInfo
  725. ) {
  726. if (error != null) {
  727. return callback(error)
  728. }
  729. for (update of Array.from(updates)) {
  730. user_ids = update.meta.user_ids || []
  731. update.meta.users = []
  732. delete update.meta.user_ids
  733. for (user_id of Array.from(user_ids)) {
  734. if (UpdatesManager._validUserId(user_id)) {
  735. update.meta.users.push(fetchedUserInfo[user_id])
  736. } else {
  737. update.meta.users.push(null)
  738. }
  739. }
  740. }
  741. return callback(null, updates)
  742. })
  743. },
  744. _validUserId(user_id) {
  745. if (user_id == null) {
  746. return false
  747. } else {
  748. return !!user_id.match(/^[a-f0-9]{24}$/)
  749. }
  750. },
  751. TIME_BETWEEN_DISTINCT_UPDATES: (fiveMinutes = 5 * 60 * 1000),
  752. SPLIT_ON_DELETE_SIZE: 16, // characters
  753. _summarizeUpdates(updates, existingSummarizedUpdates) {
  754. if (existingSummarizedUpdates == null) {
  755. existingSummarizedUpdates = []
  756. }
  757. const summarizedUpdates = existingSummarizedUpdates.slice()
  758. let previousUpdateWasBigDelete = false
  759. for (const update of Array.from(updates)) {
  760. var doc_id
  761. const earliestUpdate = summarizedUpdates[summarizedUpdates.length - 1]
  762. let shouldConcat = false
  763. // If a user inserts some text, then deletes a big chunk including that text,
  764. // the update we show might concat the insert and delete, and there will be no sign
  765. // of that insert having happened, or be able to restore to it (restoring after a big delete is common).
  766. // So, we split the summary on 'big' deletes. However, we've stepping backwards in time with
  767. // most recent changes considered first, so if this update is a big delete, we want to start
  768. // a new summarized update next timge, hence we monitor the previous update.
  769. if (previousUpdateWasBigDelete) {
  770. shouldConcat = false
  771. } else if (
  772. earliestUpdate &&
  773. earliestUpdate.meta.end_ts - update.meta.start_ts <
  774. this.TIME_BETWEEN_DISTINCT_UPDATES
  775. ) {
  776. // We're going backwards in time through the updates, so only combine if this update starts less than 5 minutes before
  777. // the end of current summarized block, so no block spans more than 5 minutes.
  778. shouldConcat = true
  779. }
  780. let isBigDelete = false
  781. for (const op of Array.from(update.op || [])) {
  782. if (op.d != null && op.d.length > this.SPLIT_ON_DELETE_SIZE) {
  783. isBigDelete = true
  784. }
  785. }
  786. previousUpdateWasBigDelete = isBigDelete
  787. if (shouldConcat) {
  788. // check if the user in this update is already present in the earliest update,
  789. // if not, add them to the users list of the earliest update
  790. earliestUpdate.meta.user_ids = _.union(earliestUpdate.meta.user_ids, [
  791. update.meta.user_id
  792. ])
  793. doc_id = update.doc_id.toString()
  794. const doc = earliestUpdate.docs[doc_id]
  795. if (doc != null) {
  796. doc.fromV = Math.min(doc.fromV, update.v)
  797. doc.toV = Math.max(doc.toV, update.v)
  798. } else {
  799. earliestUpdate.docs[doc_id] = {
  800. fromV: update.v,
  801. toV: update.v
  802. }
  803. }
  804. earliestUpdate.meta.start_ts = Math.min(
  805. earliestUpdate.meta.start_ts,
  806. update.meta.start_ts
  807. )
  808. earliestUpdate.meta.end_ts = Math.max(
  809. earliestUpdate.meta.end_ts,
  810. update.meta.end_ts
  811. )
  812. } else {
  813. const newUpdate = {
  814. meta: {
  815. user_ids: [],
  816. start_ts: update.meta.start_ts,
  817. end_ts: update.meta.end_ts
  818. },
  819. docs: {}
  820. }
  821. newUpdate.docs[update.doc_id.toString()] = {
  822. fromV: update.v,
  823. toV: update.v
  824. }
  825. newUpdate.meta.user_ids.push(update.meta.user_id)
  826. summarizedUpdates.push(newUpdate)
  827. }
  828. }
  829. return summarizedUpdates
  830. }
  831. }
  832. function __guard__(value, transform) {
  833. return typeof value !== 'undefined' && value !== null
  834. ? transform(value)
  835. : undefined
  836. }