PSA/packages/user-activities/src/hooks/useActivitiesCache.ts
Hermes 284313f908
Some checks are pending
Bidi Control Character Guard / bidi-control-guard (push) Waiting to run
Circular Dependency Check / Check for new circular dependencies (push) Waiting to run
Citus Migration Smoke / Combined migrations on single-node Citus (push) Waiting to run
E2E Fresh Install Tests / fresh-install-e2e (push) Waiting to run
ext-v2 guardrails / Run ext-v2 guard and ESLint (push) Waiting to run
Integration Tests / Check for relevant changes (push) Waiting to run
Integration Tests / ${{ (github.event_name == 'schedule' || github.event.inputs.suite == 'full') && 'Full integration suite' || 'Tier-1 integration subset' }} (push) Blocked by required conditions
Mobile checks / Mobile lint + typecheck (push) Waiting to run
Mobile checks / Mobile unit tests (push) Waiting to run
Mobile checks / Mobile dependency audit (report) (push) Waiting to run
Mobile checks / Mobile reproducibility checks (push) Waiting to run
Secrets guard (env backups) / Ensure no tracked env backup files (push) Waiting to run
Temporal Readiness / fast-readiness (push) Waiting to run
Temporal Readiness / docker-parity (push) Waiting to run
TypeScript Type Check / Nx affected typecheck (push) Waiting to run
Unit Tests / Skipped-test budget (push) Waiting to run
Unit Tests / Nx affected unit tests (push) Waiting to run
Unit Tests / Server unit coverage (informational) (push) Waiting to run
Validate Tenant Management Schema / Check for relevant changes (push) Waiting to run
Validate Tenant Management Schema / Validate Tenant Management Schema (push) Blocked by required conditions
EE Workflows Build Guard / ee-workflows-build-guard (push) Waiting to run
Initial import of AlgaPSA codebase from PSA server
Excluded: .git, node_modules, secrets/, compose.env, assemblyscript tgz

Source: /opt/alga-psa on psa.joliet.tech
2026-06-22 16:12:17 -05:00

176 lines
4.9 KiB
TypeScript

'use client';
import { useState, useCallback, useRef, useEffect } from 'react';
import { Activity, ActivityFilters, ActivityResponse, ActivityType } from '@alga-psa/types';
import { fetchActivities } from '@alga-psa/user-activities/actions';
type CacheKey = string;
interface CacheEntry {
activities: Activity[];
totalCount: number;
timestamp: number;
expiresAt: number;
}
const CACHE_TTL = {
DEFAULT: 5 * 60 * 1000,
DRAWER: 10 * 60 * 1000,
SMALL_DATASET: 15 * 60 * 1000,
};
const CACHE_SIZE_LIMIT = 50;
export function useActivitiesCache() {
const cache = useRef<Map<CacheKey, CacheEntry>>(new Map());
const [cacheHits, setCacheHits] = useState(0);
const [cacheMisses, setCacheMisses] = useState(0);
const [isLoading, setIsLoading] = useState(false);
const [isInitialLoad, setIsInitialLoad] = useState(true);
useEffect(() => {
const interval = setInterval(() => {
const now = Date.now();
let expired = 0;
cache.current.forEach((entry, key) => {
if (entry.expiresAt < now) {
cache.current.delete(key);
expired++;
}
});
if (expired > 0) {
console.log(`Cleaned up ${expired} expired cache entries`);
}
}, 60000);
return () => clearInterval(interval);
}, []);
const generateCacheKey = useCallback((filters: ActivityFilters, page: number, pageSize: number): CacheKey => {
const filterKeys = Object.keys(filters).sort();
const filterString = filterKeys
.map((key) => {
const value = filters[key as keyof ActivityFilters];
if (Array.isArray(value)) {
return `${key}:${value.sort().join(',')}`;
}
return `${key}:${value}`;
})
.join('|');
return `${filterString}|page:${page}|size:${pageSize}`;
}, []);
const getActivities = useCallback(
async (filters: ActivityFilters, page: number, pageSize: number): Promise<ActivityResponse> => {
const cacheKey = generateCacheKey(filters, page, pageSize);
const now = Date.now();
if (!isInitialLoad) {
setIsLoading(true);
}
try {
if (cache.current.has(cacheKey)) {
const entry = cache.current.get(cacheKey)!;
if (entry.expiresAt > now) {
console.log('Cache hit for activities data');
setCacheHits((prev) => prev + 1);
await new Promise((resolve) => setTimeout(resolve, 10));
return {
activities: entry.activities,
totalCount: entry.totalCount,
pageCount: Math.ceil(entry.totalCount / pageSize),
pageSize,
pageNumber: page,
};
}
}
console.log('Cache miss for activities data, fetching from server');
setCacheMisses((prev) => prev + 1);
const effectiveFilters: ActivityFilters = {
...filters,
types:
filters.types && filters.types.length > 0
? filters.types
: Object.values(ActivityType).filter((type) => type !== ActivityType.WORKFLOW_TASK),
};
const result = await fetchActivities(effectiveFilters, page, pageSize);
let cacheTtl = CACHE_TTL.DEFAULT;
if (pageSize <= 5) {
cacheTtl = CACHE_TTL.SMALL_DATASET;
} else if (filters.types && filters.types.length === 1) {
cacheTtl = CACHE_TTL.DRAWER;
}
cache.current.set(cacheKey, {
activities: result.activities,
totalCount: result.totalCount,
timestamp: now,
expiresAt: now + cacheTtl,
});
if (cache.current.size > CACHE_SIZE_LIMIT) {
const entries = Array.from(cache.current.entries());
entries.sort((a, b) => a[1].timestamp - b[1].timestamp);
const toRemove = entries.slice(0, entries.length - CACHE_SIZE_LIMIT);
toRemove.forEach(([key]) => cache.current.delete(key));
console.log(`Removed ${toRemove.length} oldest cache entries`);
}
if (isInitialLoad) {
setIsInitialLoad(false);
}
return result;
} finally {
setIsLoading(false);
}
},
[generateCacheKey, isInitialLoad]
);
const clearCache = useCallback(() => {
cache.current.clear();
setCacheHits(0);
setCacheMisses(0);
}, []);
const invalidateCache = useCallback((pattern?: string) => {
if (!pattern) {
clearCache();
return;
}
cache.current.forEach((_, key) => {
if (key.includes(pattern)) {
cache.current.delete(key);
}
});
}, [clearCache]);
const getCacheStats = useCallback(() => ({
size: cache.current.size,
cacheHits,
cacheMisses,
hitRate: cacheHits + cacheMisses > 0 ? cacheHits / (cacheHits + cacheMisses) : 0,
}), [cacheHits, cacheMisses]);
return {
getActivities,
clearCache,
invalidateCache,
getCacheStats,
cacheHits,
cacheMisses,
isLoading,
isInitialLoad,
};
}