Skip to content
Open
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
160 changes: 159 additions & 1 deletion src/lib/cacheManager.ts
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/**
* Cache Manager
* Centralized cache management with synchronization, invalidation, and storage management
* Centralized cache management with synchronization, invalidation, versioning, and LRU eviction
*/

import { logger } from '@/utils/logger';
Expand All @@ -27,6 +27,10 @@ import {
initPropertyCache,
} from './propertyCache';

// Version migration handlers
type VersionMigration = (data: unknown) => unknown;
const versionMigrations: Map<number, VersionMigration> = new Map();

// Sync queue for offline operations
interface SyncQueueItem {
id: string;
Expand All @@ -41,9 +45,11 @@ let isInitialized = false;
let isOnline = true;
let syncInProgress = false;
let lastSyncTime = 0;
let cacheVersion = DEFAULT_CACHE_CONFIG.version;

// Event listeners
const stateChangeListeners: Set<(online: boolean) => void> = new Set();
const mutationListeners: Map<string, Set<(payload: unknown) => void>> = new Map();

/**
* Initialize the cache manager
Expand All @@ -55,6 +61,9 @@ export const initCacheManager = async (): Promise<void> => {
// Initialize property cache
await initPropertyCache();

// Check and handle cache version migrations
await handleCacheVersionMigration();

// Set up online/offline detection
setupNetworkListeners();

Expand Down Expand Up @@ -298,6 +307,155 @@ export const clearSyncQueue = (): void => {
logger.info('Sync queue cleared');
};

/**
* Handle cache version migrations
*/
const handleCacheVersionMigration = async (): Promise<void> => {
if (typeof window === 'undefined') return;

try {
const storedVersion = parseInt(
localStorage.getItem(LOCAL_STORAGE_KEYS.CACHE_VERSION) || '0',
10
);
const currentVersion = DEFAULT_CACHE_CONFIG.version;

if (storedVersion < currentVersion) {
logger.info(`Migrating cache from v${storedVersion} to v${currentVersion}`);

// Run migrations for each version step
for (let v = storedVersion + 1; v <= currentVersion; v++) {
const migration = versionMigrations.get(v);
if (migration) {
try {
// Get all cached data, migrate it, and clear cache
const { getAllCachedProperties, getAllCachedMobileProperties } = await import(
'./propertyCache'
);

const properties = await getAllCachedProperties();
const mobileProperties = await getAllCachedMobileProperties();

// Apply migration
const migratedProperties = properties.map((entry) => ({
...entry,
data: migration(entry.data) as never,
}));
const migratedMobileProperties = mobileProperties.map((entry) => ({
...entry,
data: migration(entry.data) as never,
}));

// Re-store migrated data
await clearAllCachedProperties();
const { setCachedProperty, setCachedMobileProperty } = await import(
'./propertyCache'
);

for (const entry of migratedProperties) {
await setCachedProperty(entry.data);
}
for (const entry of migratedMobileProperties) {
await setCachedMobileProperty(entry.data);
}

logger.info(`Migration to v${v} completed`);
} catch (error) {
logger.error(`Error running migration to v${v}:`, error);
}
}
}

localStorage.setItem(LOCAL_STORAGE_KEYS.CACHE_VERSION, currentVersion.toString());
cacheVersion = currentVersion;
}
} catch (error) {
logger.error('Error handling cache version migration:', error);
}
};

/**
* Register a version migration handler
*/
export const registerVersionMigration = (
version: number,
handler: (data: unknown) => unknown
): void => {
versionMigrations.set(version, handler);
logger.info(`Registered version migration for v${version}`);
};

/**
* Get current cache version
*/
export const getCacheVersion = (): number => cacheVersion;

/**
* Register mutation listener for cache invalidation
*/
export const onMutation = (
mutationType: string,
handler: (payload: unknown) => void
): (() => void) => {
if (!mutationListeners.has(mutationType)) {
mutationListeners.set(mutationType, new Set());
}
mutationListeners.get(mutationType)!.add(handler);

// Return unsubscribe function
return () => {
mutationListeners.get(mutationType)?.delete(handler);
};
};

/**
* Trigger mutation and invalidate related cache
*/
export const triggerMutation = async (
mutationType: string,
payload: unknown,
invalidationPatterns?: RegExp[]
): Promise<void> => {
try {
// Call all registered listeners
const listeners = mutationListeners.get(mutationType);
if (listeners) {
listeners.forEach((handler) => {
try {
handler(payload);
} catch (error) {
logger.error(`Error calling mutation listener for ${mutationType}:`, error);
}
});
}

// Invalidate cache based on patterns
if (invalidationPatterns && invalidationPatterns.length > 0) {
for (const pattern of invalidationPatterns) {
await invalidateCache(pattern);
}
}

// Track invalidation in stats
const stats = await updateCacheStats();
if (stats && 'invalidationCount' in stats) {
const event: CacheEvent = {
type: 'invalidate',
key: mutationType,
timestamp: Date.now(),
reason: `Mutation: ${mutationType}`,
};
addCacheEventListener((listener) => {
listener(event);
});
}

logger.info(`Mutation triggered: ${mutationType}, invalidated cache patterns`);
} catch (error) {
logger.error('Error triggering mutation:', error);
}
};

/**
* Invalidate cache entries by pattern
*/
Expand Down
Loading