You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

330 lines
8.0 KiB

/**
* 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<void> {
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<unknown> {
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<void> {
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<void> {
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<void> {
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<StorageStats> {
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<string | null>;
setItem(key: string, value: string): Promise<void>;
removeItem(key: string): Promise<void>;
getAllKeys(): Promise<string[]>;
}
/**
* TTL Manager for managing time-to-live data
*/
class TTLManager {
private ttlMap: Map<string, NodeJS.Timeout> = 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<string | null> {
return localStorage.getItem(key);
}
async setItem(key: string, value: string): Promise<void> {
localStorage.setItem(key, value);
}
async removeItem(key: string): Promise<void> {
localStorage.removeItem(key);
}
async getAllKeys(): Promise<string[]> {
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}`);
}
}
}