Source: data/requester.js

/**
 * @module
 * @private
 */
import {getLinkedData} from './undercomplicate';

import { DATA_OPS } from '../registry';


class DataOperation {
    /**
     * Perform a data operation (such as a join)
     * @param {String} join_type
     * @param initiator The entity that initiated the request for data. Usually, this is the data layer. This argument exists so that a data_operation could do things like auto-define axis labels/ color scheme in response to dynamic data. It has potential for side effects if misused, so use sparingly!
     * @param params Optional user/layout parameters to be passed to the data function
     */
    constructor(join_type, initiator, params) {
        this._callable = DATA_OPS.get(join_type);
        this._initiator = initiator;
        this._params = params || [];
    }

    getData(plot_state, ...dependent_recordsets) {
        // Most operations are joins: they receive two pieces of data (eg left + right)
        //   Other ops are possible, like consolidating just one set of records to best value per key
        // Hence all dependencies are passed as first arg: [dep1, dep2, dep3...]

        // Every data operation receives plot_state, reference to the data layer that called it, the input data, & any additional options
        const context = {plot_state, data_layer: this._initiator};
        return Promise.resolve(this._callable(context, dependent_recordsets, ...this._params));
    }
}


/**
 * The Requester manages fetching of data across multiple data sources. It is used internally by LocusZoom data layers.
 *   It passes plot.state information to each adapter, and ensures that a series of requests can be performed in a
 *   designated order.
 *
 * Each data layer calls the requester object directly, and as such, each data layer has a private view of data: it can
 *   perform its own calculations, filter results, and apply transforms without influencing other layers.
 *  (while still respecting a shared cache where appropriate)
 *
 * This object is not part of the public interface. It should almost **never** be replaced or modified directly.
 *
 * @param {DataSources} sources A set of data sources used specifically by this plot instance
 * @private
 */
class Requester {
    constructor(sources) {
        this._sources = sources;
    }

    /**
     * Parse the data layer configuration when a layer is first created.
     *  Validate config, and return entities and dependencies in a format usable for data retrieval.
     *  This is used by data layers, and also other data-retrieval functions (like subscribeToDate).
     *
     *  Inherent assumptions:
     *  1. A data layer will always know its data up front, and layout mutations will only affect what is displayed.
     *  2. People will be able to add new data adapters (tracks), but if they are removed, the accompanying layers will be
     *      removed at the same time. Otherwise, the pre-parsed data fetching logic could could preserve a reference to the
     *      removed adapter.
     * @param {Object} namespace_options
     * @param {Array} data_operations
     * @param {Object|null} initiator The entity that initiated the request (the data layer). Passed to data operations,
     *   but not adapters. By baking this reference into each data operation, functions can do things like autogenerate
     *   axis tick marks or color schemes based on dyanmic data. This is an advanced usage and should be handled with care!
     * @returns {Array} Map of entities and list of dependencies
     */
    config_to_sources(namespace_options = {}, data_operations = [], initiator) {
        const entities = new Map();
        const namespace_local_names = Object.keys(namespace_options);

        // 1. Specify how to coordinate data. Precedence:
        //   a) EXPLICIT fetch logic,
        //   b) IMPLICIT auto-generate fetch order if there is only one NS,
        //   c) Throw "spec required" error if > 1, because 2 adapters may need to be fetched in a sequence
        let dependency_order = data_operations.find((item) => item.type === 'fetch');  // explicit spec: {fetch, from}
        if (!dependency_order) {
            dependency_order = { type: 'fetch', from: namespace_local_names };
            data_operations.unshift(dependency_order);
        }

        // Validate that all NS items are available to the root requester in DataSources. All layers recognize a
        //  default value, eg people copying the examples tend to have defined a datasource called "assoc"
        const ns_pattern = /^\w+$/;
        for (let [local_name, global_name] of Object.entries(namespace_options)) {
            if (!ns_pattern.test(local_name)) {
                throw new Error(`Invalid namespace name: '${local_name}'. Must contain only alphanumeric characters`);
            }

            const source = this._sources.get(global_name);
            if (!source) {
                throw new Error(`A data layer has requested an item not found in DataSources: data type '${local_name}' from ${global_name}`);
            }
            entities.set(local_name, source);

            // Note: Dependency spec checker will consider "ld(assoc)" to match a namespace called "ld"
            if (!dependency_order.from.find((dep_spec) => dep_spec.split('(')[0] === local_name)) {
                // Sometimes, a new piece of data (namespace) will be added to a layer. Often this doesn't have any dependencies, other than adding a new join.
                //  To make it easier to EXTEND existing layers, by default, we'll push any unknown namespaces to data_ops.fetch
                // Thus the default behavior is "fetch all namespaces as though they don't depend on anything.
                //  If they depend on something, only then does "data_ops[@type=fetch].from" need to be mutated
                dependency_order.from.push(local_name);
            }
        }

        let dependencies = Array.from(dependency_order.from);

        // Now check all joins. Are namespaces valid? Are they requesting known data?
        for (let config of data_operations) {
            let {type, name, requires, params} = config;
            if (type !== 'fetch') {
                let namecount = 0;
                if (!name) {
                    name = config.name = `join${namecount}`;
                    namecount += 1;
                }

                if (entities.has(name)) {
                    throw new Error(`Configuration error: within a layer, join name '${name}' must be unique`);
                }
                requires.forEach((require_name) => {
                    if (!entities.has(require_name)) {
                        throw new Error(`Data operation cannot operate on unknown provider '${require_name}'`);
                    }
                });

                const task = new DataOperation(type, initiator, params);
                entities.set(name, task);
                dependencies.push(`${name}(${requires.join(', ')})`); // Dependency resolver uses the form item(depA, depB)
            }
        }
        return [entities, dependencies];
    }

    /**
     * @param {Object} plot_state Plot state, which will be passed to every adapter. Includes view extent (chr, start, end)
     * @param {Map} entities A list of adapter and join tasks. This is created internally from data layer layouts.
     *  Keys are layer-local namespaces for data types (like assoc), and values are adapter or join task instances
     *  (things that implement a method getData).
     * @param {String[]} dependencies Instructions on what adapters to fetch from, in what order
     * @returns {Promise}
     */
    getData(plot_state, entities, dependencies) {
        if (!dependencies.length) {
            return Promise.resolve([]);
        }
        // The last dependency (usually the last join operation) determines the last thing returned.
        return getLinkedData(plot_state, entities, dependencies, true);
    }
}


export default Requester;

export {DataOperation as _JoinTask};