<commit_before>"use strict";

/** @typedef {{preventDefault: Function, toPageURL: String, pageParams: Object}} */
var PageChangeEvent,
/** @typedef {} */
    Socket;

var _ = require("underscore"),
    Class = require("nodeclass").Class,
    is = require("nodeclass").is,
    config = require("./config.client.js"),
    EventEmitter = require("../shared/EventEmitter.class.js"),
    Page = require("./Page.class.js"),
    PageLoader = require("./PageLoader.class.js"),
    pageJS = require("page"),
    resolvePageURLs = require("../shared/helpers/resolvePageURLs.js"),
    pageRegistry = require("./registries/pageRegistry.js"),
    domAdapter = require("./helpers/domAdapter.js"),
    Default404Page = require("./defaults/Default404Page.class.js");

/**
 * @class App
 * @extends EventEmitter
 */
var App = new Class("App", {

    Extends: EventEmitter,

    /**
     * @event
     * @name App#beforePageChange
     * @param {PageChangeEvent} pageChangeEvent
     */

    /**
     * @event
     * @name App#pageChange
     */

    /**
     * @type {Socket}
     */
    socket: null,

    /**
     * @private
     * @type {Page}
     */
    __mainPage: null,

    /**
     * @private
     * @type {Function}
     */
    __mainPageClass: null,

    /**
     * @private
     * @type {PageLoader}
     */
    __activePageLoader: null,

    /**
     * @private
     * @type {Boolean}
     */
    __isRunning: false,

    /**
     * @param {!Function} MainPageClass
     * @throws {TypeError}
     * @constructor
     */
    init: function (MainPageClass) {

        if (is(MainPageClass).notInstanceOf(Function)) {
            throw new TypeError("(alamid) Cannot init App: The MainPageClass must be a function.");
        }

        this.__mainPageClass = MainPageClass;

    },

    /**
     * return the socket.io instance
     *
     * @return {*}
     */
    getSocket : function() {
        return this.socket;
    },

    /**
     * Returns the current page hierarchy including the main page at index 0.
     *
     * @return {Array.<Page>}
     */
    getCurrentPages: function () {
        var page = this.__mainPage,
            currentPages = [page];

        while ((page = page.getSubPage())) {
            currentPages.push(page);
        }

        return currentPages;
    },

    /**
     * Returns the main page.
     *
     * @return {Page}
     */
    getMainPage: function () {
        return this.__mainPage;
    },

    /**
     * Registers a handler for a specific route. The route maybe a string with special parts for capturing or a
     * regular expression. For instance:
     *
     * - "/home/about"
     * - "/blog/:author/posts/:postId?"      captures the string at :author and optionally :postId
     * - "/blog/\*" (without backslash)      matches all sub routes of /blog
     *
     * The handler may be a function or a string.
     *
     * A handler-function will be called with a context object and a next-function. For a complete reference of the
     * context-object @see http://visionmedia.github.com/page.js/
     * Calling next() will delegate control to the next matching route handler.
     *
     * If the handler is a string it will be interpreted as pageURL calling app.changePage() with that given pageURL.
     *
     * @param {!String|RegExp} route
     * @param {!String|Function} handler
     * @throws {TypeError}
     * @return {App} the instance
     */
    addRoute: function (route, handler) {

        var pageJSArguments,
            handlers = [],
            argumentsCount = arguments.length,
            isDev = config.mode === "development",
            self = this;

        if (argumentsCount === 2) {
            handlers.push(handler);
        }

        if(argumentsCount > 2) {
            handlers = Array.prototype.slice.call(arguments, 1);
        }

        _(handlers).each(function prepareHandler(handlerToPrepare, index) {

            var preparedHandlerReference,
                preparedHandler = self._prepareRouteHandler(handlerToPrepare);

            if (isDev) {
                preparedHandlerReference = preparedHandler;
                preparedHandler = function debugDispatchRoute(ctx, next) {
                    console.log("dispatching:" + route, ctx);
                    preparedHandlerReference(ctx, next);
                }
            }

            handlers[index] = preparedHandler;
        });

        handlers.unshift(route);
        pageJSArguments = handlers;

        pageJS.apply(null, pageJSArguments);

        return this.Instance;
    },

    /**
     * @param @param {!String|Function} handler
     * @private
     */
    _prepareRouteHandler: function (handler) {

        var pageURL,
            self = this;

        if (is(handler).instanceOf(String)) {
            pageURL = handler;
            handler = function handleRoute(ctx) {
                self.changePage(pageURL, ctx);
            };
        } else if (is(handler).notInstanceOf(Function)) {
            throw new TypeError("(alamid) Cannot register route: The handler must be a string or a function. " +
                "Instead saw typeof '" + typeof handler + "'");
        }

        return handler;

    },

    /**
     * Initializes the main page, registers event listeners for route changes and
     * dispatches the first route according to window.location
     *
     * @param {Object=} pageJSOptions @see http://visionmedia.github.com/page.js/
     */
    start: function (pageJSOptions) {
        var MainPageClass = this.__mainPageClass,
            mainPage,
            mainPageParentNode = domAdapter(document).find('body')[0],
            default404Page,
            self = this;

        if (!mainPageParentNode) {
            throw new Error(
                '(alamid) Cannot start app: Could not find an element with data-node="page" under document.body'
            );
        }

        if (config.useWebsockets && this.socket === null) {
            this.__initSocket();
        }

        // Passing an empty object as params to keep the Page.init()-signature consistent.
        // Because the MainPage is the very first page, there are no params.
        mainPage = new MainPageClass({});
        mainPage.emit("beforeAdd");
        mainPageParentNode.appendChild(mainPage.getNode());
        mainPage.emit("add");
        this.__mainPage = mainPage;

        // If App is in development mode add as last route alamid's default 404 Route.
        if (config.mode === "development") {

            pageJS("*", function display404Page(ctx) {
                default404Page = new Default404Page();
                default404Page.setTitle("404");
                default404Page.setSubTitle("Page Not Found");
                default404Page.setMessage(
                    "This is alamid's default 404 Page." +
                    "The Page is displayed because alamid is running 'development' mode."
                );
                default404Page.setInfo(ctx);
                self.__mainPage.setSubPage(default404Page);
            });
        }

        pageJS.start(pageJSOptions);

        this.__isRunning = true;
    },

    /**
     * Dispatches the given route and gives control to the route handlers that match to the given route.
     *
     * @param {!String} route
     * @return {App}
     */
    dispatchRoute: function (route) {
        if (this.__isRunning === false) {
            this.start({dispatch: false});
        }

        pageJS(route);

        return this.Instance;
    },

    /**
     * Changes the page hierarchy according to the given pageURL.
     *
     * For instance: If the user is currently on 'home/about' and the new pageURL is 'blog/posts', the 'home'- and
     * the 'home/about'-page will be left and disposed. After that the 'blog'- and the 'blog/posts'-page will be initialized
     * and appended.
     *
     * Only those pages are changed that aren't displayed in the new state. E.g. 'blog/about' and 'blog/posts' share
     * both 'blog' as the parent page. So 'blog' won't be disposed.
     *
     * Further calls to changePage() will automatically cancel the previous page change process if still running.
     *
     * @param {!String} pageURL a string like 'blog/posts' with no leading slash
     * @param {Object.<String, String>} params the params object that will be passed to the created classes
     * @throws {TypeError}
     * @throws {Error} When one of the pages doesn't exist
     */
    changePage: function (pageURL, params) {

        var self = this,
            passedPageURL,
            pageURLs,
            pageURLsToLoad,
            pageLoader = this.__activePageLoader,
            indexOfTransitionPage,
            parentOfTransitionPage,
            currentPages,
            pagesToLeave,
            preventDefault = false;

        //Make it possible that a call to .changePage() with '/' as route resolves to MainPage
        passedPageURL = pageURL;
        if (pageURL === "/") {
            pageURL = "";
        }

        if (typeof params === "undefined") {
            params = {};
        }

        preventDefault = this.__emitBeforePageChange(passedPageURL, params);
        if (preventDefault) {
            return;
        }

        // Retrieve the current page hierarchy
        currentPages = this.getCurrentPages();
        // Remove main page because it can't be changed
        currentPages.shift();

        // Divide the pageURL in all pageURLs that have to be requested.
        // E.g. "blog/posts/comments" becomes ["blog", "blog/posts", "blog/posts/comments"]
        //resolvePageURLs can't resolve a single backlslash "/", it must be transformed to "".
        pageURLs = resolvePageURLs(pageURL);

        // Determines the position in the page hierarchy where the pages have to be changed.
        // E.g. A page change from 'blog/about' to 'blog/posts' would result in index = 2.
        // Hierarchy[0] is the main page.
        indexOfTransitionPage = this.__getIndexOfTransitionPage(pageURLs);
        parentOfTransitionPage = currentPages[indexOfTransitionPage - 1] || this.__mainPage;

        pagesToLeave = currentPages.slice(indexOfTransitionPage);
        pageURLsToLoad = pageURLs.slice(indexOfTransitionPage);

        preventDefault = this.__emitLeaveEventOnPages(pagesToLeave, passedPageURL, params);
        if (preventDefault) {
            return;
        }

        //If there is a running pageLoader then cancel loading of page.
        if (pageLoader) {
            pageLoader.cancel();
        }

        this.__activePageLoader = pageLoader = new PageLoader(pageURLsToLoad);
        pageLoader.load(params, function onPageLoaderFinished(err, pages) {

            var event = {
                toPageURL: passedPageURL,
                pageParams: params
            };

            self.__activePageLoader = null;

            if (err) {
                throw err;
            }

            //Return to MainPage
            if (pages === undefined) {
                self.__changeToMainPage();
            } else {
                self.__appendPages(parentOfTransitionPage, pages);
            }

            self.Super.emit("pageChange", event);
        });
    },

    /**
     * Determines the position in the page hierarchy where the pages have to be changed.
     * E.g. A page change from 'blog/about' to 'blog/posts' would result in index = 1.
     *
     * @param {!Array.<String>} pageURLs
     * @return {Number}
     * @private
     */
    __getIndexOfTransitionPage: function (pageURLs) {
        var page = this.__mainPage,
            index = null,
            PageClass;

        _(pageURLs).find(function findTransitionPageURL(pageURL, i) {
            index = i;
            page = page.getSubPage();
            PageClass = pageRegistry.getPageClass(pageURL);
            return typeof PageClass !== "function" || is(page).notInstanceOf(PageClass);
        });

        return index;
    },

    /**
     * Emits 'beforeLeave' with the possibility to abort the operation via preventDefault().
     * The events will be emitted on every page in the array from right to left (bottom -> top).
     *
     * @param {Array.<Page>} pages
     * @param {String} pageURL
     * @param {Object}
     * @return {Boolean} true if preventDefault() has been called
     * @private
     */
    __emitLeaveEventOnPages: function (pages, pageURL, params) {
        var preventDefault = false,
            page,
            i,
            event = {
                toPageURL: pageURL,
                pageParams: params,
                preventDefault: function () {
                    preventDefault = true;
                }
            };

        for (i = pages.length - 1; i >= 0; i--) {
            page = pages[i];
            page.emit("beforeLeave", event);
            if (preventDefault) {
                break;  // stop emitting the leave event, the current page takes the control flow
            }
        }

        return preventDefault;
    },

    /**
     * Emits 'beforePageChange' with the possibility to abort the operation via preventDefault().
     *
     * @param {String} toPageURL
     * @param {Object} pageParams
     * @return {Boolean} true if preventDefault() has been called
     * @private
     */
    __emitBeforePageChange: function (toPageURL, pageParams) {
        var preventDefault = false,
            event = {
                toPageURL: toPageURL,
                pageParams: pageParams,
                preventDefault: function () {
                    preventDefault = true;
                }
            };

        this.Super.emit("beforePageChange", event);

        return preventDefault;
    },

    /**
     * Calls setSubPage() on every page with the next item in the array.
     *
     * @param {Page} parentPage
     * @param {Array.<Page>} pages
     * @private
     */
    __appendPages: function (parentPage, pages) {

        _(pages).each(function appendPage(page) {
            parentPage.setSubPage(page);
            parentPage = page;
        });

    },

    /**
     * Removes all Sub-Pages till MainPage is the last man standing
     *
     * @private
     */
    __changeToMainPage: function () {
        var currentPages = this.getCurrentPages();

        //Walk reverse of array
        currentPages.reverse();

        _(currentPages).each(function resetSubPage(page, index) {
            //Skip first(===reversed last) page, because it has no Sub-Page
            if (index !== 0) {
                page.setSubPage(null);
            }
        });
    },

    /**
     * Initializes socket.io
     *
     * @private
     */
    __initSocket: function () {
        if (typeof io === "undefined") {
            throw new ReferenceError("(alamid) window.io is not defined: Add socket io to your scripts or " +
                "disable Websockets in your alamid config");
        }
        this.socket = io.connect();
    }
});

module.exports = App;<commit_msg>- using straight document.body for MainPage-root instead of selecting "body" via domAdapter<commit_after>"use strict";

/** @typedef {{preventDefault: Function, toPageURL: String, pageParams: Object}} */
var PageChangeEvent,
/** @typedef {} */
    Socket;

var _ = require("underscore"),
    Class = require("nodeclass").Class,
    is = require("nodeclass").is,
    config = require("./config.client.js"),
    EventEmitter = require("../shared/EventEmitter.class.js"),
    Page = require("./Page.class.js"),
    PageLoader = require("./PageLoader.class.js"),
    pageJS = require("page"),
    resolvePageURLs = require("../shared/helpers/resolvePageURLs.js"),
    pageRegistry = require("./registries/pageRegistry.js"),
    domAdapter = require("./helpers/domAdapter.js"),
    Default404Page = require("./defaults/Default404Page.class.js");

/**
 * @class App
 * @extends EventEmitter
 */
var App = new Class("App", {

    Extends: EventEmitter,

    /**
     * @event
     * @name App#beforePageChange
     * @param {PageChangeEvent} pageChangeEvent
     */

    /**
     * @event
     * @name App#pageChange
     */

    /**
     * @type {Socket}
     */
    socket: null,

    /**
     * @private
     * @type {Page}
     */
    __mainPage: null,

    /**
     * @private
     * @type {Function}
     */
    __mainPageClass: null,

    /**
     * @private
     * @type {PageLoader}
     */
    __activePageLoader: null,

    /**
     * @private
     * @type {Boolean}
     */
    __isRunning: false,

    /**
     * @param {!Function} MainPageClass
     * @throws {TypeError}
     * @constructor
     */
    init: function (MainPageClass) {

        if (is(MainPageClass).notInstanceOf(Function)) {
            throw new TypeError("(alamid) Cannot init App: The MainPageClass must be a function.");
        }

        this.__mainPageClass = MainPageClass;

    },

    /**
     * return the socket.io instance
     *
     * @return {*}
     */
    getSocket : function() {
        return this.socket;
    },

    /**
     * Returns the current page hierarchy including the main page at index 0.
     *
     * @return {Array.<Page>}
     */
    getCurrentPages: function () {
        var page = this.__mainPage,
            currentPages = [page];

        while ((page = page.getSubPage())) {
            currentPages.push(page);
        }

        return currentPages;
    },

    /**
     * Returns the main page.
     *
     * @return {Page}
     */
    getMainPage: function () {
        return this.__mainPage;
    },

    /**
     * Registers a handler for a specific route. The route maybe a string with special parts for capturing or a
     * regular expression. For instance:
     *
     * - "/home/about"
     * - "/blog/:author/posts/:postId?"      captures the string at :author and optionally :postId
     * - "/blog/\*" (without backslash)      matches all sub routes of /blog
     *
     * The handler may be a function or a string.
     *
     * A handler-function will be called with a context object and a next-function. For a complete reference of the
     * context-object @see http://visionmedia.github.com/page.js/
     * Calling next() will delegate control to the next matching route handler.
     *
     * If the handler is a string it will be interpreted as pageURL calling app.changePage() with that given pageURL.
     *
     * @param {!String|RegExp} route
     * @param {!String|Function} handler
     * @throws {TypeError}
     * @return {App} the instance
     */
    addRoute: function (route, handler) {

        var pageJSArguments,
            handlers = [],
            argumentsCount = arguments.length,
            isDev = config.mode === "development",
            self = this;

        if (argumentsCount === 2) {
            handlers.push(handler);
        }

        if(argumentsCount > 2) {
            handlers = Array.prototype.slice.call(arguments, 1);
        }

        _(handlers).each(function prepareHandler(handlerToPrepare, index) {

            var preparedHandlerReference,
                preparedHandler = self._prepareRouteHandler(handlerToPrepare);

            if (isDev) {
                preparedHandlerReference = preparedHandler;
                preparedHandler = function debugDispatchRoute(ctx, next) {
                    console.log("dispatching:" + route, ctx);
                    preparedHandlerReference(ctx, next);
                }
            }

            handlers[index] = preparedHandler;
        });

        handlers.unshift(route);
        pageJSArguments = handlers;

        pageJS.apply(null, pageJSArguments);

        return this.Instance;
    },

    /**
     * @param @param {!String|Function} handler
     * @private
     */
    _prepareRouteHandler: function (handler) {

        var pageURL,
            self = this;

        if (is(handler).instanceOf(String)) {
            pageURL = handler;
            handler = function handleRoute(ctx) {
                self.changePage(pageURL, ctx);
            };
        } else if (is(handler).notInstanceOf(Function)) {
            throw new TypeError("(alamid) Cannot register route: The handler must be a string or a function. " +
                "Instead saw typeof '" + typeof handler + "'");
        }

        return handler;

    },

    /**
     * Initializes the main page, registers event listeners for route changes and
     * dispatches the first route according to window.location
     *
     * @param {Object=} pageJSOptions @see http://visionmedia.github.com/page.js/
     */
    start: function (pageJSOptions) {
        var MainPageClass = this.__mainPageClass,
            mainPage,
            mainPageParentNode = document.body,
            default404Page,
            self = this;

        if (!mainPageParentNode) {
            throw new Error(
                '(alamid) Cannot start app: Could not find an element with data-node="page" under document.body'
            );
        }

        if (config.useWebsockets && this.socket === null) {
            this.__initSocket();
        }

        // Passing an empty object as params to keep the Page.init()-signature consistent.
        // Because the MainPage is the very first page, there are no params.
        mainPage = new MainPageClass({});
        mainPage.emit("beforeAdd");
        mainPageParentNode.appendChild(mainPage.getNode());
        mainPage.emit("add");
        this.__mainPage = mainPage;

        // If App is in development mode add as last route alamid's default 404 Route.
        if (config.mode === "development") {

            pageJS("*", function display404Page(ctx) {
                default404Page = new Default404Page();
                default404Page.setTitle("404");
                default404Page.setSubTitle("Page Not Found");
                default404Page.setMessage(
                    "This is alamid's default 404 Page." +
                    "The Page is displayed because alamid is running 'development' mode."
                );
                default404Page.setInfo(ctx);
                self.__mainPage.setSubPage(default404Page);
            });
        }

        pageJS.start(pageJSOptions);

        this.__isRunning = true;
    },

    /**
     * Dispatches the given route and gives control to the route handlers that match to the given route.
     *
     * @param {!String} route
     * @return {App}
     */
    dispatchRoute: function (route) {
        if (this.__isRunning === false) {
            this.start({dispatch: false});
        }

        pageJS(route);

        return this.Instance;
    },

    /**
     * Changes the page hierarchy according to the given pageURL.
     *
     * For instance: If the user is currently on 'home/about' and the new pageURL is 'blog/posts', the 'home'- and
     * the 'home/about'-page will be left and disposed. After that the 'blog'- and the 'blog/posts'-page will be initialized
     * and appended.
     *
     * Only those pages are changed that aren't displayed in the new state. E.g. 'blog/about' and 'blog/posts' share
     * both 'blog' as the parent page. So 'blog' won't be disposed.
     *
     * Further calls to changePage() will automatically cancel the previous page change process if still running.
     *
     * @param {!String} pageURL a string like 'blog/posts' with no leading slash
     * @param {Object.<String, String>} params the params object that will be passed to the created classes
     * @throws {TypeError}
     * @throws {Error} When one of the pages doesn't exist
     */
    changePage: function (pageURL, params) {

        var self = this,
            passedPageURL,
            pageURLs,
            pageURLsToLoad,
            pageLoader = this.__activePageLoader,
            indexOfTransitionPage,
            parentOfTransitionPage,
            currentPages,
            pagesToLeave,
            preventDefault = false;

        //Make it possible that a call to .changePage() with '/' as route resolves to MainPage
        passedPageURL = pageURL;
        if (pageURL === "/") {
            pageURL = "";
        }

        if (typeof params === "undefined") {
            params = {};
        }

        preventDefault = this.__emitBeforePageChange(passedPageURL, params);
        if (preventDefault) {
            return;
        }

        // Retrieve the current page hierarchy
        currentPages = this.getCurrentPages();
        // Remove main page because it can't be changed
        currentPages.shift();

        // Divide the pageURL in all pageURLs that have to be requested.
        // E.g. "blog/posts/comments" becomes ["blog", "blog/posts", "blog/posts/comments"]
        //resolvePageURLs can't resolve a single backlslash "/", it must be transformed to "".
        pageURLs = resolvePageURLs(pageURL);

        // Determines the position in the page hierarchy where the pages have to be changed.
        // E.g. A page change from 'blog/about' to 'blog/posts' would result in index = 2.
        // Hierarchy[0] is the main page.
        indexOfTransitionPage = this.__getIndexOfTransitionPage(pageURLs);
        parentOfTransitionPage = currentPages[indexOfTransitionPage - 1] || this.__mainPage;

        pagesToLeave = currentPages.slice(indexOfTransitionPage);
        pageURLsToLoad = pageURLs.slice(indexOfTransitionPage);

        preventDefault = this.__emitLeaveEventOnPages(pagesToLeave, passedPageURL, params);
        if (preventDefault) {
            return;
        }

        //If there is a running pageLoader then cancel loading of page.
        if (pageLoader) {
            pageLoader.cancel();
        }

        this.__activePageLoader = pageLoader = new PageLoader(pageURLsToLoad);
        pageLoader.load(params, function onPageLoaderFinished(err, pages) {

            var event = {
                toPageURL: passedPageURL,
                pageParams: params
            };

            self.__activePageLoader = null;

            if (err) {
                throw err;
            }

            //Return to MainPage
            if (pages === undefined) {
                self.__changeToMainPage();
            } else {
                self.__appendPages(parentOfTransitionPage, pages);
            }

            self.Super.emit("pageChange", event);
        });
    },

    /**
     * Determines the position in the page hierarchy where the pages have to be changed.
     * E.g. A page change from 'blog/about' to 'blog/posts' would result in index = 1.
     *
     * @param {!Array.<String>} pageURLs
     * @return {Number}
     * @private
     */
    __getIndexOfTransitionPage: function (pageURLs) {
        var page = this.__mainPage,
            index = null,
            PageClass;

        _(pageURLs).find(function findTransitionPageURL(pageURL, i) {
            index = i;
            page = page.getSubPage();
            PageClass = pageRegistry.getPageClass(pageURL);
            return typeof PageClass !== "function" || is(page).notInstanceOf(PageClass);
        });

        return index;
    },

    /**
     * Emits 'beforeLeave' with the possibility to abort the operation via preventDefault().
     * The events will be emitted on every page in the array from right to left (bottom -> top).
     *
     * @param {Array.<Page>} pages
     * @param {String} pageURL
     * @param {Object}
     * @return {Boolean} true if preventDefault() has been called
     * @private
     */
    __emitLeaveEventOnPages: function (pages, pageURL, params) {
        var preventDefault = false,
            page,
            i,
            event = {
                toPageURL: pageURL,
                pageParams: params,
                preventDefault: function () {
                    preventDefault = true;
                }
            };

        for (i = pages.length - 1; i >= 0; i--) {
            page = pages[i];
            page.emit("beforeLeave", event);
            if (preventDefault) {
                break;  // stop emitting the leave event, the current page takes the control flow
            }
        }

        return preventDefault;
    },

    /**
     * Emits 'beforePageChange' with the possibility to abort the operation via preventDefault().
     *
     * @param {String} toPageURL
     * @param {Object} pageParams
     * @return {Boolean} true if preventDefault() has been called
     * @private
     */
    __emitBeforePageChange: function (toPageURL, pageParams) {
        var preventDefault = false,
            event = {
                toPageURL: toPageURL,
                pageParams: pageParams,
                preventDefault: function () {
                    preventDefault = true;
                }
            };

        this.Super.emit("beforePageChange", event);

        return preventDefault;
    },

    /**
     * Calls setSubPage() on every page with the next item in the array.
     *
     * @param {Page} parentPage
     * @param {Array.<Page>} pages
     * @private
     */
    __appendPages: function (parentPage, pages) {

        _(pages).each(function appendPage(page) {
            parentPage.setSubPage(page);
            parentPage = page;
        });

    },

    /**
     * Removes all Sub-Pages till MainPage is the last man standing
     *
     * @private
     */
    __changeToMainPage: function () {
        var currentPages = this.getCurrentPages();

        //Walk reverse of array
        currentPages.reverse();

        _(currentPages).each(function resetSubPage(page, index) {
            //Skip first(===reversed last) page, because it has no Sub-Page
            if (index !== 0) {
                page.setSubPage(null);
            }
        });
    },

    /**
     * Initializes socket.io
     *
     * @private
     */
    __initSocket: function () {
        if (typeof io === "undefined") {
            throw new ReferenceError("(alamid) window.io is not defined: Add socket io to your scripts or " +
                "disable Websockets in your alamid config");
        }
        this.socket = io.connect();
    }
});

module.exports = App;<|endoftext|>


<commit_before>/**
 * Powered by Andy <andy@away.name>.
 * Date: 03.08.13
 */
var WebSocket = require('ws')
	, Errors = require('../Errors')
	, events = require('events')
	, util = require('util')
	, EventEmitter = events.EventEmitter
	, uuid = require('node-uuid');

var JobClient = function(options){
	this._onOpen = this._onOpen.bind(this);
	this._onMessage = this._onMessage.bind(this);
	this._onClose = this._onClose.bind(this);
	this.options = options || {};
	this.options.host = this.options.host || "localhost";
	this.options.port = this.options.port || 80;

	this.id = null;
	this.requests = {};
	this.ws = new WebSocket('ws://' + this.options.host + ":" + this.options.port);
	this.ws.on('open', this._onOpen);
	this.ws.on('close', this._onClose);
	this.ws.on('message', this._onMessage);

	this.isOpen = false;
};

/**
 * Inherits from EventEmitter.
 */
util.inherits(JobClient, events.EventEmitter);

/**
 * Event on open
 * @private
 */
JobClient.prototype._onOpen = function(){
	this.isOpen = true;
	this.emit('open');
};

/**
 * Event on WebSocket message
 * @param data
 * @param flag
 * @private
 */
JobClient.prototype._onMessage = function(data, flag){
	var self = this;
	var msg = null;
	try{
		msg = JSON.parse(data);
	}catch (err){
		return;
	}
	if (msg.type === "worker-id"){
		self.id = msg.id;
		self._executeCommand({type: "register", data: {
			maxqps: this.options.maxqps,
			executable: false
		}}, function(err, result){
			if (err){
				self.emit('error', new Error(Errors.E3003(err)));
			} else {
				self.emit('register');
			}
		});
	} else {
		var request = this.requests[msg.id];
		delete this.requests[msg.id];
		if (msg.success){
			request(null, msg.result, msg.workerId);
		} else {
			request(msg.error);
		}
	}
};

/**
 *
 * @param data
 * @param callback
 * @private
 */
JobClient.prototype._executeCommand = function(data, callback){
	var self = this;
	data.id = uuid.v4();
	this.requests[data.id] = callback;
	try{
		this.ws.send(JSON.stringify(data));
	}catch (err){
		self.emit('error', new Error(Errors.E1001(err)));
	}
};

/**
 * Event on close
 * @private
 */
JobClient.prototype._onClose = function(){
	this.isOpen = false;
	this.emit('close');
};

JobClient.prototype.registerFunction = function(name){
	this[name] = function(){
		var args = Array.prototype.slice.call(arguments);
		var callback = args.pop();
		this._executeCommand({type: "create-job", data: {
			name: name,
			args: args
		}}, callback);
	};
};

module.exports = exports = JobClient;<commit_msg>add by worker client<commit_after>/**
 * Powered by Andy <andy@away.name>.
 * Date: 03.08.13
 */
var WebSocket = require('ws')
	, Errors = require('../Errors')
	, events = require('events')
	, util = require('util')
	, EventEmitter = events.EventEmitter
	, uuid = require('node-uuid');

var JobClient = function(options){
	this._onOpen = this._onOpen.bind(this);
	this._onMessage = this._onMessage.bind(this);
	this._onClose = this._onClose.bind(this);
	this.options = options || {};
	this.options.host = this.options.host || "localhost";
	this.options.port = this.options.port || 80;

	this.id = null;
	this.requests = {};
	this.ws = new WebSocket('ws://' + this.options.host + ":" + this.options.port);
	this.ws.on('open', this._onOpen);
	this.ws.on('close', this._onClose);
	this.ws.on('message', this._onMessage);

	this.isOpen = false;
};

/**
 * Inherits from EventEmitter.
 */
util.inherits(JobClient, events.EventEmitter);

/**
 * Event on open
 * @private
 */
JobClient.prototype._onOpen = function(){
	this.isOpen = true;
	this.emit('open');
};

/**
 * Event on WebSocket message
 * @param data
 * @param flag
 * @private
 */
JobClient.prototype._onMessage = function(data, flag){
	var self = this;
	var msg = null;
	try{
		msg = JSON.parse(data);
	}catch (err){
		return;
	}
	if (msg.type === "worker-id"){
		self.id = msg.id;
		self._executeCommand({type: "register", data: {
			maxqps: this.options.maxqps,
			executable: false
		}}, function(err, result){
			if (err){
				self.emit('error', new Error(Errors.E3003(err)));
			} else {
				self.emit('register');
			}
		});
	} else {
		var request = this.requests[msg.id];
		delete this.requests[msg.id];
		if (msg.success){
			request(null, msg.result, msg.workerId);
		} else {
			request(msg.error);
		}
	}
};

/**
 *
 * @param data
 * @param callback
 * @private
 */
JobClient.prototype._executeCommand = function(data, callback){
	var self = this;
	data.id = uuid.v4();
	this.requests[data.id] = callback;
	try{
		this.ws.send(JSON.stringify(data));
	}catch (err){
		self.emit('error', new Error(Errors.E1001(err)));
	}
};

/**
 * Event on close
 * @private
 */
JobClient.prototype._onClose = function(){
	this.isOpen = false;
	this.emit('close');
};

JobClient.prototype.registerFunction = function(name){
	this[name] = function(){
		var args = Array.prototype.slice.call(arguments);
		var callback = args.pop();
		this._executeCommand({type: "create-job", data: {
			name: name,
			args: args
		}}, callback);
	};
	this[name + "ByWorker"] = function(){
		var args = Array.prototype.slice.call(arguments);
		var workerId = args.shift();
		var callback = args.pop();
		this._executeCommand({type: "create-job", data: {
			name: name,
			args: args,
			workerId: workerId
		}}, callback);
	};
};

module.exports = exports = JobClient;<|endoftext|>


<commit_before>var port = self.port;
var sent = new Array(10);
var src_prefix = window.location.protocol + '//' + window.location.host;

// Send heartbeats
setInterval(function() {
  if (
    $('#chatMainPanel').css('visibility') == 'visible' ||
    $('#chatArea').length > 0
  ) {
    port.emit('heartbeat', true);
  }
}, 3000);

// Listen to DOM mutations
if (document.getElementById('chatMainPanel')) {
  new MutationObserver(function() {
    sendMessage(captureMessage);
  }).observe(document.getElementById('chatMainPanel'), { 'childList': true, 'subtree': true });
} else if (document.getElementById('chatArea')) {
  new MutationObserver(function() {
    sendMessage(captureMessage2);
  }).observe(document.getElementById('chatArea'), { 'childList': true, 'subtree': true });
}

function sendMessage(callback) {
  var i, msg = callback();

  console.log(msg); // FIXME

  if (msg !== null) {
    port.emit('bullet', msg);
    sent.shift();
    sent.push(msg.id);
  }
}

/*
  An typical message looks like:

  {
    id: '',
    user: { name: '',  avatar: '' },
    content: { text: '', image: '' }
  };
*/

function captureMessage() {
  var i, id, text, img;

  // Check message id
  id = $('.chatItem:last').attr('un');
  for (i = 0; i < sent.length; i++) {
    if (sent[i] == id) {
      return null;
    }
  }

  // Handle text message
  if ($('.chatItemContent:last pre img').length) {
    $('.chatItemContent:last pre img').each(function() {
      if (/^\//.test($(this).attr('src'))) {
        $(this).attr('src', src_prefix + $(this).attr('src'));
      }
    });
  }
  if ($('.chatItemContent:last pre span.emoji').length) {
    $('.chatItemContent:last pre span.emoji').each(function() {
      $(this).replaceWith('');
    });
  }
  text = $('.chatItemContent:last pre').html();

  // Handle image message
  if ($('.chatItemContent:last .img_wrap img').length) {
    img = src_prefix + $('.chatItemContent:last .img_wrap img').attr('rawsrc');
  } else if ($('.chatItemContent:last .customEmoji').length) {
    img = src_prefix + $('.chatItemContent:last .customEmoji').attr('src');
  } else {
    img = '';
  }

  // Make message
  var msg = {
    id: id,
    user: {
      name: $('.chatItemContent:last .avatar').attr('title'),
      avatar: src_prefix + $('.chatItemContent:last .avatar').attr('src')
    },
    content: {
      text: text,
      image: img
    }
  };
  return msg;
}

function captureMessage2() {
  var id, text = '', img = '';

  // Check message id
  id = $.parseJSON($('#chatArea .message:last > .content > .ng-scope[data-cm]').attr('data-cm')).msgId;
  for (i = 0; i < sent.length; i++) {
    if (sent[i] == id) {
      return null;
    }
  }

  // Handle text message
  if ($('#chatArea .message:last .plain').length) {
    if ($('#chatArea .message:last .plain pre img').length) {
      $('#chatArea .message:last .plain pre img').each(function() {
        if (/^\//.test($(this).attr('src'))) {
          $(this).attr('src', src_prefix + $(this).attr('src'));
        }
      });
    }
    if ($('#chatArea .message:last .plain pre img.emoji').length) {
      $('#chatArea .message:last .plain pre img.emoji').each(function() {
        $(this).replaceWith('');
      });
    }
    text = $('#chatArea .message:last .plain pre').html();

  // Handle image message
  } else if ($('#chatArea .message:last .picture .msg-img').length &&
  /^\//.test($('#chatArea .message:last .picture .msg-img').attr('src'))) {
    img = src_prefix + $('#chatArea .message:last .picture .msg-img').attr('src').split('&type=slave')[0];

  // Handle custom_emoji message
  } else if ($('#chatArea .message:last .emoticon .custom_emoji').length) {
    img = src_prefix + $('#chatArea .message:last .custom_emoji').attr('src');

  // Capture nothing
  } else {
    return null;
  }

  // Make message
  var msg = {
    id: id,
    user: {
      name: $('#chatArea .message:last .avatar').attr('title'),
      avatar: src_prefix + $('#chatArea .message:last .avatar').attr('src')
    },
    content: {
      text: text,
      image: img
    }
  };
  return msg;
}
<commit_msg>Refactor weixin.js<commit_after>var port = self.port;
var src_pref, msg_sent;

// Whether is the target page
if (document.getElementById('chatArea')) {
  // Initialize
  src_pref = window.location.protocol + '//' + window.location.host;
  msg_sent = new Array(20);

  // Send heartbeats
  setInterval(function() {
    if ($('#chatArea').css('visibility') == 'visible') {
      port.emit('heartbeat', true);
    }
  }, 3000);

  // Listen to DOM mutations
  new MutationObserver(function() {
    console.log(captureMessage());
  }).observe(document.getElementById('chatArea'), { 'childList': true, 'subtree': true });
}

/*
  An typical message looks like:

  {
    id: '',
    user: { name: '',  avatar: '' },
    content: { text: '', image: '' }
  };
*/

function captureMessage() {
  var id, text = '', img = '';

  // Check message id
  id = $.parseJSON($('#chatArea .message:last > .content > .ng-scope[data-cm]').attr('data-cm')).msgId;
  for (i = 0; i < msg_sent.length; i++) {
    if (msg_sent[i] == id) {
      return null;
    }
  }

  // Handle text message
  if ($('#chatArea .message:last .plain').length) {
    if ($('#chatArea .message:last .plain pre img').length) {
      $('#chatArea .message:last .plain pre img').each(function() {
        if (/^\//.test($(this).attr('src'))) {
          $(this).attr('src', src_pref + $(this).attr('src'));
        }
      });
    }
    if ($('#chatArea .message:last .plain pre img.emoji').length) {
      $('#chatArea .message:last .plain pre img.emoji').each(function() {
        $(this).replaceWith('');
      });
    }
    text = $('#chatArea .message:last .plain pre').html();

  // Handle image message
  } else if ($('#chatArea .message:last .picture .msg-img').length &&
  /^\//.test($('#chatArea .message:last .picture .msg-img').attr('src'))) {
    img = src_pref + $('#chatArea .message:last .picture .msg-img').attr('src').split('&type=slave')[0];

  // Handle custom_emoji message
  } else if ($('#chatArea .message:last .emoticon .custom_emoji').length) {
    img = src_pref + $('#chatArea .message:last .custom_emoji').attr('src');

  // Capture nothing
  } else {
    return null;
  }

  // Make message
  var msg = {
    id: id,
    user: {
      name: $('#chatArea .message:last .avatar').attr('title'),
      avatar: src_pref + $('#chatArea .message:last .avatar').attr('src')
    },
    content: {
      text: text,
      image: img
    }
  };

  // Send
  port.emit('bullet', msg);
  msg_sent.shift();
  msg_sent.push(msg.id);

  return msg;
}
<|endoftext|>


<commit_before>goog.provide('clulib.cm.ComponentNode');

goog.require('goog.crypt.base64');
goog.require('goog.structs.Map');

/**
 * You should never use this class directly!
 * Look into [clulib.cm.Component] and [clulib.cm.ComponentManager] instead!
 *
 * @param {clulib.cm.ComponentManager} manager
 * @param {Element} element
 * @constructor
 */
clulib.cm.ComponentNode = function (manager, element) {
  /**
   * @type {clulib.cm.ComponentManager}
   * @private
   */
  this.manager_ = manager;

  /**
   * @type {Element}
   * @private
   */
  this.element_ = element;

  /**
   * @type {?clulib.cm.Component}
   * @private
   */
  this.component_ = null;

  /**
   * @type {?string}
   * @private
   */
  this.id_ = null;

  /**
   * @type {clulib.cm.ComponentNode}
   * @private
   */
  this.parent_ = null;

  /**
   * @type {goog.structs.Map<string, clulib.cm.ComponentNode>}
   * @private
   */
  this.children_ = new goog.structs.Map();

  /**
   * @type {number}
   * @private
   */
  this.depth_ = 0;

  /**
   * @type {string}
   * @private
   */
  this.type_ = this.element_.getAttribute(this.manager_.getTypeAttribute());

  /**
   * @type {?string}
   * @private
   */
  this.config_ = this.element_.getAttribute(this.manager_.getConfigAttribute());
};

/**
 * @param {clulib.cm.ComponentNode} node
 */
clulib.cm.ComponentNode.prototype.addChild = function (node) {
  node.parent_ = this;
  this.children_.set(node.getId(), node);
  this.component_.addChild(node.component_);
  node.setDepth(this.depth_ + 1);
};

/**
 * @param {number} depth
 */
clulib.cm.ComponentNode.prototype.setDepth = function (depth) {
  this.depth_ = depth;
  this.children_.forEach(node => node.setDepth(this.depth_ + 1));
};

/**
 * @returns {?clulib.cm.Component}
 */
clulib.cm.ComponentNode.prototype.getComponent = function () {
  return this.component_;
};

/**
 * @returns {number}
 */
clulib.cm.ComponentNode.prototype.getDepth = function () {
  return this.depth_;
};

/**
 * @returns {Element}
 */
clulib.cm.ComponentNode.prototype.getElement = function () {
  return this.element_;
};

/**
 * @returns {string}
 */
clulib.cm.ComponentNode.prototype.getType = function () {
  return this.type_;
};

/**
 * @returns {?string}
 */
clulib.cm.ComponentNode.prototype.getId = function () {
  return this.id_;
};

/**
 * @param {function(new:clulib.cm.Component)} constructor
 */
clulib.cm.ComponentNode.prototype.instantiate = function (constructor) {
  this.component_ = new constructor();

  this.id_ = this.component_.getId();
  this.element_.setAttribute(this.manager_.getIdAttribute(), this.id_);

  this.component_.manager = this.manager_;
  if (this.config_ != null)
    this.component_.setModel(JSON.parse(goog.crypt.base64.decodeString(this.config_)));
};

/**
 * @returns {Promise<clulib.cm.Component>}
 */
clulib.cm.ComponentNode.prototype.initialize = function () {
  this.component_.decorate(this.element_);
  return this.component_.waitFor()
      .then(() => this.component_.onInit());
};

clulib.cm.ComponentNode.prototype.dispose = function () {
  if (this.parent_ != null)
    this.parent_.children_.remove(this.getId());
};<commit_msg>move component.decorate() to instantiation phase<commit_after>goog.provide('clulib.cm.ComponentNode');

goog.require('goog.crypt.base64');
goog.require('goog.structs.Map');

/**
 * You should never use this class directly!
 * Look into [clulib.cm.Component] and [clulib.cm.ComponentManager] instead!
 *
 * @param {clulib.cm.ComponentManager} manager
 * @param {Element} element
 * @constructor
 */
clulib.cm.ComponentNode = function (manager, element) {
  /**
   * @type {clulib.cm.ComponentManager}
   * @private
   */
  this.manager_ = manager;

  /**
   * @type {Element}
   * @private
   */
  this.element_ = element;

  /**
   * @type {?clulib.cm.Component}
   * @private
   */
  this.component_ = null;

  /**
   * @type {?string}
   * @private
   */
  this.id_ = null;

  /**
   * @type {clulib.cm.ComponentNode}
   * @private
   */
  this.parent_ = null;

  /**
   * @type {goog.structs.Map<string, clulib.cm.ComponentNode>}
   * @private
   */
  this.children_ = new goog.structs.Map();

  /**
   * @type {number}
   * @private
   */
  this.depth_ = 0;

  /**
   * @type {string}
   * @private
   */
  this.type_ = this.element_.getAttribute(this.manager_.getTypeAttribute());

  /**
   * @type {?string}
   * @private
   */
  this.config_ = this.element_.getAttribute(this.manager_.getConfigAttribute());
};

/**
 * @param {clulib.cm.ComponentNode} node
 */
clulib.cm.ComponentNode.prototype.addChild = function (node) {
  node.parent_ = this;
  this.children_.set(node.getId(), node);
  this.component_.addChild(node.component_);
  node.setDepth(this.depth_ + 1);
};

/**
 * @param {number} depth
 */
clulib.cm.ComponentNode.prototype.setDepth = function (depth) {
  this.depth_ = depth;
  this.children_.forEach(node => node.setDepth(this.depth_ + 1));
};

/**
 * @returns {?clulib.cm.Component}
 */
clulib.cm.ComponentNode.prototype.getComponent = function () {
  return this.component_;
};

/**
 * @returns {number}
 */
clulib.cm.ComponentNode.prototype.getDepth = function () {
  return this.depth_;
};

/**
 * @returns {Element}
 */
clulib.cm.ComponentNode.prototype.getElement = function () {
  return this.element_;
};

/**
 * @returns {string}
 */
clulib.cm.ComponentNode.prototype.getType = function () {
  return this.type_;
};

/**
 * @returns {?string}
 */
clulib.cm.ComponentNode.prototype.getId = function () {
  return this.id_;
};

/**
 * @param {function(new:clulib.cm.Component)} constructor
 */
clulib.cm.ComponentNode.prototype.instantiate = function (constructor) {
  this.component_ = new constructor();

  this.id_ = this.component_.getId();
  this.element_.setAttribute(this.manager_.getIdAttribute(), this.id_);

  this.component_.manager = this.manager_;
  if (this.config_ != null)
    this.component_.setModel(JSON.parse(goog.crypt.base64.decodeString(this.config_)));

  this.component_.decorate(this.element_);
};

/**
 * @returns {Promise<clulib.cm.Component>}
 */
clulib.cm.ComponentNode.prototype.initialize = function () {
  return this.component_.waitFor()
      .then(() => this.component_.onInit());
};

clulib.cm.ComponentNode.prototype.dispose = function () {
  if (this.parent_ != null)
    this.parent_.children_.remove(this.getId());
};<|endoftext|>


<commit_before>var port = self.port;
var src_pref, msg_sent;

// Whether is the target page
if (document.getElementById('chatArea')) {
  // Initialize
  src_pref = window.location.protocol + '//' + window.location.host;
  msg_sent = new Array(20);

  // Send heartbeats
  setInterval(function() {
    if ($('#chatArea').css('visibility') == 'visible') {
      port.emit('heartbeat', true);
    }
  }, 3000);

  // Listen to DOM mutations
  new MutationObserver(function() {
    console.log(captureMessage());
  }).observe(document.getElementById('chatArea'), { 'childList': true, 'subtree': true });
}

/*
  An typical message looks like:

  {
    id: '',
    user: { name: '',  avatar: '' },
    content: { text: '', image: '' }
  };
*/

function captureMessage() {
  var id, text = '', img = '';

  // Check message id
  id = $.parseJSON($('#chatArea .message:last > .content > .ng-scope[data-cm]').attr('data-cm')).msgId;
  for (i = 0; i < msg_sent.length; i++) {
    if (msg_sent[i] == id) {
      return null;
    }
  }

  // Handle text message
  if ($('#chatArea .message:last .plain').length) {
    if ($('#chatArea .message:last .plain pre img').length) {
      $('#chatArea .message:last .plain pre img').each(function() {
        if (/^\//.test($(this).attr('src'))) {
          $(this).attr('src', src_pref + $(this).attr('src'));
        }
      });
    }
    if ($('#chatArea .message:last .plain pre img.emoji').length) {
      $('#chatArea .message:last .plain pre img.emoji').each(function() {
        $(this).replaceWith('');
      });
    }
    text = $('#chatArea .message:last .plain pre').html();

  // Handle image message
  } else if ($('#chatArea .message:last .picture .msg-img').length &&
  /^\//.test($('#chatArea .message:last .picture .msg-img').attr('src'))) {
    img = src_pref + $('#chatArea .message:last .picture .msg-img').attr('src').split('&type=slave')[0];

  // Handle custom_emoji message
  } else if ($('#chatArea .message:last .emoticon .custom_emoji').length) {
    img = src_pref + $('#chatArea .message:last .custom_emoji').attr('src');

  // Capture nothing
  } else {
    return null;
  }

  // Make message
  var msg = {
    id: id,
    user: {
      name: $('#chatArea .message:last .avatar').attr('title'),
      avatar: src_pref + $('#chatArea .message:last .avatar').attr('src')
    },
    content: {
      text: text,
      image: img
    }
  };

  // Send
  port.emit('bullet', msg);
  msg_sent.shift();
  msg_sent.push(msg.id);

  return msg;
}
<commit_msg>Close #18<commit_after>var port = self.port;
var src_pref, msg_sent;

// Whether is the target page
if (document.getElementById('chatArea')) {
  // Initialize
  src_pref = window.location.protocol + '//' + window.location.host;
  msg_sent = new Array(20);

  // Send heartbeats
  setInterval(function() {
    if ($('#chatArea').css('visibility') == 'visible') {
      port.emit('heartbeat', true);
    }
  }, 3000);

  // Listen to DOM mutations
  new MutationObserver(function() {
    captureMessage();
  }).observe(document.getElementById('chatArea'), { 'childList': true, 'subtree': true });
}

/*
  An typical message looks like:

  {
    id: '',
    user: { name: '',  avatar: '' },
    content: { text: '', image: '' }
  };
*/

function captureMessage() {
  var id, text = '', img = '';

  // Check message id
  id = $.parseJSON($('#chatArea .message:last > .content > .ng-scope[data-cm]').attr('data-cm')).msgId;
  for (i = 0; i < msg_sent.length; i++) {
    if (msg_sent[i] == id) {
      return null;
    }
  }

  // HACK for issue #18
  if (id.length < 18) {
    return null;
  }

  // Handle text message
  if ($('#chatArea .message:last .plain').length) {
    if ($('#chatArea .message:last .plain pre img').length) {
      $('#chatArea .message:last .plain pre img').each(function() {
        if (/^\//.test($(this).attr('src'))) {
          $(this).attr('src', src_pref + $(this).attr('src'));
        }
      });
    }
    if ($('#chatArea .message:last .plain pre img.emoji').length) {
      $('#chatArea .message:last .plain pre img.emoji').each(function() {
        $(this).replaceWith('');
      });
    }
    text = $('#chatArea .message:last .plain pre').html();

  // Handle image message
  } else if ($('#chatArea .message:last .picture .msg-img').length &&
  /^\//.test($('#chatArea .message:last .picture .msg-img').attr('src'))) {
    img = src_pref + $('#chatArea .message:last .picture .msg-img').attr('src').split('&type=slave')[0];

  // Handle custom_emoji message
  } else if ($('#chatArea .message:last .emoticon .custom_emoji').length) {
    img = src_pref + $('#chatArea .message:last .custom_emoji').attr('src');

  // Capture nothing
  } else {
    return null;
  }

  // Make message
  var msg = {
    id: id,
    user: {
      name: $('#chatArea .message:last .avatar').attr('title'),
      avatar: src_pref + $('#chatArea .message:last .avatar').attr('src')
    },
    content: {
      text: text,
      image: img
    }
  };

  // Send
  port.emit('bullet', msg);
  msg_sent.shift();
  msg_sent.push(msg.id);

  return msg;
}
<|endoftext|>


<commit_before>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("graceful-fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, locales;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverse) {

    let direction = reverse ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // check for undefined or null values which mess up the camparision
        if ((typeof aValue === "undefined" || aValue === null) && (typeof bValue === "undefined" || bValue === null)) {
            return 0;
        } else if (typeof aValue === "undefined" || aValue === null) {
            return -1 * direction;
        } else if (typeof bValue === "undefined" || bValue === null) {
            return 1 * direction;
        } else {

            // compare values and apply direction
            if (aValue > bValue) {
                return 1 * direction;
            } else if (aValue < bValue) {
                return -1 * direction;
            } else {
                return 0;
            }

        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                attribute = attribute.trim();

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            items.sort(function (a, b) {

                // create a list of comparison functions
                let comparisons = attributes.map(attribute => {
                    return compareObjectsByValueAtPath(attribute.field, attribute.reverse);
                });

                // call the comparison functions in order until one of them returns something other than 0 (which means equal)
                return comparisons.reduce((acc, v) => {
                    return acc || v(a,b);
                }, 0);

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

class Client {
    constructor(params) {

        this.currentSpace = params.space;
        this.clientResolveLinks = params.resolveLinks;
        this.localCachePath = params.localPath;

    }
    /**
     * Gets the Space which the client is currently configured to use
     * @memberof ContentfulClientAPI
     * @return {Promise<Entities.Space>} Promise for a Space
     * @example
     * client.getSpace()
     * .then(space => console.log(space))
     */
    getSpace () {
        return new Promise((resolve, reject) => {

            let file = path.resolve(this.localCachePath, this.currentSpace, "space.json");

            fs.readFile(file, "utf8", function (err, data) {
                if (err) {
                    reject(err);
                } else {
                    resolve(JSON.parse(data));
                }
            });

        });
    }

    /**
     * Gets a Content Type
     * @memberof ContentfulClientAPI
     * @param  {string} id
     * @return {Promise<Entities.ContentType>} Promise for a Content Type
     * @example
     * client.getContentType('contentTypeId')
     * .then(contentType => console.log(contentType))
     */
    getContentType () {
        throw new Error("Not implemented.");
    }

    /**
     * Gets a collection of Content Types
     * @memberof ContentfulClientAPI
     * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
     * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
     * @example
     * client.getContentTypes()
     * .then(contentTypes => console.log(contentTypes.items))
     */
    getContentTypes () {
        throw new Error("Not implemented.");
    }

    /**
     * Gets an Entry
     * @memberof ContentfulClientAPI
     * @param  {string} id
     * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
     * @return {Promise<Entities.Entry>} Promise for an Entry
     * @example
     * client.getEntry('entryId')
     * .then(entry => console.log(entry))
     */
    getEntry (id, query = {}) {
        return new Promise(resolve => {

            let dir, file;

            query = processQueryProperties(query);

            normalizeSelect(query);

            dir = path.resolve(this.localCachePath, this.currentSpace, "entries");
            if (query.content_type) {

                // limit to content type in query
                dir = path.resolve(dir, query.content_type);
                file = path.join(dir, `${query.content_type}_${id}.json`);

            } else {

                // look in the .all dir
                dir = path.resolve(dir, ".all");
                file = path.join(dir, `${id}.json`);

            }

            resolve(this.readFileFilterLocale(file, query));

        });

    }

    /**
     * Gets a collection of Entries
     * @memberof ContentfulClientAPI
     * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
     * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
     * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
     * @example
     * client.getEntries({content_type: 'contentTypeId'})
     * .then(entries => console.log(entries.items))
     */
    getEntries (query = {}) {

        let dir;

        query = processQueryProperties(query);

        dir = path.resolve(this.localCachePath, this.currentSpace, "entries");
        if (query.content_type) {

            // limit to content type in query
            dir = path.resolve(dir, query.content_type);

        }

        return this.readFilesAndFilter(dir, query);

    }

    getAsset (id, query = {}) {

        query = processQueryProperties(query);

        normalizeSelect(query);
        let dir = path.resolve(this.localCachePath, this.currentSpace, "assets");
        return this.readFileFilterLocale(path.join(dir, `${id}.json`), query);

    }

    getAssets (query = {}) {

        query = processQueryProperties(query);

        normalizeSelect(query);
        let dir = path.resolve(this.localCachePath, this.currentSpace, "assets");

        return this.readFilesAndFilter(dir, query);

    }

    parseEntries () {
        throw new Error("Not implemented.");
    }

    sync () {
        throw new Error("Not implemented.");
    }

    /**
     * unWrap - Contentful JSON to Plain JSON
     * @arg {Object} entry
     * @returns {Object} plain entry
     */
    unWrap (entry) {

        try {
            return Promise.resolve(Client.unWrapSync(entry));
        } catch (err) {
            return Promise.reject(err);
        }

    }

    /**
     * unWrap without a promise
     */
    static unWrapSync (entry) {

        let po;

        if (entry.items) {

            // return collections with thier items unWrapped
            entry.items = Client.unWrapCollectionSync(entry);
            return entry;

        } else if (entry.fields) {

            po = Object.keys(entry.fields).reduce((acc, v) => {

                if (entry.fields[v] && entry.fields[v].sys) {

                    acc[v] = Client.unWrapSync(entry.fields[v]);

                } else {

                    acc[v] = entry.fields[v];

                }

                return acc;

            }, {});

        }

        return po;

    }

    /**
     * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
     * @arg {Object} records
     * @returns {Object} plain objects
     */
    static unWrapCollectionSync (collection) {

        return collection.items.map(Client.unWrapSync);

    }

    /**
     * unWrapCollection - Contentful JSON Collection to Plain JSON Array
     * @arg {Object} records
     * @returns {Promise<Object>} Promise of plain objects
     */
    unWrapCollection (collection) {

        try {
            return Promise.resolve(Client.unWrapCollectionSync(collection));
        } catch (err) {
            return Promise.reject(err);
        }

    }

    /**
     * read file and optionally filter fields by locale
     */
    // TODO filter by query
    readFileFilterLocale (file, query) {
        return new Promise((resolve, reject) => {

            fs.readFile(file, "utf8", (err, data) => {

                if (err) {
                    reject(err);
                } else {

                    let record = JSON.parse(data);

                    this.getSpace(record)
                        .then(space => {
                            return new Promise((resolve) => {
                                // set default locale
                                locales = space.locales;
                                defaultLocale = locales.filter(locale => locale.default)[0];
                                resolve(defaultLocale);
                            });
                        })
                        .then(defaultLocale => {
                            return new Promise((resolve) => {
                                // filter to locale
                                record = filterLocale(record, query.locale || defaultLocale.code);

                                resolve(record);
                            });
                        })
                        .then(record => this.resolveLinks(record, query))
                        .then(resolve)
                        .catch(reject);

                }

            });

        });
    }

    readFilesAndFilter (dir, query) {
        return new Promise((resolve, reject) => {

            glob(path.join(dir, "**/*.json"), (err, files) => {

                if (err) {

                    reject(err);

                } else {

                    if (files && files.length > 0) {

                        Promise.all(
                            files.map(file => this.readFileFilterLocale(file, query))
                        )
                        .then(results => sortItemsByQuery(results, query))
                        .then(results => {
                            resolve({
                                "total": results.length,
                                "skip": 0,
                                "limit": Infinity,
                                "items": results
                            });
                        }).catch(reject);

                    } else {

                        resolve({
                            "total": 0,
                            "skip": 0,
                            "limit": Infinity,
                            "items": []
                        });

                    }

                }

            });

        });
    }

    /**
     * resolveLinks
     */
    resolveLinks (record, query) {
        return new Promise((resolve, reject) => {

            let fieldsWithLinks, promisesForRecords;

            // to avoid recursive loops, resolving links has a limit
            if (query.include > 0) {

                // find links in record fields
                if (record.fields) {

                    // get a list of fields that are links or arrays of links
                    fieldsWithLinks = Object.keys(record.fields).filter(key => {

                        if (Array.isArray(record.fields[key])) {
                            if (record.fields[key][0] && record.fields[key][0].sys) {
                                return record.fields[key][0].sys.type === "Link";
                            }
                        } else {
                            if (record.fields[key] && record.fields[key].sys) {
                                return record.fields[key].sys.type === "Link";
                            }
                        }

                    });

                    // get list of promises for the linked records
                    promisesForRecords = fieldsWithLinks.map(key => {

                        let p, sys;

                        // switch on link type
                        sys = record.fields[key].sys;
                        switch(sys.linkType) {
                        case "Entry":
                            p = this.getEntry(sys.id, { "include": query.include - 1 });
                            break;
                        case "Asset":
                            p = this.getAsset(sys.id, { "include": query.include - 1 });
                            break;
                        default:
                            p = Promise.reject(
                                new Error("Unknown link type: %s", sys.linkType)
                            );
                            break;
                        }

                        return p;

                    });

                    // resolve promises and update record with linked records
                    Promise.all(promisesForRecords).then(linkedRecords => {

                        fieldsWithLinks.forEach(key => {

                            if (Array.isArray(record.fields[key])) {

                                // field value has multiple links
                                record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                            } else {

                                // set the field to the current linked record
                                record.fields[key] = linkedRecords.shift();
                            }

                        });

                        resolve(record);

                    }).catch(reject);

                } else {

                    resolve(record);

                }

            } else {

                resolve(record);

            }

        });
    }
}

function createClient (params = {}) {

    return new Client(params);

}

module.exports = {
    createClient: createClient
};
<commit_msg>updated getEntry() to return undefined when an entry cannot be found<commit_after>/* eslint-disable no-console */
/*
 * module to provide a contentful like SDK to our local cache
 */
"use strict";

const path = require("path");
const fs = require("graceful-fs");
const glob = require("glob");

let clientResolveLinks, defaultLocale, locales;

const resolveLinksLimitMax = 10;

/**
 * sdk relies heavily on sys metadata
 * so we cannot omit the sys property on sdk level
 */
function normalizeSelect (query) {
    if (query.select && !/sys/i.test(query.select)) {
        query.select += ",sys";
    }
}

/**
 * fieldWithLocaleFallback
 */
function fieldWithLocaleFallback (field, value, preferredLocaleCode) {

    let newValue, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    if (locale.code in value) {
        newValue = value[locale.code];
    } else if (locale.fallbackCode) {
        newValue = fieldWithLocaleFallback(field, value, locale.fallbackCode);
    }

    return newValue;

}

/**
 * filter record by locale
 */
// TODO fallback locale list
function filterLocale(record, preferredLocaleCode) {

    let clone, field, locale;

    // get the locale record from the current metadata
    locale = locales.filter(l => l.code === preferredLocaleCode).shift();

    if (!locale) {
        throw new Error("Unknown locale: " + preferredLocaleCode);
    }

    // clone record
    clone = JSON.parse(JSON.stringify(record));

    if (clone.fields) {

        for (field in clone.fields) {
            if (clone.fields.hasOwnProperty(field)) {
                clone.fields[field] = fieldWithLocaleFallback(field, clone.fields[field], locale.code);
            }
        }

    }

    return clone;

}

/**
 * helper fuction to get the value of a nest property of an object by "path"
 * path is a string with property dot notation
 * i.e. "sys.createdAt" or "fields.image.title"
 */
function objectValueByPath(obj, path) {
    return path.split(".").reduce(function(acc, v) {
        return acc ? acc[v] : undefined;
    }, obj);
}

/**
 * sort fuction to sort arrays of objects by the value of the property
 * (possibly nested) refernced by the path string
 */
function compareObjectsByValueAtPath (path, reverse) {

    let direction = reverse ? -1 : 1;

    return function (a, b) {

        // get values
        let aValue = objectValueByPath(a, path);
        let bValue = objectValueByPath(b, path);

        // check for undefined or null values which mess up the camparision
        if ((typeof aValue === "undefined" || aValue === null) && (typeof bValue === "undefined" || bValue === null)) {
            return 0;
        } else if (typeof aValue === "undefined" || aValue === null) {
            return -1 * direction;
        } else if (typeof bValue === "undefined" || bValue === null) {
            return 1 * direction;
        } else {

            // compare values and apply direction
            if (aValue > bValue) {
                return 1 * direction;
            } else if (aValue < bValue) {
                return -1 * direction;
            } else {
                return 0;
            }

        }

    };

}

/**
 * sort item by order property in query
 * @arg {array} items to sort
 * @arg {query} query object
 * @returns {Promise<array>} a promise of an array of items
 */
function sortItemsByQuery (items, query) {
    return new Promise(resolve => {

        let order = query.order;

        if (order) {

            // sort items by order property in query
            // order property document here:
            // https://www.contentful.com/developers/docs/references/content-delivery-api/#/reference/search-parameters/order

            // split up multiple attributes by comma
            // and field name and decending or accending
            let attributes = order.split(",").map(attribute => {

                attribute = attribute.trim();

                if (attribute.charAt(0) === "-") {
                    return {
                        "field": attribute.slice(1),
                        "reverse": true
                    };
                } else {
                    return {
                        "field": attribute,
                        "reverse": false
                    };
                }

            });

            // sort the items by the least specified attribute and repeat
            items.sort(function (a, b) {

                // create a list of comparison functions
                let comparisons = attributes.map(attribute => {
                    return compareObjectsByValueAtPath(attribute.field, attribute.reverse);
                });

                // call the comparison functions in order until one of them returns something other than 0 (which means equal)
                return comparisons.reduce((acc, v) => {
                    return acc || v(a,b);
                }, 0);

            });

            resolve(items);

        } else {

            // no order specified, so just return the items
            resolve(items);

        }

    });
}

/**
 * process query properties
 */
function processQueryProperties (query) {

    if (query.resolveLinks === undefined) {

        // use client setting if unset
        query.resolveLinks = clientResolveLinks;

    }

    if (query.resolveLinks === undefined) {

        // default to true for resolve links
        query.resolveLinks = true;

    }

    if (query.include === undefined) {

        // default to include one level of links
        query.include = 1;

    }

    if (query.include > resolveLinksLimitMax) {

        // limit the maximum number of inclusions
        query.include = resolveLinksLimitMax;

    }

    return query;

}

class Client {
    constructor(params) {

        this.currentSpace = params.space;
        this.clientResolveLinks = params.resolveLinks;
        this.localCachePath = params.localPath;

    }
    /**
     * Gets the Space which the client is currently configured to use
     * @memberof ContentfulClientAPI
     * @return {Promise<Entities.Space>} Promise for a Space
     * @example
     * client.getSpace()
     * .then(space => console.log(space))
     */
    getSpace () {
        return new Promise((resolve, reject) => {

            let file = path.resolve(this.localCachePath, this.currentSpace, "space.json");

            fs.readFile(file, "utf8", function (err, data) {
                if (err) {
                    reject(err);
                } else {
                    resolve(JSON.parse(data));
                }
            });

        });
    }

    /**
     * Gets a Content Type
     * @memberof ContentfulClientAPI
     * @param  {string} id
     * @return {Promise<Entities.ContentType>} Promise for a Content Type
     * @example
     * client.getContentType('contentTypeId')
     * .then(contentType => console.log(contentType))
     */
    getContentType () {
        throw new Error("Not implemented.");
    }

    /**
     * Gets a collection of Content Types
     * @memberof ContentfulClientAPI
     * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
     * @return {Promise<Entities.ContentTypeCollection>} Promise for a collection of Content Types
     * @example
     * client.getContentTypes()
     * .then(contentTypes => console.log(contentTypes.items))
     */
    getContentTypes () {
        throw new Error("Not implemented.");
    }

    /**
     * Gets an Entry
     * @memberof ContentfulClientAPI
     * @param  {string} id
     * @param  {Object=} query - Object with search parameters. In this method it's only useful for `locale`.
     * @return {Promise<Entities.Entry>} Promise for an Entry
     * @example
     * client.getEntry('entryId')
     * .then(entry => console.log(entry))
     */
    getEntry (id, query = {}) {
        return new Promise(resolve => {

            let dir, file;

            query = processQueryProperties(query);

            normalizeSelect(query);

            dir = path.resolve(this.localCachePath, this.currentSpace, "entries");
            if (query.content_type) {

                // limit to content type in query
                dir = path.resolve(dir, query.content_type);
                file = path.join(dir, `${query.content_type}_${id}.json`);

            } else {

                // look in the .all dir
                dir = path.resolve(dir, ".all");
                file = path.join(dir, `${id}.json`);

            }

            resolve(this.readFileFilterLocale(file, query));

        });

    }

    /**
     * Gets a collection of Entries
     * @memberof ContentfulClientAPI
     * @param  {Object=} query - Object with search parameters. Check the JS SDK tutorial and the REST API reference for more details.
     * @param  {boolean=} query.resolveLinks - When true, links to other Entries or Assets are resolved. Default: true.
     * @return {Promise<Entities.EntryCollection>} Promise for a collection of Entries
     * @example
     * client.getEntries({content_type: 'contentTypeId'})
     * .then(entries => console.log(entries.items))
     */
    getEntries (query = {}) {

        let dir;

        query = processQueryProperties(query);

        dir = path.resolve(this.localCachePath, this.currentSpace, "entries");
        if (query.content_type) {

            // limit to content type in query
            dir = path.resolve(dir, query.content_type);

        }

        return this.readFilesAndFilter(dir, query);

    }

    getAsset (id, query = {}) {

        query = processQueryProperties(query);

        normalizeSelect(query);
        let dir = path.resolve(this.localCachePath, this.currentSpace, "assets");
        return this.readFileFilterLocale(path.join(dir, `${id}.json`), query);

    }

    getAssets (query = {}) {

        query = processQueryProperties(query);

        normalizeSelect(query);
        let dir = path.resolve(this.localCachePath, this.currentSpace, "assets");

        return this.readFilesAndFilter(dir, query);

    }

    parseEntries () {
        throw new Error("Not implemented.");
    }

    sync () {
        throw new Error("Not implemented.");
    }

    /**
     * unWrap - Contentful JSON to Plain JSON
     * @arg {Object} entry
     * @returns {Object} plain entry
     */
    unWrap (entry) {

        try {
            return Promise.resolve(Client.unWrapSync(entry));
        } catch (err) {
            return Promise.reject(err);
        }

    }

    /**
     * unWrap without a promise
     */
    static unWrapSync (entry) {

        let po;

        if (entry.items) {

            // return collections with thier items unWrapped
            entry.items = Client.unWrapCollectionSync(entry);
            return entry;

        } else if (entry.fields) {

            po = Object.keys(entry.fields).reduce((acc, v) => {

                if (entry.fields[v] && entry.fields[v].sys) {

                    acc[v] = Client.unWrapSync(entry.fields[v]);

                } else {

                    acc[v] = entry.fields[v];

                }

                return acc;

            }, {});

        }

        return po;

    }

    /**
     * unWrapCollectionSync - Contentful JSON Collection to Plain JSON Array
     * @arg {Object} records
     * @returns {Object} plain objects
     */
    static unWrapCollectionSync (collection) {

        return collection.items.map(Client.unWrapSync);

    }

    /**
     * unWrapCollection - Contentful JSON Collection to Plain JSON Array
     * @arg {Object} records
     * @returns {Promise<Object>} Promise of plain objects
     */
    unWrapCollection (collection) {

        try {
            return Promise.resolve(Client.unWrapCollectionSync(collection));
        } catch (err) {
            return Promise.reject(err);
        }

    }

    /**
     * read file and optionally filter fields by locale
     */
    // TODO filter by query
    readFileFilterLocale (file, query) {
        return new Promise((resolve, reject) => {

            fs.readFile(file, "utf8", (err, data) => {

                if (err) {
                    if (err.code === "ENOENT") {

                        // return undefined when the requested entry cannot be found
                        resolve();

                    } else {
                        reject(err);
                    }
                } else {

                    let record = JSON.parse(data);

                    this.getSpace(record)
                        .then(space => {
                            return new Promise((resolve) => {
                                // set default locale
                                locales = space.locales;
                                defaultLocale = locales.filter(locale => locale.default)[0];
                                resolve(defaultLocale);
                            });
                        })
                        .then(defaultLocale => {
                            return new Promise((resolve) => {
                                // filter to locale
                                record = filterLocale(record, query.locale || defaultLocale.code);

                                resolve(record);
                            });
                        })
                        .then(record => this.resolveLinks(record, query))
                        .then(resolve)
                        .catch(reject);

                }

            });

        });
    }

    readFilesAndFilter (dir, query) {
        return new Promise((resolve, reject) => {

            glob(path.join(dir, "**/*.json"), (err, files) => {

                if (err) {

                    reject(err);

                } else {

                    if (files && files.length > 0) {

                        Promise.all(
                            files.map(file => this.readFileFilterLocale(file, query))
                        )
                        .then(results => sortItemsByQuery(results, query))
                        .then(results => {
                            resolve({
                                "total": results.length,
                                "skip": 0,
                                "limit": Infinity,
                                "items": results
                            });
                        }).catch(reject);

                    } else {

                        resolve({
                            "total": 0,
                            "skip": 0,
                            "limit": Infinity,
                            "items": []
                        });

                    }

                }

            });

        });
    }

    /**
     * resolveLinks
     */
    resolveLinks (record, query) {
        return new Promise((resolve, reject) => {

            let fieldsWithLinks, promisesForRecords;

            // to avoid recursive loops, resolving links has a limit
            if (query.include > 0) {

                // find links in record fields
                if (record.fields) {

                    // get a list of fields that are links or arrays of links
                    fieldsWithLinks = Object.keys(record.fields).filter(key => {

                        if (Array.isArray(record.fields[key])) {
                            if (record.fields[key][0] && record.fields[key][0].sys) {
                                return record.fields[key][0].sys.type === "Link";
                            }
                        } else {
                            if (record.fields[key] && record.fields[key].sys) {
                                return record.fields[key].sys.type === "Link";
                            }
                        }

                    });

                    // get list of promises for the linked records
                    promisesForRecords = fieldsWithLinks.map(key => {

                        let p, sys;

                        // switch on link type
                        sys = record.fields[key].sys;
                        switch(sys.linkType) {
                        case "Entry":
                            p = this.getEntry(sys.id, { "include": query.include - 1 });
                            break;
                        case "Asset":
                            p = this.getAsset(sys.id, { "include": query.include - 1 });
                            break;
                        default:
                            p = Promise.reject(
                                new Error("Unknown link type: %s", sys.linkType)
                            );
                            break;
                        }

                        return p;

                    });

                    // resolve promises and update record with linked records
                    Promise.all(promisesForRecords).then(linkedRecords => {

                        fieldsWithLinks.forEach(key => {

                            if (Array.isArray(record.fields[key])) {

                                // field value has multiple links
                                record.fields[key] = linkedRecords.splice(0, record.fields[key].length);

                            } else {

                                // set the field to the current linked record
                                record.fields[key] = linkedRecords.shift();
                            }

                        });

                        resolve(record);

                    }).catch(reject);

                } else {

                    resolve(record);

                }

            } else {

                resolve(record);

            }

        });
    }
}

function createClient (params = {}) {

    return new Client(params);

}

module.exports = {
    createClient: createClient
};
<|endoftext|>


<commit_before>var fs   = require('fs');
var path = require('path');

// Constant for showing surrounding context, can be set with env variable
var context = parseInt(process.env['COFFEE_BACKTRACE_CONTEXT']);
// We have to allow "0" as a valid context, so we can't just || this part.
if (isNaN(context)) context = 3;
// The time when this script was run, which we can use to test if the source was
// modified or not since being compiled.
var safeTime = new Date();
// Our coffee package
var coffee;

function logErr(msg) {
  process.stderr.write(msg.toString() + '\n');
}

// Color the lines red and highlight one of them. Add > and >> respectively so
// that it is still readable in non-color environments.
function color(lines, highlight) {
  return lines.map(function(line, num){
    // At the end/beginning of file, it could be undefined
    if (typeof line === 'string') {
      if (num === highlight)
        return '\x1b[1;31m>>' + line + '\x1b[0m';
      else
        return '\x1b[31m> ' + line + '\x1b[0m';
    }
  });
}

// Takes a stack and maps each line to a possible CoffeeScript compiled printout
function expandStack(stack) {
  var sources = {};
  return stack.split('\n').slice(1).map(function(loc) {
    // We can't evaluate it if it isn't CoffeeScript
    if (!/\.coffee:\d/.test(loc))
      return null;

    // For the rest, get the file. `match` is formatted as:
    // [match, path, line, col]
    var match = loc.match(/\((.+\.coffee):(\d+):(\d+)/);
    var path = match[1];
    var line = match[2];

    // If we cannot read this off the filesystem for whatever reason, just skip it
    try {
      // Make sure the source was not changed since we started
      if (fs.statSync(path).mtime > safeTime)
        return color(["Source of " + path.basename(path) + " updated; cannot find line"]);
    } catch (err) { return null; }

    // So now we have a reliable source file. Get it and find the line. Again,
    // being (unnecessarily?) careful with the filesystem access functions.
    try {
      line = parseInt(line) - 1;
      if (sources[path] === undefined)
        sources[path] = coffee.compile(fs.readFileSync(path, 'utf8'));

      // Grab the context lines and color them
      var contents = sources[path].split('\n').slice(line - context, line + context + 1);
      return color(contents, context).join('\n');
    } catch (err) { return null; }
  });
}

// Handles a thrown exception by printing a reasonable stack trace
function errorHandler(error) {
  debugger;
  // We absolutely do not want this to error
  var expanded = [];
  try {
    expanded = expandStack(error.stack);
  } catch (err) {
    require('util').debug(err);
  };

  var trace = error.stack.split('\n');
  // Print first line normally
  logErr(trace[0]);
  // For each other line, print with expanded if necessary
  trace.slice(1).forEach(function(stack, i) {
    logErr(stack);
    if (expanded[i])
      logErr(expanded[i]);
  });
  process.exit(1);
}

// This is the tricky part. There are two cases that we must handle. Firstly, if
// you run `coffee <file>` and it throws an uncaught exception, it will bubble
// up to coffee's try-catch block and be printed to the screen. It does trigger
// an event though, but there's a catch: when you run `coffee` it is using
// whichever one it finds on the path, which means that the correct one is not
// necessarily node_modules/coffee-script. So we find the executable and then
// require the corresponding object so we can bind an event to it.
// Note that this should still work fine if it was symlinked.
if (path.basename(process.execPath) == 'coffee') {
  var coffeePath = path.join(process.execPath, '../../lib/coffee-script/coffee-script.js')
  // We don't want to interrupt the start-up process, so let's be kind here.
  try {
    // Get the reference to the correct CoffeeScript object (the one with the
    // EventEmitter attached to it) and bind to failure
    coffee = require(coffeePath);
    coffee.on('failure', errorHandler);
  } catch (err) {
    logErr('coffee-backtrace could not attach listener: ' + err.toString() + '\n');
  }
}

// We also need to listen to node's error handler, as coffee does not catch
// everything. Things that are done, say, in a setTimeout will bubble up to
// node.
process.on('uncaughtException', errorHandler)

// Export settings
module.exports = {
  setContext: function(num) {
    context = num;
    return this;
  }
};
<commit_msg>Remove debugger statement<commit_after>var fs   = require('fs');
var path = require('path');

// Constant for showing surrounding context, can be set with env variable
var context = parseInt(process.env['COFFEE_BACKTRACE_CONTEXT']);
// We have to allow "0" as a valid context, so we can't just || this part.
if (isNaN(context)) context = 3;
// The time when this script was run, which we can use to test if the source was
// modified or not since being compiled.
var safeTime = new Date();
// Our coffee package
var coffee;

function logErr(msg) {
  process.stderr.write(msg.toString() + '\n');
}

// Color the lines red and highlight one of them. Add > and >> respectively so
// that it is still readable in non-color environments.
function color(lines, highlight) {
  return lines.map(function(line, num){
    // At the end/beginning of file, it could be undefined
    if (typeof line === 'string') {
      if (num === highlight)
        return '\x1b[1;31m>>' + line + '\x1b[0m';
      else
        return '\x1b[31m> ' + line + '\x1b[0m';
    }
  });
}

// Takes a stack and maps each line to a possible CoffeeScript compiled printout
function expandStack(stack) {
  var sources = {};
  return stack.split('\n').slice(1).map(function(loc) {
    // We can't evaluate it if it isn't CoffeeScript
    if (!/\.coffee:\d/.test(loc))
      return null;

    // For the rest, get the file. `match` is formatted as:
    // [match, path, line, col]
    var match = loc.match(/\((.+\.coffee):(\d+):(\d+)/);
    var path = match[1];
    var line = match[2];

    // If we cannot read this off the filesystem for whatever reason, just skip it
    try {
      // Make sure the source was not changed since we started
      if (fs.statSync(path).mtime > safeTime)
        return color(["Source of " + path.basename(path) + " updated; cannot find line"]);
    } catch (err) { return null; }

    // So now we have a reliable source file. Get it and find the line. Again,
    // being (unnecessarily?) careful with the filesystem access functions.
    try {
      line = parseInt(line) - 1;
      if (sources[path] === undefined)
        sources[path] = coffee.compile(fs.readFileSync(path, 'utf8'));

      // Grab the context lines and color them
      var contents = sources[path].split('\n').slice(line - context, line + context + 1);
      return color(contents, context).join('\n');
    } catch (err) { return null; }
  });
}

// Handles a thrown exception by printing a reasonable stack trace
function errorHandler(error) {
  // We absolutely do not want this to error
  var expanded = [];
  try {
    expanded = expandStack(error.stack);
  } catch (err) {
    require('util').debug(err);
  };

  var trace = error.stack.split('\n');
  // Print first line normally
  logErr(trace[0]);
  // For each other line, print with expanded if necessary
  trace.slice(1).forEach(function(stack, i) {
    logErr(stack);
    if (expanded[i])
      logErr(expanded[i]);
  });
  process.exit(1);
}

// This is the tricky part. There are two cases that we must handle. Firstly, if
// you run `coffee <file>` and it throws an uncaught exception, it will bubble
// up to coffee's try-catch block and be printed to the screen. It does trigger
// an event though, but there's a catch: when you run `coffee` it is using
// whichever one it finds on the path, which means that the correct one is not
// necessarily node_modules/coffee-script. So we find the executable and then
// require the corresponding object so we can bind an event to it.
// Note that this should still work fine if it was symlinked.
if (path.basename(process.execPath) == 'coffee') {
  var coffeePath = path.join(process.execPath, '../../lib/coffee-script/coffee-script.js')
  // We don't want to interrupt the start-up process, so let's be kind here.
  try {
    // Get the reference to the correct CoffeeScript object (the one with the
    // EventEmitter attached to it) and bind to failure
    coffee = require(coffeePath);
    coffee.on('failure', errorHandler);
  } catch (err) {
    logErr('coffee-backtrace could not attach listener: ' + err.toString() + '\n');
  }
}

// We also need to listen to node's error handler, as coffee does not catch
// everything. Things that are done, say, in a setTimeout will bubble up to
// node.
process.on('uncaughtException', errorHandler)

// Export settings
module.exports = {
  setContext: function(num) {
    context = num;
    return this;
  }
};
<|endoftext|>