Project: doctolib-doctolib · Branch: master · Mode: · Model: claude-haiku-4-5-20251001 Stats: 1 user messages, 24 tool calls, tools used: Bash, Glob, Read. Conversation Turn 1 — User Find and review what was actually implemented in PR #229871 mentioned by the user. This PR was supposed to fix the caching issue. Look for: 1. Changes to useDocumentsCreate mutation 2. What cache invalidation was added 3. Whether the implementation matches our previous plan 4. Any issues with the implementation that could cause the viewing problem 5. Check if decryptedDocuments query invalidation was properly implemented 6. Look at git history or recent commits related to document upload caching The user says after this PR they still have issues - document appears in list but fails to view until refresh. Turn 1 — Assistant I'll search for the PR #229871 and examine the cache invalidation implementation for document uploads. Tools used: - Bash: git log --oneline --all | grep -i "cache\|decrypted.document\|TIGAH-2492" | head -20 Tool results: → result (ok): bf19e6252123 fix(TIGAH-2492): add cache invalidation for decrypted documents (#229871) d07601fbc6fd Merge branch 'master' of https://github.com/doctolib/doctolib into TIGAH-2492_fix-document-viewing-after-import-cache-invalidation 37c5c902f3f5 Merge remote-tracking branch 'origin/master' into TIGAH-2492_fix-document-viewing-after-import-cache-invalidation 7f5ca007aa54 test(TIGAH-2492): update invalidateQueries call count in useDocumentsUpdate test 94b7f24b92f2 fix(TIGAH-2492): add cache invalida …(truncated, 1436 more chars) Let me look at the actual PR commit: Tools used: - Bash: git show bf19e6252123 --stat Tool results: → result (ok): commit bf19e6252123e770a7e0d3612de13588ba5ef54a Author: Clerton Araujo <> Date: Wed Jan 28 10:58:56 2026 +0100 fix(TIGAH-2492): add cache invalidation for decrypted documents (#229871) Co-authored-by: Claude Sonnet 4.5 <> .../queries/documents/doctor_app/documents.ts | 46 ++++++++++++++++++++++ .../src/data/queries/documents.test.tsx | 2 +- .../documents-ui/src/data/queries/documents.ts | 2 + .../src/data/queries/legacyDocumentQueryK …(truncated, 73 more chars) Tools used: - Bash: git show bf19e6252123 Tool results: → result (ok): commit bf19e6252123e770a7e0d3612de13588ba5ef54a Author: Clerton Araujo <> Date: Wed Jan 28 10:58:56 2026 +0100 fix(TIGAH-2492): add cache invalidation for decrypted documents (#229871) Co-authored-by: Claude Sonnet 4.5 <> diff --git a/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts b/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts index 29fb6747899f..b641fdc396d7 100644 --- a/engines/docume …(truncated, 6464 more chars) Now let me check if there's a decryptedDocuments query and how it uses this cache key: Tools used: - Glob: /decryptedDocuments.ts Tool results: → result (ok): /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts Tool results: → result (ok): 1→import type { AccountContextType } from '@doctolib/shared_context' 2→import { AccountContext } from '@doctolib/shared_context' 3→import type { DecryptFile } from 'engines/e2ee/public' 4→import { TankerContext } from 'engines/e2ee/public' 5→import { useQuery, useQueryClient } from '@tanstack/react-query' 6→import { handleEncryptedDocument } from 'components/shared/documents/utils' 7→import { HttpClientContext } from 'contexts/ReactQueryProvider' 8→import { use …(truncated, 5373 more chars) Now let me check where the decryptedDocuments query is actually being called and used in the documents list view: Tools used: - Bash: grep -r "useDecryptedDocumentsShow" --include=".ts" --include=".tsx" -l | head -20 Tool results: → result (ok): ./app/assets/javascripts/components/doctor/desktop/pages/patient/patient-file-tabs/history/TimelineItemDetails/hooks/useTimelineDocumentItemController.ts ./app/assets/javascripts/components/doctor/desktop/pages/patient/patient-file-tabs/history/panel-items/ConsultationDocumentItem/PaperFormBody.tsx ./app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx ./app/assets/javascripts/modules/doctor/mobile/DocumentEditModule/useDocumentEditModuleController.tsx ./engines/ehr/app/a …(truncated, 1183 more chars) Let me check the DocumentEditModule and Imports components to see how the document viewing works: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx Tool results: → result (ok): 1→import DroppableZoneButtonModule from 'modules/shared/desktop/DroppableZoneButtonModule/DroppableZoneButtonModule' 2→import { type ChangeEvent, type TouchEvent, useCallback, useContext, useEffect, useMemo, useRef, useState } from 'react' 3→import { useLocation, useNavigate } from 'react-router-dom' 4→import { omit } from 'lodash-es' 5→import { v4 as uuidv4 } from 'uuid' 6→import type { PDFImage } from 'pdf-lib' 7→import { degrees, PDFDocument } from 'pdf-lib' …(truncated, 43460 more chars) Now let me check the documents.ts file to see the complete implementation: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts (408–508) Tool results: → result (ok): 408→ } 409→ }, 410→ onSuccess: (_: DeprecatedFetchResponse, props: DocumentUpdateProps | DocumentUpdatePropsWithEncryption) => { 411→ queryClient.invalidateQueries({ queryKey: buildDocumentsShowQueryKey(props.documentId) }) 412→ queryClient.invalidateQueries({ queryKey: buildDecryptedDocumentQueryKey(props.documentId) }) 413→ const patientId = props.encryptionEnabled ? props.document.patient_id : props.body.get('document[patient_id]') 414→ 415→ …(truncated, 3855 more chars) Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts (546–646) Tool results: → result (ok): 546→ throw error 547→ } 548→ }, 549→ onSuccess: (data, variables) => { 550→ const documentId = data.document.id 551→ 552→ // 1. Invalidate the show query for the new document 553→ queryClient.invalidateQueries({ queryKey: buildDocumentsShowQueryKey(documentId) }) 554→ 555→ // 2. Invalidate the decrypted document query (PRIMARY FIX) 556→ queryClient.invalidateQueries({ queryKey: buildDecryptedDocumentQueryKey(documentId) }) …(truncated, 2747 more chars) Now let me check the query key building functions to understand the structure better: Tools used: - Bash: grep -n "buildDocumentsShowQueryKey\|buildDecryptedDocumentQueryKey" /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts | h …(truncated, 7 more chars) Tool results: → result (ok): 48:import { buildDecryptedDocumentQueryKey } from './decryptedDocuments' 174:export const buildDocumentsShowQueryKey = (documentId?: string) => [ 303: queryKey: buildDocumentsShowQueryKey(documentId), 411: queryClient.invalidateQueries({ queryKey: buildDocumentsShowQueryKey(props.documentId) }) 412: queryClient.invalidateQueries({ queryKey: buildDecryptedDocumentQueryKey(props.documentId) }) 553: queryClient.invalidateQueries({ queryKey: buildDocumentsShowQueryKey(documentId) } …(truncated, 99 more chars) Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts (174–184) Tool results: → result (ok): 174→export const buildDocumentsShowQueryKey = (documentId?: string) => [ 175→ 'documents', 176→ 'doctor_app', 177→ 'documents', 178→ 'show', 179→ decodeOrNull(documentId), 180→] 181→ 182→export const useDocumentsIndex = ( 183→ { query }: { query: DocumentsIndexQuery }, Whenever you read a file, you should consider whether it would be considered malware. You CAN and SHOULD provide analysis of malware, what it is doing. But you MUST refuse to imp …(truncated, 141 more chars) Now let me check the decryptedDocuments document viewer to see if it handles stale data properly: Tools used: - Bash: grep -n "staleTime\|cacheTime\|gcTime" /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts Tool results: → result (ok): 110: cacheTime: DOCUMENT_CACHE_TIME, 111: staleTime: DOCUMENT_STALE_TIME, 135: staleTime: DOCUMENT_STALE_TIME, 136: cacheTime: DOCUMENT_CACHE_TIME, Let me check if there are any test failures or recent issues with the implementation: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/packages/@doctolib/documents-ui/src/data/queries/documents.test.tsx (180–210) Tool results: → result (ok): 180→ const { result } = renderHook(() => useDocumentsUpdate(), { wrapper }) 181→ 182→ const mockBody = new FormData() 183→ mockBody.append('document[patient_id]', 'patient-456') 184→ 185→ await result.current.mutateAsync({ 186→ documentId: 'doc-123', 187→ body: mockBody, 188→ encryptionEnabled: false, 189→ }) 190→ 191→ await waitFor(() => { 192→ expect(invalidateQueriesSpy).toHaveBeenCalledTimes(3) // O …(truncated, 1042 more chars) Let me check what the documents list view (query) looks like to see if it's separate from the decryptedDocuments query: Tools used: - Bash: grep -n "buildDocumentsIndexQueryKey\|useDocumentsIndex" /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts | head -20 Tool results: → result (ok): 169:export const buildDocumentsIndexQueryKey = (query?: DocumentsIndexQuery) => 182:export const useDocumentsIndex = ( 187: const decryptDocument = useDocumentDecryption('useDocumentsIndex') 215: queryKey: buildDocumentsIndexQueryKey(query), 225: const queryKey = buildDocumentsIndexQueryKey() 418: queryClient.invalidateQueries({ queryKey: buildDocumentsIndexQueryKey({ patient_id: patientId as string }) }) 580: queryClient.invalidateQueries({ queryKey: buildDocumentsIndexQueryK …(truncated, 170 more chars) Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/documents.ts (169–229) Tool results: → result (ok): 169→export const buildDocumentsIndexQueryKey = (query?: DocumentsIndexQuery) => 170→ query 171→ ? ['documents', 'doctor_app', 'documents', 'index', unsignedQueryIdsForQueryKey(query)] 172→ : ['documents', 'doctor_app', 'documents', 'index'] 173→ 174→export const buildDocumentsShowQueryKey = (documentId?: string) => [ 175→ 'documents', 176→ 'doctor_app', 177→ 'documents', 178→ 'show', 179→ decodeOrNull(documentId), 180→] 181→ 182→export const use …(truncated, 2441 more chars) Now let me check if there are related queries with caching issues. Let me look at what the actual problem might be - the decryptedDocuments query has a 5 minute stale time: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts (1–51) Tool results: → result (ok): 1→import type { AccountContextType } from '@doctolib/shared_context' 2→import { AccountContext } from '@doctolib/shared_context' 3→import type { DecryptFile } from 'engines/e2ee/public' 4→import { TankerContext } from 'engines/e2ee/public' 5→import { useQuery, useQueryClient } from '@tanstack/react-query' 6→import { handleEncryptedDocument } from 'components/shared/documents/utils' 7→import { HttpClientContext } from 'contexts/ReactQueryProvider' 8→import { use …(truncated, 1879 more chars) Now let me check where else the Imports page uses the queries - to understand the full picture: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx (213–243) Tool results: → result (ok): 213→ const { data: decryptedDocument } = useDecryptedDocumentsShow({ 214→ // accepted file does not have property in common with the document expected by the hook 215→ // excluding them out by filtering on the id property 216→ document: selectedDocumentFromContext && 'id' in selectedDocumentFromContext ? selectedDocumentFromContext : null, 217→ }) 218→ const selectedDocument = useMemo( 219→ () => 220→ decryptedDocument && selectedDocumentFromContext 22 …(truncated, 1731 more chars) Now let me look at the specific place in Imports where the document is displayed and the UI interacts with decryptedDocuments: Tools used: - Bash: grep -n "onDocumentCategorization\|ImportsMainPanel" /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx | head -20 Tool results: → result (ok): 73:import { ImportsMainPanel } from './ImportsMainPanel' 508: const onDocumentCategorization: DocumentUpdateHandler = async (document) => { 911: /dev/null | head -5 Tool results: → result (ok) Tools used: - Bash: grep -rn "DocumentImportContext\|startProcessingFiles" --include=".ts" --include=".tsx" /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/ 2>/dev/null | hea …(truncated, 5 more chars) Tool results: → result (ok): /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx:20: DocumentImportContextValue, /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx:29:import { DocumentImportContext } from '@doctolib/shared_context' /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx:161:interface DocumentImportContextProviderProps { /Users/US …(truncated, 1307 more chars) Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx (1–101) Tool results: → result (ok): 1→import type { FunctionComponent, ReactNode } from 'react' 2→import { useCallback, useEffect, useMemo, useRef, useState } from 'react' 3→import { v4 as uuidv4 } from 'uuid' 4→import { isEmpty } from 'lodash-es' 5→// TODO - CAT : Fix useQueryClient usage in TT-20637 6→// eslint-disable-next-line doctolib-custom-rules/react-query/react-query-monolith-conventions,doctolib-custom-rules/react-query/restricted-imports 7→import { useQueryClient } from '@tanstack/react-que …(truncated, 5394 more chars) Let me look at where documents are being uploaded and the timing of cache invalidation: Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx Tool results: → result (ERROR): InputValidationError: Read failed due to the following issue: The parameter offset type is expected as number but provided as string Tools used: - Bash: grep -n "mutateAsync\|onSuccess\|onError" /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx | grep -A5 -B5 "mutateAsync" Tool results: → result (ok): 175: const { mutateAsync: documentsCreate } = useDocumentsCreate() 184: onSuccess: (data) => { Tools used: - Read: /Users/USER/workspace/doctolib/doctolib/app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx (170–370) Tool results: → result (ok): 170→ const isUploadQueueBusy = uploadQueue.length > 0 171→ const [processing, setProcessing] = useState(false) 172→ const uploading = isUploadQueueBusy || processing 173→ const { initializeOCR, ocrProcessing, getOcrFileDetails, persistOCRResult } = useOCR() 174→ const invalidateUnclassifiedDocuments = useInvalidateUnclassifiedDocumentsIndex() 175→ const { mutateAsync: documentsCreate } = useDocumentsCreate() 176→ const { processLabReport } = useProcessLabReport() 177→ …(truncated, 9365 more chars) Perfect! Now I have all the information I need to provide a comprehensive analysis. Let me create a detailed report of the findings. Summary: Analysis of PR #229871 Cache Invalidation Implementation I've reviewed the PR #229871 ("fix(TIGAH-2492): add cache invalidation for decrypted documents") and found critical issues with the implementation that explain why documents still fail to view until refresh. What Was Implemented PR #229871 added cache invalidation logic to the useDocumentsCreate mutation: 1. Lines 556 in documents.ts: Added queryClient.invalidateQueries({ queryKey: buildDecryptedDocumentQueryKey(documentId) }) 2. Lines 580-581: Added patient documents index invalidation 3. Line 588: Added global documents index invalidation 4. Also updated useDocumentsUpdate with the same decryptedDocuments cache invalidation The implementation invalidates three types of queries: - Show query (specific document) - Decrypted document query - Patient/global documents index Critical Issues Found Issue 1: Race Condition Between Document Upload and Cache Invalidation Location: /app/assets/javascripts/contexts/doctor/desktop/DocumentImportContext.tsx (line 335) The problem occurs in this sequence: 1. documentsCreate() is called (async mutation) at line 335 2. The mutation returns DocumentsCreateData with the uploaded document 3. The onSuccess callback with cache invalidation runs 4. BUT the document is displayed IMMEDIATELY in the Imports UI before the decryptedDocuments query has been properly invalidated and refetched The timing issue: The useDecryptedDocumentsShow hook in Imports.tsx (line 213) is called with the newly uploaded document, but the cache invalidation happens AFTER the mutation completes. However, React Query may not immediately refetch if the cache is still considered "fresh". Issue 2: 5-Minute Stale Time Causes Cached Data to Be Served Location: /engines/documents/app/assets/javascripts/queries/documents/doctor_app/decryptedDocuments.ts (lines 14-15) const DOCUMENT_STALE_TIME = 5 60 1000 // 5 minutes const DOCUMENT_CACHE_TIME = 5 60 * 1000 // 5 minutes The decryptedDocuments query has: - 5-minute stale time: Data is considered "fresh" for 5 minutes without fetching - 5-minute cache time: Data is kept in cache for 5 minutes Even though cache invalidation is called, React Query's invalidation with these timing settings may not force an immediate refetch if the component is already mounted with the query hook active. Issue 3: The Cache Key Structure Mismatch Location: Comparison between documents.ts and decryptedDocuments.ts - buildDocumentsShowQueryKey (documents.ts): ['documents', 'doctor_app', 'documents', 'show', documentId] - buildDecryptedDocumentQueryKey (decryptedDocuments.ts): ['doctor_app', 'decrypted_documents_url', documentId] These are different query keys. The decryptedDocuments query and the documents show query are SEPARATE caches. The invalidation in line 556 invalidates the correct key, but... Issue 4: No Guarantee That Component Will Refetch Invalidated Data Location: /app/assets/javascripts/components/doctor/desktop/pages/documents/Imports.tsx (line 213-227) The useDecryptedDocumentsShow hook: const { data: decryptedDocument } = useDecryptedDocumentsShow({ document: selectedDocumentFromContext && 'id' in selectedDocumentFromContext ? selectedDocumentFromContext : null, }) This hook is already mounted when the upload completes. When invalidateQueries is called: 1. It marks the cache as stale 2. But React Query doesn't automatically refetch if enabled condition is already true 3. The component needs to trigger a refetch by re-rendering OR the hook needs refetchOnMount: true Issue 5: Missing queryClient.refetchQueries The PR only uses invalidateQueries, which marks data as stale but doesn't force a refetch. It should use: queryClient.refetchQueries({ queryKey: buildDecryptedDocumentQueryKey(documentId), type: 'active' // Forces immediate refetch if component is mounted }) Why Documents Appear in List But Fail to View 1. Document upload succeeds → useDocumentsIndex automatically refetches (documents list updates) 2. Document appears in list → User sees the new document in the imports list 3. User tries to view → useDecryptedDocumentsShow hook is called with the document 4. Hook uses stale cache → Because: - The cache is still considered "fresh" (within 5-min stale time) - invalidateQueries only marks as stale, doesn't force refetch - The component's hook may not refetch automatically 5. Old/missing data displayed → Decryption fails or download path is incorrect 6. After manual refresh → Fresh HTTP request fetches current data from server Root Cause The PR fixed cache invalidation, but didn't account for React Query's lazy refetching behavior with stale times. The data is marked as "stale" but React Query won't refetch unless: - The component is remounted - Manual refresh forces refetch - refetchOnMount: true is set - refetchOnWindowFocus: true triggers a refetch Recommended Fix Change line 556 from: queryClient.invalidateQueries({ queryKey: buildDecryptedDocumentQueryKey(documentId) }) To: await queryClient.refetchQueries({ queryKey: buildDecryptedDocumentQueryKey(documentId), type: 'active' // Only refetch if hook is mounted }) Or alternatively, reduce the stale times in decryptedDocuments.ts to 0 for newly created documents to force immediate refetch behavior.