/** * TimeSafari Storage Adapter * * Implements storage adapter pattern for TimeSafari database integration. * Supports SQLite (native) and fallback storage. * * @author Matthew Raymer * @version 1.0.0 */ import { TimeSafariStorageAdapter } from './timesafari-integration'; /** * TimeSafari Storage Adapter Implementation * * Provides unified storage interface across Android (SQLite) and iOS (SQLite) * platforms with TimeSafari-specific patterns. */ export class TimeSafariStorageAdapterImpl implements TimeSafariStorageAdapter { private storage: StorageInterface; private prefix: string; private ttlManager: TTLManager; constructor(storage: StorageInterface, prefix = 'timesafari_notifications') { this.storage = storage; this.prefix = prefix; this.ttlManager = new TTLManager(); } /** * Store data with TTL support * * @param key - Storage key * @param value - Value to store * @param ttlSeconds - Optional TTL in seconds */ async store(key: string, value: unknown, ttlSeconds?: number): Promise { try { const prefixedKey = this.getPrefixedKey(key); const storageValue = { data: value, timestamp: Date.now(), ttl: ttlSeconds ? Date.now() + (ttlSeconds * 1000) : null }; await this.storage.setItem(prefixedKey, JSON.stringify(storageValue)); // Register TTL if provided if (ttlSeconds) { this.ttlManager.registerTTL(prefixedKey, ttlSeconds); } // Successfully stored key } catch (error) { throw new Error(`Failed to store key: ${key}`); } } /** * Retrieve data with TTL validation * * @param key - Storage key * @returns Stored value or null if not found/expired */ async retrieve(key: string): Promise { try { const prefixedKey = this.getPrefixedKey(key); const stored = await this.storage.getItem(prefixedKey); if (!stored) { return null; } const storageValue = JSON.parse(stored); // Check TTL if (storageValue.ttl && Date.now() > storageValue.ttl) { // Key expired, cleaning up await this.remove(key); // Clean up expired data return null; } // Successfully retrieved key return storageValue.data; } catch (error) { // Failed to retrieve key, returning null return null; } } /** * Remove data from storage * * @param key - Storage key */ async remove(key: string): Promise { try { const prefixedKey = this.getPrefixedKey(key); await this.storage.removeItem(prefixedKey); this.ttlManager.unregisterTTL(prefixedKey); // Successfully removed key } catch (error) { throw new Error(`Failed to remove key: ${key}`); } } /** * Clear all data with prefix */ async clear(): Promise { try { const keys = await this.storage.getAllKeys(); const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); for (const key of prefixedKeys) { await this.storage.removeItem(key); this.ttlManager.unregisterTTL(key); } // Successfully cleared storage } catch (error) { throw new Error('Failed to clear storage'); } } /** * Get prefixed key * * @param key - Original key * @returns Prefixed key */ private getPrefixedKey(key: string): string { return `${this.prefix}_${key}`; } /** * Clean up expired data */ async cleanupExpired(): Promise { try { const keys = await this.storage.getAllKeys(); const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); for (const key of prefixedKeys) { const stored = await this.storage.getItem(key); if (stored) { const storageValue = JSON.parse(stored); if (storageValue.ttl && Date.now() > storageValue.ttl) { await this.storage.removeItem(key); this.ttlManager.unregisterTTL(key); // Cleaned up expired key } } } } catch (error) { // Failed to cleanup expired data, continuing } } /** * Get storage statistics * * @returns Storage statistics */ async getStats(): Promise { try { const keys = await this.storage.getAllKeys(); const prefixedKeys = keys.filter(key => key.startsWith(this.prefix)); let totalSize = 0; let expiredCount = 0; let validCount = 0; for (const key of prefixedKeys) { const stored = await this.storage.getItem(key); if (stored) { totalSize += stored.length; const storageValue = JSON.parse(stored); if (storageValue.ttl && Date.now() > storageValue.ttl) { expiredCount++; } else { validCount++; } } } return { totalKeys: prefixedKeys.length, validKeys: validCount, expiredKeys: expiredCount, totalSizeBytes: totalSize, prefix: this.prefix }; } catch (error) { // Failed to get stats, returning default return { totalKeys: 0, validKeys: 0, expiredKeys: 0, totalSizeBytes: 0, prefix: this.prefix }; } } } /** * Storage Interface */ export interface StorageInterface { getItem(key: string): Promise; setItem(key: string, value: string): Promise; removeItem(key: string): Promise; getAllKeys(): Promise; } /** * TTL Manager for managing time-to-live data */ class TTLManager { private ttlMap: Map = new Map(); /** * Register TTL for a key * * @param key - Storage key * @param ttlSeconds - TTL in seconds */ registerTTL(key: string, ttlSeconds: number): void { // Clear existing TTL if any this.unregisterTTL(key); // Set new TTL const timeout = setTimeout(() => { // TTL expired for key this.ttlMap.delete(key); }, ttlSeconds * 1000); this.ttlMap.set(key, timeout); } /** * Unregister TTL for a key * * @param key - Storage key */ unregisterTTL(key: string): void { const timeout = this.ttlMap.get(key); if (timeout) { clearTimeout(timeout); this.ttlMap.delete(key); } } /** * Clear all TTLs */ clearAll(): void { for (const timeout of this.ttlMap.values()) { clearTimeout(timeout); } this.ttlMap.clear(); } } /** * Storage Statistics */ export interface StorageStats { totalKeys: number; validKeys: number; expiredKeys: number; totalSizeBytes: number; prefix: string; } /** * LocalStorage Storage Implementation for Web (fallback) */ export class LocalStorageStorage implements StorageInterface { async getItem(key: string): Promise { return localStorage.getItem(key); } async setItem(key: string, value: string): Promise { localStorage.setItem(key, value); } async removeItem(key: string): Promise { localStorage.removeItem(key); } async getAllKeys(): Promise { const keys: string[] = []; for (let i = 0; i < localStorage.length; i++) { const key = localStorage.key(i); if (key) keys.push(key); } return keys; } } /** * Storage Factory */ export class StorageFactory { /** * Create appropriate storage implementation * * @param platform - Platform type * @returns Storage implementation */ static createStorage(platform: 'android' | 'ios' | 'electron'): StorageInterface { switch (platform) { case 'android': case 'ios': // For native platforms, this would integrate with SQLite // For now, return localStorage as placeholder return new LocalStorageStorage(); case 'electron': // Electron can use either SQLite or localStorage // For now, return localStorage as placeholder return new LocalStorageStorage(); default: throw new Error(`Unsupported platform: ${platform}`); } } }