WebApiManager.js 2.9 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109
  1. import { callbackify } from 'node:util'
  2. import { setTimeout } from 'node:timers/promises'
  3. import logger from '@overleaf/logger'
  4. import Metrics from '@overleaf/metrics'
  5. import Settings from '@overleaf/settings'
  6. import {
  7. fetchNothing,
  8. fetchJson,
  9. RequestFailedError,
  10. } from '@overleaf/fetch-utils'
  11. import * as Errors from './Errors.js'
  12. import * as RedisManager from './RedisManager.js'
  13. let RETRY_TIMEOUT_MS = 5000
  14. async function getHistoryId(projectId) {
  15. Metrics.inc('history_id_cache_requests_total')
  16. const cachedHistoryId =
  17. await RedisManager.promises.getCachedHistoryId(projectId)
  18. if (cachedHistoryId) {
  19. Metrics.inc('history_id_cache_hits_total')
  20. return cachedHistoryId
  21. } else {
  22. const project = await _getProjectDetails(projectId)
  23. const historyId =
  24. project.overleaf &&
  25. project.overleaf.history &&
  26. project.overleaf.history.id
  27. if (historyId != null) {
  28. await RedisManager.promises.setCachedHistoryId(projectId, historyId)
  29. }
  30. return historyId
  31. }
  32. }
  33. async function requestResync(projectId, opts = {}) {
  34. try {
  35. const body = {}
  36. if (opts.historyRangesMigration) {
  37. body.historyRangesMigration = opts.historyRangesMigration
  38. }
  39. await fetchNothing(
  40. `${Settings.apis.web.url}/project/${projectId}/history/resync`,
  41. {
  42. method: 'POST',
  43. signal: AbortSignal.timeout(6 * 60000),
  44. basicAuth: {
  45. user: Settings.apis.web.user,
  46. password: Settings.apis.web.pass,
  47. },
  48. json: body,
  49. }
  50. )
  51. } catch (err) {
  52. if (err instanceof RequestFailedError && err.response.status === 404) {
  53. throw new Errors.NotFoundError('got a 404 from web api').withCause(err)
  54. } else {
  55. throw err
  56. }
  57. }
  58. }
  59. async function _getProjectDetails(projectId, callback) {
  60. logger.debug({ projectId }, 'getting project details from web')
  61. let attempts = 0
  62. while (true) {
  63. attempts += 1
  64. try {
  65. return await fetchJson(
  66. `${Settings.apis.web.url}/project/${projectId}/details`,
  67. {
  68. signal: AbortSignal.timeout(16000),
  69. basicAuth: {
  70. user: Settings.apis.web.user,
  71. password: Settings.apis.web.pass,
  72. },
  73. }
  74. )
  75. } catch (err) {
  76. if (err instanceof RequestFailedError && err.response.status === 404) {
  77. throw new Errors.NotFoundError('got a 404 from web api').withCause(err)
  78. } else if (attempts < 2) {
  79. // retry after 5 seconds
  80. await setTimeout(RETRY_TIMEOUT_MS)
  81. } else {
  82. throw err
  83. }
  84. }
  85. }
  86. }
  87. /**
  88. * Adjust the retry timeout in tests
  89. */
  90. export async function setRetryTimeoutMs(timeoutMs) {
  91. RETRY_TIMEOUT_MS = timeoutMs
  92. }
  93. // EXPORTS
  94. const getHistoryIdCb = callbackify(getHistoryId)
  95. const requestResyncCb = callbackify(requestResync)
  96. export { getHistoryIdCb as getHistoryId, requestResyncCb as requestResync }
  97. export const promises = {
  98. getHistoryId,
  99. requestResync,
  100. }