UNPKG

strata-storage

Version:

Zero-dependency universal storage plugin providing a unified API for all storage operations across web, Android, and iOS platforms

237 lines (236 loc) 6.83 kB
/** * TTL (Time To Live) Manager * Handles automatic expiration of storage items */ import { EventEmitter } from "../utils/index.js"; /** * TTL Manager for handling item expiration */ export class TTLManager extends EventEmitter { config; cleanupInterval; cleanupCallbacks = new Map(); constructor(config = {}) { super(); this.config = { defaultTTL: config.defaultTTL || 0, cleanupInterval: config.cleanupInterval || 60000, autoCleanup: config.autoCleanup ?? true, batchSize: config.batchSize || 100, onExpire: config.onExpire || (() => { }), }; } /** * Calculate expiration timestamp */ calculateExpiration(options) { if (!options) { return this.config.defaultTTL ? Date.now() + this.config.defaultTTL : undefined; } // Specific expiration time takes precedence if (options.expireAt) { const time = options.expireAt instanceof Date ? options.expireAt.getTime() : options.expireAt; return time > Date.now() ? time : undefined; } // Expire after a certain date if (options.expireAfter) { const afterTime = options.expireAfter instanceof Date ? options.expireAfter.getTime() : options.expireAfter; return afterTime; } // TTL from now if (options.ttl) { return Date.now() + options.ttl; } // Use default TTL return this.config.defaultTTL ? Date.now() + this.config.defaultTTL : undefined; } /** * Check if a value is expired */ isExpired(value) { if (!value.expires) return false; return Date.now() > value.expires; } /** * Update expiration for sliding TTL */ updateExpiration(value, options) { if (!options?.sliding || !value.expires) { return value; } const ttl = options.ttl || this.config.defaultTTL; if (!ttl) return value; return { ...value, expires: Date.now() + ttl, updated: Date.now(), }; } /** * Start automatic cleanup */ startAutoCleanup(getKeys, getItem, removeItem) { if (!this.config.autoCleanup || this.cleanupInterval) { return; } this.cleanupInterval = setInterval(async () => { try { await this.cleanup(getKeys, getItem, removeItem); } catch (error) { this.emit('error', error); } }, this.config.cleanupInterval); } /** * Stop automatic cleanup */ stopAutoCleanup() { if (this.cleanupInterval) { clearInterval(this.cleanupInterval); this.cleanupInterval = undefined; } } /** * Perform cleanup of expired items */ async cleanup(getKeys, getItem, removeItem) { const keys = await getKeys(); const expired = []; const expiredKeys = []; const now = Date.now(); let processed = 0; for (const key of keys) { if (processed >= this.config.batchSize) { // Process remaining in next cycle break; } const item = await getItem(key); if (item && item.expires && now > item.expires) { expired.push({ key, value: item.value, expiredAt: now, }); expiredKeys.push(key); await removeItem(key); // Execute any registered cleanup callbacks const callback = this.cleanupCallbacks.get(key); if (callback) { await callback(); this.cleanupCallbacks.delete(key); } } processed++; } if (expiredKeys.length > 0) { this.emit('expired', expiredKeys); this.config.onExpire(expiredKeys); } return expired; } /** * Register a cleanup callback for a specific key */ onCleanup(key, callback) { this.cleanupCallbacks.set(key, callback); } /** * Remove cleanup callback */ removeCleanupCallback(key) { this.cleanupCallbacks.delete(key); } /** * Get all items that will expire within a time window */ async getExpiring(timeWindow, getKeys, getItem) { const keys = await getKeys(); const expiring = []; const now = Date.now(); const windowEnd = now + timeWindow; for (const key of keys) { const item = await getItem(key); if (item && item.expires && item.expires > now && item.expires <= windowEnd) { expiring.push({ key, expiresIn: item.expires - now, }); } } return expiring.sort((a, b) => a.expiresIn - b.expiresIn); } /** * Extend TTL for a key */ extendTTL(value, extension) { if (!value.expires) { return { ...value, expires: Date.now() + extension, updated: Date.now(), }; } return { ...value, expires: value.expires + extension, updated: Date.now(), }; } /** * Set item to never expire */ persist(value) { const { expires: _, ...persistedValue } = value; return { ...persistedValue, updated: Date.now(), }; } /** * Get time until expiration */ getTimeToLive(value) { if (!value.expires) return null; const ttl = value.expires - Date.now(); return ttl > 0 ? ttl : 0; } /** * Format TTL for display */ formatTTL(milliseconds) { const seconds = Math.floor(milliseconds / 1000); const minutes = Math.floor(seconds / 60); const hours = Math.floor(minutes / 60); const days = Math.floor(hours / 24); if (days > 0) { return `${days}d ${hours % 24}h`; } else if (hours > 0) { return `${hours}h ${minutes % 60}m`; } else if (minutes > 0) { return `${minutes}m ${seconds % 60}s`; } else { return `${seconds}s`; } } /** * Clear all data */ clear() { this.stopAutoCleanup(); this.cleanupCallbacks.clear(); this.removeAllListeners(); } } /** * Create a TTL manager instance */ export function createTTLManager(config) { return new TTLManager(config); }