Skip to content

Commit

Permalink
[INV-3630] cache download stops in progress, start emancipating from …
Browse files Browse the repository at this point in the history
…Redux State (#3782)

* Correct type mismatch for cached Geojson that didn't throw error

* re-introduce Cache metadata in [localForage] Database

* Add cancel prompt for when Downloads in Progress

* Unhook deleteCache from Redux State idLists to Db stored cachedIds

* set 'cachedSet' in top level download function

* more tightly assign types in RecordSet.requestCaching

* Modify getBoundingBoxFromRecordsetFilters to work when filters is null

* Do not fire Error alert if we terminated early

* remove Destructuring of recordset, add cache abort flow (else condition)

* consume errors in deleteCachedRecordsFromId function, add boolean return to download functions

* make function names 'repository' centric for consistency, add delete method

* Modify downloads to handle early exits

* add status check on last recordSet + intervals

* Reduce logic in deleteCache thunk, move into relevant class method

* Handle Orphan records when downloaded stopped early

* Add SQLite commands, cause repo clear if downloading and user selects delete

* Move business logic out of 'requestCaching' thunk and into Class

* Remove unused import
  • Loading branch information
LocalNewsTV authored Jan 14, 2025
1 parent 7c9f80c commit be1e340
Show file tree
Hide file tree
Showing 13 changed files with 449 additions and 82 deletions.
26 changes: 23 additions & 3 deletions app/src/UI/Overlay/Records/RecordSetCacheButtons.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -22,12 +22,29 @@ const RecordSetCacheButtons = ({ recordSet, setId }: PropTypes) => {
case UserRecordCacheStatus.NOT_CACHED:
downloadCache();
break;
case UserRecordCacheStatus.DOWNLOADING:
cancelCacheDownload();
break;
case UserRecordCacheStatus.ERROR:
case UserRecordCacheStatus.CACHED:
deleteCache();
break;
}
};
const cancelCacheDownload = () => {
const callback = (confirmation: boolean) => {
if (confirmation) {
dispatch(RecordCache.stopDownload({ setId }));
}
};
dispatch(
Prompt.confirmation({
title: 'Cancel Download',
prompt: 'Would you like to cancel the download in progress?',
callback
})
);
};
const downloadCache = () => {
const callback = (confirmation: boolean) => {
if (confirmation) {
Expand Down Expand Up @@ -74,9 +91,12 @@ const RecordSetCacheButtons = ({ recordSet, setId }: PropTypes) => {
useEffect(() => {
setCacheActionEnabled(
connected &&
[UserRecordCacheStatus.CACHED, UserRecordCacheStatus.NOT_CACHED, UserRecordCacheStatus.ERROR].includes(
recordSet.cacheMetadata?.status
)
[
UserRecordCacheStatus.CACHED,
UserRecordCacheStatus.NOT_CACHED,
UserRecordCacheStatus.ERROR,
UserRecordCacheStatus.DOWNLOADING
].includes(recordSet.cacheMetadata?.status)
);
}, [recordSet.cacheMetadata?.status, connected]);

Expand Down
6 changes: 6 additions & 0 deletions app/src/constants/alerts/cacheAlerts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ const cacheAlertMessages: Record<string, AlertMessage> = {
severity: AlertSeverity.Success,
subject: AlertSubjects.Cache,
autoClose: 4
},
recordSetDownloadStoppedEarly: {
content: 'Recordset download stopped',
severity: AlertSeverity.Success,
subject: AlertSubjects.Cache,
autoClose: 4
}
};

Expand Down
6 changes: 3 additions & 3 deletions app/src/interfaces/UserRecordSet.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
import { RecordSetCachedShape } from 'utils/record-cache';
import { GeoJSONSourceSpecification } from 'maplibre-gl';
import { RepositoryBoundingBoxSpec } from 'utils/tile-cache';

export enum RecordSetType {
Expand Down Expand Up @@ -35,8 +35,8 @@ export interface UserRecordSet {
cacheMetadata: {
status: UserRecordCacheStatus;
idList?: string[];
cachedGeoJson?: RecordSetCachedShape[];
cachedCentroid?: RecordSetCachedShape[];
cachedGeoJson?: GeoJSONSourceSpecification;
cachedCentroid?: GeoJSONSourceSpecification;
bbox?: RepositoryBoundingBoxSpec;
};
}
64 changes: 25 additions & 39 deletions app/src/state/actions/cache/RecordCache.ts
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
import { createAsyncThunk } from '@reduxjs/toolkit';
import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet';
import { UserRecordCacheStatus } from 'interfaces/UserRecordSet';
import { RootState } from 'state/reducers/rootReducer';
import getBoundingBoxFromRecordsetFilters from 'utils/getBoundingBoxFromRecordsetFilters';
import { RecordCacheServiceFactory } from 'utils/record-cache/context';
Expand All @@ -11,24 +11,14 @@ class RecordCache {
* @desc Deletes cached records for a recordset.
* determines duplicates with a frequency map to avoid duplicating records contained elsewhere
*/
static readonly deleteCache = createAsyncThunk(
`${this.PREFIX}/deleteCache`,
async (spec: { setId: string }, { getState }) => {
const service = await RecordCacheServiceFactory.getPlatformInstance();
const state = getState() as RootState;
const { recordSets } = state.UserSettings;
const deleteList = recordSets[spec.setId].cacheMetadata.idList ?? [];
const ids: Record<string, number> = {};
Object.keys(recordSets)
.flatMap((key) => recordSets[key].cacheMetadata.idList ?? [])
.forEach((id) => {
ids[id] ??= 0;
ids[id]++;
});
const recordsToErase = deleteList.filter((id) => ids[id] === 1);
await service.deleteCachedRecordsFromIds(recordsToErase, recordSets[spec.setId].recordSetType);
}
);
static readonly deleteCache = createAsyncThunk(`${this.PREFIX}/deleteCache`, async (spec: { setId: string }) => {
const service = await RecordCacheServiceFactory.getPlatformInstance();
await service.deleteRepository(spec.setId);
});

static readonly stopDownload = createAsyncThunk(`${this.PREFIX}/stopDownload`, async (spec: { setId: string }) => {
await (await RecordCacheServiceFactory.getPlatformInstance()).stopDownload(spec.setId);
});

static readonly requestCaching = createAsyncThunk(
`${this.PREFIX}/requestCaching`,
Expand All @@ -40,34 +30,30 @@ class RecordCache {
) => {
const service = await RecordCacheServiceFactory.getPlatformInstance();
const state: RootState = getState() as RootState;

const idsToCache: string[] = state.Map.layers
.find((l) => l.recordSetID == spec.setId)
.IDList.map((id: string | number) => id.toString());
const { recordSetType, tableFilters }: UserRecordSet = state.UserSettings.recordSets[spec.setId];
const args = {

const recordSet = state.UserSettings.recordSets[spec.setId];
const bbox = await getBoundingBoxFromRecordsetFilters(recordSet);

const responseData = await service.downloadCache({
API_BASE: state.Configuration.current.API_BASE,
bbox,
idsToCache,
setId: spec.setId,
API_BASE: state.Configuration.current.API_BASE
};
const bbox = await getBoundingBoxFromRecordsetFilters(tableFilters);
let responseData: Record<PropertyKey, any> = {
cachedGeoJSON: [],
cachedCentroid: []
};
recordSetType: recordSet.recordSetType,
setId: spec.setId
});

if (recordSetType === RecordSetType.Activity) {
await service.download(args);
responseData = await service.loadRecordsetSourceMetadata(idsToCache);
} else if (recordSetType === RecordSetType.IAPP) {
await service.downloadIapp(args);
responseData = await service.loadIappRecordsetSourceMetadata(idsToCache);
}
// Will Refactor the current uses of Cache Metadata separately [Maintain only cache status]
return {
cachedIds: idsToCache,
status: UserRecordCacheStatus.CACHED,
idList: idsToCache,
bbox: bbox,
setId: spec.setId,
bbox,
cachedGeoJson: responseData.cachedGeoJson,
cachedCentroid: responseData.cachedCentroid ?? []
cachedCentroid: responseData.cachedCentroid
};
}
);
Expand Down
9 changes: 7 additions & 2 deletions app/src/state/actions/userSettings/RecordSet.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,7 +50,7 @@ class RecordSet {
throw Error('no record cache service is available');
}

const cachedSets = await service.listCachedSets();
const cachedSets = await service.listRepositories();

// these will be passed to the reducer, which can then mark the record sets as cached
return cachedSets.map((set) => {
Expand All @@ -64,7 +64,12 @@ class RecordSet {
async (spec: { setId: string }, thunkAPI) => {
const state = thunkAPI.getState() as RootState;
const { recordSets } = state.UserSettings;
if (MOBILE && recordSets[spec.setId].cacheMetadata.status == UserRecordCacheStatus.CACHED) {
if (
MOBILE &&
[UserRecordCacheStatus.CACHED, UserRecordCacheStatus.DOWNLOADING].includes(
recordSets[spec.setId]?.cacheMetadata?.status
)
) {
const deletionResult = await thunkAPI.dispatch(RecordCache.deleteCache(spec));
if (RecordCache.deleteCache.rejected.match(deletionResult)) {
throw Error('Cache failed to delete');
Expand Down
6 changes: 5 additions & 1 deletion app/src/state/reducers/alertsAndPrompts.ts
Original file line number Diff line number Diff line change
Expand Up @@ -53,9 +53,13 @@ export function createAlertsAndPromptsReducer(
} else if (RecordCache.requestCaching.fulfilled.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheSuccess);
} else if (RecordCache.requestCaching.rejected.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheFailed);
if (action?.error?.message !== 'Early Exit') {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetCacheFailed);
}
} else if (RecordCache.deleteCache.rejected.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetDeleteCacheFailed);
} else if (RecordCache.stopDownload.fulfilled.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordSetDownloadStoppedEarly);
} else if (RecordCache.deleteCache.fulfilled.match(action)) {
draftState.alerts = addAlert(state.alerts, cacheAlertMessages.recordsetDeleteCacheSuccess);
}
Expand Down
16 changes: 11 additions & 5 deletions app/src/state/reducers/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -201,18 +201,24 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState
status: UserRecordCacheStatus.DOWNLOADING
};
} else if (RecordCache.requestCaching.rejected.match(action) || RecordCache.deleteCache.rejected.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.ERROR
};
if (action.error.message === 'Early Exit') {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.NOT_CACHED
};
} else {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.ERROR
};
}
} else if (RecordCache.requestCaching.fulfilled.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.CACHED,
idList: action.payload.cachedIds,
idList: action.payload.idList,
bbox: action.payload.bbox,
cachedGeoJson: action.payload.cachedGeoJson,
cachedCentroid: action.payload.cachedCentroid
};
} else if (RecordCache.deleteCache.pending.match(action)) {
} else if (RecordCache.deleteCache.pending.match(action) || RecordCache.stopDownload.pending.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata.status = UserRecordCacheStatus.DELETING;
} else if (RecordCache.deleteCache.fulfilled.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
Expand Down
4 changes: 2 additions & 2 deletions app/src/state/sagas/map/dataAccess.ts
Original file line number Diff line number Diff line change
Expand Up @@ -115,12 +115,12 @@ export function* handle_ACTIVITIES_GET_IDS_FOR_RECORDSET_REQUEST(action) {
});
} else {
const recordSet = currentState.recordSets[action.payload.recordSetID] ?? null;
if (recordSet?.cachedMetadata?.idList) {
if (recordSet?.cacheMetadata?.idList) {
yield put({
type: ACTIVITIES_GET_IDS_FOR_RECORDSET_SUCCESS,
payload: {
recordSetID: action.payload.recordSetID,
IDList: recordSet.cachedMetadata.idList ?? [],
IDList: recordSet.cacheMetadata.idList ?? [],
tableFiltersHash: action.payload.tableFiltersHash
}
});
Expand Down
2 changes: 1 addition & 1 deletion app/src/utils/filterRecordsetsByNetworkState.ts
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,7 @@ import { UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet';
*/
const filterRecordsetsByNetworkState = (recordSets: Record<string, UserRecordSet>, userOffline: boolean): string[] =>
Object.keys(recordSets).filter((set) => {
return recordSets[set].cacheMetadata.status === UserRecordCacheStatus.CACHED || !userOffline;
return !userOffline || recordSets[set].cacheMetadata.status === UserRecordCacheStatus.CACHED;
});

export default filterRecordsetsByNetworkState;
7 changes: 4 additions & 3 deletions app/src/utils/getBoundingBoxFromRecordsetFilters.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,18 @@
import bbox from '@turf/bbox';
import { RecordSetType, UserRecordSet } from 'interfaces/UserRecordSet';
import { UserRecordSet } from 'interfaces/UserRecordSet';
import { getCurrentJWT } from 'state/sagas/auth/auth';
import { getSelectColumnsByRecordSetType } from 'state/sagas/map/dataAccess';
import { parse } from 'wkt';
import { RepositoryBoundingBoxSpec } from './tile-cache';

const getBoundingBoxFromRecordsetFilters = async (recordSet: UserRecordSet): Promise<RepositoryBoundingBoxSpec> => {
const { recordSetType } = recordSet;
const filterObj = {
recordSetType: recordSet.recordSetType,
recordSetType: recordSetType,
sortColumn: 'short_id',
sortOrder: 'DESC',
tableFilters: recordSet.tableFilters,
selectColumns: getSelectColumnsByRecordSetType(RecordSetType.Activity)
selectColumns: getSelectColumnsByRecordSetType(recordSetType)
};

const data = await fetch(`${CONFIGURATION_API_BASE}/api/v2/activities/bbox`, {
Expand Down
Loading

0 comments on commit be1e340

Please sign in to comment.