index.js

'use strict';
const assert = require('assert');
const EventEmitter = require('eventemitter2').EventEmitter2;
const wildcard = require('socketio-wildcard');
const compose = require('koa-compose');

const DEFAULT_DELIMITER = ':';
const DEFAULT_SUCCESS_POSTFIX = 'success';
const DEFAULT_ERROR_POSTFIX = 'error';
const DEFAULT_END_POSTFIX = 'end';
const DEFAULT_SEND_END_EVENT = false;

const Event = require('./event');
const Context = require('./context');


class Router {
    /**
     * constructor - create new router
     *
     * @param  {SocketIO} io
     * @param  {Object} opt - {delimiter: ':'}
     * @return {Router}
     */
    constructor(io, opt) {
        this.io = io;
        this.routes = [];
        this.clients = [];
        this.middlewares = [];

        opt = opt || {};
        this.options = {};
        // configurable
        this.options.delimiter = opt.delimiter || DEFAULT_DELIMITER;
        this.options.sendEndEvent = opt.hasOwnProperty('sendEndEvent') ?
                    opt.sendEndEvent : DEFAULT_SEND_END_EVENT;
        // not configurable params
        this.options.successPostfix = DEFAULT_SUCCESS_POSTFIX;
        this.options.errorPostfix = DEFAULT_ERROR_POSTFIX;
        this.options.endPostfix = DEFAULT_END_POSTFIX;

        if (!opt.noWildcard) {
            io.use(wildcard());
        }

        this.io.on('connection', (s) => this._attachToSocket(s));
    }

    _attachToSocket(socket) {
        let emitter = new EventEmitter({
            delimiter: this.options.delimiter,
            wildcard: true
        });
        this.clients.push(socket);
        socket._emitter = emitter;
        for (let route of this.routes) {
            this._registryRoute(socket, route);
        }
        socket.on('*', (packet) => {
            emitter.emit.apply(emitter, packet.data);
        });
    }

    _registryRoute(socket, route) {
        const self = this;
        let requestName = route.path;
        let funcArray = [];
        let globalMiddlewares = this.middlewares;
        funcArray.push.apply(funcArray, globalMiddlewares);
        funcArray.push(route.middleware);
        let func = compose(funcArray);
        let nameWithUUID = `*${this.options.delimiter}${requestName}`;
        socket._emitter.on(nameWithUUID, function(data) {
            let event = new Event(requestName, this.event);
            let context = new Context(self.options, socket, event, data);
            func(context)
                .then()
                .catch((err) => {
                    let error = Context._formatError(err);
                    if (!context.result) {
                        context.error(error);
                    } else {
                        console.warn('the result has already been sent, but try throw error', err);
                    }
                    if (error.code === 503) {
                        self.onerror(err);
                    }
                })
                .then(() => {
                    if (self.options.sendEndEvent) {
                        context.emit(self.options.endPostfix);
                    }
                });
        });
    }

    /**
     * onerror - error handler
     *
     * @param  {Error} err
     * @private
     */
    onerror(err) {
        assert(err instanceof Error, `non-error thrown: ${err}`);

        let msg = err.stack || err.toString();
        console.error();
        console.error(msg.replace(/^/gm, '  '));
        console.error();
    }



    /**
     * route - add new route
     *
     * @param  {String} path
     * @param  {function|function[]} middlewares - function may return promise
     * @return {undefined}
     *
     * @example // success response
     * let router = new Router(io);
     * router.route('ping', (ctx, next) => {
     *     ctx.success('pong');
     *     return next();
     * });
     *
     * @example // error response
     * let router = new Router(io);
     * router.route('err-ping', (ctx, next) => {
     *     ctx.success('because f*ck you!');
     *     return next();
     * });
     *
     * @example // error response (throw)
     * let router = new Router(io);
     * router.route('err-ping', (ctx, next) => {
     *     throw {err: 'wtf', code: 400, description: 'WTF!?'};
     *     // error always prevent next()
     * });
     *
     * @example // compose middlewares
     * let router = new Router(io);
     * router.route('compose', [
     *     (ctx, next) => {
     *         ctx.result = 1;
     *         return next();
     *     },
     *     (ctx, next) => {
     *         ctx.result++;
     *         return next();
     *     },
     *     (ctx, next) => {
     *         ctx.success(ctx.result);  // return 2
     *     }
     * ]);
     *
     * @example // async handler
     * let co = require('co');
     * let router = new Router(io);
     * router.route('async', co.wrap(function*(ctx, next) {
     *     let data = yield loadFromDataBase();
     *     ctx.success(data);
     * }));
     */
    route(path, middlewares) {
        if (!Array.isArray(middlewares)) {
            middlewares = [middlewares];
        }
        let middleware = compose(middlewares);
        this.routes.push({
            path: path,
            middleware: middleware
        });
    }


    /**
     * use - global middleware
     * @param  {function|function[]} middlewares - function may return promise
     * @return {undefined}
     *
     * @example
     * let router = new Router(io);
     * router.use((ctx, next) => {
     *     console.info(`request ${ctx.event.name}`, ctx.data);
     *     next().then(() => {
     *         console.info(
     *             `request ${ctx.event.name} end with ${ctx.status}`,
     *             ctx.result
     *         );
     *     });
     * });
     * router.route('ping', (ctx) => {ctx.success('pong')});
     */
    use(middlewares) {
        if (!Array.isArray(middlewares)) {
            middlewares = [middlewares];
        }
        let middleware = compose(middlewares);
        this.middlewares.push(middleware);
    }
}

Router.defaultOptions = {
    delimiter: DEFAULT_DELIMITER,
    sendEndEvent: false,
    successPostfix: DEFAULT_SUCCESS_POSTFIX,
    errorPostfix: DEFAULT_ERROR_POSTFIX,
    endPostfix: DEFAULT_END_POSTFIX
};

Router.Context = Context;
Router.Event = Event;

module.exports = Router;