[clsi-cache] check compiler settings before using compile from cache (#24845)

* [web] provide an actual rootFolder from EditorProviders in tests

- Fixup SocketIOMock and ShareJS mocks to provide the complete interface
- Extend SocketIOMock interface to count event listeners
- Fixup test that did not expect to find a working rootDoc

* [web] expose imageName from ProjectContext

* [clsi-cache] check compiler settings before using compile from cache

* [web] avoid fetching initial compile from clsi-cache in PDF detach tab

GitOrigin-RevId: e3c754a7ceca55f03a317e1bc8ae45ed12cc2f02
This commit is contained in:
Jakob Ackermann
2025-04-16 08:05:35 +00:00
committed by Copybot
parent ef958f97a1
commit 39110d9da9
17 changed files with 416 additions and 72 deletions
@@ -277,13 +277,34 @@ if (typeof io === 'undefined' || !io) {
current = SocketShimV2
}
export class SocketIOMock extends EventEmitter {
export class SocketIOMock extends SocketShimBase {
constructor() {
super(new EventEmitter())
this.socket = {
get connected() {
return false
},
get sessionid() {
return undefined
},
get transport() {
return {}
},
get transports() {
return []
},
connect() {},
disconnect(reason) {},
}
}
addListener(event, listener) {
this.on(event, listener)
this._socket.on(event, listener)
}
removeListener(event, listener) {
this.off(event, listener)
this._socket.off(event, listener)
}
disconnect() {
@@ -294,6 +315,10 @@ export class SocketIOMock extends EventEmitter {
// Round-trip through JSON.parse/stringify to simulate (de-)serializing on network layer.
this.emit(...JSON.parse(JSON.stringify(args)))
}
countEventListeners(event) {
return this._socket.events[event].length
}
}
export default {
@@ -34,6 +34,7 @@ import { buildFileList } from '../../features/pdf-preview/util/file-list'
import { useLayoutContext } from './layout-context'
import { useUserContext } from './user-context'
import { useFileTreeData } from '@/shared/context/file-tree-data-context'
import { useDetachContext } from '@/shared/context/detach-context'
import { useFileTreePathContext } from '@/features/file-tree/contexts/file-tree-path'
import { useUserSettingsContext } from '@/shared/context/user-settings-context'
import { useFeatureFlag } from '@/shared/context/split-test-context'
@@ -46,6 +47,8 @@ import {
} from '@/shared/hooks/use-pdf-scroll-position'
import { PdfFileDataList } from '@/features/pdf-preview/util/types'
import { isSplitTestEnabled } from '@/utils/splitTestUtils'
import { captureException } from '@/infrastructure/error-reporter'
import OError from '@overleaf/o-error'
type PdfFile = Record<string, any>
@@ -117,8 +120,15 @@ export const LocalCompileContext = createContext<CompileContext | undefined>(
export const LocalCompileProvider: FC = ({ children }) => {
const { hasPremiumCompile, isProjectOwner } = useEditorContext()
const { openDocWithId, openDocs, currentDocument } = useEditorManagerContext()
const { role } = useDetachContext()
const { _id: projectId, rootDocId, joinedOnce } = useProjectContext()
const {
_id: projectId,
rootDocId,
joinedOnce,
imageName,
compiler: compilerName,
} = useProjectContext()
const { pdfPreviewOpen } = useLayoutContext()
@@ -190,10 +200,15 @@ export const LocalCompileProvider: FC = ({ children }) => {
const [compiledOnce, setCompiledOnce] = useState(false)
// fetch initial compile response from cache
const [initialCompileFromCache, setInitialCompileFromCache] = useState(
isSplitTestEnabled('initial-compile-from-clsi-cache')
isSplitTestEnabled('initial-compile-from-clsi-cache') &&
// Avoid fetching the initial compile from cache in PDF detach tab
role !== 'detached'
)
// Compile triggered while fetching the initial compile from cache
const upgradeInitialCompileFromCacheRef = useRef(false)
// fetch of initial compile from cache is pending
const [pendingInitialCompileFromCache, setPendingInitialCompileFromCache] =
useState(false)
// Raw data from clsi-cache, will need post-processing and check settings
const [dataFromCache, setDataFromCache] = useState<CompileResponseData>()
// whether the cache is being cleared
const [clearingCache, setClearingCache] = useState(false)
@@ -337,41 +352,88 @@ export const LocalCompileProvider: FC = ({ children }) => {
// try to fetch the last compile result after opening the project, potentially before joining the project.
useEffect(() => {
if (initialCompileFromCache) {
setInitialCompileFromCache(false)
setCompiling(true)
setCompiledOnce(true)
if (initialCompileFromCache && !pendingInitialCompileFromCache) {
setPendingInitialCompileFromCache(true)
getJSON(`/project/${projectId}/output/cached/output.overleaf.json`)
.then((data: any) => {
setCompiling(false)
setData({
...data,
options: compiler.defaultOptions,
})
if (upgradeInitialCompileFromCacheRef.current) {
compilingRef.current = false
compiler.compile() // trigger regular compile
}
// Hand data over to next effect, it will wait for project/doc loading.
setDataFromCache(data)
})
.catch(() => {
setCompiling(false)
if (upgradeInitialCompileFromCacheRef.current) {
compilingRef.current = false
compiler.compile() // trigger regular compile
} else {
setCompiledOnce(false) // trigger auto compile
}
// Let the isAutoCompileOnLoad effect take over
setInitialCompileFromCache(false)
setPendingInitialCompileFromCache(false)
})
}
}, [projectId, initialCompileFromCache, compiler])
}, [projectId, initialCompileFromCache, pendingInitialCompileFromCache])
// Maybe adopt the compile from cache
useEffect(() => {
if (!dataFromCache) return // no compile from cache available
if (!joinedOnce) return // wait for joinProject, it populates the file-tree.
if (!currentDocument) return // wait for current doc to load, it affects the rootDoc override
if (compiledOnce) return // regular compile triggered
// Gracefully access file-tree and getRootDocOverride
let settingsUpToDate = false
try {
dataFromCache.rootDocId = findEntityByPath(
dataFromCache.options?.rootResourcePath || ''
)?.entity?._id
const rootDocOverride = compiler.getRootDocOverrideId() || rootDocId
settingsUpToDate =
rootDocOverride === dataFromCache.rootDocId &&
dataFromCache.options.imageName === imageName &&
dataFromCache.options.compiler === compilerName &&
dataFromCache.options.stopOnFirstError === stopOnFirstError &&
dataFromCache.options.draft === draft
} catch (err) {
captureException(
OError.tag(err as unknown as Error, 'validate compile options', {
options: dataFromCache.options,
})
)
}
if (settingsUpToDate) {
setData(dataFromCache)
setCompiledOnce(true)
}
setDataFromCache(undefined)
setInitialCompileFromCache(false)
setPendingInitialCompileFromCache(false)
}, [
dataFromCache,
joinedOnce,
currentDocument,
compiledOnce,
rootDocId,
findEntityByPath,
compiler,
compilerName,
imageName,
stopOnFirstError,
draft,
])
// always compile the PDF once after opening the project, after the doc has loaded
useEffect(() => {
if (!compiledOnce && currentDocument && !initialCompileFromCache) {
if (
!compiledOnce &&
currentDocument &&
!initialCompileFromCache &&
!pendingInitialCompileFromCache
) {
setCompiledOnce(true)
compiler.compile({ isAutoCompileOnLoad: true })
}
}, [compiledOnce, currentDocument, initialCompileFromCache, compiler])
}, [
compiledOnce,
currentDocument,
initialCompileFromCache,
pendingInitialCompileFromCache,
compiler,
])
useEffect(() => {
setHasShortCompileTimeout(
@@ -619,10 +681,10 @@ export const LocalCompileProvider: FC = ({ children }) => {
// start a compile manually
const startCompile = useCallback(
options => {
upgradeInitialCompileFromCacheRef.current = true
setCompiledOnce(true)
compiler.compile(options)
},
[compiler, upgradeInitialCompileFromCacheRef]
[compiler, setCompiledOnce]
)
// stop a compile manually
@@ -34,6 +34,7 @@ export const ProjectProvider: FC = ({ children }) => {
const {
_id,
compiler,
imageName,
name,
rootDoc_id: rootDocId,
members,
@@ -59,6 +60,7 @@ export const ProjectProvider: FC = ({ children }) => {
return {
_id,
compiler,
imageName,
name,
rootDocId,
members,
@@ -75,6 +77,7 @@ export const ProjectProvider: FC = ({ children }) => {
}, [
_id,
compiler,
imageName,
name,
rootDocId,
members,
@@ -18,6 +18,7 @@ export type ProjectContextValue = {
rootDocId?: string
mainBibliographyDocId?: string
compiler: string
imageName: string
members: ProjectContextMember[]
invites: ProjectContextMember[]
features: {