compiler.js 5.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. import { isMainFile } from './editor-files'
  2. import getMeta from '../../../utils/meta'
  3. import { sendMBSampled } from '../../../infrastructure/event-tracking'
  4. import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
  5. import { debounce } from 'lodash'
  6. import { trackPdfDownload } from '../../../ide/pdf/controllers/PdfJsMetrics'
  7. const AUTO_COMPILE_MAX_WAIT = 5000
  8. // We add a 1 second debounce to sending user changes to server if they aren't
  9. // collaborating with anyone. This needs to be higher than that, and allow for
  10. // client to server latency, otherwise we compile before the op reaches the server
  11. // and then again on ack.
  12. const AUTO_COMPILE_DEBOUNCE = 2000
  13. const searchParams = new URLSearchParams(window.location.search)
  14. export default class DocumentCompiler {
  15. constructor({
  16. project,
  17. setChangedAt,
  18. setCompiling,
  19. setData,
  20. setFirstRenderDone,
  21. setError,
  22. signal,
  23. }) {
  24. this.project = project
  25. this.setChangedAt = setChangedAt
  26. this.setCompiling = setCompiling
  27. this.setData = setData
  28. this.setFirstRenderDone = setFirstRenderDone
  29. this.setError = setError
  30. this.signal = signal
  31. this.clsiServerId = null
  32. this.currentDoc = null
  33. this.error = undefined
  34. this.timer = 0
  35. this.debouncedAutoCompile = debounce(
  36. () => {
  37. this.compile({ isAutoCompileOnChange: true })
  38. },
  39. AUTO_COMPILE_DEBOUNCE,
  40. {
  41. maxWait: AUTO_COMPILE_MAX_WAIT,
  42. }
  43. )
  44. }
  45. // The main "compile" function.
  46. // Call this directly to run a compile now, otherwise call debouncedAutoCompile.
  47. async compile(options = {}) {
  48. // only compile if the feature flag is enabled
  49. if (!window.showNewPdfPreview) {
  50. return
  51. }
  52. // set "compiling" to true (in the React component's state), and return if it was already true
  53. let wasCompiling
  54. this.setCompiling(oldValue => {
  55. wasCompiling = oldValue
  56. return true
  57. })
  58. if (wasCompiling) {
  59. return
  60. }
  61. try {
  62. // log a sample of the compile requests
  63. sendMBSampled('editor-recompile-sampled', options)
  64. // reset values
  65. this.setChangedAt(0)
  66. this.validationIssues = undefined
  67. window.dispatchEvent(new CustomEvent('flush-changes')) // TODO: wait for this?
  68. const params = this.buildCompileParams(options)
  69. const t0 = performance.now()
  70. const data = await postJSON(
  71. `/project/${this.project._id}/compile?${params}`,
  72. {
  73. body: {
  74. rootDoc_id: this.getRootDocOverrideId(),
  75. draft: this.draft,
  76. check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
  77. // use incremental compile for all users but revert to a full compile
  78. // if there was previously a server error
  79. incrementalCompilesEnabled: !this.error,
  80. },
  81. signal: this.signal,
  82. }
  83. )
  84. const compileTimeClientE2E = performance.now() - t0
  85. const { firstRenderDone } = trackPdfDownload(data, compileTimeClientE2E)
  86. this.setFirstRenderDone(() => firstRenderDone)
  87. data.options = options
  88. this.setData(data)
  89. } catch (error) {
  90. console.error(error)
  91. this.setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
  92. } finally {
  93. this.setCompiling(false)
  94. }
  95. }
  96. // parse the text of the current doc in the editor
  97. // if it contains "\documentclass" then use this as the root doc
  98. getRootDocOverrideId() {
  99. // only override when not in the root doc itself
  100. if (this.currentDoc.doc_id !== this.project.rootDoc_id) {
  101. const snapshot = this.currentDoc.getSnapshot()
  102. if (snapshot && isMainFile(snapshot)) {
  103. return this.currentDoc.doc_id
  104. }
  105. }
  106. return null
  107. }
  108. // build the query parameters added to post-compile requests
  109. buildPostCompileParams() {
  110. const params = new URLSearchParams()
  111. // the id of the CLSI server that processed the previous compile request
  112. if (this.clsiServerId) {
  113. params.set('clsiserverid', this.clsiServerId)
  114. }
  115. return params
  116. }
  117. // build the query parameters for the compile request
  118. buildCompileParams(options) {
  119. const params = new URLSearchParams()
  120. // note: no clsiserverid query param is set on "compile" requests,
  121. // as this is added in the backend by the web api
  122. // tell the server whether this is an automatic or manual compile request
  123. if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
  124. params.set('auto_compile', 'true')
  125. }
  126. // use the feature flag to enable PDF caching in a ServiceWorker
  127. if (getMeta('ol-enablePdfCaching')) {
  128. params.set('enable_pdf_caching', 'true')
  129. }
  130. // use the feature flag to enable "file line errors"
  131. if (searchParams.get('file_line_errors') === 'true') {
  132. params.file_line_errors = 'true'
  133. }
  134. return params
  135. }
  136. // send a request to stop the current compile
  137. stopCompile() {
  138. // NOTE: no stoppingCompile state, as this should happen fairly quickly
  139. // and doesn't matter if it runs twice.
  140. const params = this.buildPostCompileParams()
  141. return postJSON(`/project/${this.project._id}/compile/stop?${params}`, {
  142. signal: this.signal,
  143. })
  144. .catch(error => {
  145. console.error(error)
  146. this.setError('error')
  147. })
  148. .finally(() => {
  149. this.setCompiling(false)
  150. })
  151. }
  152. // send a request to clear the cache
  153. clearCache() {
  154. const params = this.buildPostCompileParams()
  155. return deleteJSON(`/project/${this.project._id}/output?${params}`, {
  156. signal: this.signal,
  157. }).catch(error => {
  158. console.error(error)
  159. this.setError('clear-cache')
  160. })
  161. }
  162. }