DocumentManager.js 19 KB

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