DocumentManager.js 21 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808
  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('logger-sharelatex')
  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.log(
  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.log(
  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.log(
  237. { doc_id, project_id, oldLines, newLines },
  238. 'document is JSON so not updating'
  239. )
  240. return callback(null)
  241. }
  242. logger.log(
  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.log(
  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.log({ 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.log({ 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.log(
  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. function (
  579. error,
  580. lines,
  581. version,
  582. ranges,
  583. pathname,
  584. projectHistoryId
  585. ) {
  586. if (error != null) {
  587. logger.error(
  588. { project_id, doc_id, getDocError: error },
  589. 'resyncing doc contents - error retrieving from web'
  590. )
  591. return callback(error)
  592. }
  593. return ProjectHistoryRedisManager.queueResyncDocContent(
  594. project_id,
  595. projectHistoryId,
  596. doc_id,
  597. lines,
  598. version,
  599. pathname,
  600. callback
  601. )
  602. }
  603. )
  604. } else {
  605. logger.log(
  606. { project_id, doc_id },
  607. 'resyncing doc contents - doc in redis - will queue in redis'
  608. )
  609. return ProjectHistoryRedisManager.queueResyncDocContent(
  610. project_id,
  611. projectHistoryId,
  612. doc_id,
  613. lines,
  614. version,
  615. pathname,
  616. callback
  617. )
  618. }
  619. }
  620. )
  621. },
  622. getDocWithLock(project_id, doc_id, callback) {
  623. if (callback == null) {
  624. callback = function (error, lines, version) {}
  625. }
  626. const UpdateManager = require('./UpdateManager')
  627. return UpdateManager.lockUpdatesAndDo(
  628. DocumentManager.getDoc,
  629. project_id,
  630. doc_id,
  631. callback
  632. )
  633. },
  634. getDocAndRecentOpsWithLock(project_id, doc_id, fromVersion, callback) {
  635. if (callback == null) {
  636. callback = function (
  637. error,
  638. lines,
  639. version,
  640. ops,
  641. ranges,
  642. pathname,
  643. projectHistoryId
  644. ) {}
  645. }
  646. const UpdateManager = require('./UpdateManager')
  647. return UpdateManager.lockUpdatesAndDo(
  648. DocumentManager.getDocAndRecentOps,
  649. project_id,
  650. doc_id,
  651. fromVersion,
  652. callback
  653. )
  654. },
  655. getDocAndFlushIfOldWithLock(project_id, doc_id, callback) {
  656. if (callback == null) {
  657. callback = function (error, doc) {}
  658. }
  659. const UpdateManager = require('./UpdateManager')
  660. return UpdateManager.lockUpdatesAndDo(
  661. DocumentManager.getDocAndFlushIfOld,
  662. project_id,
  663. doc_id,
  664. callback
  665. )
  666. },
  667. setDocWithLock(
  668. project_id,
  669. doc_id,
  670. lines,
  671. source,
  672. user_id,
  673. undoing,
  674. callback
  675. ) {
  676. if (callback == null) {
  677. callback = function (error) {}
  678. }
  679. const UpdateManager = require('./UpdateManager')
  680. return UpdateManager.lockUpdatesAndDo(
  681. DocumentManager.setDoc,
  682. project_id,
  683. doc_id,
  684. lines,
  685. source,
  686. user_id,
  687. undoing,
  688. callback
  689. )
  690. },
  691. flushDocIfLoadedWithLock(project_id, doc_id, callback) {
  692. if (callback == null) {
  693. callback = function (error) {}
  694. }
  695. const UpdateManager = require('./UpdateManager')
  696. return UpdateManager.lockUpdatesAndDo(
  697. DocumentManager.flushDocIfLoaded,
  698. project_id,
  699. doc_id,
  700. callback
  701. )
  702. },
  703. flushAndDeleteDocWithLock(project_id, doc_id, options, callback) {
  704. const UpdateManager = require('./UpdateManager')
  705. return UpdateManager.lockUpdatesAndDo(
  706. DocumentManager.flushAndDeleteDoc,
  707. project_id,
  708. doc_id,
  709. options,
  710. callback
  711. )
  712. },
  713. acceptChangesWithLock(project_id, doc_id, change_ids, callback) {
  714. if (callback == null) {
  715. callback = function (error) {}
  716. }
  717. const UpdateManager = require('./UpdateManager')
  718. return UpdateManager.lockUpdatesAndDo(
  719. DocumentManager.acceptChanges,
  720. project_id,
  721. doc_id,
  722. change_ids,
  723. callback
  724. )
  725. },
  726. deleteCommentWithLock(project_id, doc_id, thread_id, callback) {
  727. if (callback == null) {
  728. callback = function (error) {}
  729. }
  730. const UpdateManager = require('./UpdateManager')
  731. return UpdateManager.lockUpdatesAndDo(
  732. DocumentManager.deleteComment,
  733. project_id,
  734. doc_id,
  735. thread_id,
  736. callback
  737. )
  738. },
  739. renameDocWithLock(
  740. project_id,
  741. doc_id,
  742. user_id,
  743. update,
  744. projectHistoryId,
  745. callback
  746. ) {
  747. if (callback == null) {
  748. callback = function (error) {}
  749. }
  750. const UpdateManager = require('./UpdateManager')
  751. return UpdateManager.lockUpdatesAndDo(
  752. DocumentManager.renameDoc,
  753. project_id,
  754. doc_id,
  755. user_id,
  756. update,
  757. projectHistoryId,
  758. callback
  759. )
  760. },
  761. resyncDocContentsWithLock(project_id, doc_id, callback) {
  762. if (callback == null) {
  763. callback = function (error) {}
  764. }
  765. const UpdateManager = require('./UpdateManager')
  766. return UpdateManager.lockUpdatesAndDo(
  767. DocumentManager.resyncDocContents,
  768. project_id,
  769. doc_id,
  770. callback
  771. )
  772. },
  773. }