Skip to content

Commit

Permalink
MHV-56686 handle first time data refresh (#29004)
Browse files Browse the repository at this point in the history
* MHV-56686 Added a retry-API function at the action level

* MHV-56686 Added SET_INITIAL_FHIR_LOAD

* MHV-56686 Handle displaying the records section in one place

* MHV-56686 Modify action creators to use the new API retry function

* MHV-56686 Removed old unit test

* MHV-56686 Unused imports

* MHV-56686 Added a spinner for initial FHIR data load

* MHV-56686 Added the special case for vitals

* MHV-56686 Reduce width of spinner

* MHV-56686 Added more unit tests

* MHV-56686 Fixed allergies no-records display

* MHV-56686 Changed the API-retry time to 2 minutes
  • Loading branch information
mmoyer-va authored Apr 10, 2024
1 parent f96735e commit 6572fc8
Show file tree
Hide file tree
Showing 22 changed files with 384 additions and 317 deletions.
3 changes: 2 additions & 1 deletion src/applications/mhv/medical-records/actions/allergies.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { getAllergies, getAllergy } from '../api/MrApi';
import * as Constants from '../util/constants';
import { addAlert } from './alerts';
import { dispatchDetails } from '../util/helpers';
import { getListWithRetry } from './common';

export const getAllergiesList = (isCurrent = false) => async dispatch => {
dispatch({
type: Actions.Allergies.UPDATE_LIST_STATE,
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getAllergies();
const response = await getListWithRetry(dispatch, getAllergies);
dispatch({
type: Actions.Allergies.GET_LIST,
response,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import { Actions } from '../util/actionTypes';
import { addAlert } from './alerts';
import * as Constants from '../util/constants';
import { dispatchDetails } from '../util/helpers';
import { getListWithRetry } from './common';

export const getCareSummariesAndNotesList = (
isCurrent = false,
Expand All @@ -12,7 +13,7 @@ export const getCareSummariesAndNotesList = (
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getNotes();
const response = await getListWithRetry(dispatch, getNotes);
dispatch({
type: Actions.CareSummariesAndNotes.GET_LIST,
response,
Expand Down
45 changes: 45 additions & 0 deletions src/applications/mhv/medical-records/actions/common.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
import { Actions } from '../util/actionTypes';

const defaultRetryInterval = 2000;
const defaultEndTimeOffset = 120000;

/**
* Helper function to create a delay
*/
const delay = ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};

/**
* Recursive function that will continue polling the provided API endpoint if it sends a 202 response.
* The backend returns a 202 if the patient record has not yet been created.
*
* @param {Function} dispatch the dispatch function
* @param {Function} getList the API function to poll
* @param {number} retryInterval how often to poll, e.g. 2000 for every two seconds
* @param {number} endTimeParam when to stop polling and return an error (milliseconds since epoch)
* @returns the response from the API function once it returns a 200 response
*/
export const getListWithRetry = async (
dispatch,
getList,
retryInterval = defaultRetryInterval,
endTimeParam = null,
) => {
const endTime =
endTimeParam === null ? Date.now() + defaultEndTimeOffset : endTimeParam;

if (Date.now() >= endTime) {
throw new Error('Timed out while waiting for response');
}

const response = await getList();
if (response?.status === 202) {
dispatch({ type: Actions.Refresh.SET_INITIAL_FHIR_LOAD });
if (Date.now() < endTime) {
await delay(retryInterval);
return getListWithRetry(dispatch, getList, retryInterval, endTime);
}
}
return response;
};
3 changes: 2 additions & 1 deletion src/applications/mhv/medical-records/actions/conditions.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { getConditions, getCondition } from '../api/MrApi';
import * as Constants from '../util/constants';
import { addAlert } from './alerts';
import { dispatchDetails } from '../util/helpers';
import { getListWithRetry } from './common';

export const getConditionsList = (isCurrent = false) => async dispatch => {
dispatch({
type: Actions.Conditions.UPDATE_LIST_STATE,
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getConditions();
const response = await getListWithRetry(dispatch, getConditions);
dispatch({
type: Actions.Conditions.GET_LIST,
response,
Expand Down
3 changes: 2 additions & 1 deletion src/applications/mhv/medical-records/actions/labsAndTests.js
Original file line number Diff line number Diff line change
Expand Up @@ -2,14 +2,15 @@ import { Actions } from '../util/actionTypes';
import { getLabsAndTests, getLabOrTest } from '../api/MrApi';
import * as Constants from '../util/constants';
import { addAlert } from './alerts';
import { getListWithRetry } from './common';

export const getLabsAndTestsList = (isCurrent = false) => async dispatch => {
dispatch({
type: Actions.LabsAndTests.UPDATE_LIST_STATE,
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getLabsAndTests();
const response = await getListWithRetry(dispatch, getLabsAndTests);
dispatch({
type: Actions.LabsAndTests.GET_LIST,
response,
Expand Down
3 changes: 2 additions & 1 deletion src/applications/mhv/medical-records/actions/vaccines.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { getVaccine, getVaccineList } from '../api/MrApi';
import * as Constants from '../util/constants';
import { addAlert } from './alerts';
import { dispatchDetails } from '../util/helpers';
import { getListWithRetry } from './common';

export const getVaccinesList = (isCurrent = false) => async dispatch => {
dispatch({
type: Actions.Vaccines.UPDATE_LIST_STATE,
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getVaccineList();
const response = await getListWithRetry(dispatch, getVaccineList);

dispatch({
type: Actions.Vaccines.GET_LIST,
Expand Down
3 changes: 2 additions & 1 deletion src/applications/mhv/medical-records/actions/vitals.js
Original file line number Diff line number Diff line change
Expand Up @@ -3,14 +3,15 @@ import { getVitalsList } from '../api/MrApi';
import * as Constants from '../util/constants';
import { addAlert } from './alerts';
import { isArrayAndHasItems } from '../util/helpers';
import { getListWithRetry } from './common';

export const getVitals = (isCurrent = false) => async dispatch => {
dispatch({
type: Actions.Vitals.UPDATE_LIST_STATE,
payload: Constants.loadStates.FETCHING,
});
try {
const response = await getVitalsList();
const response = await getListWithRetry(dispatch, getVitalsList);
dispatch({
type: Actions.Vitals.GET_LIST,
response,
Expand Down
105 changes: 21 additions & 84 deletions src/applications/mhv/medical-records/api/MrApi.js
Original file line number Diff line number Diff line change
Expand Up @@ -33,55 +33,6 @@ export const getRefreshStatus = () => {
});
};

/**
* Helper function to create a delay
*/
const delay = ms => {
return new Promise(resolve => setTimeout(resolve, ms));
};

/**
* Testable implementation.
* @see {@link apiRequestWithRetry} for more information
* @param {*} retryInterval how long to wait between requests
* @param {*} apiRequestFunc the API function to call; can be mocked for tests
* @returns
*/
export const testableApiRequestWithRetry = (
retryInterval,
apiRequestFunc,
) => async (path, options, endTime) => {
if (Date.now() >= endTime) {
throw new Error('Timed out while waiting for response');
}

const response = await apiRequestFunc(path, options);

// Check if the status code is 202 and if the retry time limit has not been reached
if (response?.status === 202 && Date.now() < endTime) {
await delay(retryInterval);
return testableApiRequestWithRetry(retryInterval, apiRequestFunc)(
path,
options,
endTime,
);
}

return response;
};

/**
* Recursive function that will continue polling the provided API endpoint if it sends a 404 response.
* At this time, we will only get a 404 if the patient record has not yet been created.
* @param {String} path the API endpoint
* @param {Object} options headers, method, etc.
* @param {number} endTime the cutoff time to stop polling the path and simply return the error
* @returns
*/
const apiRequestWithRetry = async (path, options, endTime) => {
return testableApiRequestWithRetry(2000, apiRequest)(path, options, endTime);
};

export const getLabsAndTests = runningUnitTest => {
if (hitApi(runningUnitTest)) {
return apiRequest(`${apiBasePath}/medical_records/labs_and_tests`, {
Expand Down Expand Up @@ -110,27 +61,21 @@ export const getLabOrTest = (id, runningUnitTest) => {
};

export const getNotes = () => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/clinical_notes`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/clinical_notes`, {
headers,
});
};

export const getNote = id => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/clinical_notes/${id}`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/clinical_notes/${id}`, {
headers,
});
};

export const getVitalsList = () => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/vitals`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/vitals`, {
headers,
});
};

export const getConditions = runningUnitTest => {
Expand Down Expand Up @@ -161,31 +106,25 @@ export const getCondition = (id, runningUnitTest) => {
};

export const getAllergies = async () => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/allergies`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/allergies`, {
headers,
});
};

export const getAllergy = id => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/allergies/${id}`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/allergies/${id}`, {
headers,
});
};

/**
* Get a patient's vaccines
* @returns list of patient's vaccines in FHIR format
*/
export const getVaccineList = () => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/vaccines`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/vaccines`, {
headers,
});
};

/**
Expand All @@ -194,11 +133,9 @@ export const getVaccineList = () => {
* @returns vaccine details in FHIR format
*/
export const getVaccine = id => {
return apiRequestWithRetry(
`${apiBasePath}/medical_records/vaccines/${id}`,
{ headers },
Date.now() + 90000, // Retry for 90 seconds
);
return apiRequest(`${apiBasePath}/medical_records/vaccines/${id}`, {
headers,
});
};

/**
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,57 @@
import React from 'react';
import PropTypes from 'prop-types';
import AccessTroubleAlertBox from './AccessTroubleAlertBox';
import NoRecordsMessage from './NoRecordsMessage';

const RecordListSection = ({
children,
accessAlert,
accessAlertType,
recordCount,
recordType,
listCurrentAsOf,
initialFhirLoad,
}) => {
if (accessAlert) {
return <AccessTroubleAlertBox alertType={accessAlertType} />;
}
if (initialFhirLoad && !listCurrentAsOf) {
return (
<div className="vads-u-margin-y--8">
<va-loading-indicator
class="hydrated initial-fhir-load"
message="We're loading your records for the first time. This can take up to 2 minutes. Stay on this page until your records load."
setFocus
data-testid="initial-fhir-loading-indicator"
/>
</div>
);
}
if (recordCount === 0) {
return <NoRecordsMessage type={recordType} />;
}
if (recordCount) {
return children;
}
return (
<div className="vads-u-margin-y--8">
<va-loading-indicator
message="We’re loading your records. This could take up to a minute."
setFocus
data-testid="loading-indicator"
/>
</div>
);
};

export default RecordListSection;

RecordListSection.propTypes = {
accessAlert: PropTypes.bool,
accessAlertType: PropTypes.string,
children: PropTypes.oneOfType([PropTypes.object, PropTypes.array]),
initialFhirLoad: PropTypes.bool,
listCurrentAsOf: PropTypes.string,
recordCount: PropTypes.number,
recordType: PropTypes.string,
};
Loading

0 comments on commit 6572fc8

Please sign in to comment.