UNPKG

@toast-studios/asset-manager

Version:

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

295 lines (294 loc) 11.2 kB
"use strict"; var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.DownloadManager = void 0; const react_native_fs_1 = __importDefault(require("react-native-fs")); // import { unzip } from "react-native-zip-archive"; // TODO: Add back when zip extraction is needed const index_1 = require("../types/index"); /** * Download manager for handling asset bundle downloads and extraction */ class DownloadManager { constructor(storageManager, enableLogging = false, customHeaders = {}) { this.activeDownloads = new Map(); this.downloadCallbacks = new Map(); this.storageManager = storageManager; this.enableLogging = enableLogging; this.customHeaders = customHeaders; } /** * Download and extract a bundle */ async downloadBundle(bundle, progressCallback) { const bundleId = bundle.id; // Check if already downloading if (this.activeDownloads.has(bundleId)) { throw new Error(`Bundle ${bundleId} is already being downloaded`); } const abortController = new AbortController(); this.activeDownloads.set(bundleId, abortController); if (progressCallback) { this.downloadCallbacks.set(bundleId, progressCallback); } try { // Check if bundle already exists and is valid const bundlePath = this.storageManager.getBundlePath(`${bundleId}.zip`); const exists = await this.storageManager.assetExists(bundlePath, bundle.etag); if (exists && bundle.hash) { const validation = await this.storageManager.validateAssetById(bundle.id, bundlePath, bundle.hash); if (validation.isValid) { this.log(`Bundle ${bundleId} already exists and is valid`); await this.extractBundle(bundle, bundlePath); return; } } // Download the bundle const tempPath = this.storageManager.getTempPath(`${bundleId}.zip`); await this.downloadFile(bundle, tempPath, abortController.signal); // Validate downloaded bundle if (bundle.hash) { const validation = await this.storageManager.validateAssetById(bundle.id, tempPath, bundle.hash); if (!validation.isValid) { await react_native_fs_1.default.unlink(tempPath); throw new Error(`Bundle validation failed: ${validation.error}`); } } // Move to bundle storage await this.storageManager.moveFile(tempPath, bundlePath); // Save ETag in registry if (bundle.etag) { await this.storageManager.saveETag(bundle.id, bundlePath, bundle.etag); } // Extract the bundle await this.extractBundle(bundle, bundlePath); this.log(`Successfully downloaded and extracted bundle: ${bundleId}`); } catch (error) { this.emitProgress(bundle, { status: index_1.DownloadStatus.FAILED, error: error instanceof Error ? error.message : String(error), }); throw error; } finally { this.activeDownloads.delete(bundleId); this.downloadCallbacks.delete(bundleId); } } /** * Cancel all active downloads */ cancelAllDownloads() { for (const controller of this.activeDownloads.values()) { controller.abort(); } this.activeDownloads.clear(); this.downloadCallbacks.clear(); } /** * Get number of active downloads */ getActiveDownloadCount() { return this.activeDownloads.size; } /** * Download a file with progress tracking */ async downloadFile(bundle, filePath, signal) { this.emitProgress(bundle, { status: index_1.DownloadStatus.DOWNLOADING, progress: 0, downloadedBytes: 0, totalBytes: bundle.size, speed: 0, }); const downloadOptions = { fromUrl: bundle.url, toFile: filePath, headers: this.customHeaders, background: false, discretionary: false, cacheable: false, progressInterval: 500, progressDivider: 1, begin: (res) => { this.log(`Starting download: ${bundle.id} (${res.contentLength} bytes)`); }, progress: (res) => { if (signal.aborted) return; // Simplified progress tracking const progress = (res.bytesWritten / res.contentLength) * 100; const speed = 0; // Simplified - no speed calculation this.emitProgress(bundle, { status: index_1.DownloadStatus.DOWNLOADING, progress, downloadedBytes: res.bytesWritten, totalBytes: res.contentLength, speed, }); }, }; try { const downloadResult = react_native_fs_1.default.downloadFile(downloadOptions); // Handle abort signal if (signal.aborted) { downloadResult.jobId && react_native_fs_1.default.stopDownload(downloadResult.jobId); throw new Error('Download was cancelled'); } signal.addEventListener('abort', () => { downloadResult.jobId && react_native_fs_1.default.stopDownload(downloadResult.jobId); }); // Wait for download completion const result = await downloadResult.promise; if (result.statusCode !== 200) { throw new Error(`Download failed with status code: ${result.statusCode}`); } this.emitProgress(bundle, { status: index_1.DownloadStatus.COMPLETED, progress: 100, downloadedBytes: bundle.size, totalBytes: bundle.size, speed: 0, }); } catch (error) { // Clean up partial download try { const exists = await react_native_fs_1.default.exists(filePath); if (exists) { await react_native_fs_1.default.unlink(filePath); } } catch (cleanupError) { this.log(`Failed to cleanup partial download: ${cleanupError}`); } if (signal.aborted) { throw new Error('Download was cancelled'); } throw error; } } /** * Extract a downloaded bundle */ async extractBundle(bundle, _bundlePath) { this.emitProgress(bundle, { status: index_1.DownloadStatus.EXTRACTING, progress: 100, downloadedBytes: bundle.size, totalBytes: bundle.size, speed: 0, }); try { // Create extraction directory const extractDir = this.storageManager.getExtractedPath(bundle.id); const exists = await react_native_fs_1.default.exists(extractDir); if (exists) { await react_native_fs_1.default.unlink(extractDir); } await react_native_fs_1.default.mkdir(extractDir); // Extract zip file // TODO: Implement zip extraction when react-native-zip-archive is added back throw new Error('Zip extraction not currently supported'); // Validate extracted assets for (const asset of bundle.assets) { const assetPath = this.storageManager.getExtractedPath(`${bundle.id}/${asset.key}`); const assetExists = await react_native_fs_1.default.exists(assetPath); if (!assetExists) { throw new Error(`Asset ${asset.id} not found after extraction`); } // Validate asset hash if provided if (asset.hash) { const validation = await this.storageManager.validateAssetById(asset.id, assetPath, asset.hash); if (!validation.isValid) { throw new Error(`Asset ${asset.id} validation failed: ${validation.error}`); } } // Save asset ETag in registry if (asset.etag) { await this.storageManager.saveETag(asset.id, assetPath, asset.etag); } } this.emitProgress(bundle, { status: index_1.DownloadStatus.COMPLETED, progress: 100, downloadedBytes: bundle.size, totalBytes: bundle.size, speed: 0, }); this.log(`Successfully extracted bundle: ${bundle.id}`); } catch (error) { this.emitProgress(bundle, { status: index_1.DownloadStatus.FAILED, error: `Extraction failed: ${error instanceof Error ? error.message : String(error)}`, }); throw error; } } /** * Emit download progress event */ emitProgress(bundle, partial) { const progress = { id: bundle.id, type: 'bundle', progress: 0, downloadedBytes: 0, totalBytes: bundle.size, speed: 0, status: index_1.DownloadStatus.PENDING, ...partial, }; const callback = this.downloadCallbacks.get(bundle.id); if (callback) { try { callback(progress); } catch (error) { this.log(`Error in progress callback: ${error}`); } } } /** * Cleanup temporary files */ async cleanupTempFiles() { try { const tempDir = this.storageManager.getTempPath(''); const exists = await react_native_fs_1.default.exists(tempDir); if (exists) { const items = await react_native_fs_1.default.readDir(tempDir); for (const item of items) { try { await react_native_fs_1.default.unlink(item.path); } catch (error) { this.log(`Failed to cleanup temp file ${item.path}: ${error}`); } } } } catch (error) { this.log(`Failed to cleanup temp files: ${error}`); } } /** * Log message if logging is enabled */ log(message) { if (this.enableLogging) { console.log(`[DownloadManager] ${message}`); } } /** * Cleanup resources */ destroy() { this.cancelAllDownloads(); } } exports.DownloadManager = DownloadManager;