From 03320bb37739a5551144e6cbc286377d066f8a4c Mon Sep 17 00:00:00 2001 From: Jakob Ackermann Date: Thu, 4 Sep 2025 09:44:42 +0200 Subject: [PATCH] Merge pull request #28264 from overleaf/jpa-synctex [web] use standard request handling for SyncTeX requests GitOrigin-RevId: ad5ba1834241d5939675f2533940ade741fc5abf --- .../app/src/Features/Compile/ClsiManager.js | 58 +++++++++- .../src/Features/Compile/CompileController.js | 100 ++++++------------ .../src/Features/Compile/CompileManager.js | 29 +++++ .../src/Compile/CompileControllerTests.js | 77 ++++++++------ 4 files changed, 162 insertions(+), 102 deletions(-) diff --git a/services/web/app/src/Features/Compile/ClsiManager.js b/services/web/app/src/Features/Compile/ClsiManager.js index 2bdf0091a9..27e8b9a1d5 100644 --- a/services/web/app/src/Features/Compile/ClsiManager.js +++ b/services/web/app/src/Features/Compile/ClsiManager.js @@ -818,9 +818,9 @@ function _finaliseRequest(projectId, options, project, docs, files) { } } -async function wordCount(projectId, userId, file, options, clsiserverid) { - const { compileBackendClass, compileGroup } = options - const req = await _buildRequest(projectId, options) +async function wordCount(projectId, userId, file, limits, clsiserverid) { + const { compileBackendClass, compileGroup } = limits + const req = await _buildRequest(projectId, limits) const filename = file || req.compile.rootResourcePath const url = _getCompilerUrl( compileBackendClass, @@ -847,6 +847,56 @@ async function wordCount(projectId, userId, file, options, clsiserverid) { return body } +async function syncTeX( + projectId, + userId, + { + direction, + compileFromClsiCache, + limits, + imageName, + validatedOptions, + clsiServerId, + } +) { + const { compileBackendClass, compileGroup } = limits + const url = _getCompilerUrl( + compileBackendClass, + compileGroup, + projectId, + userId, + `sync/${direction}` + ) + url.searchParams.set( + 'compileFromClsiCache', + compileFromClsiCache && ['alpha', 'priority'].includes(compileGroup) + ) + url.searchParams.set('imageName', imageName) + for (const [key, value] of Object.entries(validatedOptions)) { + url.searchParams.set(key, value) + } + const opts = { + method: 'GET', + } + try { + const { body } = await _makeRequestWithClsiServerId( + projectId, + userId, + compileGroup, + compileBackendClass, + url, + opts, + clsiServerId + ) + return body + } catch (err) { + if (err instanceof RequestFailedError && err.response.status === 404) { + throw new Errors.NotFoundError() + } + throw err + } +} + function _getClsiServerIdFromResponse(response) { const setCookieHeaders = response.headers.raw()['set-cookie'] ?? [] for (const header of setCookieHeaders) { @@ -883,6 +933,7 @@ module.exports = { deleteAuxFiles: callbackify(deleteAuxFiles), getOutputFileStream: callbackify(getOutputFileStream), wordCount: callbackify(wordCount), + syncTeX: callbackify(syncTeX), promises: { sendRequest, sendExternalRequest, @@ -890,5 +941,6 @@ module.exports = { deleteAuxFiles, getOutputFileStream, wordCount, + syncTeX, }, } diff --git a/services/web/app/src/Features/Compile/CompileController.js b/services/web/app/src/Features/Compile/CompileController.js index 028e19d93a..4b4d09657f 100644 --- a/services/web/app/src/Features/Compile/CompileController.js +++ b/services/web/app/src/Features/Compile/CompileController.js @@ -8,6 +8,7 @@ const CompileManager = require('./CompileManager') const ClsiManager = require('./ClsiManager') const logger = require('@overleaf/logger') const Settings = require('@overleaf/settings') +const Errors = require('../Errors/Errors') const SessionManager = require('../Authentication/SessionManager') const { RateLimiter } = require('../../infrastructure/RateLimiter') const ClsiCookieManager = require('./ClsiCookieManager')( @@ -38,16 +39,6 @@ function getOutputFilesArchiveSpecification(projectId, userId, buildId) { } } -async function getImageNameForProject(projectId) { - const project = await ProjectGetter.promises.getProject(projectId, { - imageName: 1, - }) - if (!project) { - throw new Error('project not found') - } - return project.imageName -} - async function getPdfCachingMinChunkSize(req, res) { const { variant } = await SplitTestHandler.promises.getAssignment( req, @@ -121,6 +112,32 @@ async function _getSplitTestOptions(req, res) { } } +async function _syncTeX(req, res, direction, validatedOptions) { + const projectId = req.params.Project_id + const { editorId, buildId, clsiserverid: clsiServerId } = req.query + if (!editorId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?editorId') + if (!buildId?.match(/^[a-f0-9-]+$/)) throw new Error('invalid ?buildId') + + const userId = CompileController._getUserIdForCompile(req) + const { compileFromClsiCache } = await _getSplitTestOptions(req, res) + try { + const body = await CompileManager.promises.syncTeX(projectId, userId, { + direction, + compileFromClsiCache, + validatedOptions: { + ...validatedOptions, + editorId, + buildId, + }, + clsiServerId, + }) + res.json(body) + } catch (err) { + if (err instanceof Errors.NotFoundError) return res.status(404).end() + throw err + } +} + const _CompileController = { async compile(req, res) { res.setTimeout(COMPILE_TIMEOUT_MS) @@ -470,18 +487,8 @@ const _CompileController = { return url }, - // compute a POST url for a project, user (optional) and action - _getUrl(projectId, userId, action) { - let path = `/project/${projectId}` - if (userId != null) { - path += `/user/${userId}` - } - return `${path}/${action}` - }, - async proxySyncPdf(req, res) { - const projectId = req.params.Project_id - const { page, h, v, editorId, buildId } = req.query + const { page, h, v } = req.query if (!page?.match(/^\d+$/)) { throw new Error('invalid page parameter') } @@ -491,28 +498,11 @@ const _CompileController = { if (!v?.match(/^-?\d+\.\d+$/)) { throw new Error('invalid v parameter') } - // whether this request is going to a per-user container - const userId = CompileController._getUserIdForCompile(req) - - const imageName = await getImageNameForProject(projectId) - - const { compileFromClsiCache } = await _getSplitTestOptions(req, res) - - const url = _CompileController._getUrl(projectId, userId, 'sync/pdf') - - await CompileController._proxyToClsi( - projectId, - 'sync-to-pdf', - url, - { page, h, v, imageName, editorId, buildId, compileFromClsiCache }, - req, - res - ) + await _syncTeX(req, res, 'pdf', { page, h, v }) }, async proxySyncCode(req, res) { - const projectId = req.params.Project_id - const { file, line, column, editorId, buildId } = req.query + const { file, line, column } = req.query if (file == null) { throw new Error('missing file parameter') } @@ -531,40 +521,12 @@ const _CompileController = { if (!column?.match(/^\d+$/)) { throw new Error('invalid column parameter') } - const userId = CompileController._getUserIdForCompile(req) - - const imageName = await getImageNameForProject(projectId) - - const { compileFromClsiCache } = await _getSplitTestOptions(req, res) - - const url = _CompileController._getUrl(projectId, userId, 'sync/code') - await CompileController._proxyToClsi( - projectId, - 'sync-to-code', - url, - { - file, - line, - column, - imageName, - editorId, - buildId, - compileFromClsiCache, - }, - req, - res - ) + await _syncTeX(req, res, 'code', { file, line, column }) }, async _proxyToClsi(projectId, action, url, qs, req, res) { const limits = await CompileManager.promises.getProjectCompileLimits(projectId) - if ( - qs?.compileFromClsiCache && - !['alpha', 'priority'].includes(limits.compileGroup) - ) { - qs.compileFromClsiCache = false - } return CompileController._proxyToClsiWithLimits( projectId, action, diff --git a/services/web/app/src/Features/Compile/CompileManager.js b/services/web/app/src/Features/Compile/CompileManager.js index 7897c3588d..5a90b0c4bc 100644 --- a/services/web/app/src/Features/Compile/CompileManager.js +++ b/services/web/app/src/Features/Compile/CompileManager.js @@ -110,7 +110,13 @@ async function getProjectCompileLimits(projectId) { const project = await ProjectGetter.promises.getProject(projectId, { owner_ref: 1, }) + return _getProjectCompileLimits(project) +} +async function _getProjectCompileLimits(project) { + if (!project) { + throw new Error('project not found') + } const owner = await UserGetter.promises.getUser(project.owner_ref, { _id: 1, alphaProgram: 1, @@ -162,6 +168,27 @@ async function wordCount(projectId, userId, file, clsiserverid) { ) } +async function syncTeX( + projectId, + userId, + { direction, compileFromClsiCache, validatedOptions, clsiServerId } +) { + const project = await ProjectGetter.promises.getProject(projectId, { + owner_ref: 1, + imageName: 1, + }) + const limits = await _getProjectCompileLimits(project) + const { imageName } = project + return await ClsiManager.promises.syncTeX(projectId, userId, { + direction, + limits, + imageName, + compileFromClsiCache, + validatedOptions, + clsiServerId, + }) +} + async function stopCompile(projectId, userId) { const limits = await CompileManager.promises.getProjectCompileLimits(projectId) @@ -188,6 +215,7 @@ module.exports = CompileManager = { getProjectCompileLimits, stopCompile, wordCount, + syncTeX, }, compile: callbackifyMultiResult(instrumentedCompile, [ 'status', @@ -249,6 +277,7 @@ module.exports = CompileManager = { }, wordCount: callbackify(wordCount), + syncTeX: callbackify(syncTeX), } const autoCompileRateLimiters = new Map() diff --git a/services/web/test/unit/src/Compile/CompileControllerTests.js b/services/web/test/unit/src/Compile/CompileControllerTests.js index 92bd21d176..caf9a61a66 100644 --- a/services/web/test/unit/src/Compile/CompileControllerTests.js +++ b/services/web/test/unit/src/Compile/CompileControllerTests.js @@ -22,6 +22,7 @@ describe('CompileController', function () { promises: { compile: sinon.stub(), getProjectCompileLimits: sinon.stub(), + syncTeX: sinon.stub(), }, } this.ClsiManager = { @@ -594,16 +595,24 @@ describe('CompileController', function () { }) }) describe('proxySyncCode', function () { - let file, line, column, imageName, editorId, buildId + let file, line, column, imageName, editorId, buildId, clsiServerId beforeEach(async function () { this.req.params = { Project_id: this.projectId } + clsiServerId = 'clsi-1' file = 'main.tex' line = String(Date.now()) column = String(Date.now() + 1) editorId = '172977cb-361e-4854-a4dc-a71cf11512e5' buildId = '195b4a3f9e7-03e5be430a9e7796' - this.req.query = { file, line, column, editorId, buildId } + this.req.query = { + file, + line, + column, + editorId, + buildId, + clsiserverid: clsiServerId, + } imageName = 'foo/bar:tag-0' this.ProjectGetter.promises.getProject = sinon @@ -615,37 +624,45 @@ describe('CompileController', function () { await this.CompileController.proxySyncCode(this.req, this.res, this.next) }) - it('should proxy the request with an imageName', function () { - expect(this.CompileController._proxyToClsi).to.have.been.calledWith( + it('should parse the parameters', function () { + expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith( this.projectId, - 'sync-to-code', - `/project/${this.projectId}/user/${this.user_id}/sync/code`, + this.user_id, { - file, - line, - column, - imageName, - editorId, - buildId, + direction: 'code', compileFromClsiCache: false, - }, - this.req, - this.res + validatedOptions: { + file, + line, + column, + editorId, + buildId, + }, + clsiServerId, + } ) }) }) describe('proxySyncPdf', function () { - let page, h, v, imageName, editorId, buildId + let page, h, v, imageName, editorId, buildId, clsiServerId beforeEach(async function () { this.req.params = { Project_id: this.projectId } + clsiServerId = 'clsi-1' page = String(Date.now()) h = String(Math.random()) v = String(Math.random()) editorId = '172977cb-361e-4854-a4dc-a71cf11512e5' buildId = '195b4a3f9e7-03e5be430a9e7796' - this.req.query = { page, h, v, editorId, buildId } + this.req.query = { + page, + h, + v, + editorId, + buildId, + clsiserverid: clsiServerId, + } imageName = 'foo/bar:tag-1' this.ProjectGetter.promises.getProject = sinon @@ -657,22 +674,22 @@ describe('CompileController', function () { await this.CompileController.proxySyncPdf(this.req, this.res, this.next) }) - it('should proxy the request with an imageName', function () { - expect(this.CompileController._proxyToClsi).to.have.been.calledWith( + it('should parse the parameters', function () { + expect(this.CompileManager.promises.syncTeX).to.have.been.calledWith( this.projectId, - 'sync-to-pdf', - `/project/${this.projectId}/user/${this.user_id}/sync/pdf`, + this.user_id, { - page, - h, - v, - imageName, - editorId, - buildId, + direction: 'pdf', compileFromClsiCache: false, - }, - this.req, - this.res + validatedOptions: { + page, + h, + v, + editorId, + buildId, + }, + clsiServerId, + } ) }) })