@toast-studios/asset-manager
Version:
A React Native asset management library with intelligent caching and loading strategies
295 lines (294 loc) • 11.2 kB
JavaScript
;
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;