<commit_before>/*eslint no-invalid-this: 0*/
/*
 * Maintains a lookup of connected IRC clients. These connections are transient
 * and may be closed for a variety of reasons.
 */
"use strict";
const stats = require("../config/stats");
const log = require("../logging").get("ClientPool");
const Promise = require("bluebird");
const QueuePool = require("../util/QueuePool");

function ClientPool(ircBridge) {
    this._ircBridge = ircBridge;
    // The list of bot clients on servers (not specific users)
    this._botClients = {
        // server_domain: BridgedClient
    };

    // list of virtual users on servers
    this._virtualClients = {
        // server_domain: {
        //    nicks: {
        //      <nickname>: BridgedClient
        //    },
        //    userIds: {
        //      <user_id>: BridgedClient
        //    }
        // }
    }
    // map of numbers of connected clients on each server
    // Counting these is quite expensive because we have to
    // ignore entries where the value is undefined. Instead,
    // just keep track of how many we have.
    this._virtualClientCounts = {
        // server_domain: number
    };

    this._reconnectQueues = {
        // server_domain: QueuePool
    };
}

ClientPool.prototype.killAllClients = function() {
    let domainList = Object.keys(this._virtualClients);
    let clients = [];
    domainList.forEach((domain) => {
        clients = clients.concat(
            Object.keys(this._virtualClients[domain].nicks).map(
                (nick) => this._virtualClients[domain].nicks[nick]
            )
        );

        clients = clients.concat(
            Object.keys(this._virtualClients[domain].userIds).map(
                (userId) => this._virtualClients[domain].userIds[userId]
            )
        );

        clients.push(this._botClients[domain]);
    });

    clients = clients.filter((c) => Boolean(c));

    return Promise.all(
        clients.map(
            (client) => client.kill()
        )
    );
}

ClientPool.prototype.getOrCreateReconnectQueue = function(server) {
    if (server.getConcurrentReconnectLimit() === 0) {
        return null;
    }
    let q = this._reconnectQueues[server.domain];
    if (q === undefined) {
        q = this._reconnectQueues[server.domain] = new QueuePool(
            server.getConcurrentReconnectLimit(),
            (item) => {
                log.info(`Reconnecting client. ${q.waitingItems} left.`);
                return this._reconnectClient(item);
            }
        );
    }
    return q;
};

ClientPool.prototype.setBot = function(server, client) {
    this._botClients[server.domain] = client;
};

ClientPool.prototype.getBot = function(server) {
    return this._botClients[server.domain];
};

ClientPool.prototype.createIrcClient = function(ircClientConfig, matrixUser, isBot) {
    var bridgedClient = this._ircBridge.createBridgedClient(
        ircClientConfig, matrixUser, isBot
    );
    var server = bridgedClient.server;

    if (this._virtualClients[server.domain] === undefined) {
        this._virtualClients[server.domain] = {
            nicks: Object.create(null),
            userIds: Object.create(null)
        };
        this._virtualClientCounts[server.domain] = 0;
    }
    if (isBot) {
        this._botClients[server.domain] = bridgedClient;
    }

    // add event listeners
    bridgedClient.on("client-connected", this._onClientConnected.bind(this));
    bridgedClient.on("client-disconnected", this._onClientDisconnected.bind(this));
    bridgedClient.on("nick-change", this._onNickChange.bind(this));
    bridgedClient.on("join-error", this._onJoinError.bind(this));
    bridgedClient.on("irc-names", this._onNames.bind(this));

    // store the bridged client immediately in the pool even though it isn't
    // connected yet, else we could spawn 2 clients for a single user if this
    // function is called quickly.
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = bridgedClient;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] + 1;

    // Does this server have a max clients limit? If so, check if the limit is
    // reached and start cycling based on oldest time.
    this._checkClientLimit(server);
    return bridgedClient;
};

ClientPool.prototype.getBridgedClientByUserId = function(server, userId) {
    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].userIds[userId];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientByNick = function(server, nick) {
    var bot = this.getBot(server);
    if (bot && bot.nick === nick) {
        return bot;
    }

    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].nicks[nick];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientsForUserId = function(userId) {
    var domainList = Object.keys(this._virtualClients);
    var clientList = [];
    domainList.forEach((domain) => {
        var cli = this._virtualClients[domain].userIds[userId];
        if (cli && !cli.isDead()) {
            clientList.push(cli);
        }
    });
    return clientList;
};

ClientPool.prototype._checkClientLimit = function(server) {
    if (server.getMaxClients() === 0) {
        return;
    }

    var numConnections = this._getNumberOfConnections(server);
    this._sendConnectionMetric(server);

    if (numConnections < server.getMaxClients()) {
        // under the limit, we're good for now.
        log.debug(
            "%s active connections on %s",
            numConnections, server.domain
        );
        return;
    }

    log.debug(
        "%s active connections on %s (limit %s)",
        numConnections, server.domain, server.getMaxClients()
    );

    // find the oldest client to kill.
    var oldest = null;
    Object.keys(this._virtualClients[server.domain].nicks).forEach((nick) => {
        var client = this._virtualClients[server.domain].nicks[nick];
        if (!client) {
            // possible since undefined/null values can be present from culled entries
            return;
        }
        if (client.isBot) {
            return; // don't ever kick the bot off.
        }
        if (oldest === null) {
            oldest = client;
            return;
        }
        if (client.getLastActionTs() < oldest.getLastActionTs()) {
            oldest = client;
        }
    });
    if (!oldest) {
        return;
    }
    // disconnect and remove mappings.
    this._removeBridgedClient(oldest);
    oldest.disconnect("Client limit exceeded: " + server.getMaxClients()).then(
    function() {
        log.info("Client limit exceeded: Disconnected %s on %s.",
            oldest.nick, oldest.server.domain);
    },
    function(e) {
        log.error("Error when disconnecting %s on server %s: %s",
            oldest.nick, oldest.server.domain, JSON.stringify(e));
    });
};

ClientPool.prototype._getNumberOfConnections = function(server) {
    if (!server || !this._virtualClients[server.domain]) { return 0; }
    return this._virtualClientCounts[server.domain];
};

ClientPool.prototype.countTotalConnections = function() {
    var count = 0;

    Object.keys(this._virtualClients).forEach((domain) => {
        let server = this._ircBridge.getServer(domain);
        count += this._getNumberOfConnections(server);
    });

    return count;
};

ClientPool.prototype.totalReconnectsWaiting = function (serverDomain) {
    if (this._reconnectQueues[serverDomain] !== undefined) {
        return this._reconnectQueues[serverDomain].waitingItems;
    }
    return 0;
};

ClientPool.prototype.getIdleConnectionMetrics = function() {
    return new Promise((resolve) => {
        resolve(this._calculateIdleConnectionMetrics());
    });
};

ClientPool.prototype._calculateIdleConnectionMetrics = function(server) {
    const SEVEN_DAYS_MS = 1000 * 60 * 60 * 24 * 7;
    const periodBucketTimes = {
        "7d": Date.now() - SEVEN_DAYS_MS,
        "14d": Date.now() - SEVEN_DAYS_MS*2,
        "21d": Date.now() - SEVEN_DAYS_MS*3,
    };

    const _idleConnectionMetrics = {
        "7d": 0,
        "14d": 0,
        "21d": 0,
    };
    const clients = Object.values(this._virtualClients[server].userIds);
    for (const bridgedClient of clients) {
        const lastActionMS = bridgedClient.getLastActionTs();
        let bucket = null;
        for (const periodBucket in periodBucketTimes) {
            if (lastActionMS > periodBucketTimes[periodBucket]) {
                break;
            }
            bucket = periodBucket;
        }
        if (bucket !== null) {
            _idleConnectionMetrics[bucket]++;
        }
    }
    return _idleConnectionMetrics;
};

ClientPool.prototype._sendConnectionMetric = function(server) {
    stats.ircClients(server.domain, this._getNumberOfConnections(server));
};

ClientPool.prototype._removeBridgedClient = function(bridgedClient) {
    var server = bridgedClient.server;
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = undefined;
    this._virtualClients[server.domain].nicks[bridgedClient.nick] = undefined;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] - 1;

    if (bridgedClient.isBot) {
        this._botClients[server.domain] = undefined;
    }
};

ClientPool.prototype._onClientConnected = function(bridgedClient) {
    var server = bridgedClient.server;
    var oldNick = bridgedClient.nick;
    var actualNick = bridgedClient.unsafeClient.nick;

    // assign a nick to this client
    this._virtualClients[server.domain].nicks[actualNick] = bridgedClient;

    // informative logging
    if (oldNick !== actualNick) {
        log.debug("Connected with nick '%s' instead of desired nick '%s'",
            actualNick, oldNick);
    }
};

ClientPool.prototype._onClientDisconnected = function(bridgedClient) {
    this._removeBridgedClient(bridgedClient);
    this._sendConnectionMetric(bridgedClient.server);

    if (bridgedClient.explicitDisconnect) {
        // don't reconnect users which explicitly disconnected e.g. client
        // cycling, idle timeouts, leaving rooms, etc.
        return;
    }
    // Reconnect this user
    // change the client config to use the current nick rather than the desired nick. This
    // makes sure that the client attempts to reconnect with the *SAME* nick, and also draws
    // from the latest !nick change, as the client config here may be very very old.
    var cliConfig = bridgedClient.getClientConfig();
    cliConfig.setDesiredNick(bridgedClient.nick);

    var cli = this.createIrcClient(
        cliConfig, bridgedClient.matrixUser, bridgedClient.isBot
    );
    var chanList = bridgedClient.chanList;
    // remove ref to the disconnected client so it can be GC'd. If we don't do this,
    // the timeout below holds it in a closure, preventing it from being GC'd.
    bridgedClient = undefined;

    if (chanList.length === 0) {
        log.info(`Dropping ${cli._id} ${cli.nick} because they are not joined to any channels`);
        return;
    }
    let queue = this.getOrCreateReconnectQueue(cli.server);
    if (queue === null) {
        this._reconnectClient({
            cli: cli,
            chanList: chanList,
        });
        return;
    }
    queue.enqueue(cli._id, {
        cli: cli,
        chanList: chanList,
    });
};

ClientPool.prototype._reconnectClient = function(cliChan) {
    const cli = cliChan.cli;
    const chanList = cliChan.chanList;
    return cli.connect().then(() => {
        log.info(
            "<%s> Reconnected %s@%s", cli._id, cli.nick, cli.server.domain
        );
        log.info("<%s> Rejoining %s channels", cli._id, chanList.length);
        chanList.forEach(function(c) {
            cli.joinChannel(c);
        });
        this._sendConnectionMetric(cli.server);
    }, (e) => {
        log.error(
            "<%s> Failed to reconnect %s@%s", cli._id, cli.nick, cli.server.domain
        );
    });
}

ClientPool.prototype._onNickChange = function(bridgedClient, oldNick, newNick) {
    this._virtualClients[bridgedClient.server.domain].nicks[oldNick] = undefined;
    this._virtualClients[bridgedClient.server.domain].nicks[newNick] = bridgedClient;
};

ClientPool.prototype._onJoinError = Promise.coroutine(function*(bridgedClient, chan, err) {
    var errorsThatShouldKick = [
        "err_bannedfromchan", // they aren't allowed in channels they are banned on.
        "err_inviteonlychan", // they aren't allowed in invite only channels
        "err_channelisfull", // they aren't allowed in if the channel is full
        "err_badchannelkey", // they aren't allowed in channels with a bad key
        "err_needreggednick", // they aren't allowed in +r channels if they haven't authed
    ];
    if (errorsThatShouldKick.indexOf(err) === -1) {
        return;
    }
    if (!bridgedClient.userId || bridgedClient.isBot) {
        return; // the bot itself can get these join errors
    }
    // TODO: this is a bit evil, no one in their right mind would expect
    // the client pool to be kicking matrix users from a room :(
    log.info(`Kicking ${bridgedClient.userId} from room due to ${err}`);
    let matrixRooms = yield this._ircBridge.getStore().getMatrixRoomsForChannel(
        bridgedClient.server, chan
    );
    let promises = matrixRooms.map((room) => {
        return this._ircBridge.getAppServiceBridge().getIntent().kick(
            room.getId(), bridgedClient.userId, `IRC error on ${chan}: ${err}`
        );
    });
    yield Promise.all(promises);
});

ClientPool.prototype._onNames = Promise.coroutine(function*(bridgedClient, chan, names) {
    let mls = this._ircBridge.memberListSyncers[bridgedClient.server.domain];
    if (!mls) {
        return;
    }
    yield mls.updateIrcMemberList(chan, names);
});

module.exports = ClientPool;
<commit_msg>We probably don't need to promise wrap this.<commit_after>/*eslint no-invalid-this: 0*/
/*
 * Maintains a lookup of connected IRC clients. These connections are transient
 * and may be closed for a variety of reasons.
 */
"use strict";
const stats = require("../config/stats");
const log = require("../logging").get("ClientPool");
const Promise = require("bluebird");
const QueuePool = require("../util/QueuePool");

function ClientPool(ircBridge) {
    this._ircBridge = ircBridge;
    // The list of bot clients on servers (not specific users)
    this._botClients = {
        // server_domain: BridgedClient
    };

    // list of virtual users on servers
    this._virtualClients = {
        // server_domain: {
        //    nicks: {
        //      <nickname>: BridgedClient
        //    },
        //    userIds: {
        //      <user_id>: BridgedClient
        //    }
        // }
    }
    // map of numbers of connected clients on each server
    // Counting these is quite expensive because we have to
    // ignore entries where the value is undefined. Instead,
    // just keep track of how many we have.
    this._virtualClientCounts = {
        // server_domain: number
    };

    this._reconnectQueues = {
        // server_domain: QueuePool
    };
}

ClientPool.prototype.killAllClients = function() {
    let domainList = Object.keys(this._virtualClients);
    let clients = [];
    domainList.forEach((domain) => {
        clients = clients.concat(
            Object.keys(this._virtualClients[domain].nicks).map(
                (nick) => this._virtualClients[domain].nicks[nick]
            )
        );

        clients = clients.concat(
            Object.keys(this._virtualClients[domain].userIds).map(
                (userId) => this._virtualClients[domain].userIds[userId]
            )
        );

        clients.push(this._botClients[domain]);
    });

    clients = clients.filter((c) => Boolean(c));

    return Promise.all(
        clients.map(
            (client) => client.kill()
        )
    );
}

ClientPool.prototype.getOrCreateReconnectQueue = function(server) {
    if (server.getConcurrentReconnectLimit() === 0) {
        return null;
    }
    let q = this._reconnectQueues[server.domain];
    if (q === undefined) {
        q = this._reconnectQueues[server.domain] = new QueuePool(
            server.getConcurrentReconnectLimit(),
            (item) => {
                log.info(`Reconnecting client. ${q.waitingItems} left.`);
                return this._reconnectClient(item);
            }
        );
    }
    return q;
};

ClientPool.prototype.setBot = function(server, client) {
    this._botClients[server.domain] = client;
};

ClientPool.prototype.getBot = function(server) {
    return this._botClients[server.domain];
};

ClientPool.prototype.createIrcClient = function(ircClientConfig, matrixUser, isBot) {
    var bridgedClient = this._ircBridge.createBridgedClient(
        ircClientConfig, matrixUser, isBot
    );
    var server = bridgedClient.server;

    if (this._virtualClients[server.domain] === undefined) {
        this._virtualClients[server.domain] = {
            nicks: Object.create(null),
            userIds: Object.create(null)
        };
        this._virtualClientCounts[server.domain] = 0;
    }
    if (isBot) {
        this._botClients[server.domain] = bridgedClient;
    }

    // add event listeners
    bridgedClient.on("client-connected", this._onClientConnected.bind(this));
    bridgedClient.on("client-disconnected", this._onClientDisconnected.bind(this));
    bridgedClient.on("nick-change", this._onNickChange.bind(this));
    bridgedClient.on("join-error", this._onJoinError.bind(this));
    bridgedClient.on("irc-names", this._onNames.bind(this));

    // store the bridged client immediately in the pool even though it isn't
    // connected yet, else we could spawn 2 clients for a single user if this
    // function is called quickly.
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = bridgedClient;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] + 1;

    // Does this server have a max clients limit? If so, check if the limit is
    // reached and start cycling based on oldest time.
    this._checkClientLimit(server);
    return bridgedClient;
};

ClientPool.prototype.getBridgedClientByUserId = function(server, userId) {
    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].userIds[userId];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientByNick = function(server, nick) {
    var bot = this.getBot(server);
    if (bot && bot.nick === nick) {
        return bot;
    }

    if (!this._virtualClients[server.domain]) {
        return undefined;
    }
    var cli = this._virtualClients[server.domain].nicks[nick];
    if (!cli || cli.isDead()) {
        return undefined;
    }
    return cli;
};

ClientPool.prototype.getBridgedClientsForUserId = function(userId) {
    var domainList = Object.keys(this._virtualClients);
    var clientList = [];
    domainList.forEach((domain) => {
        var cli = this._virtualClients[domain].userIds[userId];
        if (cli && !cli.isDead()) {
            clientList.push(cli);
        }
    });
    return clientList;
};

ClientPool.prototype._checkClientLimit = function(server) {
    if (server.getMaxClients() === 0) {
        return;
    }

    var numConnections = this._getNumberOfConnections(server);
    this._sendConnectionMetric(server);

    if (numConnections < server.getMaxClients()) {
        // under the limit, we're good for now.
        log.debug(
            "%s active connections on %s",
            numConnections, server.domain
        );
        return;
    }

    log.debug(
        "%s active connections on %s (limit %s)",
        numConnections, server.domain, server.getMaxClients()
    );

    // find the oldest client to kill.
    var oldest = null;
    Object.keys(this._virtualClients[server.domain].nicks).forEach((nick) => {
        var client = this._virtualClients[server.domain].nicks[nick];
        if (!client) {
            // possible since undefined/null values can be present from culled entries
            return;
        }
        if (client.isBot) {
            return; // don't ever kick the bot off.
        }
        if (oldest === null) {
            oldest = client;
            return;
        }
        if (client.getLastActionTs() < oldest.getLastActionTs()) {
            oldest = client;
        }
    });
    if (!oldest) {
        return;
    }
    // disconnect and remove mappings.
    this._removeBridgedClient(oldest);
    oldest.disconnect("Client limit exceeded: " + server.getMaxClients()).then(
    function() {
        log.info("Client limit exceeded: Disconnected %s on %s.",
            oldest.nick, oldest.server.domain);
    },
    function(e) {
        log.error("Error when disconnecting %s on server %s: %s",
            oldest.nick, oldest.server.domain, JSON.stringify(e));
    });
};

ClientPool.prototype._getNumberOfConnections = function(server) {
    if (!server || !this._virtualClients[server.domain]) { return 0; }
    return this._virtualClientCounts[server.domain];
};

ClientPool.prototype.countTotalConnections = function() {
    var count = 0;

    Object.keys(this._virtualClients).forEach((domain) => {
        let server = this._ircBridge.getServer(domain);
        count += this._getNumberOfConnections(server);
    });

    return count;
};

ClientPool.prototype.totalReconnectsWaiting = function (serverDomain) {
    if (this._reconnectQueues[serverDomain] !== undefined) {
        return this._reconnectQueues[serverDomain].waitingItems;
    }
    return 0;
};

ClientPool.prototype.getIdleConnectionMetrics = function(server) {
    const SEVEN_DAYS_MS = 1000 * 60 * 60 * 24 * 7;
    const periodBucketTimes = {
        "7d": Date.now() - SEVEN_DAYS_MS,
        "14d": Date.now() - SEVEN_DAYS_MS*2,
        "21d": Date.now() - SEVEN_DAYS_MS*3,
    };

    const _idleConnectionMetrics = {
        "7d": 0,
        "14d": 0,
        "21d": 0,
    };
    const clients = Object.values(this._virtualClients[server].userIds);
    for (const bridgedClient of clients) {
        const lastActionMS = bridgedClient.getLastActionTs();
        let bucket = null;
        for (const periodBucket in periodBucketTimes) {
            if (lastActionMS > periodBucketTimes[periodBucket]) {
                break;
            }
            bucket = periodBucket;
        }
        if (bucket !== null) {
            _idleConnectionMetrics[bucket]++;
        }
    }
    return _idleConnectionMetrics;
};

ClientPool.prototype._sendConnectionMetric = function(server) {
    stats.ircClients(server.domain, this._getNumberOfConnections(server));
};

ClientPool.prototype._removeBridgedClient = function(bridgedClient) {
    var server = bridgedClient.server;
    this._virtualClients[server.domain].userIds[bridgedClient.userId] = undefined;
    this._virtualClients[server.domain].nicks[bridgedClient.nick] = undefined;
    this._virtualClientCounts[server.domain] = this._virtualClientCounts[server.domain] - 1;

    if (bridgedClient.isBot) {
        this._botClients[server.domain] = undefined;
    }
};

ClientPool.prototype._onClientConnected = function(bridgedClient) {
    var server = bridgedClient.server;
    var oldNick = bridgedClient.nick;
    var actualNick = bridgedClient.unsafeClient.nick;

    // assign a nick to this client
    this._virtualClients[server.domain].nicks[actualNick] = bridgedClient;

    // informative logging
    if (oldNick !== actualNick) {
        log.debug("Connected with nick '%s' instead of desired nick '%s'",
            actualNick, oldNick);
    }
};

ClientPool.prototype._onClientDisconnected = function(bridgedClient) {
    this._removeBridgedClient(bridgedClient);
    this._sendConnectionMetric(bridgedClient.server);

    if (bridgedClient.explicitDisconnect) {
        // don't reconnect users which explicitly disconnected e.g. client
        // cycling, idle timeouts, leaving rooms, etc.
        return;
    }
    // Reconnect this user
    // change the client config to use the current nick rather than the desired nick. This
    // makes sure that the client attempts to reconnect with the *SAME* nick, and also draws
    // from the latest !nick change, as the client config here may be very very old.
    var cliConfig = bridgedClient.getClientConfig();
    cliConfig.setDesiredNick(bridgedClient.nick);

    var cli = this.createIrcClient(
        cliConfig, bridgedClient.matrixUser, bridgedClient.isBot
    );
    var chanList = bridgedClient.chanList;
    // remove ref to the disconnected client so it can be GC'd. If we don't do this,
    // the timeout below holds it in a closure, preventing it from being GC'd.
    bridgedClient = undefined;

    if (chanList.length === 0) {
        log.info(`Dropping ${cli._id} ${cli.nick} because they are not joined to any channels`);
        return;
    }
    let queue = this.getOrCreateReconnectQueue(cli.server);
    if (queue === null) {
        this._reconnectClient({
            cli: cli,
            chanList: chanList,
        });
        return;
    }
    queue.enqueue(cli._id, {
        cli: cli,
        chanList: chanList,
    });
};

ClientPool.prototype._reconnectClient = function(cliChan) {
    const cli = cliChan.cli;
    const chanList = cliChan.chanList;
    return cli.connect().then(() => {
        log.info(
            "<%s> Reconnected %s@%s", cli._id, cli.nick, cli.server.domain
        );
        log.info("<%s> Rejoining %s channels", cli._id, chanList.length);
        chanList.forEach(function(c) {
            cli.joinChannel(c);
        });
        this._sendConnectionMetric(cli.server);
    }, (e) => {
        log.error(
            "<%s> Failed to reconnect %s@%s", cli._id, cli.nick, cli.server.domain
        );
    });
}

ClientPool.prototype._onNickChange = function(bridgedClient, oldNick, newNick) {
    this._virtualClients[bridgedClient.server.domain].nicks[oldNick] = undefined;
    this._virtualClients[bridgedClient.server.domain].nicks[newNick] = bridgedClient;
};

ClientPool.prototype._onJoinError = Promise.coroutine(function*(bridgedClient, chan, err) {
    var errorsThatShouldKick = [
        "err_bannedfromchan", // they aren't allowed in channels they are banned on.
        "err_inviteonlychan", // they aren't allowed in invite only channels
        "err_channelisfull", // they aren't allowed in if the channel is full
        "err_badchannelkey", // they aren't allowed in channels with a bad key
        "err_needreggednick", // they aren't allowed in +r channels if they haven't authed
    ];
    if (errorsThatShouldKick.indexOf(err) === -1) {
        return;
    }
    if (!bridgedClient.userId || bridgedClient.isBot) {
        return; // the bot itself can get these join errors
    }
    // TODO: this is a bit evil, no one in their right mind would expect
    // the client pool to be kicking matrix users from a room :(
    log.info(`Kicking ${bridgedClient.userId} from room due to ${err}`);
    let matrixRooms = yield this._ircBridge.getStore().getMatrixRoomsForChannel(
        bridgedClient.server, chan
    );
    let promises = matrixRooms.map((room) => {
        return this._ircBridge.getAppServiceBridge().getIntent().kick(
            room.getId(), bridgedClient.userId, `IRC error on ${chan}: ${err}`
        );
    });
    yield Promise.all(promises);
});

ClientPool.prototype._onNames = Promise.coroutine(function*(bridgedClient, chan, names) {
    let mls = this._ircBridge.memberListSyncers[bridgedClient.server.domain];
    if (!mls) {
        return;
    }
    yield mls.updateIrcMemberList(chan, names);
});

module.exports = ClientPool;
<|endoftext|>


<commit_before>var api = require('./api');

var ItemRelations = {

		/**
		 * Retrieves one item relation
		 *
		 * @param		integer		item_relation_id		[Description]
		 * @param		function		callback		[Description]
		 *
		 * @return		void
		 */
		__get_item_relations: function (item_relation_id, callback) {
			api.handleRestRequest('get', '/item_relations/' + item_relation_id + '', null, callback);
		},

		/**
		 * Adds an item relation for two items
		 *
		 * @param		object		item_relation_data		[Description]
		 * @param		function		callback		[Description]
		 *
		 * @return		void
		 */
		__post_item_relations: function (item_relation_data, callback) {
			api.handleRestRequest('postJson', '/item_relations', item_relation_data, callback);
		},

		/**
		 * Updates an item relation
		 *
		 * @param		integer		item_relation_id		[Description]
		 * @param		object		item_relation_data		[Description]
		 * @param		function		callback		[Description]
		 *
		 * @return		void
		 */
		__post_item_relations: function (item_relation_id, item_relation_data, callback) {
			api.handleRestRequest('postJson', '/item_relations/' + item_relation_id + '', item_relation_data, callback);
		},

		/**
		 * {Custom comment here}
		 *
		 * @param		integer		item_relation_id		[Description]
		 * @param		function		callback		[Description]
		 *
		 * @return		void
		 */
		__delete_item_relations: function (item_relation_id, callback) {
			api.handleRestRequest('delete', '/item_relations/' + item_relation_id + '', null, callback);
		}

};

module.exports = ItemRelations;
<commit_msg>Item Relationship: Added routes<commit_after>var api = require('./api');

var ItemRelations = {

		/**
		 * Retrieves one item relation
		 *
		 * @param		integer		item_relation_id		ID of an item relationship
		 * @param		function	callback				Callback function
		 *
		 * @return		void
		 */
		get: function (item_relation_id, callback) {
			api.handleRestRequest('get', '/item_relations/' + item_relation_id + '', null, callback);
		},

		/**
		 * Adds an item relation for two items
		 *
		 * @param		object		item_relation_data		Data for a new item relationship
		 * @param		function	callback				Callback function
		 *
		 * @return		void
		 */
		add: function (item_relation_data, callback) {
			api.handleRestRequest('postJson', '/item_relations', item_relation_data, callback);
		},

		/**
		 * Updates an item relation
		 *
		 * @param		integer		item_relation_id		ID of an item relationship
		 * @param		object		item_relation_data		New data for an item relationship
		 * @param		function	callback				Callback function
		 *
		 * @return		void
		 */
		update: function (item_relation_id, item_relation_data, callback) {
			api.handleRestRequest('postJson', '/item_relations/' + item_relation_id + '', item_relation_data, callback);
		},

		/**
		 * {Custom comment here}
		 *
		 * @param		integer		item_relation_id		ID of an item relationship
		 * @param		function	callback				Callback function
		 *
		 * @return		void
		 */
		delete: function (item_relation_id, callback) {
			api.handleRestRequest('delete', '/item_relations/' + item_relation_id + '', null, callback);
		}

};

module.exports = ItemRelations;
<|endoftext|>


<commit_before>var converterFactory = require("./converterFactory");

module.exports = function(name, config){

  preProcessConfig(config);
  /**
   *
   * @param {object} data - Constructor data to populate the new model instance with
   * @param {...string} [tags] - filter constructor data using these tags
   * @constructor
   */
  function JModel(data, tags){
    var self = this,
      internalData = {},
      constructorTagMap = {},
      constructorTagsSupplied = false,
      t;

    if(!data) data = {};

    // Build constructor tag map
    if(arguments.length === 2 && arguments[1]){
      constructorTagsSupplied = true;
      for(t = 1; t < arguments.length; t++){
        constructorTagMap[arguments[t]] = true;
      }
    }

    // Iterate attributes and apply to this instance
    if(config.attributes){
      config.attributes.forEach(function(attribute){
        var enabled = true;
        if(constructorTagsSupplied){
          // Filter tags are in effect so check if this attribute is disabled
          if(!attribute.tags || !attribute.tags.length) return;
          enabled = false;
          attribute.tags.some(function(tag){
            if(constructorTagMap[tag]){
              enabled = true;
              return true;
            }
          });
        }
        if(enabled){
          if(attribute.type !== "function") {
            applyProperty(attribute);
          } else {
            applyHelper(attribute);
          }
        } else {
          applyDisabledProperty(attribute);
        }

      });
    }

    // Call the init method if supplied
    if(config.init) config.init.call(self, data);

    /**
     * Provides access to the models name
     * @returns {string}
     */
    self.getModelName = function(){ return name; };
    /**
     * Provides access to the models configuration
     * @returns {object}
     */
    self.getModelConfig = function(){ return config; };
    /**
     * Provides access to a JSON serialisable object representation of this model instance
     * @param {...string} [tags] - Filter data by these tags
     * @returns {object}
     */
    self.toJSON = function(tags){
      var tagMap, t, filteredData;
      if(!arguments.length || arguments[0] === "") return internalData;
      // Create a tag map for quick access later TODO: worth the initial expense?
      tagMap = {};
      for(t = 0; t < arguments.length; t++){
        tagMap[arguments[t]] = true;
      }
      // Filter data and return
      filteredData = {};
      config.attributes.forEach(function(attribute){
        var include = false;
        if(!attribute.tags || !attribute.tags.length) return;
        attribute.tags.some(function(tag){
          if(tagMap[tag] || constructorTagMap[tag]){
            include = true;
            return true;
          }
        });
        if(include) filteredData[attribute.name] = internalData[attribute.name];
      });
      return filteredData;
    };

    /**
     *
     * @param attribute
     */
    function applyProperty(attribute){
      var converter;

      if(attribute.array) converter = converterFactory.getArrayConverter(attribute.type);
      else converter = converterFactory.getConverter(attribute.type);
      // If no converter was found create a basic converter
      if(!converter) converter = function(value, attribute){ return value; };

      Object.defineProperty(self, attribute.name, {
        get: function(){

          return internalData[attribute.name];
        },
        set: function(value){
          internalData[attribute.name] = converter(value, attribute);
        }
      });

      internalData[attribute.name] = converter(data[attribute.name], attribute);
    }

    /**
     *
     * @param attribute
     */
    function applyDisabledProperty(attribute){
      Object.defineProperty(self, attribute.name, {
        get: function(){ },
        set: function(value){ }
      });
    }

    /**
     *
     * @param attribute
     */
    function applyHelper(attribute){
      self[attribute.name] = attribute.fn.bind(self);
    }
  }

  // Create converter for this model type

  converterFactory.registerConverter(name, function(value, attribute){
    if(!value) return null;
    if(value instanceof JModel) {

      if (value.getModelName() === attribute.type){
        if(attribute.nestedModelTags) return new JModel(value.toJSON(), attribute.nestedModelTags);
        else return value;
      }
      else value = value.toJSON();
    }
    if(typeof value !== "object") return null;
    else return new JModel(value, attribute.nestedModelTags);
  });

  JModel.getModelName = function(){ return name; };

  return JModel;

  function preProcessConfig(config){
    if(!config.attributes){
      config.attributes = [];
      return;
    }
    config.attributes = config.attributes.map(function(attribute){
      var processedAttribute = {
        name: getPropertyName(attribute),
        type: getPropertyType(attribute)
      };
      if(typeof attribute === "object"){
        // Copy options to new attribute config object
        Object.keys(attribute).forEach(function(key){
          if(key !== "name" && key !== "type")
            processedAttribute[key] = attribute[key];
        })
      }
      if(attribute.array) processedAttribute.array = true;
      else if(attribute.type) processedAttribute.array = Array.isArray(attribute.type);
      return processedAttribute;
    });
  }

  function getPropertyName(attribute){
    if(typeof attribute === "string") return attribute;
    else if(attribute.name) return attribute.name;
    else throw new Error("Missing attribute name");
  }

  function getPropertyType(attribute){
    var type,
      typeType;

    if(typeof attribute === "string") return "default";

    type = attribute.type;

    if(Array.isArray(type)) type = type[0];

    typeType = typeof type;

    if(typeType === "string") return type;
    else if(typeType === "function"){
      if(type.name){
        if(type.name === "JModel") return type.getModelName();
        else return type.name.toLowerCase();
      }
      else return "default";
    }

  }

};

<commit_msg>Removed internal model tag logic. All tag filtering functionality provided by separate module<commit_after>var converterFactory = require("./converterFactory");

module.exports = function(name, config){

  preProcessConfig(config);
  /**
   *
   * @param {object} data - Constructor data to populate the new model instance with
   * @param {...string} [tags] - filter constructor data using these tags
   * @constructor
   */
  function JModel(data){
    var self = this,
      internalData = {};

    if(!data) data = {};

    // Iterate attributes and apply to this instance
    if(config.attributes){
      config.attributes.forEach(function(attribute){
        if(attribute.type !== "function") {
          applyProperty(attribute);
        } else {
          applyHelper(attribute);
        }
      });
    }

    // Call the init method if supplied
    if(config.init) config.init.call(self, data);

    /**
     * Provides access to the models name
     * @returns {string}
     */
    self.getModelName = function(){ return name; };
    /**
     * Provides access to the models configuration
     * @returns {object}
     */
    self.getModelConfig = function(){ return config; };

    /**
     *
     * @param attribute
     */
    function applyProperty(attribute){
      var converter;

      if(attribute.array) converter = converterFactory.getArrayConverter(attribute.type);
      else converter = converterFactory.getConverter(attribute.type);
      // If no converter was found create a basic converter
      if(!converter) converter = function(value, attribute){ return value; };

      Object.defineProperty(self, attribute.name, {
        get: function(){

          return internalData[attribute.name];
        },
        set: function(value){
          internalData[attribute.name] = converter(value, attribute);
        }
      });

      internalData[attribute.name] = converter(data[attribute.name], attribute);
    }

    /**
     *
     * @param attribute
     */
    function applyHelper(attribute){
      self[attribute.name] = attribute.fn.bind(self);
    }
  }

  // Create converter for this model type

  converterFactory.registerConverter(name, function(value, attribute){
    if(!value) return null;
    if(value instanceof JModel) {

      if (value.getModelName() === attribute.type){
        if(attribute.nestedModelTags) return new JModel(value.toJSON(), attribute.nestedModelTags);
        else return value;
      }
      else value = value.toJSON();
    }
    if(typeof value !== "object") return null;
    else return new JModel(value, attribute.nestedModelTags);
  });

  JModel.getModelName = function(){ return name; };

  return JModel;

  function preProcessConfig(config){
    if(!config.attributes){
      config.attributes = [];
      return;
    }
    config.attributes = config.attributes.map(function(attribute){
      var processedAttribute = {
        name: getPropertyName(attribute),
        type: getPropertyType(attribute)
      };
      if(typeof attribute === "object"){
        // Copy options to new attribute config object
        Object.keys(attribute).forEach(function(key){
          if(key !== "name" && key !== "type")
            processedAttribute[key] = attribute[key];
        })
      }
      if(attribute.array) processedAttribute.array = true;
      else if(attribute.type) processedAttribute.array = Array.isArray(attribute.type);
      return processedAttribute;
    });
  }

  function getPropertyName(attribute){
    if(typeof attribute === "string") return attribute;
    else if(attribute.name) return attribute.name;
    else throw new Error("Missing attribute name");
  }

  function getPropertyType(attribute){
    var type,
      typeType;

    if(typeof attribute === "string") return "default";

    type = attribute.type;

    if(Array.isArray(type)) type = type[0];

    typeType = typeof type;

    if(typeType === "string") return type;
    else if(typeType === "function"){
      if(type.name){
        if(type.name === "JModel") return type.getModelName();
        else return type.name.toLowerCase();
      }
      else return "default";
    }

  }

};

<|endoftext|>


<commit_before>import Ember from "ember";
import ENV from '../config/environment';

export default Ember.Component.extend({
  isEditing: false,
  isEditingParameter: false,
  tagName: 'td',
  isParameter: Ember.computed.equal('model.currentType', 'parameter'),

  isEnum: function() {
    return this.get('model.variant.ident') === 'enum';
  }.property('model.variant'),

  isBool: function() {
    return this.get('model.variant.ident') === 'bool';
  }.property('model.variant'),

  isTrigger: function() {
    return this.get('model.variant.ident') === 'trigger';
  }.property('model.variant'),

  isMatrix: function() {
    return this.get('model.variant.ident') === 'matrix';
  }.property('model.variant'),

  isFile: function() {
    return this.get('model.variant.ident') === 'image' ||
      this.get('model.variant.ident') === 'file';
  }.property('model.variant'),

  timedOut: function() {
    return this.get('model.state') === 'timedOut';
  }.property('model.state'),

  current: function() {
    return this.get('model.state') === 'current';
  }.property('model.state'),

  offerReload: function() {
    if (this.get('model.behavior') === 'push') {
      return false;
    }
    if (this.get('model.behavior') === 'pull') {
      return true;
    }
    if (this.get('model.originalType') === 'output') {
      return true;
    }

    return this.get('timedOut') || this.get('accessFailed');
  }.property('timedOut', 'accessFailed', 'model.behavior'),

  offerDownload: Ember.computed.equal('model.variant.ident', 'file'),

  url: function() {
    if (this.get('model.variant.ident') !== 'file')
    {
      return '';
    }

    if (ENV.APP.API_HOST) {
      return ENV.APP.API_HOST + '/temp/' + this.get('model.value.name');
    } else {
      var location =  window.location.protocol + '//' + window.location.host;
      return location + '/temp/' + this.get('model.value.name');
    }
  }.property('model.value'),

  writable: function() {
    var variant = this.get('model.variant.ident');
    var knownTypes = ['string', 'enum', 'int', 'float', 'bool', 'trigger',
                      'matrix', 'image', 'file'];
    if (! knownTypes.contains(variant)) {
      return false;
    }

    switch(this.get('model.access')) {
    case 'full':
      return true;
    case 'inactive':
      return ! this.get('model.operator.stream.active');
    default:
      return false;
    }
  }.property('model.access', 'model.current', 'model.variant', 'model.operator.stream.active'),

  accessFailed: function() {
    return this.get('model.state') === 'accessFailed';
  }.property('model.state'),

  fileName: Ember.computed('model.value', {
    get: function() {
      return '';
    },
    set: function(key, value) {
      var strippedName = value.replace(/^.*[\\\/]/, '');
      var v = this.get('model.value');
      v.name = strippedName;
      this.set('model.value', v);
    }
  }),

  isConnector: function() {
    return this.get('model.originalType') !== 'parameter';
  }.property('model.originalType'),

  originalType: function() {
    switch (this.get('model.originalType'))
    {
      case 'input':
        return 'Input';
      case 'output':
        return 'Output';
      default:
        return '';
    }
  }.property('model.originalType'),

  editValue: Ember.computed('model.value', 'model.variant', {
    get: function() {
      var value = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'float':
          return value ? this.get('model.value').toPrecision(3) : 0.0;
        case 'int':
        case 'string':
        case 'enum':
          return value ? this.get('model.value') : '';
        default:
          return '';
      }
    },
    set: function(key, value) {
      var v = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'int':
          v = parseInt(value, 10);
          if (isNaN(v)) {
            return value;
          }
          break;
        case 'float':
          v = parseFloat(value);
          if (isNaN(v)) {
            return value;
          }
          break;
        case 'string':
          v = value;
          break;
        case 'image':
          v = {
            values: value
          };
          break;
        case 'file':
          v.content = value;
          break;
        case 'enum':
          v = parseInt(value);
          break;
        case 'bool':
          v = value === 'true';
          break;
        default:
      }

      this.set('model.value', v);
      return v;
    }
  }),

  isTrue: Ember.computed.alias('model.value'),
  isFalse: Ember.computed.not('model.value'),
  isInput: Ember.computed.equal('model.originalType', 'input'),
  isPushParameter: Ember.computed('model.behavior', {
    get: function() {
      return this.get('model.behavior') === 'push';
    },
    set: function(key, value) {
      this.set('model.behavior', value ? 'push' : 'persistent');
      return value;
    }
  }),

  isPullParameter: Ember.computed('model.behavior', {
    get: function() {
      return this.get('model.behavior') === 'pull';
    },
    set: function(key, value) {
      this.set('model.behavior', value ? 'pull' : 'persistent');
      return value;
    }
  }),

  displayValue: Ember.computed('model.value', 'model.variant', 'isEditing', {
    set: function(key, newValue) {
      return newValue;
    },
    get: function() {
      if (! this.get('current')) {
        return '';
      }

      var value = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'float':
          return value.toPrecision(3);
        case 'enum':
          return this.updateEnumTitle(value);
        case 'bool':
          return value ? 'True' : 'False';
        case 'matrix':
          return value ? value.rows + ' x ' + value.cols + ' matrix' : '';
        case 'image':
          return value ? value.width + ' x ' + value.height + ' image' : '';
        case 'trigger':
          return 'Trigger';
        case 'file':
          return value['name'];
        default:
          return value;
      }
    }
  }),

  // cf. http://stackoverflow.com/q/20623027
  updateEnumTitle: function(enumValue) {
    var value = enumValue;
    var title = this.get('model.descriptions').then( function(value){
      return value.find( function(item) {
         return item.get('value') === enumValue;
      });
    }).then( function(obj) {
      return obj.get('title');
    });

    var _this = this;
    title.then( function(title) {
      _this.set('displayValue', title);
      value = title;
    });

    return value;
  },

  save: function() {
    var model = this.get('model');
    return model.save().catch(function() {
      model.rollbackAttributes();
    });
  },

  actions: {
    editValue: function() {
      this.set('isEditing', true);
    },
    saveChanges: function() {
      this.set('isEditing', false);
      this.save();
    },
    discardChanges: function() {
      this.set('isEditing', false);
      this.set('isEditingParameter', false);
      this.get('model').rollbackAttributes();
    },
    reload: function() {
      this.get('model').reload();
    },
    trigger: function() {
      this.set('isEditing', false);
      this.set('model.value', 1);
      this.save();
    },
    editParameter: function() {
      this.set('isEditingParameter', true);
    },
    saveParameterChanges: function() {
      this.set('isEditingParameter', false);
      var op = this.get('model.operator');
      this.save().then(function() {
        op.then(function(op) {
          op.reload();
        });
      });
    },
    setBehavior: function() {
      this.set('isEditingParameter', false);
      this.save();
    }
  }
});
<commit_msg>Fix deprecation warning<commit_after>import Ember from "ember";
import ENV from '../config/environment';

export default Ember.Component.extend({
  isEditing: false,
  isEditingParameter: false,
  tagName: 'td',
  isParameter: Ember.computed.equal('model.currentType', 'parameter'),

  isEnum: function() {
    return this.get('model.variant.ident') === 'enum';
  }.property('model.variant'),

  isBool: function() {
    return this.get('model.variant.ident') === 'bool';
  }.property('model.variant'),

  isTrigger: function() {
    return this.get('model.variant.ident') === 'trigger';
  }.property('model.variant'),

  isMatrix: function() {
    return this.get('model.variant.ident') === 'matrix';
  }.property('model.variant'),

  isFile: function() {
    return this.get('model.variant.ident') === 'image' ||
      this.get('model.variant.ident') === 'file';
  }.property('model.variant'),

  timedOut: function() {
    return this.get('model.state') === 'timedOut';
  }.property('model.state'),

  current: function() {
    return this.get('model.state') === 'current';
  }.property('model.state'),

  offerReload: function() {
    if (this.get('model.behavior') === 'push') {
      return false;
    }
    if (this.get('model.behavior') === 'pull') {
      return true;
    }
    if (this.get('model.originalType') === 'output') {
      return true;
    }

    return this.get('timedOut') || this.get('accessFailed');
  }.property('timedOut', 'accessFailed', 'model.behavior'),

  offerDownload: Ember.computed.equal('model.variant.ident', 'file'),

  url: function() {
    if (this.get('model.variant.ident') !== 'file')
    {
      return '';
    }

    if (ENV.APP.API_HOST) {
      return ENV.APP.API_HOST + '/temp/' + this.get('model.value.name');
    } else {
      var location =  window.location.protocol + '//' + window.location.host;
      return location + '/temp/' + this.get('model.value.name');
    }
  }.property('model.value'),

  writable: function() {
    var variant = this.get('model.variant.ident');
    var knownTypes = ['string', 'enum', 'int', 'float', 'bool', 'trigger',
                      'matrix', 'image', 'file'];
    if (! knownTypes.includes(variant)) {
      return false;
    }

    switch(this.get('model.access')) {
    case 'full':
      return true;
    case 'inactive':
      return ! this.get('model.operator.stream.active');
    default:
      return false;
    }
  }.property('model.access', 'model.current', 'model.variant', 'model.operator.stream.active'),

  accessFailed: function() {
    return this.get('model.state') === 'accessFailed';
  }.property('model.state'),

  fileName: Ember.computed('model.value', {
    get: function() {
      return '';
    },
    set: function(key, value) {
      var strippedName = value.replace(/^.*[\\\/]/, '');
      var v = this.get('model.value');
      v.name = strippedName;
      this.set('model.value', v);
    }
  }),

  isConnector: function() {
    return this.get('model.originalType') !== 'parameter';
  }.property('model.originalType'),

  originalType: function() {
    switch (this.get('model.originalType'))
    {
      case 'input':
        return 'Input';
      case 'output':
        return 'Output';
      default:
        return '';
    }
  }.property('model.originalType'),

  editValue: Ember.computed('model.value', 'model.variant', {
    get: function() {
      var value = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'float':
          return value ? this.get('model.value').toPrecision(3) : 0.0;
        case 'int':
        case 'string':
        case 'enum':
          return value ? this.get('model.value') : '';
        default:
          return '';
      }
    },
    set: function(key, value) {
      var v = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'int':
          v = parseInt(value, 10);
          if (isNaN(v)) {
            return value;
          }
          break;
        case 'float':
          v = parseFloat(value);
          if (isNaN(v)) {
            return value;
          }
          break;
        case 'string':
          v = value;
          break;
        case 'image':
          v = {
            values: value
          };
          break;
        case 'file':
          v.content = value;
          break;
        case 'enum':
          v = parseInt(value);
          break;
        case 'bool':
          v = value === 'true';
          break;
        default:
      }

      this.set('model.value', v);
      return v;
    }
  }),

  isTrue: Ember.computed.alias('model.value'),
  isFalse: Ember.computed.not('model.value'),
  isInput: Ember.computed.equal('model.originalType', 'input'),
  isPushParameter: Ember.computed('model.behavior', {
    get: function() {
      return this.get('model.behavior') === 'push';
    },
    set: function(key, value) {
      this.set('model.behavior', value ? 'push' : 'persistent');
      return value;
    }
  }),

  isPullParameter: Ember.computed('model.behavior', {
    get: function() {
      return this.get('model.behavior') === 'pull';
    },
    set: function(key, value) {
      this.set('model.behavior', value ? 'pull' : 'persistent');
      return value;
    }
  }),

  displayValue: Ember.computed('model.value', 'model.variant', 'isEditing', {
    set: function(key, newValue) {
      return newValue;
    },
    get: function() {
      if (! this.get('current')) {
        return '';
      }

      var value = this.get('model.value');
      switch (this.get('model.variant.ident')) {
        case 'float':
          return value.toPrecision(3);
        case 'enum':
          return this.updateEnumTitle(value);
        case 'bool':
          return value ? 'True' : 'False';
        case 'matrix':
          return value ? value.rows + ' x ' + value.cols + ' matrix' : '';
        case 'image':
          return value ? value.width + ' x ' + value.height + ' image' : '';
        case 'trigger':
          return 'Trigger';
        case 'file':
          return value['name'];
        default:
          return value;
      }
    }
  }),

  // cf. http://stackoverflow.com/q/20623027
  updateEnumTitle: function(enumValue) {
    var value = enumValue;
    var title = this.get('model.descriptions').then( function(value){
      return value.find( function(item) {
         return item.get('value') === enumValue;
      });
    }).then( function(obj) {
      return obj.get('title');
    });

    var _this = this;
    title.then( function(title) {
      _this.set('displayValue', title);
      value = title;
    });

    return value;
  },

  save: function() {
    var model = this.get('model');
    return model.save().catch(function() {
      model.rollbackAttributes();
    });
  },

  actions: {
    editValue: function() {
      this.set('isEditing', true);
    },
    saveChanges: function() {
      this.set('isEditing', false);
      this.save();
    },
    discardChanges: function() {
      this.set('isEditing', false);
      this.set('isEditingParameter', false);
      this.get('model').rollbackAttributes();
    },
    reload: function() {
      this.get('model').reload();
    },
    trigger: function() {
      this.set('isEditing', false);
      this.set('model.value', 1);
      this.save();
    },
    editParameter: function() {
      this.set('isEditingParameter', true);
    },
    saveParameterChanges: function() {
      this.set('isEditingParameter', false);
      var op = this.get('model.operator');
      this.save().then(function() {
        op.then(function(op) {
          op.reload();
        });
      });
    },
    setBehavior: function() {
      this.set('isEditingParameter', false);
      this.save();
    }
  }
});
<|endoftext|>


<commit_before>'use strict';

var gulp = require('gulp'),
  plumber = require('gulp-plumber'),
  rename = require('gulp-rename');
var autoprefixer = require('gulp-autoprefixer');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var ngmin = require('gulp-ngmin');
var inject = require('gulp-inject');
var flatten = require('gulp-flatten');
var minifycss = require('gulp-minify-css');
var angularFilesort = require('gulp-angular-filesort');
var templateCache = require('gulp-angular-templatecache');
var sass = require('gulp-sass');
var wiredep = require('wiredep');
var gls = require('gulp-live-server');
var exec = require('child_process').exec;

gulp.task('vendor-scripts', function() {

  return gulp.src(wiredep().js)

  .pipe(gulp.dest('dist/vendor'));

});

gulp.task('vendor-css', function() {

  return gulp.src(wiredep().css)

  .pipe(gulp.dest('dist/vendor'));

});

gulp.task('vendor-fonts', function() {
  return gulp.src('app//**/*.woff2')
    .pipe(flatten())
    .pipe(gulp.dest('dist/fonts'));

});

gulp.task('sass', function() {
  return gulp.src(['app/assets/**/*.sass'])
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    .pipe(sass())
    .pipe(autoprefixer('last 2 versions'))
    .pipe(gulp.dest('app/assets/'));
});

gulp.task('styles', ['sass'], function() {
  return gulp.src(['app/assets/**/*.css'])
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    .pipe(gulp.dest('dist/styles/'))
    // .pipe(rename({
    // suffix: '.min'
    // }))
    .pipe(minifycss())
    .pipe(gulp.dest('dist/styles/'));
});

gulp.task('scripts', function() {
  return gulp.src('app/src/**/*.js')
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    // .pipe(ngmin({dynamic: true}))
    .pipe(angularFilesort())
    .pipe(concat('main.js'))
    .pipe(gulp.dest('dist/scripts/'))
    // .pipe(rename({
    // suffix: '.min'
    // }))
    .pipe(uglify({
      mangle: false
    }))
    .pipe(gulp.dest('dist/scripts/'));
});

gulp.task('templates', function() {
  return gulp.src(['app/**/*.html', '!/**/index.html'])
    .pipe(templateCache('templates.js', {
      module: 'TwitchCaster'
    }))
    .pipe(gulp.dest('app/src/'));
});

gulp.task('dist', [
    'templates',
    'scripts',
    'styles',
    'vendor-scripts',
    'vendor-css',
    'vendor-fonts'
  ],
  function() {

    return gulp.src('app/index.html')
      .pipe(wiredep.stream({
        fileTypes: {
          html: {
            replace: {
              js: function(filePath) {
                return '<script src="' + 'vendor/' + filePath.split('/')
                  .pop() + '"></script>';
              },
              css: function(filePath) {
                return '<link rel="stylesheet" href="' + 'vendor/' +
                  filePath.split('/').pop() + '"/>';
              }
            }
          }
        }
      }))
      .pipe(inject(
        gulp.src(['dist/scripts/**/*.js'], {
          read: false
        }), {
          addRootSlash: false,
          transform: function(filePath, file, i, length) {
            return '<script src="' + filePath.replace('dist/', '') +
              '"></script>';
          }
        }))

    .pipe(inject(
      gulp.src(['dist/styles/**/*.css'], {
        read: false
      }), {
        addRootSlash: false,
        transform: function(filePath, file, i, length) {
          return '<link rel="stylesheet" href="' + filePath.replace(
            'dist/', '') + '"/>';
        }
      }))

    .pipe(gulp.dest('dist'));
  });

gulp.task('serve', ['templates', 'sass'], function() {
  // var server = gls.static('app', 3000);
  // server.start();
  // console.log('Listening on port 3000');
  // gulp.watch('scripts/**/*', function(file) {
  // server.notify.apply(server, [file]);
  // });
  console.log('lol');
  gulp.watch(['./app/src/**/*', '!./app/src/templates.js', '!./**/*.css'], {
    interval: 1000,
    mode: 'poll'
  }, ['dist']);

  // exec('(cd app/ ; live-server --port=3000)', function(err, stdout, stderr) {
  //   console.log(stdout);
  //   console.log(stderr);
  //   cb(err);
  // });
});

gulp.task('serve-dist', ['dist'], function() {
  exec('(cd dist/ ; live-server --port=3000)', function(err, stdout, stderr) {
    console.log(stdout);
    console.log(stderr);
    cb(err);
  });
})
<commit_msg>Reworked gulp tasks according to the new file structure plan Also removed dead code<commit_after>'use strict';

var gulp = require('gulp'),
  plumber = require('gulp-plumber'),
  rename = require('gulp-rename');
var autoprefixer = require('gulp-autoprefixer');
var concat = require('gulp-concat');
var uglify = require('gulp-uglify');
var ngmin = require('gulp-ngmin');
var inject = require('gulp-inject');
var flatten = require('gulp-flatten');
var minifycss = require('gulp-minify-css');
var angularFilesort = require('gulp-angular-filesort');
var templateCache = require('gulp-angular-templatecache');
var sass = require('gulp-sass');
var wiredep = require('wiredep');
var gls = require('gulp-live-server');
var exec = require('child_process').exec;

gulp.task('vendor-scripts', function() {

  return gulp.src(wiredep().js)

  .pipe(gulp.dest('dist/vendor'));

});

gulp.task('vendor-css', function() {

  return gulp.src(wiredep().css)

  .pipe(gulp.dest('dist/vendor'));

});

gulp.task('vendor-fonts', function() {
  return gulp.src('./**/*.woff2')
    .pipe(flatten())
    .pipe(gulp.dest('dist/fonts'));

});

gulp.task('sass', function() {
  return gulp.src(['app/assets/**/*.sass'])
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    .pipe(sass())
    .pipe(autoprefixer('last 2 versions'))
    .pipe(gulp.dest('dev/assets/'));
});

gulp.task('styles', ['sass'], function() {
  return gulp.src(['dev/assets/**/*.css'])
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    .pipe(gulp.dest('dist/styles/'))
    // .pipe(rename({
    // suffix: '.min'
    // }))
    .pipe(minifycss())
    .pipe(gulp.dest('dist/styles/'));
});

gulp.task('scripts', function() {
  return gulp.src('dev/src/**/*.js')
    .pipe(angularFilesort())
    .pipe(concat('main.js'))
    .pipe(uglify({
      mangle: false
    }))
    .pipe(gulp.dest('dist/scripts/'));
});

gulp.task('dev-scripts', function() {
  return gulp.src(['app/src/**/*.js', 'app/src/templates.js'])
    .pipe(plumber({
      errorHandler: function(error) {
        console.log(error.message);
        this.emit('end');
      }
    }))
    .pipe(gulp.dest('dev/src/'))
});

gulp.task('templates', function() {
  return gulp.src(['app/**/*.html', '!/**/index.html'])
    .pipe(templateCache('templates.js', {
      module: 'TwitchCaster'
    }))
    .pipe(gulp.dest('dev/src/'));
});

gulp.task('html', function() {
  return gulp.src('app/*.html')
    .pipe(gulp.dest('dev/'))
});

gulp.task('dist', [
    'html',
    'templates',
    'dev-scripts',
    'scripts',
    'styles',
    'vendor-scripts',
    'vendor-css',
    'vendor-fonts'
  ],
  function() {

    return gulp.src('app/index.html')
      .pipe(wiredep.stream({
        fileTypes: {
          html: {
            replace: {
              js: function(filePath) {
                return '<script src="' + 'vendor/' + filePath.split('/')
                  .pop() + '"></script>';
              },
              css: function(filePath) {
                return '<link rel="stylesheet" href="' + 'vendor/' +
                  filePath.split('/').pop() + '"/>';
              }
            }
          }
        }
      }))
      .pipe(inject(
        gulp.src(['dist/scripts/**/*.js'], {
          read: false
        }), {
          addRootSlash: false,
          transform: function(filePath, file, i, length) {
            return '<script src="' + filePath.replace('dist/', '') +
              '"></script>';
          }
        }))

    .pipe(inject(
      gulp.src(['dist/styles/**/*.css'], {
        read: false
      }), {
        addRootSlash: false,
        transform: function(filePath, file, i, length) {
          return '<link rel="stylesheet" href="' + filePath.replace(
            'dist/', '') + '"/>';
        }
      }))

    .pipe(gulp.dest('dist'));
  });

gulp.task('watch', function() {
  gulp.watch(['./app/**/*'], {
    interval: 1000,
    mode: 'poll'
  }, ['dist']);
});
<|endoftext|>


<commit_before>const unexpected = require('unexpected');
const baseExpect = unexpected.clone();

baseExpect.addAssertion(
    '<string> to jest match <string>',
    (expect, subject, value) => {
        expect.errorMode = 'bubble';
        return expect(subject, 'to contain', value);
    }
);

baseExpect.addAssertion(
    '<string> to jest match <regexp>',
    (expect, subject, value) => {
        expect.errorMode = 'bubble';
        return expect(subject, 'to match', value);
    }
);

class CustomSpec {
    constructor(type, spec, value) {
        this.type = type;
        this.spec = spec;
        this.value = value;
    }
}

function isCustomSpecOfType(type) {
    return value => value instanceof CustomSpec && value.type === type;
}

baseExpect.addType({
    name: 'ContainingSpec',
    base: 'object',
    identify: isCustomSpecOfType('containing'),
    inspect: ({ spec }, depth, output, inspect) => {
        let baseName;
        if (typeof spec === 'string') {
            baseName = 'String';
        } else if (Array.isArray(spec)) {
            baseName = 'Array';
        } else {
            baseName = 'Object';
        }

        output
            .text(`${baseName}ContainingSpec(`)
            .append(inspect(spec, depth))
            .text(')');
    }
});

function assertContainingArray(subject, spec) {
    /* XXX workaround Unexpected having no direct containing assertion */

    // start a containing assertion
    let nextAssertion = [subject, 'to contain'];

    // flatten out array of elements to check for
    nextAssertion = nextAssertion.concat(spec);

    return nextAssertion;
}

function isPresentIn(element, subject) {
    return subject.indexOf(element) > -1;
}

baseExpect.addAssertion(
    '<array> to equal <ContainingSpec>',
    (expect, subject, { spec }) => {
        let hasChildSpecs = false;
        // check for and unpack nested specs
        spec = spec.map(item => {
            if (item instanceof CustomSpec) {
                hasChildSpecs = true;
                return item.spec;
            } else {
                return item;
            }
        });
        // compare the unpacked specs if found
        if (hasChildSpecs) {
            return expect(subject, 'to satisfy', spec);
        }

        try {
            expect.apply(expect, assertContainingArray(subject, spec));
        } catch (e) {
            const present = spec.filter(element =>
                isPresentIn(element, subject)
            );

            expect(present, 'to equal', spec);
        }
    }
);

baseExpect.addAssertion(
    '<object> to equal <ContainingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to satisfy', spec);
    }
);

baseExpect.addAssertion(
    '<string> to equal <ContainingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to contain', spec);
    }
);

baseExpect.addType({
    name: 'IdentitySpec',
    base: 'object',
    identify: isCustomSpecOfType('identity'),
    inspect: (subject, depth, output, inspect) =>
        output.append(inspect(subject.spec, depth))
});

baseExpect.addAssertion(
    '<array> to contain <IdentitySpec>',
    (expect, subject, { spec }) => {
        expect.errorMode = 'diff';
        return expect(subject, 'to have an item satisfying to be', spec);
    }
);

baseExpect.addType({
    name: 'KeyPathSpec',
    base: 'object',
    identify: isCustomSpecOfType('keypath'),
    inspect: (subject, depth, output, inspect) => {
        output.append(inspect(subject.spec, depth));
    }
});

function keyPathToNestedObjects(keyPathString, keyPathValue) {
    const keyPath = keyPathString.split('.');
    // the innermost property will point to a value
    let nestedObjects = keyPathValue;

    while (keyPath.length > 0) {
        const property = keyPath.pop();
        // nest previous value within a new object
        nestedObjects = {
            [property]: nestedObjects
        };
    }

    return nestedObjects;
}

baseExpect.addAssertion(
    '<object> to have property <KeyPathSpec>',
    (expect, subject, { spec, value }) => {
        if (value === undefined) {
            value = expect.it('to be defined');
        }

        expect.errorMode = 'default';
        return expect(
            subject,
            'to satisfy',
            keyPathToNestedObjects(spec, value)
        );
    }
);

baseExpect.addAssertion(
    '<object> to satisfy <KeyPathSpec>',
    (expect, subject, { spec, value }) => {
        if (value === undefined) {
            value = expect.it('to be defined');
        }

        expect.errorMode = 'default';
        return expect(
            subject,
            'to satisfy',
            keyPathToNestedObjects(spec, value)
        );
    }
);

baseExpect.addType({
    name: 'StringMatchingSpec',
    base: 'object',
    identify: isCustomSpecOfType('matching'),
    inspect: ({ spec }, depth, output, inspect) =>
        output
            .text(`StringMatchingSpec(`)
            .append(inspect(spec, depth))
            .text(')')
});

baseExpect.addAssertion(
    '<string> to equal <StringMatchingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to match', spec);
    }
);

function toCustomSpec(type) {
    return value => new CustomSpec(type, value);
}

function toCustomSpecWithConstraint(type) {
    return (value, constraint) => new CustomSpec(type, value, constraint);
}

function withFlags(assertion, flags) {
    return flags.not ? `not ${assertion}` : assertion;
}

module.exports = function expect(subject) {
    const expect = baseExpect;
    const flags = {
        not: false
    };

    const buildAssertion = assertion => {
        return value => {
            return expect(subject, withFlags(assertion, flags), value);
        };
    };

    const buildAssertionTwoArgs = assertion => {
        return value => {
            return expect(subject, withFlags(assertion, flags));
        };
    };

    const buildAssertionSomeArgs = assertion => {
        const oneArg = buildAssertion(assertion);
        const noArgs = buildAssertionTwoArgs(assertion);

        return value => (value === undefined ? noArgs() : oneArg(value));
    };

    const buildAssertionWithCustomSpec = (assertion, type) => {
        const assertionFn = buildAssertion(assertion);
        const wrapperFn = toCustomSpecWithConstraint(type);

        return (value, constraint) => assertionFn(wrapperFn(value, constraint));
    };

    return {
        toBe: buildAssertion('to be'),
        toBeDefined: buildAssertionTwoArgs('to be defined'),
        toBeFalsy: buildAssertionTwoArgs('to be falsy'),
        toBeNull: buildAssertionTwoArgs('to be null'),
        toBeTruthy: buildAssertionTwoArgs('to be truthy'),
        toContain: buildAssertionWithCustomSpec('to contain', 'identity'),
        toContainEqual: buildAssertion('to contain'),
        toEqual: buildAssertion('to equal'),
        toHaveLength: buildAssertion('to have length'),
        toHaveProperty: buildAssertionWithCustomSpec(
            'to have property',
            'keypath'
        ),
        toMatch: buildAssertion('to jest match'),
        toMatchObject: buildAssertion('to satisfy'),
        toThrow: buildAssertionSomeArgs('to throw'),
        get not() {
            flags.not = true;
            return this;
        },
        get resolves() {
            return expect(subject, 'to be fulfilled with', result => {
                subject = result;
            }).then(() => this);
        },
        get rejects() {
            return expect(subject, 'to be rejected with', result => {
                subject = result !== undefined ? result.message : undefined;
            }).then(() => this);
        }
    };
};

module.exports.arrayContaining = toCustomSpec('containing');
module.exports.objectContaining = toCustomSpec('containing');
module.exports.stringContaining = toCustomSpec('containing');
module.exports.stringMatching = toCustomSpec('matching');
<commit_msg>Collapse the CustomSpec definitions into a factory function.<commit_after>const unexpected = require('unexpected');
const baseExpect = unexpected.clone();

baseExpect.addAssertion(
    '<string> to jest match <string>',
    (expect, subject, value) => {
        expect.errorMode = 'bubble';
        return expect(subject, 'to contain', value);
    }
);

baseExpect.addAssertion(
    '<string> to jest match <regexp>',
    (expect, subject, value) => {
        expect.errorMode = 'bubble';
        return expect(subject, 'to match', value);
    }
);

class CustomSpec {
    constructor(type, spec, value, isTopLevel = false) {
        this.type = type;
        this.spec = spec;
        this.value = value;
        this.nested = !isTopLevel;
    }
}

function instantiateCustomSpec(baseName) {
    const specName = `${baseName}Spec`;

    baseExpect.addType({
        name: specName,
        base: 'object',
        identify: isCustomSpecOfType(specName),
        inspect: ({ spec, nested }, depth, output, inspect) => {
            if (nested) output.text(`${specName}(`);
            output.append(inspect(spec, depth));
            if (nested) output.text(')');
        }
    });

    return specName;
}

function isCustomSpecOfType(type) {
    return value => value instanceof CustomSpec && value.type === type;
}

const ArrayContainingSpec = instantiateCustomSpec('ArrayContaining');

function assertContainingArray(subject, spec) {
    /* XXX workaround Unexpected having no direct containing assertion */

    // start a containing assertion
    let nextAssertion = [subject, 'to contain'];

    // flatten out array of elements to check for
    nextAssertion = nextAssertion.concat(spec);

    return nextAssertion;
}

function isPresentIn(element, subject) {
    return subject.indexOf(element) > -1;
}

baseExpect.addAssertion(
    '<array> to equal <ArrayContainingSpec>',
    (expect, subject, { spec }) => {
        let hasChildSpecs = false;
        // check for and unpack nested specs
        spec = spec.map(item => {
            if (item instanceof CustomSpec) {
                hasChildSpecs = true;
                return item.spec;
            } else {
                return item;
            }
        });
        // compare the unpacked specs if found
        if (hasChildSpecs) {
            return expect(subject, 'to satisfy', spec);
        }

        try {
            expect.apply(expect, assertContainingArray(subject, spec));
        } catch (e) {
            const present = spec.filter(element =>
                isPresentIn(element, subject)
            );

            expect(present, 'to equal', spec);
        }
    }
);

const ObjectContainingSpec = instantiateCustomSpec('ObjectContaining');

baseExpect.addAssertion(
    '<object> to equal <ObjectContainingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to satisfy', spec);
    }
);

const StringContainingSpec = instantiateCustomSpec('StringContaining');

baseExpect.addAssertion(
    '<string> to equal <StringContainingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to contain', spec);
    }
);

const IdentitySpec = instantiateCustomSpec('Identity');

baseExpect.addAssertion(
    '<array> to contain <IdentitySpec>',
    (expect, subject, { spec }) => {
        expect.errorMode = 'diff';
        return expect(subject, 'to have an item satisfying to be', spec);
    }
);

const KeyPathSpec = instantiateCustomSpec('KeyPath');

function keyPathToNestedObjects(keyPathString, keyPathValue) {
    const keyPath = keyPathString.split('.');
    // the innermost property will point to a value
    let nestedObjects = keyPathValue;

    while (keyPath.length > 0) {
        const property = keyPath.pop();
        // nest previous value within a new object
        nestedObjects = {
            [property]: nestedObjects
        };
    }

    return nestedObjects;
}

baseExpect.addAssertion(
    '<object> to have property <KeyPathSpec>',
    (expect, subject, { spec, value }) => {
        if (value === undefined) {
            value = expect.it('to be defined');
        }

        expect.errorMode = 'default';
        return expect(
            subject,
            'to satisfy',
            keyPathToNestedObjects(spec, value)
        );
    }
);

baseExpect.addAssertion(
    '<object> to satisfy <KeyPathSpec>',
    (expect, subject, { spec, value }) => {
        if (value === undefined) {
            value = expect.it('to be defined');
        }

        expect.errorMode = 'default';
        return expect(
            subject,
            'to satisfy',
            keyPathToNestedObjects(spec, value)
        );
    }
);

const StringMatchingSpec = instantiateCustomSpec('StringMatching');

baseExpect.addAssertion(
    '<string> to equal <StringMatchingSpec>',
    (expect, subject, { spec }) => {
        return expect(subject, 'to match', spec);
    }
);

function toCustomSpec(type) {
    return value => new CustomSpec(type, value);
}

function toCustomSpecWithConstraint(type) {
    return (value, constraint) => new CustomSpec(type, value, constraint, true);
}

function withFlags(assertion, flags) {
    return flags.not ? `not ${assertion}` : assertion;
}

module.exports = function expect(subject) {
    const expect = baseExpect;
    const flags = {
        not: false
    };

    const buildAssertion = assertion => {
        return value => {
            return expect(subject, withFlags(assertion, flags), value);
        };
    };

    const buildAssertionTwoArgs = assertion => {
        return value => {
            return expect(subject, withFlags(assertion, flags));
        };
    };

    const buildAssertionSomeArgs = assertion => {
        const oneArg = buildAssertion(assertion);
        const noArgs = buildAssertionTwoArgs(assertion);

        return value => (value === undefined ? noArgs() : oneArg(value));
    };

    const buildAssertionWithCustomSpec = (assertion, type) => {
        const assertionFn = buildAssertion(assertion);
        const wrapperFn = toCustomSpecWithConstraint(type);

        return (value, constraint) => assertionFn(wrapperFn(value, constraint));
    };

    return {
        toBe: buildAssertion('to be'),
        toBeDefined: buildAssertionTwoArgs('to be defined'),
        toBeFalsy: buildAssertionTwoArgs('to be falsy'),
        toBeNull: buildAssertionTwoArgs('to be null'),
        toBeTruthy: buildAssertionTwoArgs('to be truthy'),
        toContain: buildAssertionWithCustomSpec('to contain', IdentitySpec),
        toContainEqual: buildAssertion('to contain'),
        toEqual: buildAssertion('to equal'),
        toHaveLength: buildAssertion('to have length'),
        toHaveProperty: buildAssertionWithCustomSpec(
            'to have property',
            KeyPathSpec
        ),
        toMatch: buildAssertion('to jest match'),
        toMatchObject: buildAssertion('to satisfy'),
        toThrow: buildAssertionSomeArgs('to throw'),
        get not() {
            flags.not = true;
            return this;
        },
        get resolves() {
            return expect(subject, 'to be fulfilled with', result => {
                subject = result;
            }).then(() => this);
        },
        get rejects() {
            return expect(subject, 'to be rejected with', result => {
                subject = result !== undefined ? result.message : undefined;
            }).then(() => this);
        }
    };
};

module.exports.arrayContaining = toCustomSpec(ArrayContainingSpec);
module.exports.objectContaining = toCustomSpec(ObjectContainingSpec);
module.exports.stringContaining = toCustomSpec(StringContainingSpec);
module.exports.stringMatching = toCustomSpec(StringMatchingSpec);
<|endoftext|>


<commit_before>import React, { Component } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';

import Cursor from './Cursor';

class Cursors extends Component {
  render() {
    return (

        {this.props.displayedUsers.map(user => (
          <Cursor
            username={user.get('username')}
            color={user.get('color')}
            x={user.get('x')}
            y={user.get('y')}
          />
        ))}

    );
  }
}

Cursors.propTypes = {
  displayedUsers: ImmutablePropTypes.orderedMap.isRequired
};

export default Cursors;
<commit_msg>Adjust Cursors render method map to iterate through a normal array<commit_after>import React, { Component } from 'react';
import ImmutablePropTypes from 'react-immutable-proptypes';

import Cursor from './Cursor';

class Cursors extends Component {
  render() {
    return (

        {this.props.displayedUsers.valueSeq().toArray().map(user => (
          <Cursor
            key={user.get('username')}
            username={user.get('username')}
            color={user.get('color')}
            x={user.get('x')}
            y={user.get('y')}
          />
        ))}

    );
  }
}

Cursors.propTypes = {
  displayedUsers: ImmutablePropTypes.orderedMap.isRequired
};

export default Cursors;
<|endoftext|>


<commit_before><commit_msg>moved util test<commit_after>import _ from 'underscore';
import test from 'tape';
import {printE, printIns, initialiseRegistry, CopyEntity} from './common';

// import {copy as CopyEntity} from '../../src/util/uuid';

test('copies a blank entity', t => {
    return initialiseRegistry().then( registry => {

        // let src = registry.createEntity();
        let src = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'alpha'}
        ]);

        let dst = CopyEntity( registry, src );

        // change src to prove dst is independent
        src.Position.set({x:15});
        t.equals( dst.Position.get('x'), 2 );
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});

test('will retain dst components missing from the src by default', t => {
    return initialiseRegistry().then( registry => {

        let src = registry.createEntity([  {id:'/component/position', x:2, y:-2}  ]);
        let dst = registry.createEntity([  {id:'/component/name', 'name':'alpha'} ]);
        let copy = CopyEntity( registry, src, dst );

        t.equals( copy.Position.get('y'), -2 );
        t.equals( copy.Name.get('name'), 'alpha');
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});

test('will remove components missing from the src', t => {
    return initialiseRegistry().then( registry => {

        let src = registry.createEntity([  {id:'/component/position', x:2, y:-2}  ]);
        let dst = registry.createEntity([  {id:'/component/name', 'name':'alpha'} ]);
        let copy = CopyEntity( registry, src, dst, {delete:true} );

        t.equals( copy.Position.get('y'), -2 );
        t.ok( !copy.Name, 'the name component should be missing from the dst' );
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});

test('returns false if nothing changed', t => {
    return initialiseRegistry().then( registry => {

        // let src = registry.createEntity();
        let src = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'alpha'}
        ]);

        let dst = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'alpha'}
        ]);

        let [copy,hasChanged] = CopyEntity( registry, src, dst, {returnChanged:true} );

        t.notOk( hasChanged, 'the src and dst entity were the same' );
        t.equals( copy.Name.get('name'), 'alpha' );
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});

test('returning whether anything was changed on the dst entity', t => {
    return initialiseRegistry().then( registry => {

        // let src = registry.createEntity();
        let src = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'alpha'}
        ]);

        let dst = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'beta'}
        ]);

        let [copy,hasChanged] = CopyEntity( registry, src, dst, {returnChanged:true} );

        t.ok( hasChanged, 'the src and dst entity were different' );
        t.equals( copy.Name.get('name'), 'alpha' );
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});

test('a missing dst component counts as a change', t => {
    return initialiseRegistry().then( registry => {

        // let src = registry.createEntity();
        let src = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
        ]);

        let dst = registry.createEntity([
            { id:'/component/position', x:2, y:-2 },
            { id:'/component/name', 'name':'alpha'}
        ]);

        let [copy,hasChanged] = CopyEntity( registry, src, dst, {delete:true,returnChanged:true} );
        t.ok( hasChanged, 'the src and dst entity were different' );
        // t.equals( copy.Name.get('name'), 'alpha' );
    })
    .then( () => t.end() )
    .catch( err => log.error('test error: %s', err.stack) )
});
<|endoftext|>


<commit_before>import logger from 'loglevel'

export default function baseEditorMixin (formRefs) {
  return {
    props: {
      clearButton: {
        type: String,
        default: ''
      },
      resetButton: {
        type: String,
        default: ''
      }
    },
    data () {
      return {
        applyButton: ''
      }
    },
    methods: {
      fillEditor () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            if (this.getObject()) {
              if (this.perspective !== '') {
                form.fill(this.getObject()[this.perspective])
              } else {
                form.fill(this.getObject())
              }
            }
          } else {
            logger.warn(`Trying to fill the editor with a non-ready form named ${form}`)
          }
        })
        // Update button accordingly
        if (this.id) {
          this.applyButton = 'Update'
        } else {
          this.applyButton = 'Create'
        }
      },
      clear () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            form.clear()
          } else {
            logger.warn(`Trying to clear the editor with a non-ready form named ${form}`)
          }
        })
      },
      reset () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            form.reset()
          } else {
            logger.warn(`Trying to reset the editor with a non-ready form named ${form}`)
          }
        })
      },
      apply (event, done) {
        // Iterate over forms for validation
        let isValid = true
        let object = {}
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            let result = form.validate()
            if (!result.isValid) {
              isValid = false
            } else {
              Object.assign(object, result.values)
            }
          } else {
            logger.warn(`Trying to apply the editor with a non-ready form named ${form}`)
            isValid = false
          }
        })

        if (!isValid) {
          if (done) done()
          return
        }

        if (this.getService()) {
          // Small helper to avoid repeating too much similar code
          let onServiceResponse = response => {
            this.$emit('applied', response)
            if (done) done()
          }

          // Update the item
          if (this.applyButton === 'Update') {
            // Edtng mode => patch the item
            if (this.perspective !== '') {
              let data = {}
              data[this.perspective] = object
              this.servicePatch(this.id, data, { query: { $select: [this.perspective] } })
              .then(onServiceResponse)
            } else {
              this.servicePatch(this.id, object)
              .then(onServiceResponse)
            }
          } else if (this.applyButton === 'Create') {
            // Creation mode => create the item
            this.serviceCreate(object)
            .then(onServiceResponse)
          } else {
            logger.warn('Invalid editor mode')
            if (done) done()
          }
        }
      },
      refresh () {
        // When the service is available
        this.loadService()
        // We can then load the schema/object and local refs in parallel
        return Promise.all([
          this.loadSchema(),
          this.loadObject(),
          this.loadRefs()
        ])
        // We finally build the forms then fill it
        .then(_ => Promise.all(formRefs.map(name => this.$refs[name].build())))
        .then(_ => this.fillEditor())
      }
    }
  }
}
<commit_msg>Fixed https://github.com/kalisio/kCore/issues/27<commit_after>import logger from 'loglevel'

export default function baseEditorMixin (formRefs) {
  return {
    props: {
      baseObject: {
        type: Object,
        default: function () {
          return {}
        }
      },
      clearButton: {
        type: String,
        default: ''
      },
      resetButton: {
        type: String,
        default: ''
      }
    },
    data () {
      return {
        applyButton: ''
      }
    },
    methods: {
      fillEditor () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            if (this.getObject()) {
              if (this.perspective !== '') {
                form.fill(this.getObject()[this.perspective])
              } else {
                form.fill(this.getObject())
              }
            }
          } else {
            logger.warn(`Trying to fill the editor with a non-ready form named ${form}`)
          }
        })
        // Update button accordingly
        if (this.id) {
          this.applyButton = 'Update'
        } else {
          this.applyButton = 'Create'
        }
      },
      clear () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            form.clear()
          } else {
            logger.warn(`Trying to clear the editor with a non-ready form named ${form}`)
          }
        })
      },
      reset () {
        // Iterate over forms
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            form.reset()
          } else {
            logger.warn(`Trying to reset the editor with a non-ready form named ${form}`)
          }
        })
      },
      apply (event, done) {
        // Iterate over forms for validation
        let isValid = true
        // Start from default object or input base object
        // This is used to keep track of existing or additional properties
        // in addition to the ones edited throught the form
        let object = Object.assign({}, this.getObject() || this.baseObject)
        formRefs.forEach(name => {
          let form = this.$refs[name]
          if (form.loadRefs().isFulfilled()) {
            let result = form.validate()
            if (!result.isValid) {
              isValid = false
            } else {
              Object.assign(object, result.values)
            }
          } else {
            logger.warn(`Trying to apply the editor with a non-ready form named ${form}`)
            isValid = false
          }
        })

        if (!isValid) {
          if (done) done()
          return
        }

        if (this.getService()) {
          // Small helper to avoid repeating too much similar code
          let onServiceResponse = response => {
            this.$emit('applied', response)
            if (done) done()
          }

          // Update the item
          if (this.applyButton === 'Update') {
            // Edtng mode => patch the item
            if (this.perspective !== '') {
              let data = {}
              data[this.perspective] = object
              this.servicePatch(this.id, data, { query: { $select: [this.perspective] } })
              .then(onServiceResponse)
            } else {
              this.servicePatch(this.id, object)
              .then(onServiceResponse)
            }
          } else if (this.applyButton === 'Create') {
            // Creation mode => create the item
            this.serviceCreate(object)
            .then(onServiceResponse)
          } else {
            logger.warn('Invalid editor mode')
            if (done) done()
          }
        }
      },
      refresh () {
        // When the service is available
        this.loadService()
        // We can then load the schema/object and local refs in parallel
        return Promise.all([
          this.loadSchema(),
          this.loadObject(),
          this.loadRefs()
        ])
        // We finally build the forms then fill it
        .then(_ => Promise.all(formRefs.map(name => this.$refs[name].build())))
        .then(_ => this.fillEditor())
      }
    }
  }
}
<|endoftext|>