UNPKG

ipull

Version:

The only file downloader you'll ever need. For node.js and the browser, CLI and library for fast and reliable file downloads.

244 lines 9.4 kB
import { EventEmitter } from "eventemitter3"; import ProgressStatisticsBuilder from "../../transfer-visualize/progress-statistics-builder.js"; import BaseDownloadEngine from "./base-download-engine.js"; import { concurrency } from "../utils/concurrency.js"; import { DownloadFlags, DownloadStatus } from "../download-file/progress-status-file.js"; import { promiseWithResolvers } from "../utils/promiseWithResolvers.js"; const DEFAULT_OPTIONS = { parallelDownloads: 1, unpackInnerMultiDownloadsStatues: true, finalizeDownloadAfterAllSettled: true }; export default class DownloadEngineMultiDownload extends EventEmitter { downloads = []; _options; _aborted = false; _activeEngines = new Set(); _progressStatisticsBuilder = new ProgressStatisticsBuilder(); _downloadStatues = []; _closeFiles = []; _lastStatus = null; _loadingDownloads = 0; _reloadDownloadParallelisms; _engineWaitPromises = new Set(); /** * @internal */ _downloadEndPromise = promiseWithResolvers(); /** * @internal */ _downloadStarted = false; /** * @internal */ constructor(options = {}) { super(); this._options = { ...DEFAULT_OPTIONS, ...options }; this._init(); } get activeDownloads() { return Array.from(this._activeEngines); } get parallelDownloads() { return this._options.parallelDownloads; } get loadingDownloads() { if (!this._options.unpackInnerMultiDownloadsStatues) { return this._loadingDownloads; } let totalLoading = this._loadingDownloads; for (const download of this.downloads) { if (download instanceof DownloadEngineMultiDownload) { totalLoading += download.loadingDownloads; } } return totalLoading; } /** * @internal */ get _flatEngines() { return this.downloads.map(engine => { if (engine instanceof DownloadEngineMultiDownload) { return engine._flatEngines; } return engine; }) .flat(); } set parallelDownloads(value) { if (this._options.parallelDownloads === value) return; this._options.parallelDownloads = value; this._reloadDownloadParallelisms?.(); } get downloadStatues() { const statues = this._downloadStatues.flat(); return statues.filter(((status, index) => statues.findIndex(x => x.downloadId === status.downloadId) === index)); } get status() { return this._lastStatus; } get downloadSize() { return this.downloads.reduce((acc, engine) => acc + engine.downloadSize, 0); } _init() { this._progressStatisticsBuilder.downloadStatus = DownloadStatus.NotStarted; this._progressStatisticsBuilder.on("progress", progress => { progress = { ...progress, fileName: this._options.downloadName ?? progress.fileName, comment: this._options.downloadComment ?? progress.comment, downloadFlags: progress.downloadFlags.concat([DownloadFlags.DownloadSequence]) }; this._lastStatus = progress; this.emit("progress", progress); }); const originalProgress = this._progressStatisticsBuilder.status; this._lastStatus = { ...originalProgress, downloadFlags: originalProgress.downloadFlags.concat([DownloadFlags.DownloadSequence]) }; } _addEngine(engine, index) { this.emit("downloadAdded", engine); const getStatus = (defaultProgress = engine.status) => (this._options.unpackInnerMultiDownloadsStatues && engine instanceof DownloadEngineMultiDownload ? engine.downloadStatues : defaultProgress); this._downloadStatues[index] = getStatus(); engine.on("progress", (progress) => { this._downloadStatues[index] = getStatus(progress); }); if (this._options.finalizeDownloadAfterAllSettled) { this._changeEngineFinishDownload(engine); } this.downloads.push(engine); this._reloadDownloadParallelisms?.(); } async _addDownloadNoStatisticUpdate(engine) { const index = this.downloads.length + this._loadingDownloads; this._downloadStatues[index] = ProgressStatisticsBuilder.loadingStatusEmptyStatistics(); this._loadingDownloads++; this._progressStatisticsBuilder._totalDownloadParts++; this._progressStatisticsBuilder._sendLatestProgress(); const isPromise = engine instanceof Promise; if (isPromise) { this._engineWaitPromises.add(engine); } const awaitEngine = isPromise ? await engine : engine; if (isPromise) { this._engineWaitPromises.delete(engine); } this._progressStatisticsBuilder._totalDownloadParts--; this._loadingDownloads--; this._addEngine(awaitEngine, index); this._progressStatisticsBuilder.add(awaitEngine, true); return awaitEngine; } async addDownload(...engines) { await Promise.all(engines.map(this._addDownloadNoStatisticUpdate.bind(this))); } async download() { if (this._downloadStarted) { return this._downloadEndPromise.promise; } try { this._progressStatisticsBuilder.downloadStatus = DownloadStatus.Active; this._downloadStarted = true; this.emit("start"); const concurrencyCount = this._options.parallelDownloads || DEFAULT_OPTIONS.parallelDownloads; let continueIteration = true; while (this._loadingDownloads > 0 || continueIteration) { continueIteration = false; const { reload, promise } = concurrency(this.downloads, concurrencyCount, async (engine) => { if (this._aborted) return; this._activeEngines.add(engine); this.emit("childDownloadStarted", engine); if (engine._downloadStarted || this._options.naturalDownloadStart) { await engine._downloadEndPromise.promise; } else { await engine.download(); } this.emit("childDownloadClosed", engine); this._activeEngines.delete(engine); }); this._reloadDownloadParallelisms = reload; await promise; continueIteration = this._engineWaitPromises.size > 0; if (continueIteration) { await Promise.race(this._engineWaitPromises); } } this._progressStatisticsBuilder.downloadStatus = DownloadStatus.Finished; this.emit("finished"); await this._finishEnginesDownload(); await this.close(); this._downloadEndPromise.resolve(); } catch (error) { this._downloadEndPromise.reject(error); throw error; } finally { this._downloadEndPromise = promiseWithResolvers(); } } _changeEngineFinishDownload(engine) { if (engine instanceof DownloadEngineMultiDownload) { const _finishEnginesDownload = engine._finishEnginesDownload.bind(engine); engine._finishEnginesDownload = async () => { }; this._closeFiles.push(_finishEnginesDownload); return; } if (!(engine instanceof BaseDownloadEngine)) { return; } const options = engine._fileEngineOptions; const onFinishAsync = options.onFinishAsync; const onCloseAsync = options.onCloseAsync; options.onFinishAsync = undefined; options.onCloseAsync = undefined; this._closeFiles.push(async () => { await onFinishAsync?.(); await options.writeStream.close(); await onCloseAsync?.(); }); } async _finishEnginesDownload() { await Promise.all(this._closeFiles.map(func => func())); } pause() { this._progressStatisticsBuilder.downloadStatus = DownloadStatus.Paused; this._activeEngines.forEach(engine => { if ("pause" in engine) engine.pause(); }); } resume() { this._progressStatisticsBuilder.downloadStatus = DownloadStatus.Active; this._activeEngines.forEach(engine => { if ("resume" in engine) engine.resume(); }); } async close() { if (this._aborted) return; this._aborted = true; if (this._progressStatisticsBuilder.downloadStatus !== DownloadStatus.Finished) { this._progressStatisticsBuilder.downloadStatus = DownloadStatus.Cancelled; } const closePromises = Array.from(this._activeEngines) .map(engine => { if ("close" in engine) { return engine.close(); } return Promise.resolve(); }); await Promise.all(closePromises); this.emit("closed"); } } //# sourceMappingURL=download-engine-multi-download.js.map