"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
const core_loggers_1 = require("@pnpm/core-loggers");
const error_1 = require("@pnpm/error");
const retry = require("@zkochan/retry");
const fetch_from_npm_registry_1 = require("fetch-from-npm-registry");
const ssri = require("ssri");
const urlLib = require("url");
const errorTypes_1 = require("./errorTypes");
class TarballFetchError extends error_1.default {
    constructor(uri, response) {
        super('TARBALL_FETCH', `${response.status} ${response.statusText}: ${uri}`);
        this.httpStatusCode = response.status;
        this.uri = uri;
        this.response = response;
    }
}
class TarballIntegrityError extends error_1.default {
    constructor(opts) {
        super('TARBALL_INTEGRITY', `Got unexpected checksum for "${opts.url}". Wanted "${opts.expected}". Got "${opts.found}".`);
        this.found = opts.found;
        this.expected = opts.expected;
        this.algorithm = opts.algorithm;
        this.sri = opts.sri;
        this.url = opts.url;
    }
}
exports.default = (gotOpts) => {
    // The fetch library can retry requests on bad HTTP responses.
    // However, it is not enough to retry on bad HTTP responses only.
    // Requests should also be retried when the tarball's integrity check fails.
    // Hence, we tell fetch to not retry,
    // and we perform the retries from this function instead.
    const fetchFromNpmRegistry = fetch_from_npm_registry_1.default({ retry: { retries: 0 } });
    const retryOpts = {
        factor: 10,
        maxTimeout: 6e4,
        minTimeout: 1e4,
        retries: 2,
        ...gotOpts.retry,
    };
    return async function download(url, opts) {
        // If a tarball is hosted on a different place than the manifest, only send
        // credentials on `alwaysAuth`
        const shouldAuth = opts.auth && (opts.auth.alwaysAuth ||
            !opts.registry ||
            urlLib.parse(url).host === urlLib.parse(opts.registry).host);
        const op = retry.operation(retryOpts);
        return new Promise((resolve, reject) => {
            op.attempt(async (attempt) => {
                try {
                    resolve(await fetch(attempt));
                }
                catch (error) {
                    if (error.httpStatusCode === 403) {
                        reject(error);
                    }
                    const timeout = op.retry(error);
                    if (timeout === false) {
                        reject(op.mainError());
                        return;
                    }
                    core_loggers_1.requestRetryLogger.debug({
                        attempt,
                        error,
                        maxRetries: retryOpts.retries,
                        method: 'GET',
                        timeout,
                        url,
                    });
                }
            });
        });
        async function fetch(currentAttempt) {
            var _a;
            try {
                const res = await fetchFromNpmRegistry(url, {
                    authHeaderValue: shouldAuth ? (_a = opts.auth) === null || _a === void 0 ? void 0 : _a.authHeaderValue : undefined,
                });
                if (res.status !== 200) {
                    throw new TarballFetchError(url, res);
                }
                const contentLength = res.headers.has('content-length') && res.headers.get('content-length');
                const size = typeof contentLength === 'string'
                    ? parseInt(contentLength, 10)
                    : null;
                if (opts.onStart) {
                    opts.onStart(size, currentAttempt);
                }
                const onProgress = opts.onProgress;
                let downloaded = 0;
                res.body.on('data', (chunk) => {
                    downloaded += chunk.length;
                    if (onProgress)
                        onProgress(downloaded);
                });
                return await new Promise(async (resolve, reject) => {
                    const stream = res.body
                        .on('error', reject);
                    try {
                        const [integrityCheckResult, filesIndex] = await Promise.all([
                            opts.integrity && safeCheckStream(res.body, opts.integrity, url) || true,
                            opts.cafs.addFilesFromTarball(res.body, opts.manifest),
                            waitTillClosed({ stream, size, getDownloaded: () => downloaded, url }),
                        ]);
                        if (integrityCheckResult !== true) {
                            throw integrityCheckResult;
                        }
                        resolve({ filesIndex: filesIndex });
                    }
                    catch (err) {
                        reject(err);
                    }
                });
            }
            catch (err) {
                err.attempts = currentAttempt;
                err.resource = url;
                throw err;
            }
        }
    };
};
async function safeCheckStream(stream, integrity, url) {
    try {
        await ssri.checkStream(stream, integrity);
        return true;
    }
    catch (err) {
        return new TarballIntegrityError({
            algorithm: err['algorithm'],
            expected: err['expected'],
            found: err['found'],
            sri: err['sri'],
            url,
        });
    }
}
function waitTillClosed(opts) {
    return new Promise((resolve, reject) => {
        opts.stream.on('end', () => {
            const downloaded = opts.getDownloaded();
            if (opts.size !== null && opts.size !== downloaded) {
                const err = new errorTypes_1.BadTarballError({
                    expectedSize: opts.size,
                    receivedSize: downloaded,
                    tarballUrl: opts.url,
                });
                reject(err);
                return;
            }
            resolve();
        });
    });
}
//# sourceMappingURL=createDownloader.js.map