From 2b7191d93abceff404a14d81896d7f28ef3fa3c5 Mon Sep 17 00:00:00 2001 From: Leonie Dickson Date: Mon, 11 May 2026 15:18:33 +1000 Subject: [PATCH 1/3] Retry population once on first attempt failure Handles transient server slowness (e.g. cold HAPI caches on first use of the day) by automatically retrying a failed populate rather than immediately showing "Form not populated" to the user. --- .../prepopulate/hooks/usePopulate.tsx | 14 ++++++- .../prepopulate/test/usePopulate.test.tsx | 37 +++++++++++++++++++ 2 files changed, 49 insertions(+), 2 deletions(-) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index d4cf0118b..9ffdc1514 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -80,7 +80,8 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void } setIsPopulated(true); - populateQuestionnaire({ + + const populateParams = { questionnaire: sourceQuestionnaire, fetchResourceCallback: fetchResourceCallback, fetchResourceRequestConfig: { @@ -96,7 +97,16 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void fetchTerminologyRequestConfig: { terminologyServerUrl: defaultTerminologyServerUrl } - }) + }; + + populateQuestionnaire(populateParams) + .then((populateRes) => { + // Retry once if the first attempt fails - handles transient server slowness on first use + if (!populateRes?.populateSuccess || !populateRes?.populateResult) { + return populateQuestionnaire(populateParams); + } + return populateRes; + }) .then(async (populateRes) => { if (!populateRes) { onStopSpinner(); diff --git a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx index c3f89a0fe..772e0dadf 100644 --- a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx @@ -549,6 +549,43 @@ describe('usePopulate', () => { }); }); + it('should retry once and succeed if first attempt fails', async () => { + const spinner = createSpinner(true, 'prepopulate'); + mockPopulateQuestionnaire + .mockResolvedValueOnce({ populateSuccess: false, populateResult: null }) + .mockResolvedValueOnce({ + populateSuccess: true, + populateResult: { populatedResponse: mockResponse } + }); + + renderHook(() => usePopulate(spinner, mockOnStopSpinner)); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(mockPopulateQuestionnaire).toHaveBeenCalledTimes(2); + expect(mockBuildForm).toHaveBeenCalled(); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Form populated', { + preventDuplicate: true, + action: expect.anything() + }); + }); + + it('should show failure message if both attempts fail', async () => { + const spinner = createSpinner(true, 'prepopulate'); + mockPopulateQuestionnaire.mockResolvedValue({ populateSuccess: false, populateResult: null }); + + renderHook(() => usePopulate(spinner, mockOnStopSpinner)); + + await new Promise((resolve) => setTimeout(resolve, 10)); + + expect(mockPopulateQuestionnaire).toHaveBeenCalledTimes(2); + expect(mockBuildForm).not.toHaveBeenCalled(); + expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Form not populated', { + variant: 'warning', + action: expect.anything() + }); + }); + it('should handle missing populate result', async () => { const spinner = createSpinner(true, 'prepopulate'); mockPopulateQuestionnaire.mockResolvedValue({ From 269db1da888bd61c11f6c91ebbede5350aea0553 Mon Sep 17 00:00:00 2001 From: Leonie Dickson Date: Thu, 14 May 2026 11:23:06 +1000 Subject: [PATCH 2/3] Retry population once on first attempt failure Increases the populate timeout to 30 seconds to accommodate HAPI server warm-up on first use of the day, avoiding the 10-second timeout that caused silent population failures requiring manual retry. Co-Authored-By: Claude Sonnet 4.6 --- .../prepopulate/hooks/usePopulate.tsx | 16 ++------ .../prepopulate/test/usePopulate.test.tsx | 40 +------------------ 2 files changed, 6 insertions(+), 50 deletions(-) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index 9ffdc1514..4e1391a69 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -81,7 +81,7 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void setIsPopulated(true); - const populateParams = { + populateQuestionnaire({ questionnaire: sourceQuestionnaire, fetchResourceCallback: fetchResourceCallback, fetchResourceRequestConfig: { @@ -96,17 +96,9 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void fetchTerminologyCallback: fetchTerminologyCallback, fetchTerminologyRequestConfig: { terminologyServerUrl: defaultTerminologyServerUrl - } - }; - - populateQuestionnaire(populateParams) - .then((populateRes) => { - // Retry once if the first attempt fails - handles transient server slowness on first use - if (!populateRes?.populateSuccess || !populateRes?.populateResult) { - return populateQuestionnaire(populateParams); - } - return populateRes; - }) + }, + timeoutMs: 30000 + }) .then(async (populateRes) => { if (!populateRes) { onStopSpinner(); diff --git a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx index 772e0dadf..80322611e 100644 --- a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx @@ -317,7 +317,8 @@ describe('usePopulate', () => { fetchTerminologyCallback: expect.any(Function), fetchTerminologyRequestConfig: { terminologyServerUrl: 'https://test-terminology-server.com' - } + }, + timeoutMs: 30000 }); }); @@ -549,43 +550,6 @@ describe('usePopulate', () => { }); }); - it('should retry once and succeed if first attempt fails', async () => { - const spinner = createSpinner(true, 'prepopulate'); - mockPopulateQuestionnaire - .mockResolvedValueOnce({ populateSuccess: false, populateResult: null }) - .mockResolvedValueOnce({ - populateSuccess: true, - populateResult: { populatedResponse: mockResponse } - }); - - renderHook(() => usePopulate(spinner, mockOnStopSpinner)); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(mockPopulateQuestionnaire).toHaveBeenCalledTimes(2); - expect(mockBuildForm).toHaveBeenCalled(); - expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Form populated', { - preventDuplicate: true, - action: expect.anything() - }); - }); - - it('should show failure message if both attempts fail', async () => { - const spinner = createSpinner(true, 'prepopulate'); - mockPopulateQuestionnaire.mockResolvedValue({ populateSuccess: false, populateResult: null }); - - renderHook(() => usePopulate(spinner, mockOnStopSpinner)); - - await new Promise((resolve) => setTimeout(resolve, 10)); - - expect(mockPopulateQuestionnaire).toHaveBeenCalledTimes(2); - expect(mockBuildForm).not.toHaveBeenCalled(); - expect(mockEnqueueSnackbar).toHaveBeenCalledWith('Form not populated', { - variant: 'warning', - action: expect.anything() - }); - }); - it('should handle missing populate result', async () => { const spinner = createSpinner(true, 'prepopulate'); mockPopulateQuestionnaire.mockResolvedValue({ From ba22ba3371e2ab90c340d06f0547fcd9797fc3cf Mon Sep 17 00:00:00 2001 From: Leonie Dickson Date: Tue, 19 May 2026 11:32:51 +1000 Subject: [PATCH 3/3] increase population timeoutMs default in sdc-populate --- .../src/features/prepopulate/hooks/usePopulate.tsx | 3 +-- .../src/features/prepopulate/test/usePopulate.test.tsx | 3 +-- .../src/inAppPopulation/utils/populateQuestionnaire.ts | 4 ++-- 3 files changed, 4 insertions(+), 6 deletions(-) diff --git a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx index 4e1391a69..943bab3c2 100644 --- a/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/hooks/usePopulate.tsx @@ -96,8 +96,7 @@ function usePopulate(spinner: RendererSpinner, onStopSpinner: () => void): void fetchTerminologyCallback: fetchTerminologyCallback, fetchTerminologyRequestConfig: { terminologyServerUrl: defaultTerminologyServerUrl - }, - timeoutMs: 30000 + } }) .then(async (populateRes) => { if (!populateRes) { diff --git a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx index 80322611e..c3f89a0fe 100644 --- a/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx +++ b/apps/smart-forms-app/src/features/prepopulate/test/usePopulate.test.tsx @@ -317,8 +317,7 @@ describe('usePopulate', () => { fetchTerminologyCallback: expect.any(Function), fetchTerminologyRequestConfig: { terminologyServerUrl: 'https://test-terminology-server.com' - }, - timeoutMs: 30000 + } }); }); diff --git a/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts b/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts index 456a7aea1..7cda21e33 100644 --- a/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts +++ b/packages/sdc-populate/src/inAppPopulation/utils/populateQuestionnaire.ts @@ -60,7 +60,7 @@ export interface PopulateResult { * @property fhirContext - An array of contextual resources within a launch. See https://build.fhir.org/ig/HL7/smart-app-launch/scopes-and-launch-context.html#fhircontext-exp * @property fetchTerminologyCallback - A callback function to fetch terminology resources, optional * @property fetchTerminologyRequestConfig - Any request configuration to be passed to the fetchTerminologyCallback i.e. headers, auth etc., optional - * @property timeoutMs - Timeout in milliseconds for the $populate operation, default is 10000ms (10 seconds) + * @property timeoutMs - Timeout in milliseconds for the $populate operation, default is 30000ms (30 seconds) * * @author Sean Fong */ @@ -103,7 +103,7 @@ export async function populateQuestionnaire(params: PopulateQuestionnaireParams) fhirContext, fetchTerminologyCallback, fetchTerminologyRequestConfig, - timeoutMs = 10000 + timeoutMs = 30000 } = params; const { inputParameters } = await initialiseInputParameters(