socket/SocketUnit.js

/**
 * @module flitter-socket/SocketUnit
 */

const Unit = require('libflitter/Unit')
const express = require('express')
const SocketController = require('./Controller')

/**
 * Adds a two-way transactional websocket server to Flitter.
 * @extends module:libflitter/Unit~Unit
 */
class SocketUnit extends Unit {
    static get services() {
        return [...super.services, 'app', 'configs', 'express', 'output', 'routers', 'canon']
    }

    static get name() {
        return 'sockets'
    }

    /**
     * Initialize the SocketUnit. Bootstraps the websocket adapters into the Express
     * app and http/s server, then registers handlers for any route definitions.
     * @param {module:libflitter/app/FlitterApp~FlitterApp} app - the current Flitter app
     * @returns {Promise<void>}
     */
    async go(app){
        // Bootstrap express with the websocket server
        const server = this.express.server
        const ws = require('express-ws')(app.express, server)

        // Store the server for future use
        this.server = ws

        // if we have routing, search for socket route definitions
        if ( app.di().has('routers') ){
            this.output.debug('Loading routing schemas for socket route definitions.')

            // Fetch the registered router schemas
            const schemas = Object.values(this.routers.canonical_items).map(x => x.schema)
            for ( const schema of schemas ) {
                if ( schema.socket ) {
                    const prefix = schema.prefix ? schema.prefix : '/'
                    for ( const route in schema.socket ) {
                        if ( !schema.socket.hasOwnProperty(route) ) continue
                        this.output.debug(`Registering socket handlers for ${prefix} -> ${route}`)

                        let handlers = schema.socket[route]
                        const router = express.Router()

                        if ( typeof handlers === 'string' ) handlers = [handlers]
                        if ( !Array.isArray(handlers) ) throw new Error(`Invalid socket handler definition on ${prefix} -> ${route}`)

                        const final_handler = handlers.pop()

                        // Register middleware handlers
                        for ( const mw of handlers ) {
                            if ( typeof mw === 'string' ) {
                                const mw_fn = this.canon.get(mw)
                                if ( typeof mw_fn !== 'function' ) throw new Error(`Invalid socket middleware. The canonical reference must resolve to a function: ${mw}`)
                                router.use(mw_fn)
                            } else if ( typeof mw === 'function' ) {
                                this.output.warn('Specifying socket handlers as functions is deprecated and will throw an error in the future. Prefer canonical reference names.')
                                router.use(mw)
                            } else throw new Error(`Invalid socket middleware on ${prefix} -> ${route}. Please provide a canonical reference name.`)
                        }

                        // register the final handler - should be canonical ref to SocketController instance
                        if ( typeof final_handler === 'string' ) {
                            const ctrl = this.canon.get(final_handler)
                            if ( !(ctrl instanceof SocketController) ) {
                                throw new Error (`Unable to register socket handler ${final_handler}. Canonical names must resolve to instances of flitter-socket/Controller.`)
                            }

                            router.ws(route, ctrl._connect.bind(ctrl))
                            app.express.use(prefix, router)
                        } else throw new Error(`Invalid socket handler for ${prefix} -> ${route}. Please provide a canonical reference name.`)
                    }
                }
            }
        }
    }

    /**
     * Get the templates provided by the unit.
     * @returns {{controller: {template: ((function(string): string)|*|controller), extension: string, directory: string}}}
     */
    templates(){
        return {
            "socket:controller": {
                template: require('./templates/controller'),
                directory: this.app.directories.controllers,
                extension: '.controller.js'
            }
        }
    }

}

module.exports = exports = SocketUnit