<commit_before>'use strict';

const fs = require('fs');
const office2pdf = require('office2pdf');
const client = require('../../db/client.js');
const mailgun = require('../utils/mailgun.js');
const layout = { layout: 'client' };

const emailAgencyOnClientShortlistAcceptance = require('../utils/mailgun.js').emailAgencyOnClientShortlistAcceptance;
const emailAgencyOnClientShortlistRejection = require('../utils/mailgun.js').emailAgencyOnClientShortlistRejection;
const addRemoveFromSets = require('../../db/redis.js').addRemoveFromSets;
const getVacancyDetails = require('../../db/redis.js').getVacancyDetails;
const getSetMembersInfo = require('../../db/redis.js').getSetMembersInfo;
const removeVacancy = require('../../db/redis.js').removeVacancy;
const formatReason = require('../utils/app.js').formatReason;
const removeCV = require('../../db/redis.js').removeCV;
const getHash = require('../../db/redis.js').getHash;

exports.register = (server, options, next) => {

    server.route([{
        method: 'GET',
        path: '/client/job/{vid}',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let vid = request.params.vid;
                let opts = { title: 'Job page', vid: vid };

                getVacancyDetails(vid, (res) => {
                    if (res) {
                        opts.data = res;
                        let shortlist = vid + 'clientShortlist';
                        getSetMembersInfo(shortlist, (res) => {
                            if (res) {
                                opts.cvs = res;
                            } else {
                                opts.message = 'There are no candidates for you to approve at the moment... come back when there are!';
                            }
                            reply.view('clientjob', opts, layout);
                        });
                    } else {
                        opts.message = 'Sorry, something went wrong, please try again';
                        reply.view('message', opts, layout);
                    }
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/remove/{vid}',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let opts = { title: 'Removal successful!' };
                let vid = request.params.vid;
                let id = request.state.user.profile.id;

                removeVacancy(vid, id, (res) => {
                    if (res) {
                        opts.message = 'You have successfully removed this vacancy... Return home';
                    } else {
                        opts.title = 'Sorry...';
                        opts.message = 'Something went wrong, please go back and try again';
                    }
                    reply.view('message', opts, layout);
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/reject',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let agencyEmail = request.query.email;
                let reason = request.query.reason;
                let cvid = request.query.cvid;
                let vid = request.query.vid;
                let set = vid + 'clientShortlist';

                getHash(vid, (res) => {
                    if (!res) {
                        return reply(false);
                    }

                    let jobTitle = res.jobTitle;
                    let companyName = res.companyName;

                    getHash(cvid, (res) => {
                        if (!res) {
                            return reply(false);
                        }

                        let candidateName = res.candidateName;
                        let formattedReason = formatReason(reason);

                        emailAgencyOnClientShortlistRejection.to = agencyEmail;
                        emailAgencyOnClientShortlistRejection.html = emailAgencyOnClientShortlistRejection.html.replace('-candidateName-', candidateName).replace('-jobTitle-', jobTitle).replace('-companyName-', companyName).replace('-formattedReason-', formattedReason);
                        mailgun.messages().send(emailAgencyOnClientShortlistRejection);

                        removeCV(cvid, set, (res) => { // has to be here otherwise cvid won't exist for the above email function
                            if (res) {
                                reply(true);
                            } else {
                                reply(false);
                            }
                        });
                    });
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/accept',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let vid = request.query.vid;
                let cvid = request.query.cvid;
                let agencyId = request.query.agencyId;
                let jobTitle = request.query.jobTitle;
                let clientId = request.auth.credentials.profile.id;
                let agencyEmail = request.query.email;
                let emailContext = emailAgencyOnClientShortlistAcceptance;
                let candidateName = request.query.candidateName;

                emailContext.to = agencyEmail;
                emailContext.subject = 'Great news! ' + candidateName + ' has been accepted';

                client.sadd(agencyId + 'agencyScheduling', vid, (err) => {
                    if (err) {
                        reply(false);
                    } else {
                        addRemoveFromSets(vid + 'stageOne', vid + 'clientShortlist', cvid, (res) => {
                            if (res) {

                                client.hget(clientId, 'companyName', (err, res) => {
                                    let clientName = res;
                                    client.hget(agencyId, 'contactName', (err, res) => {
                                        let firstName = res.split(' ')[0];
                                        emailContext.html = emailContext.html.replace('-candidateName-', candidateName).replace('-jobTitle-', jobTitle).replace('-firstName-', firstName).replace('-clientName-', clientName);
                                        mailgun.messages().send(emailContext);
                                    });
                                });

                                client.hset(cvid, 'stage', 'stageOne', (err, res) => {
                                    if (res) {
                                        reply(true);
                                    } else {
                                        reply(false);
                                    }
                                });
                            } else {
                                reply(false);
                            }
                        });
                    }
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/file-exists',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let cvUrl = request.query.cvUrl;
                let fileName = cvUrl.split(process.env.BUCKET_URL)[1];
                let filePath = './public/assets/Downloads/';
                //let existingFilesInDownload = fs.readdirSync(filePath);
                existingFilesInDownload.indexOf(fileName) > -1 ?
                    reply(fileName) : reply('notFound');
            }
        }
    }, {
        method: 'GET',
        path: '/client/convert-file',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let fileName = request.query.fileName;
                console.log(fileName);
                let filePath = './public/assets/Downloads/';

                office2pdf.generatePdf(filePath + fileName, function(error, result) {
                    console.log('officetoPdf', result);
                    error ? reply('error') : reply('success');
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/clear-downloads',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let filePath = './public/assets/Downloads/';
                let existingFilesInDownload = fs.readdirSync(filePath);
                existingFilesInDownload.forEach((file, index) => {
                    fs.unlink(filePath + file, (error) => {
                        if (error) {
                            console.log(error);
                        } else {
                            console.log('deleted');
                            if (index === existingFilesInDownload.length) {
                                reply(true);
                            }
                        }

                    });
                });
            }
        }
    }]);
    return next();
};

exports.register.attributes = {
    name: 'ClientJob'
};
<commit_msg>accept button<commit_after>'use strict';

const fs = require('fs');
const office2pdf = require('office2pdf');
const client = require('../../db/client.js');
const mailgun = require('../utils/mailgun.js');
const layout = { layout: 'client' };

const emailAgencyOnClientShortlistAcceptance = require('../utils/mailgun.js').emailAgencyOnClientShortlistAcceptance;
const emailAgencyOnClientShortlistRejection = require('../utils/mailgun.js').emailAgencyOnClientShortlistRejection;
const addRemoveFromSets = require('../../db/redis.js').addRemoveFromSets;
const getVacancyDetails = require('../../db/redis.js').getVacancyDetails;
const getSetMembersInfo = require('../../db/redis.js').getSetMembersInfo;
const removeVacancy = require('../../db/redis.js').removeVacancy;
const formatReason = require('../utils/app.js').formatReason;
const removeCV = require('../../db/redis.js').removeCV;
const getHash = require('../../db/redis.js').getHash;

exports.register = (server, options, next) => {

    server.route([{
        method: 'GET',
        path: '/client/job/{vid}',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let vid = request.params.vid;
                let opts = { title: 'Job page', vid: vid };

                getVacancyDetails(vid, (res) => {
                    if (res) {
                        opts.data = res;
                        let shortlist = vid + 'clientShortlist';
                        getSetMembersInfo(shortlist, (res) => {
                            if (res) {
                                opts.cvs = res;
                            } else {
                                opts.message = 'There are no candidates for you to approve at the moment... come back when there are!';
                            }
                            reply.view('clientjob', opts, layout);
                        });
                    } else {
                        opts.message = 'Sorry, something went wrong, please try again';
                        reply.view('message', opts, layout);
                    }
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/remove/{vid}',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let opts = { title: 'Removal successful!' };
                let vid = request.params.vid;
                let id = request.state.user.profile.id;

                removeVacancy(vid, id, (res) => {
                    if (res) {
                        opts.message = 'You have successfully removed this vacancy... Return home';
                    } else {
                        opts.title = 'Sorry...';
                        opts.message = 'Something went wrong, please go back and try again';
                    }
                    reply.view('message', opts, layout);
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/reject',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let agencyEmail = request.query.email;
                let reason = request.query.reason;
                let cvid = request.query.cvid;
                let vid = request.query.vid;
                let set = vid + 'clientShortlist';

                getHash(vid, (res) => {
                    if (!res) {
                        return reply(false);
                    }

                    let jobTitle = res.jobTitle;
                    let companyName = res.companyName;

                    getHash(cvid, (res) => {
                        if (!res) {
                            return reply(false);
                        }

                        let candidateName = res.candidateName;
                        let formattedReason = formatReason(reason);

                        emailAgencyOnClientShortlistRejection.to = agencyEmail;
                        emailAgencyOnClientShortlistRejection.html = emailAgencyOnClientShortlistRejection.html.replace('-candidateName-', candidateName).replace('-jobTitle-', jobTitle).replace('-companyName-', companyName).replace('-formattedReason-', formattedReason);
                        mailgun.messages().send(emailAgencyOnClientShortlistRejection);

                        removeCV(cvid, set, (res) => { // has to be here otherwise cvid won't exist for the above email function
                            if (res) {
                                reply(true);
                            } else {
                                reply(false);
                            }
                        });
                    });
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/job/accept',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let vid = request.query.vid;
                let cvid = request.query.cvid;
                let agencyId = request.query.agencyId;
                let jobTitle = request.query.jobTitle;
                let clientId = request.auth.credentials.profile.id;
                let agencyEmail = request.query.email;
                let emailContext = emailAgencyOnClientShortlistAcceptance;
                let candidateName = request.query.candidateName;

                emailContext.to = agencyEmail;
                emailContext.subject = 'Great news! ' + candidateName + ' has been accepted';

                client.sadd(agencyId + 'agencyScheduling', vid, (err) => {
                    if (err) {
                        reply(false);
                    } else {
                        addRemoveFromSets(vid + 'stageOne', vid + 'clientShortlist', cvid, (res) => {
                            if (res) {

                                client.hget(clientId, 'companyName', (err, res) => {
                                    let clientName = res;
                                    client.hget(agencyId, 'contactName', (err, res) => {
                                        let firstName = res.split(' ')[0];
                                        emailContext.html = emailContext.html.replace('-candidateName-', candidateName).replace('-jobTitle-', jobTitle).replace('-firstName-', firstName).replace('-clientName-', clientName);
                                        mailgun.messages().send(emailContext);
                                    });
                                });

                                client.hset(cvid, 'stage', 'stageOne', (err, res) => {
                                    if (res) {
                                        reply(true);
                                    } else {
                                        reply(false);
                                    }
                                });
                            } else {
                                reply(false);
                            }
                        });
                    }
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/file-exists',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let cvUrl = request.query.cvUrl;
                let fileName = cvUrl.split(process.env.BUCKET_URL)[1];
                let filePath = './public/assets/Downloads/';
                let existingFilesInDownload = fs.readdirSync(filePath);
                existingFilesInDownload.indexOf(fileName) > -1 ?
                    reply(fileName) : reply('notFound');
            }
        }
    }, {
        method: 'GET',
        path: '/client/convert-file',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let fileName = request.query.fileName;
                console.log(fileName);
                let filePath = './public/assets/Downloads/';

                office2pdf.generatePdf(filePath + fileName, function(error, result) {
                    console.log('officetoPdf', result);
                    error ? reply('error') : reply('success');
                });
            }
        }
    }, {
        method: 'GET',
        path: '/client/clear-downloads',
        config: {
            auth: {
                strategy: 'hrns-cookie',
                scope: 'client'
            },
            handler: (request, reply) => {
                let filePath = './public/assets/Downloads/';
                let existingFilesInDownload = fs.readdirSync(filePath);
                existingFilesInDownload.forEach((file, index) => {
                    fs.unlink(filePath + file, (error) => {
                        if (error) {
                            console.log(error);
                        } else {
                            console.log('deleted');
                            if (index === existingFilesInDownload.length) {
                                reply(true);
                            }
                        }

                    });
                });
            }
        }
    }]);
    return next();
};

exports.register.attributes = {
    name: 'ClientJob'
};
<|endoftext|>


<commit_before>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var crypto = require('crypto');

var assert = require('assert-plus');

var bodyReader = require('./body_reader');
var errors = require('../errors');

///--- API

/**
 * Returns a plugin that will parse the HTTP request body IFF the
 * contentType is application/json.
 *
 * If req.params already contains a given key, that key is skipped and an
 * error is logged.
 *
 * @return {Function} restify handler.
 * @throws {TypeError} on bad input
 */
function jsonBodyParser(options) {
        assert.optionalObject(options, 'options');
        options = options || {};

        var override = options.overrideParams;

        function parseJson(req, res, next) {
                if (req.getContentType() !== 'application/json' || !req.body) {
                        next();
                        return;
                }

                var params;
                try {
                        params = JSON.parse(req.body);
                } catch (e) {
                        next(new errors.InvalidContentError('Invalid JSON: ' +
                                                            e.message));
                        return;
                }

                if (options.mapParams !== false) {
                        if (Array.isArray(params)) {
                                req.params = params;
                        } else if (typeof (params) === 'object') {
                                Object.keys(params).forEach(function (k) {
                                        var p = req.params[k];
                                        if (p && !override)
                                                return (false);
                                        req.params[k] = params[k];
                                        return (true);
                                });
                        } else {
                                req.params = params;
                        }
                } else {
                        req._body = req.body;
                        req.body = params;
                }

                next();
        }

        var chain = [];
        if (!options.bodyReader)
                chain.push(bodyReader(options));
        chain.push(parseJson);
        return (chain);
}

module.exports = jsonBodyParser;
<commit_msg>always parse the body as JSON when the content-type is 'application/json' in header<commit_after>// Copyright 2012 Mark Cavage, Inc.  All rights reserved.

var crypto = require('crypto');

var assert = require('assert-plus');

var bodyReader = require('./body_reader');
var errors = require('../errors');

///--- API

/**
 * Returns a plugin that will parse the HTTP request body IFF the
 * contentType is application/json.
 *
 * If req.params already contains a given key, that key is skipped and an
 * error is logged.
 *
 * @return {Function} restify handler.
 * @throws {TypeError} on bad input
 */
function jsonBodyParser(options) {
        assert.optionalObject(options, 'options');
        options = options || {};

        var override = options.overrideParams;

        function parseJson(req, res, next) {
                if (req.getContentType() !== 'application/json' || !req.body) {
                        next();
                        return;
                }

                var params;
                try {
                        params = JSON.parse(req.body);
                } catch (e) {
                        next(new errors.InvalidContentError('Invalid JSON: ' +
                                                            e.message));
                        return;
                }

                req.body = params;

                if (options.mapParams !== false) {
                        if (Array.isArray(params)) {
                                req.params = params;
                        } else if (typeof (params) === 'object') {
                                Object.keys(params).forEach(function (k) {
                                        var p = req.params[k];
                                        if (p && !override)
                                                return (false);
                                        req.params[k] = params[k];
                                        return (true);
                                });
                        } else {
                                req.params = params;
                        }
                } else {
                        req._body = req.body;
                }

                next();
        }

        var chain = [];
        if (!options.bodyReader)
                chain.push(bodyReader(options));
        chain.push(parseJson);
        return (chain);
}

module.exports = jsonBodyParser;
<|endoftext|>


<commit_before>const async = require('async');

const { versioning } = require('arsenal');
const { DbPrefixes } = versioning.VersioningConstants;

const BackbeatProducer = require('../BackbeatProducer');
const ReplicationQueuePopulator =
    require('../../extensions/replication/ReplicationQueuePopulator');
const NotificationQueuePopulator =
    require('../../extensions/notification/NotificationQueuePopulator');

const { metricsExtension, metricsTypeQueued } =
    require('../../extensions/replication/constants');

class LogReader {

    /**
     * Create a log reader object to populate kafka topics from a
     * single log source (e.g. one raft session)
     *
     * @constructor
     * @param {Object} params - constructor params
     * @param {Object} params.zkClient - zookeeper client object
     * @param {Object} params.kafkaConfig - kafka configuration object
     * @param {Object} [params.logConsumer] - log consumer object
     * @param {function} params.logConsumer.readRecords - function
     *   that fetches records from the log, called by LogReader
     * @param {String} params.logId - log source unique identifier
     * @param {Logger} params.logger - logger object
     * @param {QueuePopulatorExtension[]} params.extensions - array of
     *   queue populator extension modules
     * @param {MetricsProducer} params.metricsProducer - instance of metrics
     *   producer
     */
    constructor(params) {
        this.zkClient = params.zkClient;
        this.kafkaConfig = params.kafkaConfig;
        this.logConsumer = params.logConsumer;
        this.pathToLogOffset = `/logState/${params.logId}/logOffset`;
        this.logOffset = null;
        this.log = params.logger;
        this._producers = {};
        this._extensions = params.extensions;
        this._mProducer = params.metricsProducer;
    }

    _setEntryBatch(entryBatch) {
        this._extensions.forEach(ext => ext.setBatch(entryBatch));
    }

    _unsetEntryBatch() {
        this._extensions.forEach(ext => ext.unsetBatch());
    }

    setLogConsumer(logConsumer) {
        this.logConsumer = logConsumer;
    }

    /**
     * prepare the log reader before starting to populate entries
     *
     * This function may be overriden by subclasses, if source log
     * initialization is needed, in which case they must still call
     * LogReader.setup().
     *
     * @param {function} done - callback function
     * @return {undefined}
     */
    setup(done) {
        this.log.debug('setting up log source',
            {
                method: 'LogReader.setup',
                logSource: this.getLogInfo(),
            });
        this._readLogOffset((err, offset) => {
            if (err) {
                this.log.error('log source setup failed',
                    {
                        method: 'LogReader.setup',
                        logSource: this.getLogInfo(),
                    });
                return done(err);
            }
            this.logOffset = offset;
            this.log.info('log reader is ready to populate replication ' +
                'queue',
                {
                    logSource: this.getLogInfo(),
                    logOffset: this.logOffset,
                });
            return done();
        });
    }

    /**
     * get the next offset to fetch from source log (aka. log sequence
     * number)
     *
     * @return {number} the next log offset to fetch
     */
    getLogOffset() {
        return this.logOffset;
    }

    _readLogOffset(done) {
        const pathToLogOffset = this.pathToLogOffset;
        this.zkClient.getData(pathToLogOffset, (err, data) => {
            if (err) {
                if (err.name !== 'NO_NODE') {
                    this.log.error(
                        'Could not fetch log offset',
                        {
                            method: 'LogReader._readLogOffset',
                            error: err,
                        });
                    return done(err);
                }
                return this.zkClient.mkdirp(pathToLogOffset, err => {
                    if (err) {
                        this.log.error(
                            'Could not pre-create path in zookeeper',
                            {
                                method: 'LogReader._readLogOffset',
                                zkPath: pathToLogOffset,
                                error: err,
                            });
                        return done(err);
                    }
                    return done(null, 1);
                });
            }
            if (data) {
                const logOffset = Number.parseInt(data, 10);
                if (Number.isNaN(logOffset)) {
                    this.log.error(
                        'invalid stored log offset',
                        {
                            method: 'LogReader._readLogOffset',
                            zkPath: pathToLogOffset,
                            logOffset: data.toString(),
                        });
                    return done(null, 1);
                }
                this.log.debug(
                    'fetched current log offset successfully',
                    {
                        method: 'LogReader._readLogOffset',
                        zkPath: pathToLogOffset,
                        logOffset,
                    });
                return done(null, logOffset);
            }
            return done(null, 1);
        });
    }

    _writeLogOffset(done) {
        const pathToLogOffset = this.pathToLogOffset;
        const offsetString = this.logOffset.toString();
        this.zkClient.setData(
            pathToLogOffset,
            Buffer.from(offsetString), -1,
            err => {
                if (err) {
                    this.log.error(
                        'error saving log offset',
                        {
                            method: 'LogReader._writeLogOffset',
                            zkPath: pathToLogOffset,
                            logOffset: this.logOffset,
                        });
                    return done(err);
                }
                this.log.debug(
                    'saved log offset',
                    {
                        method: 'LogReader._writeLogOffset',
                        zkPath: pathToLogOffset,
                        logOffset: this.logOffset,
                    });
                return done();
            });
    }

    /**
     * Process log entries, up to the maximum defined in params
     *
     * @param {Object} [params] - parameters object
     * @param {Number} [params.maxRead] - max number of records to process
     *   from the log. Records may contain multiple entries and all entries
     *   are not queued, so the number of queued entries is not directly
     *   related to this number.
     * @param {function} done - callback when done processing the
     *   entries. Called with an error, or null and a statistics object as
     *   second argument. On success, the statistics contain the following:
     *     - readRecords {Number} - number of records read
     *     - readEntries {Number} - number of entries read (records can
     *       hold multiple entries)
     *     - queuedEntries {Object} - number of new entries queued in
     *       various kafka topics
     *     - processedAll {Boolean} - true if the log has no more unread
     *       records
     * @return {undefined}
     */
    processLogEntries(params, done) {
        const batchState = {
            logRes: null,
            logStats: {
                nbLogRecordsRead: 0,
                nbLogEntriesRead: 0,
            },
            entriesToPublish: {},
            publishedEntries: {},
            currentRecord: {},
        };

        async.waterfall([
            next => this._processReadRecords(params, batchState, next),
            next => this._processPrepareEntries(batchState, next),
            next => this._processFilterEntries(batchState, next),
            next => this._processPublishEntries(batchState, next),
            next => this._processSaveLogOffset(batchState, next),
        ],
            err => {
                if (err) {
                    return done(err);
                }
                const queuedEntries = {};
                const { publishedEntries } = batchState;
                Object.keys(publishedEntries).forEach(topic => {
                    queuedEntries[topic] = publishedEntries[topic].length;
                });
                const processedAll = !params
                    || !params.maxRead
                    || (batchState.logStats.nbLogRecordsRead
                        < params.maxRead);
                return done(null, {
                    readRecords: batchState.logStats.nbLogRecordsRead,
                    readEntries: batchState.logStats.nbLogEntriesRead,
                    queuedEntries,
                    processedAll,
                });
            });
        return undefined;
    }

    processAllLogEntries(params, done) {
        const self = this;
        const countersTotal = {
            readRecords: 0,
            readEntries: 0,
            queuedEntries: {},
        };
        function cbProcess(err, counters) {
            if (err) {
                return done(err);
            }
            countersTotal.readRecords += counters.readRecords;
            countersTotal.readEntries += counters.readEntries;
            Object.keys(counters.queuedEntries).forEach(topic => {
                if (countersTotal.queuedEntries[topic] === undefined) {
                    countersTotal.queuedEntries[topic] = 0;
                }
                countersTotal.queuedEntries[topic] +=
                    counters.queuedEntries[topic];
            });
            self.log.debug('process batch finished',
                { counters, countersTotal });
            if (counters.processedAll) {
                return done(null, countersTotal);
            }
            return self.processLogEntries(params, cbProcess);
        }
        return self.processLogEntries(params, cbProcess);
    }

    /* eslint-disable no-param-reassign */

    _processReadRecords(params, batchState, done) {
        const readOptions = {};
        if (this.logOffset !== undefined) {
            readOptions.startSeq = this.logOffset;
        }
        if (params && params.maxRead !== undefined) {
            readOptions.limit = params.maxRead;
        }
        this.log.debug('reading records', { readOptions });
        this.logConsumer.readRecords(readOptions, (err, res) => {
            if (err) {
                this.log.error(
                    'error while reading log records',
                    {
                        method: 'LogReader._processReadRecords',
                        params, error: err,
                    });
                return done(err);
            }
            this.log.debug(
                'readRecords callback',
                {
                    method: 'LogReader._processReadRecords',
                    params,
                    info: res.info,
                });
            batchState.logRes = res;
            return done();
        });
    }

    _processLogEntry(batchState, record, entry, cb) {
        function _transformKey(key) {
            if (!key) {
                return undefined;
            }
            if (key.startsWith(DbPrefixes.Master)) {
                return key.slice(DbPrefixes.Master.length);
            }
            if (key.startsWith(DbPrefixes.Version)) {
                return key.slice(DbPrefixes.Version.length);
            }
            return key;
        }

        function _executeFilter(ext, entry, cb) {
            if (typeof ext.filterAsync === 'function') {
                ext.filterAsync(entry, cb);
            } else {
                ext.filter(entry);
                process.nextTick(cb);
            }
        }

        async.eachSeries(this._extensions, (ext, next) => {
            let entryValue = entry.value;
            const notifExtension = ext instanceof NotificationQueuePopulator;
            if (notifExtension) {
                entryValue = entry.value || '{}';
            }
            if (!notifExtension &&
                (entry.key === undefined || entry.value === undefined)) {
                return next();
            }
            const entryToFilter = {
                type: entry.type,
                bucket: record.db,
                key: _transformKey(entry.key),
                value: entryValue,
            };
            return _executeFilter(ext, entryToFilter, next);
        }, cb);
    }

    _filterEntries(batchState, record, cb) {
        const { logStats } = batchState;

        return async.eachSeries(record.entries, (entry, next) => {
            logStats.nbLogEntriesRead += 1;
            return this._processLogEntry(batchState, record, entry, next);
        }, cb);
    }

    _processPrepareEntries(batchState, done) {
        const { logRes, logStats } = batchState;

        if (logRes.info.start === null) {
            return done(null);
        }

        logRes.log.on('data', record => {
            this.log.debug('received log data',
                {
                    nbEntries: record.entries.length,
                    info: logRes.info,
                });
            logStats.nbLogRecordsRead += 1;
            batchState.currentRecord = record;
        });
        logRes.log.on('error', err => {
            this.log.error('error fetching entries from log',
                {
                    method: 'LogReader._processPrepareEntries',
                    error: err,
                });
            return done(err);
        });
        logRes.log.on('end', () => {
            this.log.debug('ending record stream', { info: logRes.info });
            return done();
        });
        return undefined;
    }

    _processFilterEntries(batchState, done) {
        const { entriesToPublish, currentRecord } = batchState;
        if (!currentRecord || !currentRecord.entries) {
            return done();
        }
        this._setEntryBatch(entriesToPublish);
        this._filterEntries(batchState, currentRecord, err => {
            batchState.currentRecord = {};
            this._unsetEntryBatch();
            if (err) {
                this.log.error('error filtering entries from log', {
                    method: 'LogReader._processFilterEntries',
                    error: err,
                });
                return done(err);
            }
            return done();
        });
        return undefined;
    }

    _setupProducer(topic, done) {
        if (this._producers[topic] !== undefined) {
            return process.nextTick(done);
        }
        const producer = new BackbeatProducer({
            kafka: { hosts: this.kafkaConfig.hosts },
            topic,
        });
        producer.once('error', done);
        producer.once('ready', () => {
            this.log.debug('producer is ready',
                {
                    kafkaConfig: this.kafkaConfig,
                    topic,
                });
            producer.removeAllListeners('error');
            producer.on('error', err => {
                this.log.error('error from backbeat producer',
                    { topic, error: err });
            });
            this._producers[topic] = producer;
            done();
        });
        return undefined;
    }

    _processPublishEntries(batchState, done) {
        const { entriesToPublish, logRes, logStats } = batchState;

        if (logRes.info.start === null) {
            return done();
        }
        batchState.nextLogOffset =
            logRes.info.start + logStats.nbLogRecordsRead;

        return async.each(Object.keys(entriesToPublish), (topic, done) => {
            const topicEntries = entriesToPublish[topic];
            if (topicEntries.length === 0) {
                return done();
            }
            return async.series([
                done => this._setupProducer(topic, done),
                done => this._producers[topic].send(topicEntries, done),
            ], err => {
                if (err) {
                    this.log.error(
                        'error publishing entries from log to topic',
                        {
                            method: 'LogReader._processPublishEntries',
                            topic,
                            entryCount: topicEntries.length,
                            error: err,
                        });
                    return done(err);
                }
                this.log.debug('entries published successfully to topic',
                    {
                        method: 'LogReader._processPublishEntries',
                        topic,
                        entryCount: topicEntries.length,
                    });
                batchState.publishedEntries[topic] = topicEntries;
                return done();
            });
        }, err => {
            // TODO: On error, produce error metrics
            if (err) {
                return done(err);
            }
            // Find the CRR Class extension
            const crrExtension = this._extensions.find(ext => (
                ext instanceof ReplicationQueuePopulator
            ));
            if (crrExtension) {
                const extMetrics = crrExtension.getAndResetMetrics();
                if (Object.keys(extMetrics).length > 0) {
                    this._mProducer.publishMetrics(extMetrics,
                        metricsTypeQueued, metricsExtension, () => { });
                }
            }
            return done();
        });
    }

    _processSaveLogOffset(batchState, done) {
        if (batchState.nextLogOffset !== undefined &&
            batchState.nextLogOffset !== this.logOffset) {
            this.logOffset = batchState.nextLogOffset;
            return this._writeLogOffset(done);
        }
        return process.nextTick(() => done());
    }

    /* eslint-enable no-param-reassign */

    /**
     * return an object containing useful info about the log
     * source. The default implementation returns an empty object,
     * subclasses may override to provide source-specific info.
     *
     * @return {object} log info object
     */
    getLogInfo() {
        return {};
    }
}

module.exports = LogReader;
<commit_msg>bf: S3C-3306 Correction in accumulating records<commit_after>const async = require('async');

const { versioning } = require('arsenal');
const { DbPrefixes } = versioning.VersioningConstants;

const BackbeatProducer = require('../BackbeatProducer');
const ReplicationQueuePopulator =
    require('../../extensions/replication/ReplicationQueuePopulator');
const NotificationQueuePopulator =
    require('../../extensions/notification/NotificationQueuePopulator');

const { metricsExtension, metricsTypeQueued } =
    require('../../extensions/replication/constants');

class LogReader {

    /**
     * Create a log reader object to populate kafka topics from a
     * single log source (e.g. one raft session)
     *
     * @constructor
     * @param {Object} params - constructor params
     * @param {Object} params.zkClient - zookeeper client object
     * @param {Object} params.kafkaConfig - kafka configuration object
     * @param {Object} [params.logConsumer] - log consumer object
     * @param {function} params.logConsumer.readRecords - function
     *   that fetches records from the log, called by LogReader
     * @param {String} params.logId - log source unique identifier
     * @param {Logger} params.logger - logger object
     * @param {QueuePopulatorExtension[]} params.extensions - array of
     *   queue populator extension modules
     * @param {MetricsProducer} params.metricsProducer - instance of metrics
     *   producer
     */
    constructor(params) {
        this.zkClient = params.zkClient;
        this.kafkaConfig = params.kafkaConfig;
        this.logConsumer = params.logConsumer;
        this.pathToLogOffset = `/logState/${params.logId}/logOffset`;
        this.logOffset = null;
        this.log = params.logger;
        this._producers = {};
        this._extensions = params.extensions;
        this._mProducer = params.metricsProducer;
    }

    _setEntryBatch(entryBatch) {
        this._extensions.forEach(ext => ext.setBatch(entryBatch));
    }

    _unsetEntryBatch() {
        this._extensions.forEach(ext => ext.unsetBatch());
    }

    setLogConsumer(logConsumer) {
        this.logConsumer = logConsumer;
    }

    /**
     * prepare the log reader before starting to populate entries
     *
     * This function may be overriden by subclasses, if source log
     * initialization is needed, in which case they must still call
     * LogReader.setup().
     *
     * @param {function} done - callback function
     * @return {undefined}
     */
    setup(done) {
        this.log.debug('setting up log source',
            {
                method: 'LogReader.setup',
                logSource: this.getLogInfo(),
            });
        this._readLogOffset((err, offset) => {
            if (err) {
                this.log.error('log source setup failed',
                    {
                        method: 'LogReader.setup',
                        logSource: this.getLogInfo(),
                    });
                return done(err);
            }
            this.logOffset = offset;
            this.log.info('log reader is ready to populate replication ' +
                'queue',
                {
                    logSource: this.getLogInfo(),
                    logOffset: this.logOffset,
                });
            return done();
        });
    }

    /**
     * get the next offset to fetch from source log (aka. log sequence
     * number)
     *
     * @return {number} the next log offset to fetch
     */
    getLogOffset() {
        return this.logOffset;
    }

    _readLogOffset(done) {
        const pathToLogOffset = this.pathToLogOffset;
        this.zkClient.getData(pathToLogOffset, (err, data) => {
            if (err) {
                if (err.name !== 'NO_NODE') {
                    this.log.error(
                        'Could not fetch log offset',
                        {
                            method: 'LogReader._readLogOffset',
                            error: err,
                        });
                    return done(err);
                }
                return this.zkClient.mkdirp(pathToLogOffset, err => {
                    if (err) {
                        this.log.error(
                            'Could not pre-create path in zookeeper',
                            {
                                method: 'LogReader._readLogOffset',
                                zkPath: pathToLogOffset,
                                error: err,
                            });
                        return done(err);
                    }
                    return done(null, 1);
                });
            }
            if (data) {
                const logOffset = Number.parseInt(data, 10);
                if (Number.isNaN(logOffset)) {
                    this.log.error(
                        'invalid stored log offset',
                        {
                            method: 'LogReader._readLogOffset',
                            zkPath: pathToLogOffset,
                            logOffset: data.toString(),
                        });
                    return done(null, 1);
                }
                this.log.debug(
                    'fetched current log offset successfully',
                    {
                        method: 'LogReader._readLogOffset',
                        zkPath: pathToLogOffset,
                        logOffset,
                    });
                return done(null, logOffset);
            }
            return done(null, 1);
        });
    }

    _writeLogOffset(done) {
        const pathToLogOffset = this.pathToLogOffset;
        const offsetString = this.logOffset.toString();
        this.zkClient.setData(
            pathToLogOffset,
            Buffer.from(offsetString), -1,
            err => {
                if (err) {
                    this.log.error(
                        'error saving log offset',
                        {
                            method: 'LogReader._writeLogOffset',
                            zkPath: pathToLogOffset,
                            logOffset: this.logOffset,
                        });
                    return done(err);
                }
                this.log.debug(
                    'saved log offset',
                    {
                        method: 'LogReader._writeLogOffset',
                        zkPath: pathToLogOffset,
                        logOffset: this.logOffset,
                    });
                return done();
            });
    }

    /**
     * Process log entries, up to the maximum defined in params
     *
     * @param {Object} [params] - parameters object
     * @param {Number} [params.maxRead] - max number of records to process
     *   from the log. Records may contain multiple entries and all entries
     *   are not queued, so the number of queued entries is not directly
     *   related to this number.
     * @param {function} done - callback when done processing the
     *   entries. Called with an error, or null and a statistics object as
     *   second argument. On success, the statistics contain the following:
     *     - readRecords {Number} - number of records read
     *     - readEntries {Number} - number of entries read (records can
     *       hold multiple entries)
     *     - queuedEntries {Object} - number of new entries queued in
     *       various kafka topics
     *     - processedAll {Boolean} - true if the log has no more unread
     *       records
     * @return {undefined}
     */
    processLogEntries(params, done) {
        const batchState = {
            logRes: null,
            logStats: {
                nbLogRecordsRead: 0,
                nbLogEntriesRead: 0,
            },
            entriesToPublish: {},
            publishedEntries: {},
            currentRecords: [],
        };

        async.waterfall([
            next => this._processReadRecords(params, batchState, next),
            next => this._processPrepareEntries(batchState, next),
            next => this._processFilterEntries(batchState, next),
            next => this._processPublishEntries(batchState, next),
            next => this._processSaveLogOffset(batchState, next),
        ],
            err => {
                if (err) {
                    return done(err);
                }
                const queuedEntries = {};
                const { publishedEntries } = batchState;
                Object.keys(publishedEntries).forEach(topic => {
                    queuedEntries[topic] = publishedEntries[topic].length;
                });
                const processedAll = !params
                    || !params.maxRead
                    || (batchState.logStats.nbLogRecordsRead
                        < params.maxRead);
                return done(null, {
                    readRecords: batchState.logStats.nbLogRecordsRead,
                    readEntries: batchState.logStats.nbLogEntriesRead,
                    queuedEntries,
                    processedAll,
                });
            });
        return undefined;
    }

    processAllLogEntries(params, done) {
        const self = this;
        const countersTotal = {
            readRecords: 0,
            readEntries: 0,
            queuedEntries: {},
        };
        function cbProcess(err, counters) {
            if (err) {
                return done(err);
            }
            countersTotal.readRecords += counters.readRecords;
            countersTotal.readEntries += counters.readEntries;
            Object.keys(counters.queuedEntries).forEach(topic => {
                if (countersTotal.queuedEntries[topic] === undefined) {
                    countersTotal.queuedEntries[topic] = 0;
                }
                countersTotal.queuedEntries[topic] +=
                    counters.queuedEntries[topic];
            });
            self.log.debug('process batch finished',
                { counters, countersTotal });
            if (counters.processedAll) {
                return done(null, countersTotal);
            }
            return self.processLogEntries(params, cbProcess);
        }
        return self.processLogEntries(params, cbProcess);
    }

    /* eslint-disable no-param-reassign */

    _processReadRecords(params, batchState, done) {
        const readOptions = {};
        if (this.logOffset !== undefined) {
            readOptions.startSeq = this.logOffset;
        }
        if (params && params.maxRead !== undefined) {
            readOptions.limit = params.maxRead;
        }
        this.log.debug('reading records', { readOptions });
        this.logConsumer.readRecords(readOptions, (err, res) => {
            if (err) {
                this.log.error(
                    'error while reading log records',
                    {
                        method: 'LogReader._processReadRecords',
                        params, error: err,
                    });
                return done(err);
            }
            this.log.debug(
                'readRecords callback',
                {
                    method: 'LogReader._processReadRecords',
                    params,
                    info: res.info,
                });
            batchState.logRes = res;
            return done();
        });
    }

    _processLogEntry(batchState, record, entry, cb) {
        function _transformKey(key) {
            if (!key) {
                return undefined;
            }
            if (key.startsWith(DbPrefixes.Master)) {
                return key.slice(DbPrefixes.Master.length);
            }
            if (key.startsWith(DbPrefixes.Version)) {
                return key.slice(DbPrefixes.Version.length);
            }
            return key;
        }

        function _executeFilter(ext, entry, cb) {
            if (typeof ext.filterAsync === 'function') {
                ext.filterAsync(entry, cb);
            } else {
                ext.filter(entry);
                process.nextTick(cb);
            }
        }

        async.eachSeries(this._extensions, (ext, next) => {
            let entryValue = entry.value;
            const notifExtension = ext instanceof NotificationQueuePopulator;
            if (notifExtension) {
                entryValue = entry.value || '{}';
            }
            if (!notifExtension &&
                (entry.key === undefined || entry.value === undefined)) {
                return next();
            }
            const entryToFilter = {
                type: entry.type,
                bucket: record.db,
                key: _transformKey(entry.key),
                value: entryValue,
            };
            return _executeFilter(ext, entryToFilter, next);
        }, cb);
    }

    _filterEntries(batchState, record, cb) {
        const { logStats } = batchState;

        return async.eachSeries(record.entries, (entry, next) => {
            logStats.nbLogEntriesRead += 1;
            return this._processLogEntry(batchState, record, entry, next);
        }, cb);
    }

    _processPrepareEntries(batchState, done) {
        const { logRes, logStats } = batchState;

        if (logRes.info.start === null) {
            return done(null);
        }

        logRes.log.on('data', record => {
            this.log.debug('received log data',
                {
                    nbEntries: record.entries.length,
                    info: logRes.info,
                });
            logStats.nbLogRecordsRead += 1;
            batchState.currentRecords.push(record);
        });
        logRes.log.on('error', err => {
            this.log.error('error fetching entries from log',
                {
                    method: 'LogReader._processPrepareEntries',
                    error: err,
                });
            return done(err);
        });
        logRes.log.on('end', () => {
            this.log.debug('ending record stream', { info: logRes.info });
            return done();
        });
        return undefined;
    }

    _processFilterEntry(batchState, record, done) {
        const { entriesToPublish } = batchState;
        if (!record || !record.entries) {
            return done();
        }
        this._setEntryBatch(entriesToPublish);
        return this._filterEntries(batchState, record, err => {
            this._unsetEntryBatch();
            if (err) {
                this.log.error('error filtering entries from log', {
                    method: 'LogReader._processFilterEntries',
                    error: err,
                });
                return done(err);
            }
            return done();
        });
    }

    _processFilterEntries(batchState, done) {
        const { currentRecords } = batchState;
        if (!currentRecords || currentRecords.length === 0) {
            return done();
        }
        async.eachSeries(currentRecords,
            (rec, next) => this._processFilterEntry(batchState, rec, next),
            err => {
                if (err) {
                    this.log.error('error filtering entries from log', {
                        method: 'LogReader._processFilterEntries',
                        error: err,
                    });
                    return done(err);
                }
                return done();
            }
        );
        return undefined;
    }

    _setupProducer(topic, done) {
        if (this._producers[topic] !== undefined) {
            return process.nextTick(done);
        }
        const producer = new BackbeatProducer({
            kafka: { hosts: this.kafkaConfig.hosts },
            topic,
        });
        producer.once('error', done);
        producer.once('ready', () => {
            this.log.debug('producer is ready',
                {
                    kafkaConfig: this.kafkaConfig,
                    topic,
                });
            producer.removeAllListeners('error');
            producer.on('error', err => {
                this.log.error('error from backbeat producer',
                    { topic, error: err });
            });
            this._producers[topic] = producer;
            done();
        });
        return undefined;
    }

    _processPublishEntries(batchState, done) {
        const { entriesToPublish, logRes, logStats } = batchState;

        if (logRes.info.start === null) {
            return done();
        }
        batchState.nextLogOffset =
            logRes.info.start + logStats.nbLogRecordsRead;

        return async.each(Object.keys(entriesToPublish), (topic, done) => {
            const topicEntries = entriesToPublish[topic];
            if (topicEntries.length === 0) {
                return done();
            }
            return async.series([
                done => this._setupProducer(topic, done),
                done => this._producers[topic].send(topicEntries, done),
            ], err => {
                if (err) {
                    this.log.error(
                        'error publishing entries from log to topic',
                        {
                            method: 'LogReader._processPublishEntries',
                            topic,
                            entryCount: topicEntries.length,
                            error: err,
                        });
                    return done(err);
                }
                this.log.debug('entries published successfully to topic',
                    {
                        method: 'LogReader._processPublishEntries',
                        topic,
                        entryCount: topicEntries.length,
                    });
                batchState.publishedEntries[topic] = topicEntries;
                return done();
            });
        }, err => {
            // TODO: On error, produce error metrics
            if (err) {
                return done(err);
            }
            // Find the CRR Class extension
            const crrExtension = this._extensions.find(ext => (
                ext instanceof ReplicationQueuePopulator
            ));
            if (crrExtension) {
                const extMetrics = crrExtension.getAndResetMetrics();
                if (Object.keys(extMetrics).length > 0) {
                    this._mProducer.publishMetrics(extMetrics,
                        metricsTypeQueued, metricsExtension, () => { });
                }
            }
            return done();
        });
    }

    _processSaveLogOffset(batchState, done) {
        if (batchState.nextLogOffset !== undefined &&
            batchState.nextLogOffset !== this.logOffset) {
            this.logOffset = batchState.nextLogOffset;
            return this._writeLogOffset(done);
        }
        return process.nextTick(() => done());
    }

    /* eslint-enable no-param-reassign */

    /**
     * return an object containing useful info about the log
     * source. The default implementation returns an empty object,
     * subclasses may override to provide source-specific info.
     *
     * @return {object} log info object
     */
    getLogInfo() {
        return {};
    }
}

module.exports = LogReader;
<|endoftext|>


<commit_before>var subs = (function() {

  // What server shall we connect to by default?
  var server;

  // What type of connection should we emit ?
  var server_type;

  // Subscription ID counter.
  var id_counter = -1;

  // Subscriptions
  var subs = {};

  // Switch to turn on/off event processing
  var active = true;

  // Error queue for notification
  var errorQueue = [];

  // Instrumentation
  var load1 = profile.load(1000);
  var load5 = profile.load(5000);

  // Get a new subscription ID.
  var newId = function() {
    return id_counter += 1;
  }

  // Close a subscription's websocket channel.
  var close = function(sub) {
    return sub.close();
  }

  // Closes a subscription and deletes it from the subscription manager.
  var unsubscribe = function(sub) {
    clock.unsubscribe(sub.clockSub);
    delete subs[sub.id];
    return sub.close();
  }

  // Unsubscribe from all subscriptions.
  var unsubscribeAll = function() {
    _.each(subs, unsubscribe);
  }

  // Open a subscription's websocket channel.
  var open = function(sub) {
    return sub.open();
  }

  // Emit an expired event if oldest item from prioqueue is in the past.
  var expire = function(sub, now) {
    prioqueue = subs[sub.id].prioqueue;
    if (prioqueue) {
      bottom = prioqueue.bottomPriority();
      if (bottom) {
        expiry = new Date(bottom);
        if (expiry < now) {
          expired = prioqueue.shift();
          expired.time = expiry;
          expired.state = 'expired';
          sub.f(expired);
        }
      }
    }
  }

  var Subscription = Backbone.Model.extend({

    initialize: function(id, query, f) {
      this.id = id;
      this.query = query;
      this.f = f;
      this.prioqueue = new PriorityQueue();
      this.clockSub = false;
    },

    isOpen: function() {
      if (server_type == "ws") {
        return this.ws && (this.ws.readyState != EventSource.CLOSED)
      } else {
        return this.ws && (this.ws.readyState != WebSocket.CLOSED)
      }
    },
    isClosed: function() { return !this.isOpen() },

    url: function() {
      var queryString = "query=" + encodeURIComponent(this.query);
      var loc = window.location, ws_uri;

      if (server_type == "sse") {
        return loc.protocol + "//" + server + "/index?" + queryString;
      } else {
        ws_uri = (loc.protocol == "https:") ? "wss://" : "ws://";
        return ws_uri + server + "/index?subscribe=true&" + queryString;
      }
    },

    open: function() {
      if (this.isOpen()) return this;
      console.log("will open url: " + this.url());

      var ws;
      if (server_type == "sse") {
        ws = this.ws = new EventSource(this.url());
      } else {
        ws = this.ws = new WebSocket(this.url());
      }

      ws.onopen = _.bind(function() {
        console.log("Socket opened", this.query);
      }, this);

      ws.onclose = _.bind(function(e) {
        console.log("Socket closed", this.query);
        this.ws = null;
      }, this);

      ws.onerror = _.bind(function(e) {
        console.log("Socket error", this.query);
        errorQueue.push(e);
        this.close();
      }, this);

      ws.onmessage = _.bind(function(e) {
        console.log("Received", e);
        t1 = Date.now();
        if (active) {
          var event = JSON.parse(e.data);
          event.time = Date.parse(event.time);
          clock.advance(event.time);

          // Update local index.
          if (event.state !== "expired") { // only expired events have no TTL
            // TODO: get a prioqueue supporting delete so we can delete expired
            // events.
            this.prioqueue.update(
              {host: event.host, service: event.service},
              event.time + ((event.ttl || 60) * 1000) // convert TTL to ms
            );
            this.f(event);
          }
        }
        var t2 = Date.now();
        load1(t1, t2);
        load5(t1, t2);

        console.log("Done receiving.");
      }, this);

      return this;
    },

    close: function() {
      if (this.ws) {
        this.ws.close();
        this.ws = void 0;
      }
      return this;
    }
  });

  // Add a subscription. Returns a subscription object. Subscriptions are
  // opened immediately.
  var subscribe = function(query, f) {
    var sub = new Subscription(newId(), query, f).open();
    subs[sub.id] = sub;

    sub.clockSub = clock.subscribe(function(now) {
      expire(sub, now);
    });

    return sub;
  }

  // Reconnect all inactive subs.
  var converge = function() {
    var closed = _.filter(subs, function(sub) {
      return sub.isClosed();
    });
    if (_.isEmpty(closed)) {
      // Done here.
      return;
    }

    // Reopen
    _.each(closed, function(sub) {
      open(sub);
    });
  }

  var notifyErrors = function() {
    if (errorQueue.length == 0) {
      return;
    }
    toastr.warning(errorQueue.length + " socket errors; check the server field above.");
    errorQueue.length = 0;
  }

  // Periodically notify of errors.
  window.setInterval(notifyErrors, 100);

  // Periodically converge.
  setInterval(converge, 6000);

  // When terminating, close all connections.
  $(window).unload(unsubscribeAll);

  return {
    subscribe: subscribe,
    unsubscribe: unsubscribe,
    unsubscribeAll: unsubscribeAll,
    converge: converge,
    load1:   load1,
    load5:   load5,
    subs:    function() { return subs; },
    enable:  function() { active = true; console.log("Subs enabled."); },
    disable: function() { active = false; console.log("Subs disabled."); },
    toggle:  function() {
      active = ! active;
      if (active) {
        console.log("Subs enabled.");
      } else {
        console.log("Subs disabled.");
      }
    },
    server:  function(s) {
      if (s === undefined) {
        return server;
      } else {
        server = s;
        return s;
      }
    },
    server_type: function(s) {
      if (s === undefined) {
        return server_type;
      } else {
        server_type = s;
        return s;
      }
    }
  };
})();
<commit_msg>subs.js: fix a reconnection bug<commit_after>var subs = (function() {

  // What server shall we connect to by default?
  var server;

  // What type of connection should we emit ?
  var server_type;

  // Subscription ID counter.
  var id_counter = -1;

  // Subscriptions
  var subs = {};

  // Switch to turn on/off event processing
  var active = true;

  // Error queue for notification
  var errorQueue = [];

  // Instrumentation
  var load1 = profile.load(1000);
  var load5 = profile.load(5000);

  // Get a new subscription ID.
  var newId = function() {
    return id_counter += 1;
  }

  // Close a subscription's websocket channel.
  var close = function(sub) {
    return sub.close();
  }

  // Closes a subscription and deletes it from the subscription manager.
  var unsubscribe = function(sub) {
    clock.unsubscribe(sub.clockSub);
    delete subs[sub.id];
    return sub.close();
  }

  // Unsubscribe from all subscriptions.
  var unsubscribeAll = function() {
    _.each(subs, unsubscribe);
  }

  // Open a subscription's websocket channel.
  var open = function(sub) {
    return sub.open();
  }

  // Emit an expired event if oldest item from prioqueue is in the past.
  var expire = function(sub, now) {
    prioqueue = subs[sub.id].prioqueue;
    if (prioqueue) {
      bottom = prioqueue.bottomPriority();
      if (bottom) {
        expiry = new Date(bottom);
        if (expiry < now) {
          expired = prioqueue.shift();
          expired.time = expiry;
          expired.state = 'expired';
          sub.f(expired);
        }
      }
    }
  }

  var Subscription = Backbone.Model.extend({

    initialize: function(id, query, f) {
      this.id = id;
      this.query = query;
      this.f = f;
      this.prioqueue = new PriorityQueue();
      this.clockSub = false;
    },

    isOpen: function() {
      if (server_type == "ws") {
        return this.ws && (this.ws.readyState != WebSocket.CLOSED)
      } else {
        return this.ws && (this.ws.readyState != EventSource.CLOSED)
      }
    },
    isClosed: function() { return !this.isOpen() },

    url: function() {
      var queryString = "query=" + encodeURIComponent(this.query);
      var loc = window.location, ws_uri;

      if (server_type == "sse") {
        return loc.protocol + "//" + server + "/index?" + queryString;
      } else {
        ws_uri = (loc.protocol == "https:") ? "wss://" : "ws://";
        return ws_uri + server + "/index?subscribe=true&" + queryString;
      }
    },

    open: function() {
      if (this.isOpen()) return this;

      console.log("will open url: " + this.url());

      var ws;
      if (server_type == "sse") {
        ws = this.ws = new EventSource(this.url());
      } else {
        ws = this.ws = new WebSocket(this.url());
      }

      ws.onopen = _.bind(function() {
        console.log("Socket opened", this.query);
      }, this);

      ws.onclose = _.bind(function(e) {
        console.log("Socket closed", this.query);
        this.ws = null;
      }, this);

      ws.onerror = _.bind(function(e) {
        console.log("Socket error", this.query);
        errorQueue.push(e);
        this.close();
      }, this);

      ws.onmessage = _.bind(function(e) {
        console.log("Received", e);
        t1 = Date.now();
        if (active) {
          var event = JSON.parse(e.data);
          event.time = Date.parse(event.time);
          clock.advance(event.time);

          // Update local index.
          if (event.state !== "expired") { // only expired events have no TTL
            // TODO: get a prioqueue supporting delete so we can delete expired
            // events.
            this.prioqueue.update(
              {host: event.host, service: event.service},
              event.time + ((event.ttl || 60) * 1000) // convert TTL to ms
            );
            this.f(event);
          }
        }
        var t2 = Date.now();
        load1(t1, t2);
        load5(t1, t2);

        console.log("Done receiving.");
      }, this);

      return this;
    },

    close: function() {
      if (this.ws) {
        this.ws.close();
        this.ws = void 0;
      }
      return this;
    }
  });

  // Add a subscription. Returns a subscription object. Subscriptions are
  // opened immediately.
  var subscribe = function(query, f) {
    var sub = new Subscription(newId(), query, f).open();
    subs[sub.id] = sub;

    sub.clockSub = clock.subscribe(function(now) {
      expire(sub, now);
    });

    return sub;
  }

  // Reconnect all inactive subs.
  var converge = function() {
    var closed = _.filter(subs, function(sub) {
      return sub.isClosed();
    });
    if (_.isEmpty(closed)) {
      // Done here.
      return;
    }

    // Reopen
    _.each(closed, function(sub) {
      open(sub);
    });
  }

  var notifyErrors = function() {
    if (errorQueue.length == 0) {
      return;
    }
    toastr.warning(errorQueue.length + " socket errors; check the server field above.");
    errorQueue.length = 0;
  }

  // Periodically notify of errors.
  window.setInterval(notifyErrors, 100);

  // Periodically converge.
  setInterval(converge, 6000);

  // When terminating, close all connections.
  $(window).unload(unsubscribeAll);

  return {
    subscribe: subscribe,
    unsubscribe: unsubscribe,
    unsubscribeAll: unsubscribeAll,
    converge: converge,
    load1:   load1,
    load5:   load5,
    subs:    function() { return subs; },
    enable:  function() { active = true; console.log("Subs enabled."); },
    disable: function() { active = false; console.log("Subs disabled."); },
    toggle:  function() {
      active = ! active;
      if (active) {
        console.log("Subs enabled.");
      } else {
        console.log("Subs disabled.");
      }
    },
    server:  function(s) {
      if (s === undefined) {
        return server;
      } else {
        server = s;
        return s;
      }
    },
    server_type: function(s) {
      if (s === undefined) {
        return server_type;
      } else {
        server_type = s;
        return s;
      }
    }
  };
})();
<|endoftext|>


<commit_before>const createError = require('../utils/createError')

module.exports = ({ adapter, serviceId }) => async ({ request, response }) => {
  if (response) {
    return {}
  }

  if (typeof request.data === 'undefined' || request.data === null) {
    return { request }
  }

  try {
    const serialized = await adapter.serialize(request)
    return { request: serialized }
  } catch (error) {
    return {
      response: createError(`Error serializing data for service '${serviceId}'. ${error}`)
    }
  }
}
<commit_msg>Always serialize request<commit_after>const createError = require('../utils/createError')

module.exports = ({ adapter, serviceId }) => async ({ request, response }) => {
  if (response) {
    return {}
  }

  try {
    const serialized = await adapter.serialize(request)
    return { request: serialized }
  } catch (error) {
    return {
      response: createError(`Error serializing data for service '${serviceId}'. ${error}`)
    }
  }
}
<|endoftext|>


<commit_before>'use strict';

/**
 * Kalabox lib -> services -> kbox module.
 * @module kbox
 */

var path = require('path');
var fs = require('fs');
var mkdirp = require('mkdirp');
var core = require('./../../core.js');
var util = require('./../../util.js');
var helpers = util.helpers;
var engine = require('./../../engine.js');
var serviceInfo = require('./services.js');

var serviceStatus = function(service, callback) {
  var cid = serviceInfo.getCid(service);
  if (cid !== false) {
    engine.inspect(cid, function(err, data) {
      if (err) {
        callback(err, false);
      }
      else {
        callback(null, data);
      }
    });
  }
  else {
    callback(null, false);
  }
};

var isNotRunning = function(service, callback) {
  var cid = serviceInfo.getCid(service);
  if (cid !== false) {
    engine.inspect(cid, function(err, data) {
      if (err) {
        callback(err, false);
      }
      else {
        callback(null, !data.State.Running);
      }
    });
  }
  else {
    callback(null, false);
  }
};

var installService = function(service, callback) {
  engine.build(service, function(err) {
    if (err) {
      callback(err);
    } else {
      if (service.createOpts) {
        serviceStatus(service, function(err, data) {
          if (err) {
            callback(err);
          }
          if (!data) {
            var installOptions = serviceInfo.getInstallOptions(service);
            engine.create(installOptions, function(err, container) {
              if (err) {
                throw err;
              }
              if (container) {
                console.log('installOptions: ' + JSON.stringify(installOptions));
                fs.writeFileSync(serviceInfo.getCidFile(service), container.cid);
                callback(err);
              }
            });
          }
          else {
            callback(null);
          }
        });
      }
      else {
        callback(null);
      }
    }
  });
};

var verifyServiceInstalled = function(service, callback) {
  serviceStatus(service, function(err, data) {
    if (err) {
      callback(err);
    }
    if (!data) {
      var installOptions = serviceInfo.getInstallOptions(service);
      engine.create(installOptions, function(err, container) {
        if (err) {
          throw err;
        }
        if (container) {
          console.log('installOptions: ' + JSON.stringify(installOptions));
          fs.writeFileSync(serviceInfo.getCidFile(service), container.cid);
          callback(err);
        }
      });
    }
    else {
      callback(null);
    }
  });
};

var startService = function(service, callback) {
  var opts = serviceInfo.getStartOptions(service);
  var cid = serviceInfo.getCid(service);
  engine.start(cid, opts, function(err) {
    callback(err);
  });
};

var stopService = function(service, callback) {
  var cid = serviceInfo.getCid(service);
  engine.stop(cid, function(err) {
    callback(err);
  });
};

var start = function(callback) {
  helpers.mapAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      startService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

var halt = function(callback) {
  helpers.mapAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      stopService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

var rebootServices = function(callback) {
  console.log('Startup services need to be restarted!');
  halt(function(err) {
    start(function(err) {
      callback(null);
    });
  });
};

exports.install = function(callback) {
  var cidRoot = serviceInfo.getCidRoot();
  if (!fs.existsSync(cidRoot)) {
    mkdirp.sync(cidRoot);
  }
  helpers.mapAsync(
    serviceInfo.getCoreImages(),
    function(service, done) {
      installService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

exports.verify = function(callback) {
  helpers.findAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      isNotRunning(service, done);
    },
    function(errs, result) {
      if (errs) {
        callback(errs);
      }
      else if (result) {
        rebootServices(callback);
      }
      else {
        callback(null);
      }
    }
  );
};
<commit_msg>#169: maybe less strong but more stable check on services install<commit_after>'use strict';

/**
 * Kalabox lib -> services -> kbox module.
 * @module kbox
 */

var path = require('path');
var fs = require('fs');
var mkdirp = require('mkdirp');
var core = require('./../../core.js');
var util = require('./../../util.js');
var helpers = util.helpers;
var engine = require('./../../engine.js');
var serviceInfo = require('./services.js');

var isNotRunning = function(service, callback) {
  var cid = serviceInfo.getCid(service);
  if (cid !== false) {
    engine.inspect(cid, function(err, data) {
      if (err) {
        callback(err, false);
      }
      else {
        callback(null, !data.State.Running);
      }
    });
  }
  else {
    callback(null, false);
  }
};

var installService = function(service, callback) {
  engine.build(service, function(err) {
    if (err) {
      callback(err);
    } else {
      if (service.createOpts) {
        var cidFile = serviceInfo.getCidFile(service);
        var installOptions = serviceInfo.getInstallOptions(service);
        engine.create(installOptions, function(err, container) {
          if (err) {
            throw err;
          }
          if (container) {
            console.log('installOptions: ' + JSON.stringify(installOptions));
            fs.writeFileSync(cidFile, container.cid);
            callback(err);
          }
        });
      }
      else {
        callback(null);
      }
    }
  });
};

var startService = function(service, callback) {
  var opts = serviceInfo.getStartOptions(service);
  var cid = serviceInfo.getCid(service);
  engine.start(cid, opts, function(err) {
    callback(err);
  });
};

var stopService = function(service, callback) {
  var cid = serviceInfo.getCid(service);
  engine.stop(cid, function(err) {
    callback(err);
  });
};

var start = function(callback) {
  helpers.mapAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      startService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

var halt = function(callback) {
  helpers.mapAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      stopService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

var rebootServices = function(callback) {
  console.log('Startup services need to be started!');
  halt(function(err) {
    start(function(err) {
      callback(null);
    });
  });
};

exports.install = function(callback) {
  var cidRoot = serviceInfo.getCidRoot();
  if (!fs.existsSync(cidRoot)) {
    mkdirp.sync(cidRoot);
  }
  helpers.mapAsync(
    serviceInfo.getCoreImages(),
    function(service, done) {
      installService(service, done);
    },
    function(errs) {
      callback(errs);
    }
  );
};

exports.verify = function(callback) {
  helpers.findAsync(
    serviceInfo.getStartableServices(),
    function(service, done) {
      isNotRunning(service, done);
    },
    function(errs, result) {
      if (errs) {
        callback(errs);
      }
      else if (result) {
        rebootServices(callback);
      }
      else {
        callback(null);
      }
    }
  );
};
<|endoftext|>


<commit_before>'use strict';

var utils = require('../../utils/iframe')
  , random = require('../../utils/random')
  , browser = require('../../utils/browser')
  , util = require('util')
  , EventEmitter = require('events').EventEmitter
  , debug = require('debug')('sockjs-client:receiver:jsonp')
  ;

function JsonpReceiver(url) {
  debug(url);
  var self = this;
  EventEmitter.call(this);

  utils.polluteGlobalNamespace();

  this.id = 'a' + random.string(6);
  var urlWithId = url + '?c=' + encodeURIComponent(utils.WPrefix + '.' + this.id);

  global[utils.WPrefix][this.id] = this._callback.bind(this);
  this._createScript(urlWithId);

  // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty.
  this.timeoutId = setTimeout(function() {
    debug('timeout');
    self._abort(new Error('JSONP script loaded abnormally (timeout)'));
  }, JsonpReceiver.timeout);
}

util.inherits(JsonpReceiver, EventEmitter);

JsonpReceiver.prototype.abort = function () {
  debug('abort');
  if (global[utils.WPrefix][this.id]) {
    var err = new Error('JSONP user aborted read');
    err.code = 1000;
    this._abort(err);
  }
};

JsonpReceiver.timeout = 35000;
JsonpReceiver.scriptErrorTimeout = 1000;

JsonpReceiver.prototype._callback = function (data) {
  debug('_callback', data);
  this._cleanup();
  delete global[utils.WPrefix][this.id];

  if (this.aborting) {
    return;
  }

  if (data) {
    debug('message', data);
    this.emit('message', data);
  }
  this.emit('close', null, 'network');
  this.removeAllListeners();
};

JsonpReceiver.prototype._abort = function (err) {
  debug('_abort', err);
  this._cleanup();
  this.aborting = true;
  this.emit('close', err.code, err.message);
  this.removeAllListeners();
};

JsonpReceiver.prototype._cleanup = function () {
  debug('_cleanup');
  clearTimeout(this.timeoutId);
  if (this.script2) {
    this.script2.parentNode.removeChild(this.script2);
    this.script2 = null;
  }
  if (this.script) {
    var script = this.script;
    // Unfortunately, you can't really abort script loading of
    // the script.
    script.parentNode.removeChild(script);
    script.onreadystatechange = script.onerror =
        script.onload = script.onclick = null;
    this.script = null;
  }
};

JsonpReceiver.prototype._scriptError = function () {
  debug('_scriptError');
  var self = this;
  if (this.errorTimer) {
    return;
  }

  this.errorTimer = setTimeout(function() {
    if (!self.loadedOkay) {
      self._abort(new Error('JSONP script loaded abnormally (onerror)'));
    }
  }, JsonpReceiver.scriptErrorTimeout);
};

JsonpReceiver.prototype._createScript = function (url) {
  debug('_createScript', url);
  var self = this;
  var script = this.script = global.document.createElement('script');
  var script2;  // Opera synchronous load trick.

  script.id = 'a' + random.string(8);
  script.src = url;
  script.type = 'text/javascript';
  script.charset = 'UTF-8';
  script.onerror = this._scriptError.bind(this);
  script.onload = function() {
    debug('onload');
    self._abort(new Error('JSONP script loaded abnormally (onload)'));
  };

  // IE9 fires 'error' event after onreadystatechange or before, in random order.
  // Use loadedOkay to determine if actually errored
  script.onreadystatechange = function() {
    debug('onreadystatechange', script.readyState);
    if (/loaded|closed/.test(script.readyState)) {
      if (script && script.htmlFor && script.onclick) {
        self.loadedOkay = true;
        try {
          // In IE, actually execute the script.
          script.onclick();
        } catch (x) {}
      }
      if (script) {
        self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)'));
      }
    }
  };
  // IE: event/htmlFor/onclick trick.
  // One can't rely on proper order for onreadystatechange. In order to
  // make sure, set a 'htmlFor' and 'event' properties, so that
  // script code will be installed as 'onclick' handler for the
  // script object. Later, onreadystatechange, manually execute this
  // code. FF and Chrome doesn't work with 'event' and 'htmlFor'
  // set. For reference see:
  //   http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
  // Also, read on that about script ordering:
  //   http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
  if (typeof script.async === 'undefined' && global.document.attachEvent) {
    // According to mozilla docs, in recent browsers script.async defaults
    // to 'true', so we may use it to detect a good browser:
    // https://developer.mozilla.org/en/HTML/Element/script
    if (!browser.isOpera()) {
      // Naively assume we're in IE
      try {
          script.htmlFor = script.id;
          script.event = 'onclick';
      } catch (x) {}
      script.async = true;
    } else {
      // Opera, second sync script hack
      script2 = this.script2 = global.document.createElement('script');
      script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};";
      script.async = script2.async = false;
    }
  }
  if (typeof script.async !== 'undefined') {
    script.async = true;
  }

  var head = global.document.getElementsByTagName('head')[0];
  head.insertBefore(script, head.firstChild);
  if (script2) {
    head.insertBefore(script2, head.firstChild);
  }
};

module.exports = JsonpReceiver;
<commit_msg>Don't leak globals from jsonp receiver<commit_after>'use strict';

var utils = require('../../utils/iframe')
  , random = require('../../utils/random')
  , browser = require('../../utils/browser')
  , util = require('util')
  , EventEmitter = require('events').EventEmitter
  , debug = require('debug')('sockjs-client:receiver:jsonp')
  ;

function JsonpReceiver(url) {
  debug(url);
  var self = this;
  EventEmitter.call(this);

  utils.polluteGlobalNamespace();

  this.id = 'a' + random.string(6);
  var urlWithId = url + '?c=' + encodeURIComponent(utils.WPrefix + '.' + this.id);

  global[utils.WPrefix][this.id] = this._callback.bind(this);
  this._createScript(urlWithId);

  // Fallback mostly for Konqueror - stupid timer, 35 seconds shall be plenty.
  this.timeoutId = setTimeout(function() {
    debug('timeout');
    self._abort(new Error('JSONP script loaded abnormally (timeout)'));
  }, JsonpReceiver.timeout);
}

util.inherits(JsonpReceiver, EventEmitter);

JsonpReceiver.prototype.abort = function () {
  debug('abort');
  if (global[utils.WPrefix][this.id]) {
    var err = new Error('JSONP user aborted read');
    err.code = 1000;
    this._abort(err);
  }
};

JsonpReceiver.timeout = 35000;
JsonpReceiver.scriptErrorTimeout = 1000;

JsonpReceiver.prototype._callback = function (data) {
  debug('_callback', data);
  this._cleanup();

  if (this.aborting) {
    return;
  }

  if (data) {
    debug('message', data);
    this.emit('message', data);
  }
  this.emit('close', null, 'network');
  this.removeAllListeners();
};

JsonpReceiver.prototype._abort = function (err) {
  debug('_abort', err);
  this._cleanup();
  this.aborting = true;
  this.emit('close', err.code, err.message);
  this.removeAllListeners();
};

JsonpReceiver.prototype._cleanup = function () {
  debug('_cleanup');
  clearTimeout(this.timeoutId);
  if (this.script2) {
    this.script2.parentNode.removeChild(this.script2);
    this.script2 = null;
  }
  if (this.script) {
    var script = this.script;
    // Unfortunately, you can't really abort script loading of
    // the script.
    script.parentNode.removeChild(script);
    script.onreadystatechange = script.onerror =
        script.onload = script.onclick = null;
    this.script = null;
  }
  delete global[utils.WPrefix][this.id];
};

JsonpReceiver.prototype._scriptError = function () {
  debug('_scriptError');
  var self = this;
  if (this.errorTimer) {
    return;
  }

  this.errorTimer = setTimeout(function() {
    if (!self.loadedOkay) {
      self._abort(new Error('JSONP script loaded abnormally (onerror)'));
    }
  }, JsonpReceiver.scriptErrorTimeout);
};

JsonpReceiver.prototype._createScript = function (url) {
  debug('_createScript', url);
  var self = this;
  var script = this.script = global.document.createElement('script');
  var script2;  // Opera synchronous load trick.

  script.id = 'a' + random.string(8);
  script.src = url;
  script.type = 'text/javascript';
  script.charset = 'UTF-8';
  script.onerror = this._scriptError.bind(this);
  script.onload = function() {
    debug('onload');
    self._abort(new Error('JSONP script loaded abnormally (onload)'));
  };

  // IE9 fires 'error' event after onreadystatechange or before, in random order.
  // Use loadedOkay to determine if actually errored
  script.onreadystatechange = function() {
    debug('onreadystatechange', script.readyState);
    if (/loaded|closed/.test(script.readyState)) {
      if (script && script.htmlFor && script.onclick) {
        self.loadedOkay = true;
        try {
          // In IE, actually execute the script.
          script.onclick();
        } catch (x) {}
      }
      if (script) {
        self._abort(new Error('JSONP script loaded abnormally (onreadystatechange)'));
      }
    }
  };
  // IE: event/htmlFor/onclick trick.
  // One can't rely on proper order for onreadystatechange. In order to
  // make sure, set a 'htmlFor' and 'event' properties, so that
  // script code will be installed as 'onclick' handler for the
  // script object. Later, onreadystatechange, manually execute this
  // code. FF and Chrome doesn't work with 'event' and 'htmlFor'
  // set. For reference see:
  //   http://jaubourg.net/2010/07/loading-script-as-onclick-handler-of.html
  // Also, read on that about script ordering:
  //   http://wiki.whatwg.org/wiki/Dynamic_Script_Execution_Order
  if (typeof script.async === 'undefined' && global.document.attachEvent) {
    // According to mozilla docs, in recent browsers script.async defaults
    // to 'true', so we may use it to detect a good browser:
    // https://developer.mozilla.org/en/HTML/Element/script
    if (!browser.isOpera()) {
      // Naively assume we're in IE
      try {
          script.htmlFor = script.id;
          script.event = 'onclick';
      } catch (x) {}
      script.async = true;
    } else {
      // Opera, second sync script hack
      script2 = this.script2 = global.document.createElement('script');
      script2.text = "try{var a = document.getElementById('" + script.id + "'); if(a)a.onerror();}catch(x){};";
      script.async = script2.async = false;
    }
  }
  if (typeof script.async !== 'undefined') {
    script.async = true;
  }

  var head = global.document.getElementsByTagName('head')[0];
  head.insertBefore(script, head.firstChild);
  if (script2) {
    head.insertBefore(script2, head.firstChild);
  }
};

module.exports = JsonpReceiver;
<|endoftext|>