Source: data/undercomplicate/requests.js

/**
 * Perform a series of requests, respecting order of operations
 * @private
 */

import {Sorter} from '@hapi/topo';


function _parse_declaration(spec) {
    // Parse a dependency declaration like `assoc` or `ld(assoc)` or `join(assoc, ld)`. Return node and edges that can be used to build a graph.
    const parsed = /^(?<name_alone>\w+)$|((?<name_deps>\w+)+\(\s*(?<deps>[^)]+?)\s*\))/.exec(spec);
    if (!parsed) {
        throw new Error(`Unable to parse dependency specification: ${spec}`);
    }

    let {name_alone, name_deps, deps} = parsed.groups;
    if (name_alone) {
        return [name_alone, []];
    }

    deps = deps.split(/\s*,\s*/);
    return [name_deps, deps];
}

/**
 * Perform a request for data from a set of providers, taking into account dependencies between requests.
 *  This can be a mix of network requests or other actions (like join tasks), provided that the provider be some
 *  object that implements a method `instance.getData`
 *
 * Each data layer in LocusZoom will translate the internal configuration into a format used by this function.
 *  This function is never called directly in custom user code. In locuszoom, Requester Handles You
 *
 * TODO Future: It would be great to add a warning if the final element in the DAG does not reference all dependencies. This is a limitation of the current toposort library we use.
 *
 * @param {object} shared_options Options passed globally to all requests. In LocusZoom, this is often a copy of "plot.state"
 * @param {Map} entities A lookup of named entities that implement the method `instance.getData -> Promise`
 * @param {String[]} dependencies A description of how to fetch entities, and what they depend on, like `['assoc', 'ld(assoc)']`.
 *   **Order will be determined by a DAG and the last item in the DAG is all that is returned.**
 * @param {boolean} [consolidate=true] Whether to return all results (almost never used), or just the last item in the resolved DAG.
 *   This can be a pitfall in common usage: if you forget a "join/consolidate" task, the last result may appear to be missing some data.
 * @returns {Promise<Object[]>}
 */
function getLinkedData(shared_options, entities, dependencies, consolidate = true) {
    if (!dependencies.length) {
        return [];
    }

    const parsed = dependencies.map((spec) => _parse_declaration(spec));
    const dag = new Map(parsed);

    // Define the order to perform requests in, based on a DAG
    const toposort = new Sorter();
    for (let [name, deps] of dag.entries()) {
        try {
            toposort.add(name, {after: deps, group: name});
        } catch (e) {
            throw new Error(`Invalid or possible circular dependency specification for: ${name}`);
        }
    }
    const order = toposort.nodes;

    // Verify that all requested entities exist by name!
    const responses = new Map();
    for (let name of order) {
        const provider = entities.get(name);
        if (!provider) {
            throw new Error(`Data has been requested from source '${name}', but no matching source was provided`);
        }

        // Each promise should only be triggered when the things it depends on have been resolved
        const depends_on = dag.get(name) || [];
        const prereq_promises = Promise.all(depends_on.map((name) => responses.get(name)));

        const this_result = prereq_promises.then((prior_results) => {
            // Each request will be told the name of the provider that requested it. This can be used during post-processing,
            //   eg to use the same endpoint adapter twice and label where the fields came from (assoc.id, assoc2.id)
            // This has a secondary effect: it ensures that any changes made to "shared" options in one adapter will
            //  not leak out to others via a mutable shared object reference.
            const options = Object.assign({_provider_name: name}, shared_options);
            return provider.getData(options, ...prior_results);
        });
        responses.set(name, this_result);
    }
    return Promise.all([...responses.values()])
        .then((all_results) => {
            if (consolidate) {
                // Some usages- eg fetch + data join tasks- will only require the last response in the sequence
                // Consolidate mode is the common use case, since returning a list of responses is not so helpful (depends on order of request, not order specified)
                return all_results[all_results.length - 1];
            }
            return all_results;
        });
}


export {getLinkedData};

// For testing only
export {_parse_declaration};