UNPKG

native-update

Version:

Foundation package for building a comprehensive update system for Capacitor apps. Provides architecture and interfaces but requires backend implementation.

325 lines 11.1 kB
import { ConfigManager } from '../core/config'; import { Logger } from '../core/logger'; import { StorageError, ErrorCode } from '../core/errors'; /** * Manages bundle storage and lifecycle */ export class BundleManager { constructor() { this.STORAGE_KEY = 'capacitor_native_update_bundles'; this.ACTIVE_BUNDLE_KEY = 'capacitor_native_update_active'; this.preferences = null; this.cache = new Map(); this.cacheExpiry = 0; this.logger = Logger.getInstance(); this.configManager = ConfigManager.getInstance(); } /** * Initialize the bundle manager with preferences */ async initialize() { this.preferences = this.configManager.get('preferences'); if (!this.preferences) { throw new StorageError(ErrorCode.MISSING_DEPENDENCY, 'Preferences not configured. Please configure the plugin first.'); } await this.loadCache(); } /** * Load cache from preferences */ async loadCache() { if (Date.now() < this.cacheExpiry) { return; // Cache still valid } try { const { value } = await this.preferences.get({ key: this.STORAGE_KEY }); if (value) { const bundles = JSON.parse(value); this.cache.clear(); bundles.forEach((bundle) => this.cache.set(bundle.bundleId, bundle)); } this.cacheExpiry = Date.now() + 5000; // 5 second cache } catch (error) { this.logger.error('Failed to load bundles from storage', error); this.cache.clear(); } } /** * Save cache to preferences */ async saveCache() { try { const bundles = Array.from(this.cache.values()); await this.preferences.set({ key: this.STORAGE_KEY, value: JSON.stringify(bundles), }); this.logger.debug('Saved bundles to storage', { count: bundles.length }); } catch (error) { throw new StorageError(ErrorCode.STORAGE_FULL, 'Failed to save bundles to storage', undefined, error); } } /** * Save bundle information */ async saveBundleInfo(bundle) { this.validateBundleInfo(bundle); this.cache.set(bundle.bundleId, bundle); await this.saveCache(); this.logger.info('Bundle saved', { bundleId: bundle.bundleId, version: bundle.version, }); } /** * Validate bundle information */ validateBundleInfo(bundle) { if (!bundle.bundleId || typeof bundle.bundleId !== 'string') { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Invalid bundle ID'); } if (!bundle.version || typeof bundle.version !== 'string') { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Invalid bundle version'); } if (!bundle.path || typeof bundle.path !== 'string') { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Invalid bundle path'); } if (typeof bundle.size !== 'number' || bundle.size < 0) { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Invalid bundle size'); } } /** * Get all bundles */ async getAllBundles() { await this.loadCache(); return Array.from(this.cache.values()); } /** * Get bundle by ID */ async getBundle(bundleId) { if (!bundleId) { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID is required'); } await this.loadCache(); return this.cache.get(bundleId) || null; } /** * Delete bundle */ async deleteBundle(bundleId) { if (!bundleId) { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID is required'); } await this.loadCache(); const bundle = this.cache.get(bundleId); if (!bundle) { this.logger.warn('Attempted to delete non-existent bundle', { bundleId }); return; } this.cache.delete(bundleId); await this.saveCache(); // If this was the active bundle, clear it const activeBundleId = await this.getActiveBundleId(); if (activeBundleId === bundleId) { await this.clearActiveBundle(); } this.logger.info('Bundle deleted', { bundleId }); } /** * Get active bundle */ async getActiveBundle() { const activeBundleId = await this.getActiveBundleId(); if (!activeBundleId) return null; return this.getBundle(activeBundleId); } /** * Set active bundle */ async setActiveBundle(bundleId) { if (!bundleId) { throw new StorageError(ErrorCode.INVALID_BUNDLE_FORMAT, 'Bundle ID is required'); } const bundle = await this.getBundle(bundleId); if (!bundle) { throw new StorageError(ErrorCode.FILE_NOT_FOUND, `Bundle ${bundleId} not found`); } // Update previous active bundle status const previousActive = await this.getActiveBundle(); if (previousActive && previousActive.bundleId !== bundleId) { previousActive.status = 'READY'; await this.saveBundleInfo(previousActive); } // Set new active bundle bundle.status = 'ACTIVE'; await this.saveBundleInfo(bundle); await this.preferences.set({ key: this.ACTIVE_BUNDLE_KEY, value: bundleId, }); this.logger.info('Active bundle set', { bundleId, version: bundle.version, }); } /** * Get active bundle ID */ async getActiveBundleId() { try { const { value } = await this.preferences.get({ key: this.ACTIVE_BUNDLE_KEY, }); return value; } catch (error) { this.logger.error('Failed to get active bundle ID', error); return null; } } /** * Clear active bundle */ async clearActiveBundle() { await this.preferences.remove({ key: this.ACTIVE_BUNDLE_KEY }); this.logger.info('Active bundle cleared'); } /** * Clear all bundles */ async clearAllBundles() { await this.preferences.remove({ key: this.STORAGE_KEY }); await this.preferences.remove({ key: this.ACTIVE_BUNDLE_KEY }); this.cache.clear(); this.cacheExpiry = 0; this.logger.info('All bundles cleared'); } /** * Clean up old bundles */ async cleanupOldBundles(keepCount) { if (keepCount < 1) { throw new StorageError(ErrorCode.INVALID_CONFIG, 'Keep count must be at least 1'); } const bundles = await this.getAllBundles(); const activeBundleId = await this.getActiveBundleId(); // Sort by download time (newest first) const sorted = bundles.sort((a, b) => b.downloadTime - a.downloadTime); // Keep the active bundle and the most recent ones const toKeep = new Set(); if (activeBundleId) { toKeep.add(activeBundleId); } let kept = toKeep.size; for (const bundle of sorted) { if (kept >= keepCount) break; if (!toKeep.has(bundle.bundleId)) { toKeep.add(bundle.bundleId); kept++; } } // Delete bundles not in the keep set let deletedCount = 0; for (const bundle of bundles) { if (!toKeep.has(bundle.bundleId)) { await this.deleteBundle(bundle.bundleId); deletedCount++; } } if (deletedCount > 0) { this.logger.info('Cleaned up old bundles', { deleted: deletedCount, kept, }); } } /** * Get bundles older than specified time */ async getBundlesOlderThan(timestamp) { if (timestamp < 0) { throw new StorageError(ErrorCode.INVALID_CONFIG, 'Timestamp must be non-negative'); } const bundles = await this.getAllBundles(); return bundles.filter((b) => b.downloadTime < timestamp); } /** * Mark bundle as verified */ async markBundleAsVerified(bundleId) { const bundle = await this.getBundle(bundleId); if (!bundle) { throw new StorageError(ErrorCode.FILE_NOT_FOUND, `Bundle ${bundleId} not found`); } bundle.verified = true; await this.saveBundleInfo(bundle); this.logger.info('Bundle marked as verified', { bundleId }); } /** * Get total storage used by bundles */ async getTotalStorageUsed() { const bundles = await this.getAllBundles(); return bundles.reduce((total, bundle) => total + bundle.size, 0); } /** * Check if storage limit is exceeded */ async isStorageLimitExceeded(additionalSize = 0) { const totalUsed = await this.getTotalStorageUsed(); // Try to get actual device storage quota let maxStorage = this.configManager.get('maxBundleSize') * 3; // Default: 3x max bundle size try { if ('storage' in navigator && 'estimate' in navigator.storage) { const estimate = await navigator.storage.estimate(); if (estimate.quota) { // Use actual quota, but keep 100MB buffer for system const bufferBytes = 100 * 1024 * 1024; // 100MB maxStorage = Math.max(maxStorage, estimate.quota - bufferBytes); } } } catch (_a) { // Storage API not available, use config-based limit this.logger.warn('Storage API not available for quota check, using config limit'); } return totalUsed + additionalSize > maxStorage; } /** * Create default bundle */ createDefaultBundle() { return { bundleId: 'default', version: '1.0.0', path: '/', downloadTime: Date.now(), size: 0, status: 'ACTIVE', checksum: '', verified: true, }; } /** * Clean expired cache entries */ async cleanExpiredBundles() { const expirationTime = this.configManager.get('cacheExpiration'); const cutoffTime = Date.now() - expirationTime; const expiredBundles = await this.getBundlesOlderThan(cutoffTime); for (const bundle of expiredBundles) { // Don't delete active bundle const activeBundleId = await this.getActiveBundleId(); if (bundle.bundleId !== activeBundleId) { await this.deleteBundle(bundle.bundleId); } } } } //# sourceMappingURL=bundle-manager.js.map