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
JavaScript
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