compiler.ts 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276
  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. const rootDocId = this.getRootDocOverrideId() || this.projectRootDocId
  124. const rootResourcePath =
  125. (rootDocId && this.pathInFolder(rootDocId)) || 'main.tex'
  126. const body = {
  127. rootDoc_id: rootDocId,
  128. rootResourcePath,
  129. draft: options.draft,
  130. check: 'silent', // NOTE: 'error' and 'validate' are possible, but unused
  131. // use incremental compile for all users but revert to a full compile
  132. // if there was previously a server error
  133. incrementalCompilesEnabled: !this.error,
  134. stopOnFirstError: options.stopOnFirstError,
  135. editorId: EDITOR_SESSION_ID,
  136. }
  137. const data: CompileResponseData = await postJSON(
  138. `/project/${this.projectId}/compile?${params}`,
  139. { body, signal: this.signal }
  140. )
  141. const compileTimeClientE2E = Math.ceil(performance.now() - t0)
  142. const { deliveryLatencies, firstRenderDone } = trackPdfDownload(
  143. data,
  144. compileTimeClientE2E,
  145. t0
  146. )
  147. this.setDeliveryLatencies(() => deliveryLatencies)
  148. this.setFirstRenderDone(() => firstRenderDone)
  149. // unset the error before it's set again later, so that components are recreated and events are tracked
  150. this.setError(undefined)
  151. data.options = options
  152. data.rootDocId = rootDocId
  153. if (data.clsiServerId) {
  154. this.clsiServerId = data.clsiServerId
  155. }
  156. this.setData(data)
  157. } catch (error: any) {
  158. debugConsole.error(error)
  159. this.cleanupCompileResult()
  160. this.setError(error.info?.statusCode === 429 ? 'rate-limited' : 'error')
  161. } finally {
  162. this.setCompiling(false)
  163. }
  164. }
  165. // parse the text of the current doc in the editor
  166. // if it contains "\documentclass" then use this as the root doc
  167. getRootDocOverrideId() {
  168. // only override when not in the root doc itself
  169. if (this.currentDoc && this.currentDoc.doc_id !== this.projectRootDocId) {
  170. const snapshot = this.currentDoc.getSnapshot()
  171. if (snapshot && isMainFile(snapshot)) {
  172. return this.currentDoc.doc_id
  173. }
  174. }
  175. return null
  176. }
  177. // build the query parameters added to post-compile requests
  178. buildPostCompileParams() {
  179. const params = new URLSearchParams()
  180. // the id of the CLSI server that processed the previous compile request
  181. if (this.clsiServerId) {
  182. params.set('clsiserverid', this.clsiServerId)
  183. }
  184. return params
  185. }
  186. // build the query parameters for the compile request
  187. buildCompileParams(options: CompileOptions) {
  188. const params = new URLSearchParams()
  189. // note: no clsiserverid query param is set on "compile" requests,
  190. // as this is added in the backend by the web api
  191. // tell the server whether this is an automatic or manual compile request
  192. if (options.isAutoCompileOnLoad || options.isAutoCompileOnChange) {
  193. params.set('auto_compile', 'true')
  194. }
  195. // use the feature flag to enable PDF caching
  196. if (enablePdfCaching) {
  197. params.set('enable_pdf_caching', 'true')
  198. }
  199. // use the feature flag to enable "file line errors"
  200. if (searchParams.get('file_line_errors') === 'true') {
  201. params.set('file_line_errors', 'true')
  202. }
  203. return params
  204. }
  205. // send a request to stop the current compile
  206. stopCompile() {
  207. // NOTE: no stoppingCompile state, as this should happen fairly quickly
  208. // and doesn't matter if it runs twice.
  209. const params = this.buildPostCompileParams()
  210. return postJSON(`/project/${this.projectId}/compile/stop?${params}`, {
  211. signal: this.signal,
  212. })
  213. .catch(error => {
  214. debugConsole.error(error)
  215. this.setError('error')
  216. })
  217. .finally(() => {
  218. this.setCompiling(false)
  219. })
  220. }
  221. // send a request to clear the cache
  222. clearCache() {
  223. const params = this.buildPostCompileParams()
  224. return deleteJSON(`/project/${this.projectId}/output?${params}`, {
  225. signal: this.signal,
  226. }).catch(error => {
  227. debugConsole.error(error)
  228. this.setError('clear-cache')
  229. })
  230. }
  231. setOption<Key extends keyof CompileOptions>(
  232. option: Key,
  233. value: CompileOptions[Key]
  234. ) {
  235. this.defaultOptions[option] = value
  236. }
  237. }