From 442c1952ab015f7db459d5764556a589ee2592a3 Mon Sep 17 00:00:00 2001 From: Mathias Jakobsen Date: Fri, 5 Sep 2025 11:02:33 +0100 Subject: [PATCH] [web] Avoid double indexing of client side referencing (#28235) * [web] Move chat client id to shared module * [web] Avoid double indexing of client references GitOrigin-RevId: 993930e66fdc9952649e3e8d345c70dd37516121 --- .../LinkedFiles/LinkedFilesController.mjs | 4 +++- .../References/ReferencesController.mjs | 18 ++++++++++++++---- .../js/features/chat/context/chat-context.tsx | 10 ++-------- .../components/file-view-refresh-button.tsx | 2 ++ .../ide-react/context/references-context.tsx | 9 +++++++-- services/web/frontend/js/utils/client-id.ts | 7 +++++++ .../chat/context/chat-context.test.tsx | 4 ++-- 7 files changed, 37 insertions(+), 17 deletions(-) create mode 100644 services/web/frontend/js/utils/client-id.ts diff --git a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs index b6ddf815fa..a749feca70 100644 --- a/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs +++ b/services/web/app/src/Features/LinkedFiles/LinkedFilesController.mjs @@ -84,6 +84,7 @@ async function createLinkedFile(req, res, next) { async function refreshLinkedFile(req, res, next) { const { project_id: projectId, file_id: fileId } = req.params + const { clientId } = req.body const userId = SessionManager.getLoggedInUserId(req.session) const { file, parentFolder } = await LinkedFilesHandler.promises.getFileById( @@ -138,7 +139,8 @@ async function refreshLinkedFile(req, res, next) { projectId, 'references:keys:updated', data.keys, - true + true, + clientId ) res.json({ new_file_id: newFileId }) } else { diff --git a/services/web/app/src/Features/References/ReferencesController.mjs b/services/web/app/src/Features/References/ReferencesController.mjs index 378f096a40..14dbaec1eb 100644 --- a/services/web/app/src/Features/References/ReferencesController.mjs +++ b/services/web/app/src/Features/References/ReferencesController.mjs @@ -19,7 +19,7 @@ let ReferencesController export default ReferencesController = { indexAll(req, res, next) { const projectId = req.params.Project_id - const { shouldBroadcast } = req.body + const { shouldBroadcast, clientId } = req.body return ReferencesHandler.indexAll(projectId, function (error, data) { if (error) { OError.tag(error, 'failed to index references', { projectId }) @@ -31,12 +31,21 @@ export default ReferencesController = { projectId, shouldBroadcast, true, - data + data, + clientId ) }) }, - _handleIndexResponse(req, res, projectId, shouldBroadcast, isAllDocs, data) { + _handleIndexResponse( + req, + res, + projectId, + shouldBroadcast, + isAllDocs, + data, + clientId + ) { if (data == null || data.keys == null) { return res.json({ projectId, keys: [] }) } @@ -45,7 +54,8 @@ export default ReferencesController = { projectId, 'references:keys:updated', data.keys, - isAllDocs + isAllDocs, + clientId ) } return res.json(data) diff --git a/services/web/frontend/js/features/chat/context/chat-context.tsx b/services/web/frontend/js/features/chat/context/chat-context.tsx index fe6fe96afa..4b437378ba 100644 --- a/services/web/frontend/js/features/chat/context/chat-context.tsx +++ b/services/web/frontend/js/features/chat/context/chat-context.tsx @@ -8,8 +8,7 @@ import { useRef, FC, } from 'react' -import { v4 as uuid } from 'uuid' - +import clientIdGenerator from '@/utils/client-id' import { useUserContext } from '../../../shared/context/user-context' import { useProjectContext } from '../../../shared/context/project-context' import { getJSON, postJSON } from '../../../infrastructure/fetch-json' @@ -78,11 +77,6 @@ type Action = error: any } -// Wrap uuid in an object method so that it can be stubbed -export const chatClientIdGenerator = { - generate: () => uuid(), -} - let nextChatMessageId = 1 function generateChatMessageId() { @@ -201,7 +195,7 @@ export const ChatProvider: FC = ({ children }) => { const clientId = useRef() if (clientId.current === undefined) { - clientId.current = chatClientIdGenerator.generate() + clientId.current = clientIdGenerator.get() } const user = useUserContext() const { projectId } = useProjectContext() diff --git a/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx b/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx index b032e8cd9f..a750d69182 100644 --- a/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx +++ b/services/web/frontend/js/features/file-view/components/file-view-refresh-button.tsx @@ -14,6 +14,7 @@ import importOverleafModules from '../../../../macros/import-overleaf-module.mac import OLButton from '@/shared/components/ol/ol-button' import { sendMB } from '@/infrastructure/event-tracking' import useIsMounted from '@/shared/hooks/use-is-mounted' +import clientId from '@/utils/client-id' type FileViewRefreshButtonProps = { setRefreshError: Dispatch>> @@ -42,6 +43,7 @@ export default function FileViewRefreshButton({ window.expectingLinkedFileRefreshedSocketFor = file.name const body = { shouldReindexReferences: isTPR || /\.bib$/.test(file.name), + clientId: clientId.get(), } postJSON(`/project/${projectId}/linked_file/${file.id}/refresh`, { body, diff --git a/services/web/frontend/js/features/ide-react/context/references-context.tsx b/services/web/frontend/js/features/ide-react/context/references-context.tsx index e698f00c49..c256e5fd87 100644 --- a/services/web/frontend/js/features/ide-react/context/references-context.tsx +++ b/services/web/frontend/js/features/ide-react/context/references-context.tsx @@ -24,6 +24,7 @@ import { debugConsole } from '@/utils/debugging' import { useFeatureFlag } from '@/shared/context/split-test-context' import type { ReferenceIndexer } from '../references/reference-indexer' import { AdvancedReferenceSearchResult } from '@/features/ide-react/references/types' +import clientId from '@/utils/client-id' export const ReferencesContext = createContext< | { @@ -105,7 +106,7 @@ export const ReferencesProvider: FC = ({ if (shouldBroadcast) { // Inform other clients about change in keys await postJSON(`/project/${projectId}/references/indexAll`, { - body: { shouldBroadcast: true }, + body: { shouldBroadcast: true, clientId: clientId.get() }, }).catch(error => { // allow the request to fail debugConsole.error(error) @@ -176,8 +177,12 @@ export const ReferencesProvider: FC = ({ const handleProjectJoined = () => { // We only need to grab the references when the editor first loads, // not on every reconnect - socket.on('references:keys:updated', (keys, allDocs) => { + socket.on('references:keys:updated', (keys, allDocs, refresherId) => { if (clientSideReferences) { + if (refresherId === clientId.get()) { + // We asked for this broadcast, so we must have already done the indexing + return + } indexAllReferences(false) } else { setReferenceKeys(oldDocs => diff --git a/services/web/frontend/js/utils/client-id.ts b/services/web/frontend/js/utils/client-id.ts new file mode 100644 index 0000000000..a625ecce05 --- /dev/null +++ b/services/web/frontend/js/utils/client-id.ts @@ -0,0 +1,7 @@ +import { v4 as uuid } from 'uuid' + +// Wrap uuid in an object method so that it can be stubbed +const clientId = uuid() +export default { + get: () => clientId, +} diff --git a/services/web/test/frontend/features/chat/context/chat-context.test.tsx b/services/web/test/frontend/features/chat/context/chat-context.test.tsx index 550cef7c0a..f392842baa 100644 --- a/services/web/test/frontend/features/chat/context/chat-context.test.tsx +++ b/services/web/test/frontend/features/chat/context/chat-context.test.tsx @@ -5,9 +5,9 @@ import { renderHook, act, waitFor } from '@testing-library/react' import { expect } from 'chai' import sinon from 'sinon' import fetchMock from 'fetch-mock' +import clientIdGenerator from '@/utils/client-id' import { useChatContext, - chatClientIdGenerator, ServerMessageEntry, } from '@/features/chat/context/chat-context' import { stubMathJax, tearDownMathJaxStubs } from '../components/stubs' @@ -35,7 +35,7 @@ describe('ChatContext', function () { window.metaAttributesCache.set('ol-user', user) window.metaAttributesCache.set('ol-preventCompileOnLoad', true) - this.stub = sinon.stub(chatClientIdGenerator, 'generate').returns(uuidValue) + this.stub = sinon.stub(clientIdGenerator, 'get').returns(uuidValue) }) afterEach(function () {