WConversiServer.mjs

import Hapi from '@hapi/hapi'
import Inert from '@hapi/inert' //提供靜態檔案
import { Server } from 'socket.io'
import events from 'events'
import get from 'lodash-es/get.js'
import ispint from 'wsemi/src/ispint.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import cint from 'wsemi/src/cint.mjs'
import sendSplitData from './sendSplitData.mjs'
import mergeSplitData from './mergeSplitData.mjs'


let SocketIO = Server


/**
 * 建立SocketIO伺服器
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Integer} [opt.port=8080] 輸入SocketIO伺服器所在port,預設8080
 * @param {String} [opt.pathPolling=undefined] 輸入socket.io伺服器無法使用WebSocket連線自動降級成為輪詢(polling)所在子目錄字串,預設undefined,代表使用'/socket.io'
 * @param {Integer} [opt.strSplitLength=1000000] 輸入傳輸封包長度整數,預設為1000000
 * @returns {Object} 回傳通訊物件,可監聽事件open、error、clientChange、execute、broadcast、deliver,可使用函數broadcast
 * @example
 *
 * import WConversiServer from 'w-conversi/dist/w-conversi-server.umd.js'
 *
 * let opt = {
 *     port: 8080,
 * }
 *
 * //new
 * let wo = new WConversiServer(opt)
 *
 * wo.on('open', function() {
 *     console.log(`Server[port:${opt.port}]: open`)
 *
 *     //broadcast
 *     // let n = 0
 *     // setInterval(() => {
 *     //     n += 1
 *     //     let o = {
 *     //         text: `server broadcast hi(${n})`,
 *     //         data: new Uint8Array([66, 97, 115]), //support Uint8Array data
 *     //     }
 *     //     wo.broadcast(o, function (prog) {
 *     //         console.log('broadcast prog', prog)
 *     //     })
 *     // }, 1000)
 *
 * })
 * wo.on('error', function(err) {
 *     console.log(`Server[port:${opt.port}]: error`, err)
 * })
 * wo.on('clientChange', function(clients) {
 *     console.log(`Server[port:${opt.port}]: now clients: ${clients.length}`)
 * })
 * wo.on('execute', function(func, input, callback) {
 *     console.log(`Server[port:${opt.port}]: execute`, func, input)
 *
 *     if (func === 'add') {
 *         let r = input.p1 + input.p2
 *         callback(r)
 *     }
 *
 * })
 * wo.on('broadcast', function(data) {
 *     console.log(`Server[port:${opt.port}]: broadcast`, data)
 * })
 * wo.on('deliver', function(data) {
 *     console.log(`Server[port:${opt.port}]: deliver`, data)
 * })
 *
 */
function WConversiServer(opt = {}) {


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


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


    //strSplitLength
    let strSplitLength = get(opt, 'strSplitLength')
    if (!ispint(strSplitLength)) {
        strSplitLength = 1000000
    }


    //ee
    let ee = new events.EventEmitter()


    //eeEmit
    function eeEmit(name, ...args) {
        setTimeout(() => {
            ee.emit(name, ...args)
        }, 1)
    }


    //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) {
        error('create SocketIO catch error', err)
        return ee
    }


    //check
    if (!io) {
        error('can not create SocketIO', 'io from SocketIO is null')
        return ee
    }


    /**
     * SocketIO監聽開啟事件
     *
     * @memberof WConversiServer
     * @example
     * wo.on('open', function() {
     *     ...
     * })
     */
    function onOpen() {} onOpen()
    function open() {
        eeEmit('open')
    }
    open()


    /**
     * SocketIO監聽錯誤事件
     *
     * @memberof WConversiServer
     * @param {*} err 傳入錯誤訊息
     * @example
     * wo.on('error', function(err) {
     *     ...
     * })
     */
    function onError() {} onError()
    function error(msg, err) {
        eeEmit('error', { msg, err })
    }


    /**
     * SocketIO監聽客戶端變更(上下線)事件
     *
     * @memberof WConversiServer
     * @example
     * wo.on('clientChange', function(clients) {
     *     ...
     * })
     */
    function onClientChange() {} onClientChange()
    function clientChange() {
        eeEmit('clientChange', clients)
    }


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


        //client connected
        clients.push(client.id)
        clientChange()


        function sendData(data, cbProgress) {
            //console.log('sendData', data)
            try {
                //sendSplitData
                sendSplitData(client, strSplitLength, data, cbProgress, function (err) {
                    error('can not send message', err)
                })
            }
            catch (err) {
                error('send message catch error', err)
            }
        }


        //bind for execute
        client.sendData = sendData


        function parserData(data) {
            //console.log('parserData', data)

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

            //cbResult for execute
            function cbResult(output) {

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

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

                //sendData
                sendData(data, null) //回傳執行結果就不處理進度回調

            }

            //emit
            if (_mode === 'execute') {

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

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

                //execute 執行
                eeEmit('execute', func, input, cbResult, sendData)

            }
            else if (_mode === 'broadcast') {

                //broadcast 廣播
                eeEmit('broadcast', get(data, 'data'))

            }
            else if (_mode === 'deliver') {

                //deliver 交付
                eeEmit('deliver', get(data, 'data'))

            }
            else {
                error('can not find _mode in data', data)
            }

        }


        function message(message) {
            //console.log('message', message)

            //mergeSplitData
            mergeSplitData(message, parserData)

        }
        client.on('message', message)


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

            //刪除client
            clients = clients.filter(function(id) {
                return id !== client.id
            })
            clientChange()

        }
        client.on('disconnect', disconnect)


        function triggerBroadcast(data, cbProgress) {

            //msg
            let msg = {
                _mode: 'broadcast',
                data,
            }

            //sendData
            sendData(msg, cbProgress)

        }


        //triggerBroadcast, 需對全部客戶端廣播, 不能清除過去監聽事件
        ee.on('triggerBroadcast', triggerBroadcast)


    })


    /**
     * SocketIO監聽客戶端執行事件
     *
     * @memberof WConversiServer
     * @param {String} func 傳入執行函數名稱字串
     * @param {*} input 傳入執行函數之輸入數據
     * @param {Function} callback 傳入執行函數之回調函數
     * @param {Function} sendData 傳入執行函數之強制回傳函數,提供傳送任意訊息給客戶端,供由服器多次回傳數據之用
     * @example
     * wo.on('execute', function(func, input, callback, sendData) {
     *     ...
     * })
     */
    function onExecute() {} onExecute()


    /**
     * SocketIO監聽客戶端廣播事件
     *
     * @memberof WConversiServer
     * @param {*} data 傳入廣播訊息
     * @example
     * wo.on('broadcast', function(data) {
     *     ...
     * })
     */
    function onBroadcast() {} onBroadcast()


    /**
     * SocketIO監聽客戶端交付事件
     *
     * @memberof WConversiServer
     * @param {*} data 傳入交付訊息
     * @example
     * wo.on('deliver', function(data) {
     *     ...
     * })
     */
    function onDeliver() {} onDeliver()


    /**
     * SocketIO通訊物件對客戶端廣播函數
     *
     * @memberof WConversiServer
     * @function broadcast
     * @param {*} data 輸入廣播函數之輸入資訊
     * @example
     * let data = {...}
     * wo.broadcast(data)
     */
    ee.broadcast = function (data, cbProgress = function () {}) {
        eeEmit('triggerBroadcast', data, cbProgress)
    }


    async function startServer() {

        //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-conversi-client.umd.js',
                handler: {
                    file: {
                        path: './dist/w-conversi-client.umd.js'
                    },
                },
            },
            {
                method: 'GET',
                path: '/dist/w-conversi-client.umd.js.map',
                handler: {
                    file: {
                        path: './dist/w-conversi-client.umd.js.map'
                    },
                },
            },
        ]

        //route
        server.route(api)

        //start
        await server.start()

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

    }

    if (opt.serverHapi) {
        //none
    }
    else {
        startServer()
    }

    return ee
}


export default WConversiServer