mirror of
https://github.com/Memo-2023/mana-monorepo.git
synced 2026-05-22 17:06:42 +02:00
feat: unify utilities into shared packages (Tier 1)
- Add LanguageSelector component to @manacore/shared-i18n - Add keyboard.ts to @manacore/shared-utils (shortcuts handling) - Add cache.ts to @manacore/shared-utils (IndexedDB caching) - Extend format.ts and date.ts with additional utilities - Update Memoro to use shared utilities with app-specific wrappers 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
7d426d57fd
commit
74ccad38d5
15 changed files with 877 additions and 647 deletions
205
packages/shared-utils/src/cache.ts
Normal file
205
packages/shared-utils/src/cache.ts
Normal file
|
|
@ -0,0 +1,205 @@
|
|||
/**
|
||||
* IndexedDB Cache Utility
|
||||
* Provides persistent storage for cached data like signed URLs
|
||||
* Survives page reloads and provides better performance than localStorage
|
||||
*/
|
||||
|
||||
interface CacheEntry<T = string> {
|
||||
id: string;
|
||||
data: T;
|
||||
expires: number;
|
||||
createdAt: number;
|
||||
}
|
||||
|
||||
interface CacheConfig {
|
||||
/** Database name */
|
||||
dbName: string;
|
||||
/** Store name */
|
||||
storeName: string;
|
||||
/** Database version */
|
||||
version?: number;
|
||||
}
|
||||
|
||||
let dbPromises: Map<string, Promise<IDBDatabase>> = new Map();
|
||||
|
||||
/**
|
||||
* Initialize IndexedDB connection
|
||||
*/
|
||||
function getDB(config: CacheConfig): Promise<IDBDatabase> {
|
||||
const key = `${config.dbName}:${config.storeName}`;
|
||||
|
||||
if (dbPromises.has(key)) {
|
||||
return dbPromises.get(key)!;
|
||||
}
|
||||
|
||||
const promise = new Promise<IDBDatabase>((resolve, reject) => {
|
||||
if (typeof indexedDB === 'undefined') {
|
||||
reject(new Error('IndexedDB not available'));
|
||||
return;
|
||||
}
|
||||
|
||||
const request = indexedDB.open(config.dbName, config.version || 1);
|
||||
|
||||
request.onerror = () => reject(request.error);
|
||||
request.onsuccess = () => resolve(request.result);
|
||||
|
||||
request.onupgradeneeded = (event) => {
|
||||
const db = (event.target as IDBOpenDBRequest).result;
|
||||
|
||||
// Create store if it doesn't exist
|
||||
if (!db.objectStoreNames.contains(config.storeName)) {
|
||||
const store = db.createObjectStore(config.storeName, { keyPath: 'id' });
|
||||
store.createIndex('expires', 'expires', { unique: false });
|
||||
}
|
||||
};
|
||||
});
|
||||
|
||||
dbPromises.set(key, promise);
|
||||
return promise;
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a cache instance for a specific database/store
|
||||
*/
|
||||
export function createCache<T = string>(config: CacheConfig) {
|
||||
return {
|
||||
/**
|
||||
* Get cached data by ID
|
||||
* Returns null if not found or expired
|
||||
*/
|
||||
async get(id: string): Promise<T | null> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readonly');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
|
||||
return new Promise((resolve) => {
|
||||
const request = store.get(id);
|
||||
|
||||
request.onsuccess = () => {
|
||||
const result = request.result as CacheEntry<T> | undefined;
|
||||
if (result && Date.now() < result.expires) {
|
||||
resolve(result.data);
|
||||
} else {
|
||||
resolve(null);
|
||||
}
|
||||
};
|
||||
|
||||
request.onerror = () => {
|
||||
console.error('Error getting cached data:', request.error);
|
||||
resolve(null);
|
||||
};
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Store data in cache
|
||||
* @param id - Unique identifier for the cached data
|
||||
* @param data - Data to cache
|
||||
* @param expiresInMs - Time until expiration in milliseconds
|
||||
*/
|
||||
async set(id: string, data: T, expiresInMs: number): Promise<void> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readwrite');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
|
||||
const entry: CacheEntry<T> = {
|
||||
id,
|
||||
data,
|
||||
expires: Date.now() + expiresInMs,
|
||||
createdAt: Date.now()
|
||||
};
|
||||
|
||||
store.put(entry);
|
||||
} catch (error) {
|
||||
console.error('Error caching data:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete cached data by ID
|
||||
*/
|
||||
async delete(id: string): Promise<void> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readwrite');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
store.delete(id);
|
||||
} catch (error) {
|
||||
console.error('Error deleting cached data:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clean up expired entries
|
||||
* Should be called periodically
|
||||
*/
|
||||
async cleanupExpired(): Promise<void> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readwrite');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
const index = store.index('expires');
|
||||
const now = Date.now();
|
||||
|
||||
const request = index.openCursor(IDBKeyRange.upperBound(now));
|
||||
|
||||
request.onsuccess = (event) => {
|
||||
const cursor = (event.target as IDBRequest).result;
|
||||
if (cursor) {
|
||||
cursor.delete();
|
||||
cursor.continue();
|
||||
}
|
||||
};
|
||||
} catch (error) {
|
||||
console.error('Error cleaning up expired entries:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Clear all cached data
|
||||
*/
|
||||
async clear(): Promise<void> {
|
||||
try {
|
||||
const db = await getDB(config);
|
||||
const transaction = db.transaction(config.storeName, 'readwrite');
|
||||
const store = transaction.objectStore(config.storeName);
|
||||
store.clear();
|
||||
} catch (error) {
|
||||
console.error('Error clearing cache:', error);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Check if an item exists and is not expired
|
||||
*/
|
||||
async has(id: string): Promise<boolean> {
|
||||
const data = await this.get(id);
|
||||
return data !== null;
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
// Default URL cache for signed URLs (like Supabase storage URLs)
|
||||
const DEFAULT_URL_CACHE_CONFIG: CacheConfig = {
|
||||
dbName: 'manacore-cache',
|
||||
storeName: 'urls',
|
||||
version: 1
|
||||
};
|
||||
|
||||
/**
|
||||
* Pre-configured cache for signed URLs
|
||||
* Useful for caching Supabase storage signed URLs
|
||||
*/
|
||||
export const urlCache = createCache<string>(DEFAULT_URL_CACHE_CONFIG);
|
||||
|
||||
// Convenience exports for URL caching (backward compatibility)
|
||||
export const getCachedUrl = urlCache.get.bind(urlCache);
|
||||
export const setCachedUrl = urlCache.set.bind(urlCache);
|
||||
export const deleteCachedUrl = urlCache.delete.bind(urlCache);
|
||||
export const cleanupExpiredUrls = urlCache.cleanupExpired.bind(urlCache);
|
||||
export const clearAllCachedUrls = urlCache.clear.bind(urlCache);
|
||||
Loading…
Add table
Add a link
Reference in a new issue