<commit_before>import LearningView from './LearningView';
import Chart from './Chart';
const { callApi } = require('../../util/common');
import _uniq from 'lodash/uniq';
import _floor from 'lodash/floor';
import _sum from 'lodash/sum';
import _mean from 'lodash/mean';

export const classes = [
    'ai_learning_train',
    'ai_learning_number',
    'number_learning_attr_1',
    'number_learning_attr_2',
    'number_learning_attr_3'
];

class NumberClassification {
    #attrLength = 0;
    #trainParam = null;
    #table = {};
    #trainCallback;
    #chart = null;
    #isTrained = false;
    #chartEnable = false;
    #view = null;
    #predictResult = null;
    #name = ''
    constructor({ name, url, table, trainParam }) {
        this.#view = new LearningView({ name, status: 0 });
        this.#name = name;
        this.#trainParam = trainParam;
        this.#table = table;
        this.#trainCallback = (value) => {
            this.#view.setValue(value)
        };;
        this.#isTrained = true;

        this.#attrLength = table?.select?.[0]?.length || 0;
        if (this.#attrLength === 2) {
            this.#chartEnable = true;
        }
        this.load(`/uploads/${url}/model.json`);
    }

    destroy() {
        this.#view.destroy();
        if(this.#chart) {
            this.#chart.destroy();
            this.#chart = null;
        }
    }

    createColor() {
        return this.#trainParam.labels.reduce((acc, cur, idx, arr) => {
            return {
                ...acc,
                [cur]: Entry.Utils.randomColor(),
            }
        }, {});
    }

    setVisible(visible) {
        this.#view.setVisible(visible);
    }

    unbanBlocks(blockMenu) {
        blockMenu.unbanClass(`ai_learning_train`);
        blockMenu.unbanClass(`ai_learning_number`);
        blockMenu.unbanClass(`number_learning_attr_${this.#attrLength}`);
        if (this.#chartEnable) {
            blockMenu.unbanClass('ai_learning_train_chart');
        }
    }

    isTrained() {
        return this.#isTrained;
    }

    openChart() {
        if (!this.#chartEnable) {
            return ;
        }
        if (!this.#chart) {
            this.#chart = new Chart({
                title: this.#name,
                source: this.chartData,
            });
        } else {
            this.#chart.show();
        }
    }

    closeChart() {
        if (this.#chart) {
            this.#chart.hide();
        }
    }

    setTrainOption(type, value) {
        this.#trainParam[type] = value;
    }

    getTrainOption() {
        return this.#trainParam;
    }

    getResult() {
        return this.#predictResult;
    }

    train() {
        this.#trainCallback(0);
        this.#isTrained = false;
        this.#chart = null;
        const { data: trainData, labels } = convertTableToKnnData(this.#table);
        const trainLabels = labels[0];
        const uniqLabels = _uniq(labels[0]).sort((a, b) => String(a).localeCompare(String(b)));
        const numLabels = uniqLabels.length;
        const maxVector = new Array(trainData[0].length).fill(-Infinity);
        trainData.forEach(e => {
            for (let i = 0; i < maxVector.length; i++) {
                maxVector[i] = maxVector[i] > e[i] ? maxVector[i] : e[i];
            }
        });

        const minVector = new Array(trainData[0].length).fill(Infinity);
        trainData.forEach(e => {
            for (let i = 0; i < minVector.length; i++) {
                minVector[i] = minVector[i] < e[i] ? minVector[i] : e[i];
            }
        });
        this.#trainParam = {
            ...this.#trainParam,
            trainData,
            trainLabels,
            labels: uniqLabels,
            numLabels,
            maxVector,
            minVector,
        };
        this.#trainCallback(100);
        this.#isTrained = true;
        this.colors = this.createColor();
    }

    async load(url) {
        const { data: savedData } = await callApi(url, { url });
        this.#trainParam.trainData = savedData.data;
        this.#trainParam.trainLabels = savedData.labels;
        this.#trainParam.labels = _uniq(savedData.labels).sort((a, b) => String(a).localeCompare(String(b)));
        this.#trainParam.maxVector = savedData.maxVector;
        this.#trainParam.minVector = savedData.minVector;
        this.#trainParam.numLabels = savedData.numLabels;
        this.#trainParam.neighbors = savedData.neighbors;
        this.#trainParam.isLoaded = true;
        this.colors = this.createColor();
    }

    normalize(data = []) {
        const normData = [];
        const { minVector, maxVector } = this.#trainParam;
        for (let i = 0; i < data.length; i++) {
            normData.push((data[i] - minVector[i]) / (maxVector[i] - minVector[i]));
        }
        return normData;
    }

    predict(data) {
        let distData = [];
        const { trainData, trainLabels, neighbors } = this.#trainParam;

        for (let i = 0; i < trainData.length; i++) {
            let dist = eudist(this.normalize(data), this.normalize(trainData[i]));
            distData.push({
                "index": i,
                "dist": dist,
                "label": trainLabels[i]
            });
        }

        distData.sort((a, b) => a.dist - b.dist);

        let counts = {};
        for (let i = 0; i < neighbors; i++) {
            const { label, dist } = distData[i];
            if (!counts[label]) {
                counts[label] = []
            }
            counts[label].push(dist);
        }

        const totalDistance = _sum(Object.values(counts).flat());
        this.#predictResult = Object.keys(counts).map(className => {
            const dist = _mean(counts[className]);
            const count = counts[className].length;
            const distProbability = totalDistance === 0 ? 1 : (totalDistance - dist) / totalDistance;
            const probability = (count / neighbors) * 99 + distProbability;
            return {
                className,
                count,
                probability
            }
        }).sort((a, b) => b.probability - a.probability);
        return this.#predictResult;
    }

    findLabel(x, y) {
        const index = this.#trainParam.trainData.findIndex((row) => row[0] === x && row[1] === y);
        return this.#trainParam.trainLabels[index];
    }

    findColor(id, a, b) {
        if (id === 'y') {
            return this.colors[this.findLabel(a, b)];
        }
        return undefined;
    }

    get chartData() {
        const json = this.#trainParam.trainData.map((row, idx) => ({
            x: row[0],
            y: row[1],
            index: this.#trainParam.trainLabels[idx],
        }));
        return {
            data: {
                type: 'scatter',
                json,
                keys: { value: ['y'], x: 'x', },
                color: (color, d) => this.findColor(d.id, d.x, d.value) || color,
                labels: false,
            },
            options: {
                point: {
                    pattern: [
                        "circle"
                    ]
                },
                legend: {
                    show: false
                },
                tooltip: {
                    contents: (data, b, c, d, e) =>{
                        console.log(data, b, c,d,e);
                        const [{ x, value }] = data;
                        const label = this.findLabel(x, value);

                        return `${label}, ${x}, ${value}`;
                    }
                },
                axis: {
                    x: {
                        tick: {
                            fit: false,
                            count: 10
                        },
                    }
                },
                grid: {
                    x: {
                        show: true
                    },
                    y: {
                        show: true
                    }
                }
            }
        }
    }
};

export default NumberClassification;

function eudist(a, b) {
    let sum = 0;
    for (let i = 0; i < a.length; i++) {
        let d = (a[i] || 0) - (b[i] || 0);
        sum += d * d;
    }

    return sum;
}

function convertTableToKnnData(tableData) {
    const { select = [[0], [1]], data: table } = tableData;
    const [attr, predict] = select;
    return table.reduce((accumulator, row) => {
        const { data = [], labels = [] } = accumulator;
        return {
            data: [
                ...data,
                row.filter((data, index) => attr.includes(index))
            ],
            labels: predict.map((i, idx) => {
                const arr = labels[idx] || [];
                return [...arr, row[i]];
            }),
        };
    }, { data: [], labels: [] });
}<commit_msg>그래프에 field 명 추가.<commit_after>import LearningView from './LearningView';
import Chart from './Chart';
const { callApi } = require('../../util/common');
import _uniq from 'lodash/uniq';
import _floor from 'lodash/floor';
import _sum from 'lodash/sum';
import _mean from 'lodash/mean';

export const classes = [
    'ai_learning_train',
    'ai_learning_number',
    'number_learning_attr_1',
    'number_learning_attr_2',
    'number_learning_attr_3'
];

class NumberClassification {
    #attrLength = 0;
    #trainParam = null;
    #table = {};
    #trainCallback;
    #chart = null;
    #isTrained = false;
    #chartEnable = false;
    #view = null;
    #predictResult = null;
    #name = '';
    #fields = [];

    constructor({ name, url, table, trainParam }) {
        this.#view = new LearningView({ name, status: 0 });
        this.#name = name;
        this.#trainParam = trainParam;
        this.#table = table;
        this.#trainCallback = (value) => {
            this.#view.setValue(value)
        };;
        this.#isTrained = true;

        this.#attrLength = table?.select?.[0]?.length || 0;
        this.#fields = table?.select?.[0]?.map((index) => {
            return table?.fields[index];
        })
        if (this.#attrLength === 2) {
            this.#chartEnable = true;
        }
        this.load(`/uploads/${url}/model.json`);
    }

    destroy() {
        this.#view.destroy();
        if(this.#chart) {
            this.#chart.destroy();
            this.#chart = null;
        }
    }

    createColor() {
        return this.#trainParam.labels.reduce((acc, cur, idx, arr) => {
            return {
                ...acc,
                [cur]: Entry.Utils.randomColor(),
            }
        }, {});
    }

    setVisible(visible) {
        this.#view.setVisible(visible);
    }

    unbanBlocks(blockMenu) {
        blockMenu.unbanClass(`ai_learning_train`);
        blockMenu.unbanClass(`ai_learning_number`);
        blockMenu.unbanClass(`number_learning_attr_${this.#attrLength}`);
        if (this.#chartEnable) {
            blockMenu.unbanClass('ai_learning_train_chart');
        }
    }

    isTrained() {
        return this.#isTrained;
    }

    openChart() {
        if (!this.#chartEnable) {
            return ;
        }
        if (!this.#chart) {
            const { numLabels } = this.#trainParam;
            this.#chart = new Chart({
                title: this.#name,
                description: `<em>${Lang.AiLearning.class}</em>: ${numLabels}, <em>${Lang.AiLearning.model_attr_str} 1</em>: ${this.#fields[0]}, <em>${Lang.AiLearning.model_attr_str} 2</em>: ${this.#fields[1]}`,
                source: this.chartData,
            });
        } else {
            this.#chart.show();
        }
    }

    closeChart() {
        if (this.#chart) {
            this.#chart.hide();
        }
    }

    setTrainOption(type, value) {
        this.#trainParam[type] = value;
    }

    getTrainOption() {
        return this.#trainParam;
    }

    getResult() {
        return this.#predictResult;
    }

    train() {
        this.#trainCallback(0);
        this.#isTrained = false;
        this.#chart = null;
        const { data: trainData, labels } = convertTableToKnnData(this.#table);
        const trainLabels = labels[0];
        const uniqLabels = _uniq(labels[0]).sort((a, b) => String(a).localeCompare(String(b)));
        const numLabels = uniqLabels.length;
        const maxVector = new Array(trainData[0].length).fill(-Infinity);
        trainData.forEach(e => {
            for (let i = 0; i < maxVector.length; i++) {
                maxVector[i] = maxVector[i] > e[i] ? maxVector[i] : e[i];
            }
        });

        const minVector = new Array(trainData[0].length).fill(Infinity);
        trainData.forEach(e => {
            for (let i = 0; i < minVector.length; i++) {
                minVector[i] = minVector[i] < e[i] ? minVector[i] : e[i];
            }
        });
        this.#trainParam = {
            ...this.#trainParam,
            trainData,
            trainLabels,
            labels: uniqLabels,
            numLabels,
            maxVector,
            minVector,
        };
        this.#trainCallback(100);
        this.#isTrained = true;
        this.colors = this.createColor();
    }

    async load(url) {
        const { data: savedData } = await callApi(url, { url });
        this.#trainParam.trainData = savedData.data;
        this.#trainParam.trainLabels = savedData.labels;
        this.#trainParam.labels = _uniq(savedData.labels).sort((a, b) => String(a).localeCompare(String(b)));
        this.#trainParam.maxVector = savedData.maxVector;
        this.#trainParam.minVector = savedData.minVector;
        this.#trainParam.numLabels = savedData.numLabels;
        this.#trainParam.neighbors = savedData.neighbors;
        this.#trainParam.isLoaded = true;
        this.colors = this.createColor();
    }

    normalize(data = []) {
        const normData = [];
        const { minVector, maxVector } = this.#trainParam;
        for (let i = 0; i < data.length; i++) {
            normData.push((data[i] - minVector[i]) / (maxVector[i] - minVector[i]));
        }
        return normData;
    }

    predict(data) {
        let distData = [];
        const { trainData, trainLabels, neighbors } = this.#trainParam;

        for (let i = 0; i < trainData.length; i++) {
            let dist = eudist(this.normalize(data), this.normalize(trainData[i]));
            distData.push({
                "index": i,
                "dist": dist,
                "label": trainLabels[i]
            });
        }

        distData.sort((a, b) => a.dist - b.dist);

        let counts = {};
        for (let i = 0; i < neighbors; i++) {
            const { label, dist } = distData[i];
            if (!counts[label]) {
                counts[label] = []
            }
            counts[label].push(dist);
        }

        const totalDistance = _sum(Object.values(counts).flat());
        this.#predictResult = Object.keys(counts).map(className => {
            const dist = _mean(counts[className]);
            const count = counts[className].length;
            const distProbability = totalDistance === 0 ? 1 : (totalDistance - dist) / totalDistance;
            const probability = (count / neighbors) * 99 + distProbability;
            return {
                className,
                count,
                probability
            }
        }).sort((a, b) => b.probability - a.probability);
        return this.#predictResult;
    }

    findLabel(x, y) {
        const index = this.#trainParam.trainData.findIndex((row) => row[0] === x && row[1] === y);
        return this.#trainParam.trainLabels[index];
    }

    findColor(id, a, b) {
        if (id === 'y') {
            return this.colors[this.findLabel(a, b)];
        }
        return undefined;
    }

    get chartData() {
        const json = this.#trainParam.trainData.map((row, idx) => ({
            x: row[0],
            y: row[1],
            index: this.#trainParam.trainLabels[idx],
        }));
        return {
            data: {
                type: 'scatter',
                json,
                keys: { value: ['y'], x: 'x', },
                color: (color, d) => this.findColor(d.id, d.x, d.value) || color,
                labels: false,
            },
            options: {
                point: {
                    pattern: [
                        "circle"
                    ]
                },
                legend: {
                    show: false
                },
                tooltip: {
                    contents: (data) =>{
                        const [{ x, value }] = data;
                        const label = this.findLabel(x, value);
                        return `
                        <div class="chart_handle_wrapper">
                            ${Lang.AiLearning.class}: ${label}, ${this.#fields[0]}: ${x}, ${this.#fields[1]}: ${value}
                        <div>`;
                    }
                },
                axis: {
                    x: {
                        tick: {
                            fit: false,
                            count: 10
                        },
                    }
                },
                grid: {
                    x: {
                        show: true
                    },
                    y: {
                        show: true
                    }
                }
            }
        }
    }
};

export default NumberClassification;

function eudist(a, b) {
    let sum = 0;
    for (let i = 0; i < a.length; i++) {
        let d = (a[i] || 0) - (b[i] || 0);
        sum += d * d;
    }

    return sum;
}

function convertTableToKnnData(tableData) {
    const { select = [[0], [1]], data: table } = tableData;
    const [attr, predict] = select;
    return table.reduce((accumulator, row) => {
        const { data = [], labels = [] } = accumulator;
        return {
            data: [
                ...data,
                row.filter((data, index) => attr.includes(index))
            ],
            labels: predict.map((i, idx) => {
                const arr = labels[idx] || [];
                return [...arr, row[i]];
            }),
        };
    }, { data: [], labels: [] });
}<|endoftext|>


<commit_before>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2017, MapCreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import ResourceCache from './ResourceCache';

/**
 * Used for wrapping {@link PaginatedResourceListing} to make it spa friendly
 */
export default class PaginatedResourceWrapper {
  /**
   *
   * @param {PaginatedResourceListing} listing - Listing result
   * @param {Maps4News} api - Instance of the api
   * @param {Boolean} cacheEnabled - If the pagination cache should be used
   * @param {Number} cacheTime - Amount of seconds to store a value in cache
   * @param {Boolean} shareCache - Share cache across instances
   */
  constructor(listing, api = listing.api, cacheEnabled = api.defaults.cacheEnabled, cacheTime = api.defaults.cacheSeconds, shareCache = api.defaults._shareCache) {

    // Fields
    this._api = api;
    this.cacheEnabled = cacheEnabled;
    this.cacheTime = cacheTime;
    this._shareCache = shareCache;
    this._currentPage = 1;

    /**
     * Available data assembled from the cache
     * @type {Array<ResourceBase>} - Available data
     */
    this.data = [];

    // Internal
    this._localCache = new ResourceCache(api, cacheTime);
    this._inflight = [];
    this._last = listing;
    this._waiting = false;

    this._promiseCallback(listing);
  }

  get _promiseCallback() {
    return result => {
      const query = this.query;

      this._last = result;
      this._query = query;

      this.cache.push(result);

      const inflightId = this.inflight.findIndex(x => x === result.page);

      if (inflightId >= 0) {
        this._inflight.splice(inflightId, 1);
      }

      this._waiting = this.inflight.length > 0;

      this.rebuild();
    };
  }

  /**
   * Manually fetch a page. This will change the current page.
   * @param {Number|Array<Number>} pageId - Page(s) to fetch
   * @returns {void}
   */
  get(pageId) {
    if (pageId instanceof Array) {
      pageId.map(this.get);
    } else {
      this._waiting = true;

      this._inflight.push(pageId);
      this._last.getPage(pageId).then(this._promiseCallback);
    }
  }

  /**
   * Grab the next page
   * @returns {void}
   */
  next() {
    this.get(++this.currentPage);
  }

  /**
   * Grab the previous page
   * @returns {void}
   */
  previous() {
    this.get(--this.currentPage);
  }

  /**
   * Manually rebuild the data
   * @returns {void}
   */
  rebuild() {
    this.data = this.cache
      .resolve(this.route, this._last.cacheToken)
      .filter(value => typeof value !== 'undefined');

    this.cache.emitter.emit('post-rebuild', {resourceUrl: this._last.url});
  }

  /**
   * Updates the cached pages.
   * @param {Boolean} flush - Clear the cached route data
   * @returns {void}
   */
  refresh(flush = false) {
    if (flush) {
      this.cache.clear(this.route);
    }

    this.cache
      .collectPages(this.route, this._last.cacheToken)
      .map(page => this.get(page.page));
  }

  /**
   * Returns the page number that is currently being used as a reference point
   * @returns {Number} - The current page
   * @see {@link PaginatedResourceWrapper#next}
   * @see {@link PaginatedResourceWrapper#previous}
   */
  get currentPage() {
    return this._currentPage;
  }

  /**
   * Set the current page number
   * @param {Number} value - page number
   */
  set currentPage(value) {
    this._currentPage = Math.max(1, value);
  }

  /**
   * Get the route of the resource
   * @returns {String} - route
   */
  get route() {
    return this._last.route;
  }

  /**
   * Get the number of pages available
   * @returns {Number} - Page count
   */
  get pageCount() {
    return this._last.pageCount;
  }

  /**
   * Optional search query
   * @default {}
   * @return {Object<String, String|Array<String>>} - Query
   */
  get query() {
    return this._last.query;
  }

  /**
   * Optional search query
   * @param {Object<String, String|Array<String>>} value - Query
   * @throws TypeError
   * @default {}
   * @see {@link ResourceProxy#search}
   */
  set query(value) {
    this._last.query = value;
    this.rebuild();
  }

  /**
   * Get api instance
   * @returns {Maps4News} - Api instance
   */
  get api() {
    return this._api;
  }

  /**
   * Get the active cache instance
   * @returns {ResourceCache} - Cache instance
   */
  get cache() {
    return this.shareCache ? this.api.cache : this._localCache;
  }

  /**
   * Get if the shared cache should be used
   * @returns {Boolean} - Should the shared cache be used
   */
  get shareCache() {
    return this._shareCache;
  }

  /**
   * Sets if the shared cache should be used
   * @param {Boolean} value - Should the shared cache be used
   */
  set shareCache(value) {
    this._shareCache = Boolean(value);
  }

  /**
   * If there is a next page
   * @returns {boolean} - If there is a next page
   */
  get hasNext() {
    return this.inflight.length === 0 ? this._last.hasNext : this.currentPage < this.pageCount;
  }

  /**
   * If there is a previous page
   * @returns {boolean} - If there is a previous page
   */
  get hasPrevious() {
    return this._last.hasPrevious;
  }

  /**
   * List of page numbers that are still mid-flight
   * @returns {Array} - Page numbers that are still mid-flight
   */
  get inflight() {
    return this._inflight;
  }

  /**
   * Returns if there are still requests mid-flight
   * @returns {boolean} - Returns if the wrapper is waiting for requests to finish
   */
  get waiting() {
    return this._waiting;
  }

  /**
   * Register an event handler for the given type.
   *
   * @param {string} type Type of event to listen for, or `"*"` for all events.
   * @param {function(event: any): void} handler Function to call in response to the given event.
   * @returns {void}
   */
  on(type, handler) {
    this.cache.emitter.on(type, handler);
  }

  /**
   * Function to call in response to the given event
   *
   * @param {string} type Type of event to unregister `handler` from, or `"*"`
   * @param {function(event: any): void} handler Handler function to remove.
   * @returns {void}
   */
  off(type, handler) {
    this.cache.emitter.off(type, handler);
  }
}
<commit_msg>Fix cache share default lookup<commit_after>/*
 * BSD 3-Clause License
 *
 * Copyright (c) 2017, MapCreator
 * All rights reserved.
 *
 * Redistribution and use in source and binary forms, with or without
 * modification, are permitted provided that the following conditions are met:
 *
 *  Redistributions of source code must retain the above copyright notice, this
 *   list of conditions and the following disclaimer.
 *
 *  Redistributions in binary form must reproduce the above copyright notice,
 *   this list of conditions and the following disclaimer in the documentation
 *   and/or other materials provided with the distribution.
 *
 *  Neither the name of the copyright holder nor the names of its
 *   contributors may be used to endorse or promote products derived from
 *   this software without specific prior written permission.
 *
 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS"
 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE
 * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE
 * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE
 * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL
 * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR
 * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER
 * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY,
 * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 */

import ResourceCache from './ResourceCache';

/**
 * Used for wrapping {@link PaginatedResourceListing} to make it spa friendly
 */
export default class PaginatedResourceWrapper {
  /**
   *
   * @param {PaginatedResourceListing} listing - Listing result
   * @param {Maps4News} api - Instance of the api
   * @param {Boolean} cacheEnabled - If the pagination cache should be used
   * @param {Number} cacheTime - Amount of seconds to store a value in cache
   * @param {Boolean} shareCache - Share cache across instances
   */
  constructor(listing, api = listing.api, cacheEnabled = api.defaults.cacheEnabled, cacheTime = api.defaults.cacheSeconds, shareCache = api.defaults.shareCache) {

    // Fields
    this._api = api;
    this.cacheEnabled = cacheEnabled;
    this.cacheTime = cacheTime;
    this._shareCache = shareCache;
    this._currentPage = 1;

    /**
     * Available data assembled from the cache
     * @type {Array<ResourceBase>} - Available data
     */
    this.data = [];

    // Internal
    this._localCache = new ResourceCache(api, cacheTime);
    this._inflight = [];
    this._last = listing;
    this._waiting = false;

    this._promiseCallback(listing);
  }

  get _promiseCallback() {
    return result => {
      const query = this.query;

      this._last = result;
      this._query = query;

      this.cache.push(result);

      const inflightId = this.inflight.findIndex(x => x === result.page);

      if (inflightId >= 0) {
        this._inflight.splice(inflightId, 1);
      }

      this._waiting = this.inflight.length > 0;

      this.rebuild();
    };
  }

  /**
   * Manually fetch a page. This will change the current page.
   * @param {Number|Array<Number>} pageId - Page(s) to fetch
   * @returns {void}
   */
  get(pageId) {
    if (pageId instanceof Array) {
      pageId.map(this.get);
    } else {
      this._waiting = true;

      this._inflight.push(pageId);
      this._last.getPage(pageId).then(this._promiseCallback);
    }
  }

  /**
   * Grab the next page
   * @returns {void}
   */
  next() {
    this.get(++this.currentPage);
  }

  /**
   * Grab the previous page
   * @returns {void}
   */
  previous() {
    this.get(--this.currentPage);
  }

  /**
   * Manually rebuild the data
   * @returns {void}
   */
  rebuild() {
    this.data = this.cache
      .resolve(this.route, this._last.cacheToken)
      .filter(value => typeof value !== 'undefined');

    this.cache.emitter.emit('post-rebuild', {resourceUrl: this._last.url});
  }

  /**
   * Updates the cached pages.
   * @param {Boolean} flush - Clear the cached route data
   * @returns {void}
   */
  refresh(flush = false) {
    if (flush) {
      this.cache.clear(this.route);
    }

    this.cache
      .collectPages(this.route, this._last.cacheToken)
      .map(page => this.get(page.page));
  }

  /**
   * Returns the page number that is currently being used as a reference point
   * @returns {Number} - The current page
   * @see {@link PaginatedResourceWrapper#next}
   * @see {@link PaginatedResourceWrapper#previous}
   */
  get currentPage() {
    return this._currentPage;
  }

  /**
   * Set the current page number
   * @param {Number} value - page number
   */
  set currentPage(value) {
    this._currentPage = Math.max(1, value);
  }

  /**
   * Get the route of the resource
   * @returns {String} - route
   */
  get route() {
    return this._last.route;
  }

  /**
   * Get the number of pages available
   * @returns {Number} - Page count
   */
  get pageCount() {
    return this._last.pageCount;
  }

  /**
   * Optional search query
   * @default {}
   * @return {Object<String, String|Array<String>>} - Query
   */
  get query() {
    return this._last.query;
  }

  /**
   * Optional search query
   * @param {Object<String, String|Array<String>>} value - Query
   * @throws TypeError
   * @default {}
   * @see {@link ResourceProxy#search}
   */
  set query(value) {
    this._last.query = value;
    this.rebuild();
  }

  /**
   * Get api instance
   * @returns {Maps4News} - Api instance
   */
  get api() {
    return this._api;
  }

  /**
   * Get the active cache instance
   * @returns {ResourceCache} - Cache instance
   */
  get cache() {
    return this.shareCache ? this.api.cache : this._localCache;
  }

  /**
   * Get if the shared cache should be used
   * @returns {Boolean} - Should the shared cache be used
   */
  get shareCache() {
    return this._shareCache;
  }

  /**
   * Sets if the shared cache should be used
   * @param {Boolean} value - Should the shared cache be used
   */
  set shareCache(value) {
    this._shareCache = Boolean(value);
  }

  /**
   * If there is a next page
   * @returns {boolean} - If there is a next page
   */
  get hasNext() {
    return this.inflight.length === 0 ? this._last.hasNext : this.currentPage < this.pageCount;
  }

  /**
   * If there is a previous page
   * @returns {boolean} - If there is a previous page
   */
  get hasPrevious() {
    return this._last.hasPrevious;
  }

  /**
   * List of page numbers that are still mid-flight
   * @returns {Array} - Page numbers that are still mid-flight
   */
  get inflight() {
    return this._inflight;
  }

  /**
   * Returns if there are still requests mid-flight
   * @returns {boolean} - Returns if the wrapper is waiting for requests to finish
   */
  get waiting() {
    return this._waiting;
  }

  /**
   * Register an event handler for the given type.
   *
   * @param {string} type Type of event to listen for, or `"*"` for all events.
   * @param {function(event: any): void} handler Function to call in response to the given event.
   * @returns {void}
   */
  on(type, handler) {
    this.cache.emitter.on(type, handler);
  }

  /**
   * Function to call in response to the given event
   *
   * @param {string} type Type of event to unregister `handler` from, or `"*"`
   * @param {function(event: any): void} handler Handler function to remove.
   * @returns {void}
   */
  off(type, handler) {
    this.cache.emitter.off(type, handler);
  }
}
<|endoftext|>


<commit_before>var fs = require('fs');
var util = require('util');
var libpb = require('libprofitbricks');
var SSH = require('simple-ssh');
var format = util.format;
var mockHandlers = require('./mock-handlers');

var dummyCb = function() {};
// 5 seconds.
var SERVER_QUERY_INTERVAL = 5000;
// 3 minutes total.
var MAX_QUERY_ATTEMPTS = 36;

var PbServer = function(pb, ssh, logger) {
  this.pbHandler = libpb;
  this.sshHandler = SSH;
  this.pb = pb;
  this.ssh = ssh;

  if (logger) {
    this.logger = logger;
  }
  else {
    this.logger = console;
    this.logger.debug = this.logger.log;
  }
  this.mockHandlers = mockHandlers(this.logger);

  this.stateChangeQueryInterval = pb.stateChangeQueryInterval ? pb.stateChangeQueryInterval : SERVER_QUERY_INTERVAL;
  this.maxStateChangeQueryAttempts = pb.maxStateChangeQueryAttempts ? pb.maxStateChangeQueryAttempts : MAX_QUERY_ATTEMPTS;
  this.sshKey = ssh.key ? fs.readFileSync(ssh.key) : null;

  this.pbHandler.setauth(this.pb.username, this.pb.password);
  this.pbHandler.setdepth(this.pb.depth);

  var self = this;
  var _tracked = function(serverLabel, command, machineToState, serverToState, message, cb) {
    cb = cb ? cb : dummyCb;
    var postCommand = function(err, resp, body) {
      if (err) {
        self.logger.error(format('%s command returned error: %s, %s', command, err, body));
        cb(err, body);
      }
      else {
        var stateChangeCallback = function(err, data) {
          if (err) {
            self.logger.error(format('State change returned error: %s, %s', err, data));
            cb(err, data);
          }
          else {
            self.logger.info(message);
            cb(null, data);
          }
        }
        self.serverStateChange(serverLabel, machineToState, serverToState, stateChangeCallback);
      }
    }
    self[command](serverLabel, postCommand);
  }

  this.startServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'startServer', 'AVAILABLE', 'RUNNING', 'Server started!', cb);
  }

  this.shutdownServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'shutdownServer', 'AVAILABLE', 'SHUTOFF', 'Server shut down!', cb);
  }

  this.stopServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'stopServer', 'INACTIVE', 'SHUTOFF', 'Server stopped!', cb);
  }

  var parseBody = function(body) {
    try {
      var data = JSON.parse(body);
      return [null, data];
    }
    catch(err) {
      return [err, body];
    }
  }

  this.parseBody = function(body) {
    return parseBody(body);
  }

  var labelError = function(label, cb) {
    self.logger.error(format("Server label '%s' does not exist, or is misconfigured, check config", label));
    cb("Config error");
  }

  var configFromLabel = function(label, cb) {
    try {
      var serverId = self.pb.serverIds[label];
      var sshHost = self.ssh[label].host;
      var sshPort = self.ssh[label].port || self.ssh.port;
      var sshUser = self.ssh[label].user || self.ssh.user;
      if (serverId && sshHost && sshPort && sshUser) {
        return {
          serverId: serverId,
          sshHost: sshHost,
          sshPort: sshPort,
          sshUser: sshUser,
        }
      }
      else {
        labelError(label, cb);
        return false;
      }
    }
    catch(err) {
      labelError(label, cb);
      return false;
    }
  }

  this.configFromLabel = function(label, cb) {
    return configFromLabel(label, cb);
  }

}

PbServer.prototype.setMockHandlers = function(handlers) {
  this.mockHandlers = handlers;
}

PbServer.prototype.useMockHandlers = function(successStates) {
  // Mocks shouldn't need any more attempts than this.
  this.maxStateChangeQueryAttempts = 2;
  this.mockHandlers.setSuccessStates(successStates);
  this.pbHandler = this.mockHandlers.pbHandler;
  this.sshHandler = this.mockHandlers.sshHandler;
}

PbServer.prototype.useLiveHandlers = function() {
  this.maxStateChangeQueryAttempts = MAX_QUERY_ATTEMPTS;
  this.pbHandler = libpb;
  this.sshHandler = SSH;
}

PbServer.prototype.listDatacenters = function(cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info("Getting datacenter info...");
  this.pbHandler.listDatacenters(apiCallback);
}

PbServer.prototype.listServers = function(cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info("Listing servers...");
  this.pbHandler.listServers(this.pb.datacenterId, apiCallback)
}

PbServer.prototype.getServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info("Getting server status...");
  this.pbHandler.getServer(this.pb.datacenterId, config.serverId, apiCallback)
}

PbServer.prototype.startServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  this.logger.info("Starting server...");
  this.pbHandler.startServer(this.pb.datacenterId, config.serverId, cb)
}

PbServer.prototype.stopServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  this.logger.info("Powering off server...");
  this.pbHandler.stopServer(this.pb.datacenterId, config.serverId, cb)
}

PbServer.prototype.shutdownServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  this.logger.info("Shutting down server...");
  var ssh = new this.sshHandler({
    host: config.sshHost,
    port: config.sshPort,
    user: config.sshUser,
    key: this.sshKey,
  });
  var exit = function(code, stdout, stderr) {
    self.logger.debug(format("SSH command exit code: %s", code));
    if (code === 0) {
      self.logger.debug("Shutdown command execution succeeded...");
      cb(null, code);
    }
    else {
      var message = format('SSH command returned with error code: %d, %s', code, stderr);
      self.logger.error(message);
      cb(message);
    }
  }
  var execConfig = {
    exit: exit,
  };
  var startConfig = {
    success: function() {
      self.logger.debug("SSH connection successful...");
    },
    fail: function(err) {
      self.logger.debug(format("SSH connection failed: %s", err));
      cb(err);
    },
  }
  // Executing shutdown without backgrounding hangs, backgrounding the command
  // allows the shutdown to proceed, and our SSH command to get a return value.
  // NOTE: These commands kept separate to support the mock functionality.
  ssh.exec('shutdown -P now shutdown-now&', execConfig);
  ssh.start(startConfig);
}

PbServer.prototype.serverStateChange = function(serverLabel, machineToState, serverToState, cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var count = 1;
  var checkState = function(err, data) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, data));
      cb(err, data);
      count++;
    }
    else {
      if (count > self.maxStateChangeQueryAttempts) {
        clearInterval(serverStateChange);
        var message = "Max attempts exceeded.";
        self.logger.error(message);
        cb(message);
      }
      else {
        var machineState = data.metadata.state;
        var serverState = data.properties.vmState;
        self.logger.debug(format("Attempt #%d", count));
        self.logger.debug("-------------------------------------");
        self.logger.debug(format("Machine state: %s", machineState));
        self.logger.debug(format("Server state: %s", serverState));
        self.logger.debug("-------------------------------------");
        if (machineState == machineToState && serverState == serverToState) {
          self.logger.info(format("State change to (%s, %s) complete", machineToState , serverToState));
          clearInterval(serverStateChange);
          cb(null, data);
        }
        count++;
      }
    }
  }
  var get = function() {
    self.getServer(serverLabel, checkState);
  }
  this.logger.info(format("Waiting for server state to change to (%s, %s)", machineToState , serverToState));
  get();
  var serverStateChange = setInterval(get, this.stateChangeQueryInterval);
}

PbServer.prototype.checkCommand = function(serverLabel, command, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var count = 1;
  // This prevents overlapping checks and messages.
  var timeout = this.stateChangeQueryInterval - 1000;
  this.logger.debug(format("Checking command: %s", command));
  this.logger.debug(format("SSH connection timeout set to %d milliseconds", timeout));
  var exit = function(code, stdout, stderr) {
    if (code === 0) {
      clearInterval(checkCommand);
      self.logger.info("Command succeeded");
      cb(null, code);
    }
    else {
      self.logger.debug(format('Command returned with error code: %d, %s', code, stderr));
    }
  }
  var execConfig = {
    exit: exit,
  };
  var startConfig = {
    success: function() {
      self.logger.debug("SSH connection successful...");
    },
    fail: function(err) {
      self.logger.debug(format("SSH connection failed: %s", err));
    },
  }
  var check = function() {
    if (count > self.maxStateChangeQueryAttempts) {
      clearInterval(checkCommand);
      var message = "Max attempts exceeded.";
      self.logger.error(message);
      cb(message);
    }
    else {
      self.logger.debug(format("Attempt #%d", count));
      var ssh = new self.sshHandler({
        host: config.sshHost,
        port: config.sshPort,
        user: config.sshUser,
        key: self.sshKey,
        timeout: timeout,
      });
      // NOTE: These commands kept separate to support the mock functionality.
      ssh.exec(command, execConfig);
      ssh.start(startConfig);
      count++;
    }
  }
  check();
  var checkCommand = setInterval(check, this.stateChangeQueryInterval);
}

PbServer.prototype.updateServer = function(serverLabel, profile, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info(format("Updating server to profile: %s", profile));
  var data = this.pb.profiles[profile];
  if (data) {
    var updateData = {
      properties: data,
    }
    this.pbHandler.updateServer(this.pb.datacenterId, config.serverId, updateData, apiCallback);
  }
  else {
    this.logger.error(format("ERROR: profile '%s' does not exist", profile));
  }
}

if (module.exports) {
  module.exports = PbServer;
}
<commit_msg>add server labels to logging.<commit_after>var fs = require('fs');
var util = require('util');
var libpb = require('libprofitbricks');
var SSH = require('simple-ssh');
var format = util.format;
var mockHandlers = require('./mock-handlers');

var dummyCb = function() {};
// 5 seconds.
var SERVER_QUERY_INTERVAL = 5000;
// 3 minutes total.
var MAX_QUERY_ATTEMPTS = 36;

var PbServer = function(pb, ssh, logger) {
  this.pbHandler = libpb;
  this.sshHandler = SSH;
  this.pb = pb;
  this.ssh = ssh;

  if (logger) {
    this.logger = logger;
  }
  else {
    this.logger = console;
    this.logger.debug = this.logger.log;
  }
  this.mockHandlers = mockHandlers(this.logger);

  this.stateChangeQueryInterval = pb.stateChangeQueryInterval ? pb.stateChangeQueryInterval : SERVER_QUERY_INTERVAL;
  this.maxStateChangeQueryAttempts = pb.maxStateChangeQueryAttempts ? pb.maxStateChangeQueryAttempts : MAX_QUERY_ATTEMPTS;
  this.sshKey = ssh.key ? fs.readFileSync(ssh.key) : null;

  this.pbHandler.setauth(this.pb.username, this.pb.password);
  this.pbHandler.setdepth(this.pb.depth);

  var self = this;
  var _tracked = function(serverLabel, command, machineToState, serverToState, message, cb) {
    cb = cb ? cb : dummyCb;
    var postCommand = function(err, resp, body) {
      if (err) {
        self.logger.error(format('%s command returned error: %s, %s', command, err, body));
        cb(err, body);
      }
      else {
        var stateChangeCallback = function(err, data) {
          if (err) {
            self.logger.error(format('State change returned error: %s, %s', err, data));
            cb(err, data);
          }
          else {
            self.logger.info(message);
            cb(null, data);
          }
        }
        self.serverStateChange(serverLabel, machineToState, serverToState, stateChangeCallback);
      }
    }
    self[command](serverLabel, postCommand);
  }

  this.startServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'startServer', 'AVAILABLE', 'RUNNING', format("Server '%s' started!", serverLabel), cb);
  }

  this.shutdownServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'shutdownServer', 'AVAILABLE', 'SHUTOFF', format("Server '%s' shut down!", serverLabel), cb);
  }

  this.stopServerTracked = function(serverLabel, cb) {
    _tracked(serverLabel, 'stopServer', 'INACTIVE', 'SHUTOFF', format("Server '%s' stopped!", serverLabel), cb);
  }

  var parseBody = function(body) {
    try {
      var data = JSON.parse(body);
      return [null, data];
    }
    catch(err) {
      return [err, body];
    }
  }

  this.parseBody = function(body) {
    return parseBody(body);
  }

  var labelError = function(label, cb) {
    self.logger.error(format("Server label '%s' does not exist, or is misconfigured, check config", label));
    cb("Config error");
  }

  var configFromLabel = function(label, cb) {
    try {
      var serverId = self.pb.serverIds[label];
      var sshHost = self.ssh[label].host;
      var sshPort = self.ssh[label].port || self.ssh.port;
      var sshUser = self.ssh[label].user || self.ssh.user;
      if (serverId && sshHost && sshPort && sshUser) {
        return {
          serverId: serverId,
          sshHost: sshHost,
          sshPort: sshPort,
          sshUser: sshUser,
        }
      }
      else {
        labelError(label, cb);
        return false;
      }
    }
    catch(err) {
      labelError(label, cb);
      return false;
    }
  }

  this.configFromLabel = function(label, cb) {
    return configFromLabel(label, cb);
  }

}

PbServer.prototype.setMockHandlers = function(handlers) {
  this.mockHandlers = handlers;
}

PbServer.prototype.useMockHandlers = function(successStates) {
  // Mocks shouldn't need any more attempts than this.
  this.maxStateChangeQueryAttempts = 2;
  this.mockHandlers.setSuccessStates(successStates);
  this.pbHandler = this.mockHandlers.pbHandler;
  this.sshHandler = this.mockHandlers.sshHandler;
}

PbServer.prototype.useLiveHandlers = function() {
  this.maxStateChangeQueryAttempts = MAX_QUERY_ATTEMPTS;
  this.pbHandler = libpb;
  this.sshHandler = SSH;
}

PbServer.prototype.listDatacenters = function(cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info("Getting datacenter info...");
  this.pbHandler.listDatacenters(apiCallback);
}

PbServer.prototype.listServers = function(cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info("Listing servers...");
  this.pbHandler.listServers(this.pb.datacenterId, apiCallback)
}

PbServer.prototype.getServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info(format("Getting server status for '%s'...", serverLabel));
  this.pbHandler.getServer(this.pb.datacenterId, config.serverId, apiCallback)
}

PbServer.prototype.startServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  this.logger.info(format("Starting server '%s'...", serverLabel));
  this.pbHandler.startServer(this.pb.datacenterId, config.serverId, cb)
}

PbServer.prototype.stopServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  this.logger.info(format("Powering off server '%s'...", serverLabel));
  this.pbHandler.stopServer(this.pb.datacenterId, config.serverId, cb)
}

PbServer.prototype.shutdownServer = function(serverLabel, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  this.logger.info(format("Shutting down server '%s'...", serverLabel));
  var ssh = new this.sshHandler({
    host: config.sshHost,
    port: config.sshPort,
    user: config.sshUser,
    key: this.sshKey,
  });
  var exit = function(code, stdout, stderr) {
    self.logger.debug(format("SSH command exit code: %s", code));
    if (code === 0) {
      self.logger.debug("Shutdown command execution succeeded...");
      cb(null, code);
    }
    else {
      var message = format('SSH command returned with error code: %d, %s', code, stderr);
      self.logger.error(message);
      cb(message);
    }
  }
  var execConfig = {
    exit: exit,
  };
  var startConfig = {
    success: function() {
      self.logger.debug("SSH connection successful...");
    },
    fail: function(err) {
      self.logger.debug(format("SSH connection failed: %s", err));
      cb(err);
    },
  }
  // Executing shutdown without backgrounding hangs, backgrounding the command
  // allows the shutdown to proceed, and our SSH command to get a return value.
  // NOTE: These commands kept separate to support the mock functionality.
  ssh.exec('shutdown -P now shutdown-now&', execConfig);
  ssh.start(startConfig);
}

PbServer.prototype.serverStateChange = function(serverLabel, machineToState, serverToState, cb) {
  var self = this;
  cb = cb ? cb : dummyCb;
  var count = 1;
  var checkState = function(err, data) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, data));
      cb(err, data);
      count++;
    }
    else {
      if (count > self.maxStateChangeQueryAttempts) {
        clearInterval(serverStateChange);
        var message = "Max attempts exceeded.";
        self.logger.error(message);
        cb(message);
      }
      else {
        var machineState = data.metadata.state;
        var serverState = data.properties.vmState;
        self.logger.debug(format("Attempt #%d for '%s'", count, serverLabel));
        self.logger.debug("-------------------------------------");
        self.logger.debug(format("Machine state: %s", machineState));
        self.logger.debug(format("Server state: %s", serverState));
        self.logger.debug("-------------------------------------");
        if (machineState == machineToState && serverState == serverToState) {
          self.logger.info(format("State change to (%s, %s) complete for '%s'", machineToState , serverToState, serverLabel));
          clearInterval(serverStateChange);
          cb(null, data);
        }
        count++;
      }
    }
  }
  var get = function() {
    self.getServer(serverLabel, checkState);
  }
  this.logger.info(format("Waiting for '%s' server state to change to (%s, %s)", serverLabel, machineToState , serverToState));
  get();
  var serverStateChange = setInterval(get, this.stateChangeQueryInterval);
}

PbServer.prototype.checkCommand = function(serverLabel, command, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var count = 1;
  // This prevents overlapping checks and messages.
  var timeout = this.stateChangeQueryInterval - 1000;
  this.logger.debug(format("Checking command: '%s' on '%s'", command, serverLabel));
  this.logger.debug(format("SSH connection timeout set to %d milliseconds", timeout));
  var exit = function(code, stdout, stderr) {
    if (code === 0) {
      clearInterval(checkCommand);
      self.logger.info("Command succeeded");
      cb(null, code);
    }
    else {
      self.logger.debug(format('Command returned with error code: %d, %s', code, stderr));
    }
  }
  var execConfig = {
    exit: exit,
  };
  var startConfig = {
    success: function() {
      self.logger.debug("SSH connection successful...");
    },
    fail: function(err) {
      self.logger.debug(format("SSH connection failed: %s", err));
    },
  }
  var check = function() {
    if (count > self.maxStateChangeQueryAttempts) {
      clearInterval(checkCommand);
      var message = "Max attempts exceeded.";
      self.logger.error(message);
      cb(message);
    }
    else {
      self.logger.debug(format("Attempt #%d on '%s'", count, serverLabel));
      var ssh = new self.sshHandler({
        host: config.sshHost,
        port: config.sshPort,
        user: config.sshUser,
        key: self.sshKey,
        timeout: timeout,
      });
      // NOTE: These commands kept separate to support the mock functionality.
      ssh.exec(command, execConfig);
      ssh.start(startConfig);
      count++;
    }
  }
  check();
  var checkCommand = setInterval(check, this.stateChangeQueryInterval);
}

PbServer.prototype.updateServer = function(serverLabel, profile, cb) {
  var config = this.configFromLabel(serverLabel, cb);
  if (!config) { return; }
  var self = this;
  cb = cb ? cb : dummyCb;
  var apiCallback = function(err, resp, body) {
    if (err) {
      self.logger.error(format("ERROR: %s, %s", err, body));
      cb(err, body);
    }
    else {
      var result = self.parseBody(body);
      cb.apply(self, result);
    }
  }
  this.logger.info(format("Updating server '%s' to profile: %s", serverLabel, profile));
  var data = this.pb.profiles[profile];
  if (data) {
    var updateData = {
      properties: data,
    }
    this.pbHandler.updateServer(this.pb.datacenterId, config.serverId, updateData, apiCallback);
  }
  else {
    this.logger.error(format("ERROR: profile '%s' does not exist", profile));
  }
}

if (module.exports) {
  module.exports = PbServer;
}
<|endoftext|>


<commit_before>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/* global _playground */

define(function (require, exports, module) {
    "use strict";

    var EventEmitter = require("eventEmitter"),
        util = require("../util"),
        Promise = require("bluebird");

    /**
     * The Descriptor object provides helper methods for dealing with the
     * low-level native binding to Photoshop. This object will typically
     * not be used by user-level code.
     *
     * Emits low-level Photoshop events such as "select" with
     * the following parameters:
     *    1. @param {?} info about the event, dependent on event type (Note:
     *           this should become more specific as the native interface is
     *           further defined.)
     *
     * @extends EventEmitter
     * @constructor
     * @private
     */
    var Descriptor = function () {
        EventEmitter.call(this);

        this._psEventHandler = this._psEventHandler.bind(this);
    };
    util.inherits(Descriptor, EventEmitter);

    /**
     * Event handler for events from the native bridge.
     *
     * @private
     * @param {*=} err Error information
     * @param {String} eventID typeID for event type
     * @param {Object} payload serialized ActionDescriptor for the event, dependent on event type
     */
    Descriptor.prototype._psEventHandler = function (err, eventID, payload) {
        if (err) {
            this.emit("error", "Failed to handle Photoshop event: " + err);
            return;
        }

        this.emit("all", eventID, payload);
        this.emit(eventID, payload);
    };

    /**
     * Emit the named event with the given arguments as parameters. Throws if the
     * event is "error" and there are no listeners.
     *
     * @override EventEmitter.prototype.emitEvent
     * @param {string|RegExp} event Name of the event to emit and execute listeners for
     * @param {Array=} args Optional array of arguments to be passed to each listener
     * @return {object} Current instance for chaining
     */
    Descriptor.prototype.emitEvent = function (event, args) {
        if (event === "error") {
            var listeners = this.getListeners(event);

            if (listeners.length === 0) {
                var message,
                    error;

                if (args.length > 0 && typeof args[0] === "string") {
                    message = args.shift();
                } else {
                    message = "Unhandled error event";
                }

                error = new Error(message);
                error.args = args;
                throw error;
            }
        }

        Descriptor.super_.prototype.emitEvent.call(this, event, args);
    };

    /**
     * Wraps certain type of parameters making it easier to call Descriptor.prototype.get
     * For arrays, returns the reference to array recursively mapping everything inside
     * For string values, references to the currently active one
     * For objects, leaves them as is.
     *
     * @private
     * @param {(string|Array.<object>|object)} toWrap object to reference to
     * @return {object} Reference to the toWrap object in a form .get will accept
     */
    var _wrap = function (toWrap) {
        var reference;

        if (Array.isArray(toWrap)) {
            reference = {
                ref: toWrap.map(_wrap).reverse()
            };
        } else if (typeof toWrap === "string") {
            reference = {
                ref: toWrap,
                enum: "ordinal",
                value: "targetEnum"
            };
        } else {
            reference = toWrap;

            // Special case for play objects
            if (reference.hasOwnProperty("null")) {
                reference["null"] = _wrap(reference["null"]);
            }

        }

        return reference;
    };

    /**
     * Constructs a property reference from the given action reference and
     * property name.
     *
     * @private
     * @param {(string|Array.<object>|object)} reference
     * @param {string} property
     * @return {Array.<object>}
     */
    var _makePropertyReference = function (reference, property) {
        var propertyDescriptor = {
                ref: "property",
                property: property
            };

        return Array.isArray(reference) ?
                reference.concat(propertyDescriptor) :
                [reference, propertyDescriptor];
    };

    /**
     * Executes a low-level "get" call using an ActionReference.
     *
     * @param {(string|Array.<(string|Object)>|Object)} reference The reference to retrieve. Can be:
     *     - string of a class name
     *     - Object representation of ActionReference key/value pairs
     *     - An array of a combination of the above, which will get turned into the appropriate ActionReference
     * @return {Promise.<?>} The value of the reference, dependent on reference type
     */
    Descriptor.prototype.get = function (reference) {
        var wrappedReference = _wrap(reference),
            getAsync = Promise.promisify(_playground.ps.descriptor.get,
                _playground.ps.descriptor);

        return getAsync(wrappedReference);
    };

    /**
     * Retrieves a property of a reference
     *
     * @param {String} reference The name of the reference
     * @param {String} property The name of the property
     * @return {Promise.<?>} The value of the property, dependent on reference type
     */
    Descriptor.prototype.getProperty = function (reference, property) {
        var propertyReference = _makePropertyReference(reference, property);

        return this.get(propertyReference)
            .then(function (obj) {
                if (!obj || !obj.hasOwnProperty(property)) {
                    throw new Error("No such property: " + property);
                }

                return obj[property];
            });
    };

    /**
     * Defines an enumeration of three constants that control dialog display
     * while executing action descriptors: DONT_DISPLAY, DISPLAY and SILENT.
     *
     * @const
     * @type {Object.<string: number>}
     */
    Descriptor.prototype.interactionMode = _playground.ps.descriptor.interactionMode;

    /**
     * Executes a low-level "play" call on the specified ActionDescriptor.
     *
     * @param {string} name Name of the ActionDescriptor command
     * @param {Object=} descriptor JS Object representation of ActionDescriptor key/value pairs, defaults to {}
     * @param {Object=} options options, defaults to "silent"
     * @return {Promise.<object>} Resolves when the call is complete (Note: eventually, this will
     *     return the value resulting from the execution of the ActionDescriptor, if any).
     */
    Descriptor.prototype.play = function (name, descriptor, options) {
        descriptor = descriptor || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var playAsync = Promise.promisify(_playground.ps.descriptor.play,
            _playground.ps.descriptor);

        return playAsync(name, descriptor, options);
    };

    /**
     * Executes a low-level "play" call on the PlayObject by unwrapping it
     *
     * @param {PlayObject} playObject Contains command, descriptor and options information
     *
     * @returns {Promise} Resolves to the result of the call
     */
    Descriptor.prototype.playObject = function (playObject) {
        var command = playObject.command,
            descriptor = _wrap(playObject.descriptor),
            options = playObject.options;

        return this.play(command, descriptor, options);
    };

    /**
     * Executes a low-level "batchPlay" call on the specified ActionDescriptors.
     *
     * @param {Array.<{name: string, descriptor: object}>} commands Array of ActionDescriptors to play
     * @param {object=} options Options applied to the execution of each ActionDescriptor individually
     * @param {{continueOnError: boolean=}}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.<object>>} Resolves with the list of ActionDescriptor results, or rejects
     *      with either an adapter error, or a single command error if not continueOnError mode. In
     *      continueOnError mode, always resolve with both the results and errors arrays.
     */
    Descriptor.prototype.batchPlay = function (commands, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var batchPlayAsync = Promise.promisify(_playground.ps.descriptor.batchPlay,
            _playground.ps.descriptor);

        return batchPlayAsync(commands, options, batchOptions)
            .then(function (response) {
                // Never reject in continueOnError mode; the caller must always check the results
                if (batchOptions.continueOnError) {
                    return response;
                }

                var theError;
                response[1].some(function (error) {
                    if (error) {
                        theError = error;
                        return true;
                    }
                });

                if (theError) {
                    // otherwise, throw the first error, because there is only one
                    throw theError;
                }

                // if there are no errors, resolve with just the results
                return response[0];
            });
    };

    /**
     * Executes a low-level "batchPlay" call on the specified PlayObjects.
     *
     * @param {Array.<PlayObject>} objects Array of PlayObjects to play
     * @param {object=} options Options applied to the execution of each PlayObject individually
     * @param {{continueOnError: boolean}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.object>} Resolves with the list of ActionDescriptor results.
     */
    Descriptor.prototype.batchPlayObjects = function (objects, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var commands = objects.map(function (object) {
            return {
                name: object.command,
                descriptor: _wrap(object.descriptor)
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "get" calls using batchPlay.
     *
     * NOTE: batchGet is currently slightly slower than simply executing the
     * get calls independently, which is almost certainly an adapter bug.
     * If that bug isn't fixed, we should consider replacing the implementation
     * of this method with one that simply performs the gets independently.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<object>} references The references to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of results.
     */
    Descriptor.prototype.batchGet = function (references, options, batchOptions) {
        var commands = references.map(function (reference) {
            return {
                name: "get",
                descriptor: {
                    "null": _wrap(reference)
                }
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "getProperty" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<{reference: object, property: string}>} refObjs
     *      The references and properties to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperties = function (refObjs, options, batchOptions) {
        batchOptions = batchOptions || {};
        var propertyReferences = refObjs.map(function (refObj) {
            return _makePropertyReference(refObj.reference, refObj.property);
        });

        return this.batchGet(propertyReferences, options, batchOptions)
            .then(function (response) {
                if (batchOptions.continueOnError) {
                    return response;
                }

                return response.map(function (result, index) {
                    var property = refObjs[index].property;
                    if (!result || !result.hasOwnProperty(property)) {
                        throw new Error("No such property: " + property);
                    }

                    return result[property];
                });
            });
    };

    /**
     * Fetch optional properties, which might not exist, and ignore errors.
     *
     * @param {object} reference
     * @param {Array.<string>} properties
     * @return {Promise.<object>} Always resolves to an object, but keys that
     *  don't exist are omitted from the resolved value.
     */
    Descriptor.prototype.batchGetOptionalProperties = function (reference, properties) {
        var makeRefObj = function (property) {
            return {
                reference: reference,
                property: property
            };
        };

        var refObjs = properties.map(makeRefObj),
            batchOptions = {
                continueOnError: true
            };

        return this.batchGetProperties(refObjs, undefined, batchOptions)
            .then(function (results) {
                var values = results[0];

                return values.reduce(function (result, value, index) {
                    var property = properties[index];
                    if (value && value.hasOwnProperty(property)) {
                        result[property] = value[property];
                    }
                    return result;
                }, {});
            });
    };

    /**
     * Executes a sequence of low-level "getProperty" calls for a single property
     * using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {object} references The references to retrieve
     * @param {string} property The property to retrieve
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperty = function (references, property, options, batchOptions) {
        var refObjs = references.map(function (reference) {
            return {
                reference: reference,
                property: property
            };
        });

        return this.batchGetProperties(refObjs, options, batchOptions);
    };

    /**
     * @type {Descriptor} The Descriptor singleton
     */
    var descriptor = new Descriptor();

    // bind native Photoshop event handler to our handler function
    _playground.setNotifier(_playground.notifierGroup.PHOTOSHOP, {}, descriptor._psEventHandler);

    module.exports = descriptor;
});
<commit_msg>Add optional options parameter to playObject<commit_after>/*
 * Copyright (c) 2014 Adobe Systems Incorporated. All rights reserved.
 *
 * Permission is hereby granted, free of charge, to any person obtaining a
 * copy of this software and associated documentation files (the "Software"),
 * to deal in the Software without restriction, including without limitation
 * the rights to use, copy, modify, merge, publish, distribute, sublicense,
 * and/or sell copies of the Software, and to permit persons to whom the
 * Software is furnished to do so, subject to the following conditions:
 *
 * The above copyright notice and this permission notice shall be included in
 * all copies or substantial portions of the Software.
 *
 * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
 * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
 * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
 * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
 * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
 * FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER
 * DEALINGS IN THE SOFTWARE.
 *
 */

/* global _playground */

define(function (require, exports, module) {
    "use strict";

    var EventEmitter = require("eventEmitter"),
        util = require("../util"),
        Promise = require("bluebird");

    /**
     * The Descriptor object provides helper methods for dealing with the
     * low-level native binding to Photoshop. This object will typically
     * not be used by user-level code.
     *
     * Emits low-level Photoshop events such as "select" with
     * the following parameters:
     *    1. @param {?} info about the event, dependent on event type (Note:
     *           this should become more specific as the native interface is
     *           further defined.)
     *
     * @extends EventEmitter
     * @constructor
     * @private
     */
    var Descriptor = function () {
        EventEmitter.call(this);

        this._psEventHandler = this._psEventHandler.bind(this);
    };
    util.inherits(Descriptor, EventEmitter);

    /**
     * Event handler for events from the native bridge.
     *
     * @private
     * @param {*=} err Error information
     * @param {String} eventID typeID for event type
     * @param {Object} payload serialized ActionDescriptor for the event, dependent on event type
     */
    Descriptor.prototype._psEventHandler = function (err, eventID, payload) {
        if (err) {
            this.emit("error", "Failed to handle Photoshop event: " + err);
            return;
        }

        this.emit("all", eventID, payload);
        this.emit(eventID, payload);
    };

    /**
     * Emit the named event with the given arguments as parameters. Throws if the
     * event is "error" and there are no listeners.
     *
     * @override EventEmitter.prototype.emitEvent
     * @param {string|RegExp} event Name of the event to emit and execute listeners for
     * @param {Array=} args Optional array of arguments to be passed to each listener
     * @return {object} Current instance for chaining
     */
    Descriptor.prototype.emitEvent = function (event, args) {
        if (event === "error") {
            var listeners = this.getListeners(event);

            if (listeners.length === 0) {
                var message,
                    error;

                if (args.length > 0 && typeof args[0] === "string") {
                    message = args.shift();
                } else {
                    message = "Unhandled error event";
                }

                error = new Error(message);
                error.args = args;
                throw error;
            }
        }

        Descriptor.super_.prototype.emitEvent.call(this, event, args);
    };

    /**
     * Wraps certain type of parameters making it easier to call Descriptor.prototype.get
     * For arrays, returns the reference to array recursively mapping everything inside
     * For string values, references to the currently active one
     * For objects, leaves them as is.
     *
     * @private
     * @param {(string|Array.<object>|object)} toWrap object to reference to
     * @return {object} Reference to the toWrap object in a form .get will accept
     */
    var _wrap = function (toWrap) {
        var reference;

        if (Array.isArray(toWrap)) {
            reference = {
                ref: toWrap.map(_wrap).reverse()
            };
        } else if (typeof toWrap === "string") {
            reference = {
                ref: toWrap,
                enum: "ordinal",
                value: "targetEnum"
            };
        } else {
            reference = toWrap;

            // Special case for play objects
            if (reference.hasOwnProperty("null")) {
                reference["null"] = _wrap(reference["null"]);
            }

        }

        return reference;
    };

    /**
     * Constructs a property reference from the given action reference and
     * property name.
     *
     * @private
     * @param {(string|Array.<object>|object)} reference
     * @param {string} property
     * @return {Array.<object>}
     */
    var _makePropertyReference = function (reference, property) {
        var propertyDescriptor = {
                ref: "property",
                property: property
            };

        return Array.isArray(reference) ?
                reference.concat(propertyDescriptor) :
                [reference, propertyDescriptor];
    };

    /**
     * Executes a low-level "get" call using an ActionReference.
     *
     * @param {(string|Array.<(string|Object)>|Object)} reference The reference to retrieve. Can be:
     *     - string of a class name
     *     - Object representation of ActionReference key/value pairs
     *     - An array of a combination of the above, which will get turned into the appropriate ActionReference
     * @return {Promise.<?>} The value of the reference, dependent on reference type
     */
    Descriptor.prototype.get = function (reference) {
        var wrappedReference = _wrap(reference),
            getAsync = Promise.promisify(_playground.ps.descriptor.get,
                _playground.ps.descriptor);

        return getAsync(wrappedReference);
    };

    /**
     * Retrieves a property of a reference
     *
     * @param {String} reference The name of the reference
     * @param {String} property The name of the property
     * @return {Promise.<?>} The value of the property, dependent on reference type
     */
    Descriptor.prototype.getProperty = function (reference, property) {
        var propertyReference = _makePropertyReference(reference, property);

        return this.get(propertyReference)
            .then(function (obj) {
                if (!obj || !obj.hasOwnProperty(property)) {
                    throw new Error("No such property: " + property);
                }

                return obj[property];
            });
    };

    /**
     * Defines an enumeration of three constants that control dialog display
     * while executing action descriptors: DONT_DISPLAY, DISPLAY and SILENT.
     *
     * @const
     * @type {Object.<string: number>}
     */
    Descriptor.prototype.interactionMode = _playground.ps.descriptor.interactionMode;

    /**
     * Executes a low-level "play" call on the specified ActionDescriptor.
     *
     * @param {string} name Name of the ActionDescriptor command
     * @param {Object=} descriptor JS Object representation of ActionDescriptor key/value pairs, defaults to {}
     * @param {Object=} options options, defaults to "silent"
     * @return {Promise.<object>} Resolves when the call is complete (Note: eventually, this will
     *     return the value resulting from the execution of the ActionDescriptor, if any).
     */
    Descriptor.prototype.play = function (name, descriptor, options) {
        descriptor = descriptor || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var playAsync = Promise.promisify(_playground.ps.descriptor.play,
            _playground.ps.descriptor);

        return playAsync(name, descriptor, options);
    };

    /**
     * Executes a low-level "play" call on the PlayObject by unwrapping it
     *
     * @param {PlayObject} playObject Contains command, descriptor and options information
     * @param {object=} options Overrides any options in the playObject
     * @returns {Promise} Resolves to the result of the call
     */
    Descriptor.prototype.playObject = function (playObject, options) {
        var command = playObject.command,
            descriptor = _wrap(playObject.descriptor),
            playOptions = playObject.options;

        // merge playOptions into options
        if (options === undefined) {
            options = playOptions;
        } else if (playOptions) {
            Object.keys(playOptions).forEach(function (key) {
                if (!options.hasOwnProperty(key)) {
                    options[key] = playOptions[key];
                }
            });
        }

        return this.play(command, descriptor, options);
    };

    /**
     * Executes a low-level "batchPlay" call on the specified ActionDescriptors.
     *
     * @param {Array.<{name: string, descriptor: object}>} commands Array of ActionDescriptors to play
     * @param {object=} options Options applied to the execution of each ActionDescriptor individually
     * @param {{continueOnError: boolean=}}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.<object>>} Resolves with the list of ActionDescriptor results, or rejects
     *      with either an adapter error, or a single command error if not continueOnError mode. In
     *      continueOnError mode, always resolve with both the results and errors arrays.
     */
    Descriptor.prototype.batchPlay = function (commands, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var batchPlayAsync = Promise.promisify(_playground.ps.descriptor.batchPlay,
            _playground.ps.descriptor);

        return batchPlayAsync(commands, options, batchOptions)
            .then(function (response) {
                // Never reject in continueOnError mode; the caller must always check the results
                if (batchOptions.continueOnError) {
                    return response;
                }

                var theError;
                response[1].some(function (error) {
                    if (error) {
                        theError = error;
                        return true;
                    }
                });

                if (theError) {
                    // otherwise, throw the first error, because there is only one
                    throw theError;
                }

                // if there are no errors, resolve with just the results
                return response[0];
            });
    };

    /**
     * Executes a low-level "batchPlay" call on the specified PlayObjects.
     *
     * @param {Array.<PlayObject>} objects Array of PlayObjects to play
     * @param {object=} options Options applied to the execution of each PlayObject individually
     * @param {{continueOnError: boolean}=} batchOptions Options that control how the batch of
     *      ActionDescriptors is executed.
     * @return {Promise.<Array.object>} Resolves with the list of ActionDescriptor results.
     */
    Descriptor.prototype.batchPlayObjects = function (objects, options, batchOptions) {
        batchOptions = batchOptions || {};
        options = options || {
            interactionMode: this.interactionMode.SILENT
        };

        var commands = objects.map(function (object) {
            return {
                name: object.command,
                descriptor: _wrap(object.descriptor)
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "get" calls using batchPlay.
     *
     * NOTE: batchGet is currently slightly slower than simply executing the
     * get calls independently, which is almost certainly an adapter bug.
     * If that bug isn't fixed, we should consider replacing the implementation
     * of this method with one that simply performs the gets independently.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<object>} references The references to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of results.
     */
    Descriptor.prototype.batchGet = function (references, options, batchOptions) {
        var commands = references.map(function (reference) {
            return {
                name: "get",
                descriptor: {
                    "null": _wrap(reference)
                }
            };
        });

        return this.batchPlay(commands, options, batchOptions);
    };

    /**
     * Executes a sequence of low-level "getProperty" calls using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {Array.<{reference: object, property: string}>} refObjs
     *      The references and properties to retrieve.
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperties = function (refObjs, options, batchOptions) {
        batchOptions = batchOptions || {};
        var propertyReferences = refObjs.map(function (refObj) {
            return _makePropertyReference(refObj.reference, refObj.property);
        });

        return this.batchGet(propertyReferences, options, batchOptions)
            .then(function (response) {
                if (batchOptions.continueOnError) {
                    return response;
                }

                return response.map(function (result, index) {
                    var property = refObjs[index].property;
                    if (!result || !result.hasOwnProperty(property)) {
                        throw new Error("No such property: " + property);
                    }

                    return result[property];
                });
            });
    };

    /**
     * Fetch optional properties, which might not exist, and ignore errors.
     *
     * @param {object} reference
     * @param {Array.<string>} properties
     * @return {Promise.<object>} Always resolves to an object, but keys that
     *  don't exist are omitted from the resolved value.
     */
    Descriptor.prototype.batchGetOptionalProperties = function (reference, properties) {
        var makeRefObj = function (property) {
            return {
                reference: reference,
                property: property
            };
        };

        var refObjs = properties.map(makeRefObj),
            batchOptions = {
                continueOnError: true
            };

        return this.batchGetProperties(refObjs, undefined, batchOptions)
            .then(function (results) {
                var values = results[0];

                return values.reduce(function (result, value, index) {
                    var property = properties[index];
                    if (value && value.hasOwnProperty(property)) {
                        result[property] = value[property];
                    }
                    return result;
                }, {});
            });
    };

    /**
     * Executes a sequence of low-level "getProperty" calls for a single property
     * using batchPlay.
     *
     * @see Descriptor.prototype.get
     * @see Descriptor.prototype.batchPlay
     * @param {object} references The references to retrieve
     * @param {string} property The property to retrieve
     * @param {object=} options
     * @param {object=} batchOptions
     * @return {Promise.<Array.<object>>} Resolves with an array of property results.
     */
    Descriptor.prototype.batchGetProperty = function (references, property, options, batchOptions) {
        var refObjs = references.map(function (reference) {
            return {
                reference: reference,
                property: property
            };
        });

        return this.batchGetProperties(refObjs, options, batchOptions);
    };

    /**
     * @type {Descriptor} The Descriptor singleton
     */
    var descriptor = new Descriptor();

    // bind native Photoshop event handler to our handler function
    _playground.setNotifier(_playground.notifierGroup.PHOTOSHOP, {}, descriptor._psEventHandler);

    module.exports = descriptor;
});
<|endoftext|>