Refactor FilterStore with type safety and caching improvements

- Add TypeScript interfaces and proper type annotations
- Implement cache TTL and LRU eviction for better memory management
- Improve error handling with try-catch blocks and graceful fallbacks
- Extract helper methods and fix parameter naming (sietId -> siteId)
- Add utility methods to FilterItem class for validation and cloning
This commit is contained in:
Shekar Siri 2025-06-02 13:26:40 +02:00
parent 4a54830cad
commit 9294f7840a
2 changed files with 279 additions and 108 deletions

View file

@ -26,67 +26,96 @@ interface TopValuesParams {
isEvent?: boolean;
}
interface FilterOption {
label: string;
value: string;
}
interface CacheEntry {
data: Filter[];
timestamp: number;
}
// Constants
const CACHE_TTL = 5 * 60 * 1000; // 5 minutes
const MAX_CACHE_SIZE = 100;
export default class FilterStore {
topValues: TopValues = {};
filters: ProjectFilters = {};
commonFilters: Filter[] = [];
isLoadingFilters: boolean = true;
filterCache: Record<string, Filter[]> = {};
private filterCache: Record<string, CacheEntry> = {};
private pendingFetches: Record<string, Promise<Filter[]>> = {};
constructor() {
makeAutoObservable(this);
this.initCommonFilters();
}
getEventOptions = (sietId: string) => {
return this.getFilters(sietId)
.filter((i: Filter) => i.isEvent)
.map((i: Filter) => {
return {
label: i.displayName || i.name,
value: i.name,
};
});
// Fixed typo: sietId -> siteId
getEventOptions = (siteId: string): FilterOption[] => {
return this.getFilters(siteId)
.filter((filter: Filter) => filter.isEvent)
.map((filter: Filter) => ({
label: filter.displayName || filter.name,
value: filter.name,
}));
};
setTopValues = (key: string, values: Record<string, any> | TopValue[]) => {
setTopValues = (
key: string,
values: Record<string, any> | TopValue[],
): void => {
const vals = Array.isArray(values) ? values : values.data;
this.topValues[key] = vals?.filter(
(value: any) => value !== null && value.value !== '',
);
this.topValues[key] =
vals?.filter((value: any) => value !== null && value?.value !== '') || [];
};
resetValues = () => {
resetValues = (): void => {
this.topValues = {};
};
fetchTopValues = async (params: TopValuesParams) => {
const valKey = `${params.siteId}_${params.id}${params.source || ''}`;
fetchTopValues = async (params: TopValuesParams): Promise<TopValue[]> => {
const valKey = this.createTopValuesKey(params);
if (this.topValues[valKey] && this.topValues[valKey].length) {
return Promise.resolve(this.topValues[valKey]);
// Return cached values if available
if (this.topValues[valKey]?.length) {
return this.topValues[valKey];
}
const filter = this.filters[params.siteId + '']?.find(
(i) => i.id === params.id,
);
const filter = this.findFilterById(params.siteId || '', params.id || '');
if (!filter) {
console.error('Filter not found in store:', valKey);
return Promise.resolve([]);
console.warn(`Filter not found for key: ${valKey}`);
return [];
}
return searchService
.fetchTopValues({
try {
const response = await searchService.fetchTopValues({
[params.isEvent ? 'eventName' : 'propertyName']: filter.name,
})
.then((response: []) => {
});
runInAction(() => {
this.setTopValues(valKey, response);
});
return this.topValues[valKey] || [];
} catch (error) {
console.error('Failed to fetch top values:', error);
return [];
}
};
setFilters = (projectId: string, filters: Filter[]) => {
private createTopValuesKey = (params: TopValuesParams): string => {
return `${params.siteId}_${params.id}${params.source || ''}`;
};
private findFilterById = (siteId: string, id: string): Filter | undefined => {
return this.filters[siteId]?.find((filter) => filter.id === id);
};
setFilters = (projectId: string, filters: Filter[]): void => {
this.filters[projectId] = filters;
};
@ -95,12 +124,18 @@ export default class FilterStore {
return this.addOperatorsToFilters(filters);
};
setIsLoadingFilters = (loading: boolean) => {
setIsLoadingFilters = (loading: boolean): void => {
this.isLoadingFilters = loading;
};
resetFilters = () => {
resetFilters = (): void => {
this.filters = {};
this.clearCache();
};
private clearCache = (): void => {
this.filterCache = {};
this.pendingFetches = {};
};
processFilters = (filters: Filter[], category?: string): Filter[] => {
@ -110,12 +145,7 @@ export default class FilterStore {
filter.possibleTypes?.map((type) => type.toLowerCase()) || [],
dataType: filter.dataType || 'string',
category: category || 'custom',
subCategory:
category === 'events'
? filter.autoCaptured
? 'autocapture'
: 'user'
: category,
subCategory: this.determineSubCategory(category, filter),
displayName: filter.displayName || filter.name,
icon: FilterKey.LOCATION, // TODO - use actual icons
isEvent: category === 'events',
@ -125,67 +155,84 @@ export default class FilterStore {
}));
};
addOperatorsToFilters = (filters: Filter[]): Filter[] => {
return filters.map((filter) => ({
...filter,
}));
private determineSubCategory = (
category: string | undefined,
filter: Filter,
): string | undefined => {
if (category === 'events') {
return filter.autoCaptured ? 'autocapture' : 'user';
}
return category;
};
addOperatorsToFilters = (filters: Filter[]): Filter[] => {
// Currently just returns filters as-is, but keeping for future enhancements
return filters.map((filter) => ({ ...filter }));
};
// Modified to not add operators in cache
fetchFilters = async (projectId: string): Promise<Filter[]> => {
// Return cached filters with operators if available
if (this.filters[projectId] && this.filters[projectId].length) {
return Promise.resolve(this.getFilters(projectId));
// Return cached filters if available
if (this.filters[projectId]?.length) {
return this.getFilters(projectId);
}
this.setIsLoadingFilters(true);
try {
const response = await filterService.fetchFilters(projectId);
const processedFilters = this.processFilterResponse(response.data);
const processedFilters: Filter[] = [];
Object.keys(response.data).forEach((category: string) => {
const { list, total } = response.data[category] || {
list: [],
total: 0,
};
const filters = this.processFilters(list, category);
processedFilters.push(...filters);
runInAction(() => {
this.setFilters(projectId, processedFilters);
});
this.setFilters(projectId, processedFilters);
return this.getFilters(projectId);
} catch (error) {
console.error('Failed to fetch filters:', error);
throw error;
} finally {
this.setIsLoadingFilters(false);
runInAction(() => {
this.setIsLoadingFilters(false);
});
}
};
initCommonFilters = () => {
private processFilterResponse = (data: Record<string, any>): Filter[] => {
const processedFilters: Filter[] = [];
Object.entries(data).forEach(([category, categoryData]) => {
const { list = [], total = 0 } = categoryData || {};
const filters = this.processFilters(list, category);
processedFilters.push(...filters);
});
return processedFilters;
};
initCommonFilters = (): void => {
this.commonFilters = [...COMMON_FILTERS];
};
getAllFilters = (projectId: string): Filter[] => {
const projectFilters = this.filters[projectId] || [];
// return this.addOperatorsToFilters([...this.commonFilters, ...projectFilters]);
return this.addOperatorsToFilters([...projectFilters]);
};
getCurrentProjectFilters = (): Filter[] => {
return this.getAllFilters(projectStore.activeSiteId + '');
return this.getAllFilters(String(projectStore.activeSiteId));
};
getEventFilters = async (eventName: string): Promise<Filter[]> => {
const cacheKey = `${projectStore.activeSiteId}_${eventName}`;
if (this.filterCache[cacheKey]) {
return this.filterCache[cacheKey];
// Check cache with TTL
const cachedEntry = this.filterCache[cacheKey];
if (cachedEntry && this.isCacheValid(cachedEntry)) {
return cachedEntry.data;
}
if (await this.pendingFetches[cacheKey]) {
// Return pending fetch if in progress
if (this.pendingFetches[cacheKey]) {
return this.pendingFetches[cacheKey];
}
@ -193,39 +240,78 @@ export default class FilterStore {
this.pendingFetches[cacheKey] =
this.fetchAndProcessPropertyFilters(eventName);
const filters = await this.pendingFetches[cacheKey];
console.log('filters', filters);
runInAction(() => {
this.filterCache[cacheKey] = filters;
this.setCacheEntry(cacheKey, filters);
});
delete this.pendingFetches[cacheKey];
return filters;
} catch (error) {
delete this.pendingFetches[cacheKey];
console.error('Failed to fetch event filters:', error);
throw error;
} finally {
delete this.pendingFetches[cacheKey];
}
};
private isCacheValid = (entry: CacheEntry): boolean => {
return Date.now() - entry.timestamp < CACHE_TTL;
};
private setCacheEntry = (key: string, data: Filter[]): void => {
// Implement simple LRU by removing oldest entries
if (Object.keys(this.filterCache).length >= MAX_CACHE_SIZE) {
const oldestKey = Object.keys(this.filterCache)[0];
delete this.filterCache[oldestKey];
}
this.filterCache[key] = {
data,
timestamp: Date.now(),
};
};
private fetchAndProcessPropertyFilters = async (
eventName: string,
isAutoCapture?: boolean,
): Promise<Filter[]> => {
const resp = await filterService.fetchProperties(eventName, isAutoCapture);
const names = resp.data.map((i: any) => i['name']);
try {
const response = await filterService.fetchProperties(
eventName,
isAutoCapture,
);
const propertyNames = response.data.map((item: any) => item.name);
const activeSiteId = projectStore.activeSiteId + '';
return (
this.filters[activeSiteId]
?.filter((i: any) => names.includes(i.name))
.map((f: any) => ({
...f,
const activeSiteId = String(projectStore.activeSiteId);
const siteFilters = this.filters[activeSiteId] || [];
return siteFilters
.filter((filter: Filter) => propertyNames.includes(filter.name))
.map((filter: Filter) => ({
...filter,
eventName,
})) || []
);
}));
} catch (error) {
console.error('Failed to fetch property filters:', error);
return [];
}
};
setCommonFilters = (filters: Filter[]) => {
this.commonFilters = filters;
setCommonFilters = (filters: Filter[]): void => {
this.commonFilters = [...filters];
};
// Cleanup method for memory management
cleanup = (): void => {
this.clearExpiredCacheEntries();
};
private clearExpiredCacheEntries = (): void => {
const now = Date.now();
Object.entries(this.filterCache).forEach(([key, entry]) => {
if (now - entry.timestamp > CACHE_TTL) {
delete this.filterCache[key];
}
});
};
}

View file

@ -4,6 +4,32 @@ import { FilterProperty, Operator } from '@/mstore/types/filterConstants';
type JsonData = Record<string, any>;
// Define a proper interface for initialization data
interface FilterItemData {
id?: string;
name?: string;
displayName?: string;
description?: string;
possibleTypes?: string[];
autoCaptured?: boolean;
metadataName?: string;
category?: string;
subCategory?: string;
type?: string;
icon?: string;
properties?: FilterProperty[];
operator?: string;
operators?: Operator[];
isEvent?: boolean;
value?: string[];
propertyOrder?: string;
filters?: FilterItemData[];
autoOpen?: boolean;
}
// Define valid keys that can be updated
type FilterItemKeys = keyof FilterItemData;
export default class FilterItem {
id: string = '';
name: string = '';
@ -12,9 +38,9 @@ export default class FilterItem {
possibleTypes?: string[];
autoCaptured?: boolean;
metadataName?: string;
category: string; // 'event' | 'filter' | 'action' | etc.
category: string = '';
subCategory?: string;
type?: string; // 'number' | 'string' | 'boolean' | etc.
type?: string;
icon?: string;
properties?: FilterProperty[];
operator?: string;
@ -25,67 +51,108 @@ export default class FilterItem {
filters?: FilterItem[];
autoOpen?: boolean;
constructor(data: any = {}) {
constructor(data: FilterItemData = {}) {
makeAutoObservable(this);
this.initializeFromData(data);
}
private initializeFromData(data: FilterItemData): void {
// Set default operator if not provided
const processedData = {
...data,
operator: data.operator || 'is',
};
// Handle filters array transformation
if (Array.isArray(data.filters)) {
data.filters = data.filters.map(
(i: Record<string, any>) => new FilterItem(i),
processedData.filters = data.filters.map(
(filterData: FilterItemData) => new FilterItem(filterData),
);
}
data.operator = data.operator || 'is';
this.merge(data);
this.merge(processedData);
}
updateKey(key: string, value: any) {
// @ts-ignore
this[key] = value;
updateKey<K extends FilterItemKeys>(key: K, value: FilterItemData[K]): void {
if (key in this) {
(this as any)[key] = value;
} else {
console.warn(`Attempted to update invalid key: ${key}`);
}
}
merge(data: any) {
Object.keys(data).forEach((key) => {
// @ts-ignore
this[key] = data[key];
merge(data: FilterItemData): void {
Object.entries(data).forEach(([key, value]) => {
if (key in this && value !== undefined) {
(this as any)[key] = value;
}
});
}
fromData(data: any) {
fromData(data: FilterItemData): FilterItem {
if (!data) {
console.warn('fromData called with null/undefined data');
return this;
}
Object.assign(this, data);
this.type = 'string';
this.name = data.type;
this.category = data.category;
this.name = data.name || '';
this.category = data.category || '';
this.subCategory = data.subCategory;
this.operator = data.operator;
this.filters = data.filters.map((i: JsonData) => new FilterItem(i));
// Safely handle filters array
if (Array.isArray(data.filters)) {
this.filters = data.filters.map(
(filterData: FilterItemData) => new FilterItem(filterData),
);
} else {
this.filters = [];
}
return this;
}
fromJson(data: JsonData) {
fromJson(data: JsonData): FilterItem {
if (!data) {
console.warn('fromJson called with null/undefined data');
return this;
}
this.type = 'string';
this.name = data.type;
this.category = data.category;
this.name = data.type || '';
this.category = data.category || '';
this.subCategory = data.subCategory;
this.operator = data.operator;
this.value = data.value || [''];
this.filters = data.filters.map((i: JsonData) => new FilterItem(i));
this.value = Array.isArray(data.value) ? data.value : [''];
// Safely handle filters array
if (Array.isArray(data.filters)) {
this.filters = data.filters.map(
(filterData: JsonData) => new FilterItem(filterData),
);
} else {
this.filters = [];
}
return this;
}
toJson(): any {
const json: any = {
toJson(): JsonData {
const json: JsonData = {
type: this.name,
isEvent: Boolean(this.isEvent),
value: this.value?.map((i: any) => (i ? i.toString() : '')) || [],
value:
this.value?.map((item: any) => (item ? item.toString() : '')) || [],
operator: this.operator,
source: this.name,
filters: Array.isArray(this.filters)
? this.filters.map((i) => i.toJson())
? this.filters.map((filter) => filter.toJson())
: [],
};
// Handle metadata category
const isMetadata = this.category === FilterCategory.METADATA;
if (isMetadata) {
json.type = FilterKey.METADATA;
@ -93,10 +160,28 @@ export default class FilterItem {
json.sourceOperator = this.operator;
}
// Handle duration type
if (this.type === FilterKey.DURATION) {
json.value = this.value?.map((i: any) => (!i ? 0 : i));
json.value = this.value?.map((item: any) => (item ? Number(item) : 0));
}
return json;
}
// Additional utility methods
isValid(): boolean {
return Boolean(this.name && this.category);
}
clone(): FilterItem {
return new FilterItem(JSON.parse(JSON.stringify(this.toJson())));
}
reset(): void {
this.value = [''];
this.operator = 'is';
if (this.filters) {
this.filters.forEach((filter) => filter.reset());
}
}
}