From dd7a41baeab65406e0a198bcb5b470af98092f45 Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 08:37:21 +0200 Subject: [PATCH 1/4] fix(defaultRouteInit): defer prior fetches and guard study browser thumbnails --- .../Panels/StudyBrowser/PanelStudyBrowser.tsx | 14 +- .../app/src/routes/Mode/defaultRouteInit.ts | 202 +++++++++++++++--- 2 files changed, 189 insertions(+), 27 deletions(-) diff --git a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx index c91e8240685..c6e5a28ae20 100644 --- a/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx +++ b/extensions/default/src/Panels/StudyBrowser/PanelStudyBrowser.tsx @@ -521,11 +521,23 @@ function _getComponentType(ds) { } function getImageIdForThumbnail(displaySet, imageIds) { + if (!Array.isArray(imageIds) || imageIds.length === 0) { + return; + } + let imageId; if (displaySet.isDynamicVolume) { - const timePoints = displaySet.dynamicVolumeInfo.timePoints; + const timePoints = displaySet.dynamicVolumeInfo?.timePoints; + if (!Array.isArray(timePoints) || timePoints.length === 0) { + return; + } + const middleIndex = Math.floor(timePoints.length / 2); const middleTimePointImageIds = timePoints[middleIndex]; + if (!Array.isArray(middleTimePointImageIds) || middleTimePointImageIds.length === 0) { + return; + } + imageId = middleTimePointImageIds[Math.floor(middleTimePointImageIds.length / 2)]; } else { imageId = imageIds[Math.floor(imageIds.length / 2)]; diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index aa98b4ed3e3..6b9e35dc3b5 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -4,6 +4,105 @@ import isSeriesFilterUsed from '../../utils/isSeriesFilterUsed'; const { getSplitParam } = utils; +/** + * Gets all studies for a patient by MRN from the first study. + */ +async function getStudiesForPatientByMRN(dataSource, qidoForStudyUID) { + const mrn = qidoForStudyUID[0]?.mrn; + if (!mrn) { + return qidoForStudyUID; + } + + return dataSource.query.studies.search({ patientId: mrn, disableWildcard: true }); +} + +function normalizeModalities(modalities) { + if (Array.isArray(modalities)) { + return modalities.filter(Boolean); + } + + if (typeof modalities === 'string') { + return modalities.split('\\').filter(Boolean); + } + + return []; +} + +function upsertStudyMetadata(studyMetadata) { + const existingStudy = DicomMetadataStore.getStudy(studyMetadata.StudyInstanceUID); + + if (!existingStudy) { + DicomMetadataStore.addStudy(studyMetadata); + return; + } + + const mergedModalities = Array.from( + new Set([ + ...normalizeModalities(existingStudy.ModalitiesInStudy), + ...normalizeModalities(studyMetadata.ModalitiesInStudy), + ]) + ); + + Object.assign(existingStudy, { + PatientID: studyMetadata.PatientID ?? existingStudy.PatientID, + PatientName: studyMetadata.PatientName ?? existingStudy.PatientName, + StudyDate: studyMetadata.StudyDate ?? existingStudy.StudyDate, + StudyTime: studyMetadata.StudyTime ?? existingStudy.StudyTime, + StudyDescription: studyMetadata.StudyDescription ?? existingStudy.StudyDescription, + ModalitiesInStudy: mergedModalities, + AccessionNumber: studyMetadata.AccessionNumber ?? existingStudy.AccessionNumber, + NumInstances: studyMetadata.NumInstances ?? existingStudy.NumInstances, + }); +} + +/** + * Fetches all studies for a patient by MRN from the first study + * and adds them to the DICOM metadata store. + */ +async function fetchAndStorePatientStudies(studyInstanceUID: string, dataSource) { + try { + const qidoForStudyUID = await dataSource.query.studies.search({ + studyInstanceUid: studyInstanceUID, + }); + + if (!qidoForStudyUID?.length) { + console.warn('Could not find study:', studyInstanceUID); + return []; + } + + let qidoStudiesForPatient = qidoForStudyUID; + try { + qidoStudiesForPatient = await getStudiesForPatientByMRN(dataSource, qidoForStudyUID); + } catch (error) { + console.warn('Could not fetch patient studies by MRN:', error); + } + + const storedStudyUIDs = []; + + qidoStudiesForPatient.forEach(study => { + const studyMetadata = { + StudyInstanceUID: study.studyInstanceUid, + PatientID: study.mrn, + PatientName: study.patientName, + StudyDate: study.date, + StudyTime: study.time, + StudyDescription: study.description, + ModalitiesInStudy: normalizeModalities(study.modalities), + AccessionNumber: study.accession, + NumInstances: study.instances, + }; + + upsertStudyMetadata(studyMetadata); + storedStudyUIDs.push(studyMetadata.StudyInstanceUID); + }); + + return storedStudyUIDs; + } catch (error) { + console.error('Error fetching patient studies:', error); + return []; + } +} + /** * Initialize the route. * @@ -87,10 +186,20 @@ export async function defaultRouteInit( unsubscriptions.push(instanceAddedUnsubscribe); + const firstStudyUID = studyInstanceUIDs?.[0]; + const activeStudyUIDs = studyInstanceUIDs?.length + ? studyInstanceUIDs + : firstStudyUID + ? [firstStudyUID] + : []; + const patientStudiesPromise = firstStudyUID + ? fetchAndStorePatientStudies(firstStudyUID, dataSource) + : Promise.resolve([]); + log.time(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); log.time(Enums.TimingEnum.STUDY_TO_FIRST_IMAGE); - const allRetrieves = studyInstanceUIDs.map(StudyInstanceUID => + const allRetrieves = activeStudyUIDs.map(StudyInstanceUID => dataSource.retrieve.series.metadata({ StudyInstanceUID, filters, @@ -118,43 +227,84 @@ export async function defaultRouteInit( displaySetFromUrl = true; } - await Promise.allSettled(allRetrieves).then(async promises => { - log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); - log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE); - log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); + function startRemainingPromises(remainingPromises) { + remainingPromises.forEach(p => p.forEach(promise => promise.start())); + } - const allPromises = []; + async function collectSeriesPromises(retrieves) { + const settledRetrieves = await Promise.allSettled(retrieves); + const requiredSeriesPromises = []; const remainingPromises = []; - function startRemainingPromises(remainingPromises) { - remainingPromises.forEach(p => p.forEach(p => p.start())); - } - - promises.forEach(promise => { - const retrieveSeriesMetadataPromise = promise.value; - if (!Array.isArray(retrieveSeriesMetadataPromise)) { + settledRetrieves.forEach(retrieve => { + if (retrieve.status !== 'fulfilled' || !Array.isArray(retrieve.value)) { return; } if (displaySetFromUrl) { - const requiredSeriesPromises = retrieveSeriesMetadataPromise.map(promise => - promise.start() - ); - allPromises.push(Promise.allSettled(requiredSeriesPromises)); - } else { - const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun( - hangingProtocolId, - retrieveSeriesMetadataPromise - ); - const requiredSeriesPromises = requiredSeries.map(promise => promise.start()); - allPromises.push(Promise.allSettled(requiredSeriesPromises)); - remainingPromises.push(remaining); + requiredSeriesPromises.push(...retrieve.value.map(promise => promise.start())); + return; } + + const { requiredSeries, remaining } = hangingProtocolService.filterSeriesRequiredForRun( + hangingProtocolId, + retrieve.value + ); + + requiredSeriesPromises.push(...requiredSeries.map(promise => promise.start())); + remainingPromises.push(remaining); }); - await Promise.allSettled(allPromises).then(applyHangingProtocol); + return { requiredSeriesPromises, remainingPromises }; + } + + async function startPriorFetches() { + const patientStudyUIDs = Array.from(new Set(await patientStudiesPromise)); + const activeStudyUIDSet = new Set(activeStudyUIDs); + const priorStudyUIDs = patientStudyUIDs.filter(uid => uid && !activeStudyUIDSet.has(uid)); + + if (!priorStudyUIDs.length) { + return; + } + + const priorRetrieves = priorStudyUIDs.map(StudyInstanceUID => + dataSource.retrieve.series.metadata({ + StudyInstanceUID, + filters, + returnPromises: true, + sortCriteria: customizationService.getCustomization('sortingCriteria'), + }) + ); + + priorRetrieves.forEach(retrieve => { + retrieve.catch(error => { + console.error(error); + }); + }); + + const { requiredSeriesPromises, remainingPromises } = + await collectSeriesPromises(priorRetrieves); + + await Promise.allSettled(requiredSeriesPromises); + applyHangingProtocol(); startRemainingPromises(remainingPromises); applyHangingProtocol(); + } + + const { requiredSeriesPromises, remainingPromises } = await collectSeriesPromises(allRetrieves); + + log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); + log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE); + log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); + + await Promise.allSettled(requiredSeriesPromises); + await patientStudiesPromise; + applyHangingProtocol(); + startRemainingPromises(remainingPromises); + applyHangingProtocol(); + + void startPriorFetches().catch(error => { + console.error(error); }); return unsubscriptions; From 78367048e94330d33ddf835fa0af12981a354c02 Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 09:00:46 +0200 Subject: [PATCH 2/4] fix(app): unblock initial hanging protocol run --- platform/app/src/routes/Mode/defaultRouteInit.ts | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index 6b9e35dc3b5..b8eca7b248b 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -187,11 +187,7 @@ export async function defaultRouteInit( unsubscriptions.push(instanceAddedUnsubscribe); const firstStudyUID = studyInstanceUIDs?.[0]; - const activeStudyUIDs = studyInstanceUIDs?.length - ? studyInstanceUIDs - : firstStudyUID - ? [firstStudyUID] - : []; + const activeStudyUIDs = studyInstanceUIDs?.length ? studyInstanceUIDs : []; const patientStudiesPromise = firstStudyUID ? fetchAndStorePatientStudies(firstStudyUID, dataSource) : Promise.resolve([]); @@ -288,7 +284,6 @@ export async function defaultRouteInit( await Promise.allSettled(requiredSeriesPromises); applyHangingProtocol(); startRemainingPromises(remainingPromises); - applyHangingProtocol(); } const { requiredSeriesPromises, remainingPromises } = await collectSeriesPromises(allRetrieves); @@ -298,10 +293,8 @@ export async function defaultRouteInit( log.time(Enums.TimingEnum.DISPLAY_SETS_TO_ALL_IMAGES); await Promise.allSettled(requiredSeriesPromises); - await patientStudiesPromise; applyHangingProtocol(); startRemainingPromises(remainingPromises); - applyHangingProtocol(); void startPriorFetches().catch(error => { console.error(error); From 3a748bb020f8ea36fbae4536cf4ea106a1b56c43 Mon Sep 17 00:00:00 2001 From: Denny Spiegelberg Date: Mon, 20 Apr 2026 11:46:08 +0200 Subject: [PATCH 3/4] fix(app): keep MRN study fallback when patient query returns empty --- platform/app/src/routes/Mode/defaultRouteInit.ts | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index b8eca7b248b..eab873c3d18 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -72,7 +72,8 @@ async function fetchAndStorePatientStudies(studyInstanceUID: string, dataSource) let qidoStudiesForPatient = qidoForStudyUID; try { - qidoStudiesForPatient = await getStudiesForPatientByMRN(dataSource, qidoForStudyUID); + qidoStudiesForPatient = + (await getStudiesForPatientByMRN(dataSource, qidoForStudyUID)) ?? qidoForStudyUID; } catch (error) { console.warn('Could not fetch patient studies by MRN:', error); } From 65c763d654fbe89032387681c3a07466155c9dac Mon Sep 17 00:00:00 2001 From: Spiegelberg-Adm Date: Thu, 30 Apr 2026 16:13:48 +0200 Subject: [PATCH 4/4] fix(app): limit URL display-set loading to active studies --- platform/app/src/routes/Mode/defaultRouteInit.ts | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/platform/app/src/routes/Mode/defaultRouteInit.ts b/platform/app/src/routes/Mode/defaultRouteInit.ts index eab873c3d18..40b7617c179 100644 --- a/platform/app/src/routes/Mode/defaultRouteInit.ts +++ b/platform/app/src/routes/Mode/defaultRouteInit.ts @@ -228,7 +228,7 @@ export async function defaultRouteInit( remainingPromises.forEach(p => p.forEach(promise => promise.start())); } - async function collectSeriesPromises(retrieves) { + async function collectSeriesPromises(retrieves, { includeDisplaySetFromUrl = false } = {}) { const settledRetrieves = await Promise.allSettled(retrieves); const requiredSeriesPromises = []; const remainingPromises = []; @@ -238,7 +238,7 @@ export async function defaultRouteInit( return; } - if (displaySetFromUrl) { + if (includeDisplaySetFromUrl && displaySetFromUrl) { requiredSeriesPromises.push(...retrieve.value.map(promise => promise.start())); return; } @@ -279,15 +279,18 @@ export async function defaultRouteInit( }); }); - const { requiredSeriesPromises, remainingPromises } = - await collectSeriesPromises(priorRetrieves); + const { requiredSeriesPromises, remainingPromises } = await collectSeriesPromises( + priorRetrieves + ); await Promise.allSettled(requiredSeriesPromises); applyHangingProtocol(); startRemainingPromises(remainingPromises); } - const { requiredSeriesPromises, remainingPromises } = await collectSeriesPromises(allRetrieves); + const { requiredSeriesPromises, remainingPromises } = await collectSeriesPromises(allRetrieves, { + includeDisplaySetFromUrl: true, + }); log.timeEnd(Enums.TimingEnum.STUDY_TO_DISPLAY_SETS); log.time(Enums.TimingEnum.DISPLAY_SETS_TO_FIRST_IMAGE);