compiler.ts 8.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282
  1. import { isMainFile } from './editor-files'
  2. import getMeta from '../../../utils/meta'
  3. import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
  4. import { debounce, DebouncedFunc } from 'lodash'
  5. import { EDITOR_SESSION_ID, trackPdfDownload } from './metrics'
  6. import { enablePdfCaching } from './pdf-caching-flags'
  7. import { debugConsole } from '@/utils/debugging'
  8. import { signalWithTimeout } from '@/utils/abort-signal'
  9. import { Dispatch, SetStateAction } from 'react'
  10. import { OpenDocuments } from '@/features/ide-react/editor/open-documents'
  11. import { DocumentContainer } from '@/features/ide-react/editor/document-container'
  12. import { CompileOptions, CompileResponseData } from '@ol-types/compile'
  13. import { DeliveryLatencies } from './types'
  14. const AUTO_COMPILE_MAX_WAIT = 5000
  15. // We add a 2 second debounce to sending user changes to server if they aren't
  16. // collaborating with anyone. This needs to be higher than SINGLE_USER_FLUSH_DELAY, and allow for
  17. // client to server latency, otherwise we compile before the op reaches the server
  18. // and then again on ack.
  19. const AUTO_COMPILE_DEBOUNCE = 2500
  20. // If there is a pending op, wait for it to be saved before compiling
  21. const PENDING_OP_MAX_WAIT = 10000
  22. const searchParams = new URLSearchParams(window.location.search)
  23. export default class DocumentCompiler {
  24. compilingRef: React.MutableRefObject<boolean>
  25. projectId: string
  26. setChangedAt: Dispatch<SetStateAction<number>>
  27. setCompiling: Dispatch<SetStateAction<boolean>>
  28. setData: Dispatch<SetStateAction<CompileResponseData | undefined>>
  29. setFirstRenderDone: Dispatch<SetStateAction<() => void>>
  30. setDeliveryLatencies: Dispatch<SetStateAction<DeliveryLatencies>>
  31. setError: Dispatch<SetStateAction<string | undefined>>
  32. cleanupCompileResult: () => void
  33. signal: AbortSignal
  34. openDocs: OpenDocuments
  35. projectRootDocId: string | null
  36. clsiServerId: string | null
  37. currentDoc: DocumentContainer | null
  38. error: Error | undefined
  39. timer: number
  40. defaultOptions: CompileOptions
  41. debouncedAutoCompile: DebouncedFunc<() => void>
  42. pathInFolder: (docId: string) => string | null
  43. constructor({
  44. compilingRef,
  45. projectId,
  46. setChangedAt,
  47. setCompiling,
  48. setData,
  49. setFirstRenderDone,
  50. setDeliveryLatencies,
  51. setError,
  52. cleanupCompileResult,
  53. signal,
  54. openDocs,
  55. }: {
  56. compilingRef: React.MutableRefObject<boolean>
  57. projectId: string
  58. setChangedAt: Dispatch<SetStateAction<number>>
  59. setCompiling: Dispatch<SetStateAction<boolean>>
  60. setData: Dispatch<SetStateAction<CompileResponseData | undefined>>
  61. setFirstRenderDone: Dispatch<SetStateAction<() => void>>
  62. setDeliveryLatencies: Dispatch<SetStateAction<DeliveryLatencies>>
  63. setError: Dispatch<SetStateAction<string | undefined>>
  64. cleanupCompileResult: () => void
  65. signal: AbortSignal
  66. openDocs: OpenDocuments
  67. }) {
  68. this.compilingRef = compilingRef
  69. this.projectId = projectId
  70. this.setChangedAt = setChangedAt
  71. this.setCompiling = setCompiling
  72. this.setData = setData
  73. this.setFirstRenderDone = setFirstRenderDone
  74. this.setDeliveryLatencies = setDeliveryLatencies
  75. this.setError = setError
  76. this.cleanupCompileResult = cleanupCompileResult
  77. this.signal = signal
  78. this.openDocs = openDocs
  79. this.pathInFolder = () => null
  80. this.projectRootDocId = null
  81. this.clsiServerId = null
  82. this.currentDoc = null
  83. this.error = undefined
  84. this.timer = 0
  85. this.defaultOptions = {
  86. draft: false,
  87. stopOnFirstError: false,
  88. }
  89. this.debouncedAutoCompile = debounce(
  90. () => {
  91. this.compile({ isAutoCompileOnChange: true })
  92. },
  93. AUTO_COMPILE_DEBOUNCE,
  94. {
  95. maxWait: AUTO_COMPILE_MAX_WAIT,
  96. }
  97. )
  98. }
  99. // The main "compile" function.
  100. // Call this directly to run a compile now, otherwise call debouncedAutoCompile.
  101. async compile(options: CompileOptions = {}) {
  102. options = { ...this.defaultOptions, ...options }
  103. if (options.isAutoCompileOnLoad && getMeta('ol-preventCompileOnLoad')) {
  104. return
  105. }
  106. // set "compiling" to true (in the React component's state), and return if it was already true
  107. const wasCompiling = this.compilingRef.current
  108. this.setCompiling(true)
  109. if (wasCompiling) {
  110. if (options.isAutoCompileOnChange) {
  111. this.debouncedAutoCompile()
  112. }
  113. return
  114. }
  115. try {
  116. await this.openDocs.awaitBufferedOps(
  117. signalWithTimeout(this.signal, PENDING_OP_MAX_WAIT)
  118. )
  119. // reset values
  120. this.setChangedAt(0) // TODO: wait for doc:saved?
  121. const params = this.buildCompileParams(options)
  122. const t0 = performance.now()
  123. let rootDocIdOverride = this.getRootDocOverrideId()
  124. let rootResourcePath
  125. try {
  126. // Only required for compile-from-history
  127. rootDocIdOverride = rootDocIdOverride || this.projectRootDocId
  128. rootResourcePath = rootDocIdOverride
  129. ? this.pathInFolder(rootDocIdOverride)
  130. : 'main.tex'
  131. } catch {}
  132. const body = {
  133. rootDoc_id: rootDocIdOverride,
  134. rootResourcePath,
  135. draft: options.draft,
  136. check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
  137. // use incremental compile for all users but revert to a full compile
  138. // if there was previously a server error
  139. incrementalCompilesEnabled: !this.error,
  140. stopOnFirstError: options.stopOnFirstError,
  141. editorId: EDITOR_SESSION_ID,
  142. }
  143. const data: CompileResponseData = await postJSON(
  144. `/project/${this.projectId}/compile?${params}`,
  145. { body, signal: this.signal }
  146. )
  147. const compileTimeClientE2E = Math.ceil(performance.now() - t0)
  148. const { deliveryLatencies, firstRenderDone } = trackPdfDownload(
  149. data,
  150. compileTimeClientE2E,
  151. t0
  152. )
  153. this.setDeliveryLatencies(() => deliveryLatencies)
  154. this.setFirstRenderDone(() => firstRenderDone)
  155. // unset the error before it's set again later, so that components are recreated and events are tracked
  156. this.setError(undefined)
  157. data.options = options
  158. data.rootDocId = rootDocIdOverride
  159. if (data.clsiServerId) {
  160. this.clsiServerId = data.clsiServerId
  161. }
  162. this.setData(data)
  163. } catch (error: any) {
  164. debugConsole.error(error)
  165. this.cleanupCompileResult()
  166. this.setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
  167. } finally {
  168. this.setCompiling(false)
  169. }
  170. }
  171. // parse the text of the current doc in the editor
  172. // if it contains "\documentclass" then use this as the root doc
  173. getRootDocOverrideId() {
  174. // only override when not in the root doc itself
  175. if (this.currentDoc && this.currentDoc.doc_id !== this.projectRootDocId) {
  176. const snapshot = this.currentDoc.getSnapshot()
  177. if (snapshot && isMainFile(snapshot)) {
  178. return this.currentDoc.doc_id
  179. }
  180. }
  181. return null
  182. }
  183. // build the query parameters added to post-compile requests
  184. buildPostCompileParams() {
  185. const params = new URLSearchParams()
  186. // the id of the CLSI server that processed the previous compile request
  187. if (this.clsiServerId) {
  188. params.set('clsiserverid', this.clsiServerId)
  189. }
  190. return params
  191. }
  192. // build the query parameters for the compile request
  193. buildCompileParams(options: CompileOptions) {
  194. const params = new URLSearchParams()
  195. // note: no clsiserverid query param is set on "compile" requests,
  196. // as this is added in the backend by the web api
  197. // tell the server whether this is an automatic or manual compile request
  198. if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
  199. params.set('auto_compile', 'true')
  200. }
  201. // use the feature flag to enable PDF caching
  202. if (enablePdfCaching) {
  203. params.set('enable_pdf_caching', 'true')
  204. }
  205. // use the feature flag to enable "file line errors"
  206. if (searchParams.get('file_line_errors') === 'true') {
  207. params.set('file_line_errors', 'true')
  208. }
  209. return params
  210. }
  211. // send a request to stop the current compile
  212. stopCompile() {
  213. // NOTE: no stoppingCompile state, as this should happen fairly quickly
  214. // and doesn't matter if it runs twice.
  215. const params = this.buildPostCompileParams()
  216. return postJSON(`/project/${this.projectId}/compile/stop?${params}`, {
  217. signal: this.signal,
  218. })
  219. .catch(error => {
  220. debugConsole.error(error)
  221. this.setError('error')
  222. })
  223. .finally(() => {
  224. this.setCompiling(false)
  225. })
  226. }
  227. // send a request to clear the cache
  228. clearCache() {
  229. const params = this.buildPostCompileParams()
  230. return deleteJSON(`/project/${this.projectId}/output?${params}`, {
  231. signal: this.signal,
  232. }).catch(error => {
  233. debugConsole.error(error)
  234. this.setError('clear-cache')
  235. })
  236. }
  237. setOption<Key extends keyof CompileOptions>(
  238. option: Key,
  239. value: CompileOptions[Key]
  240. ) {
  241. this.defaultOptions[option] = value
  242. }
  243. }