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
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}`);
|
|
}
|
|
}
|
|
}
|
|
|