118 lines
3.5 KiB
TypeScript
118 lines
3.5 KiB
TypeScript
import React, { createContext, useContext, useState, useCallback, useMemo, useRef, ReactNode } from 'react';
|
|||
|
|
import { DRIVE_FAVORITE_CHANGED_EVENT } from '@/services/drive';
|
||
|
|
|
||
|
|
interface CacheContextType {
|
||
|
|
get: (key: string) => any;
|
||
|
|
set: (key: string, value: any, ttl?: number) => void;
|
||
|
|
invalidate: (key: string) => void;
|
||
|
|
invalidatePattern: (pattern: string) => void;
|
||
|
|
clear: () => void;
|
||
|
|
}
|
||
|
|
|
||
|
|
interface CacheEntry {
|
||
|
|
value: any;
|
||
|
|
expiry: number;
|
||
|
|
}
|
||
|
|
|
||
|
|
const CacheContext = createContext<CacheContextType | undefined>(undefined);
|
||
|
|
|
||
|
|
export const CacheProvider: React.FC<{ children: ReactNode }> = ({ children }) => {
|
||
|
|
const [cache, setCache] = useState<Map<string, CacheEntry>>(new Map());
|
||
|
|
|
||
|
|
const cacheRef = useRef(cache);
|
||
|
|
cacheRef.current = cache;
|
||
|
|
|
||
|
|
const get = useCallback((key: string) => {
|
||
|
|
const entry = cacheRef.current.get(key);
|
||
|
|
if (!entry) return null;
|
||
|
|
|
||
|
|
if (Date.now() > entry.expiry) {
|
||
|
|
setCache(prev => {
|
||
|
|
const newCache = new Map(prev);
|
||
|
|
newCache.delete(key);
|
||
|
|
return newCache;
|
||
|
|
});
|
||
|
|
return null;
|
||
|
|
}
|
||
|
|
|
||
|
|
return entry.value;
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const set = useCallback((key: string, value: any, ttl: number = Infinity) => {
|
||
|
|
setCache(prev => {
|
||
|
|
const newCache = new Map(prev);
|
||
|
|
newCache.set(key, {
|
||
|
|
value,
|
||
|
|
expiry: ttl === Infinity ? Infinity : Date.now() + ttl
|
||
|
|
});
|
||
|
|
return newCache;
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const invalidate = useCallback((key: string) => {
|
||
|
|
setCache(prev => {
|
||
|
|
const newCache = new Map(prev);
|
||
|
|
newCache.delete(key);
|
||
|
|
return newCache;
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const invalidatePattern = useCallback((pattern: string) => {
|
||
|
|
setCache(prev => {
|
||
|
|
const newCache = new Map(prev);
|
||
|
|
const regex = new RegExp(pattern.replace('*', '.*'));
|
||
|
|
|
||
|
|
for (const key of newCache.keys()) {
|
||
|
|
if (regex.test(key)) {
|
||
|
|
newCache.delete(key);
|
||
|
|
}
|
||
|
|
}
|
||
|
|
|
||
|
|
return newCache;
|
||
|
|
});
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
const clear = useCallback(() => {
|
||
|
|
setCache(new Map());
|
||
|
|
}, []);
|
||
|
|
|
||
|
|
React.useEffect(() => {
|
||
|
|
const handleLogout = () => {
|
||
|
|
clear();
|
||
|
|
|
||
|
|
};
|
||
|
|
|
||
|
|
window.addEventListener('auth:logout', handleLogout);
|
||
|
|
return () => window.removeEventListener('auth:logout', handleLogout);
|
||
|
|
}, [clear]);
|
||
|
|
|
||
|
|
React.useEffect(() => {
|
||
|
|
const handleFavoriteChanged = () => {
|
||
|
|
invalidatePattern('mydocs:*');
|
||
|
|
invalidate('favorites:starred');
|
||
|
|
invalidate('dashboard:main');
|
||
|
|
invalidate('recent:combined');
|
||
|
|
invalidate('shared:with_me');
|
||
|
|
};
|
||
|
|
|
||
|
|
window.addEventListener(DRIVE_FAVORITE_CHANGED_EVENT, handleFavoriteChanged);
|
||
|
|
return () => window.removeEventListener(DRIVE_FAVORITE_CHANGED_EVENT, handleFavoriteChanged);
|
||
|
|
}, [invalidate, invalidatePattern]);
|
||
|
|
|
||
|
|
const value = useMemo(() => ({ get, set, invalidate, invalidatePattern, clear }), [get, set, invalidate, invalidatePattern, clear]);
|
||
|
|
|
||
|
|
return (
|
||
|
|
<CacheContext.Provider value={value}>
|
||
|
|
{children}
|
||
|
|
</CacheContext.Provider>
|
||
|
|
);
|
||
|
|
};
|
||
|
|
|
||
|
|
export const useCache = (): CacheContextType => {
|
||
|
|
const context = useContext(CacheContext);
|
||
|
|
if (!context) {
|
||
|
|
throw new Error('useCache must be used within a CacheProvider');
|
||
|
|
}
|
||
|
|
return context;
|
||
|
|
};
|