<commit_before>import Cachemap from 'cachemap';
import { castArray, flatten, isFunction } from 'lodash';
import uuidV1 from 'uuid/v1';
import logger from './logger';

let instance;

/**
 *
 * The getta rest client
 */
export default class RestClient {
  /**
   *
   * @constructor
   * @param {Object} config
   * @return {RestClient}
   */
  constructor({
    /**
     *
     * Base url to which to make requests.
     *
     * @type {string}
     */
    baseURL,
    /**
     * How many assets can be batched together in
     * the same request.
     *
     * @type {number}
     */
    batchLimit = '20',
    /**
     *
     * Optional function used to parse the body of
     * a response prior the client returning it.
     * The client requires any data returned to be
     * on a property of 'data' and any errors returned
     * to be on a property of 'errors'.
     *
     * @type {Function}
     */
    bodyParser = null,
    /**
     *
     * Optional function used to parse the data of
     * a response prior the client returning it.
     *
     * @type {Function}
     */
    dataParser = null,
    /**
     *
     * Optional configuration to be passed to the
     * cachemap module.
     *
     * @type {Object}
     */
    cachemapOptions,
    /**
     *
     * Disables caching of responses against request url.
     *
     * @type {boolean}
     */
    disableCaching = false,
    /**
     * Any headers to be sent with requests, such as
     * authentication headers.
     *
     * @type {Object}
     */
    headers = null,
    /**
     * Whether to create a new instance of a
     * client or return the existing instance.
     *
     * @type {boolean}
     */
    newInstance = false,
    /**
     * Optional reference object used to store
     * request information against a unique
     * identifier on the context.
     *
     * @type {Object}
     */
    tracker = {},
  } = {}) {
    if (instance && !newInstance) {
      return instance;
    }

    if (!baseURL) {
      throw new Error('baseURL is mandatory properties for a rest client.');
    }

    this._baseURL = baseURL;
    this._batchLimit = batchLimit;
    this._bodyParser = bodyParser;
    this._cache = new Cachemap(cachemapOptions);
    this._dataParser = dataParser;
    this._disableCaching = disableCaching;
    this._headers = headers;
    this._tracker = tracker;
    instance = this;
    return instance;
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {Object} tracker
   * @param {string} resource
   * @param {string} fetchID
   * @param {string} requestID
   * @return {Object}
   */
  _addToFetch(path, tracker, resource, fetchID, requestID) {
    const index = tracker.active.findIndex(obj => obj.value === resource);
    const activeMatch = (index !== -1) ? tracker.active.splice(index, 1)[0] : null;
    const activeFetchID = activeMatch ? activeMatch.fetchID : fetchID;
    const fetchMatch = tracker.fetching.find(obj => obj.value === resource);

    if (!fetchMatch) {
      tracker.fetching.push({ fetchID: activeFetchID, value: resource });
      return {};
    }

    const promises = [];
    if (!tracker.pending[resource]) tracker.pending[resource] = [];

    promises.push(this._setPendingRequest(
      tracker.pending[resource], { fetchID: fetchMatch.fetchID },
    ));

    this._updatePendingRequests({ fetchID: fetchMatch.fetchID, resource }, requestID, path);
    return { pending: true, promises };
  }

  /**
   *
   * @private
   * @param {Object} context
   * @param {Object} options
   * @return {Object}
   */
  _batchRequest({ path, resource, fetchID, requestID }, { batch, batchLimit }) {
    const tracker = this._getTracker(requestID, path);

    let promises = [];

    for (let i = resource.active.length - 1; i >= 0; i -= 1) {
      const added = this._addToFetch(path, tracker, resource.active[i], fetchID, requestID);
      promises = [...promises, ...added.promises];
      if (added.pending) resource.pending = [...resource.pending, ...resource.active.splice(i, 1)];
    }

    const skip = !resource.active.length;

    if (!skip && batch) {
      const limit = batchLimit || this._batchLimit;
      resource.batched = resource.active;

      if (tracker.active.length && resource.batched.length < limit) {
        for (let i = tracker.active.length - 1; i >= 0; i -= 1) {
          resource.batched.push(tracker.active[i].value);
          tracker.fetching.push({ fetchID, value: tracker.active[i].value });

          this._updatePendingRequests(
            { fetchID, resource: tracker.active[i].value }, requestID, path,
          );

          tracker.active.splice(i, 1);
          if (resource.batched.length === limit) break;
        }
      }
    }

    return { promises, skip };
  }

  /**
   *
   * @private
   * @param {Array<string>} resource
   * @return {Array<Array>}
   */
  _batchResource(resource) {
    const batches = [];
    const countLimit = Math.ceil(resource.length / this._batchLimit);
    let count = 0;
    let start = 0;
    let stop = this._batchLimit;

    do {
      batches.push(resource.slice(start, stop));
      start += this._batchLimit;
      stop += this._batchLimit;
      count += 1;
    } while (count < countLimit);

    return batches;
  }

  /**
   *
   * @private
   * @param {Object} queryParams
   * @return {string}
   */
  _buildQueryString(queryParams) {
    let queryString = '';
    let paramCount = 0;

    Object.keys(queryParams).forEach((key) => {
      paramCount += 1;
      const prefix = paramCount === 1 ? '?' : '&';
      queryString += `${prefix}${key}=${queryParams[key]}`;
    });

    return queryString;
  }

  /**
   *
   * @private
   * @param {Object} context
   * @param {Object} options
   * @return {Array<Object>}
   */
  _buildEndpoints({ path, resource, queryParams }, { batch }) {
    let endpoint = resource ? `${path}/{resource}` : path;
    if (queryParams) endpoint += this._buildQueryString(queryParams);
    if (!resource) return [{ endpoint }];
    return this._populateResource(endpoint, batch ? resource.batched : resource.active, batch);
  }

  /**
   *
   * @private
   * @param {Object} context
   * @return {Promise}
   */
  async _checkCache({ path, resource, queryParams }) {
    if (this._disableCaching) return {};
    const endpoints = this._buildEndpoints({ path, resource, queryParams }, { batch: false });
    const promises = [];

    endpoints.forEach(({ endpoint, values }) => {
      promises.push(this._checkEntry(endpoint, values, resource));
    });

    const data = await Promise.all(promises);
    const skip = (resource && !resource.active.length) || (!resource && data.length);
    return { data, skip };
  }

  /**
   *
   * @private
   * @param {string} endpoint
   * @param {string} value
   * @param {Object} resource
   * @return {Object}
   */
  async _checkEntry(endpoint, value, resource) {
    let data;

    try {
      if (await this._cache.has(endpoint)) data = await this._cache.get(endpoint);
    } catch (err) {
      logger.error(err);
    }

    if (data && resource) {
      const index = resource.active.findIndex(val => val === value);
      if (index !== -1) resource.active.splice(index, 1);
    }

    return data || null;
  }

  /**
   *
   * @private
   * @param {Object} context
   * @return {Object}
   */
  _dedupRequest({ path, resource, fetchID, requestID }) {
    const tracker = this._getTracker(requestID, path);

    if (!tracker.active.length) {
      tracker.active = resource.active.map(value => ({ fetchID, value }));
      return {};
    }

    const promises = [];

    for (let i = resource.active.length - 1; i >= 0; i -= 1) {
      const match = tracker.active.find(obj => obj.value === resource.active[i]);

      if (match) {
        if (!tracker.pending[resource.active[i]]) tracker.pending[resource.active[i]] = [];

        promises.push(this._setPendingRequest(
          tracker.pending[resource.active[i]], { fetchID: match.fetchID },
        ));

        resource.pending = [...resource.pending, ...resource.active.splice(i, 1)];
      }
    }

    if (resource.active.length) {
      tracker.active = [...tracker.active, ...resource.active.map(value => ({ fetchID, value }))];
    }

    return { promises };
  }

  /**
   *
   * @private
   * @param {string} method
   * @param {string} endpoint
   * @param {string|Array<string>} values
   * @param {Object} context
   * @param {Object} options
   * @return {Promise}
   */
  async _fetch(method, endpoint, values, context, options) {
    let res, errors;

    try {
      logger.info(`${this._name} fetching: ${endpoint}`, context);
      const headers = options.headers ? { ...this._headers, ...options.headers } : this._headers;

      res = await fetch(`${this._baseURL}${endpoint}`, {
        method,
        headers: new Headers(headers),
      });
    } catch (err) {
      errors = err;
      logger.error(err);
    }

    if (errors) {
      this._resolveRequests({ errors }, values, context);
      return { errors };
    }

    let body = await res.json();
    body = isFunction(this._bodyParser) ? this._bodyParser(body, context) : body;

    if (body.errors) {
      this._resolveRequests({ errors: body.errors }, values, context);
      return { errors: body.errors };
    }

    const data = isFunction(this._dataParser)
        ? this._dataParser(body.data, context, res.headers) : body.data;

    if (!values) return data;
    this._resolveRequests({ data }, values, context);
    return this._resolveResource(data, context);
  }

  /**
   *
   * @private
   * @param {string} requestID
   * @param {string} path
   * @return {string}
   */
  _getTracker(requestID, path) {
    if (requestID && !this._tracker[requestID]) this._tracker[requestID] = {};
    let tracker = requestID ? this._tracker[requestID] : this._tracker;
    if (!tracker[path]) tracker[path] = {};
    tracker = tracker[path];
    if (!tracker.active) tracker.active = [];
    if (!tracker.fetching) tracker.fetching = [];
    if (!tracker.pending) tracker.pending = {};
    return tracker[path];
  }

  /**
   *
   * @private
   * @param {string} endpoint
   * @param {Array<string>} resource
   * @param {boolean} batch
   * @return {Array<Object>}
   */
  _populateResource(endpoint, resource, batch) {
    const regex = /{resource}/;
    const endpoints = [];

    if (!batch) {
      resource.forEach((value) => {
        endpoints.push({ endpoint: endpoint.replace(regex, value), values: value });
      });
    } else if (resource.length <= this._batchLimit) {
      endpoints.push({ endpoint: endpoint.replace(regex, resource.join(',')), values: resource });
    } else {
      this._batchResource(resource).forEach((group) => {
        endpoints.push({ endpoint: endpoint.replace(regex, group.join(',')), values: group });
      });
    }

    return endpoints;
  }

  /**
   *
   * @private
   * @param {Object|Array<Object>} res
   * @param {string|Array<string>} values
   * @param {Object} context
   * @return {void}
   */
  async _resolveRequests(res, values, { fetchID, requestID, path, resource, queryParams }) {
    if (!values) return;
    const { fetching, pending } = this._getTracker(requestID, path);
    const data = castArray(res.data);
    values = castArray(values);

    values.forEach((value) => {
      const index = fetching.findIndex(obj => obj.value === value);
      if (index !== -1) fetching.splice(index, 1);

      const match = res.errors
          ? { errors: res.errors } : data.find(obj => obj[resource.key] === value);

      if (!res.errors && match) this._setCache(path, value, queryParams, match);
      if (!pending[value]) return;

      for (let i = pending[value].length - 1; i >= 0; i -= 1) {
        if (pending[value][i].fetchID === fetchID) {
          pending[value][i].resolve(res);
          pending[value].splice(i, 1);
        }
      }
    });
  }

  /**
   *
   * @private
   * @param {Object|Array<Object>} data
   * @param {Object} context
   * @return {Object|Array<Object>}
   */
  _resolveResource(data, { resource }) {
    const filtered = [];

    resource.active.forEach((value) => {
      const match = data.find(obj => obj[resource.key] === value);
      if (match) filtered.push(match);
    });

    return filtered;
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {string} resource
   * @param {Object} queryParams
   * @param {Object} data
   * @return {Promise}
   */
  async _setCache(path, resource, queryParams, data) {
    if (this._disableCaching) return;
    const endpoints = this._buildEndpoints({ path, resource, queryParams }, { batch: false });

    try {
      this._cache.set(endpoints[0], data);
    } catch (err) {
      logger.error(err);
    }
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {Object} resource
   * @param {Object} queryParams
   * @param {Object} context
   * @return {Object}
   */
  _setContext(path, resource, queryParams, context) {
    if (resource) {
      const key = Object.keys(resource)[0];
      resource = { active: castArray(resource[key]), batched: [], pending: [], key };
    }

    return { ...{ path, resource, queryParams, fetchID: uuidV1() }, ...context };
  }

  /**
   *
   * @private
   * @param {Array<Function>} pending
   * @param {string} context
   * @return {Promise}
   */
  _setPendingRequest(pending, { fetchID }) {
    return new Promise((resolve) => {
      pending.push({ fetchID, resolve });
    });
  }

  /**
   *
   * @private
   * @param {Object} ids
   * @param {string} requestID
   * @param {string} path
   * @return {void}
   */
  _updatePendingRequests({ fetchID, resource }, requestID, path) {
    const { pending } = this._getTracker(requestID, path);

    if (pending[resource]) {
      pending[resource].forEach((obj) => {
        obj.fetchID = fetchID;
      });
    }
  }

  /**
   *
   * @param {Object} config
   * @return {Promise}
   */
  async get({ path, resource = null, queryParams = null, options = {}, context = {} } = {}) {
    if (!path) return null;
    context = this._setContext(path, resource, queryParams, context);
    const check = await this._checkCache(context);
    if (check.skip) return check.data;
    let promises, skip;

    if (resource) {
      const dedup = this._dedupRequest(context);

      const batch = await new Promise((resolve) => {
        setImmediate(() => {
          resolve(this._batchRequest(context, options));
        });
      });

      promises = [...dedup.promises, ...batch.promises];
      skip = batch.skip;
    }

    if (!skip) {
      const endpoints = this._buildEndpoints(context, options);

      endpoints.forEach(({ endpoint, values }) => {
        promises.push(this._fetch(endpoint, values, context, options));
      });
    }

    const data = flatten(await Promise.all(promises));
    return [...data, ...check.data];
  }
}
<commit_msg>Adding more options configurations into logic.<commit_after>import Cachemap from 'cachemap';
import { castArray, flatten, isFunction } from 'lodash';
import uuidV1 from 'uuid/v1';
import logger from './logger';

let instance;

/**
 *
 * The getta rest client
 */
export default class RestClient {
  /**
   *
   * @constructor
   * @param {Object} config
   * @return {RestClient}
   */
  constructor({
    /**
     *
     * Base url to which to make requests.
     *
     * @type {string}
     */
    baseURL,
    /**
     * How many assets can be batched together in
     * the same request.
     *
     * @type {number}
     */
    batchLimit = '20',
    /**
     *
     * Optional function used to parse the body of
     * a response prior the client returning it.
     * The client requires any data returned to be
     * on a property of 'data' and any errors returned
     * to be on a property of 'errors'.
     *
     * @type {Function}
     */
    bodyParser = null,
    /**
     *
     * Optional function used to parse the data of
     * a response prior the client returning it.
     *
     * @type {Function}
     */
    dataParser = null,
    /**
     *
     * Optional configuration to be passed to the
     * cachemap module.
     *
     * @type {Object}
     */
    cachemapOptions,
    /**
     *
     * Disables caching of responses against request url.
     *
     * @type {boolean}
     */
    disableCaching = false,
    /**
     * Any headers to be sent with requests, such as
     * authentication headers.
     *
     * @type {Object}
     */
    headers = null,
    /**
     * Whether to create a new instance of a
     * client or return the existing instance.
     *
     * @type {boolean}
     */
    newInstance = false,
    /**
     * Optional reference object used to store
     * request information against a unique
     * identifier on the context.
     *
     * @type {Object}
     */
    tracker = {},
  } = {}) {
    if (instance && !newInstance) {
      return instance;
    }

    if (!baseURL) {
      throw new Error('baseURL is mandatory properties for a rest client.');
    }

    this._baseURL = baseURL;
    this._batchLimit = batchLimit;
    this._bodyParser = bodyParser;
    this._cache = new Cachemap(cachemapOptions);
    this._dataParser = dataParser;
    this._disableCaching = disableCaching;
    this._headers = headers;
    this._tracker = tracker;
    instance = this;
    return instance;
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {Object} tracker
   * @param {string} resource
   * @param {string} fetchID
   * @param {string} requestID
   * @return {Object}
   */
  _addToFetch(path, tracker, resource, fetchID, requestID) {
    const index = tracker.active.findIndex(obj => obj.value === resource);
    const activeMatch = (index !== -1) ? tracker.active.splice(index, 1)[0] : null;
    const activeFetchID = activeMatch ? activeMatch.fetchID : fetchID;
    const fetchMatch = tracker.fetching.find(obj => obj.value === resource);

    if (!fetchMatch) {
      tracker.fetching.push({ fetchID: activeFetchID, value: resource });
      return {};
    }

    const promises = [];
    if (!tracker.pending[resource]) tracker.pending[resource] = [];

    promises.push(this._setPendingRequest(
      tracker.pending[resource], { fetchID: fetchMatch.fetchID },
    ));

    this._updatePendingRequests({ fetchID: fetchMatch.fetchID, resource }, requestID, path);
    return { pending: true, promises };
  }

  /**
   *
   * @private
   * @param {Object} context
   * @param {Object} options
   * @return {Object}
   */
  _batchRequest({ path, resource, fetchID, requestID }, { batch, batchLimit }) {
    const tracker = this._getTracker(requestID, path);

    let promises = [];

    for (let i = resource.active.length - 1; i >= 0; i -= 1) {
      const added = this._addToFetch(path, tracker, resource.active[i], fetchID, requestID);
      promises = [...promises, ...added.promises];
      if (added.pending) resource.pending = [...resource.pending, ...resource.active.splice(i, 1)];
    }

    const skip = !resource.active.length;

    if (!skip && batch) {
      const limit = batchLimit || this._batchLimit;
      resource.batched = resource.active;

      if (tracker.active.length && resource.batched.length < limit) {
        for (let i = tracker.active.length - 1; i >= 0; i -= 1) {
          resource.batched.push(tracker.active[i].value);
          tracker.fetching.push({ fetchID, value: tracker.active[i].value });

          this._updatePendingRequests(
            { fetchID, resource: tracker.active[i].value }, requestID, path,
          );

          tracker.active.splice(i, 1);
          if (resource.batched.length === limit) break;
        }
      }
    }

    return { promises, skip };
  }

  /**
   *
   * @private
   * @param {Array<string>} resource
   * @return {Array<Array>}
   */
  _batchResource(resource) {
    const batches = [];
    const countLimit = Math.ceil(resource.length / this._batchLimit);
    let count = 0;
    let start = 0;
    let stop = this._batchLimit;

    do {
      batches.push(resource.slice(start, stop));
      start += this._batchLimit;
      stop += this._batchLimit;
      count += 1;
    } while (count < countLimit);

    return batches;
  }

  /**
   *
   * @private
   * @param {Object} queryParams
   * @return {string}
   */
  _buildQueryString(queryParams) {
    let queryString = '';
    let paramCount = 0;

    Object.keys(queryParams).forEach((key) => {
      paramCount += 1;
      const prefix = paramCount === 1 ? '?' : '&';
      queryString += `${prefix}${key}=${queryParams[key]}`;
    });

    return queryString;
  }

  /**
   *
   * @private
   * @param {Object} context
   * @param {Object} options
   * @return {Array<Object>}
   */
  _buildEndpoints({ path, resource, queryParams }, { batch }) {
    let endpoint = resource ? `${path}/{resource}` : path;
    if (queryParams) endpoint += this._buildQueryString(queryParams);
    if (!resource) return [{ endpoint }];
    return this._populateResource(endpoint, batch ? resource.batched : resource.active, batch);
  }

  /**
   *
   * @private
   * @param {Object} context
   * @return {Promise}
   */
  async _checkCache({ path, resource, queryParams }) {
    if (this._disableCaching) return {};
    const endpoints = this._buildEndpoints({ path, resource, queryParams }, { batch: false });
    const promises = [];

    endpoints.forEach(({ endpoint, values }) => {
      promises.push(this._checkEntry(endpoint, values, resource));
    });

    const data = await Promise.all(promises);
    const skip = (resource && !resource.active.length) || (!resource && data.length);
    return { data, skip };
  }

  /**
   *
   * @private
   * @param {string} endpoint
   * @param {string} value
   * @param {Object} resource
   * @return {Object}
   */
  async _checkEntry(endpoint, value, resource) {
    let data;

    try {
      if (await this._cache.has(endpoint)) data = await this._cache.get(endpoint);
    } catch (err) {
      logger.error(err);
    }

    if (data && resource) {
      const index = resource.active.findIndex(val => val === value);
      if (index !== -1) resource.active.splice(index, 1);
    }

    return data || null;
  }

  /**
   *
   * @private
   * @param {Object} context
   * @return {Object}
   */
  _dedupRequest({ path, resource, fetchID, requestID }) {
    const tracker = this._getTracker(requestID, path);

    if (!tracker.active.length) {
      tracker.active = resource.active.map(value => ({ fetchID, value }));
      return {};
    }

    const promises = [];

    for (let i = resource.active.length - 1; i >= 0; i -= 1) {
      const match = tracker.active.find(obj => obj.value === resource.active[i]);

      if (match) {
        if (!tracker.pending[resource.active[i]]) tracker.pending[resource.active[i]] = [];

        promises.push(this._setPendingRequest(
          tracker.pending[resource.active[i]], { fetchID: match.fetchID },
        ));

        resource.pending = [...resource.pending, ...resource.active.splice(i, 1)];
      }
    }

    if (resource.active.length) {
      tracker.active = [...tracker.active, ...resource.active.map(value => ({ fetchID, value }))];
    }

    return { promises };
  }

  /**
   *
   * @private
   * @param {string} method
   * @param {string} endpoint
   * @param {string|Array<string>} values
   * @param {Object} context
   * @param {Object} options
   * @return {Promise}
   */
  async _fetch(method, endpoint, values, context, options) {
    let res, errors;

    try {
      logger.info(`${this._name} fetching: ${endpoint}`, context);
      const headers = options.headers ? { ...this._headers, ...options.headers } : this._headers;

      res = await fetch(`${this._baseURL}${endpoint}`, {
        method,
        headers: new Headers(headers),
      });
    } catch (err) {
      errors = err;
      logger.error(err);
    }

    if (errors) {
      this._resolveRequests({ errors }, values, context);
      return { errors };
    }

    let body = await res.json();
    const bodyParser = options.bodyParser || this._bodyParser;
    body = isFunction(bodyParser) ? bodyParser(body, context) : body;

    if (body.errors) {
      this._resolveRequests({ errors: body.errors }, values, context);
      return { errors: body.errors };
    }

    const dataParser = options.dataParser || this._dataParser;
    const data = isFunction(dataParser) ? dataParser(body.data, context, res.headers) : body.data;
    if (!values) return data;
    this._resolveRequests({ data }, values, context);
    return this._resolveResource(data, context);
  }

  /**
   *
   * @private
   * @param {string} requestID
   * @param {string} path
   * @return {string}
   */
  _getTracker(requestID, path) {
    if (requestID && !this._tracker[requestID]) this._tracker[requestID] = {};
    let tracker = requestID ? this._tracker[requestID] : this._tracker;
    if (!tracker[path]) tracker[path] = {};
    tracker = tracker[path];
    if (!tracker.active) tracker.active = [];
    if (!tracker.fetching) tracker.fetching = [];
    if (!tracker.pending) tracker.pending = {};
    return tracker[path];
  }

  /**
   *
   * @private
   * @param {string} endpoint
   * @param {Array<string>} resource
   * @param {boolean} batch
   * @return {Array<Object>}
   */
  _populateResource(endpoint, resource, batch) {
    const regex = /{resource}/;
    const endpoints = [];

    if (!batch) {
      resource.forEach((value) => {
        endpoints.push({ endpoint: endpoint.replace(regex, value), values: value });
      });
    } else if (resource.length <= this._batchLimit) {
      endpoints.push({ endpoint: endpoint.replace(regex, resource.join(',')), values: resource });
    } else {
      this._batchResource(resource).forEach((group) => {
        endpoints.push({ endpoint: endpoint.replace(regex, group.join(',')), values: group });
      });
    }

    return endpoints;
  }

  /**
   *
   * @private
   * @param {Object|Array<Object>} res
   * @param {string|Array<string>} values
   * @param {Object} context
   * @return {void}
   */
  async _resolveRequests(res, values, { fetchID, requestID, path, resource, queryParams }) {
    if (!values) return;
    const { fetching, pending } = this._getTracker(requestID, path);
    const data = castArray(res.data);
    values = castArray(values);

    values.forEach((value) => {
      const index = fetching.findIndex(obj => obj.value === value);
      if (index !== -1) fetching.splice(index, 1);

      const match = res.errors
          ? { errors: res.errors } : data.find(obj => obj[resource.key] === value);

      if (!res.errors && match) this._setCache(path, value, queryParams, match);
      if (!pending[value]) return;

      for (let i = pending[value].length - 1; i >= 0; i -= 1) {
        if (pending[value][i].fetchID === fetchID) {
          pending[value][i].resolve(res);
          pending[value].splice(i, 1);
        }
      }
    });
  }

  /**
   *
   * @private
   * @param {Object|Array<Object>} data
   * @param {Object} context
   * @return {Object|Array<Object>}
   */
  _resolveResource(data, { resource }) {
    const filtered = [];

    resource.active.forEach((value) => {
      const match = data.find(obj => obj[resource.key] === value);
      if (match) filtered.push(match);
    });

    return filtered;
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {string} resource
   * @param {Object} queryParams
   * @param {Object} data
   * @return {Promise}
   */
  async _setCache(path, resource, queryParams, data) {
    if (this._disableCaching) return;
    const endpoints = this._buildEndpoints({ path, resource, queryParams }, { batch: false });

    try {
      this._cache.set(endpoints[0], data);
    } catch (err) {
      logger.error(err);
    }
  }

  /**
   *
   * @private
   * @param {string} path
   * @param {Object} resource
   * @param {Object} queryParams
   * @param {Object} context
   * @return {Object}
   */
  _setContext(path, resource, queryParams, context) {
    if (resource) {
      const key = Object.keys(resource)[0];
      resource = { active: castArray(resource[key]), batched: [], pending: [], key };
    }

    return { ...{ path, resource, queryParams, fetchID: uuidV1() }, ...context };
  }

  /**
   *
   * @private
   * @param {Array<Function>} pending
   * @param {string} context
   * @return {Promise}
   */
  _setPendingRequest(pending, { fetchID }) {
    return new Promise((resolve) => {
      pending.push({ fetchID, resolve });
    });
  }

  /**
   *
   * @private
   * @param {Object} ids
   * @param {string} requestID
   * @param {string} path
   * @return {void}
   */
  _updatePendingRequests({ fetchID, resource }, requestID, path) {
    const { pending } = this._getTracker(requestID, path);

    if (pending[resource]) {
      pending[resource].forEach((obj) => {
        obj.fetchID = fetchID;
      });
    }
  }

  /**
   *
   * @param {Object} config
   * @return {Promise}
   */
  async get({ path, resource = null, queryParams = null, options = {}, context = {} } = {}) {
    if (!path) return null;
    context = this._setContext(path, resource, queryParams, context);
    const check = await this._checkCache(context);
    if (check.skip) return check.data;
    let promises, skip;

    if (resource) {
      const dedup = this._dedupRequest(context);

      const batch = await new Promise((resolve) => {
        setImmediate(() => {
          resolve(this._batchRequest(context, options));
        });
      });

      promises = [...dedup.promises, ...batch.promises];
      skip = batch.skip;
    }

    if (!skip) {
      const endpoints = this._buildEndpoints(context, options);

      endpoints.forEach(({ endpoint, values }) => {
        promises.push(this._fetch(endpoint, values, context, options));
      });
    }

    const data = flatten(await Promise.all(promises));
    return [...data, ...check.data];
  }
}
<|endoftext|>


<commit_before>export {
  default as View
}
from './view/view';

export {
  default as List
}
from './view/list/list';

export {
  default as Tree
}
from './view/tree/tree';

export {
  default as Form
}
from './view/form/form';

export {
  default as ViewContainer
}
from './view/container';

export {
  default as Container
}
from './container/container';

export {
  default as Tab
}
from './container/tab';

export {
  default as Window
}
from './window/window';

export {
  default as Dialog
}
from './window/dialog';

export {
  default as Prompt
}
from './window/prompt';

export {
  default as Text
}
from './component/text';

export {
  default as Component
}
from './component/component';

export {
  default as Progress
}
from './component/progress';

export {
  default as Button
}
from './control/button';

export {
  default as Upload
}
from './control/upload';

export {
  default as Color
}
from './control/color';

export {
  default as Date
}
from './control/date';

export {
  default as ButtonMenu
}
from './control/button-menu';

export {
  default as Choice
}
from './control/choice';

export {
  default as Search
}
from './control/search';

export {
  default as Separator
}
from './control/separator';

export {
  default as Border
}
from './selector/border';

export {
  default as Context
}
from './menu/context';

export {
  default as Toolbar
}
from './toolbar/toolbar';

export {
  default as Layout
}
from './layout/layout';

export {
  default as Selector
}
from './selector/selector';

export {
  default as controlIcon
}
from './icon/control';

export {
  default as appIcon
}
from './icon/app';
<commit_msg>rename button-menu to button menu<commit_after>export {
  default as View
}
from './view/view';

export {
  default as List
}
from './view/list/list';

export {
  default as Tree
}
from './view/tree/tree';

export {
  default as Form
}
from './view/form/form';

export {
  default as ViewContainer
}
from './view/container';

export {
  default as Container
}
from './container/container';

export {
  default as Tab
}
from './container/tab';

export {
  default as Window
}
from './window/window';

export {
  default as Dialog
}
from './window/dialog';

export {
  default as Prompt
}
from './window/prompt';

export {
  default as Text
}
from './component/text';

export {
  default as Component
}
from './component/component';

export {
  default as Progress
}
from './component/progress';

export {
  default as Button
}
from './control/button';

export {
  default as Upload
}
from './control/upload';

export {
  default as Color
}
from './control/color';

export {
  default as Date
}
from './control/date';

export {
  default as Buttonmenu
}
from './control/button-menu';

export {
  default as Choice
}
from './control/choice';

export {
  default as Search
}
from './control/search';

export {
  default as Separator
}
from './control/separator';

export {
  default as Border
}
from './selector/border';

export {
  default as Context
}
from './menu/context';

export {
  default as Toolbar
}
from './toolbar/toolbar';

export {
  default as Layout
}
from './layout/layout';

export {
  default as Selector
}
from './selector/selector';

export {
  default as controlIcon
}
from './icon/control';

export {
  default as appIcon
}
from './icon/app';
<|endoftext|>


<commit_before>import merge from 'lodash/merge';
import {normalize} from 'normalizr';

const request = (path, method, body, apiOptions = {}, requestHeaders = {}) => {
  const {apiHeaders, apiGateway} = apiOptions;
  const headers = Object.assign({}, apiHeaders, requestHeaders);

  const jsonContentAllowed = method === 'PUT' || method === 'POST';
  if (jsonContentAllowed && body && !headers['Content-Type']) {
    headers['Content-Type'] = 'application/json';
  }

  const acceptsJson = method === 'GET' || method === 'POST' || method === 'OPTIONS';
  if (acceptsJson) {
    headers.Accept = 'application/json';
  }

  const gateway = (!apiGateway || apiGateway === '/') ? '' : apiGateway;
  const url = `${gateway}${path}`;

  return new Promise(resolve => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    Object.keys(headers).forEach(header => {
      xhr.setRequestHeader(header, headers[header]);
    });
    xhr.withCredentials = true;
    xhr.onload = () => {
      const {status, statusText, response} = xhr;
      if (status >= 200 && status < 300) {
        resolve(JSON.parse(response));
      } else {
        throw statusText;
      }
    };
    xhr.send(body);
  });
};

const creator = apiOptions => {
  const middleware = ({dispatch, getState}) => next => action => {
    const {
      types,
      schema,
      xhr,
      payload = {}
    } = action;

    if (!types) {
      // Normal action: pass it on
      return next(action);
    }

    if (!Array.isArray(types) ||
      types.length !== 3 ||
      !types.every(type => typeof type === 'string')
    ) {
      throw new Error('api: Expected an array of three string types.');
    }

    if ((typeof xhr !== 'object') || !xhr.url || !xhr.method) {
      throw new Error('api: Expected xhr to be an object with at least a url and a method.');
    }

    const [requestType, successType, failType] = types;

    dispatch(merge({}, {
      type: requestType,
      payload
    }));

    const method = xhr.method.toUpperCase();
    return request(xhr.url, method, xhr.data, apiOptions, xhr.headers)
    .then(response => {
      if (!schema) {
        dispatch({
          type: successType,
          payload: Object.assign({}, payload, response)
        });
      } else {
        const state = getState();
        if (method.toUpperCase() === 'DELETE') {
          const urlParts = xhr.url.split('/');
          const entityId = urlParts[urlParts.length - 1];
          const collection = schema.getKey();
          state.entities[collection][entityId] = null;
          dispatch(Object.assign({}, {
            type: successType
          }));
        } else {
          let updatedResponse = response;
          const isCollection = Array.isArray(response);
          const hasPayload = !!Object.entries(payload).length;
          if (hasPayload) {
            // Does response item/collection have sub collections?
            // If it does have sub collections, we need to link the entity to those collections,
            // ie. subcollections need to have the id of this entity, ie. the entity they belong to.
            //
            // itemId, ie. key for subcollections, must be specified in the schema:
            // e.g.
            // let's say our current entity is 'hotel' with id: 'XYZ',
            // and 'hotel' entity has 'rooms' subcollection on it.
            // The schemas for 'hotel' and 'room' entities will look like below:
            //
            // const hotelSchema = new Schema('hotels');
            // const roomSchema = new Schema('rooms');
            // hotelSchema.define({
            //   rooms: arrayOf(roomSchema),
            //   itemId: 'hotelId' // key to populate in rooms
            // });
            //
            // When the above schema setup is processed by the middleware here,
            // the middleware will add hotelId: 'XYZ' to each zone entry in the 'rooms' subcollection.
            const itemSchema = schema.getItemSchema ? schema.getItemSchema() : schema;
            const itemKey = itemSchema.itemId;
            const sampleItem = isCollection ? response[0] || {} : response;
            // Get collection props
            const arrayProps = Object.keys(sampleItem).filter(prop => Array.isArray(sampleItem[prop]));
            // Insert itemId in sub collection...
            if (isCollection) {
              arrayProps.forEach(prop => {
                if (itemSchema[prop]) {
                  response.forEach(item => {
                    item[prop] = item[prop].map(subItem => merge({}, subItem, {[itemKey]: item.id}));
                  });
                }
              });
              updatedResponse = response.map(entry => Object.assign({}, entry, payload));
            } else {
              arrayProps.forEach(prop => {
                if (itemSchema[prop]) {
                  response[prop] = response[prop].map(subItem => merge({}, subItem, {[itemKey]: response.id}));
                }
              });
              updatedResponse = Object.assign({}, response, payload);
            }
          }
          let entitySchema = Object.assign({}, schema);
          const schemaKey = isCollection ? schema.getItemSchema().getKey() : schema.getKey();
          if (schemaKey) {
            entitySchema = {[schemaKey]: schema};
            updatedResponse = {[schemaKey]: updatedResponse};
          }

          dispatch({
            type: successType,
            payload: normalize(updatedResponse, entitySchema)
          });
        }
      }
      return response;
    })
    .catch(
        error => {
          dispatch({
            type: failType,
            payload: Object.assign({}, payload, error)
          });
          throw error;
        }
      );
  };
  return middleware;
};

export default creator;
<commit_msg>Adapted to normalizr v3.<commit_after>import merge from 'lodash/merge';
import {normalize} from 'normalizr';

const request = (path, method, body, apiOptions = {}, requestHeaders = {}) => {
  const {apiHeaders, apiGateway} = apiOptions;
  const headers = Object.assign({}, apiHeaders, requestHeaders);

  const jsonContentAllowed = method === 'PUT' || method === 'POST';
  if (jsonContentAllowed && body && !headers['Content-Type']) {
    headers['Content-Type'] = 'application/json';
  }

  const acceptsJson = method === 'GET' || method === 'POST' || method === 'OPTIONS';
  if (acceptsJson) {
    headers.Accept = 'application/json';
  }

  const gateway = (!apiGateway || apiGateway === '/') ? '' : apiGateway;
  const url = `${gateway}${path}`;

  return new Promise(resolve => {
    const xhr = new XMLHttpRequest();
    xhr.open(method, url);
    Object.keys(headers).forEach(header => {
      xhr.setRequestHeader(header, headers[header]);
    });
    xhr.withCredentials = true;
    xhr.onload = () => {
      const {status, statusText, response} = xhr;
      if (status >= 200 && status < 300) {
        resolve(JSON.parse(response));
      } else {
        throw statusText;
      }
    };
    xhr.send(body);
  });
};

const creator = apiOptions => {
  const middleware = ({dispatch, getState}) => next => action => {
    const {
      types,
      schema,
      xhr,
      payload = {}
    } = action;

    if (!types) {
      // Normal action: pass it on
      return next(action);
    }

    if (!Array.isArray(types) ||
      types.length !== 3 ||
      !types.every(type => typeof type === 'string')
    ) {
      throw new Error('api: Expected an array of three string types.');
    }

    if ((typeof xhr !== 'object') || !xhr.url || !xhr.method) {
      throw new Error('api: Expected xhr to be an object with at least a url and a method.');
    }

    const [requestType, successType, failType] = types;

    dispatch(merge({}, {
      type: requestType,
      payload
    }));

    const method = xhr.method.toUpperCase();
    return request(xhr.url, method, xhr.data, apiOptions, xhr.headers)
    .then(response => {
      if (!schema) {
        dispatch({
          type: successType,
          payload: Object.assign({}, payload, response)
        });
      } else {
        const state = getState();
        if (method.toUpperCase() === 'DELETE') {
          const urlParts = xhr.url.split('/');
          const entityId = urlParts[urlParts.length - 1];
          const collection = schema.getKey();
          state.entities[collection][entityId] = null;
          dispatch(Object.assign({}, {
            type: successType
          }));
        } else {
          let updatedResponse = response;
          const isCollection = Array.isArray(response);
          const hasPayload = !!Object.entries(payload).length;
          if (hasPayload) {
            // Does response item/collection have sub collections?
            // If it does have sub collections, we need to link the entity to those collections,
            // ie. subcollections need to have the id of this entity, ie. the entity they belong to.
            //
            // itemId, ie. key for subcollections, must be specified in the schema:
            // e.g.
            // let's say our current entity is 'hotel' with id: 'XYZ',
            // and 'hotel' entity has 'rooms' subcollection on it.
            // The schemas for 'hotel' and 'room' entities will look like below:
            //
            // const hotelSchema = new Schema('hotels');
            // const roomSchema = new Schema('rooms');
            // hotelSchema.define({
            //   rooms: arrayOf(roomSchema),
            //   itemId: 'hotelId' // key to populate in rooms
            // });
            //
            // When the above schema setup is processed by the middleware here,
            // the middleware will add hotelId: 'XYZ' to each zone entry in the 'rooms' subcollection.
            const itemSchema = schema.getItemSchema ? schema.getItemSchema() : schema;
            const itemKey = itemSchema.itemId;
            const sampleItem = isCollection ? response[0] || {} : response;
            // Get collection props
            const arrayProps = Object.keys(sampleItem).filter(prop => Array.isArray(sampleItem[prop]));
            // Insert itemId in sub collection...
            if (isCollection) {
              arrayProps.forEach(prop => {
                if (itemSchema[prop]) {
                  response.forEach(item => {
                    item[prop] = item[prop].map(subItem => merge({}, subItem, {[itemKey]: item.id}));
                  });
                }
              });
              updatedResponse = response.map(entry => Object.assign({}, entry, payload));
            } else {
              arrayProps.forEach(prop => {
                if (itemSchema[prop]) {
                  response[prop] = response[prop].map(subItem => merge({}, subItem, {[itemKey]: response.id}));
                }
              });
              updatedResponse = Object.assign({}, response, payload);
            }
          }

          dispatch({
            type: successType,
            payload: normalize(updatedResponse, schema)
          });
        }
      }
      return response;
    })
    .catch(
        error => {
          dispatch({
            type: failType,
            payload: Object.assign({}, payload, error)
          });
          throw error;
        }
      );
  };
  return middleware;
};

export default creator;
<|endoftext|>


<commit_before>/*

This file contains a linker for bundles of (redux) actions and reducers,
(redux-saga) selectors and sagas, and (React) views.

A bundle is implemented as a generator that yields linker steps to:

  - include another bundle;
  - declare the use of dependencies defined by other bundles;
  - define an action, selector, or view;
  - add an action reducer or a saga

A bundle's generator function takes as its single argument an object which
the linker populates with the definitions made or used by the bundle.
A shared flat namespace is used for actions, selectors, and views.

Actions are redux actions and have string values.

Selectors are cheap functions that take the global state and extract a limited
view of it.  A selector may be defined in terms of other selectors, and may
also be used in action reducers and sagas.

Multiple action reducers for the same action are currently not supported (an
exception is thrown at link time), but they could be composed in link order.

If a selector match the name of a view name, the view is connected to the
store using the selector.

*/

import {createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
import {delay} from 'redux-saga';
import {call, cancelled, fork} from 'redux-saga/effects';
import {connect} from 'react-redux';

function makeSafeProxy (obj, onError) {
  if (typeof Proxy !== 'function') {
    return obj;
  }
  const safeGet = function (target, property) {
    if (property in target) {
      return target[property];
    } else {
      return onError(target, property);
    }
  }
  return new Proxy(obj, {get: safeGet});
}

export function include (bundle) {
  return {type: 'include', bundle};
};

export function use (...names) {
  return {type: 'use', names};
};

export function defineAction (name, action) {
  return {type: 'defineAction', name, action};
};

export function defineSelector (name, selector) {
  return {type: 'defineSelector', name, selector};
};

export function defineView (name, selector, view) {
  if (view === undefined) {
    view = selector;
    selector = undefined;
  }
  return {type: 'defineView', name, view, selector};
};

// Add an action reducer.
export function addReducer (name, reducer) {
  return {type: 'addReducer', name, reducer};
};

export function addSaga (saga) {
  return {type: 'addSaga', saga};
};

export function addEnhancer (enhancer) {
  return {type: 'addEnhancer', enhancer};
};

export function link (rootBundle) {

  // name → value
  const scope = {};

  // Map(name → type)
  const typeMap = new Map();

  // Map(action type → action name)
  const nameForActionType = new Map();

  // action name → action reducer
  const reducerMap = new Map();

  const sagas = [];

  const enhancers = [];

  // Reducer linking is deferred until all actions have been defined.
  const reducerQueue = [];

  // 'use' directives are queued and dependency objects are populated after
  // all definitions have taken effect.
  const useQueue = [];

  // View linking is defered until all selectors have been added.
  const viewQueue = [];

  const linkErrors = [];

  function undeclaredDependencyError (target, property) {
    throw new Error(`use of undeclared dependency ${property}`);
  }

  const depsBase = {
    get: function (name) {
      return scope[name];
    }
  };

  function include_ (bundle) {
    const base = Object.create(depsBase);
    const deps = makeSafeProxy(base, undeclaredDependencyError);
    const it = bundle(deps);
    let result = it.next();
    while (!result.done) {
      interpretDirective(result.value, deps);
      result = it.next();
    }
  }

  function defineAction_ (name, action) {
    if (name in scope) {
      throw new Error(`linker conflict on ${name}`);
    }
    if (nameForActionType.has(action)) {
      throw `action type conflict: ${action}`;
    }
    scope[name] = action;
    typeMap.set(name, 'action');
    nameForActionType.set(action, name);
  }

  function defineSelector_ (name, selector) {
    if (name in scope) {
      throw new Error(`linker conflict on ${name}`);
    }
    scope[name] = function () {
      try {
        return selector.apply(null, arguments);
      } catch (ex) {
        console.log(`selector ${name} threw an exception`, ex);
        return {};
      }
    };
    typeMap.set(name, 'selector');
  }

  function addReducer_ (name, reducer) {
    if (!(name in scope)) {
      throw new Error(`reducer for undefined action ${name}`);
    }
    if (typeMap.get(name) !== 'action') {
      throw new Error(`reducer for non-action ${name}`);
    }
    const actionType = scope[name];
    if (reducerMap.has(actionType)) {
      const prevReducer = reducerMap.get(actionType);
      reducerMap.set(actionType, function (state, action) {
        return reducer(prevReducer(state, action), action);
      });
    } else {
      reducerMap.set(actionType, reducer);
    }
  }

  function interpretDirective (dir, deps) {
    switch (dir.type) {
      case 'include':
        include_(dir.bundle);
        break;
      case 'use':
        useQueue.push([deps, dir.names]);
        break;
      case 'defineAction':
        defineAction_(dir.name, dir.action);
        useQueue.push([deps, dir.name]);
        break;
      case 'defineSelector':
        defineSelector_(dir.name, dir.selector);
        useQueue.push([deps, dir.name]);
        break;
      case 'defineView':
        viewQueue.push(dir);
        useQueue.push([deps, dir.name]);
        break;
      case 'addReducer':
        reducerQueue.push(dir);
        break;
      case 'addSaga':
        sagas.push(dir.saga);
        break;
      case 'addEnhancer':
        enhancers.push(dir.enhancer);
        break;
      default:
        throw `unhandled link directive type ${dir.type}`;
    }
  }

  function provide (deps, name) {
    if (name in scope) {
      deps[name] = scope[name];
    } else {
      throw new Error(`undefined dependency: ${name}`);
    }
  }

  include_(rootBundle);

  // Define reducers.
  reducerQueue.forEach(function (dir) {
    if (typeMap.get(dir.name) !== 'action') {
      throw new Error(`invalid reducer target ${dir.name}`);
    }
    addReducer_(dir.name, dir.reducer);
  });

  // Define and connect views.
  viewQueue.forEach(function (dir) {
    let {name, selector, view} = dir;
    if (selector in scope) {
      view = connect(scope[selector])(view);
      view.displayName = `View(${name})`;
    }
    scope[name] = view;
    typeMap.set(name, 'view');
  });

  // Provide dependencies.
  useQueue.forEach(function (item) {
    const deps = item[0];
    if (typeof item[1] === 'string') {
      provide(deps, item[1]);
    } else if (Array.isArray(item[1])) {
      item[1].forEach(name => provide(deps, name));
    } else {
      throw new Error('invalid use');
    }
  });

  // Build the reducer.
  const reducer = function (state, action) {
    // TODO: add support for reducer hooks
    if (reducerMap.has(action.type)) {
      try {
        state = reducerMap.get(action.type)(state, action);
      } catch (ex) {
        console.log('exception in reducer', action, state, ex);
      }
    }
    return state;
  }

  const sagaMiddleware = createSagaMiddleware();
  let enhancer = applyMiddleware(sagaMiddleware);
  enhancers.forEach(function (other) {
    enhancer = compose(enhancer, other);
  });

  // Create the store.
  const store = createStore(reducer, null, enhancer);

  function* runSaga (saga) {
    try {
      yield call(saga);
    } catch (ex) {
      console.log(`saga ${saga.name} has exited`, ex);
    }
  }

  // Start the sagas.
  function* rootSaga () {
    yield sagas.map(saga => fork(runSaga, saga));
  }

  function start () {
    sagaMiddleware.run(rootSaga);
  }

  return {store, scope, start};
};
<commit_msg>remove typeMap<commit_after>/*

This file contains a linker for bundles of (redux) actions and reducers,
(redux-saga) selectors and sagas, and (React) views.

A bundle is implemented as a generator that yields linker steps to:

  - include another bundle;
  - declare the use of dependencies defined by other bundles;
  - define an action, selector, or view;
  - add an action reducer or a saga

A bundle's generator function takes as its single argument an object which
the linker populates with the definitions made or used by the bundle.
A shared flat namespace is used for actions, selectors, and views.

Actions are redux actions and have string values.

Selectors are cheap functions that take the global state and extract a limited
view of it.  A selector may be defined in terms of other selectors, and may
also be used in action reducers and sagas.

Multiple action reducers for the same action are currently not supported (an
exception is thrown at link time), but they could be composed in link order.

If a selector match the name of a view name, the view is connected to the
store using the selector.

*/

import {createStore, applyMiddleware, compose} from 'redux';
import createSagaMiddleware from 'redux-saga';
import {delay} from 'redux-saga';
import {call, cancelled, fork} from 'redux-saga/effects';
import {connect} from 'react-redux';

function makeSafeProxy (obj, onError) {
  if (typeof Proxy !== 'function') {
    return obj;
  }
  const safeGet = function (target, property) {
    if (property in target) {
      return target[property];
    } else {
      return onError(target, property);
    }
  }
  return new Proxy(obj, {get: safeGet});
}

export function include (bundle) {
  return {type: 'include', bundle};
};

export function use (...names) {
  return {type: 'use', names};
};

export function defineAction (name, action) {
  return {type: 'defineAction', name, action};
};

export function defineSelector (name, selector) {
  return {type: 'defineSelector', name, selector};
};

export function defineView (name, selector, view) {
  if (view === undefined) {
    view = selector;
    selector = undefined;
  }
  return {type: 'defineView', name, view, selector};
};

// Add an action reducer.
export function addReducer (name, reducer) {
  return {type: 'addReducer', name, reducer};
};

export function addSaga (saga) {
  return {type: 'addSaga', saga};
};

export function addEnhancer (enhancer) {
  return {type: 'addEnhancer', enhancer};
};

export function link (rootBundle) {

  // name → value
  const scope = {};

  // Map(action type → action name)
  const nameForActionType = new Map();

  // action name → action reducer
  const reducerMap = new Map();

  const sagas = [];

  const enhancers = [];

  // Reducer linking is deferred until all actions have been defined.
  const reducerQueue = [];

  // 'use' directives are queued and dependency objects are populated after
  // all definitions have taken effect.
  const useQueue = [];

  // View linking is defered until all selectors have been added.
  const viewQueue = [];

  const linkErrors = [];

  function undeclaredDependencyError (target, property) {
    throw new Error(`use of undeclared dependency ${property}`);
  }

  const depsBase = {
    get: function (name) {
      return scope[name];
    }
  };

  function include_ (bundle) {
    const base = Object.create(depsBase);
    const deps = makeSafeProxy(base, undeclaredDependencyError);
    const it = bundle(deps);
    let result = it.next();
    while (!result.done) {
      interpretDirective(result.value, deps);
      result = it.next();
    }
  }

  function defineAction_ (name, action) {
    if (name in scope) {
      throw new Error(`linker conflict on ${name}`);
    }
    if (nameForActionType.has(action)) {
      throw `action type conflict: ${action}`;
    }
    scope[name] = action;
    nameForActionType.set(action, name);
  }

  function defineSelector_ (name, selector) {
    if (name in scope) {
      throw new Error(`linker conflict on ${name}`);
    }
    scope[name] = function () {
      try {
        return selector.apply(null, arguments);
      } catch (ex) {
        console.log(`selector ${name} threw an exception`, ex);
        return {};
      }
    };
  }

  function addReducer_ (name, reducer) {
    if (!(name in scope)) {
      throw new Error(`reducer for undefined action ${name}`);
    }
    const actionType = scope[name];
    if (reducerMap.has(actionType)) {
      const prevReducer = reducerMap.get(actionType);
      reducerMap.set(actionType, function (state, action) {
        return reducer(prevReducer(state, action), action);
      });
    } else {
      reducerMap.set(actionType, reducer);
    }
  }

  function interpretDirective (dir, deps) {
    switch (dir.type) {
      case 'include':
        include_(dir.bundle);
        break;
      case 'use':
        useQueue.push([deps, dir.names]);
        break;
      case 'defineAction':
        defineAction_(dir.name, dir.action);
        useQueue.push([deps, dir.name]);
        break;
      case 'defineSelector':
        defineSelector_(dir.name, dir.selector);
        useQueue.push([deps, dir.name]);
        break;
      case 'defineView':
        viewQueue.push(dir);
        useQueue.push([deps, dir.name]);
        break;
      case 'addReducer':
        reducerQueue.push(dir);
        break;
      case 'addSaga':
        sagas.push(dir.saga);
        break;
      case 'addEnhancer':
        enhancers.push(dir.enhancer);
        break;
      default:
        throw `unhandled link directive type ${dir.type}`;
    }
  }

  function provide (deps, name) {
    if (name in scope) {
      deps[name] = scope[name];
    } else {
      throw new Error(`undefined dependency: ${name}`);
    }
  }

  include_(rootBundle);

  // Define reducers.
  reducerQueue.forEach(function (dir) {
    addReducer_(dir.name, dir.reducer);
  });

  // Define and connect views.
  viewQueue.forEach(function (dir) {
    let {name, selector, view} = dir;
    if (selector in scope) {
      view = connect(scope[selector])(view);
      view.displayName = `View(${name})`;
    }
    scope[name] = view;
  });

  // Provide dependencies.
  useQueue.forEach(function (item) {
    const deps = item[0];
    if (typeof item[1] === 'string') {
      provide(deps, item[1]);
    } else if (Array.isArray(item[1])) {
      item[1].forEach(name => provide(deps, name));
    } else {
      throw new Error('invalid use');
    }
  });

  // Build the reducer.
  const reducer = function (state, action) {
    // TODO: add support for reducer hooks
    if (reducerMap.has(action.type)) {
      try {
        state = reducerMap.get(action.type)(state, action);
      } catch (ex) {
        console.log('exception in reducer', action, state, ex);
      }
    }
    return state;
  }

  const sagaMiddleware = createSagaMiddleware();
  let enhancer = applyMiddleware(sagaMiddleware);
  enhancers.forEach(function (other) {
    enhancer = compose(enhancer, other);
  });

  // Create the store.
  const store = createStore(reducer, null, enhancer);

  function* runSaga (saga) {
    try {
      yield call(saga);
    } catch (ex) {
      console.log(`saga ${saga.name} has exited`, ex);
    }
  }

  // Start the sagas.
  function* rootSaga () {
    yield sagas.map(saga => fork(runSaga, saga));
  }

  function start () {
    sagaMiddleware.run(rootSaga);
  }

  return {store, scope, start};
};
<|endoftext|>


<commit_before>var _ = require('underscore');
var EventEmitter = require('events');
var SKIP_WORDS = require('./skipWords');
var spellchecker = require('spellchecker');
var util = require('util');

/**
 * Creates a spell-check provider to be passed to `webFrame.setSpellCheckProvider`.
 *
 * @param {String} language - The language that is being spell-checked. 'en-US'
 *   is the only supported value at present.
 *
 * @return {SpellCheckProvider}
 */
var SpellCheckProvider = function(language) {
  EventEmitter.call(this);

  this._language = language;
};

util.inherits(SpellCheckProvider, EventEmitter);

_.extend(SpellCheckProvider.prototype, {
  spellCheck: function(text) {
    var skipWords = SKIP_WORDS[this._language];
    if (_.contains(skipWords, text)) return true;

    var textIsMisspelled = !spellchecker.isMisspelled(text);
    if (textIsMisspelled) {
      this.emit('misspelling', spellchecker.getCorrectionsForMisspelling(text));
    }
    return !textIsMisspelled;
  }
});

module.exports = SpellCheckProvider;
<commit_msg>bug fixed: spellchecker returns false if word is  missplelled. Code seems to work on the assumption that it'll return true. Need to validate if it is a platform or version specific isue.<commit_after>var _ = require('underscore');
var EventEmitter = require('events');
var SKIP_WORDS = require('./skipWords');
var spellchecker = require('spellchecker');
var util = require('util');

/**
 * Creates a spell-check provider to be passed to `webFrame.setSpellCheckProvider`.
 *
 * @param {String} language - The language that is being spell-checked. 'en-US'
 *   is the only supported value at present.
 *
 * @return {SpellCheckProvider}
 */
var SpellCheckProvider = function(language) {
  EventEmitter.call(this);

  this._language = language;
};

util.inherits(SpellCheckProvider, EventEmitter);

_.extend(SpellCheckProvider.prototype, {
  spellCheck: function(text) {
    var skipWords = SKIP_WORDS[this._language];
    if (_.contains(skipWords, text)) return true;

      var textIsMisspelled = (""!==spellchecker.isMisspelled(text)) && (true!==spellchecker.isMisspelled(text));
      if (textIsMisspelled) {
      this.emit('misspelling', spellchecker.getCorrectionsForMisspelling(text));
    }
    return !textIsMisspelled;
  }
});

module.exports = SpellCheckProvider;
<|endoftext|>


<commit_before>/* eslint-disable no-unused-vars */
import path from 'path';
import { app } from 'electron';
import HtmlBody from './htmlBody';
import SplashWindow from './splashWindow';

/**
 * Settings object.
 * @typedef {Object} PluginSettings
 * @property {boolean} enabled        - is splash screen enabled
 * @property {string}  windowTitle    - title of the window that shows splash screen - it defaults
 *                                      to the `name` from settings.json
 * @property {string}  imagePath      - path to the image relative to the .desktop dir
 * @property {Object}  style          - style of the html body that displays the image
 * @property {Object}  windowSettings - settings passed to BrowserWindow
 * @property {boolean} clickThrough   - enables window click-through [true by default]
 * @property {boolean} debug          - enables devTools, makes the window remain open,
 *                                      sets `resizable` and `alwaysOnTop` to false
 */

/**
 * Implements a simple splash screen for Meteor Desktop.
 *
 * @class
 */
export default class SplashScreen {

    /**
     * @param {Object} log              - Winston logger
     * @param {Object} appSettings      - settings.json object
     * @param {Object} eventsBus        - event emitter for listening or emitting events on the
     *                                    desktop side
     * @param {PluginSettings} settings - plugin settings
     */
    constructor({ log, appSettings, eventsBus, settings }) {
        if (process.env.METEOR_DESKTOP_NO_SPLASH_SCREEN ||
            ('enabled' in settings && !settings.enabled)) return;

        const installPath = app.getPath('userData');

        const appPath = app.getAppPath();

        this.eventsBus = eventsBus;

        this.log = log;

        this.htmlBody = new HtmlBody(
            this.log.getLoggerFor('html'),
            path.join(__dirname, 'splash.html'),
            // TODO: make this path configurable and absolute
            ~appPath.indexOf('asar') ? path.resolve(path.join(appPath, '..')) : appPath,
            installPath,
            ('windowTitle' in settings) ? settings.windowTitle : appSettings.name,
            ('imagePath' in settings) ? settings.imagePath : undefined,
            ('style' in settings) ? settings.style : {}
        );

        const windowSettings = ('windowSettings' in settings) ? settings.windowSettings : {};
        windowSettings.clickThrough = 'clickThrough' in settings ? settings.clickThrough : true;

        if ('window' in appSettings && 'icon' in appSettings.window) {
            windowSettings.icon = appSettings.window.icon;
        }

        this.splashWindow = new SplashWindow(
            this.log.getLoggerFor('splashWindow'),
            this.htmlBody.getInstallPath(),
            windowSettings,
            settings.debug
        );

        this.registerToEvents();
    }

    /**
     * Registers listeners on crucial events emitted on the events bus.
     */
    registerToEvents() {
        // beforeModulesLoad is invoked directly before the skeleton starts loading modules.
        this.eventsBus.on('beforeModulesLoad', () => {
            this.htmlBody.prepare();
            this.splashWindow.show();
        });

        // beforeLoadFinish is emitted just before the app window is shown, so it is the best time
        // to hide the splashscreen.
        this.eventsBus.on('beforeLoadFinish',
            this.splashWindow.close.bind(this.splashWindow));

        // We need to close the splash screen in case something went wrong.
        this.eventsBus.on('startupFailed', this.splashWindow.close.bind(this.splashWindow));
        this.eventsBus.on('unhandledException', this.splashWindow.close.bind(this.splashWindow));
    }
}
<commit_msg>Support METEOR_DESKTOP_DEBUG<commit_after>/* eslint-disable no-unused-vars */
import path from 'path';
import { app } from 'electron';
import HtmlBody from './htmlBody';
import SplashWindow from './splashWindow';

/**
 * Settings object.
 * @typedef {Object} PluginSettings
 * @property {boolean} enabled        - is splash screen enabled
 * @property {string}  windowTitle    - title of the window that shows splash screen - it defaults
 *                                      to the `name` from settings.json
 * @property {string}  imagePath      - path to the image relative to the .desktop dir
 * @property {Object}  style          - style of the html body that displays the image
 * @property {Object}  windowSettings - settings passed to BrowserWindow
 * @property {boolean} clickThrough   - enables window click-through [true by default]
 * @property {boolean} debug          - enables devTools, makes the window remain open,
 *                                      sets `resizable` and `alwaysOnTop` to false
 */

/**
 * Implements a simple splash screen for Meteor Desktop.
 *
 * @class
 */
export default class SplashScreen {

    /**
     * @param {Object} log              - Winston logger
     * @param {Object} appSettings      - settings.json object
     * @param {Object} eventsBus        - event emitter for listening or emitting events on the
     *                                    desktop side
     * @param {PluginSettings} settings - plugin settings
     */
    constructor({ log, appSettings, eventsBus, settings }) {
        if (process.env.METEOR_DESKTOP_NO_SPLASH_SCREEN ||
            ('enabled' in settings && !settings.enabled)) return;

        const installPath = app.getPath('userData');

        const appPath = app.getAppPath();

        this.eventsBus = eventsBus;

        this.log = log;

        this.htmlBody = new HtmlBody(
            this.log.getLoggerFor('html'),
            path.join(__dirname, 'splash.html'),
            // TODO: make this path configurable and absolute
            ~appPath.indexOf('asar') ? path.resolve(path.join(appPath, '..')) : appPath,
            installPath,
            ('windowTitle' in settings) ? settings.windowTitle : appSettings.name,
            ('imagePath' in settings) ? settings.imagePath : undefined,
            ('style' in settings) ? settings.style : {}
        );

        const windowSettings = ('windowSettings' in settings) ? settings.windowSettings : {};
        windowSettings.clickThrough = 'clickThrough' in settings ? settings.clickThrough : true;

        if ('window' in appSettings && 'icon' in appSettings.window) {
            windowSettings.icon = appSettings.window.icon;
        }

        const debug = process.env.METEOR_DESKTOP_DEBUG ? true : settings.debug;

        this.splashWindow = new SplashWindow(
            this.log.getLoggerFor('splashWindow'),
            this.htmlBody.getInstallPath(),
            windowSettings,
            debug
        );

        this.registerToEvents();
    }

    /**
     * Registers listeners on crucial events emitted on the events bus.
     */
    registerToEvents() {
        // beforeModulesLoad is invoked directly before the skeleton starts loading modules.
        this.eventsBus.on('beforeModulesLoad', () => {
            this.htmlBody.prepare();
            this.splashWindow.show();
        });

        // beforeLoadFinish is emitted just before the app window is shown, so it is the best time
        // to hide the splashscreen.
        this.eventsBus.on('beforeLoadFinish',
            this.splashWindow.close.bind(this.splashWindow));

        // We need to close the splash screen in case something went wrong.
        this.eventsBus.on('startupFailed', this.splashWindow.close.bind(this.splashWindow));
        this.eventsBus.on('unhandledException', this.splashWindow.close.bind(this.splashWindow));
    }
}
<|endoftext|>


<commit_before>/*!
 * resol-vbus - A JavaScript library for processing RESOL VBus data
 *
 * Copyright (c) 2013-2014, Daniel Wippermann.
 *
 * This Software is released under the MIT license.
 */
'use strict';

var packageInfo = require('../package.json');

var utils = require('./utils');
var extend = require('./extend');

var I18N = require('./i18n');

var Specification = require('./specification');

var Header = require('./header');
var Packet = require('./packet');
var Datagram = require('./datagram');
var Telegram = require('./telegram');

var HeaderSet = require('./header-set');
var HeaderSetConsolidator = require('./header-set-consolidator');

var Connection = require('./connection');
var SerialConnection = require('./serial-connection');
var TcpConnection = require('./tcp-connection');

var DataSource = require('./data-source');
var SerialDataSource = require('./serial-data-source');
var TcpDataSource = require('./tcp-data-source');

var DataSourceProvider = require('./data-source-provider');
var SerialDataSourceProvider = require('./serial-data-source-provider');
var TcpDataSourceProvider = require('./tcp-data-source-provider');

var Converter = require('./converter');
var VBusRecordingConverter = require('./vbus-recording-converter');
var TextConverter = require('./text-converter');
var DLxJsonConverter = require('./dlx-json-converter');

var Recorder = require('./recorder');
var DLxRecorder = require('./dlx-recorder');
var FileSystemRecorder = require('./filesystem-recorder');

var ConfigurationOptimizer = require('./configuration-optimizer');

var Customizer = require('./customizer');
var ConnectionCustomizer = require('./connection-customizer');

module.exports = {

    VERSION: packageInfo.version,

    utils: utils,
    extend: extend,

    I18N: I18N,

    Specification: Specification,

    Header: Header,
    Packet: Packet,
    Datagram: Datagram,
    Telegram: Telegram,

    HeaderSet: HeaderSet,
    HeaderSetConsolidator: HeaderSetConsolidator,

    Connection: Connection,
    SerialConnection: SerialConnection,
    TcpConnection: TcpConnection,

    DataSource: DataSource,
    SerialDataSource: SerialDataSource,
    TcpDataSource: TcpDataSource,

    DataSourceProvider: DataSourceProvider,
    SerialDataSourceProvider: SerialDataSourceProvider,
    TcpDataSourceProvider: TcpDataSourceProvider,

    Converter: Converter,
    VBusRecordingConverter: VBusRecordingConverter,
    TextConverter: TextConverter,
    DLxJsonConverter: DLxJsonConverter,

    Recorder: Recorder,
    DLxRecorder: DLxRecorder,
    FileSystemRecorder: FileSystemRecorder,

    ConfigurationOptimizer: ConfigurationOptimizer,

    Customizer: Customizer,
    ConnectionCustomizer: ConnectionCustomizer,

};
<commit_msg>Export TcpConnectionEndpoint class.<commit_after>/*!
 * resol-vbus - A JavaScript library for processing RESOL VBus data
 *
 * Copyright (c) 2013-2014, Daniel Wippermann.
 *
 * This Software is released under the MIT license.
 */
'use strict';

var packageInfo = require('../package.json');

var utils = require('./utils');
var extend = require('./extend');

var I18N = require('./i18n');

var Specification = require('./specification');

var Header = require('./header');
var Packet = require('./packet');
var Datagram = require('./datagram');
var Telegram = require('./telegram');

var HeaderSet = require('./header-set');
var HeaderSetConsolidator = require('./header-set-consolidator');

var Connection = require('./connection');
var SerialConnection = require('./serial-connection');
var TcpConnection = require('./tcp-connection');

var DataSource = require('./data-source');
var SerialDataSource = require('./serial-data-source');
var TcpDataSource = require('./tcp-data-source');

var DataSourceProvider = require('./data-source-provider');
var SerialDataSourceProvider = require('./serial-data-source-provider');
var TcpDataSourceProvider = require('./tcp-data-source-provider');

var Converter = require('./converter');
var VBusRecordingConverter = require('./vbus-recording-converter');
var TextConverter = require('./text-converter');
var DLxJsonConverter = require('./dlx-json-converter');

var Recorder = require('./recorder');
var DLxRecorder = require('./dlx-recorder');
var FileSystemRecorder = require('./filesystem-recorder');

var ConfigurationOptimizer = require('./configuration-optimizer');

var Customizer = require('./customizer');
var ConnectionCustomizer = require('./connection-customizer');

var TcpConnectionEndpoint = require('./tcp-connection-endpoint');

module.exports = {

    VERSION: packageInfo.version,

    utils: utils,
    extend: extend,

    I18N: I18N,

    Specification: Specification,

    Header: Header,
    Packet: Packet,
    Datagram: Datagram,
    Telegram: Telegram,

    HeaderSet: HeaderSet,
    HeaderSetConsolidator: HeaderSetConsolidator,

    Connection: Connection,
    SerialConnection: SerialConnection,
    TcpConnection: TcpConnection,

    DataSource: DataSource,
    SerialDataSource: SerialDataSource,
    TcpDataSource: TcpDataSource,

    DataSourceProvider: DataSourceProvider,
    SerialDataSourceProvider: SerialDataSourceProvider,
    TcpDataSourceProvider: TcpDataSourceProvider,

    Converter: Converter,
    VBusRecordingConverter: VBusRecordingConverter,
    TextConverter: TextConverter,
    DLxJsonConverter: DLxJsonConverter,

    Recorder: Recorder,
    DLxRecorder: DLxRecorder,
    FileSystemRecorder: FileSystemRecorder,

    ConfigurationOptimizer: ConfigurationOptimizer,

    Customizer: Customizer,
    ConnectionCustomizer: ConnectionCustomizer,

    TcpConnectionEndpoint: TcpConnectionEndpoint,

};
<|endoftext|>


<commit_before>import Boot from 'states/Boot';
import Preload from 'states/Preload';
import Menu from 'states/Menu';
import Main from 'states/Main';
import Stats from 'states/Stats';
import End from 'states/End';
import How from 'states/How';

class Game extends Phaser.Game {

	constructor() {

		super(window.innerWidth * window.devicePixelRatio, window.innerHeight * window.devicePixelRatio, Phaser.CANVAS, 'gameArea');

		this.state.add('Boot', Boot, false);
		this.state.add('Preload', Preload, false);
		this.state.add('Menu', Menu, false);
		this.state.add('Main', Main, false);
		this.state.add('Stats', Stats, false);
		this.state.add('End', End, false);
		this.state.add('How', How, false);

		this.state.start('Boot');
	}

}

new Game();
<commit_msg>edit screen size 960x640<commit_after>import Boot from 'states/Boot';
import Preload from 'states/Preload';
import Menu from 'states/Menu';
import Main from 'states/Main';
import Stats from 'states/Stats';
import End from 'states/End';
import How from 'states/How';

class Game extends Phaser.Game {

	constructor() {

		super(960, 640, Phaser.CANVAS, 'gameArea');

		this.state.add('Boot', Boot, false);
		this.state.add('Preload', Preload, false);
		this.state.add('Orientation', Orientation, false);
		this.state.add('Menu', Menu, false);
		this.state.add('Main', Main, false);
		this.state.add('Stats', Stats, false);
		this.state.add('End', End, false);
		this.state.add('How', How, false);

		this.state.start('Boot');
	}

}

new Game();
<|endoftext|>


<commit_before>const Keyboard = require('./Keyboard');
const KeyboardButton = require('./KeyboardButton');
const KeyboardedInput = require('./KeyboardedInput');

module.exports.Keyboard = Keyboard.default || Keyboard;
module.exports = KeyboardedInput.default || KeyboardedInput;
module.exports.KeyboardButton = KeyboardButton.default || KeyboardButton;
<commit_msg>Add missing required package<commit_after>require('custom-event-polyfill');

const Keyboard = require('./Keyboard');
const KeyboardButton = require('./KeyboardButton');
const KeyboardedInput = require('./KeyboardedInput');

module.exports.Keyboard = Keyboard.default || Keyboard;
module.exports = KeyboardedInput.default || KeyboardedInput;
module.exports.KeyboardButton = KeyboardButton.default || KeyboardButton;
<|endoftext|>


<commit_before>"use strict"

var happens = require("happens");

var pages = [];
var pageIndex = 0;
var data = undefined;
var currentPageIndex = 0
var currentPage = undefined
var options = {
  itemsPerPage: 1
};

var api = {};

module.exports = function(items, opts)
{
  data = [];
  data = items;
  happens(api);

  pages = [];
  pageIndex = 0;
  currentPageIndex = 0
  currentPage = undefined

  if(opts && opts.itemsPerPage)
    options.itemsPerPage = opts.itemsPerPage

  createPages();

  api.next = next;
  api.prev = prev;
  api.go = go;
  api.getPages = getPages

  return api;
}

function createPages()
{
  var dataLength = data.length;
  var count = 0;
  pages[pageIndex] = []

  for(var i = 0; i < dataLength; i++)
  {
    if(i > 0)
    {
      count++;

      if(count % options.itemsPerPage == 0)
      {
        count = 0;
        pageIndex++
        pages[pageIndex] = []
      }
    }

    pages[pageIndex].push(data[i]);
  }

  api.totalPages = pages.length;
}

function next()
{
  currentPageIndex++
  checkNextPrev()
  api.emit("change", pages[currentPageIndex], currentPageIndex);
  api.emit("next", pages[currentPageIndex], currentPageIndex);
}

function prev()
{
  currentPageIndex--
  checkNextPrev()
  api.emit("change", pages[currentPageIndex], currentPageIndex);
  api.emit("prev", pages[currentPageIndex], currentPageIndex);
}

function checkNextPrev()
{
  if(currentPageIndex > pages.length - 1)
  {
    currentPageIndex = 0;
  }

  if(currentPageIndex < 0)
  {
    currentPageIndex = pages.length - 1;
  }
}

function go(at)
{
  currentPageIndex = at;
  checkNextPrev()
  api.emit("change", pages[currentPageIndex], currentPageIndex);
  api.emit("go", pages[currentPageIndex], currentPageIndex);
}

function getPages()
{
  return pages;
}
<commit_msg>Update index.js<commit_after>"use strict"

var happens = require("happens");

module.exports = function(items, opts)
{
  var api = {};
  api.pages = [];
  api.pageIndex = 0;
  api.data = items;
  api.currentPageIndex = 0
  api.currentPage = undefined
  api.options = {
    itemsPerPage: 1,
    loop: false
  };

  happens(api);

  if(opts)
  {
    if(opts.itemsPerPage)
      api.options.itemsPerPage = opts.itemsPerPage

    if(opts.loop != undefined)
      api.options.loop = opts.loop
  }

  api.hasNext = function()
  {
    return (api.currentPageIndex < api.pages.length - 1)
  }

  api.hasPrev = function()
  {
    return (api.currentPageIndex > 0)
  }

  api.createPages = function()
  {
    var dataLength = api.data.length;
    var count = 0;
    api.pages[api.pageIndex] = [];

    for(var i = 0; i < dataLength; i++)
    {
      if(i > 0)
      {
        count++;

        if(count % api.options.itemsPerPage == 0)
        {
          count = 0;
          api.pageIndex++
          api.pages[api.pageIndex] = []
        }
      }

      api.pages[api.pageIndex].push(api.data[i]);
    }

    api.totalPages = api.pages.length;
  }

  api.next = function()
  {
    api.currentPageIndex++
    api.checkNextPrev()
    api.emit("change", api.pages[api.currentPageIndex], api.currentPageIndex);
    api.emit("next", api.pages[api.currentPageIndex], api.currentPageIndex);
  }

  api.prev = function()
  {
    api.currentPageIndex--
    api.checkNextPrev()
    api.emit("change", api.pages[api.currentPageIndex], api.currentPageIndex);
    api.emit("prev", api.pages[api.currentPageIndex], api.currentPageIndex);
  }

  api.checkNextPrev = function()
  {
    if(api.currentPageIndex > api.pages.length - 1)
    {
      if(api.options.loop)
      {
        api.currentPageIndex = 0;
      }
      else
      {
        api.currentPageIndex = api.pages.length - 1;
        api.emit("last:next");
      }
    }

    if(api.currentPageIndex < 0)
    {
      if(api.options.loop)
      {
        api.currentPageIndex = api.pages.length - 1;
      }
      else
      {
        api.currentPageIndex = 0;
        api.emit("last:prev");
      }
    }
  }

  api.go = function(at)
  {
    api.currentPageIndex = at;
    api.checkNextPrev()
    api.emit("change", api.pages[api.currentPageIndex], api.currentPageIndex);
    api.emit("go", api.pages[api.currentPageIndex], api.currentPageIndex);
  }

  api.createPages();

  return api;
}
<|endoftext|>


<commit_before>// import { run } from '@cycle/most-run'
import { run } from '@cycle/xstream-run'
import { makeDOMDriver, div } from '@cycle/dom'
// import { of, combine, merge, periodic, combineArray } from 'most'
import xs from 'xstream'
const {merge} = xs

import {getTranslations} from './utils/getTranslations'
import {fromMost, mergeReducers} from './utils/cycle'
import onionify from 'cycle-onionify'

import windowApiDriver from './sideEffects/windowApiDriver'
import addressBarDriver from './sideEffects/addressBarDriver'
import appMetadataDriver from './sideEffects/appMetadataDriver'
import eventsDriver from './sideEffects/eventsDriver'

import App from './ui/app'

function main (sources) {
  // initial state
  const init = () => ({
    print: {
      settings: {
        support: {toggled: true},
        brim: {toggled: true},
        qualityPreset: undefined,
        supportExtruder: -1
      },
      status: 'n/a',
      running: false,
      paused: false
    },

    printing: {
      settings: {
        printersPollRate: 10000, // how often to update the camera
        cameraPollRate: 10000 // how often to update the camera
      },
      printers: [
        /* {id: '080987D', name: 'my printer', infos: {type: 'ultimaker3'}} */
      ],
      // for printers list
      printersStatus: undefined,
      // for individual printers
      printerStatus: {message: 'fetching printer data , please wait...', state: undefined, busy: false},
      activePrinterId: undefined,
      frame: undefined

    },

    buildplate: {
      settings: {
        snapTranslation: false,
        snapRotation: false,
        snapScaling: false,
        uniformScaling: false
      },
      entities: [],
      activeTool: -1,
      selections: {
        instIds: []
      }
    },

    // ui related
    steps: [{name: 'Printer Setup'}, {name: 'Material Setup'}, {name: 'Monitor Print'}, {name: 'Print Settings'}],
    currentStep: 0,

    t: x => ''// stand in
  })

  const translations$ = fromMost(getTranslations({en: {translation: require('../assets/i18n/en/strings.json')}}))

  // sub components
  const app = App(sources)

  const setTranslationsReducer$ = translations$.map(t => state => ({...state, t}))
  const state$ = sources.onion.state$

  const reducer$ = merge(
    setTranslationsReducer$,
    mergeReducers(init, [app])
  )
  // const vdom$ = app.DOM.map(app => app)
  return {
    DOM: app.DOM,
    events: app.events,
    // log: app.log,
    onion: reducer$
  }
}

const wrappedMain = onionify(main)

const drivers = {
  DOM: makeDOMDriver('#app'),
  window: windowApiDriver,
  addressBar: addressBarDriver,
  metaData: appMetadataDriver,
  events: eventsDriver,
  log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) }
}

run(wrappedMain, drivers)
<commit_msg>feat(state): updated defaults<commit_after>// import { run } from '@cycle/most-run'
import { run } from '@cycle/xstream-run'
import { makeDOMDriver, div } from '@cycle/dom'
// import { of, combine, merge, periodic, combineArray } from 'most'
import xs from 'xstream'
const {merge} = xs

import {getTranslations} from './utils/getTranslations'
import {fromMost, mergeReducers} from './utils/cycle'
import onionify from 'cycle-onionify'

import windowApiDriver from './sideEffects/windowApiDriver'
import addressBarDriver from './sideEffects/addressBarDriver'
import appMetadataDriver from './sideEffects/appMetadataDriver'
import eventsDriver from './sideEffects/eventsDriver'

import App from './ui/app'

function main (sources) {
  // initial state
  const init = () => ({
    print: {
      settings: {
        support: {toggled: true},
        brim: {toggled: true},
        qualityPreset: undefined,
        supportExtruder: -1
      },
      status: 'n/a',
      running: false,
      paused: false
    },

    printing: {
      settings: {
        printersPollRate: 20000, // how often to update the camera
        cameraPollRate: 10000 // how often to update the camera
      },
      printers: [
        /* {id: '080987D', name: 'my printer', infos: {type: 'ultimaker3'}} */
      ],
      // for printers list
      printersStatus: undefined,
      // for individual printers
      printerStatus: {message: 'fetching printer data , please wait...', state: undefined, busy: false},
      activePrinterId: undefined,
      frame: undefined

    },

    buildplate: {
      settings: {
        snapTranslation: false,
        snapRotation: false,
        snapScaling: false,
        uniformScaling: false
      },
      entities: [],
      activeTool: -1,
      selections: {
        instIds: []
      }
    },

    // ui related
    steps: [{name: 'Printer Setup'}, {name: 'Material Setup'}, {name: 'Monitor Print'}, {name: 'Print Settings'}],
    currentStep: 0,
    printerListToggled: true,

    t: x => ''// stand in
  })

  const translations$ = fromMost(getTranslations({en: {translation: require('../assets/i18n/en/strings.json')}}))

  // sub components
  const app = App(sources)

  const setTranslationsReducer$ = translations$.map(t => state => ({...state, t}))
  const state$ = sources.onion.state$

  const reducer$ = merge(
    setTranslationsReducer$,
    mergeReducers(init, [app])
  )
  // const vdom$ = app.DOM.map(app => app)
  return {
    DOM: app.DOM,
    events: app.events,
    // log: app.log,
    onion: reducer$
  }
}

const wrappedMain = onionify(main)

const drivers = {
  DOM: makeDOMDriver('#app'),
  window: windowApiDriver,
  addressBar: addressBarDriver,
  metaData: appMetadataDriver,
  events: eventsDriver,
  log: msg$ => { msg$.addListener({next: msg => console.log(msg)}) }
}

run(wrappedMain, drivers)
<|endoftext|>


<commit_before>"use strict";

var assign = require("object-assign");
var r = require("react-wrapper");
var scrollerMap = r.wrap(require("./scroller-map"));
var scrollerTile = r.wrap(require("./scroller-tile"));

module.exports = {
  propTypes: function () {
    return {
      columnCount: r.propTypes.number,
      columnWidth: r.propTypes.number,
      rowCount: r.propTypes.number,
      rowHeight: r.propTypes.number,
      tileValidity: r.propTypes.number,
      tileChild: r.propTypes.func.isRequired,
      tileChildProps: r.propTypes.object
    };
  },

  getDefaultProps: function () {
    return {
      columnCount: 1,
      columnWidth: 1000,
      rowCount: 1,
      rowHeight: 1000,
      tileValidity: 5,
      tileChildProps: {}
    };
  },

  getInitialState: function () {
    return {};
  },

  componentDidMount: function () {
    var node = r.domNode(this).firstChild;
    node.addEventListener("scroll", this.onRefresh);
    addEventListener("resize", this.onRefresh);
    this.updateTileExpiry();
  },

  componentWillUnmount: function () {
    var node = r.domNode(this).firstChild;
    node.removeEventListener("scroll", this.onRefresh);
    removeEventListener("resize", this.onRefresh);
  },

  onRefresh: function (event) {
    window.requestAnimationFrame(function () {
        if (this.isMounted()) {
          this.updateTileExpiry();
        }
      }.bind(this));
  },

  updateTileExpiry: function () {
    var tileExpiry = Date.now() + this.props.tileValidity * 1000;
    var state = this.computeTileVisibility();
    for (var x = state.firstVisibleColumn; x <= state.lastVisibleColumn; x += 1) {
      for (var y = state.firstVisibleRow; y <= state.lastVisibleRow; y += 1) {
        state["tileExpiry-" + x + "-" + y] = tileExpiry;
      }
    }
    this.setState(state);
  },

  computeTileVisibility: function () {
    var node = r.domNode(this).firstChild;
    var firstVisibleColumn = this.computeColumn(node.scrollLeft);
    var lastVisibleColumn = this.computeColumn(node.scrollLeft + node.clientWidth);
    var firstVisibleRow = this.computeRow(node.scrollTop);
    var lastVisibleRow = this.computeRow(node.scrollTop + node.clientHeight);
    return {
      firstVisibleColumn: firstVisibleColumn,
      lastVisibleColumn: lastVisibleColumn,
      firstVisibleRow: firstVisibleRow,
      lastVisibleRow: lastVisibleRow
    };
  },

  computeColumn: function (x) {
    return (
      Math.min(
        Math.floor(x / this.props.columnWidth),
        this.props.columnCount - 1));
  },

  computeRow: function (y) {
    return (
      Math.min(
        Math.floor(y / this.props.rowHeight),
        this.props.rowCount - 1));
  },

  render: function () {
    var now = Date.now();
    var tiles = [];
    var mapTiles = [];
    for (var x = 0; x < this.props.columnCount; x += 1) {
      for (var y = 0; y < this.props.rowCount; y += 1) {
        var tileExpiry = this.state["tileExpiry-" + x + "-" + y];
        if (tileExpiry > now) {
          var isTileVisible = (
            x >= this.state.firstVisibleColumn &&
            x <= this.state.lastVisibleColumn &&
            y >= this.state.firstVisibleRow &&
            y <= this.state.lastVisibleRow);
          tiles.push(
            scrollerTile(assign({}, this.props, {
                key: "t-" + x + "-" + y,
                x: x,
                y: y
              })));
          mapTiles.push({
              x: x,
              y: y,
              isVisible: isTileVisible
            });
        }
      }
    }
    var frameWidth = (
      this.props.columnCount === 1 ? "100%" :
        this.props.columnCount * this.props.columnWidth);
    var frameHeight = (
      this.props.rowCount === 1 ? "100%" :
        this.props.rowCount * this.props.rowHeight);
    return (
      r.div({
          className: "scroller",
          style: {
            width: "100%",
            height: "100%",
            position: "relative"
          }
        },
        r.div({
            className: "scroller-window",
            style: {
              width: "100%",
              height: "100%",
              overflow: "scroll",
              WebkitOverflowScrolling: "touch"
            }
          },
          r.div({
              className: "scroller-frame",
              style: {
                width: frameWidth,
                height: frameHeight,
                position: "relative",
                zIndex: -1
              }
            },
            tiles)),
        scrollerMap({
            columnCount: this.props.columnCount,
            rowCount: this.props.rowCount
          },
          mapTiles)));
  }
};

r.makeComponent("Scroller", module);
<commit_msg>Tweak function naming<commit_after>"use strict";

var assign = require("object-assign");
var r = require("react-wrapper");
var scrollerMap = r.wrap(require("./scroller-map"));
var scrollerTile = r.wrap(require("./scroller-tile"));

module.exports = {
  propTypes: function () {
    return {
      columnCount: r.propTypes.number,
      columnWidth: r.propTypes.number,
      rowCount: r.propTypes.number,
      rowHeight: r.propTypes.number,
      tileValidity: r.propTypes.number,
      tileChild: r.propTypes.func.isRequired,
      tileChildProps: r.propTypes.object
    };
  },

  getDefaultProps: function () {
    return {
      columnCount: 1,
      columnWidth: 1000,
      rowCount: 1,
      rowHeight: 1000,
      tileValidity: 5,
      tileChildProps: {}
    };
  },

  getInitialState: function () {
    return {};
  },

  componentDidMount: function () {
    var node = r.domNode(this).firstChild;
    node.addEventListener("scroll", this.onRefresh);
    addEventListener("resize", this.onRefresh);
    this.updateState();
  },

  componentWillUnmount: function () {
    var node = r.domNode(this).firstChild;
    node.removeEventListener("scroll", this.onRefresh);
    removeEventListener("resize", this.onRefresh);
  },

  onRefresh: function (event) {
    window.requestAnimationFrame(function () {
        if (this.isMounted()) {
          this.updateState();
        }
      }.bind(this));
  },

  updateState: function () {
    var tileExpiry = Date.now() + this.props.tileValidity * 1000;
    var state = this.computeTileVisibility();
    for (var x = state.firstVisibleColumn; x <= state.lastVisibleColumn; x += 1) {
      for (var y = state.firstVisibleRow; y <= state.lastVisibleRow; y += 1) {
        state["tileExpiry-" + x + "-" + y] = tileExpiry;
      }
    }
    this.setState(state);
  },

  computeTileVisibility: function () {
    var node = r.domNode(this).firstChild;
    var firstVisibleColumn = this.computeColumn(node.scrollLeft);
    var lastVisibleColumn = this.computeColumn(node.scrollLeft + node.clientWidth);
    var firstVisibleRow = this.computeRow(node.scrollTop);
    var lastVisibleRow = this.computeRow(node.scrollTop + node.clientHeight);
    return {
      firstVisibleColumn: firstVisibleColumn,
      lastVisibleColumn: lastVisibleColumn,
      firstVisibleRow: firstVisibleRow,
      lastVisibleRow: lastVisibleRow
    };
  },

  computeColumn: function (x) {
    return (
      Math.min(
        Math.floor(x / this.props.columnWidth),
        this.props.columnCount - 1));
  },

  computeRow: function (y) {
    return (
      Math.min(
        Math.floor(y / this.props.rowHeight),
        this.props.rowCount - 1));
  },

  render: function () {
    var now = Date.now();
    var tiles = [];
    var mapTiles = [];
    for (var x = 0; x < this.props.columnCount; x += 1) {
      for (var y = 0; y < this.props.rowCount; y += 1) {
        var tileExpiry = this.state["tileExpiry-" + x + "-" + y];
        if (tileExpiry > now) {
          var isTileVisible = (
            x >= this.state.firstVisibleColumn &&
            x <= this.state.lastVisibleColumn &&
            y >= this.state.firstVisibleRow &&
            y <= this.state.lastVisibleRow);
          tiles.push(
            scrollerTile(assign({}, this.props, {
                key: "t-" + x + "-" + y,
                x: x,
                y: y
              })));
          mapTiles.push({
              x: x,
              y: y,
              isVisible: isTileVisible
            });
        }
      }
    }
    var frameWidth = (
      this.props.columnCount === 1 ? "100%" :
        this.props.columnCount * this.props.columnWidth);
    var frameHeight = (
      this.props.rowCount === 1 ? "100%" :
        this.props.rowCount * this.props.rowHeight);
    return (
      r.div({
          className: "scroller",
          style: {
            width: "100%",
            height: "100%",
            position: "relative"
          }
        },
        r.div({
            className: "scroller-window",
            style: {
              width: "100%",
              height: "100%",
              overflow: "scroll",
              WebkitOverflowScrolling: "touch"
            }
          },
          r.div({
              className: "scroller-frame",
              style: {
                width: frameWidth,
                height: frameHeight,
                position: "relative",
                zIndex: -1
              }
            },
            tiles)),
        scrollerMap({
            columnCount: this.props.columnCount,
            rowCount: this.props.rowCount
          },
          mapTiles)));
  }
};

r.makeComponent("Scroller", module);
<|endoftext|>