"use strict";
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
    return new (P || (P = Promise))(function (resolve, reject) {
        function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
        function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
        function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
        step((generator = generator.apply(thisArg, _arguments || [])).next());
    });
};
Object.defineProperty(exports, "__esModule", { value: true });
const check_package_1 = require("@pnpm/check-package");
const logger_1 = require("@pnpm/logger");
const pkgid_to_filename_1 = require("@pnpm/pkgid-to-filename");
const loadJsonFile = require("load-json-file");
const mkdirp = require("mkdirp-promise");
const fs = require("mz/fs");
const PQueue = require("p-queue");
const path = require("path");
const exists = require("path-exists");
const renameOverwrite = require("rename-overwrite");
const rimraf = require("rimraf-then");
const symlinkDir = require("symlink-dir");
const write_json_file_1 = require("write-json-file");
const readPkg_1 = require("./fs/readPkg");
const loggers_1 = require("./loggers");
const TARBALL_INTEGRITY_FILENAME = 'tarball-integrity';
function default_1(resolve, fetchers, opts) {
    opts = opts || {};
    const networkConcurrency = opts.networkConcurrency || 16;
    const requestsQueue = new PQueue({
        concurrency: networkConcurrency,
    });
    requestsQueue['counter'] = 0; // tslint:disable-line
    requestsQueue['concurrency'] = networkConcurrency; // tslint:disable-line
    const fetch = fetcher.bind(null, fetchers);
    const fetchPackageToStore = fetchToStore.bind(null, {
        fetch,
        fetchingLocker: new Map(),
        requestsQueue,
        storeIndex: opts.storeIndex,
        storePath: opts.storePath,
    });
    const requestPackage = resolveAndFetch.bind(null, {
        fetchPackageToStore,
        requestsQueue,
        resolve,
        storePath: opts.storePath,
    });
    requestPackage['requestPackage'] = requestPackage; // tslint:disable-line
    requestPackage['fetchPackageToStore'] = fetchPackageToStore; // tslint:disable-line
    return requestPackage;
}
exports.default = default_1;
function resolveAndFetch(ctx, wantedDependency, options) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            let latest;
            let pkg;
            let normalizedPref;
            let resolution = options.shrinkwrapResolution;
            let pkgId = options.currentPkgId;
            const skipResolution = resolution && !options.update;
            let forceFetch = false;
            let updated = false;
            let resolvedVia;
            // When fetching is skipped, resolution cannot be skipped.
            // We need the package's manifest when doing `shrinkwrap-only` installs.
            // When we don't fetch, the only way to get the package's manifest is via resolving it.
            //
            // The resolution step is never skipped for local dependencies.
            if (!skipResolution || options.skipFetch || pkgId && pkgId.startsWith('file:')) {
                const resolveResult = yield ctx.requestsQueue.add(() => ctx.resolve(wantedDependency, {
                    defaultTag: options.defaultTag,
                    localPackages: options.localPackages,
                    preferredVersions: options.preferredVersions,
                    prefix: options.prefix,
                    registry: options.registry,
                }), { priority: options.downloadPriority });
                pkg = resolveResult.package;
                latest = resolveResult.latest;
                resolvedVia = resolveResult.resolvedVia;
                // If the integrity of a local tarball dependency has changed,
                // the local tarball should be unpacked, so a fetch to the store should be forced
                forceFetch = Boolean(options.shrinkwrapResolution &&
                    pkgId && pkgId.startsWith('file:') &&
                    options.shrinkwrapResolution['integrity'] !== resolveResult.resolution['integrity']);
                if (!skipResolution || forceFetch) {
                    updated = pkgId !== resolveResult.id || !resolution || forceFetch;
                    // Keep the shrinkwrap resolution when possible
                    // to keep the original shasum.
                    if (updated) {
                        resolution = resolveResult.resolution;
                    }
                    pkgId = resolveResult.id;
                    normalizedPref = resolveResult.normalizedPref;
                }
            }
            const id = pkgId;
            loggers_1.progressLogger.debug({ status: 'resolved', pkgId: id, pkg: options.loggedPkg });
            if (resolution.type === 'directory') {
                if (!pkg) {
                    throw new Error(`Couldn't read package.json of local dependency ${wantedDependency.alias ? wantedDependency.alias + '@' : ''}${wantedDependency.pref}`);
                }
                return {
                    body: {
                        cacheByEngine: options.sideEffectsCache ? yield getCacheByEngine(ctx.storePath, id) : new Map(),
                        id,
                        isLocal: true,
                        manifest: pkg,
                        normalizedPref,
                        resolution: resolution,
                        resolvedVia,
                        updated,
                    },
                };
            }
            // We can skip fetching the package only if the manifest
            // is present after resolution
            if (options.skipFetch && pkg) {
                return {
                    body: {
                        cacheByEngine: options.sideEffectsCache ? yield getCacheByEngine(ctx.storePath, id) : new Map(),
                        id,
                        inStoreLocation: path.join(ctx.storePath, pkgid_to_filename_1.default(id, options.prefix)),
                        isLocal: false,
                        latest,
                        manifest: pkg,
                        normalizedPref,
                        resolution,
                        resolvedVia,
                        updated,
                    },
                };
            }
            const fetchResult = ctx.fetchPackageToStore({
                fetchRawManifest: updated || !pkg,
                force: forceFetch,
                pkgId: id,
                pkgName: pkg && pkg.name,
                prefix: options.prefix,
                resolution: resolution,
                verifyStoreIntegrity: options.verifyStoreIntegrity,
            });
            return {
                body: {
                    cacheByEngine: options.sideEffectsCache ? yield getCacheByEngine(ctx.storePath, id) : new Map(),
                    id,
                    inStoreLocation: fetchResult.inStoreLocation,
                    isLocal: false,
                    latest,
                    manifest: pkg,
                    normalizedPref,
                    resolution,
                    resolvedVia,
                    updated,
                },
                fetchingFiles: fetchResult.fetchingFiles,
                fetchingRawManifest: fetchResult.fetchingRawManifest,
                finishing: fetchResult.finishing,
            };
        }
        catch (err) {
            loggers_1.progressLogger.debug({ status: 'error', pkg: options.loggedPkg });
            throw err;
        }
    });
}
function fetchToStore(ctx, opts) {
    const targetRelative = pkgid_to_filename_1.default(opts.pkgId, opts.prefix);
    const target = path.join(ctx.storePath, targetRelative);
    if (!ctx.fetchingLocker.has(opts.pkgId)) {
        const fetchingRawManifest = differed();
        const fetchingFiles = differed();
        const finishing = differed();
        doFetchToStore(fetchingRawManifest, fetchingFiles, finishing);
        if (opts.fetchRawManifest) {
            ctx.fetchingLocker.set(opts.pkgId, {
                fetchingFiles: removeKeyOnFail(fetchingFiles.promise),
                fetchingRawManifest: removeKeyOnFail(fetchingRawManifest.promise),
                finishing: removeKeyOnFail(finishing.promise),
                inStoreLocation: target,
            });
        }
        else {
            ctx.fetchingLocker.set(opts.pkgId, {
                fetchingFiles: removeKeyOnFail(fetchingFiles.promise),
                finishing: removeKeyOnFail(finishing.promise),
                inStoreLocation: target,
            });
        }
        // When fetchingFiles resolves, the cached result has to set fromStore to true, without
        // affecting previous invocations: so we need to replace the cache.
        //
        // Changing the value of fromStore is needed for correct reporting of `pnpm server`.
        // Otherwise, if a package was not in store when the server started, it will be always
        // reported as "downloaded" instead of "reused".
        fetchingFiles.promise.then(({ filenames, fromStore }) => {
            // If it's already in the store, we don't need to update the cache
            if (fromStore) {
                return;
            }
            const tmp = ctx.fetchingLocker.get(opts.pkgId);
            // If fetching failed then it was removed from the cache.
            // It is OK. In that case there is no need to update it.
            if (!tmp)
                return;
            ctx.fetchingLocker.set(opts.pkgId, {
                fetchingFiles: Promise.resolve({
                    filenames,
                    fromStore: true,
                }),
                fetchingRawManifest: tmp.fetchingRawManifest,
                finishing: tmp.finishing,
                inStoreLocation: tmp.inStoreLocation,
            });
        });
        fetchingFiles.promise.catch((err) => {
            ctx.fetchingLocker.delete(opts.pkgId);
            throw err;
        });
    }
    const result = ctx.fetchingLocker.get(opts.pkgId);
    if (opts.fetchRawManifest && !result.fetchingRawManifest) {
        result.fetchingRawManifest = removeKeyOnFail(result.fetchingFiles.then(() => readPkg_1.fromDir(path.join(result.inStoreLocation, 'package'))));
    }
    return result;
    function removeKeyOnFail(p) {
        return p.catch((err) => {
            ctx.fetchingLocker.delete(opts.pkgId);
            throw err;
        });
    }
    function doFetchToStore(fetchingRawManifest, fetchingFiles, finishing) {
        return __awaiter(this, void 0, void 0, function* () {
            try {
                const isLocalTarballDep = opts.pkgId.startsWith('file:');
                const linkToUnpacked = path.join(target, 'package');
                // We can safely assume that if there is no data about the package in `store.json` then
                // it is not in the store yet.
                // In case there is record about the package in `store.json`, we check it in the file system just in case
                const targetExists = ctx.storeIndex[targetRelative] && (yield exists(path.join(linkToUnpacked, 'package.json')));
                if (!opts.force && targetExists &&
                    (isLocalTarballDep === false ||
                        opts.resolution['integrity'] && (yield tarballSatisfiesIntegrity(opts.resolution['integrity'], target)) // tslint:disable-line:no-string-literal
                    )) {
                    // if target exists and it wasn't modified, then no need to refetch it
                    const satisfiedIntegrity = opts.verifyStoreIntegrity
                        ? yield check_package_1.default(linkToUnpacked)
                        : yield loadJsonFile(path.join(path.dirname(linkToUnpacked), 'integrity.json'));
                    if (satisfiedIntegrity) {
                        fetchingFiles.resolve({
                            filenames: Object.keys(satisfiedIntegrity).filter((f) => !satisfiedIntegrity[f].isDir),
                            fromStore: true,
                        });
                        if (opts.fetchRawManifest) {
                            readPkg_1.fromDir(linkToUnpacked)
                                .then(fetchingRawManifest.resolve)
                                .catch(fetchingRawManifest.reject);
                        }
                        finishing.resolve(undefined);
                        return;
                    }
                    logger_1.storeLogger.warn(`Refetching ${target} to store. It was either modified or had no integrity checksums`);
                }
                // We fetch into targetStage directory first and then fs.rename() it to the
                // target directory.
                let filesIndex;
                let tempLocation;
                yield Promise.all([
                    (() => __awaiter(this, void 0, void 0, function* () {
                        // Tarballs are requested first because they are bigger than metadata files.
                        // However, when one line is left available, allow it to be picked up by a metadata request.
                        // This is done in order to avoid situations when tarballs are downloaded in chunks
                        // As much tarballs should be downloaded simultaneously as possible.
                        const priority = (++ctx.requestsQueue['counter'] % ctx.requestsQueue['concurrency'] === 0 ? -1 : 1) * 1000; // tslint:disable-line
                        const fetchedPackage = yield ctx.requestsQueue.add(() => ctx.fetch(opts.resolution, target, {
                            cachedTarballLocation: path.join(ctx.storePath, opts.pkgId, 'packed.tgz'),
                            onProgress: (downloaded) => {
                                loggers_1.progressLogger.debug({ status: 'fetching_progress', pkgId: opts.pkgId, downloaded });
                            },
                            onStart: (size, attempt) => {
                                loggers_1.progressLogger.debug({ status: 'fetching_started', pkgId: opts.pkgId, size, attempt });
                            },
                            pkgId: opts.pkgId,
                            prefix: opts.prefix,
                        }), { priority });
                        filesIndex = fetchedPackage.filesIndex;
                        tempLocation = fetchedPackage.tempLocation;
                    }))(),
                    // removing only the folder with the unpacked files
                    // not touching tarball and integrity.json
                    targetExists && (yield rimraf(path.join(target, 'node_modules'))),
                ]);
                // Ideally, fetchingFiles wouldn't care about when integrity is calculated.
                // However, we can only rename the temp folder once we know the package name.
                // And we cannot rename the temp folder till we're calculating integrities.
                if (opts.verifyStoreIntegrity) {
                    const fileIntegrities = yield Promise.all(Object.keys(filesIndex)
                        .map((filename) => filesIndex[filename].generatingIntegrity
                        .then((fileIntegrity) => ({
                        [filename]: {
                            integrity: fileIntegrity,
                            size: filesIndex[filename].size,
                        },
                    }))));
                    const integrity = fileIntegrities
                        .reduce((acc, info) => {
                        Object.assign(acc, info);
                        return acc;
                    }, {});
                    yield write_json_file_1.default(path.join(target, 'integrity.json'), integrity, { indent: null });
                }
                else {
                    // TODO: save only filename: {size}
                    yield write_json_file_1.default(path.join(target, 'integrity.json'), filesIndex, { indent: null });
                }
                finishing.resolve(undefined);
                let pkgName = opts.pkgName;
                if (!pkgName || opts.fetchRawManifest) {
                    const pkg = yield readPkg_1.fromDir(tempLocation);
                    fetchingRawManifest.resolve(pkg);
                    if (!pkgName) {
                        pkgName = pkg.name;
                    }
                }
                const unpacked = path.join(target, 'node_modules', pkgName);
                yield mkdirp(path.dirname(unpacked));
                // rename(oldPath, newPath) is an atomic operation, so we do it at the
                // end
                yield renameOverwrite(tempLocation, unpacked);
                yield symlinkDir(unpacked, linkToUnpacked);
                if (isLocalTarballDep && opts.resolution['integrity']) { // tslint:disable-line:no-string-literal
                    yield fs.writeFile(path.join(target, TARBALL_INTEGRITY_FILENAME), opts.resolution['integrity'], 'utf8'); // tslint:disable-line:no-string-literal
                }
                ctx.storeIndex[targetRelative] = ctx.storeIndex[targetRelative] || [];
                fetchingFiles.resolve({
                    filenames: Object.keys(filesIndex).filter((f) => !filesIndex[f].isDir),
                    fromStore: false,
                });
            }
            catch (err) {
                fetchingFiles.reject(err);
                if (opts.fetchRawManifest) {
                    fetchingRawManifest.reject(err);
                }
            }
        });
    }
}
function tarballSatisfiesIntegrity(wantedIntegrity, pkgInStoreLocation) {
    return __awaiter(this, void 0, void 0, function* () {
        try {
            const currentIntegrity = (yield fs.readFile(path.join(pkgInStoreLocation, TARBALL_INTEGRITY_FILENAME), 'utf8'));
            return currentIntegrity === wantedIntegrity;
        }
        catch (err) {
            return false;
        }
    });
}
// tslint:disable-next-line
function noop() { }
function differed() {
    let pResolve = noop;
    let pReject = noop;
    const promise = new Promise((resolve, reject) => {
        pResolve = resolve;
        pReject = reject;
    });
    return {
        promise,
        reject: pReject,
        resolve: pResolve,
    };
}
function fetcher(fetcherByHostingType, resolution, target, opts) {
    return __awaiter(this, void 0, void 0, function* () {
        const fetch = fetcherByHostingType[resolution.type || 'tarball'];
        if (!fetch) {
            throw new Error(`Fetching for dependency type "${resolution.type}" is not supported`);
        }
        try {
            return yield fetch(resolution, target, opts);
        }
        catch (err) {
            logger_1.storeLogger.warn(`Fetching ${opts.pkgId} failed!`);
            throw err;
        }
    });
}
// TODO: It might make sense to have this function as part of storeController.
//  Ask @etamponi if it is fine for when pnpm is used as a server
// TODO: cover with tests
function getCacheByEngine(storePath, id) {
    return __awaiter(this, void 0, void 0, function* () {
        const map = new Map();
        const cacheRoot = path.join(storePath, id, 'side_effects');
        if (!(yield fs.exists(cacheRoot))) {
            return map;
        }
        const dirContents = (yield fs.readdir(cacheRoot)).map((content) => path.join(cacheRoot, content));
        yield Promise.all(dirContents.map((dir) => __awaiter(this, void 0, void 0, function* () {
            if (!(yield fs.lstat(dir)).isDirectory()) {
                return;
            }
            const engineName = path.basename(dir);
            map[engineName] = path.join(dir, 'package');
        })));
        return map;
    });
}
exports.getCacheByEngine = getCacheByEngine;
//# sourceMappingURL=packageRequester.js.map