DocumentManager.js 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662
  1. let DocumentManager
  2. const RedisManager = require('./RedisManager')
  3. const ProjectHistoryRedisManager = require('./ProjectHistoryRedisManager')
  4. const PersistenceManager = require('./PersistenceManager')
  5. const DiffCodec = require('./DiffCodec')
  6. const logger = require('@overleaf/logger')
  7. const Metrics = require('./Metrics')
  8. const HistoryManager = require('./HistoryManager')
  9. const Errors = require('./Errors')
  10. const RangesManager = require('./RangesManager')
  11. const MAX_UNFLUSHED_AGE = 300 * 1000 // 5 mins, document should be flushed to mongo this time after a change
  12. module.exports = DocumentManager = {
  13. getDoc(projectId, docId, _callback) {
  14. const timer = new Metrics.Timer('docManager.getDoc')
  15. const callback = (...args) => {
  16. timer.done()
  17. _callback(...args)
  18. }
  19. RedisManager.getDoc(
  20. projectId,
  21. docId,
  22. (
  23. error,
  24. lines,
  25. version,
  26. ranges,
  27. pathname,
  28. projectHistoryId,
  29. unflushedTime
  30. ) => {
  31. if (error) {
  32. return callback(error)
  33. }
  34. if (lines == null || version == null) {
  35. logger.debug(
  36. { projectId, docId },
  37. 'doc not in redis so getting from persistence API'
  38. )
  39. PersistenceManager.getDoc(
  40. projectId,
  41. docId,
  42. (
  43. error,
  44. lines,
  45. version,
  46. ranges,
  47. pathname,
  48. projectHistoryId,
  49. projectHistoryType
  50. ) => {
  51. if (error) {
  52. return callback(error)
  53. }
  54. logger.debug(
  55. {
  56. projectId,
  57. docId,
  58. lines,
  59. version,
  60. pathname,
  61. projectHistoryId,
  62. projectHistoryType,
  63. },
  64. 'got doc from persistence API'
  65. )
  66. RedisManager.putDocInMemory(
  67. projectId,
  68. docId,
  69. lines,
  70. version,
  71. ranges,
  72. pathname,
  73. projectHistoryId,
  74. error => {
  75. if (error) {
  76. return callback(error)
  77. }
  78. RedisManager.setHistoryType(
  79. docId,
  80. projectHistoryType,
  81. error => {
  82. if (error) {
  83. return callback(error)
  84. }
  85. callback(
  86. null,
  87. lines,
  88. version,
  89. ranges || {},
  90. pathname,
  91. projectHistoryId,
  92. null,
  93. false
  94. )
  95. }
  96. )
  97. }
  98. )
  99. }
  100. )
  101. } else {
  102. callback(
  103. null,
  104. lines,
  105. version,
  106. ranges,
  107. pathname,
  108. projectHistoryId,
  109. unflushedTime,
  110. true
  111. )
  112. }
  113. }
  114. )
  115. },
  116. getDocAndRecentOps(projectId, docId, fromVersion, _callback) {
  117. const timer = new Metrics.Timer('docManager.getDocAndRecentOps')
  118. const callback = (...args) => {
  119. timer.done()
  120. _callback(...args)
  121. }
  122. DocumentManager.getDoc(
  123. projectId,
  124. docId,
  125. (error, lines, version, ranges, pathname, projectHistoryId) => {
  126. if (error) {
  127. return callback(error)
  128. }
  129. if (fromVersion === -1) {
  130. callback(null, lines, version, [], ranges, pathname, projectHistoryId)
  131. } else {
  132. RedisManager.getPreviousDocOps(
  133. docId,
  134. fromVersion,
  135. version,
  136. (error, ops) => {
  137. if (error) {
  138. return callback(error)
  139. }
  140. callback(
  141. null,
  142. lines,
  143. version,
  144. ops,
  145. ranges,
  146. pathname,
  147. projectHistoryId
  148. )
  149. }
  150. )
  151. }
  152. }
  153. )
  154. },
  155. setDoc(projectId, docId, newLines, source, userId, undoing, _callback) {
  156. const timer = new Metrics.Timer('docManager.setDoc')
  157. const callback = (...args) => {
  158. timer.done()
  159. _callback(...args)
  160. }
  161. if (newLines == null) {
  162. return callback(new Error('No lines were provided to setDoc'))
  163. }
  164. const UpdateManager = require('./UpdateManager')
  165. DocumentManager.getDoc(
  166. projectId,
  167. docId,
  168. (
  169. error,
  170. oldLines,
  171. version,
  172. ranges,
  173. pathname,
  174. projectHistoryId,
  175. unflushedTime,
  176. alreadyLoaded
  177. ) => {
  178. if (error) {
  179. return callback(error)
  180. }
  181. if (
  182. oldLines != null &&
  183. oldLines.length > 0 &&
  184. oldLines[0].text != null
  185. ) {
  186. logger.debug(
  187. { docId, projectId, oldLines, newLines },
  188. 'document is JSON so not updating'
  189. )
  190. return callback(null)
  191. }
  192. logger.debug(
  193. { docId, projectId, oldLines, newLines },
  194. 'setting a document via http'
  195. )
  196. DiffCodec.diffAsShareJsOp(oldLines, newLines, (error, op) => {
  197. if (error) {
  198. return callback(error)
  199. }
  200. if (undoing) {
  201. for (const o of op || []) {
  202. o.u = true
  203. } // Turn on undo flag for each op for track changes
  204. }
  205. const update = {
  206. doc: docId,
  207. op,
  208. v: version,
  209. meta: {
  210. type: 'external',
  211. source,
  212. user_id: userId,
  213. },
  214. }
  215. UpdateManager.applyUpdate(projectId, docId, update, error => {
  216. if (error) {
  217. return callback(error)
  218. }
  219. // If the document was loaded already, then someone has it open
  220. // in a project, and the usual flushing mechanism will happen.
  221. // Otherwise we should remove it immediately since nothing else
  222. // is using it.
  223. if (alreadyLoaded) {
  224. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  225. if (error) {
  226. return callback(error)
  227. }
  228. callback(null)
  229. })
  230. } else {
  231. DocumentManager.flushAndDeleteDoc(projectId, docId, {}, error => {
  232. // There is no harm in flushing project history if the previous
  233. // call failed and sometimes it is required
  234. HistoryManager.flushProjectChangesAsync(projectId)
  235. if (error) {
  236. return callback(error)
  237. }
  238. callback(null)
  239. })
  240. }
  241. })
  242. })
  243. }
  244. )
  245. },
  246. flushDocIfLoaded(projectId, docId, _callback) {
  247. const timer = new Metrics.Timer('docManager.flushDocIfLoaded')
  248. const callback = (...args) => {
  249. timer.done()
  250. _callback(...args)
  251. }
  252. RedisManager.getDoc(
  253. projectId,
  254. docId,
  255. (
  256. error,
  257. lines,
  258. version,
  259. ranges,
  260. pathname,
  261. projectHistoryId,
  262. unflushedTime,
  263. lastUpdatedAt,
  264. lastUpdatedBy
  265. ) => {
  266. if (error) {
  267. return callback(error)
  268. }
  269. if (lines == null || version == null) {
  270. logger.debug(
  271. { projectId, docId },
  272. 'doc is not loaded so not flushing'
  273. )
  274. // TODO: return a flag to bail out, as we go on to remove doc from memory?
  275. callback(null)
  276. } else {
  277. logger.debug({ projectId, docId, version }, 'flushing doc')
  278. PersistenceManager.setDoc(
  279. projectId,
  280. docId,
  281. lines,
  282. version,
  283. ranges,
  284. lastUpdatedAt,
  285. lastUpdatedBy,
  286. error => {
  287. if (error) {
  288. return callback(error)
  289. }
  290. RedisManager.clearUnflushedTime(docId, callback)
  291. }
  292. )
  293. }
  294. }
  295. )
  296. },
  297. flushAndDeleteDoc(projectId, docId, options, _callback) {
  298. const timer = new Metrics.Timer('docManager.flushAndDeleteDoc')
  299. const callback = (...args) => {
  300. timer.done()
  301. _callback(...args)
  302. }
  303. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  304. if (error) {
  305. if (options.ignoreFlushErrors) {
  306. logger.warn(
  307. { projectId, docId, err: error },
  308. 'ignoring flush error while deleting document'
  309. )
  310. } else {
  311. return callback(error)
  312. }
  313. }
  314. // Flush in the background since it requires a http request
  315. HistoryManager.flushDocChangesAsync(projectId, docId)
  316. RedisManager.removeDocFromMemory(projectId, docId, error => {
  317. if (error) {
  318. return callback(error)
  319. }
  320. callback(null)
  321. })
  322. })
  323. },
  324. acceptChanges(projectId, docId, changeIds, _callback) {
  325. if (changeIds == null) {
  326. changeIds = []
  327. }
  328. const timer = new Metrics.Timer('docManager.acceptChanges')
  329. const callback = (...args) => {
  330. timer.done()
  331. _callback(...args)
  332. }
  333. DocumentManager.getDoc(
  334. projectId,
  335. docId,
  336. (error, lines, version, ranges) => {
  337. if (error) {
  338. return callback(error)
  339. }
  340. if (lines == null || version == null) {
  341. return callback(
  342. new Errors.NotFoundError(`document not found: ${docId}`)
  343. )
  344. }
  345. RangesManager.acceptChanges(changeIds, ranges, (error, newRanges) => {
  346. if (error) {
  347. return callback(error)
  348. }
  349. RedisManager.updateDocument(
  350. projectId,
  351. docId,
  352. lines,
  353. version,
  354. [],
  355. newRanges,
  356. {},
  357. error => {
  358. if (error) {
  359. return callback(error)
  360. }
  361. callback()
  362. }
  363. )
  364. })
  365. }
  366. )
  367. },
  368. deleteComment(projectId, docId, commentId, _callback) {
  369. const timer = new Metrics.Timer('docManager.deleteComment')
  370. const callback = (...args) => {
  371. timer.done()
  372. _callback(...args)
  373. }
  374. DocumentManager.getDoc(
  375. projectId,
  376. docId,
  377. (error, lines, version, ranges) => {
  378. if (error) {
  379. return callback(error)
  380. }
  381. if (lines == null || version == null) {
  382. return callback(
  383. new Errors.NotFoundError(`document not found: ${docId}`)
  384. )
  385. }
  386. RangesManager.deleteComment(commentId, ranges, (error, newRanges) => {
  387. if (error) {
  388. return callback(error)
  389. }
  390. RedisManager.updateDocument(
  391. projectId,
  392. docId,
  393. lines,
  394. version,
  395. [],
  396. newRanges,
  397. {},
  398. error => {
  399. if (error) {
  400. return callback(error)
  401. }
  402. callback()
  403. }
  404. )
  405. })
  406. }
  407. )
  408. },
  409. renameDoc(projectId, docId, userId, update, projectHistoryId, _callback) {
  410. const timer = new Metrics.Timer('docManager.updateProject')
  411. const callback = (...args) => {
  412. timer.done()
  413. _callback(...args)
  414. }
  415. RedisManager.renameDoc(
  416. projectId,
  417. docId,
  418. userId,
  419. update,
  420. projectHistoryId,
  421. callback
  422. )
  423. },
  424. getDocAndFlushIfOld(projectId, docId, callback) {
  425. DocumentManager.getDoc(
  426. projectId,
  427. docId,
  428. (
  429. error,
  430. lines,
  431. version,
  432. ranges,
  433. pathname,
  434. projectHistoryId,
  435. unflushedTime,
  436. alreadyLoaded
  437. ) => {
  438. if (error) {
  439. return callback(error)
  440. }
  441. // if doc was already loaded see if it needs to be flushed
  442. if (
  443. alreadyLoaded &&
  444. unflushedTime != null &&
  445. Date.now() - unflushedTime > MAX_UNFLUSHED_AGE
  446. ) {
  447. DocumentManager.flushDocIfLoaded(projectId, docId, error => {
  448. if (error) {
  449. return callback(error)
  450. }
  451. callback(null, lines, version)
  452. })
  453. } else {
  454. callback(null, lines, version)
  455. }
  456. }
  457. )
  458. },
  459. resyncDocContents(projectId, docId, path, callback) {
  460. logger.debug({ projectId, docId, path }, 'start resyncing doc contents')
  461. RedisManager.getDoc(
  462. projectId,
  463. docId,
  464. (error, lines, version, ranges, pathname, projectHistoryId) => {
  465. if (error) {
  466. return callback(error)
  467. }
  468. // To avoid issues where the same docId appears with different paths,
  469. // we use the path from the resyncProjectStructure update. If we used
  470. // the path from the getDoc call to web then the two occurences of the
  471. // docId would map to the same path, and this would be rejected by
  472. // project-history as an unexpected resyncDocContent update.
  473. if (lines == null || version == null) {
  474. logger.debug(
  475. { projectId, docId },
  476. 'resyncing doc contents - not found in redis - retrieving from web'
  477. )
  478. PersistenceManager.getDoc(
  479. projectId,
  480. docId,
  481. { peek: true },
  482. (error, lines, version, ranges, pathname, projectHistoryId) => {
  483. if (error) {
  484. logger.error(
  485. { projectId, docId, getDocError: error },
  486. 'resyncing doc contents - error retrieving from web'
  487. )
  488. return callback(error)
  489. }
  490. ProjectHistoryRedisManager.queueResyncDocContent(
  491. projectId,
  492. projectHistoryId,
  493. docId,
  494. lines,
  495. version,
  496. path, // use the path from the resyncProjectStructure update
  497. callback
  498. )
  499. }
  500. )
  501. } else {
  502. logger.debug(
  503. { projectId, docId },
  504. 'resyncing doc contents - doc in redis - will queue in redis'
  505. )
  506. ProjectHistoryRedisManager.queueResyncDocContent(
  507. projectId,
  508. projectHistoryId,
  509. docId,
  510. lines,
  511. version,
  512. path, // use the path from the resyncProjectStructure update
  513. callback
  514. )
  515. }
  516. }
  517. )
  518. },
  519. getDocWithLock(projectId, docId, callback) {
  520. const UpdateManager = require('./UpdateManager')
  521. UpdateManager.lockUpdatesAndDo(
  522. DocumentManager.getDoc,
  523. projectId,
  524. docId,
  525. callback
  526. )
  527. },
  528. getDocAndRecentOpsWithLock(projectId, docId, fromVersion, callback) {
  529. const UpdateManager = require('./UpdateManager')
  530. UpdateManager.lockUpdatesAndDo(
  531. DocumentManager.getDocAndRecentOps,
  532. projectId,
  533. docId,
  534. fromVersion,
  535. callback
  536. )
  537. },
  538. getDocAndFlushIfOldWithLock(projectId, docId, callback) {
  539. const UpdateManager = require('./UpdateManager')
  540. UpdateManager.lockUpdatesAndDo(
  541. DocumentManager.getDocAndFlushIfOld,
  542. projectId,
  543. docId,
  544. callback
  545. )
  546. },
  547. setDocWithLock(projectId, docId, lines, source, userId, undoing, callback) {
  548. const UpdateManager = require('./UpdateManager')
  549. UpdateManager.lockUpdatesAndDo(
  550. DocumentManager.setDoc,
  551. projectId,
  552. docId,
  553. lines,
  554. source,
  555. userId,
  556. undoing,
  557. callback
  558. )
  559. },
  560. flushDocIfLoadedWithLock(projectId, docId, callback) {
  561. const UpdateManager = require('./UpdateManager')
  562. UpdateManager.lockUpdatesAndDo(
  563. DocumentManager.flushDocIfLoaded,
  564. projectId,
  565. docId,
  566. callback
  567. )
  568. },
  569. flushAndDeleteDocWithLock(projectId, docId, options, callback) {
  570. const UpdateManager = require('./UpdateManager')
  571. UpdateManager.lockUpdatesAndDo(
  572. DocumentManager.flushAndDeleteDoc,
  573. projectId,
  574. docId,
  575. options,
  576. callback
  577. )
  578. },
  579. acceptChangesWithLock(projectId, docId, changeIds, callback) {
  580. const UpdateManager = require('./UpdateManager')
  581. UpdateManager.lockUpdatesAndDo(
  582. DocumentManager.acceptChanges,
  583. projectId,
  584. docId,
  585. changeIds,
  586. callback
  587. )
  588. },
  589. deleteCommentWithLock(projectId, docId, threadId, callback) {
  590. const UpdateManager = require('./UpdateManager')
  591. UpdateManager.lockUpdatesAndDo(
  592. DocumentManager.deleteComment,
  593. projectId,
  594. docId,
  595. threadId,
  596. callback
  597. )
  598. },
  599. renameDocWithLock(
  600. projectId,
  601. docId,
  602. userId,
  603. update,
  604. projectHistoryId,
  605. callback
  606. ) {
  607. const UpdateManager = require('./UpdateManager')
  608. UpdateManager.lockUpdatesAndDo(
  609. DocumentManager.renameDoc,
  610. projectId,
  611. docId,
  612. userId,
  613. update,
  614. projectHistoryId,
  615. callback
  616. )
  617. },
  618. resyncDocContentsWithLock(projectId, docId, path, callback) {
  619. const UpdateManager = require('./UpdateManager')
  620. UpdateManager.lockUpdatesAndDo(
  621. DocumentManager.resyncDocContents,
  622. projectId,
  623. docId,
  624. path,
  625. callback
  626. )
  627. },
  628. }