From 8f173d7dc7612c466a5cdb73ccdc7b83ca146ee0 Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Fri, 6 Feb 2026 11:39:22 +0100 Subject: [PATCH] [web] integrate example project with clsi-cache (#31365) * [web] integrate example project with clsi-cache * [web] make prettier happy * [e2e] gracefully handle initial compile from cache GitOrigin-RevId: c7730a511e889749399ce660d692cad6e57dd5b3 --- .../Project/ProjectCreationHandler.mjs | 94 ++++++++++++++++++- 1 file changed, 90 insertions(+), 4 deletions(-) diff --git a/services/web/app/src/Features/Project/ProjectCreationHandler.mjs b/services/web/app/src/Features/Project/ProjectCreationHandler.mjs index 095fe12391..dc30e23886 100644 --- a/services/web/app/src/Features/Project/ProjectCreationHandler.mjs +++ b/services/web/app/src/Features/Project/ProjectCreationHandler.mjs @@ -1,5 +1,6 @@ import OError from '@overleaf/o-error' import metrics from '@overleaf/metrics' +import logger from '@overleaf/logger' import Settings from '@overleaf/settings' import mongodb from 'mongodb-legacy' import Features from '../../infrastructure/Features.mjs' @@ -16,6 +17,8 @@ import _ from 'lodash' import AnalyticsManager from '../Analytics/AnalyticsManager.mjs' import TpdsUpdateSender from '../ThirdPartyDataStore/TpdsUpdateSender.mjs' import SplitTestHandler from '../SplitTests/SplitTestHandler.mjs' +import ClsiCacheManager from '../Compile/ClsiCacheManager.mjs' +import crypto from 'node:crypto' const { ObjectId } = mongodb @@ -95,10 +98,85 @@ async function createBasicProject(ownerId, projectName) { return project } +function hashProjectContent({ fileEntries, docEntries }) { + const entries = [] + for (const { + path, + file: { hash }, + } of fileEntries) { + entries.push(`${path}:${hash}`) + } + for (const { path, docLines } of docEntries) { + entries.push(`${path}:${docLines}`) + } + entries.sort() + const hash = crypto.createHash('sha256') + for (const item of entries) { + hash.update(item) + } + return hash.digest('hex') +} + +let exampleProjectContentHash + +async function populateClsiCacheForExampleProject( + ownerId, + project, + fileEntries, + docEntries +) { + const hash = hashProjectContent({ fileEntries, docEntries }) + if (exampleProjectContentHash && exampleProjectContentHash !== hash) { + // We need a stable identifier for example projects. Otherwise we will + // generate a lot of cruft in clsi-cache/GCS. + const err = new Error('example project content is not static') + logger.error({ err }, err.message) + return + } + exampleProjectContentHash = hash + + const templateVersionId = `example-project-${hash}` + const { _id: projectId, imageName } = project + const found = await ClsiCacheManager.prepareClsiCache(projectId, ownerId, { + templateVersionId, + imageName: imageName && path.basename(imageName), + }).catch(err => { + logger.error( + { err, templateVersionId, projectId }, + 'failed to prepare clsi-cache from example project' + ) + return undefined + }) + if (found === false) { + ClsiCacheManager.createTemplateClsiCache({ + templateVersionId, + project, + fileEntries, + docEntries, + }).catch(err => { + logger.error( + { err, templateVersionId }, + 'failed to create example project clsi-cache' + ) + }) + } + return projectId +} + async function createExampleProject(ownerId, projectName) { const project = await _createBlankProject(ownerId, projectName) - await _addExampleProjectFiles(ownerId, projectName, project) + const { fileEntries, docEntries } = await _addExampleProjectFiles( + ownerId, + projectName, + project + ) + await populateClsiCacheForExampleProject( + ownerId, + project, + fileEntries, + docEntries + ) AnalyticsManager.recordEventForUserInBackground(ownerId, 'project-created', { projectId: project._id, @@ -113,14 +191,14 @@ async function _addExampleProjectFiles(ownerId, projectName, project) { ownerId, projectName ) - await _createRootDoc(project, ownerId, mainDocLines) + const rootDoc = await _createRootDoc(project, ownerId, mainDocLines) const bibDocLines = await _buildTemplate( `${templateProjectDir}/sample.bib`, ownerId, projectName ) - await ProjectEntityUpdateHandler.promises.addDoc( + const bibDoc = await ProjectEntityUpdateHandler.promises.addDoc( project._id, project.rootFolder[0]._id, 'sample.bib', @@ -133,7 +211,7 @@ async function _addExampleProjectFiles(ownerId, projectName, project) { import.meta.dirname, `/../../../templates/project_files/${templateProjectDir}/frog.jpg` ) - await ProjectEntityUpdateHandler.promises.addFile( + const { fileRef } = await ProjectEntityUpdateHandler.promises.addFile( project._id, project.rootFolder[0]._id, 'frog.jpg', @@ -142,6 +220,13 @@ async function _addExampleProjectFiles(ownerId, projectName, project) { ownerId, null ) + return { + fileEntries: [{ path: fileRef.name, file: fileRef }], + docEntries: [ + { path: 'main.tex', doc: rootDoc, docLines: mainDocLines.join('\n') }, + { path: 'sample.bib', doc: bibDoc, docLines: bibDocLines.join('\n') }, + ], + } } async function _createBlankProject( @@ -225,6 +310,7 @@ async function _createRootDoc(project, ownerId, docLines) { null ) await ProjectEntityUpdateHandler.promises.setRootDoc(project._id, doc._id) + return doc } catch (error) { throw OError.tag(error, 'error adding root doc when creating project') }