UNPKG

@toast-studios/asset-manager

Version:

A React Native asset management library with intelligent caching and loading strategies

507 lines (506 loc) 19.3 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.ToastAssetManager = void 0; const index_1 = require("../types/index"); const NetworkAwareStrategy_1 = require("../strategies/NetworkAwareStrategy"); const NetworkMonitor_1 = require("../utils/NetworkMonitor"); const StorageManager_1 = require("../utils/StorageManager"); const DownloadManager_1 = require("../utils/DownloadManager"); /** * Main Toast Asset Manager class for intelligent asset downloading and management */ class ToastAssetManager { constructor(config) { var _a, _b, _c, _d, _e; // State management this.downloadQueue = new Map(); this.processing = false; this.destroyed = false; // Initialize configuration with defaults this.config = { basePath: config.basePath || '/assets', strategy: config.strategy || index_1.DownloadStrategy.NETWORK_AWARE, maxConcurrentDownloads: config.maxConcurrentDownloads || 3, enableNetworkDetection: (_a = config.enableNetworkDetection) !== null && _a !== void 0 ? _a : true, pingTimeout: config.pingTimeout || 5000, pingTargets: config.pingTargets, appVersion: config.appVersion, manifestUrl: config.manifestUrl, appLanguage: config.appLanguage, retryAttempts: config.retryAttempts || 3, retryDelay: config.retryDelay || 1000, enableIntegrityCheck: (_b = config.enableIntegrityCheck) !== null && _b !== void 0 ? _b : true, enableAutoCleanup: (_c = config.enableAutoCleanup) !== null && _c !== void 0 ? _c : true, cacheLimit: (_d = config.cacheLimit) !== null && _d !== void 0 ? _d : 500 * 1024 * 1024, customHeaders: config.customHeaders || {}, enableLogging: (_e = config.enableLogging) !== null && _e !== void 0 ? _e : false, }; // Initialize components this.networkMonitor = new NetworkMonitor_1.NetworkMonitor(this.config.pingTimeout, this.config.pingTargets); this.storageManager = new StorageManager_1.StorageManager(this.config.basePath, this.config.enableLogging); this.downloadManager = new DownloadManager_1.DownloadManager(this.storageManager, this.config.enableLogging, this.config.customHeaders); // Initialize strategy (always NetworkAware for optimization) this.strategy = new NetworkAwareStrategy_1.NetworkAwareStrategy(); this.log('ToastAssetManager initialized'); } /** * Start the complete asset management process - initialize, load manifest, and begin downloads */ async start(manifest) { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } try { this.log('Starting ToastAssetManager...'); // Step 1: Initialize the system await this.initialize(); // Step 2: Load manifest await this.loadManifest(manifest); // Step 3: Start downloads await this.startDownloads(); this.log('ToastAssetManager started successfully'); } catch (error) { throw new Error(`Failed to start ToastAssetManager: ${error}`); } } /** * Initialize the asset manager */ async initialize() { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } try { // Initialize storage await this.storageManager.initialize(); // Start network monitoring if enabled if (this.config.enableNetworkDetection) { this.currentNetworkConditions = await this.networkMonitor.getCurrentConditions(); this.networkMonitor.startMonitoring(conditions => { this.handleNetworkChange(conditions); }); } // Start auto cleanup if enabled if (this.config.enableAutoCleanup) { this.startAutoCleanup(); } this.log('ToastAssetManager initialization completed'); } catch (error) { throw new Error(`Failed to initialize ToastAssetManager: ${error}`); } } /** * Load asset manifest - either from parameter or from configured URL */ async loadManifest(manifest) { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } try { if (manifest) { // Load provided manifest directly this.manifest = manifest; this.log(`Loaded manifest from parameter: ${manifest.bundles.length} bundles, ${manifest.version}`); } else if (this.config.manifestUrl) { // Fetch manifest from URL this.manifest = await this.fetchManifestFromUrl(); this.log(`Loaded manifest from URL: ${this.manifest.bundles.length} bundles, ${this.manifest.version}`); } else { throw new Error('No manifest provided and no manifestUrl configured'); } } catch (error) { throw new Error(`Failed to load manifest: ${error}`); } } /** * Fetch manifest from the configured URL with app version and language parameters */ async fetchManifestFromUrl() { if (!this.config.manifestUrl) { throw new Error('Manifest URL is not configured'); } try { // Build URL with query parameters const url = new URL(this.config.manifestUrl); if (this.config.appVersion) { url.searchParams.append('appVersion', this.config.appVersion); } if (this.config.appLanguage) { url.searchParams.append('language', this.config.appLanguage); } this.log(`Fetching manifest from: ${url.toString()}`); // Make GET request with timeout const controller = new AbortController(); const timeoutId = setTimeout(() => controller.abort(), this.config.pingTimeout || 30000); const response = await fetch(url.toString(), { method: 'GET', headers: { Accept: 'application/json', 'Content-Type': 'application/json', ...this.config.customHeaders, }, signal: controller.signal, }); clearTimeout(timeoutId); if (!response.ok) { throw new Error(`HTTP ${response.status}: ${response.statusText}`); } const manifest = (await response.json()); // Validate manifest structure if (!manifest.version || !manifest.bundles || !Array.isArray(manifest.bundles)) { throw new Error('Invalid manifest structure: missing version or bundles array'); } this.log(`Fetched manifest version: ${manifest.version} with ${manifest.bundles.length} bundles`); return manifest; } catch (error) { if (error instanceof TypeError && error.message.includes('fetch')) { throw new Error(`Network error while fetching manifest: ${error.message}`); } if (error instanceof Error && error.name === 'AbortError') { throw new Error('Manifest fetch request timed out'); } throw error; } } /** * Start downloading assets based on current strategy */ async startDownloads() { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } if (!this.manifest) { throw new Error('No manifest loaded'); } if (this.processing) { this.log('Downloads already in progress'); return; } this.processing = true; try { // Get current network conditions const conditions = this.currentNetworkConditions || (await this.networkMonitor.getCurrentConditions()); // Filter and prioritize bundles based on strategy const bundlesToDownload = this.manifest.bundles.filter(bundle => this.strategy.shouldDownload(bundle, conditions)); const prioritizedBundles = this.strategy.prioritizeBundles(bundlesToDownload, conditions); // Add bundles to download queue for (const bundle of prioritizedBundles) { await this.queueDownload(bundle); } // Start processing queue await this.processDownloadQueue(); } catch (error) { this.log(`Error starting downloads: ${error}`); throw error; } finally { this.processing = false; } } /** * Check if game is ready to play (all default assets downloaded) */ async isGameReadyToPlay() { var _a; if (!this.manifest) { return false; } try { // Check if all default assets are available for (const assetId of this.manifest.defaultAssets) { const asset = this.findAssetById(assetId); if (!asset) continue; const assetPath = this.getAssetPath(asset); const exists = await this.storageManager.assetExistsById(asset.id, assetPath, asset.etag); if (!exists) { return false; } // Validate integrity if enabled if (this.config.enableIntegrityCheck && asset.hash) { const validation = await this.storageManager.validateAssetById(asset.id, assetPath, asset.hash); if (!validation.isValid) { return false; } } } (_a = this.gameReadyCallback) === null || _a === void 0 ? void 0 : _a.call(this, true); return true; } catch (error) { this.log(`Error checking game readiness: ${error}`); return false; } } /** * Get path to a specific asset */ getAssetPath(asset) { return this.storageManager.getExtractedPath(asset.key); } /** * Get asset by ID */ getAsset(assetId) { return this.findAssetById(assetId); } /** * Get storage information */ async getStorageInfo() { return this.storageManager.getStorageInfo(); } /** * Get hash-based deduplication statistics */ async getDeduplicationStats() { const stats = await this.storageManager.getDeduplicationStats(); return { ...stats, savedSpaceMB: Math.round((stats.savedSpace / (1024 * 1024)) * 100) / 100, // Convert to MB }; } /** * Get the actual file path for an asset by ID (resolves hash-based references) */ async getAssetPathById(assetId) { return this.storageManager.getAssetPath(assetId); } /** * Cleanup old assets */ async cleanup() { const options = this.config.cacheLimit ? { maxSize: this.config.cacheLimit } : {}; const deletedCount = await this.storageManager.cleanup(options); // Also cleanup temporary files await this.downloadManager.cleanupTempFiles(); return deletedCount; } /** * Set progress callback for download updates */ onProgress(callback) { this.progressCallback = callback; } /** * Set game ready callback */ onGameReady(callback) { this.gameReadyCallback = callback; } /** * Set network change callback */ onNetworkChange(callback) { this.networkChangeCallback = callback; } /** * Destroy the asset manager and cleanup resources */ async destroy() { var _a, _b; if (this.destroyed) return; this.destroyed = true; this.processing = false; // Stop all downloads this.downloadManager.cancelAllDownloads(); // Stop monitoring this.networkMonitor.stopMonitoring(); // Cleanup components (_b = (_a = this.networkMonitor).destroy) === null || _b === void 0 ? void 0 : _b.call(_a); this.downloadManager.destroy(); await this.storageManager.destroy(); // Ensure batched saves are flushed // Clear state this.downloadQueue.clear(); this.progressCallback = undefined; this.gameReadyCallback = undefined; this.networkChangeCallback = undefined; this.log('ToastAssetManager destroyed'); } /** * Manually flush all pending storage operations to disk * Useful before critical operations or app shutdown */ async flushStorage() { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } await this.storageManager.flush(); this.log('Storage operations flushed to disk'); } /** * Get storage batching statistics * Useful for monitoring and debugging performance */ getStorageBatchingStats() { if (this.destroyed) { throw new Error('ToastAssetManager has been destroyed'); } return this.storageManager.getBatchingStats(); } /** * Queue a bundle for download */ async queueDownload(bundle) { if (this.downloadQueue.has(bundle.id)) { return; // Already queued } // Check if bundle already exists and is valid const bundlePath = this.storageManager.getBundlePath(`${bundle.id}.zip`); const exists = await this.storageManager.assetExistsById(bundle.id, bundlePath, bundle.etag); if (exists && bundle.hash) { const validation = await this.storageManager.validateAssetById(bundle.id, bundlePath, bundle.hash); if (validation.isValid) { this.log(`Bundle ${bundle.id} already exists and is valid, skipping download`); return; } } const queueItem = { bundle, priority: this.getBundlePriority(bundle), retryCount: 0, }; this.downloadQueue.set(bundle.id, queueItem); } /** * Process download queue with concurrency control */ async processDownloadQueue() { const conditions = this.currentNetworkConditions || (await this.networkMonitor.getCurrentConditions()); const maxConcurrent = Math.min(this.config.maxConcurrentDownloads, this.strategy.getMaxConcurrentDownloads(conditions)); const activeDownloads = this.downloadManager.getActiveDownloadCount(); const availableSlots = maxConcurrent - activeDownloads; if (availableSlots <= 0 || this.downloadQueue.size === 0) { return; } // Sort queue by priority const sortedQueue = Array.from(this.downloadQueue.values()).sort((a, b) => a.priority - b.priority); // Start downloads for available slots const downloadsToStart = sortedQueue.slice(0, availableSlots); for (const queueItem of downloadsToStart) { this.downloadQueue.delete(queueItem.bundle.id); this.startBundleDownload(queueItem); } } /** * Start downloading a bundle */ async startBundleDownload(queueItem) { var _a; const { bundle } = queueItem; try { await this.downloadManager.downloadBundle(bundle, progress => { var _a; (_a = this.progressCallback) === null || _a === void 0 ? void 0 : _a.call(this, progress); }); // Check if game is ready after each successful download const isReady = await this.isGameReadyToPlay(); if (isReady) { (_a = this.gameReadyCallback) === null || _a === void 0 ? void 0 : _a.call(this, true); } } catch (error) { // Retry logic if (queueItem.retryCount < this.config.retryAttempts) { queueItem.retryCount++; // Add delay before retry setTimeout(() => { if (!this.destroyed) { this.downloadQueue.set(bundle.id, queueItem); this.processDownloadQueue(); } }, this.config.retryDelay * queueItem.retryCount); } else { this.log(`Max retries exceeded for bundle: ${bundle.id}`); } } // Continue processing queue if (!this.destroyed) { await this.processDownloadQueue(); } } /** * Get bundle priority */ getBundlePriority(bundle) { if (bundle.isCritical) { return index_1.DownloadPriority.CRITICAL; } // Calculate average priority of assets in bundle if (bundle.assets.length === 0) { return index_1.DownloadPriority.LOW; } const totalPriority = bundle.assets.reduce((sum, asset) => sum + asset.priority, 0); return Math.round(totalPriority / bundle.assets.length); } /** * Find asset by ID across all bundles */ findAssetById(assetId) { if (!this.manifest) return undefined; for (const bundle of this.manifest.bundles) { const asset = bundle.assets.find(a => a.id === assetId); if (asset) return asset; } return undefined; } /** * Handle network condition changes */ async handleNetworkChange(conditions) { var _a; this.currentNetworkConditions = conditions; (_a = this.networkChangeCallback) === null || _a === void 0 ? void 0 : _a.call(this, conditions); // Adjust concurrent downloads based on new conditions if (this.processing) { const newMaxConcurrent = this.strategy.getMaxConcurrentDownloads(conditions); const currentActive = this.downloadManager.getActiveDownloadCount(); // If network improved, start more downloads if (newMaxConcurrent > currentActive && this.downloadQueue.size > 0) { await this.processDownloadQueue(); } } } /** * Start automatic cleanup */ startAutoCleanup() { setInterval(async () => { if (this.destroyed) return; try { await this.cleanup(); } catch (error) { this.log(`Auto cleanup error: ${error}`); } }, 60 * 24 * 60 * 60 * 1000); // Daily cleanup } /** * Log message if logging is enabled */ log(message) { if (this.config.enableLogging) { console.log(`[ToastAssetManager] ${message}`); } } } exports.ToastAssetManager = ToastAssetManager;