WComorSocketioServer.mjs

import Hapi from '@hapi/hapi'
import Inert from '@hapi/inert' //提供靜態檔案
import { Server } from 'socket.io'
import keys from 'lodash-es/keys.js'
import get from 'lodash-es/get.js'
import genPm from 'wsemi/src/genPm.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import j2o from 'wsemi/src/j2o.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import cint from 'wsemi/src/cint.mjs'
import arrHas from 'wsemi/src/arrHas.mjs'


let SocketIO = Server


/**
 * 建立socket.io伺服器
 *
 * @param {Object} opt 輸入設定參數物件
 * @param {Object} [opt.serverHapi={}] 輸入hapi伺服器物件,若提供,本服務將自動加入api至route。使用外部hapi伺服器時,需開啟跨域功能,或是使用nginx反向代理轉入api請求
 * @param {Integer} [opt.port=8080] 輸入hapi與socket.io伺服器所在port,為hapi與socket.io共用,預設8080
 * @param {String} [opt.pathPolling=undefined] 輸入socket.io伺服器無法使用WebSocket連線自動降級成為輪詢(polling)所在子目錄字串,預設undefined,代表使用'/socket.io'
 * @param {Function} opt.authenticate 輸入使用者身份認證函數,供伺服器端驗證之用,函數會傳入使用者端連線之token參數,回傳為Promise,resolve(true)為驗證通過,resolve(false)為驗證不通過
 * @param {Object} [opt.funcs={}] 輸入伺服器端供使用者端呼叫之函數物件,各key為函數名稱,對應value為函數本體。各函數之輸入需為單一物件,而各函數回傳皆為Promise,可通過resolve與reject回傳結果,預設{}
 * @param {Array} [opt.routes=[]] 輸入伺服器額外掛載routes陣列,預設[]
 * @example
 *
 * import WComorSocketioServer from 'w-comor-socketio/dist/w-comor-socketio-server.umd.js'
 *
 * function random(min, max) {
 *     return Math.floor(Math.random() * max) + min
 * }
 *
 * let opt = {
 *     port: 8080,
 *     authenticate: function(token) {
 *         //使用token驗證使用者身份
 *         return new Promise(function(resolve, reject) {
 *             setTimeout(function() {
 *                 resolve(true)
 *             }, 1000)
 *         })
 *     },
 *     filterFuncs: function(token, funcs) {
 *         //使用token驗證使用者身份與過濾可用funcs
 *         return new Promise(function(resolve, reject) {
 *             funcs = funcs.filter(function(v) {
 *                 return v.indexOf('Hide') < 0
 *             })
 *             resolve(funcs)
 *         })
 *     },
 *     onClientChange: function(clients, opt) {
 *         console.log(`Server[port:${opt.port}] now clients: ${clients.length}`)
 *     },
 *     funcs: {
 *         'group.plus': function({ p1, p2 }) {
 *             return new Promise(function(resolve, reject) {
 *                 setTimeout(function() {
 *                     resolve(p1 * p2)
 *                 }, random(100, 3000))
 *             })
 *         },
 *         'group.div': function({ p1, p2 }) {
 *             return new Promise(function(resolve, reject) {
 *                 setTimeout(function() {
 *                     resolve(p1 / p2)
 *                 }, random(100, 3000))
 *             })
 *         },
 *         'add': function({ p1, p2 }) {
 *             return new Promise(function(resolve, reject) {
 *                 setTimeout(function() {
 *                     resolve(p1 + p2)
 *                 }, random(100, 3000))
 *             })
 *         },
 *         'addHide': function({ p1, p2 }) {
 *             return new Promise(function(resolve, reject) {
 *                 setTimeout(function() {
 *                     resolve(p1 + p2)
 *                 }, random(100, 3000))
 *             })
 *         },
 *         'minu': function({ p1, p2 }) {
 *             return new Promise(function(resolve, reject) {
 *                 setTimeout(function() {
 *                     resolve(p1 - p2)
 *                 }, random(100, 3000))
 *             })
 *         },
 *     },
 * }
 *
 * new WComorSocketioServer(opt)
 *
 */
function WComorSocketioServer(opt) {


    //port
    let port = get(opt, 'port')
    if (!ispint(port)) {
        port = 8080
    }
    port = cint(port)


    //pathPolling
    let pathPolling = get(opt, 'pathPolling')


    //funcs
    let funcs = []
    if (haskey(opt, 'funcs')) {
        funcs = keys(opt['funcs'])
    }


    //authenticate
    function authenticate(token) {
        let pm = genPm()
        if (isfun(opt.authenticate)) {
            opt.authenticate(token)
                .then(function(vd) {
                    pm.resolve(vd)
                })
        }
        else {
            pm.resolve(true)
        }
        return pm
    }


    //server
    let server
    if (opt.serverHapi) {

        //use serverHapi
        server = opt.serverHapi

    }
    else {

        //create server
        server = Hapi.server({
            port: opt.port,
            //host: 'localhost',
            routes: {
                cors: true
            },
        })

    }


    //io
    let io = null
    let ioOpt = {}
    if (isestr(pathPolling)) {
        ioOpt = { path: pathPolling }
    }
    try {
        io = new SocketIO(server.listener, ioOpt)
    }
    catch (err) {
        console.log(`create SocketIO catch:`, err)
        return null
    }


    //connect
    let clients = []
    io.on('connect', function(client) {
        //console.log('connect', client.id)


        //push
        clients.push(client.id)
        if (isfun(opt.onClientChange)) {
            opt.onClientChange(clients, opt)
        }


        //execFunction
        async function execFunction(data) {
            //console.log('execFunction', client.id, data)

            //token
            let token = get(data, 'token', '')

            //vd
            let vd = await authenticate(token)

            //check
            if (vd) {

                //func
                let func = get(data, 'func', '')

                //input
                let input = get(data, 'input')

                //getFuncs
                if (func === 'getFuncs') {

                    if (isfun(opt.filterFuncs)) {
                        funcs = await opt.filterFuncs(token, funcs)
                    }

                    //add output
                    data['output'] = { sys: 'sys', funcs }

                }
                //call
                else if (arrHas(funcs, func)) {

                    //call func in opt.funcs
                    let output = await opt['funcs'][func](input)

                    //add output
                    data['output'] = output

                }
                else {

                    //add output
                    data['output'] = { err: `can not find: ${func}` }

                }

            }
            else {

                //add output
                data['output'] = { err: `can not authenticate token: ${token}` }

            }

            //delete input, 因input可能很大故回傳數據不包含原input
            delete data['input']

            //send
            try {
                client.send(JSON.stringify(data), function(err) {
                    if (err) {
                        console.log(`Server: send output error:`, err)
                    }
                })
            }
            catch (err) {
                console.log(`Server: send output catch:`, err)
            }

        }


        //message
        client.on('message', function(msg) {
            //console.log('message', client.id, msg)

            //data
            let data = j2o(msg)

            //execFunction
            execFunction(data)

        })


        //disconnect, close
        client.on('disconnect', function(msg) {
            //console.log('disconnect', client.id, msg)

            //刪除client
            clients = clients.filter(function(id) {
                return id !== client.id
            })
            if (isfun(opt.onClientChange)) {
                opt.onClientChange(clients, opt)
            }

        })


    })


    async function createServer() {

        if (!opt.serverHapi) {

            //register inert
            await server.register(Inert)

            //api
            let api = [
                // {
                //     method: 'GET',
                //     path: '/{file*}',
                //     handler: {
                //         directory: {
                //             path: './'
                //         },
                //     },
                // },
                //預設僅提供測試之3檔案
                {
                    method: 'GET',
                    path: '/web.html',
                    handler: {
                        file: {
                            path: './web.html'
                        },
                    },
                },
                {
                    method: 'GET',
                    path: '/dist/w-comor-socketio-client.umd.js',
                    handler: {
                        file: {
                            path: './dist/w-comor-socketio-client.umd.js'
                        },
                    },
                },
                {
                    method: 'GET',
                    path: '/dist/w-comor-socketio-client.umd.js.map',
                    handler: {
                        file: {
                            path: './dist/w-comor-socketio-client.umd.js.map'
                        },
                    },
                },
            ]

            //route
            server.route(api)

        }

        //start
        await server.start()

        console.log(`Server running at: ${server.info.uri}`)

    }
    createServer()


}


export default WComorSocketioServer