DocumentManager.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809
  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. * DS207: Consider shorter variations of null checks
  13. * Full docs: https://github.com/decaffeinate/decaffeinate/blob/master/docs/suggestions.md
  14. */
  15. let DocumentManager
  16. const RedisManager = require('./RedisManager')
  17. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  18. const PersistenceManager = require('./PersistenceManager')
  19. const DiffCodec = require('./DiffCodec')
  20. const logger = require('@overleaf/logger')
  21. const Metrics = require('./Metrics')
  22. const HistoryManager = require('./HistoryManager')
  23. const RealTimeRedisManager = require('./RealTimeRedisManager')
  24. const Errors = require('./Errors')
  25. const RangesManager = require('./RangesManager')
  26. const async = require('async')
  27. const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change
  28. module.exports = DocumentManager = {
  29. getDoc(project_id, doc_id, _callback) {
  30. if (_callback == null) {
  31. _callback = function (
  32. error,
  33. lines,
  34. version,
  35. ranges,
  36. pathname,
  37. projectHistoryId,
  38. unflushedTime,
  39. alreadyLoaded
  40. ) {}
  41. }
  42. const timer = new Metrics.Timer('docManager.getDoc')
  43. const callback = function (...args) {
  44. timer.done()
  45. return _callback(...Array.from(args || []))
  46. }
  47. return RedisManager.getDoc(
  48. project_id,
  49. doc_id,
  50. function (
  51. error,
  52. lines,
  53. version,
  54. ranges,
  55. pathname,
  56. projectHistoryId,
  57. unflushedTime
  58. ) {
  59. if (error != null) {
  60. return callback(error)
  61. }
  62. if (lines == null || version == null) {
  63. logger.debug(
  64. { project_id, doc_id },
  65. 'doc not in redis so getting from persistence API'
  66. )
  67. return PersistenceManager.getDoc(
  68. project_id,
  69. doc_id,
  70. function (
  71. error,
  72. lines,
  73. version,
  74. ranges,
  75. pathname,
  76. projectHistoryId,
  77. projectHistoryType
  78. ) {
  79. if (error != null) {
  80. return callback(error)
  81. }
  82. logger.debug(
  83. {
  84. project_id,
  85. doc_id,
  86. lines,
  87. version,
  88. pathname,
  89. projectHistoryId,
  90. projectHistoryType,
  91. },
  92. 'got doc from persistence API'
  93. )
  94. return RedisManager.putDocInMemory(
  95. project_id,
  96. doc_id,
  97. lines,
  98. version,
  99. ranges,
  100. pathname,
  101. projectHistoryId,
  102. function (error) {
  103. if (error != null) {
  104. return callback(error)
  105. }
  106. return RedisManager.setHistoryType(
  107. doc_id,
  108. projectHistoryType,
  109. function (error) {
  110. if (error != null) {
  111. return callback(error)
  112. }
  113. return callback(
  114. null,
  115. lines,
  116. version,
  117. ranges || {},
  118. pathname,
  119. projectHistoryId,
  120. null,
  121. false
  122. )
  123. }
  124. )
  125. }
  126. )
  127. }
  128. )
  129. } else {
  130. return callback(
  131. null,
  132. lines,
  133. version,
  134. ranges,
  135. pathname,
  136. projectHistoryId,
  137. unflushedTime,
  138. true
  139. )
  140. }
  141. }
  142. )
  143. },
  144. getDocAndRecentOps(project_id, doc_id, fromVersion, _callback) {
  145. if (_callback == null) {
  146. _callback = function (
  147. error,
  148. lines,
  149. version,
  150. ops,
  151. ranges,
  152. pathname,
  153. projectHistoryId
  154. ) {}
  155. }
  156. const timer = new Metrics.Timer('docManager.getDocAndRecentOps')
  157. const callback = function (...args) {
  158. timer.done()
  159. return _callback(...Array.from(args || []))
  160. }
  161. return DocumentManager.getDoc(
  162. project_id,
  163. doc_id,
  164. function (error, lines, version, ranges, pathname, projectHistoryId) {
  165. if (error != null) {
  166. return callback(error)
  167. }
  168. if (fromVersion === -1) {
  169. return callback(
  170. null,
  171. lines,
  172. version,
  173. [],
  174. ranges,
  175. pathname,
  176. projectHistoryId
  177. )
  178. } else {
  179. return RedisManager.getPreviousDocOps(
  180. doc_id,
  181. fromVersion,
  182. version,
  183. function (error, ops) {
  184. if (error != null) {
  185. return callback(error)
  186. }
  187. return callback(
  188. null,
  189. lines,
  190. version,
  191. ops,
  192. ranges,
  193. pathname,
  194. projectHistoryId
  195. )
  196. }
  197. )
  198. }
  199. }
  200. )
  201. },
  202. setDoc(project_id, doc_id, newLines, source, user_id, undoing, _callback) {
  203. if (_callback == null) {
  204. _callback = function (error) {}
  205. }
  206. const timer = new Metrics.Timer('docManager.setDoc')
  207. const callback = function (...args) {
  208. timer.done()
  209. return _callback(...Array.from(args || []))
  210. }
  211. if (newLines == null) {
  212. return callback(new Error('No lines were provided to setDoc'))
  213. }
  214. const UpdateManager = require('./UpdateManager')
  215. return DocumentManager.getDoc(
  216. project_id,
  217. doc_id,
  218. function (
  219. error,
  220. oldLines,
  221. version,
  222. ranges,
  223. pathname,
  224. projectHistoryId,
  225. unflushedTime,
  226. alreadyLoaded
  227. ) {
  228. if (error != null) {
  229. return callback(error)
  230. }
  231. if (
  232. oldLines != null &&
  233. oldLines.length > 0 &&
  234. oldLines[0].text != null
  235. ) {
  236. logger.debug(
  237. { doc_id, project_id, oldLines, newLines },
  238. 'document is JSON so not updating'
  239. )
  240. return callback(null)
  241. }
  242. logger.debug(
  243. { doc_id, project_id, oldLines, newLines },
  244. 'setting a document via http'
  245. )
  246. return DiffCodec.diffAsShareJsOp(
  247. oldLines,
  248. newLines,
  249. function (error, op) {
  250. if (error != null) {
  251. return callback(error)
  252. }
  253. if (undoing) {
  254. for (const o of Array.from(op || [])) {
  255. o.u = true
  256. } // Turn on undo flag for each op for track changes
  257. }
  258. const update = {
  259. doc: doc_id,
  260. op,
  261. v: version,
  262. meta: {
  263. type: 'external',
  264. source,
  265. user_id,
  266. },
  267. }
  268. return UpdateManager.applyUpdate(
  269. project_id,
  270. doc_id,
  271. update,
  272. function (error) {
  273. if (error != null) {
  274. return callback(error)
  275. }
  276. // If the document was loaded already, then someone has it open
  277. // in a project, and the usual flushing mechanism will happen.
  278. // Otherwise we should remove it immediately since nothing else
  279. // is using it.
  280. if (alreadyLoaded) {
  281. return DocumentManager.flushDocIfLoaded(
  282. project_id,
  283. doc_id,
  284. function (error) {
  285. if (error != null) {
  286. return callback(error)
  287. }
  288. return callback(null)
  289. }
  290. )
  291. } else {
  292. return DocumentManager.flushAndDeleteDoc(
  293. project_id,
  294. doc_id,
  295. {},
  296. function (error) {
  297. // There is no harm in flushing project history if the previous
  298. // call failed and sometimes it is required
  299. HistoryManager.flushProjectChangesAsync(project_id)
  300. if (error != null) {
  301. return callback(error)
  302. }
  303. return callback(null)
  304. }
  305. )
  306. }
  307. }
  308. )
  309. }
  310. )
  311. }
  312. )
  313. },
  314. flushDocIfLoaded(project_id, doc_id, _callback) {
  315. if (_callback == null) {
  316. _callback = function (error) {}
  317. }
  318. const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
  319. const callback = function (...args) {
  320. timer.done()
  321. return _callback(...Array.from(args || []))
  322. }
  323. return RedisManager.getDoc(
  324. project_id,
  325. doc_id,
  326. function (
  327. error,
  328. lines,
  329. version,
  330. ranges,
  331. pathname,
  332. projectHistoryId,
  333. unflushedTime,
  334. lastUpdatedAt,
  335. lastUpdatedBy
  336. ) {
  337. if (error != null) {
  338. return callback(error)
  339. }
  340. if (lines == null || version == null) {
  341. logger.debug(
  342. { project_id, doc_id },
  343. 'doc is not loaded so not flushing'
  344. )
  345. return callback(null) // TODO: return a flag to bail out, as we go on to remove doc from memory?
  346. } else {
  347. logger.debug({ project_id, doc_id, version }, 'flushing doc')
  348. return PersistenceManager.setDoc(
  349. project_id,
  350. doc_id,
  351. lines,
  352. version,
  353. ranges,
  354. lastUpdatedAt,
  355. lastUpdatedBy,
  356. function (error) {
  357. if (error != null) {
  358. return callback(error)
  359. }
  360. return RedisManager.clearUnflushedTime(doc_id, callback)
  361. }
  362. )
  363. }
  364. }
  365. )
  366. },
  367. flushAndDeleteDoc(project_id, doc_id, options, _callback) {
  368. const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
  369. const callback = function (...args) {
  370. timer.done()
  371. return _callback(...Array.from(args || []))
  372. }
  373. return DocumentManager.flushDocIfLoaded(
  374. project_id,
  375. doc_id,
  376. function (error) {
  377. if (error != null) {
  378. if (options.ignoreFlushErrors) {
  379. logger.warn(
  380. { project_id, doc_id, err: error },
  381. 'ignoring flush error while deleting document'
  382. )
  383. } else {
  384. return callback(error)
  385. }
  386. }
  387. // Flush in the background since it requires a http request
  388. HistoryManager.flushDocChangesAsync(project_id, doc_id)
  389. return RedisManager.removeDocFromMemory(
  390. project_id,
  391. doc_id,
  392. function (error) {
  393. if (error != null) {
  394. return callback(error)
  395. }
  396. return callback(null)
  397. }
  398. )
  399. }
  400. )
  401. },
  402. acceptChanges(project_id, doc_id, change_ids, _callback) {
  403. if (change_ids == null) {
  404. change_ids = []
  405. }
  406. if (_callback == null) {
  407. _callback = function (error) {}
  408. }
  409. const timer = new Metrics.Timer('docManager.acceptChanges')
  410. const callback = function (...args) {
  411. timer.done()
  412. return _callback(...Array.from(args || []))
  413. }
  414. return DocumentManager.getDoc(
  415. project_id,
  416. doc_id,
  417. function (error, lines, version, ranges) {
  418. if (error != null) {
  419. return callback(error)
  420. }
  421. if (lines == null || version == null) {
  422. return callback(
  423. new Errors.NotFoundError(`document not found: ${doc_id}`)
  424. )
  425. }
  426. return RangesManager.acceptChanges(
  427. change_ids,
  428. ranges,
  429. function (error, new_ranges) {
  430. if (error != null) {
  431. return callback(error)
  432. }
  433. return RedisManager.updateDocument(
  434. project_id,
  435. doc_id,
  436. lines,
  437. version,
  438. [],
  439. new_ranges,
  440. {},
  441. function (error) {
  442. if (error != null) {
  443. return callback(error)
  444. }
  445. return callback()
  446. }
  447. )
  448. }
  449. )
  450. }
  451. )
  452. },
  453. deleteComment(project_id, doc_id, comment_id, _callback) {
  454. if (_callback == null) {
  455. _callback = function (error) {}
  456. }
  457. const timer = new Metrics.Timer('docManager.deleteComment')
  458. const callback = function (...args) {
  459. timer.done()
  460. return _callback(...Array.from(args || []))
  461. }
  462. return DocumentManager.getDoc(
  463. project_id,
  464. doc_id,
  465. function (error, lines, version, ranges) {
  466. if (error != null) {
  467. return callback(error)
  468. }
  469. if (lines == null || version == null) {
  470. return callback(
  471. new Errors.NotFoundError(`document not found: ${doc_id}`)
  472. )
  473. }
  474. return RangesManager.deleteComment(
  475. comment_id,
  476. ranges,
  477. function (error, new_ranges) {
  478. if (error != null) {
  479. return callback(error)
  480. }
  481. return RedisManager.updateDocument(
  482. project_id,
  483. doc_id,
  484. lines,
  485. version,
  486. [],
  487. new_ranges,
  488. {},
  489. function (error) {
  490. if (error != null) {
  491. return callback(error)
  492. }
  493. return callback()
  494. }
  495. )
  496. }
  497. )
  498. }
  499. )
  500. },
  501. renameDoc(project_id, doc_id, user_id, update, projectHistoryId, _callback) {
  502. if (_callback == null) {
  503. _callback = function (error) {}
  504. }
  505. const timer = new Metrics.Timer('docManager.updateProject')
  506. const callback = function (...args) {
  507. timer.done()
  508. return _callback(...Array.from(args || []))
  509. }
  510. return RedisManager.renameDoc(
  511. project_id,
  512. doc_id,
  513. user_id,
  514. update,
  515. projectHistoryId,
  516. callback
  517. )
  518. },
  519. getDocAndFlushIfOld(project_id, doc_id, callback) {
  520. if (callback == null) {
  521. callback = function (error, doc) {}
  522. }
  523. return DocumentManager.getDoc(
  524. project_id,
  525. doc_id,
  526. function (
  527. error,
  528. lines,
  529. version,
  530. ranges,
  531. pathname,
  532. projectHistoryId,
  533. unflushedTime,
  534. alreadyLoaded
  535. ) {
  536. if (error != null) {
  537. return callback(error)
  538. }
  539. // if doc was already loaded see if it needs to be flushed
  540. if (
  541. alreadyLoaded &&
  542. unflushedTime != null &&
  543. Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
  544. ) {
  545. return DocumentManager.flushDocIfLoaded(
  546. project_id,
  547. doc_id,
  548. function (error) {
  549. if (error != null) {
  550. return callback(error)
  551. }
  552. return callback(null, lines, version)
  553. }
  554. )
  555. } else {
  556. return callback(null, lines, version)
  557. }
  558. }
  559. )
  560. },
  561. resyncDocContents(project_id, doc_id, callback) {
  562. logger.debug({ project_id, doc_id }, 'start resyncing doc contents')
  563. return RedisManager.getDoc(
  564. project_id,
  565. doc_id,
  566. function (error, lines, version, ranges, pathname, projectHistoryId) {
  567. if (error != null) {
  568. return callback(error)
  569. }
  570. if (lines == null || version == null) {
  571. logger.debug(
  572. { project_id, doc_id },
  573. 'resyncing doc contents - not found in redis - retrieving from web'
  574. )
  575. return PersistenceManager.getDoc(
  576. project_id,
  577. doc_id,
  578. { peek: true },
  579. function (
  580. error,
  581. lines,
  582. version,
  583. ranges,
  584. pathname,
  585. projectHistoryId
  586. ) {
  587. if (error != null) {
  588. logger.error(
  589. { project_id, doc_id, getDocError: error },
  590. 'resyncing doc contents - error retrieving from web'
  591. )
  592. return callback(error)
  593. }
  594. return ProjectHistoryRedisManager.queueResyncDocContent(
  595. project_id,
  596. projectHistoryId,
  597. doc_id,
  598. lines,
  599. version,
  600. pathname,
  601. callback
  602. )
  603. }
  604. )
  605. } else {
  606. logger.debug(
  607. { project_id, doc_id },
  608. 'resyncing doc contents - doc in redis - will queue in redis'
  609. )
  610. return ProjectHistoryRedisManager.queueResyncDocContent(
  611. project_id,
  612. projectHistoryId,
  613. doc_id,
  614. lines,
  615. version,
  616. pathname,
  617. callback
  618. )
  619. }
  620. }
  621. )
  622. },
  623. getDocWithLock(project_id, doc_id, callback) {
  624. if (callback == null) {
  625. callback = function (error, lines, version) {}
  626. }
  627. const UpdateManager = require('./UpdateManager')
  628. return UpdateManager.lockUpdatesAndDo(
  629. DocumentManager.getDoc,
  630. project_id,
  631. doc_id,
  632. callback
  633. )
  634. },
  635. getDocAndRecentOpsWithLock(project_id, doc_id, fromVersion, callback) {
  636. if (callback == null) {
  637. callback = function (
  638. error,
  639. lines,
  640. version,
  641. ops,
  642. ranges,
  643. pathname,
  644. projectHistoryId
  645. ) {}
  646. }
  647. const UpdateManager = require('./UpdateManager')
  648. return UpdateManager.lockUpdatesAndDo(
  649. DocumentManager.getDocAndRecentOps,
  650. project_id,
  651. doc_id,
  652. fromVersion,
  653. callback
  654. )
  655. },
  656. getDocAndFlushIfOldWithLock(project_id, doc_id, callback) {
  657. if (callback == null) {
  658. callback = function (error, doc) {}
  659. }
  660. const UpdateManager = require('./UpdateManager')
  661. return UpdateManager.lockUpdatesAndDo(
  662. DocumentManager.getDocAndFlushIfOld,
  663. project_id,
  664. doc_id,
  665. callback
  666. )
  667. },
  668. setDocWithLock(
  669. project_id,
  670. doc_id,
  671. lines,
  672. source,
  673. user_id,
  674. undoing,
  675. callback
  676. ) {
  677. if (callback == null) {
  678. callback = function (error) {}
  679. }
  680. const UpdateManager = require('./UpdateManager')
  681. return UpdateManager.lockUpdatesAndDo(
  682. DocumentManager.setDoc,
  683. project_id,
  684. doc_id,
  685. lines,
  686. source,
  687. user_id,
  688. undoing,
  689. callback
  690. )
  691. },
  692. flushDocIfLoadedWithLock(project_id, doc_id, callback) {
  693. if (callback == null) {
  694. callback = function (error) {}
  695. }
  696. const UpdateManager = require('./UpdateManager')
  697. return UpdateManager.lockUpdatesAndDo(
  698. DocumentManager.flushDocIfLoaded,
  699. project_id,
  700. doc_id,
  701. callback
  702. )
  703. },
  704. flushAndDeleteDocWithLock(project_id, doc_id, options, callback) {
  705. const UpdateManager = require('./UpdateManager')
  706. return UpdateManager.lockUpdatesAndDo(
  707. DocumentManager.flushAndDeleteDoc,
  708. project_id,
  709. doc_id,
  710. options,
  711. callback
  712. )
  713. },
  714. acceptChangesWithLock(project_id, doc_id, change_ids, callback) {
  715. if (callback == null) {
  716. callback = function (error) {}
  717. }
  718. const UpdateManager = require('./UpdateManager')
  719. return UpdateManager.lockUpdatesAndDo(
  720. DocumentManager.acceptChanges,
  721. project_id,
  722. doc_id,
  723. change_ids,
  724. callback
  725. )
  726. },
  727. deleteCommentWithLock(project_id, doc_id, thread_id, callback) {
  728. if (callback == null) {
  729. callback = function (error) {}
  730. }
  731. const UpdateManager = require('./UpdateManager')
  732. return UpdateManager.lockUpdatesAndDo(
  733. DocumentManager.deleteComment,
  734. project_id,
  735. doc_id,
  736. thread_id,
  737. callback
  738. )
  739. },
  740. renameDocWithLock(
  741. project_id,
  742. doc_id,
  743. user_id,
  744. update,
  745. projectHistoryId,
  746. callback
  747. ) {
  748. if (callback == null) {
  749. callback = function (error) {}
  750. }
  751. const UpdateManager = require('./UpdateManager')
  752. return UpdateManager.lockUpdatesAndDo(
  753. DocumentManager.renameDoc,
  754. project_id,
  755. doc_id,
  756. user_id,
  757. update,
  758. projectHistoryId,
  759. callback
  760. )
  761. },
  762. resyncDocContentsWithLock(project_id, doc_id, callback) {
  763. if (callback == null) {
  764. callback = function (error) {}
  765. }
  766. const UpdateManager = require('./UpdateManager')
  767. return UpdateManager.lockUpdatesAndDo(
  768. DocumentManager.resyncDocContents,
  769. project_id,
  770. doc_id,
  771. callback
  772. )
  773. },
  774. }