Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

[INV-3757] Clear Records from a Cache #3768

Merged
merged 6 commits into from
Jan 3, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion app/src/UI/Overlay/Records/Records.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -49,7 +49,7 @@ export const Records = () => {
e.stopPropagation();
const callback = (userConfirmation: boolean) => {
if (userConfirmation) {
dispatch(UserSettings.RecordSet.remove(set));
dispatch(UserSettings.RecordSet.requestRemoval({ setId: set }));
}
};
dispatch(
Expand Down
27 changes: 22 additions & 5 deletions app/src/state/actions/cache/RecordCache.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,11 +7,28 @@ import { RecordCacheServiceFactory } from 'utils/record-cache/context';
class RecordCache {
static readonly PREFIX = 'RecordCache';

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

await service.deleteCachedSet(spec.setId);
});
/**
* @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 requestCaching = createAsyncThunk(
`${this.PREFIX}/requestCaching`,
Expand Down
18 changes: 18 additions & 0 deletions app/src/state/actions/userSettings/RecordSet.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,10 @@
import { createAction, createAsyncThunk, nanoid } from '@reduxjs/toolkit';
import { RECORD_COLOURS } from 'constants/colors';
import { RecordSetType, UserRecordCacheStatus, UserRecordSet } from 'interfaces/UserRecordSet';
import { MOBILE } from 'state/build-time-config';
import { RootState } from 'state/reducers/rootReducer';
import { RecordCacheServiceFactory } from 'utils/record-cache/context';
import RecordCache from '../cache/RecordCache';

export interface IUpdateFilter {
setID: string | number;
Expand Down Expand Up @@ -56,6 +59,21 @@ class RecordSet {
}
);

static readonly requestRemoval = createAsyncThunk(
`${this.PREFIX}/requestRemoval`,
async (spec: { setId: string }, thunkAPI) => {
const state = thunkAPI.getState() as RootState;
const { recordSets } = state.UserSettings;
if (MOBILE && recordSets[spec.setId].cacheMetadata.status == UserRecordCacheStatus.CACHED) {
const deletionResult = await thunkAPI.dispatch(RecordCache.deleteCache(spec));
if (RecordCache.deleteCache.rejected.match(deletionResult)) {
throw Error('Cache failed to delete');
}
}
return spec.setId;
}
);

static readonly updateFilter = createAction<IUpdateFilter>(`${this.PREFIX}/updateFilter`);
static readonly removeFilter = createAction<IRemoveFilter>(`${this.PREFIX}/removeFilter`);

Expand Down
25 changes: 3 additions & 22 deletions app/src/state/reducers/userSettings.ts
Original file line number Diff line number Diff line change
Expand Up @@ -104,7 +104,7 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState
draftState.mapCenter = action.payload as [number, number];
} else if (UserSettings.RecordSet.add.match(action)) {
draftState.recordSets[Date.now().toString()] ??= action.payload;
} else if (UserSettings.RecordSet.remove.match(action)) {
} else if (UserSettings.RecordSet.requestRemoval.fulfilled.match(action)) {
delete draftState.recordSets[action.payload];
} else if (UserSettings.RecordSet.set.match(action)) {
Object.keys(action.payload.updatedSet).forEach((key) => {
Expand Down Expand Up @@ -200,7 +200,7 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.DOWNLOADING
};
} else if (RecordCache.requestCaching.rejected.match(action)) {
} else if (RecordCache.requestCaching.rejected.match(action) || RecordCache.deleteCache.rejected.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.ERROR
};
Expand All @@ -213,30 +213,11 @@ function createUserSettingsReducer(configuration: AppConfig): (UserSettingsState
cachedCentroid: action.payload.cachedCentroid
};
} else if (RecordCache.deleteCache.pending.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.DELETING
};
} else if (RecordCache.deleteCache.rejected.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.ERROR
};
draftState.recordSets[action.meta.arg.setId].cacheMetadata.status = UserRecordCacheStatus.DELETING;
} else if (RecordCache.deleteCache.fulfilled.match(action)) {
draftState.recordSets[action.meta.arg.setId].cacheMetadata = {
status: UserRecordCacheStatus.NOT_CACHED
};
} else if (UserSettings.RecordSet.syncCacheStatusWithCacheService.fulfilled.match(action)) {
const cacheStatus = action.payload;
for (const cachedSet of cacheStatus) {
if (draftState.recordSets[cachedSet.setId]) {
const { idList, cachedGeoJson, cachedCentroid } = draftState.recordSets[cachedSet.setId].cacheMetadata;
draftState.recordSets[cachedSet.setId].cacheMetadata = {
status: UserRecordCacheStatus.CACHED,
idList,
cachedGeoJson,
cachedCentroid
};
}
}
} else if (Activity.deleteSuccess.match(action)) {
draftState.activeActivity = null;
draftState.activeActivityDescription = null;
Expand Down
12 changes: 1 addition & 11 deletions app/src/utils/record-cache/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,10 +16,6 @@ export interface RecordCacheDownloadRequestSpec {
idsToCache: string[];
}

export interface RecordCacheAddSpec {
setId: string;
idsToCache: string[];
}
/**
* @desc Cached Metadata for Recordsets
* @property { string } setID Recordset ID
Expand Down Expand Up @@ -58,16 +54,10 @@ abstract class RecordCacheService {
abstract saveActivity(id: string, data: unknown): Promise<void>;

abstract saveIapp(id: string, iappRecord: unknown, iappTableRow: unknown): Promise<void>;

abstract deleteCachedRecordsFromIds(idsToDelete: string[], recordSetType: RecordSetType): Promise<void>;
abstract loadActivity(id: string): Promise<unknown>;
abstract loadIapp(id: string, type: IappRecordMode): Promise<IappRecord | IappTableRow>;

abstract addCachedSet(spec: RecordCacheAddSpec): Promise<void>;

abstract listCachedSets(): Promise<RecordSetCacheMetadata[]>;

abstract deleteCachedSet(id: string): Promise<void>;

abstract fetchPaginatedCachedIappRecords(
recordSetIdList: string[],
page: number,
Expand Down
87 changes: 5 additions & 82 deletions app/src/utils/record-cache/localforage-cache.ts
Original file line number Diff line number Diff line change
@@ -1,17 +1,12 @@
import UserRecord from 'interfaces/UserRecord';
import localForage from 'localforage';
import centroid from '@turf/centroid';
import {
IappRecordMode,
RecordCacheAddSpec,
RecordCacheService,
RecordSetCacheMetadata,
RecordSetSourceMetadata
} from 'utils/record-cache/index';
import { IappRecordMode, RecordCacheService, RecordSetSourceMetadata } from 'utils/record-cache/index';
import { Feature } from '@turf/helpers';
import { GeoJSONSourceSpecification } from 'maplibre-gl';
import IappRecord from 'interfaces/IappRecord';
import IappTableRow from 'interfaces/IappTableRecord';
import { RecordSetType } from 'interfaces/UserRecordSet';

class LocalForageRecordCacheService extends RecordCacheService {
private static _instance: LocalForageRecordCacheService;
Expand Down Expand Up @@ -92,24 +87,6 @@ class LocalForageRecordCacheService extends RecordCacheService {
return data;
}

async addCachedSet(spec: RecordCacheAddSpec): Promise<void> {
if (this.store == null) {
throw new Error('cache not available');
}

const cachedSets = await this.listCachedSets();
const foundIndex = cachedSets.findIndex((p) => p.setId == spec.setId);
if (foundIndex !== -1) {
throw new Error('cached set already exists');
}

cachedSets.push({
setId: spec.setId,
cachedIds: spec.idsToCache
});
await this.store.setItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY, cachedSets);
}

/**
* @desc fetch `n` records for a given recordset, supporting pagination
* @param recordSetID Recordset to filter from
Expand All @@ -131,21 +108,6 @@ class LocalForageRecordCacheService extends RecordCacheService {
return results;
}

async listCachedSets(): Promise<RecordSetCacheMetadata[]> {
if (this.store == null) {
return [];
}

const metadata = (await this.store.getItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY)) as
| RecordSetCacheMetadata[]
| null;
if (metadata == null) {
console.error('expected key not found');
return [];
}
return metadata;
}

/**
* @desc Iterate ids to produce list of values to populate in the map.
* The values only change with the recordsets, so we create the list at cache-ception to avoid querying
Expand Down Expand Up @@ -207,51 +169,12 @@ class LocalForageRecordCacheService extends RecordCacheService {
return { cachedCentroid, cachedGeoJson };
}

async deleteCachedSet(id: string): Promise<void> {
async deleteCachedRecordsFromIds(idsToDelete: string[], recordSetType: RecordSetType): Promise<void> {
if (this.store == null) {
throw new Error('cache not available');
}

const cachedSets = await this.listCachedSets();
const foundIndex = cachedSets.findIndex((p) => p.setId == id);
if (foundIndex !== -1) {
cachedSets.splice(foundIndex, 1);
}
try {
await this.cleanupOrphanActivities();
} finally {
await this.store.setItem(LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY, cachedSets);
}
}

private async cleanupOrphanActivities(): Promise<void> {
if (this.store == null) {
throw new Error('cache not available');
}
const cachedSets = await this.listCachedSets();

const allKeys = await this.store.keys();
const deletionQueue: string[] = [];
for (const k of allKeys) {
if (k == LocalForageRecordCacheService.CACHED_SETS_METADATA_KEY) {
continue;
}
let referenced = false;
for (const set of cachedSets) {
if (set.cachedIds?.includes(k)) {
referenced = true;
}
}
if (!referenced) {
deletionQueue.push(k);
}
}
for (const d of deletionQueue) {
try {
await this.store.removeItem(d);
} catch (e) {
console.error(e);
}
for (const id of idsToDelete) {
await this.store.removeItem(id.toString());
}
}

Expand Down
Loading
Loading