WConverhpClient.mjs

// import axiosNode from 'axios/lib/axios.js'
// import axiosNode from 'axios'
// import axiosBrowser from 'axios/dist/axios.min.js'
// import axiosBrowser from 'axios/dist/esm/axios.min.js'
import axios from 'axios' //axios已可自動依照調用環境切換
// import * as FormData from 'form-data/lib/form_data.js'
// import FormData from 'form-data'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
// import getGlobal from 'wsemi/src/getGlobal.mjs'
import isWindow from 'wsemi/src/isWindow.mjs'
import genPm from 'wsemi/src/genPm.mjs'
import genID from 'wsemi/src/genID.mjs'
import Evem from 'wsemi/src/evem.mjs'
import pm2resolve from 'wsemi/src/pm2resolve.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import strright from 'wsemi/src/strright.mjs'
import blob2u8arr from 'wsemi/src/blob2u8arr.mjs'
import obj2u8arr from 'wsemi/src/obj2u8arr.mjs'
import u8arr2obj from 'wsemi/src/u8arr2obj.mjs'


/**
 * 建立Hapi使用者(Node.js與Browser)端物件
 *
 * @class
 * @param {Object} opt 輸入設定參數物件
 * @param {String} [opt.url='http://localhost:8080'] 輸入Hapi伺服器網址,預設為'http://localhost:8080'
 * @param {String} [opt.apiName='api'] 輸入api名稱,預設為'api'
 * @param {Integer} [opt.timePolling=2000] 輸入輪詢間隔時間整數,單位為毫秒,預設為2000
 * @param {Integer} [opt.retry=3] 輸入傳輸失敗重試次數整數,預設為3
 * @returns {Object} 回傳通訊物件,可監聽事件open、openOnce、close、error、reconn、broadcast、deliver,可使用函數execute、broadcast、deliver
 * @example
 *
 * import WConverhpClient from 'w-converhp/dist/w-converhp-client.umd.js'
 *
 * let opt = {
 *     url: 'http://localhost:8080',
 *     apiName: 'api',
 * }
 *
 * //new
 * let wo = new WConverhpClient(opt)
 *
 * wo.on('open', function() {
 *     console.log('client nodejs: open')
 * })
 * wo.on('openOnce', function() {
 *     console.log('client nodejs: openOnce')
 *
 *     //p
 *     let name = 'zdata.b1'
 *     let p = {
 *         a: 12,
 *         b: 34.56,
 *         c: 'test中文',
 *         d: {
 *             name: name,
 *             u8a: new Uint8Array([66, 97, 115]),
 *             //u8a: new Uint8Array(fs.readFileSync('C:\\Users\\Administrator\\Desktop\\'+name)),
 *         }
 *     }
 *
 *     //execute
 *     wo.execute('add', { p },
 *         function (prog, p, m) {
 *             console.log('client nodejs: execute: prog', prog, p, m)
 *         })
 *         .then(function(r) {
 *             console.log('client nodejs: execute: add', r)
 *         })
 *         .catch(function(err) {
 *             console.log('client nodejs: execute: catch', err)
 *         })
 *
 *     //broadcast
 *     wo.broadcast('client nodejs broadcast hi', function (prog) {
 *         console.log('client nodejs: broadcast: prog', prog)
 *     })
 *         .catch(function(err) {
 *             console.log('client nodejs: broadcast: catch', err)
 *         })
 *
 *     //deliver
 *     wo.deliver('client nodejs deliver hi', function (prog) {
 *         console.log('client nodejs: deliver: prog', prog)
 *     })
 *         .catch(function(err) {
 *             console.log('client nodejs: deliver: catch', err)
 *         })
 *
 * })
 * wo.on('close', function() {
 *     console.log('client nodejs: close')
 * })
 * wo.on('error', function(err) {
 *     console.log('client nodejs: error', err)
 * })
 * wo.on('reconn', function() {
 *     console.log('client nodejs: reconn')
 * })
 * wo.on('broadcast', function(data) {
 *     console.log('client nodejs: broadcast', data)
 * })
 * wo.on('deliver', function(data) {
 *     console.log('client nodejs: deliver', data)
 * })
 *
 */
function WConverhpClient(opt) {
    let clientId = genID() //供伺服器識別真實連線使用者


    //ee, ev
    let ee = new Evem()
    //let ev = new Evem()


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


    function core() {


        //default
        if (!opt.url) {
            opt.url = 'http://localhost:8080'
        }
        if (!opt.apiName) {
            opt.apiName = 'api'
        }
        if (!opt.timePolling) {
            opt.timePolling = 2000
        }
        if (!opt.retry) {
            opt.retry = 3
        }


        //url
        let url = ''
        if (strright(opt.url, 1) === '/') {
            url = opt.url + opt.apiName
        }
        else {
            url = opt.url + '/' + opt.apiName
        }


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


        /**
         * Hapi監聽第一次開啟事件
         *
         * @memberof WConverhpClient
         * @example
         * wo.on('openOnce', function() {
         *     ...
         * })
         */
        function onOpenOnce() {} onOpenOnce()
        function openOnce() {
            eeEmit('openOnce')
        }


        /**
         * Hapi監聽錯誤事件
         *
         * @memberof WConverhpClient
         * @param {*} err 接收錯誤訊息
         * @example
         * wo.on('error', function(err) {
         *     ...
         * })
         */
        function onError() {} onError()
        function error(msg, err) {
            eeEmit('error', { msg, err })
        }


        //res2u8arr
        async function res2u8arr(env, bb) {
            let u8a
            if (env === 'browser') {
                u8a = await blob2u8arr(bb)
            }
            else {
                u8a = new Uint8Array(bb)
            }
            return u8a
        }


        //sendDataCore
        function sendDataCore(data, cbProgress) {
            //console.log('sendData', data, cbProgress)

            //pm
            let pm = genPm()

            //env
            let env = isWindow() ? 'browser' : 'nodejs'
            // console.log('env', env)

            // //g
            // let g = getGlobal()

            //obj2u8arr
            let u8a = obj2u8arr(data)
            // console.log('u8a', u8a)

            //u8a to blob(in browser) or buffer(in nodejs)
            let bb
            if (env === 'browser') {
                bb = new Blob([u8a.buffer])
            }
            else { //nodejs
                bb = Buffer.from(u8a)
            }
            // console.log('bb', bb)

            //new
            let fmd
            if (env === 'browser') {
                fmd = new FormData()
            }
            else {
                if (isfun(opt.FormData)) {
                    fmd = new opt.FormData({ maxDataSize: 1024 * 1024 * 1024 }) //nodejs, 使用套件form-data設定資料量最大為1g
                }
                else {
                    console.log(`invalid opt.FormData, need [npm i form-data] and [import FormData from 'form-data'] to set opt.FormData = FormData`)
                    throw new Error('invalid opt.FormData')
                }
            }

            //append
            //fmd.append('aa', 'test')
            fmd.append('bb', bb)
            // console.log('fmd', fmd)

            //ct
            let ct = 'multipart/form-data'
            if (env === 'nodejs') {
                ct += `; boundary=${fmd.getBoundary()}` //nodejs, 使用套件form-data需設定boundary
            }
            // console.log('ct', ct)

            //rt
            let rt = 'blob'
            if (env === 'nodejs') {
                rt = 'arraybuffer' //nodejs下沒有blob, 只能設定'json', 'arraybuffer', 'document', 'json', 'text', 'stream'
            }
            // console.log('rt', rt)

            //s
            let s = {
                method: 'POST',
                url,
                data: fmd,
                headers: {
                    'Content-Type': ct, //數據視為file上傳
                },
                timeout: 5 * 60 * 1000, //5分鐘
                maxContentLength: Infinity, //1024 * 1024 * 1024, Infinity //axios於nodejs中會限制內容大小故需改為無限
                maxBodyLength: Infinity, //1024 * 1024 * 1024, Infinity //axios於nodejs中會限制內容大小故需改為無限
                responseType: rt,
                onUploadProgress: function(ev) {
                    //console.log('onUploadProgress', ev)

                    //r
                    let r = 0
                    let loaded = ev.loaded
                    let total = ev.total
                    if (ispint(total)) {
                        r = (loaded * 100) / total
                    }

                    //cbProgress
                    if (isfun(cbProgress)) {
                        cbProgress(Math.floor(r), loaded, 'upload')
                    }

                },
                onDownloadProgress: function (ev) {
                    // console.log('onDownloadProgress', ev)

                    //r
                    let r = 0
                    let loaded = ev.loaded
                    // let total = ev.srcElement.getResponseHeader('Content-length') //若需要得知下載進度, 需於伺服器回傳時提供Content-length
                    let total = ev.total
                    if (ispint(total)) {
                        r = (loaded * 100) / total
                    }

                    //cbProgress
                    if (isfun(cbProgress)) {
                        cbProgress(Math.floor(r), loaded, 'donwload')
                    }

                },
            }
            // console.log('s', s)

            //axios
            //使用import axios from 'axios', 若於套件內測試, 由於執行時axios自動選用nodejs或browser版本, 分不出差異
            //但若是發佈成套件再由其他套件呼叫使用就會預設使用axiosNode版本, 導致瀏覽器端出錯: Cannot convert undefined or null to object[at mergeConfig]
            // let axios = null
            // if (env === 'browser') {
            //     axios = axiosBrowser
            // }
            // else {
            //     axios = axiosNode
            // }

            //axios
            axios(s)
                .then(async (res) => {
                    // console.log('axios then', res)

                    //bb
                    let bb = get(res, 'data')
                    // console.log('bb', bb)

                    //res2u8arr
                    let u8a = await res2u8arr(env, bb)
                    // console.log('u8a', u8a)

                    //u8arr2obj
                    let data = u8arr2obj(u8a)
                    // console.log('data', data)

                    pm.resolve(data)
                })
                .catch(async (res) => {
                    //console.log('axios catch', res.toJSON())
                    //Network Error除可能是網路斷線之外, 亦可能因硬碟空間不足(<4g)無法下載, 或是被瀏覽器外掛封鎖阻擋

                    //statusText, err
                    let statusText = get(res, 'response.statusText') || get(res, 'message')
                    let err = get(res, 'response.data') || get(res, 'stack')

                    if (statusText) {
                        // console.log('statusText', statusText)
                        data = statusText
                    }
                    else if (err) {
                        // console.log('err', err)
                        data = err
                    }
                    else {
                        try {
                            res = res.toJSON()
                        }
                        catch (err) {}
                        console.log('err', res)
                        data = 'Can not connect to server.'
                    }
                    if (data === 'Network Error') {
                        data = `${data}. Make sure your space of hard drive is large enough or blocking by browser plugins.`
                    }

                    pm.reject(data)
                })

            return pm
        }


        //sendData
        async function sendData(data, cbProgress) {

            //sendDataCore
            let r = await pm2resolve(sendDataCore)(data, cbProgress)

            let n = 0
            while (r.state === 'error') {
                n += 1
                if (n > opt.retry) {
                    break
                }
                console.log(`retry n=${n}`)
                r = await pm2resolve(sendDataCore)(data, cbProgress)
            }

            //return
            if (r.state === 'success') {
                return r.msg
            }
            else {
                return Promise.reject(r.msg)
            }

        }


        //sendMsg
        function sendMsg(msg, cbResult, cbProgress) {
            //console.log(msg, cbResult, cbProgress)

            //sendData
            sendData(msg, cbProgress)
                .then((res) => {
                    //console.log('sendData then', res)

                    //cbResult
                    cbResult(res)

                })
                .catch((err) => {
                    //console.log('sendData catch', err)

                    //cbResult
                    cbResult({
                        error: err,
                    })

                })

        }


        function polling() {

            //setInterval
            setInterval(() => {

                //triggerPolling
                triggerPolling()
                    .then((res) => {
                        //console.log('polling res', res)

                        //output
                        let output = get(res, 'success.output', null)
                        //console.log('output', output)

                        //check
                        if (output === null) {
                            return
                        }
                        if (!isearr(output)) {
                            return
                        }

                        //each
                        each(output, (v, k) => {
                            setTimeout(() => {

                                if (get(v, 'mode') === 'broadcast') {
                                    //broadcast 廣播
                                    eeEmit('broadcast', get(v, 'data'))
                                }
                                else if (get(v, 'mode') === 'deliver') {
                                    //deliver 發送
                                    eeEmit('deliver', get(v, 'data'))
                                }
                                else {
                                    error('invalid data.mode in polling', v)
                                }

                            }, 10 * (k + 1))
                        })

                    })
                    .catch((err) => {
                        error('can not polling', err)
                        eeEmit('reconn')
                    })

            }, opt.timePolling)

        }


        function triggerPolling() {

            //pm
            let pm = genPm()

            //msg
            let msg = {
                _mode: 'polling',
                clientId,
            }

            //cb
            function cb(res) {
                pm.resolve(res)
            }

            //sendMsg
            sendMsg(msg, cb, () => {})

            return pm
        }


        function triggerExecute(func, input, cbResult, cbProgress) {
            //console.log('triggerExecute', func, input, cbResult, cbProgress)

            //mode
            let mode = 'execute'

            //msg
            let msg = {
                _mode: mode,
                clientId,
                func,
                input,
            }

            //sendMsg
            sendMsg(msg, cbResult, cbProgress)

        }


        function triggerBroadcast(input, cbResult, cbProgress) {
            triggerCommon(input, cbResult, cbProgress, 'broadcast')
        }


        function triggerDeliver(input, cbResult, cbProgress) {
            triggerCommon(input, cbResult, cbProgress, 'deliver')
        }


        function triggerCommon(input, cbResult, cbProgress, mode) {

            //msg
            let msg = {
                _mode: mode,
                clientId,
                input,
            }

            //sendMsg
            sendMsg(msg, cbResult, cbProgress)

        }


        //triggerExecute, 若斷線重連則需自動清除過去監聽事件
        ee.removeAllListeners('triggerExecute')
        ee.on('triggerExecute', triggerExecute)


        //triggerBroadcast, 若斷線重連則需自動清除過去監聽事件
        ee.removeAllListeners('triggerBroadcast')
        ee.on('triggerBroadcast', triggerBroadcast)


        //triggerDeliver, 若斷線重連則需自動清除過去監聽事件
        ee.removeAllListeners('triggerDeliver')
        ee.on('triggerDeliver', triggerDeliver)


        //open, openOnce, polling
        open()
        openOnce()
        polling()


    }


    //parseOutput
    function parseOutput(pm, data) {
        let resSuccess = get(data, 'success', null)
        let resError = get(data, 'error', null)
        if (resSuccess !== null) {
            let output = get(resSuccess, 'output')
            pm.resolve(output)
        }
        else {
            pm.reject(resError)
        }
    }


    /**
     * Hapi通訊物件對伺服器端執行函數,表示傳送資料給伺服器,並請伺服器執行函數
     *
     * @memberof WConverhpClient
     * @function execute
     * @param {String} func 輸入執行函數之名稱字串
     * @param {*} [input=null] 輸入執行函數之輸入資訊
     * @example
     * let func = 'NameOfFunction'
     * let input = {...}
     * wo.execute(func, input)
     */
    ee.execute = function (func, input, cbProgress = function () {}) {
        let pm = genPm()
        eeEmit('triggerExecute', func, input,
            function(data) { //結果用promise回傳
                parseOutput(pm, data)
            },
            cbProgress //傳輸進度用cb回傳
        )
        return pm
    }


    /**
     * Hapi通訊物件對伺服器端廣播函數,表示傳送資料給伺服器,還需轉送其他客戶端
     *
     * @memberof WConverhpClient
     * @function broadcast
     * @param {*} data 輸入廣播函數之輸入資訊
     * @example
     * let data = {...}
     * wo.broadcast(data)
     */
    ee.broadcast = function (data, cbProgress = function () {}) {
        let pm = genPm()
        eeEmit('triggerBroadcast', data,
            function(data) { //結果用promise回傳
                parseOutput(pm, data)
            },
            cbProgress //傳輸進度用cb回傳
        )
        return pm
    }


    /**
     * Hapi通訊物件對伺服器端發送函數,表示僅傳送資料給伺服器
     *
     * @memberof WConverhpClient
     * @function deliver
     * @param {*} data 輸入發送函數之輸入資訊
     * @example
     * let data = {...}
     * wo.deliver(data)
     */
    ee.deliver = function (data, cbProgress = function () {}) {
        let pm = genPm()
        eeEmit('triggerDeliver', data,
            function(data) { //結果用promise回傳
                parseOutput(pm, data)
            },
            cbProgress //傳輸進度用cb回傳
        )
        return pm
    }


    // /**
    //  * Hapi監聽重連事件
    //  *
    //  * @memberof WConverhpClient
    //  * @example
    //  * wo.on('reconn', function() {
    //  *     ...
    //  * })
    //  */
    // function onReconn() {} onReconn()
    // function reconn() {
    //     eeEmit('reconn')
    //     setTimeout(function() {
    //         core()
    //     }, 1000)
    // }


    //core
    core()


    return ee
}


export default WConverhpClient