[clsi-cache] backend (#24388)

* [clsi-cache] initial revision of the clsi-cache service

* [clsi] send output files to clsi-cache and import from clsi-cache

* [web] pass editorId to clsi

* [web] clear clsi-cache when clearing clsi cache

* [web] add split-tests for controlling clsi-cache rollout

* [web] populate clsi-cache when cloning/creating project from template

* [clsi-cache] produce less noise when populating cache hits 404

* [clsi-cache] push docker image to AR

* [clsi-cache] push docker image to AR

* [clsi-cache] allow compileGroup in job payload

* [clsi-cache] set X-Zone header from latest endpoint

* [clsi-cache] use method POST for /enqueue endpoint

* [web] populate clsi-cache in zone b with template data

* [clsi-cache] limit number of editors per project/user folder to 10

* [web] clone: populate the clsi-cache unless the TeXLive release changed

* [clsi-cache] keep user folder when clearing cache as anonymous user

* [clsi] download old output.tar.gz when synctex finds empty compile dir

* [web] fix lint

* [clsi-cache] multi-zonal lookup of single build output

* [clsi-cache] add more validation and limits

Co-authored-by: Brian Gough <brian.gough@overleaf.com>

* [clsi] do not include clsi-cache tar-ball in output.zip

* [clsi-cache] fix reference after remaining constant

Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>

* [web] consolidate validation of filename into ClsiCacheHandler

* [clsi-cache] extend metrics and event tracking

- break down most of the clsi metrics by label
  - compile=initial - new compile dir without previous output files
  - compile=recompile - recompile in existing compile dir
  - compile=from-cache - compile using previous clsi-cache
- extend segmentation on compile-result-backend event
  - isInitialCompile=true - found new compile dir at start of request
  - restoredClsiCache=true - restored compile dir from clsi-cache

* [clsi] rename metrics labels for download of clsi-cache

This is in preparation for synctex changes.

* [clsi] use constant for limit of entries in output.tar.gz

Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>

* [clsi-cache] fix cloning of project cache

---------

Co-authored-by: Brian Gough <brian.gough@overleaf.com>
Co-authored-by: Alf Eaton <alf.eaton@overleaf.com>
Co-authored-by: Eric Mc Sween <eric.mcsween@overleaf.com>
GitOrigin-RevId: 4901a65497af13be1549af7f38ceee3188fcf881
This commit is contained in:
Jakob Ackermann
2025-04-10 08:05:17 +00:00
committed by Copybot
co-authored by Eric Mc Sween Brian Gough Alf Eaton
parent 7920cd9d3d
commit b538d56591
30 changed files with 1004 additions and 149 deletions
@@ -0,0 +1,127 @@
const {
fetchNothing,
fetchRedirectWithResponse,
RequestFailedError,
} = require('@overleaf/fetch-utils')
const logger = require('@overleaf/logger')
const Settings = require('@overleaf/settings')
const OError = require('@overleaf/o-error')
const { NotFoundError, InvalidNameError } = require('../Errors/Errors')
function validateFilename(filename) {
if (
![
'output.blg',
'output.log',
'output.pdf',
'output.overleaf.json',
'output.tar.gz',
].includes(filename) ||
filename.endsWith('.blg')
) {
throw new InvalidNameError('bad filename')
}
}
async function clearCache(projectId, userId) {
let path = `/project/${projectId}`
if (userId) {
path += `/user/${userId}`
}
path += '/output'
await Promise.all(
Settings.apis.clsiCache.instances.map(async ({ url, zone }) => {
const u = new URL(url)
u.pathname = path
try {
await fetchNothing(u, {
method: 'DELETE',
signal: AbortSignal.timeout(15_000),
})
} catch (err) {
throw OError.tag(err, 'clear clsi-cache', { url, zone })
}
})
)
}
async function getLatestOutputFile(
projectId,
userId,
filename,
signal = AbortSignal.timeout(15_000)
) {
validateFilename(filename)
let path = `/project/${projectId}`
if (userId) {
path += `/user/${userId}`
}
path += `/latest/output/${filename}`
for (const { url, zone } of Settings.apis.clsiCache.instances) {
const u = new URL(url)
u.pathname = path
try {
const {
location,
response: { headers },
} = await fetchRedirectWithResponse(u, {
signal,
})
// Success, return the cache entry.
return {
location,
zone: headers.get('X-Zone'),
lastModified: new Date(headers.get('X-Last-Modified')),
size: parseInt(headers.get('X-Content-Length'), 10),
allFiles: JSON.parse(headers.get('X-All-Files')),
}
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 404) {
break // No clsi-cache instance has cached something for this project/user.
}
logger.warn(
{ err, projectId, userId, url, zone },
'getLatestOutputFile from clsi-cache failed'
)
// This clsi-cache instance is down, try the next backend.
}
}
throw new NotFoundError('nothing cached yet')
}
async function prepareCacheSource(
projectId,
userId,
{ sourceProjectId, templateId, templateVersionId, lastUpdated, zone, signal }
) {
const url = new URL(
`/project/${projectId}/user/${userId}/import-from`,
Settings.apis.clsiCache.instances.find(i => i.zone === zone).url
)
try {
await fetchNothing(url, {
method: 'POST',
json: {
sourceProjectId,
lastUpdated,
templateId,
templateVersionId,
},
signal,
})
} catch (err) {
if (err instanceof RequestFailedError && err.response.status === 404) {
throw new NotFoundError()
}
throw err
}
}
module.exports = {
clearCache,
getLatestOutputFile,
prepareCacheSource,
}
@@ -0,0 +1,86 @@
const { NotFoundError } = require('../Errors/Errors')
const ClsiCacheHandler = require('./ClsiCacheHandler')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const ProjectGetter = require('../Project/ProjectGetter')
const SplitTestHandler = require('../SplitTests/SplitTestHandler')
async function getLatestBuildFromCache(projectId, userId, filename, signal) {
const [
{ location, lastModified: lastCompiled, zone, size, allFiles },
lastUpdatedInRedis,
{ lastUpdated: lastUpdatedInMongo, name: projectName },
] = await Promise.all([
ClsiCacheHandler.getLatestOutputFile(projectId, userId, filename, signal),
DocumentUpdaterHandler.promises.getProjectLastUpdatedAt(projectId),
ProjectGetter.promises.getProject(projectId, { lastUpdated: 1, name: 1 }),
])
const lastUpdated =
lastUpdatedInRedis > lastUpdatedInMongo
? lastUpdatedInRedis
: lastUpdatedInMongo
const isUpToDate = lastCompiled >= lastUpdated
return {
internal: {
location,
zone,
projectName,
},
external: {
isUpToDate,
lastUpdated,
size,
allFiles,
},
}
}
async function prepareClsiCache(
projectId,
userId,
{ sourceProjectId, templateId, templateVersionId }
) {
const { variant } = await SplitTestHandler.promises.getAssignmentForUser(
userId,
'copy-clsi-cache'
)
if (variant !== 'enabled') return
const signal = AbortSignal.timeout(5_000)
let lastUpdated
let zone = 'b' // populate template data on zone b
if (sourceProjectId) {
try {
;({
internal: { zone },
external: { lastUpdated },
} = await getLatestBuildFromCache(
sourceProjectId,
userId,
'output.tar.gz',
signal
))
} catch (err) {
if (err instanceof NotFoundError) return // nothing cached yet
throw err
}
}
try {
await ClsiCacheHandler.prepareCacheSource(projectId, userId, {
sourceProjectId,
templateId,
templateVersionId,
zone,
lastUpdated,
signal,
})
} catch (err) {
if (err instanceof NotFoundError) return // nothing cached yet/expired.
throw err
}
}
module.exports = {
getLatestBuildFromCache,
prepareClsiCache,
}
@@ -25,6 +25,7 @@ const ClsiFormatChecker = require('./ClsiFormatChecker')
const DocumentUpdaterHandler = require('../DocumentUpdater/DocumentUpdaterHandler')
const Metrics = require('@overleaf/metrics')
const Errors = require('../Errors/Errors')
const ClsiCacheHandler = require('./ClsiCacheHandler')
const { getBlobLocation } = require('../History/HistoryManager')
const VALID_COMPILERS = ['pdflatex', 'latex', 'xelatex', 'lualatex']
@@ -148,6 +149,13 @@ async function deleteAuxFiles(projectId, userId, options, clsiserverid) {
clsiserverid
)
} finally {
// always clear the clsi-cache
try {
await ClsiCacheHandler.clearCache(projectId, userId)
} catch (err) {
logger.warn({ err, projectId, userId }, 'purge clsi-cache failed')
}
// always clear the project state from the docupdater, even if there
// was a problem with the request to the clsi
try {
@@ -766,6 +774,7 @@ function _finaliseRequest(projectId, options, project, docs, files) {
compile: {
options: {
buildId: options.buildId,
editorId: options.editorId,
compiler: project.compiler,
timeout: options.timeout,
imageName: project.imageName,
@@ -775,6 +784,8 @@ function _finaliseRequest(projectId, options, project, docs, files) {
syncType: options.syncType,
syncState: options.syncState,
compileGroup: options.compileGroup,
compileFromClsiCache: options.compileFromClsiCache,
populateClsiCache: options.populateClsiCache,
enablePdfCaching:
(Settings.enablePdfCaching && options.enablePdfCaching) || false,
pdfCachingMinChunkSize: options.pdfCachingMinChunkSize,
@@ -66,11 +66,31 @@ const getSplitTestOptions = callbackify(async function (req, res) {
} catch (e) {}
const editorReq = { ...req, query }
// Lookup the clsi-cache flag in the backend.
// We may need to turn off the feature on a short notice, without requiring
// all users to reload their editor page to disable the feature.
const { variant: compileFromClsiCacheVariant } =
await SplitTestHandler.promises.getAssignment(
editorReq,
res,
'compile-from-clsi-cache'
)
const compileFromClsiCache = compileFromClsiCacheVariant === 'enabled'
const { variant: populateClsiCacheVariant } =
await SplitTestHandler.promises.getAssignment(
editorReq,
res,
'populate-clsi-cache'
)
const populateClsiCache = populateClsiCacheVariant === 'enabled'
const pdfDownloadDomain = Settings.pdfDownloadDomain
if (!req.query.enable_pdf_caching) {
// The frontend does not want to do pdf caching.
return {
compileFromClsiCache,
populateClsiCache,
pdfDownloadDomain,
enablePdfCaching: false,
}
@@ -88,12 +108,16 @@ const getSplitTestOptions = callbackify(async function (req, res) {
if (!enablePdfCaching) {
// Skip the lookup of the chunk size when caching is not enabled.
return {
compileFromClsiCache,
populateClsiCache,
pdfDownloadDomain,
enablePdfCaching: false,
}
}
const pdfCachingMinChunkSize = await getPdfCachingMinChunkSize(editorReq, res)
return {
compileFromClsiCache,
populateClsiCache,
pdfDownloadDomain,
enablePdfCaching,
pdfCachingMinChunkSize,
@@ -112,6 +136,7 @@ module.exports = CompileController = {
isAutoCompile,
fileLineErrors,
stopOnFirstError,
editorId: req.body.editorId,
}
if (req.body.rootDoc_id) {
@@ -138,8 +163,15 @@ module.exports = CompileController = {
getSplitTestOptions(req, res, (err, splitTestOptions) => {
if (err) return next(err)
let { enablePdfCaching, pdfCachingMinChunkSize, pdfDownloadDomain } =
splitTestOptions
let {
compileFromClsiCache,
populateClsiCache,
enablePdfCaching,
pdfCachingMinChunkSize,
pdfDownloadDomain,
} = splitTestOptions
options.compileFromClsiCache = compileFromClsiCache
options.populateClsiCache = populateClsiCache
options.enablePdfCaching = enablePdfCaching
if (enablePdfCaching) {
options.pdfCachingMinChunkSize = pdfCachingMinChunkSize
@@ -193,6 +225,8 @@ module.exports = CompileController = {
timeout: limits.timeout === 60 ? 'short' : 'long',
server: clsiServerId?.includes('-c2d-') ? 'faster' : 'normal',
isAutoCompile,
isInitialCompile: stats.isInitialCompile === 1,
restoredClsiCache: stats.restoredClsiCache === 1,
stopOnFirstError,
}
)
@@ -497,7 +531,7 @@ module.exports = CompileController = {
proxySyncPdf(req, res, next) {
const projectId = req.params.Project_id
const { page, h, v } = req.query
const { page, h, v, editorId, buildId } = req.query
if (!page?.match(/^\d+$/)) {
return next(new Error('invalid page parameter'))
}
@@ -515,23 +549,29 @@ module.exports = CompileController = {
getImageNameForProject(projectId, (error, imageName) => {
if (error) return next(error)
const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
CompileController.proxyToClsi(
projectId,
'sync-to-pdf',
url,
{ page, h, v, imageName },
req,
res,
next
)
getSplitTestOptions(req, res, (error, splitTestOptions) => {
if (error) return next(error)
const { compileFromClsiCache } = splitTestOptions
const url = CompileController._getUrl(projectId, userId, 'sync/pdf')
CompileController.proxyToClsi(
projectId,
'sync-to-pdf',
url,
{ page, h, v, imageName, editorId, buildId, compileFromClsiCache },
req,
res,
next
)
})
})
})
},
proxySyncCode(req, res, next) {
const projectId = req.params.Project_id
const { file, line, column } = req.query
const { file, line, column, editorId, buildId } = req.query
if (file == null) {
return next(new Error('missing file parameter'))
}
@@ -557,16 +597,29 @@ module.exports = CompileController = {
getImageNameForProject(projectId, (error, imageName) => {
if (error) return next(error)
const url = CompileController._getUrl(projectId, userId, 'sync/code')
CompileController.proxyToClsi(
projectId,
'sync-to-code',
url,
{ file, line, column, imageName },
req,
res,
next
)
getSplitTestOptions(req, res, (error, splitTestOptions) => {
if (error) return next(error)
const { compileFromClsiCache } = splitTestOptions
const url = CompileController._getUrl(projectId, userId, 'sync/code')
CompileController.proxyToClsi(
projectId,
'sync-to-code',
url,
{
file,
line,
column,
imageName,
editorId,
buildId,
compileFromClsiCache,
},
req,
res,
next
)
})
})
})
},
@@ -21,6 +21,7 @@ const TpdsProjectFlusher = require('../ThirdPartyDataStore/TpdsProjectFlusher')
const _ = require('lodash')
const TagsHandler = require('../Tags/TagsHandler')
const Features = require('../../infrastructure/Features')
const ClsiCacheManager = require('../Compile/ClsiCacheManager')
module.exports = {
duplicate: callbackify(duplicate),
@@ -35,6 +36,7 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
originalProjectId,
{
compiler: true,
imageName: true,
rootFolder: true,
rootDoc_id: true,
fromV1TemplateId: true,
@@ -73,6 +75,21 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
{ segmentation }
)
let prepareClsiCacheInBackground = Promise.resolve()
if (originalProject.imageName === newProject.imageName) {
// Populate the clsi-cache unless the TeXLive release has changed.
prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
newProject._id,
owner._id,
{ sourceProjectId: originalProjectId }
).catch(err => {
logger.warn(
{ err, originalProjectId, projectId: newProject._id },
'failed to prepare clsi-cache for cloned project'
)
})
}
try {
await ProjectOptionsHandler.promises.setCompiler(
newProject._id,
@@ -120,6 +137,10 @@ async function duplicate(owner, originalProjectId, newProjectName, tags = []) {
})
}
try {
await prepareClsiCacheInBackground
} catch {}
return newProject
}
@@ -17,6 +17,7 @@ const settings = require('@overleaf/settings')
const crypto = require('crypto')
const Errors = require('../Errors/Errors')
const { pipeline } = require('stream/promises')
const ClsiCacheManager = require('../Compile/ClsiCacheManager')
const TemplatesManager = {
async createProjectFromV1Template(
@@ -63,6 +64,17 @@ const TemplatesManager = {
attributes
)
const prepareClsiCacheInBackground = ClsiCacheManager.prepareClsiCache(
project._id,
userId,
{ templateId, templateVersionId }
).catch(err => {
logger.warn(
{ err, templateId, templateVersionId, projectId: project._id },
'failed to prepare clsi-cache from template'
)
})
await TemplatesManager._setCompiler(project._id, compiler)
await TemplatesManager._setImage(project._id, imageName)
await TemplatesManager._setMainFile(project._id, mainFile)
@@ -74,6 +86,8 @@ const TemplatesManager = {
}
await Project.updateOne({ _id: project._id }, update, {})
await prepareClsiCacheInBackground
return project
} finally {
await fs.promises.unlink(dumpPath)
+3
View File
@@ -242,6 +242,9 @@ module.exports = {
submissionBackendClass:
process.env.CLSI_SUBMISSION_BACKEND_CLASS || 'n2d',
},
clsiCache: {
instances: JSON.parse(process.env.CLSI_CACHE_INSTANCES || '[]'),
},
project_history: {
sendProjectStructureOps: true,
url: `http://${process.env.PROJECT_HISTORY_HOST || '127.0.0.1'}:3054`,
@@ -144,6 +144,7 @@ function PdfSynctexControls() {
const {
clsiServerId,
pdfFile,
pdfUrl,
pdfViewer,
position,
@@ -239,6 +240,8 @@ function PdfSynctexControls() {
if (clsiServerId) {
params += `&clsiserverid=${clsiServerId}`
}
if (pdfFile?.editorId) params += `&editorId=${pdfFile.editorId}`
if (pdfFile?.build) params += `&buildId=${pdfFile.build}`
getJSON(`/project/${projectId}/sync/code?${params}`, { signal })
.then(data => {
@@ -253,6 +256,7 @@ function PdfSynctexControls() {
})
},
[
pdfFile,
clsiServerId,
isMounted,
projectId,
@@ -344,6 +348,8 @@ function PdfSynctexControls() {
if (clsiServerId) {
params.set('clsiserverid', clsiServerId)
}
if (pdfFile?.editorId) params.set('editorId', pdfFile.editorId)
if (pdfFile?.build) params.set('buildId', pdfFile.build)
getJSON(`/project/${projectId}/sync/pdf?${params}`, { signal })
.then(data => {
@@ -358,6 +364,7 @@ function PdfSynctexControls() {
})
},
[
pdfFile,
clsiServerId,
projectId,
signal,
@@ -2,7 +2,7 @@ import { isMainFile } from './editor-files'
import getMeta from '../../../utils/meta'
import { deleteJSON, postJSON } from '../../../infrastructure/fetch-json'
import { debounce } from 'lodash'
import { trackPdfDownload } from './metrics'
import { EDITOR_SESSION_ID, trackPdfDownload } from './metrics'
import { enablePdfCaching } from './pdf-caching-flags'
import { debugConsole } from '@/utils/debugging'
import { signalWithTimeout } from '@/utils/abort-signal'
@@ -109,6 +109,7 @@ export default class DocumentCompiler {
// if there was previously a server error
incrementalCompilesEnabled: !this.error,
stopOnFirstError: options.stopOnFirstError,
editorId: EDITOR_SESSION_ID,
}
const data = await postJSON(
@@ -8,7 +8,7 @@ import { debugConsole } from '@/utils/debugging'
const VERSION = 9
// editing session id
const EDITOR_SESSION_ID = uuid()
export const EDITOR_SESSION_ID = uuid()
const pdfCachingMetrics = {
viewerId: EDITOR_SESSION_ID,
@@ -4,6 +4,7 @@ import { enablePdfCaching } from './pdf-caching-flags'
import { debugConsole } from '@/utils/debugging'
import { dirname, findEntityByPath } from '@/features/file-tree/util/path'
import '@/utils/readable-stream-async-iterator-polyfill'
import { EDITOR_SESSION_ID } from '@/features/pdf-preview/util/metrics'
// Warnings that may disappear after a second LaTeX pass
const TRANSIENT_WARNING_REGEX = /^(Reference|Citation).+undefined on input line/
@@ -15,6 +16,8 @@ export function handleOutputFiles(outputFiles, projectId, data) {
const outputFile = outputFiles.get('output.pdf')
if (!outputFile) return null
outputFile.editorId = outputFile.editorId || EDITOR_SESSION_ID
// build the URL for viewing the PDF in the preview UI
const params = new URLSearchParams({
compileGroup: data.compileGroup,
@@ -144,6 +144,9 @@ describe('ClsiManager', function () {
enablePdfCaching: true,
clsiCookie: { key: 'clsiserver' },
}
this.ClsiCacheHandler = {
clearCache: sinon.stub().resolves(),
}
this.Features = {
hasFeature: sinon.stub().withArgs('project-history-blobs').returns(true),
}
@@ -172,6 +175,7 @@ describe('ClsiManager', function () {
this.DocumentUpdaterHandler,
'./ClsiCookieManager': () => this.ClsiCookieManager,
'./ClsiStateManager': this.ClsiStateManager,
'./ClsiCacheHandler': this.ClsiCacheHandler,
'@overleaf/fetch-utils': this.FetchUtils,
'./ClsiFormatChecker': this.ClsiFormatChecker,
'@overleaf/metrics': this.Metrics,
@@ -390,6 +394,8 @@ describe('ClsiManager', function () {
incrementalCompilesEnabled: true,
compileBackendClass: 'e2',
compileGroup: 'priority',
compileFromClsiCache: true,
populateClsiCache: true,
enablePdfCaching: true,
pdfCachingMinChunkSize: 1337,
}
@@ -448,6 +454,8 @@ describe('ClsiManager', function () {
syncType: 'incremental',
syncState: '01234567890abcdef',
compileGroup: 'priority',
compileFromClsiCache: true,
populateClsiCache: true,
enablePdfCaching: true,
pdfCachingMinChunkSize: 1337,
metricsMethod: 'priority',
@@ -945,6 +953,12 @@ describe('ClsiManager', function () {
)
})
it('should clear the output.tar.gz files in clsi-cache', function () {
this.ClsiCacheHandler.clearCache
.calledWith(this.project._id, this.user_id)
.should.equal(true)
})
it('should clear the project state from the docupdater', function () {
this.DocumentUpdaterHandler.promises.clearProjectState
.calledWith(this.project._id)
@@ -244,9 +244,12 @@ describe('CompileController', function () {
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
@@ -284,9 +287,12 @@ describe('CompileController', function () {
this.user_id,
{
isAutoCompile: true,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
@@ -305,10 +311,37 @@ describe('CompileController', function () {
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
draft: true,
fileLineErrors: false,
stopOnFirstError: false,
editorId: undefined,
}
)
})
})
describe('with an editor id', function () {
beforeEach(function (done) {
this.res.callback = done
this.req.body = { editorId: 'the-editor-id' }
this.CompileController.compile(this.req, this.res, this.next)
})
it('should pass the editor id to the compiler', function () {
this.CompileManager.compile.should.have.been.calledWith(
this.projectId,
this.user_id,
{
isAutoCompile: false,
compileFromClsiCache: false,
populateClsiCache: false,
enablePdfCaching: false,
fileLineErrors: false,
stopOnFirstError: false,
editorId: 'the-editor-id',
}
)
})
@@ -542,14 +575,16 @@ describe('CompileController', function () {
})
})
describe('proxySyncCode', function () {
let file, line, column, imageName
let file, line, column, imageName, editorId, buildId
beforeEach(function (done) {
this.req.params = { Project_id: this.projectId }
file = 'main.tex'
line = String(Date.now())
column = String(Date.now() + 1)
this.req.query = { file, line, column }
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
buildId = '195b4a3f9e7-03e5be430a9e7796'
this.req.query = { file, line, column, editorId, buildId }
imageName = 'foo/bar:tag-0'
this.ProjectGetter.getProject = sinon.stub().yields(null, { imageName })
@@ -566,7 +601,15 @@ describe('CompileController', function () {
this.projectId,
'sync-to-code',
`/project/${this.projectId}/user/${this.user_id}/sync/code`,
{ file, line, column, imageName },
{
file,
line,
column,
imageName,
editorId,
buildId,
compileFromClsiCache: false,
},
this.req,
this.res,
this.next
@@ -575,14 +618,16 @@ describe('CompileController', function () {
})
describe('proxySyncPdf', function () {
let page, h, v, imageName
let page, h, v, imageName, editorId, buildId
beforeEach(function (done) {
this.req.params = { Project_id: this.projectId }
page = String(Date.now())
h = String(Math.random())
v = String(Math.random())
this.req.query = { page, h, v }
editorId = '172977cb-361e-4854-a4dc-a71cf11512e5'
buildId = '195b4a3f9e7-03e5be430a9e7796'
this.req.query = { page, h, v, editorId, buildId }
imageName = 'foo/bar:tag-1'
this.ProjectGetter.getProject = sinon.stub().yields(null, { imageName })
@@ -599,7 +644,15 @@ describe('CompileController', function () {
this.projectId,
'sync-to-pdf',
`/project/${this.projectId}/user/${this.user_id}/sync/pdf`,
{ page, h, v, imageName },
{
page,
h,
v,
imageName,
editorId,
buildId,
compileFromClsiCache: false,
},
this.req,
this.res,
this.next
@@ -245,6 +245,9 @@ describe('ProjectDuplicator', function () {
'../Tags/TagsHandler': this.TagsHandler,
'../History/HistoryManager': this.HistoryManager,
'../../infrastructure/Features': this.Features,
'../Compile/ClsiCacheManager': {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
},
},
})
})
@@ -121,6 +121,9 @@ describe('TemplatesManager', function () {
fs: this.fs,
'../../models/Project': { Project: this.Project },
'stream/promises': { pipeline: this.pipeline },
'../Compile/ClsiCacheManager': {
prepareClsiCache: sinon.stub().rejects(new Error('ignore this')),
},
},
}).promises
return (this.zipUrl =