Skip to content

Commit

Permalink
Update the handling of ids used to store VEP submission data (#1175)
Browse files Browse the repository at this point in the history
Approach taken in this commit

- Added BrowserTabIdService that provides a stable browser tab over the lifetime of a tab
- This tab id is used to generate temporary VEP submission id while the form is being filled
- When a VEP form is submitted, there is a time interval between when the user presses the submit button
   and when the server returns the response (and only at this latter point the final id of the submission becomes known). 
   This interval can be as long as dozens of seconds, or even over a minute, during which the user may start filling in
   another VEP form.
- At the moment of form submission, the temporary submission id is changed from one temporary format
   (the one based on tab id, where there can be only one such submission per tab) to another (based just on a 
   client-side-generated uuid). This completely disconnects any submissions that are in the process of being submitted
   from the VEP form that the user user may start filling in while the previous submission is still in flight.
  • Loading branch information
azangru authored Oct 8, 2024
1 parent c264b7b commit 0b95451
Show file tree
Hide file tree
Showing 10 changed files with 218 additions and 128 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -20,35 +20,24 @@ import { useAppDispatch, useAppSelector } from 'src/store';

import * as urlFor from 'src/shared/helpers/urlHelper';

import { getVepSubmission } from 'src/content/app/tools/vep/services/vepStorageService';

import {
getTemporaryVepSubmissionId,
getSelectedSpecies,
getVepFormParameters,
getVepFormInputText,
getVepFormInputFileName,
getVepFormInputCommittedFlag
} from 'src/content/app/tools/vep/state/vep-form/vepFormSelectors';

import { useVepFormSubmissionMutation } from 'src/content/app/tools/vep/state/vep-api/vepApiSlice';
import { onVepFormSubmission } from 'src/content/app/tools/vep/state/vep-form/vepFormSlice';

import { PrimaryButton } from 'src/shared/components/button/Button';

import type {
VepSubmissionPayload,
VepSelectedSpecies
} from 'src/content/app/tools/vep/types/vepSubmission';

const VepSubmitButton = (props: { className?: string }) => {
const submissionId = useAppSelector(getTemporaryVepSubmissionId);
const selectedSpecies = useAppSelector(getSelectedSpecies);
const inputText = useAppSelector(getVepFormInputText);
const inputFileName = useAppSelector(getVepFormInputFileName);
const formParameters = useAppSelector(getVepFormParameters);
const isInputCommitted = useAppSelector(getVepFormInputCommittedFlag);
const [submitVepForm] = useVepFormSubmissionMutation();
const navigate = useNavigate();
const dispatch = useAppDispatch();

Expand All @@ -60,20 +49,8 @@ const VepSubmitButton = (props: { className?: string }) => {
);

const onSubmit = async () => {
const payload = await preparePayload({
submissionId: submissionId as string,
species: selectedSpecies as VepSelectedSpecies,
inputText,
parameters: formParameters
});

await dispatch(
onVepFormSubmission({ submissionId: submissionId as string })
);

await dispatch(onVepFormSubmission());
navigate(urlFor.vepUnviewedSubmissionsList());

submitVepForm(payload);
};

return (
Expand All @@ -87,39 +64,4 @@ const VepSubmitButton = (props: { className?: string }) => {
);
};

const preparePayload = async ({
submissionId,
species,
inputText,
parameters
}: {
submissionId: string;
species: VepSelectedSpecies;
inputText: string | null;
parameters: Record<string, unknown>;
}): Promise<VepSubmissionPayload> => {
let inputFile: File;

if (inputText) {
inputFile = new File([inputText], 'input.txt', {
type: 'text/plain'
});
} else {
const storedSubmission = await getVepSubmission(submissionId);
if (!storedSubmission) {
throw new Error(
`Submission with id ${submissionId} does not exist in browser storage`
);
}
inputFile = storedSubmission.inputFile as File;
}

return {
submission_id: submissionId,
genome_id: species.genome_id,
input_file: inputFile as File,
parameters: JSON.stringify(parameters)
};
};

export default VepSubmitButton;
22 changes: 0 additions & 22 deletions src/content/app/tools/vep/services/vepStorageService.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -29,7 +29,6 @@ import {
getVepSubmission,
updateVepSubmission,
changeVepSubmissionId,
getUncompletedVepSubmission,
getVepSubmissions,
deleteVepSubmission,
deleteExpiredVepSubmissions
Expand Down Expand Up @@ -173,27 +172,6 @@ describe('vepStorageService', () => {
});
});

describe('getUncompletedVepSubmission', () => {
it('retrieves VEP submission data that have not yet been submitted', async () => {
// arrange
const submission1 = createVepSubmission();
const submission2 = createVepSubmission();
const submission3 = createVepSubmission();
submission1.submittedAt = Date.now();
submission2.submittedAt = null;
submission3.submittedAt = Date.now();
await saveVepSubmission(submission1);
await saveVepSubmission(submission2);
await saveVepSubmission(submission3);

// act
const uncompletedSubmission = await getUncompletedVepSubmission();

// assert
expect(uncompletedSubmission).toEqual(submission2);
});
});

describe('getVepSubmissions', () => {
it('retrieves all VEP submissions other than the uncompleted one', async () => {
// arrange
Expand Down
45 changes: 11 additions & 34 deletions src/content/app/tools/vep/services/vepStorageService.ts
Original file line number Diff line number Diff line change
Expand Up @@ -18,7 +18,8 @@ import IndexedDB from 'src/services/indexeddb-service';

import {
VEP_SUBMISSIONS_STORE_NAME as STORE_NAME,
VEP_SUBMISSION_STORAGE_DURATION
VEP_SUBMISSION_STORAGE_DURATION,
TEMPORARY_VEP_SUBMISSION_STORAGE_DURATION
} from './vepStorageServiceConstants';

import type {
Expand Down Expand Up @@ -107,34 +108,6 @@ export const changeVepSubmissionId = async (
await transaction.done;
};

// Returns the data for a VEP submission that the user is still preparing and has not yet submitted.
// There should only ever be one such submission.
export const getUncompletedVepSubmission = async () => {
const db = await IndexedDB.getDB();
let cursor = await db.transaction(STORE_NAME).store.openCursor();

while (cursor) {
const storedSubmission: VepSubmission = cursor.value;

if (!storedSubmission.submittedAt) {
return storedSubmission;
} else {
cursor = await cursor.continue();
}
}
};

// Similarly to getVepSubmissionWithoutInputFile, strips potentially heavy input fields before returning
export const getUncompletedVepSubmissionWithoutInputFile = async () => {
const storedSubmission = await getUncompletedVepSubmission();

if (!storedSubmission) {
return;
}

return removeInputFileFromSubmission(storedSubmission);
};

// Excluding the submission that has not been completed/submitted
// And removing the input file from every submission in the return value
export const getVepSubmissions = async (): Promise<
Expand Down Expand Up @@ -167,11 +140,15 @@ export const deleteExpiredVepSubmissions = async () => {
const transaction = db.transaction(STORE_NAME, 'readwrite');
for await (const cursor of transaction.store) {
const submission: VepSubmission = cursor.value;
const { submittedAt } = submission;
if (
submittedAt &&
submittedAt < Date.now() - VEP_SUBMISSION_STORAGE_DURATION
) {
const { submittedAt, createdAt } = submission;

const isOldTemporarySubmission =
!submittedAt &&
createdAt < Date.now() - TEMPORARY_VEP_SUBMISSION_STORAGE_DURATION;
const isExpiredSubmission =
submittedAt && submittedAt < Date.now() - VEP_SUBMISSION_STORAGE_DURATION;

if (isOldTemporarySubmission || isExpiredSubmission) {
cursor.delete();
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,4 +23,9 @@ export const VEP_SUBMISSIONS_STORE_NAME = 'vep-submissions';

export const VEP_RESULTS_AVAILABILITY_DURATION =
ONE_DAY_IN_MILLISECONDS * 7 - ONE_HOUR_IN_MILLISECONDS;

export const VEP_SUBMISSION_STORAGE_DURATION = ONE_DAY_IN_MILLISECONDS * 28;

// To clear any leftover temporary VEP submission data
export const TEMPORARY_VEP_SUBMISSION_STORAGE_DURATION =
ONE_DAY_IN_MILLISECONDS;
7 changes: 4 additions & 3 deletions src/content/app/tools/vep/state/vep-api/vepApiSlice.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,17 +48,18 @@ const vepApiSlice = restApiSlice.injectEndpoints({
const { data: exampleObjects } = await dispatch(
fetchExampleObjectsForGenome.initiate(genomeId, { subscribe: false })
);
const emptyResults = { data: {} };

if (!exampleObjects) {
throw new Error(); // FIXME
return emptyResults;
}

const exampleVariant = exampleObjects.find(
(item) => item.type === 'variant'
);

if (!exampleVariant) {
throw new Error(); // FIXME
return emptyResults;
}

const { variant } = await request<VepExampleVariantQueryResult>({
Expand All @@ -71,7 +72,7 @@ const vepApiSlice = restApiSlice.injectEndpoints({
});

if (!variant) {
throw new Error(); // FIXME
return emptyResults;
}

const firstAltAllele = variant.alleles[0];
Expand Down
Loading

0 comments on commit 0b95451

Please sign in to comment.