<commit_before>const FIRST_UPDATE=1000;

var events = require('events'),
    moment = require('moment'),
    url = require('url'),
    querystring = require('querystring'),
    util = require('util'),
    debug = require('debug')('server'),
    debugSend = require('debug')('server:send');

function Server(programConfig, http, serverList, minUpdateTime, maxUpdateTime, waitTimeout) {
    this.clientList = [];
    this.isHTTPS = programConfig.ssl;
    this.useEndPointUrl = programConfig.useendpointurl;
    this.http = http;
    this.serverList = serverList;
    this.numServers = serverList.length;

    this.minUpdateTime = minUpdateTime;
    this.maxUpdateTime = maxUpdateTime;
    this.waitTimeout = waitTimeout;

    this.timeout = null;

    this.sendTimeout = null;
    this.semRequest = (this.isHTTPS) ? 100: 25;   // controls max number of requests we have in flight
    this.requestQueue = []; // list of endpoints that should get an update
}

util.inherits(Server, events.EventEmitter);

function random(min, max) {
    return Math.random()*(max-min) + min;
}
Server.prototype.randomNext = function() {
    return Math.floor(random(this.minUpdateTime, this.maxUpdateTime));
}

Server.prototype.addClient  = function(client) {
    debug("Registered Client: %s", client.uaid);
    this.clientList.push(client);
}

Server.prototype.start = function() {
    var self = this;

    this.timeout = setTimeout(function queueUpdates() {
        var client, i, k, e,
            numClients = self.clientList.length ;

        debug("Server checking %d clients, queue size: %d", numClients, self.requestQueue.length);

        var maxQueueSize = Math.min(numClients, self.semRequest * 2)

        for (i=0; i<numClients; i++) {
            client = self.clientList[i];

            for (k in client.endpoints) {
                e = client.endpoints[k];

                /*
                 * for this to work right, the Endpoint must:
                 *
                 * - set nextVersionRequest to 0 and expectingUpdate to false
                 *   for a new update to be triggered by the server
                 */

                // a zero means when we should schedule a new update for the
                // endpoint
                if (e.nextVersionRequest === 0 && e.queued === false) {
                    // just schedule it for sometime in the future

                    if (e.updateCount === 0) {
                        e.nextVersionRequest = Date.now() + random(10, self.maxUpdateTime);
                    } else {
                        e.nextVersionRequest = Date.now() + self.randomNext();
                    }

                    debug("%s next update in: %dms", e.channelID, e.nextVersionRequest - Date.now());
                    e.updateCount += 1;
                    continue;
                }

                /*
                 * use e.expectingUpdate to control TIMEOUT and whether we should send
                 * forget the old request and send out a new one. We expect the endpoint
                 * to reset e.expectingUpdate to `false`
                 */
                if (e.expectingUpdate === true && e.queued === false && Date.now() - e.nextVersionRequest > self.waitTimeout) {
                    e.expectingUpdate = false;
                    e.nextVersionRequest = Date.now() + random(500, FIRST_UPDATE);
                    debug("TIMEOUT %s, doing again in %d", e.channelID, e.nextVersionRequest - Date.now());
                    self.emit('TIMEOUT', e.channelID, self.waitTimeout);
                } else if (e.nextVersionRequest < Date.now() && e.queued == false) {
                    // we push these onto a queue so we can control how many are in flight at any given time
                    if (self.requestQueue.length < maxQueueSize) {
                        debug("Queuing %s", e.channelID);
                        e.queued = true;
                        self.requestQueue.push(e);
                    }
                }
            }
        }

        self.timeout = setTimeout(queueUpdates, 1000);

    }, 1000);

    this.sendTimeout = setTimeout(function flushRequests() {
        if (self.requestQueue.length > 0) {
            debugSend("Queue Size: ", self.requestQueue.length);
        }

        // flush stuff out of the request queue
        while (self.semRequest > 0 && self.requestQueue.length > 0) {
            self.semRequest -= 1;
            var e = self.requestQueue.shift();
            self.sendUpdate(e);
        }

        this.sendTimeout = setTimeout(flushRequests, 250);
    }, 250);
}

Server.prototype.stop = function() {
    clearTimeout(this.timeout);
    clearTimeout(this.sendTimeout);
}

Server.prototype.sendUpdate = function(endpoint) {
    var self = this;

    endpoint.expectingUpdate = true;
    endpoint.expectedVersion = endpoint.version + 1;
    var data = querystring.stringify({'version': endpoint.expectedVersion.toString()});

    var u = url.parse(endpoint.url);
    if (this.useEndPointUrl == true) {
        var serverName = u.hostname;
        var port = u.port;
    } else {
        // rotate through the server nodes, instead of choosing them at random
        var parts = this.serverList[Math.floor(random(0, this.numServers))].split(':');
        var serverName = parts[0];
        var port =  parts[1] || (this.isHTTPS) ? 443 : 80;
    }

    var opts = {
        hostname: serverName,
        port: port,
        method: 'PUT',
        path: u.path,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': data.length
        },
        agent: this.http.localAgent
    };

    if (u.hostname == serverName) {
        debugSend('PUT *SAME*, v:%d, endpoint=%s:%s%s', endpoint.expectedVersion, serverName, port, u.path);
    } else {
        debugSend('PUT *DIFF*, v:%d, endpoint=%s:%s%s', endpoint.expectedVersion, serverName, port, u.path);
    }

    var req = this.http.request(opts, function(res) {
        debugSend("RES %s, %s", endpoint.channelID, res.statusCode);

        var body = '';

        res.on('data', function(chunk) {
            body += chunk;
        });

        res.once('end', function() {
            self.semRequest += 1; // we have more room :)
            endpoint.queued = false;

            // these are inside the `end` event because we want the
            // response body in the emitted event.
            if(res.statusCode !== 200) {
                self.emit('PUT_FAIL', endpoint.channelID, res.statusCode, body);
            } else {
                self.serverPutTime = Date.now();
                self.emit('PUT_OK', endpoint.channelID);
            }
        });
    });

    req.on('error', function(e) {
        self.semRequest += 1; // we have more room :)
        self.emit('ERR_NETWORK', e);
        debugSend('Request Error: %s', e);
    });

    req.write(data);
    req.end();
};

module.exports = Server;
<commit_msg>fix bug when endpoint URL has no port defined<commit_after>const FIRST_UPDATE=1000;

var events = require('events'),
    moment = require('moment'),
    url = require('url'),
    querystring = require('querystring'),
    util = require('util'),
    debug = require('debug')('server'),
    debugSend = require('debug')('server:send');

function Server(programConfig, http, serverList, minUpdateTime, maxUpdateTime, waitTimeout) {
    this.clientList = [];
    this.isHTTPS = programConfig.ssl;
    this.useEndPointUrl = programConfig.useendpointurl;
    this.http = http;
    this.serverList = serverList;
    this.numServers = serverList.length;

    this.minUpdateTime = minUpdateTime;
    this.maxUpdateTime = maxUpdateTime;
    this.waitTimeout = waitTimeout;

    this.timeout = null;

    this.sendTimeout = null;
    this.semRequest = (this.isHTTPS) ? 100: 25;   // controls max number of requests we have in flight
    this.requestQueue = []; // list of endpoints that should get an update
}

util.inherits(Server, events.EventEmitter);

function random(min, max) {
    return Math.random()*(max-min) + min;
}
Server.prototype.randomNext = function() {
    return Math.floor(random(this.minUpdateTime, this.maxUpdateTime));
}

Server.prototype.addClient  = function(client) {
    debug("Registered Client: %s", client.uaid);
    this.clientList.push(client);
}

Server.prototype.start = function() {
    var self = this;

    this.timeout = setTimeout(function queueUpdates() {
        var client, i, k, e,
            numClients = self.clientList.length ;

        debug("Server checking %d clients, queue size: %d", numClients, self.requestQueue.length);

        var maxQueueSize = Math.min(numClients, self.semRequest * 2)

        for (i=0; i<numClients; i++) {
            client = self.clientList[i];

            for (k in client.endpoints) {
                e = client.endpoints[k];

                /*
                 * for this to work right, the Endpoint must:
                 *
                 * - set nextVersionRequest to 0 and expectingUpdate to false
                 *   for a new update to be triggered by the server
                 */

                // a zero means when we should schedule a new update for the
                // endpoint
                if (e.nextVersionRequest === 0 && e.queued === false) {
                    // just schedule it for sometime in the future

                    if (e.updateCount === 0) {
                        e.nextVersionRequest = Date.now() + random(10, self.maxUpdateTime);
                    } else {
                        e.nextVersionRequest = Date.now() + self.randomNext();
                    }

                    debug("%s next update in: %dms", e.channelID, e.nextVersionRequest - Date.now());
                    e.updateCount += 1;
                    continue;
                }

                /*
                 * use e.expectingUpdate to control TIMEOUT and whether we should send
                 * forget the old request and send out a new one. We expect the endpoint
                 * to reset e.expectingUpdate to `false`
                 */
                if (e.expectingUpdate === true && e.queued === false && Date.now() - e.nextVersionRequest > self.waitTimeout) {
                    e.expectingUpdate = false;
                    e.nextVersionRequest = Date.now() + random(500, FIRST_UPDATE);
                    debug("TIMEOUT %s, doing again in %d", e.channelID, e.nextVersionRequest - Date.now());
                    self.emit('TIMEOUT', e.channelID, self.waitTimeout);
                } else if (e.nextVersionRequest < Date.now() && e.queued == false) {
                    // we push these onto a queue so we can control how many are in flight at any given time
                    if (self.requestQueue.length < maxQueueSize) {
                        debug("Queuing %s", e.channelID);
                        e.queued = true;
                        self.requestQueue.push(e);
                    }
                }
            }
        }

        self.timeout = setTimeout(queueUpdates, 1000);

    }, 1000);

    this.sendTimeout = setTimeout(function flushRequests() {
        if (self.requestQueue.length > 0) {
            debugSend("Queue Size: ", self.requestQueue.length);
        }

        // flush stuff out of the request queue
        while (self.semRequest > 0 && self.requestQueue.length > 0) {
            self.semRequest -= 1;
            var e = self.requestQueue.shift();
            self.sendUpdate(e);
        }

        this.sendTimeout = setTimeout(flushRequests, 250);
    }, 250);
}

Server.prototype.stop = function() {
    clearTimeout(this.timeout);
    clearTimeout(this.sendTimeout);
}

Server.prototype.sendUpdate = function(endpoint) {
    var self = this;

    endpoint.expectingUpdate = true;
    endpoint.expectedVersion = endpoint.version + 1;
    var data = querystring.stringify({'version': endpoint.expectedVersion.toString()});

    var u = url.parse(endpoint.url);
    if (this.useEndPointUrl == true) {
        var serverName = u.hostname;
        var port = (u.port) ? u.port : (u.protocol == "https:") ? 443 : 80;
    } else {
        // rotate through the server nodes, instead of choosing them at random
        var parts = this.serverList[Math.floor(random(0, this.numServers))].split(':');
        var serverName = parts[0];
        var port =  parts[1] || (this.isHTTPS) ? 443 : 80;
    }

    var opts = {
        hostname: serverName,
        port: port,
        method: 'PUT',
        path: u.path,
        headers: {
            'Content-Type': 'application/x-www-form-urlencoded',
            'Content-Length': data.length
        },
        agent: this.http.localAgent
    };

    if (u.hostname == serverName) {
        debugSend('PUT *SAME*, v:%d, endpoint=%s:%s%s', endpoint.expectedVersion, serverName, port, u.path);
    } else {
        debugSend('PUT *DIFF*, v:%d, endpoint=%s:%s%s', endpoint.expectedVersion, serverName, port, u.path);
    }

    var req = this.http.request(opts, function(res) {
        debugSend("RES %s, %s", endpoint.channelID, res.statusCode);

        var body = '';

        res.on('data', function(chunk) {
            body += chunk;
        });

        res.once('end', function() {
            self.semRequest += 1; // we have more room :)
            endpoint.queued = false;

            // these are inside the `end` event because we want the
            // response body in the emitted event.
            if(res.statusCode !== 200) {
                self.emit('PUT_FAIL', endpoint.channelID, res.statusCode, body);
            } else {
                self.serverPutTime = Date.now();
                self.emit('PUT_OK', endpoint.channelID);
            }
        });
    });

    req.on('error', function(e) {
        self.semRequest += 1; // we have more room :)
        self.emit('ERR_NETWORK', e);
        debugSend('Request Error: %s', e);
    });

    req.write(data);
    req.end();
};

module.exports = Server;
<|endoftext|>


<commit_before>'use strict';

const {Readable} = require('stream');
const {EventEmitter} = require('events');
const {Server: WSServer} = require('ws');
const msgpack = require('msgpack-lite');
const checkType = require('./checkType.js');
const {
    TYPES: {T_REQUEST, T_UPLOAD, T_SUBSCRIBE, T_UNSUBSCRIBE, T_RESOLVE, T_REJECT, T_PUBLISH, T_MESSAGE}
} = require('./constants.js');

const INIT_MIDDLEWARES = Symbol('init middlewares');
const API_METHODS = Symbol('api methods');
const CHAN_METHODS = Symbol('channel methods');
const UPLOAD_CHUNKS = Symbol('upload chunks');
const UNSUBSCRIBE_FNS = Symbol('unsubscribe functions');

const msgTypesFn = {
    [T_REQUEST](ctx, {
        id,
        an: apiName,
        p: payload,
        u: hasUpload
    }) {
        const fn = this[API_METHODS][apiName];
        if(!fn) {
            send(ctx.socket, {
                id,
                t: T_REJECT,
                r: new Error('Unknown api method ' + apiName)
            });
            return;
        }
        const uploadStream = hasUpload ? createUploadStream(id, ctx) : void 0;
        Promise.resolve()
            .then(() => fn.call(ctx, payload, uploadStream, ctx))
            .then(r => send(ctx.socket, {
                id,
                t: T_RESOLVE,
                r
            }))
            .catch(r => send(ctx.socket, {
                id,
                t: T_REJECT,
                r
            }));
    },
    [T_UPLOAD](ctx, {id, c: chunk}) {
        const chunksList = ctx[UPLOAD_CHUNKS][id];
        if(!chunksList) { return; }
        const payload = {
            chunk,
            next: chunksList.first
        };
        chunksList.first = payload;
        if(chunksList.last === null) {
            chunksList.last = payload;
        }
        if(chunk === null) {
            delete ctx[UPLOAD_CHUNKS][id];
        }
        if(chunksList.onReady) {
            chunksList.onReady();
        }
    },
    [T_SUBSCRIBE](ctx, {c: channel}) {
        const fn = this[CHAN_METHODS][channel];
        if(!fn) { return; }
        function publish(payload) {
            send(ctx.socket, {
                t: T_PUBLISH,
                c: channel,
                p: payload
            });
        }
        try {
            const unsubscribe = fn.call(ctx, publish, ctx);
            ctx[UNSUBSCRIBE_FNS][channel] = unsubscribe;
        } catch(e) {
            this.emit('error', e);
        }
    },
    [T_UNSUBSCRIBE](ctx, {c: channel}) {
        const fn = ctx[UNSUBSCRIBE_FNS][channel];
        delete ctx[UNSUBSCRIBE_FNS][channel];
        if(typeof fn !== 'function') { return; }
        try {
            fn.call(ctx, ctx);
        } catch(e) {
            this.emit('error', e);
        }
    },
    [T_MESSAGE](ctx, {p: payload}) {
        ctx.emit('message', payload);
    }
};

class ApiServer extends EventEmitter {
    constructor(options) {
        super();
        this[INIT_MIDDLEWARES] = [];
        this[API_METHODS] = {};
        this[CHAN_METHODS] = {};
        if(options && options.server instanceof WSServer) {
            this._server = options.server;
        } else {
            this._server = new WSServer(options);
        }
        this._server.on('error', (...args) => this.emit('error', ...args));
        this._server.on('listening', (...args) => this.emit('listening', ...args));
        this._server.on('headers', (...args) => this.emit('headers', ...args));
        this._server.on('connection', async (socket, request) => {
            const ctx = Object.assign(new EventEmitter(), {
                socket,
                request,
                session: {},
                closeCode: void 0,
                closeReason: void 0,
                [UPLOAD_CHUNKS]: {},
                [UNSUBSCRIBE_FNS]: {},
                get openedChannels() {
                    return Object.getOwnPropertyNames(this[UNSUBSCRIBE_FNS]);
                },
                send(payload) {
                    send(socket, {
                        t: T_MESSAGE,
                        p: payload
                    });
                    return this;
                },
                close(code = ctx.closeCode, reason = ctx.closeReason) {
                    return socket.close(code, reason);
                }
            });
            var ready = false;
            socket.on('message', async (data) => {
                const payload = msgpack.decode(data);
                while(!ready) {
                    await delay();
                }
                const fn = msgTypesFn[payload.t];
                if(!fn) { return; }
                fn.call(this, ctx, payload);
            });
            socket.on('close', async (code, reason) => {
                while(!ready) {
                    await delay();
                }
                ctx.closeCode = code;
                ctx.closeReason = reason;
                for(let c of ctx.openedChannels) {
                    msgTypesFn[T_UNSUBSCRIBE](ctx, {c});
                }
                ctx.emit('close', code, reason, ctx);
            });
            for(let fn of this[INIT_MIDDLEWARES]) {
                try {
                    await fn.call(ctx, ctx);
                } catch(e) {
                    this.emit('error', e);
                }
            }
            ready = true;
        });
    }

    get apiMethods() {
        return Object.getOwnPropertyNames(this[API_METHODS]);
    }

    get channels() {
        return Object.getOwnPropertyNames(this[CHAN_METHODS]);
    }

    use(fn) {
        checkType(fn, 'function', 'Middleware');
        this[INIT_MIDDLEWARES].push(fn);
        return this;
    }

    mount(prefix, fns) {
        setFns(prefix, fns, this[API_METHODS], 'Api');
        return this;
    }

    channel(prefix, fns) {
        setFns(prefix, fns, this[CHAN_METHODS], 'Channel');
        return this;
    }

    close(cb) {
        this._server.close(cb);
    }
}

module.exports = ApiServer;

function delay(ms = 0) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function setFns(prefix, fns, target, type) {
    var $prefix, $fns;
    if(typeof prefix === 'object') {
        $prefix = '';
        $fns = prefix;
    } else {
        $prefix = prefix;
        $fns = fns;
    }
    checkType($prefix, 'string', type + ' name prefix');
    if(typeof $fns === 'function') {
        target[$prefix] = $fns;
        return;
    }
    checkType($fns, 'object', type + ' methods');
    const props = Object.getOwnPropertyNames($fns);
    for(let prop of props) {
        let fn = $fns[prop];
        if(typeof fn === 'function') {
            target[$prefix + prop] = fn;
        }
    }
}

function send(socket, payload) {
    const data = msgpack.encode(payload);
    socket.send(data);
}

function createUploadStream(id, ctx) {
    const chunksList = ctx[UPLOAD_CHUNKS][id] = {
        onReady: null,
        first: null,
        last: null
    };
    return new Readable({
        async read() {
            if(chunksList.first === null) {
                await new Promise(onReady => {
                    chunksList.onReady = onReady;
                });
                chunksList.onReady = null;
            }
            const payload = chunksList.first;
            chunksList.first = payload.next;
            if(!payload.next) {
                chunksList.last = null;
            }
            this.push(payload.chunk);
        }
    });
}
<commit_msg>update middleware logic<commit_after>'use strict';

const {Readable} = require('stream');
const {EventEmitter} = require('events');
const {Server: WSServer} = require('ws');
const msgpack = require('msgpack-lite');
const checkType = require('./checkType.js');
const {
    TYPES: {T_REQUEST, T_UPLOAD, T_SUBSCRIBE, T_UNSUBSCRIBE, T_RESOLVE, T_REJECT, T_PUBLISH, T_MESSAGE}
} = require('./constants.js');

const INIT_MIDDLEWARES = Symbol('init middlewares');
const API_METHODS = Symbol('api methods');
const CHAN_METHODS = Symbol('channel methods');
const UPLOAD_CHUNKS = Symbol('upload chunks');
const UNSUBSCRIBE_FNS = Symbol('unsubscribe functions');

const msgTypesFn = {
    [T_REQUEST](ctx, {
        id,
        an: apiName,
        p: payload,
        u: hasUpload
    }) {
        const fn = this[API_METHODS][apiName];
        if(!fn) {
            send(ctx.socket, {
                id,
                t: T_REJECT,
                r: new Error('Unknown api method ' + apiName)
            });
            return;
        }
        const uploadStream = hasUpload ? createUploadStream(id, ctx) : void 0;
        Promise.resolve()
            .then(() => fn.call(ctx, payload, uploadStream, ctx))
            .then(r => send(ctx.socket, {
                id,
                t: T_RESOLVE,
                r
            }))
            .catch(r => send(ctx.socket, {
                id,
                t: T_REJECT,
                r
            }));
    },
    [T_UPLOAD](ctx, {id, c: chunk}) {
        const chunksList = ctx[UPLOAD_CHUNKS][id];
        if(!chunksList) { return; }
        const payload = {
            chunk,
            next: chunksList.first
        };
        chunksList.first = payload;
        if(chunksList.last === null) {
            chunksList.last = payload;
        }
        if(chunk === null) {
            delete ctx[UPLOAD_CHUNKS][id];
        }
        if(chunksList.onReady) {
            chunksList.onReady();
        }
    },
    [T_SUBSCRIBE](ctx, {c: channel}) {
        const fn = this[CHAN_METHODS][channel];
        if(!fn) { return; }
        function publish(payload) {
            send(ctx.socket, {
                t: T_PUBLISH,
                c: channel,
                p: payload
            });
        }
        try {
            const unsubscribe = fn.call(ctx, publish, ctx);
            ctx[UNSUBSCRIBE_FNS][channel] = unsubscribe;
        } catch(e) {
            this.emit('error', e);
        }
    },
    [T_UNSUBSCRIBE](ctx, {c: channel}) {
        const fn = ctx[UNSUBSCRIBE_FNS][channel];
        delete ctx[UNSUBSCRIBE_FNS][channel];
        if(typeof fn !== 'function') { return; }
        try {
            fn.call(ctx, ctx);
        } catch(e) {
            this.emit('error', e);
        }
    },
    [T_MESSAGE](ctx, {p: payload}) {
        ctx.emit('message', payload);
    }
};

class ApiServer extends EventEmitter {
    constructor(options) {
        super();
        this[INIT_MIDDLEWARES] = [];
        this[API_METHODS] = {};
        this[CHAN_METHODS] = {};
        if(options && options.server instanceof WSServer) {
            this._server = options.server;
        } else {
            this._server = new WSServer(options);
        }
        this._server.on('error', (...args) => this.emit('error', ...args));
        this._server.on('listening', (...args) => this.emit('listening', ...args));
        this._server.on('headers', (...args) => this.emit('headers', ...args));
        this._server.on('connection', async (socket, request) => {
            const ctx = Object.assign(new EventEmitter(), {
                socket,
                request,
                session: {},
                closeCode: void 0,
                closeReason: void 0,
                [UPLOAD_CHUNKS]: {},
                [UNSUBSCRIBE_FNS]: {},
                get openedChannels() {
                    return Object.getOwnPropertyNames(this[UNSUBSCRIBE_FNS]);
                },
                send(payload) {
                    send(socket, {
                        t: T_MESSAGE,
                        p: payload
                    });
                    return this;
                },
                close(code = ctx.closeCode, reason = ctx.closeReason) {
                    return socket.close(code, reason);
                }
            });
            var ready = false;
            socket.on('message', async (data) => {
                const payload = msgpack.decode(data);
                while(!ready) {
                    await delay();
                }
                const fn = msgTypesFn[payload.t];
                if(!fn) { return; }
                fn.call(this, ctx, payload);
            });
            socket.on('close', async (code, reason) => {
                while(!ready) {
                    await delay();
                }
                ctx.closeCode = code;
                ctx.closeReason = reason;
                for(let c of ctx.openedChannels) {
                    msgTypesFn[T_UNSUBSCRIBE](ctx, {c});
                }
                ctx.emit('close', code, reason, ctx);
            });
            for(let fn of this[INIT_MIDDLEWARES]) {
                try {
                    await fn.call(ctx, ctx);
                } catch(e) {
                    this.emit('error', e);
                }
            }
            ready = true;
        });
    }

    get apiMethods() {
        return Object.getOwnPropertyNames(this[API_METHODS]);
    }

    get channels() {
        return Object.getOwnPropertyNames(this[CHAN_METHODS]);
    }

    use(_fn) {
        const fn = _fn && typeof _fn.install === 'function' ? _fn.install(this) : _fn;
        checkType(fn, 'function', 'Middleware');
        this[INIT_MIDDLEWARES].push(fn);
        return this;
    }

    mount(prefix, fns) {
        setFns(prefix, fns, this[API_METHODS], 'Api');
        return this;
    }

    channel(prefix, fns) {
        setFns(prefix, fns, this[CHAN_METHODS], 'Channel');
        return this;
    }

    close(cb) {
        this._server.close(cb);
    }
}

module.exports = ApiServer;

function delay(ms = 0) {
    return new Promise(resolve => setTimeout(resolve, ms));
}

function setFns(prefix, fns, target, type) {
    var $prefix, $fns;
    if(typeof prefix === 'object') {
        $prefix = '';
        $fns = prefix;
    } else {
        $prefix = prefix;
        $fns = fns;
    }
    checkType($prefix, 'string', type + ' name prefix');
    if(typeof $fns === 'function') {
        target[$prefix] = $fns;
        return;
    }
    checkType($fns, 'object', type + ' methods');
    const props = Object.getOwnPropertyNames($fns);
    for(let prop of props) {
        let fn = $fns[prop];
        if(typeof fn === 'function') {
            target[$prefix + prop] = fn;
        }
    }
}

function send(socket, payload) {
    const data = msgpack.encode(payload);
    socket.send(data);
}

function createUploadStream(id, ctx) {
    const chunksList = ctx[UPLOAD_CHUNKS][id] = {
        onReady: null,
        first: null,
        last: null
    };
    return new Readable({
        async read() {
            if(chunksList.first === null) {
                await new Promise(onReady => {
                    chunksList.onReady = onReady;
                });
                chunksList.onReady = null;
            }
            const payload = chunksList.first;
            chunksList.first = payload.next;
            if(!payload.next) {
                chunksList.last = null;
            }
            this.push(payload.chunk);
        }
    });
}
<|endoftext|>


<commit_before>//
// Module dependencies
//
var path       = require('path');
var requireAll = require('require-all');
var from       = require('from2');
var through    = require('through2');
var duplex     = require('duplexer2');
var pipeline   = require('multipipe');
var slice      = Array.prototype.slice;

//
// Import functions
//
var inputs     = requireAll(path.join(__dirname, 'inputs'));
var outputs    = requireAll(path.join(__dirname, 'outputs'));
var transforms = requireAll(path.join(__dirname, 'transforms'));
var consumers  = requireAll(path.join(__dirname, 'consumers'));

//
// Create a new stream
//
function createStream() {
  var streams = slice.call(arguments);
  return new Stream(streams);
}

//
// Exports
//
module.exports = exports = createStream;

exports.Stream   = Stream;
exports.pipeline = pipeline;
exports.through  = through;
exports.duplex   = duplex;
exports.from     = from;

//
// Stream constructor
//
function Stream(streams, options, input) {
  options = options || {};

  var len = Array.isArray(streams) ? streams.length : 0;

  // No streams passed, create one
  if(!len) {
    this._wrapped = through.obj();
  }
  else {
    // Convert Stream instances to node streams
    streams = streams.map(function(item) {
      return item && item.toStream ? item.toStream() : item;
    });

    // Assign _wrapped
    if(len === 1) this._wrapped = streams[0];
    else this._wrapped = pipeline.apply(this, streams);
  }

  // The original input stream is passed when chaining.
  // This is so toStream() can return a duplex stream covering
  // the entire pipeline and not just the last stream added.
  if(input) this._input = input;
  else {
    this._input = through.obj();
    this._input.pipe(this._wrapped);
  }
}

//
// Return a duplex Stream which writes to
// the first stream and reads from the last.
//
Stream.prototype.toStream = function() {
  return duplex(this._input, this._wrapped);
};

//
// Return the raw stream, piped to the stream provided
//
Stream.prototype.pipe = function(stream) {
  return this._wrapped.pipe(stream);
};

//
// Add input functions to Stream
// Input functions return a readable Stream.
//
Object.keys(inputs).forEach(function(fn) {
  exports[fn]           = inputs[fn];
  Stream.prototype[fn]  = createChainableInput(fn);
});

//
// Add output functions to Stream
// Output functions return a writeable Stream.
//
Object.keys(outputs).forEach(function(fn) {
  exports[fn]          = outputs[fn];
  Stream.prototype[fn] = createChainableOutput(fn);
});

//
// Add transform functions to Stream
// Transform functions return a duplex/through Stream
//
Object.keys(transforms).forEach(function(fn) {
  exports[fn]          = transforms[fn];
  Stream.prototype[fn] = createChainableTransform(fn);
});

//
// Add consumer functions to Stream
// Consumer functions return the unmodified Stream they are passed
//
Object.keys(consumers).forEach(function(fn) {
  exports[fn]          = consumers[fn];
  Stream.prototype[fn] = createChainableConsumer(fn);
});

//
// Create a chainable version of an output function
//
function createChainableOutput(fn) {
  return function() {
    var next = exports[fn].apply(null, arguments);

    if(this._wrapped && this._wrapped.readable) {
      this._wrapped.pipe(next);
      this._wrapped.on('error', next.emit.bind(next, 'error'));
    }

    if(next.readable) {
      return new Stream([next], {}, this._input);
    }

    return this;
  };
}

//
// Create a chainable version of an input function
//
function createChainableInput(fn) {
  return function() {
    var next  = exports[fn].apply(null, arguments);

    if(this._input && this._input.writable) {
      next.pipe(this._input);
    }

    return this;
  };
}

//
// Create a chainable version of a consumer function
//
function createChainableConsumer(fn) {
  return function() {
    if(this._wrapped) {
      var args = slice.call(arguments);
      args.unshift(this._wrapped);
      exports[fn].apply(null, args);
    }

    return this;
  };
}

//
// Create a chainable version of a transform function
//
function createChainableTransform(fn) {
  return function() {
    var next = exports[fn].apply(null, arguments);

    if(this._wrapped && this._wrapped.readable) {
      this._wrapped.pipe(next);
      this._wrapped.on('error', next.emit.bind(next, 'error'));
    }

    return new Stream([next], {}, this._input);
  };
}
<commit_msg>Stylin'<commit_after>//
// pied-piper: Stream and pipeline utilities for node.js
// ======================================================
//
// Licence:  http://opensource.org/licenses/MIT
// NPM:      http://npmjs.org/package/pied-piper
// GitHub:   http://github.com/jameswyse/pied-piper
//

//
// Module dependencies
//
var path       = require('path');
var from       = require('from2');
var through    = require('through2');
var duplex     = require('duplexer2');
var pipeline   = require('multipipe');
var requireAll = require('require-all');

//
// Shortcuts
//
var slice      = Array.prototype.slice;
var resolve    = path.resolve.bind(null, __dirname);
var loader     = function(dir) { return requireAll(resolve(dir)); };

//
// Import functions
//
var inputs     = loader('inputs');
var outputs    = loader('outputs');
var transforms = loader('transforms');
var consumers  = loader('consumers');

//
// Create a new stream
//
function createStream() {
  var streams = slice.call(arguments);
  return new Stream(streams);
}

//
// Exports
//
module.exports = exports = createStream;

exports.Stream   = Stream;
exports.pipeline = pipeline;
exports.through  = through;
exports.duplex   = duplex;
exports.from     = from;

//
// Stream constructor
//
function Stream(streams, options, input) {
  options = options || {};

  var len = Array.isArray(streams) ? streams.length : 0;

  // No streams passed, create one
  if(!len) {
    this._wrapped = through.obj();
  }
  else {
    // Convert Stream instances to node streams
    streams = streams.map(function(item) {
      return item && item.toStream ? item.toStream() : item;
    });

    // Assign _wrapped
    if(len === 1) {
      this._wrapped = streams[0];
    }
    else {
      this._wrapped = pipeline.apply(this, streams);
    }
  }

  // The original input stream is passed when chaining.
  // This is so toStream() can return a duplex stream covering
  // the entire pipeline and not just the last stream added.
  if(input) {
    this._input = input;
  }
  else {
    this._input = through.obj();
    this._input.pipe(this._wrapped);
  }
}

//
// Return a duplex Stream which writes to
// the first stream and reads from the last.
//
Stream.prototype.toStream = function() {
  return duplex(this._input, this._wrapped);
};

//
// Return the raw stream, piped to the stream provided
//
Stream.prototype.pipe = function(stream) {
  return this._wrapped.pipe(stream);
};

//
// Add input functions to Stream
// Input functions return a readable Stream.
//
Object.keys(inputs).forEach(function(fn) {
  exports[fn]           = inputs[fn];
  Stream.prototype[fn]  = createChainableInput(fn);
});

//
// Add output functions to Stream
// Output functions return a writeable Stream.
//
Object.keys(outputs).forEach(function(fn) {
  exports[fn]          = outputs[fn];
  Stream.prototype[fn] = createChainableOutput(fn);
});

//
// Add transform functions to Stream
// Transform functions return a duplex/through Stream
//
Object.keys(transforms).forEach(function(fn) {
  exports[fn]          = transforms[fn];
  Stream.prototype[fn] = createChainableTransform(fn);
});

//
// Add consumer functions to Stream
// Consumer functions return the unmodified Stream they are passed
//
Object.keys(consumers).forEach(function(fn) {
  exports[fn]          = consumers[fn];
  Stream.prototype[fn] = createChainableConsumer(fn);
});

//
// Create a chainable version of an output function
//
function createChainableOutput(fn) {
  return function() {
    var next = exports[fn].apply(null, arguments);

    if(this._wrapped && this._wrapped.readable) {
      this._wrapped.pipe(next);
      this._wrapped.on('error', next.emit.bind(next, 'error'));
    }

    if(next.readable) {
      return new Stream([next], {}, this._input);
    }

    return this;
  };
}

//
// Create a chainable version of an input function
//
function createChainableInput(fn) {
  return function() {
    var next  = exports[fn].apply(null, arguments);

    if(this._input && this._input.writable) {
      next.pipe(this._input);
    }

    return this;
  };
}

//
// Create a chainable version of a consumer function
//
function createChainableConsumer(fn) {
  return function() {
    if(this._wrapped) {
      var args = slice.call(arguments);
      args.unshift(this._wrapped);
      exports[fn].apply(null, args);
    }

    return this;
  };
}

//
// Create a chainable version of a transform function
//
function createChainableTransform(fn) {
  return function() {
    var next = exports[fn].apply(null, arguments);

    if(this._wrapped && this._wrapped.readable) {
      this._wrapped.pipe(next);
      this._wrapped.on('error', next.emit.bind(next, 'error'));
    }

    return new Stream([next], {}, this._input);
  };
}
<|endoftext|>


<commit_before>"use strict"

const Stream = require("stream");
const Emit = require("./Emit");

module.exports = (options, sequence_id) => {

    const io_stream = new Stream.Duplex({

        // input
        write(chunk, enc, done) {

            // write to input and handle back pressure
            if (!io_stream.input.write(chunk)) {
                io_stream.cork();
                io_stream.input.once('drain', () => {
                    io_stream.uncork()
                });
            }

            done();
        },

        // output
        read(size) {

            // wait for output source to be set
            if (io_stream.output && io_stream.output.isPaused()) {
                io_stream.output.resume();
            }
        }
    });

    // error helper
    io_stream.error = (err) => {
        io_stream.emit('error', err);
    };

    // create input stream for handlers
    io_stream.id = sequence_id;
    io_stream.input = Stream.PassThrough();
    io_stream.input.on('error', io_stream.error);
    io_stream.input.cork();
    io_stream.on('end', () => {
        io_stream.input.push(null);
    });

    // setup default error handling
    setImmediate(() => {
        if (!io_stream.listenerCount('error')) {
            io_stream.on('error', (err) => {console.error(err)});
        }
    });

    // emit sequence and get out stream
    io_stream.setup = (scope, sequence, data, next) => {
        console.log(arguments);
        /*Emit(scope, sequence, data, io_stream.input, (err, data, stream) => {

            io_stream.output = stream;

            if (typeof next === "function") {
                next(err, data, io_stream);
            }

            if (err) {

                // error sequence
                if (sequence.err) {
                    scope.flow(sequence.err, err);
                }

                return io_stream.error(err);
            }

            // read from ouput
            stream.on('end', () => {io_stream.push(null)});
            stream.on('error', io_stream.error);
            stream.on('data', (chunk) => {
                if (!io_stream.push(chunk)) {
                    stream.pause();
                }
            });

            // open input resume emitting data
            io_stream.input.uncork();
            io_stream.resume();
        });*/
    };

    return io_stream;
};
<commit_msg>finalize flow event stream<commit_after>"use strict"

const Stream = require("stream");
const Emit = require("./Emit");

module.exports = (scope, sequence_id, data, options, done) => {

    if (typeof options !== "object") {
        options = {};
    }

    // input
    options.write = (chunk, enc, done) => {

        // write to input and handle back pressure
        if (!event.input.write(chunk)) {
            event.cork();
            event.input.once('drain', () => {
                event.uncork()
            });
        }

        done();
    };

    // output
    options.read = (size) => {

        // wait for output source to be set
        if (event.output && event.output.isPaused()) {
            event.output.resume();
        }
    };

    const event = new Stream.Duplex(options);

    // error helper
    event.error = (err) => {event.emit('error', err)};

    // event info
    event.scope = scope;
    event.role = (options && options.role) || scope.env.role;
    event.data = data;
    event.done = typeof done === "function" ? done : (err) => {
        err && console.error("Flow.event.done:", sequence_id, err);
    };

    // create input stream for handlers
    event.sequence = sequence_id;
    event.input = Stream.PassThrough();
    event.input.on('error', event.error);
    event.input.cork();
    event.on('end', () => {
        event.input.push(null);
    });

    // emit sequence and get out stream
    event.open = (args) => {

        const sequence = args[0];
        const data  = args[1];
        const stream = args[2];

        event.output = stream;

        // read from ouput
        stream.on('end', () => {event.push(null)});
        stream.on('error', event.error);
        stream.on('data', (chunk) => {
            if (!event.push(chunk)) {
                stream.pause();
            }
        });

        event.done(null, data, event);

        // open input resume emitting data
        event.input.uncork();
        event.resume();
    };

    return event;
};
<|endoftext|>


<commit_before>'use strict';

const Denque = require('denque');
const EventEmitter = require('events').EventEmitter;
const makeCounter = require('../utils').makeCounter;
const MongoError = require('../core/error').MongoError;
const Connection = require('./connection').Connection;
const eachAsync = require('../core/utils').eachAsync;
const connect = require('../core/connection/connect');
const relayEvents = require('../core/utils').relayEvents;

const errors = require('./errors');
const PoolClosedError = errors.PoolClosedError;
const WaitQueueTimeoutError = errors.WaitQueueTimeoutError;

const events = require('./events');
const ConnectionPoolCreatedEvent = events.ConnectionPoolCreatedEvent;
const ConnectionPoolClosedEvent = events.ConnectionPoolClosedEvent;
const ConnectionCreatedEvent = events.ConnectionCreatedEvent;
const ConnectionReadyEvent = events.ConnectionReadyEvent;
const ConnectionClosedEvent = events.ConnectionClosedEvent;
const ConnectionCheckOutStartedEvent = events.ConnectionCheckOutStartedEvent;
const ConnectionCheckOutFailedEvent = events.ConnectionCheckOutFailedEvent;
const ConnectionCheckedOutEvent = events.ConnectionCheckedOutEvent;
const ConnectionCheckedInEvent = events.ConnectionCheckedInEvent;
const ConnectionPoolClearedEvent = events.ConnectionPoolClearedEvent;

const kConnections = Symbol('connections');
const kPermits = Symbol('permits');
const kMinPoolSizeTimer = Symbol('minPoolSizeTimer');
const kGeneration = Symbol('generation');
const kConnectionCounter = Symbol('connectionCounter');
const kCancellationToken = Symbol('cancellationToken');
const kWaitQueue = Symbol('waitQueue');
const kCancelled = Symbol('cancelled');

const VALID_POOL_OPTIONS = new Set([
  // `connect` options
  'host',
  'port',
  'bson',
  'connectionType',
  'monitorCommands',
  'socketTimeout',
  'credentials',
  'compression',

  // node Net options
  'localAddress',
  'localPort',
  'family',
  'hints',
  'lookup',
  'checkServerIdentity',
  'rejectUnauthorized',
  'ALPNProtocols',
  'servername',
  'checkServerIdentity',
  'session',
  'minDHSize',
  'secureContext',

  // spec options
  'maxPoolSize',
  'minPoolSize',
  'maxIdleTimeMS',
  'waitQueueTimeoutMS'
]);

function resolveOptions(options, defaults) {
  const newOptions = Array.from(VALID_POOL_OPTIONS).reduce((obj, key) => {
    if (options.hasOwnProperty(key)) {
      obj[key] = options[key];
    }

    return obj;
  }, {});

  return Object.freeze(Object.assign({}, defaults, newOptions));
}

/**
 * Configuration options for drivers wrapping the node driver.
 *
 * @typedef {Object} ConnectionPoolOptions
 * @property
 * @property {string} [host] The host to connect to
 * @property {number} [port] The port to connect to
 * @property {bson} [bson] The BSON instance to use for new connections
 * @property {number} [maxPoolSize=100] The maximum number of connections that may be associated with a pool at a given time. This includes in use and available connections.
 * @property {number} [minPoolSize=0] The minimum number of connections that MUST exist at any moment in a single connection pool.
 * @property {number} [maxIdleTimeMS] The maximum amount of time a connection should remain idle in the connection pool before being marked idle.
 * @property {number} [waitQueueTimeoutMS=10000] The maximum amount of time operation execution should wait for a connection to become available.
 */

/**
 * A pool of connections which dynamically resizes, and emit events related to pool activity
 *
 * @property {number} generation An integer representing the SDAM generation of the pool
 * @property {number} totalConnectionCount An integer expressing how many total connections (active + in use) the pool currently has
 * @property {number} availableConnectionCount An integer expressing how many connections are currently available in the pool.
 * @property {string} address The address of the endpoint the pool is connected to
 *
 * @emits ConnectionPool#connectionPoolCreated
 * @emits ConnectionPool#connectionPoolClosed
 * @emits ConnectionPool#connectionCreated
 * @emits ConnectionPool#connectionReady
 * @emits ConnectionPool#connectionClosed
 * @emits ConnectionPool#connectionCheckOutStarted
 * @emits ConnectionPool#connectionCheckOutFailed
 * @emits ConnectionPool#connectionCheckedOut
 * @emits ConnectionPool#connectionCheckedIn
 * @emits ConnectionPool#connectionPoolCleared
 */
class ConnectionPool extends EventEmitter {
  /**
   * Create a new Connection Pool
   *
   * @param {ConnectionPoolOptions} options
   */
  constructor(options) {
    super();
    options = options || {};

    this.closed = false;
    this.options = resolveOptions(options, {
      connectionType: Connection,
      maxPoolSize: typeof options.maxPoolSize === 'number' ? options.maxPoolSize : 100,
      minPoolSize: typeof options.minPoolSize === 'number' ? options.minPoolSize : 0,
      maxIdleTimeMS: typeof options.maxIdleTimeMS === 'number' ? options.maxIdleTimeMS : 0,
      waitQueueTimeoutMS:
        typeof options.waitQueueTimeoutMS === 'number' ? options.waitQueueTimeoutMS : 10000,
      autoEncrypter: options.autoEncrypter
    });

    if (options.minSize > options.maxSize) {
      throw new TypeError('Pool minimum size must not be greater than maxiumum pool size');
    }

    this[kConnections] = new Denque();
    this[kPermits] = this.options.maxPoolSize;
    this[kMinPoolSizeTimer] = undefined;
    this[kGeneration] = 0;
    this[kConnectionCounter] = makeCounter(1);
    this[kCancellationToken] = new EventEmitter();
    this[kCancellationToken].setMaxListeners(Infinity);
    this[kWaitQueue] = new Denque();

    process.nextTick(() => {
      this.emit('connectionPoolCreated', new ConnectionPoolCreatedEvent(this));
      ensureMinPoolSize(this);
    });
  }

  /**
   * Check a connection out of this pool. The connection will continue to be tracked, but no reference to it
   * will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
   * explicitly destroyed by the new owner.
   *
   * @param {ConnectionPool~checkOutCallback} callback
   */
  checkOut(callback) {
    this.emit('connectionCheckOutStarted', new ConnectionCheckOutStartedEvent(this));

    if (this.closed) {
      this.emit('connectionCheckOutFailed', new ConnectionCheckOutFailedEvent(this, 'poolClosed'));
      callback(new PoolClosedError(this));
      return;
    }

    const pool = this;
    const waitQueueTimeoutMS = this.options.waitQueueTimeoutMS;

    // add this request to the wait queue
    const waitQueueMember = { callback };
    waitQueueMember.timer = setTimeout(() => {
      waitQueueMember[kCancelled] = true;
      waitQueueMember.timer = undefined;

      pool.emit('connectionCheckOutFailed', new ConnectionCheckOutFailedEvent(pool, 'timeout'));
      waitQueueMember.callback(new WaitQueueTimeoutError(pool));
    }, waitQueueTimeoutMS);

    // place the member at the end of the wait queue
    this[kWaitQueue].push(waitQueueMember);

    // process the wait queue
    processWaitQueue(this);
  }

  /**
   * Check a connection into the pool.
   *
   * @param {Connection} connection The connection to check in
   */
  checkIn(connection) {
    const poolClosed = this.closed;
    const stale = connectionIsStale(this, connection);
    const willDestroy = !!(poolClosed || stale || connection.closed);

    // Properly adjust state of connection
    if (!willDestroy) {
      connection.markAvailable();

      this[kConnections].push(connection);
    }

    this.emit('connectionCheckedIn', new ConnectionCheckedInEvent(this, connection));

    if (willDestroy) {
      const reason = connection.closed ? 'error' : poolClosed ? 'poolClosed' : 'stale';
      destroyConnection(this, connection, reason);
    }

    processWaitQueue(this);
  }

  /**
   * Clear the pool
   *
   * Pool reset is handled by incrementing the pool's generation count. Any existing connection of a
   * previous generation will eventually be pruned during subsequent checkouts.
   */
  clear() {
    this[kGeneration] += 1;
    this.emit('connectionPoolCleared', new ConnectionPoolClearedEvent(this));
  }

  /**
   * Close the pool
   *
   * @param {object} [options] Optional settings
   * @param {boolean} [options.force] Force close connections
   * @param {Function} callback
   */
  close(options, callback) {
    if (typeof options === 'function') {
      callback = options;
    }

    options = Object.assign({ force: false }, options);
    if (this.closed) {
      return callback();
    }

    // immediately cancel any in-flight connections
    this[kCancellationToken].emit('cancel');

    // drain the wait queue
    while (this[kWaitQueue].length) {
      const waitQueueMember = this[kWaitQueue].pop();
      clearTimeout(waitQueueMember.timer);
      if (!waitQueueMember[kCancelled]) {
        waitQueueMember.callback(new MongoError('connection pool closed'));
      }
    }

    // clear the min pool size timer
    if (this[kMinPoolSizeTimer]) {
      clearTimeout(this[kMinPoolSizeTimer]);
    }

    // end the connection counter
    if (typeof this[kConnectionCounter].return === 'function') {
      this[kConnectionCounter].return();
    }

    // mark the pool as closed immediately
    this.closed = true;

    eachAsync(
      this[kConnections].toArray(),
      (conn, cb) => {
        this.emit('connectionClosed', new ConnectionClosedEvent(this, conn, 'poolClosed'));
        conn.destroy(options, cb);
      },
      err => {
        this[kConnections].clear();
        this.emit('connectionPoolClosed', new ConnectionPoolClosedEvent(this));
        callback(err);
      }
    );
  }

  /**
   * Runs a lambda with an implicitly checked out connection, checking that connection back in when the lambda
   * has completed by calling back.
   *
   * NOTE: please note the required signature of `fn`
   *
   * @param {ConnectionPool~withConnectionCallback} fn A function which operates on a managed connection
   * @param {Function} callback The original callback
   * @return {Promise}
   */
  withConnection(fn, callback) {
    this.checkOut((err, conn) => {
      // don't callback with `err` here, we might want to act upon it inside `fn`

      fn(err, conn, (fnErr, result) => {
        if (typeof callback === 'function') {
          if (fnErr) {
            callback(fnErr);
          } else {
            callback(undefined, result);
          }
        }

        if (conn) {
          this.checkIn(conn);
        }
      });
    });
  }

  get generation() {
    return this[kGeneration];
  }

  get totalConnectionCount() {
    return this[kConnections].length + (this.options.maxPoolSize - this[kPermits]);
  }

  get availableConnectionCount() {
    return this[kConnections].length;
  }

  get address() {
    return `${this.options.host}:${this.options.port}`;
  }

  // Private Helpers
  _propagateError() {
    return;
  }
}

function ensureMinPoolSize(pool) {
  if (pool.closed) {
    return;
  }

  const minPoolSize = pool.options.minPoolSize;
  for (let i = pool.totalConnectionCount; i < minPoolSize; ++i) {
    createConnection(pool);
  }

  pool[kMinPoolSizeTimer] = setTimeout(() => ensureMinPoolSize(pool), 10);
}

function connectionIsStale(pool, connection) {
  return connection.generation !== pool[kGeneration];
}

function connectionIsIdle(pool, connection) {
  return !!(pool.options.maxIdleTimeMS && connection.idleTime > pool.options.maxIdleTimeMS);
}

function createConnection(pool, callback) {
  const connectOptions = Object.assign(
    {
      id: pool[kConnectionCounter].next().value,
      generation: pool[kGeneration]
    },
    pool.options
  );

  pool[kPermits]--;
  connect(connectOptions, pool[kCancellationToken], (err, connection) => {
    if (err) {
      pool[kPermits]++;

      // NOTE: integrate logger here
      pool._propagateError(err);
      if (typeof callback === 'function') {
        callback(err);
      }

      return;
    }

    // The pool might have closed since we started trying to create a connection
    if (pool.closed) {
      connection.destroy({ force: true });
      return;
    }

    // forward all events from the connection to the pool
    relayEvents(connection, pool, [
      'commandStarted',
      'commandFailed',
      'commandSucceeded',
      'clusterTimeReceived'
    ]);

    pool.emit('connectionCreated', new ConnectionCreatedEvent(pool, connection));

    connection.markAvailable();
    pool.emit('connectionReady', new ConnectionReadyEvent(pool, connection));

    // if a callback has been provided, check out the connection immediately
    if (typeof callback === 'function') {
      pool.emit('connectionCheckedOut', new ConnectionCheckedOutEvent(pool, connection));
      callback(undefined, connection);
      return;
    }

    // otherwise add it to the pool for later acquisition, and try to process the wait queue
    pool[kConnections].push(connection);
    processWaitQueue(pool);
  });
}

function destroyConnection(pool, connection, reason) {
  pool.emit('connectionClosed', new ConnectionClosedEvent(pool, connection, reason));

  // allow more connections to be created
  pool[kPermits]++;

  // destroy the connection
  process.nextTick(() => connection.destroy());
}

function processWaitQueue(pool) {
  if (pool.closed) {
    return;
  }

  while (pool[kWaitQueue].length && pool.availableConnectionCount) {
    const waitQueueMember = pool[kWaitQueue].peekFront();
    if (waitQueueMember[kCancelled]) {
      pool[kWaitQueue].shift();
      continue;
    }

    const connection = pool[kConnections].shift();
    const isStale = connectionIsStale(pool, connection);
    const isIdle = connectionIsIdle(pool, connection);
    if (!isStale && !isIdle && !connection.closed) {
      pool.emit('connectionCheckedOut', new ConnectionCheckedOutEvent(pool, connection));
      clearTimeout(waitQueueMember.timer);
      pool[kWaitQueue].shift();
      waitQueueMember.callback(undefined, connection);
      return;
    }

    const reason = connection.closed ? 'error' : isStale ? 'stale' : 'idle';
    destroyConnection(pool, connection, reason);
  }

  const maxPoolSize = pool.options.maxPoolSize;
  if (pool[kWaitQueue].length && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
    createConnection(pool);
    return;
  }
}

/**
 * A callback provided to `withConnection`
 *
 * @callback ConnectionPool~withConnectionCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {Connection} connection The managed connection which was checked out of the pool.
 * @param {Function} callback A function to call back after connection management is complete
 */

/**
 * A callback provided to `checkOut`
 *
 * @callback ConnectionPool~checkOutCallback
 * @param {MongoError} error An error instance representing the error during checkout
 * @param {Connection} connection A connection from the pool
 */

/**
 * Emitted once when the connection pool is created
 *
 * @event ConnectionPool#connectionPoolCreated
 * @type {PoolCreatedEvent}
 */

/**
 * Emitted once when the connection pool is closed
 *
 * @event ConnectionPool#connectionPoolClosed
 * @type {PoolClosedEvent}
 */

/**
 * Emitted each time a connection is created
 *
 * @event ConnectionPool#connectionCreated
 * @type {ConnectionCreatedEvent}
 */

/**
 * Emitted when a connection becomes established, and is ready to use
 *
 * @event ConnectionPool#connectionReady
 * @type {ConnectionReadyEvent}
 */

/**
 * Emitted when a connection is closed
 *
 * @event ConnectionPool#connectionClosed
 * @type {ConnectionClosedEvent}
 */

/**
 * Emitted when an attempt to check out a connection begins
 *
 * @event ConnectionPool#connectionCheckOutStarted
 * @type {ConnectionCheckOutStartedEvent}
 */

/**
 * Emitted when an attempt to check out a connection fails
 *
 * @event ConnectionPool#connectionCheckOutFailed
 * @type {ConnectionCheckOutFailedEvent}
 */

/**
 * Emitted each time a connection is successfully checked out of the connection pool
 *
 * @event ConnectionPool#connectionCheckedOut
 * @type {ConnectionCheckedOutEvent}
 */

/**
 * Emitted each time a connection is successfully checked into the connection pool
 *
 * @event ConnectionPool#connectionCheckedIn
 * @type {ConnectionCheckedInEvent}
 */

/**
 * Emitted each time the connection pool is cleared and it's generation incremented
 *
 * @event ConnectionPool#connectionPoolCleared
 * @type {PoolClearedEvent}
 */

module.exports = {
  ConnectionPool
};
<commit_msg>refactor: no wait queue timeout by default<commit_after>'use strict';

const Denque = require('denque');
const EventEmitter = require('events').EventEmitter;
const makeCounter = require('../utils').makeCounter;
const MongoError = require('../core/error').MongoError;
const Connection = require('./connection').Connection;
const eachAsync = require('../core/utils').eachAsync;
const connect = require('../core/connection/connect');
const relayEvents = require('../core/utils').relayEvents;

const errors = require('./errors');
const PoolClosedError = errors.PoolClosedError;
const WaitQueueTimeoutError = errors.WaitQueueTimeoutError;

const events = require('./events');
const ConnectionPoolCreatedEvent = events.ConnectionPoolCreatedEvent;
const ConnectionPoolClosedEvent = events.ConnectionPoolClosedEvent;
const ConnectionCreatedEvent = events.ConnectionCreatedEvent;
const ConnectionReadyEvent = events.ConnectionReadyEvent;
const ConnectionClosedEvent = events.ConnectionClosedEvent;
const ConnectionCheckOutStartedEvent = events.ConnectionCheckOutStartedEvent;
const ConnectionCheckOutFailedEvent = events.ConnectionCheckOutFailedEvent;
const ConnectionCheckedOutEvent = events.ConnectionCheckedOutEvent;
const ConnectionCheckedInEvent = events.ConnectionCheckedInEvent;
const ConnectionPoolClearedEvent = events.ConnectionPoolClearedEvent;

const kConnections = Symbol('connections');
const kPermits = Symbol('permits');
const kMinPoolSizeTimer = Symbol('minPoolSizeTimer');
const kGeneration = Symbol('generation');
const kConnectionCounter = Symbol('connectionCounter');
const kCancellationToken = Symbol('cancellationToken');
const kWaitQueue = Symbol('waitQueue');
const kCancelled = Symbol('cancelled');

const VALID_POOL_OPTIONS = new Set([
  // `connect` options
  'host',
  'port',
  'bson',
  'connectionType',
  'monitorCommands',
  'socketTimeout',
  'credentials',
  'compression',

  // node Net options
  'localAddress',
  'localPort',
  'family',
  'hints',
  'lookup',
  'checkServerIdentity',
  'rejectUnauthorized',
  'ALPNProtocols',
  'servername',
  'checkServerIdentity',
  'session',
  'minDHSize',
  'secureContext',

  // spec options
  'maxPoolSize',
  'minPoolSize',
  'maxIdleTimeMS',
  'waitQueueTimeoutMS'
]);

function resolveOptions(options, defaults) {
  const newOptions = Array.from(VALID_POOL_OPTIONS).reduce((obj, key) => {
    if (options.hasOwnProperty(key)) {
      obj[key] = options[key];
    }

    return obj;
  }, {});

  return Object.freeze(Object.assign({}, defaults, newOptions));
}

/**
 * Configuration options for drivers wrapping the node driver.
 *
 * @typedef {Object} ConnectionPoolOptions
 * @property
 * @property {string} [host] The host to connect to
 * @property {number} [port] The port to connect to
 * @property {bson} [bson] The BSON instance to use for new connections
 * @property {number} [maxPoolSize=100] The maximum number of connections that may be associated with a pool at a given time. This includes in use and available connections.
 * @property {number} [minPoolSize=0] The minimum number of connections that MUST exist at any moment in a single connection pool.
 * @property {number} [maxIdleTimeMS] The maximum amount of time a connection should remain idle in the connection pool before being marked idle.
 * @property {number} [waitQueueTimeoutMS=0] The maximum amount of time operation execution should wait for a connection to become available. The default is 0 which means there is no limit.
 */

/**
 * A pool of connections which dynamically resizes, and emit events related to pool activity
 *
 * @property {number} generation An integer representing the SDAM generation of the pool
 * @property {number} totalConnectionCount An integer expressing how many total connections (active + in use) the pool currently has
 * @property {number} availableConnectionCount An integer expressing how many connections are currently available in the pool.
 * @property {string} address The address of the endpoint the pool is connected to
 *
 * @emits ConnectionPool#connectionPoolCreated
 * @emits ConnectionPool#connectionPoolClosed
 * @emits ConnectionPool#connectionCreated
 * @emits ConnectionPool#connectionReady
 * @emits ConnectionPool#connectionClosed
 * @emits ConnectionPool#connectionCheckOutStarted
 * @emits ConnectionPool#connectionCheckOutFailed
 * @emits ConnectionPool#connectionCheckedOut
 * @emits ConnectionPool#connectionCheckedIn
 * @emits ConnectionPool#connectionPoolCleared
 */
class ConnectionPool extends EventEmitter {
  /**
   * Create a new Connection Pool
   *
   * @param {ConnectionPoolOptions} options
   */
  constructor(options) {
    super();
    options = options || {};

    this.closed = false;
    this.options = resolveOptions(options, {
      connectionType: Connection,
      maxPoolSize: typeof options.maxPoolSize === 'number' ? options.maxPoolSize : 100,
      minPoolSize: typeof options.minPoolSize === 'number' ? options.minPoolSize : 0,
      maxIdleTimeMS: typeof options.maxIdleTimeMS === 'number' ? options.maxIdleTimeMS : 0,
      waitQueueTimeoutMS:
        typeof options.waitQueueTimeoutMS === 'number' ? options.waitQueueTimeoutMS : 0,
      autoEncrypter: options.autoEncrypter
    });

    if (options.minSize > options.maxSize) {
      throw new TypeError('Pool minimum size must not be greater than maxiumum pool size');
    }

    this[kConnections] = new Denque();
    this[kPermits] = this.options.maxPoolSize;
    this[kMinPoolSizeTimer] = undefined;
    this[kGeneration] = 0;
    this[kConnectionCounter] = makeCounter(1);
    this[kCancellationToken] = new EventEmitter();
    this[kCancellationToken].setMaxListeners(Infinity);
    this[kWaitQueue] = new Denque();

    process.nextTick(() => {
      this.emit('connectionPoolCreated', new ConnectionPoolCreatedEvent(this));
      ensureMinPoolSize(this);
    });
  }

  /**
   * Check a connection out of this pool. The connection will continue to be tracked, but no reference to it
   * will be held by the pool. This means that if a connection is checked out it MUST be checked back in or
   * explicitly destroyed by the new owner.
   *
   * @param {ConnectionPool~checkOutCallback} callback
   */
  checkOut(callback) {
    this.emit('connectionCheckOutStarted', new ConnectionCheckOutStartedEvent(this));

    if (this.closed) {
      this.emit('connectionCheckOutFailed', new ConnectionCheckOutFailedEvent(this, 'poolClosed'));
      callback(new PoolClosedError(this));
      return;
    }

    // add this request to the wait queue
    const waitQueueMember = { callback };

    const pool = this;
    const waitQueueTimeoutMS = this.options.waitQueueTimeoutMS;
    if (waitQueueTimeoutMS) {
      waitQueueMember.timer = setTimeout(() => {
        waitQueueMember[kCancelled] = true;
        waitQueueMember.timer = undefined;

        pool.emit('connectionCheckOutFailed', new ConnectionCheckOutFailedEvent(pool, 'timeout'));
        waitQueueMember.callback(new WaitQueueTimeoutError(pool));
      }, waitQueueTimeoutMS);
    }

    // place the member at the end of the wait queue
    this[kWaitQueue].push(waitQueueMember);

    // process the wait queue
    processWaitQueue(this);
  }

  /**
   * Check a connection into the pool.
   *
   * @param {Connection} connection The connection to check in
   */
  checkIn(connection) {
    const poolClosed = this.closed;
    const stale = connectionIsStale(this, connection);
    const willDestroy = !!(poolClosed || stale || connection.closed);

    // Properly adjust state of connection
    if (!willDestroy) {
      connection.markAvailable();

      this[kConnections].push(connection);
    }

    this.emit('connectionCheckedIn', new ConnectionCheckedInEvent(this, connection));

    if (willDestroy) {
      const reason = connection.closed ? 'error' : poolClosed ? 'poolClosed' : 'stale';
      destroyConnection(this, connection, reason);
    }

    processWaitQueue(this);
  }

  /**
   * Clear the pool
   *
   * Pool reset is handled by incrementing the pool's generation count. Any existing connection of a
   * previous generation will eventually be pruned during subsequent checkouts.
   */
  clear() {
    this[kGeneration] += 1;
    this.emit('connectionPoolCleared', new ConnectionPoolClearedEvent(this));
  }

  /**
   * Close the pool
   *
   * @param {object} [options] Optional settings
   * @param {boolean} [options.force] Force close connections
   * @param {Function} callback
   */
  close(options, callback) {
    if (typeof options === 'function') {
      callback = options;
    }

    options = Object.assign({ force: false }, options);
    if (this.closed) {
      return callback();
    }

    // immediately cancel any in-flight connections
    this[kCancellationToken].emit('cancel');

    // drain the wait queue
    while (this[kWaitQueue].length) {
      const waitQueueMember = this[kWaitQueue].pop();
      clearTimeout(waitQueueMember.timer);
      if (!waitQueueMember[kCancelled]) {
        waitQueueMember.callback(new MongoError('connection pool closed'));
      }
    }

    // clear the min pool size timer
    if (this[kMinPoolSizeTimer]) {
      clearTimeout(this[kMinPoolSizeTimer]);
    }

    // end the connection counter
    if (typeof this[kConnectionCounter].return === 'function') {
      this[kConnectionCounter].return();
    }

    // mark the pool as closed immediately
    this.closed = true;

    eachAsync(
      this[kConnections].toArray(),
      (conn, cb) => {
        this.emit('connectionClosed', new ConnectionClosedEvent(this, conn, 'poolClosed'));
        conn.destroy(options, cb);
      },
      err => {
        this[kConnections].clear();
        this.emit('connectionPoolClosed', new ConnectionPoolClosedEvent(this));
        callback(err);
      }
    );
  }

  /**
   * Runs a lambda with an implicitly checked out connection, checking that connection back in when the lambda
   * has completed by calling back.
   *
   * NOTE: please note the required signature of `fn`
   *
   * @param {ConnectionPool~withConnectionCallback} fn A function which operates on a managed connection
   * @param {Function} callback The original callback
   * @return {Promise}
   */
  withConnection(fn, callback) {
    this.checkOut((err, conn) => {
      // don't callback with `err` here, we might want to act upon it inside `fn`

      fn(err, conn, (fnErr, result) => {
        if (typeof callback === 'function') {
          if (fnErr) {
            callback(fnErr);
          } else {
            callback(undefined, result);
          }
        }

        if (conn) {
          this.checkIn(conn);
        }
      });
    });
  }

  get generation() {
    return this[kGeneration];
  }

  get totalConnectionCount() {
    return this[kConnections].length + (this.options.maxPoolSize - this[kPermits]);
  }

  get availableConnectionCount() {
    return this[kConnections].length;
  }

  get address() {
    return `${this.options.host}:${this.options.port}`;
  }

  // Private Helpers
  _propagateError() {
    return;
  }
}

function ensureMinPoolSize(pool) {
  if (pool.closed) {
    return;
  }

  const minPoolSize = pool.options.minPoolSize;
  for (let i = pool.totalConnectionCount; i < minPoolSize; ++i) {
    createConnection(pool);
  }

  pool[kMinPoolSizeTimer] = setTimeout(() => ensureMinPoolSize(pool), 10);
}

function connectionIsStale(pool, connection) {
  return connection.generation !== pool[kGeneration];
}

function connectionIsIdle(pool, connection) {
  return !!(pool.options.maxIdleTimeMS && connection.idleTime > pool.options.maxIdleTimeMS);
}

function createConnection(pool, callback) {
  const connectOptions = Object.assign(
    {
      id: pool[kConnectionCounter].next().value,
      generation: pool[kGeneration]
    },
    pool.options
  );

  pool[kPermits]--;
  connect(connectOptions, pool[kCancellationToken], (err, connection) => {
    if (err) {
      pool[kPermits]++;

      // NOTE: integrate logger here
      pool._propagateError(err);
      if (typeof callback === 'function') {
        callback(err);
      }

      return;
    }

    // The pool might have closed since we started trying to create a connection
    if (pool.closed) {
      connection.destroy({ force: true });
      return;
    }

    // forward all events from the connection to the pool
    relayEvents(connection, pool, [
      'commandStarted',
      'commandFailed',
      'commandSucceeded',
      'clusterTimeReceived'
    ]);

    pool.emit('connectionCreated', new ConnectionCreatedEvent(pool, connection));

    connection.markAvailable();
    pool.emit('connectionReady', new ConnectionReadyEvent(pool, connection));

    // if a callback has been provided, check out the connection immediately
    if (typeof callback === 'function') {
      pool.emit('connectionCheckedOut', new ConnectionCheckedOutEvent(pool, connection));
      callback(undefined, connection);
      return;
    }

    // otherwise add it to the pool for later acquisition, and try to process the wait queue
    pool[kConnections].push(connection);
    processWaitQueue(pool);
  });
}

function destroyConnection(pool, connection, reason) {
  pool.emit('connectionClosed', new ConnectionClosedEvent(pool, connection, reason));

  // allow more connections to be created
  pool[kPermits]++;

  // destroy the connection
  process.nextTick(() => connection.destroy());
}

function processWaitQueue(pool) {
  if (pool.closed) {
    return;
  }

  while (pool[kWaitQueue].length && pool.availableConnectionCount) {
    const waitQueueMember = pool[kWaitQueue].peekFront();
    if (waitQueueMember[kCancelled]) {
      pool[kWaitQueue].shift();
      continue;
    }

    const connection = pool[kConnections].shift();
    const isStale = connectionIsStale(pool, connection);
    const isIdle = connectionIsIdle(pool, connection);
    if (!isStale && !isIdle && !connection.closed) {
      pool.emit('connectionCheckedOut', new ConnectionCheckedOutEvent(pool, connection));
      clearTimeout(waitQueueMember.timer);
      pool[kWaitQueue].shift();
      waitQueueMember.callback(undefined, connection);
      return;
    }

    const reason = connection.closed ? 'error' : isStale ? 'stale' : 'idle';
    destroyConnection(pool, connection, reason);
  }

  const maxPoolSize = pool.options.maxPoolSize;
  if (pool[kWaitQueue].length && (maxPoolSize <= 0 || pool.totalConnectionCount < maxPoolSize)) {
    createConnection(pool);
    return;
  }
}

/**
 * A callback provided to `withConnection`
 *
 * @callback ConnectionPool~withConnectionCallback
 * @param {MongoError} error An error instance representing the error during the execution.
 * @param {Connection} connection The managed connection which was checked out of the pool.
 * @param {Function} callback A function to call back after connection management is complete
 */

/**
 * A callback provided to `checkOut`
 *
 * @callback ConnectionPool~checkOutCallback
 * @param {MongoError} error An error instance representing the error during checkout
 * @param {Connection} connection A connection from the pool
 */

/**
 * Emitted once when the connection pool is created
 *
 * @event ConnectionPool#connectionPoolCreated
 * @type {PoolCreatedEvent}
 */

/**
 * Emitted once when the connection pool is closed
 *
 * @event ConnectionPool#connectionPoolClosed
 * @type {PoolClosedEvent}
 */

/**
 * Emitted each time a connection is created
 *
 * @event ConnectionPool#connectionCreated
 * @type {ConnectionCreatedEvent}
 */

/**
 * Emitted when a connection becomes established, and is ready to use
 *
 * @event ConnectionPool#connectionReady
 * @type {ConnectionReadyEvent}
 */

/**
 * Emitted when a connection is closed
 *
 * @event ConnectionPool#connectionClosed
 * @type {ConnectionClosedEvent}
 */

/**
 * Emitted when an attempt to check out a connection begins
 *
 * @event ConnectionPool#connectionCheckOutStarted
 * @type {ConnectionCheckOutStartedEvent}
 */

/**
 * Emitted when an attempt to check out a connection fails
 *
 * @event ConnectionPool#connectionCheckOutFailed
 * @type {ConnectionCheckOutFailedEvent}
 */

/**
 * Emitted each time a connection is successfully checked out of the connection pool
 *
 * @event ConnectionPool#connectionCheckedOut
 * @type {ConnectionCheckedOutEvent}
 */

/**
 * Emitted each time a connection is successfully checked into the connection pool
 *
 * @event ConnectionPool#connectionCheckedIn
 * @type {ConnectionCheckedInEvent}
 */

/**
 * Emitted each time the connection pool is cleared and it's generation incremented
 *
 * @event ConnectionPool#connectionPoolCleared
 * @type {PoolClearedEvent}
 */

module.exports = {
  ConnectionPool
};
<|endoftext|>