WConverhpClient.mjs

import axios from 'axios'
import get from 'lodash-es/get.js'
import isWindow from 'wsemi/src/isWindow.mjs'
import evem from 'wsemi/src/evem.mjs'
import genPm from 'wsemi/src/genPm.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import isfun from 'wsemi/src/isfun.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import isp0int from 'wsemi/src/isp0int.mjs'
import isestr from 'wsemi/src/isestr.mjs'
import isobj from 'wsemi/src/isobj.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import ispm from 'wsemi/src/ispm.mjs'
import cint from 'wsemi/src/cint.mjs'
import strright from 'wsemi/src/strright.mjs'
import b642str from 'wsemi/src/b642str.mjs'
import blob2u8arr from 'wsemi/src/blob2u8arr.mjs'
import obj2u8arr from 'wsemi/src/obj2u8arr.mjs'
import u8arr2obj from 'wsemi/src/u8arr2obj.mjs'
import pmConvertResolve from 'wsemi/src/pmConvertResolve.mjs'
import getFileXxHash from 'wsemi/src/getFileXxHash.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 {Function} [opt.getToken=()=>''] 輸入取得使用者token的回調函數,預設()=>''
 * @param {String} [opt.tokenType='Bearer'] 輸入token類型字串,預設'Bearer'
 * @param {Integer} [opt.sizeSlice=1024*1024] 輸入切片上傳檔案之切片檔案大小整數,單位為Byte,預設為1024*1024
 * @param {Integer} [opt.retry=3] 輸入傳輸失敗重試次數整數,預設為3
 * @returns {Object} 回傳事件物件,可使用函數execute、upload
 * @example
 *
 * import path from 'path'
 * import fs from 'fs'
 * import _ from 'lodash-es'
 * import w from 'wsemi'
 * import FormData from 'form-data'
 * import WConverhpClient from './src/WConverhpClient.mjs'
 *
 * let ms = []
 *
 * let opt = {
 *     FormData,
 *     url: 'http://localhost:8080',
 *     apiName: 'api',
 *     getToken: () => {
 *         return 'token-for-test'
 *     },
 * }
 *
 * //new
 * let wo = new WConverhpClient(opt)
 *
 * wo.on('error', (err) => {
 *     console.log(`error`, err)
 * })
 *
 * function downloadLargeFile() {
 *     let core = async() => {
 *
 *         await wo.download('id-for-file',
 *             function ({ prog, p, m }) {
 *                 // console.log('client web: download: prog', prog, p, m)
 *                 if (m === 'download') {
 *                     console.log('client web: download: prog', prog)
 *                 }
 *             },
 *             {
 *                 fdDownload: './', //於nodejs環境才能提供
 *             })
 *             .then(function(res) {
 *                 console.log('client web: download: then', res)
 *                 ms.push({ 'download output': res })
 *             })
 *             .catch(function (err) {
 *                 console.log('client web: download: catch', err)
 *             })
 *
 *         console.log('ms', ms)
 *
 *     }
 *     core()
 * }
 *
 * downloadLargeFile()
 *
 */
function WConverhpClient(opt) {

    //_url
    let _url = get(opt, 'url')
    if (!isestr(_url)) {
        _url = 'http://localhost:8080'
    }

    //apiName
    let apiName = get(opt, 'apiName')
    if (!isestr(apiName)) {
        apiName = 'api'
    }

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

    //getToken
    let getToken = get(opt, 'getToken', null)
    if (!isfun(getToken)) {
        getToken = () => {
            return ''
        }
    }

    //tokenType
    let tokenType = get(opt, 'tokenType')
    if (!isestr(tokenType)) {
        tokenType = 'Bearer'
    }

    //sizeSlice
    let sizeSlice = get(opt, 'sizeSlice')
    if (!ispint(sizeSlice)) {
        sizeSlice = 1024 * 1024 //1m
    }

    //retry
    let retry = get(opt, 'retry')
    if (!isp0int(retry)) {
        retry = 3
    }

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

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

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

    //getUrlUse
    let getUrlUse = (type) => {

        //urlUse
        let urlUse = ''
        if (type === 'basic') {
            urlUse = url
        }
        else if (type === 'slice') {
            urlUse = `${url}slc`
        }
        else if (type === 'upload-controller') {
            urlUse = `${url}ulctr`
        }
        // else if (type === 'slice-merge') {
        //     urlUse = `${url}slcm`
        // }
        else if (type === 'download-get-filename') {
            urlUse = `${url}dwgfn`
        }
        else if (type === 'download-get') {
            urlUse = `${url}dwgf`
        }
        else if (type === 'download') {
            urlUse = `${url}dw`
        }
        else {
            throw new Error(`invalid type[${type}]`)
        }

        return urlUse
    }

    //res2u8arr
    async function res2u8arr(bb) {
        //blob(in browser) or buffer(in nodejs) to u8a
        let u8a
        if (env === 'browser') {
            u8a = await blob2u8arr(bb)
        }
        else {
            u8a = new Uint8Array(bb)
        }
        return u8a
    }

    //u8arr2bb
    function u8arr2bb(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)
        }
        return bb
    }

    //send
    async function send(type, pkg, opt = {}) {

        //headers
        let headers = get(opt, 'headers')
        if (!isobj(headers)) {
            headers = {}
        }
        // console.log('headers', headers)

        //dataType
        let dataType = get(opt, 'dataType', '')
        if (dataType !== 'blob' && dataType !== 'fmd' && dataType !== 'json') {
            dataType = 'blob'
        }
        // console.log('dataType', dataType)

        //cbProgress
        let cbProgress = get(opt, 'cbProgress')
        if (!isfun(cbProgress)) {
            cbProgress = () => {}
        }

        //urlUse
        let urlUse = getUrlUse(type)

        //pm
        let pm = genPm()

        //dd, ct
        let dd = null
        let ct = {}
        if (dataType === 'blob') {

            //set ct
            ct = {
                'Content-Type': 'application/octet-stream',
            }

            //set dd
            dd = pkg

        }
        else if (dataType === 'fmd') {

            //fmd
            let fmd
            if (env === 'browser') {
                fmd = new FormData()
            }
            else {
                if (isfun(opt.FormData)) {
                    fmd = new opt.FormData({ maxDataSize: 1024 * 1024 * 1024 * 1024 }) //nodejs, 使用套件form-data設定資料量最大為1tb
                }
                else {
                    // console.log(`invalid opt.FormData, need [npm i form-data] and [import FormData from 'form-data'] to set opt.FormData = FormData`)
                    eeEmit('error', `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('bb', pkg)
            // console.log('fmd', fmd)

            //set ct
            if (env === 'nodejs') {
                ct = {
                    'Content-Type': `multipart/form-data; boundary=${fmd.getBoundary()}` //nodejs, 使用套件form-data需設定boundary
                }
                // console.log('ct', ct)
            }
            else {
                //browser會自動根據fmd的邊界boundary來設定
            }

            //set dd
            dd = fmd

        }
        else if (dataType === 'json') {
            //axios預設會將物件自動轉換為JSON, 此處指定Content-Type且強制轉, 避免可能問題

            //set ct
            ct = {
                'Content-Type': 'application/json',
            }

            dd = JSON.stringify(pkg)
        }
        // console.log('dd', dd)

        //rt
        let rt = 'blob'
        if (env === 'nodejs') {
            rt = 'arraybuffer' //nodejs下沒有blob, 只能設定'json', 'arraybuffer', 'document', 'json', 'text', 'stream'
            if (type === 'download') {
                rt = 'stream' //nodejs download模式採用stream接收
            }
        }
        // console.log('rt', rt)

        //token
        let token = getToken()
        if (ispm(token)) {
            token = await token
        }

        //s
        let s = {
            method: 'POST',
            url: urlUse,
            data: dd,
            headers: {
                Authorization: `${tokenType} ${token}`,
                ...ct,
                ...headers,
            },
            timeout: 60 * 60 * 1000, //1hr
            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
                cbProgress({ prog: Math.floor(r), p: loaded, m: '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
                cbProgress({ prog: Math.floor(r), p: loaded, m: 'download' })

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

        //getFilenameByHeader
        let getFilenameByHeader = (contentDisposition) => {
            let fn = 'unknow'
            try {
                let reg = /filename="(.+?)"/
                let matches = reg.exec(contentDisposition)
                fn = matches ? matches[1] : 'unknown'
            }
            catch (err) {}
            return fn
        }

        //downloadStream
        let downloadStream = async (res) => {
            // console.log('res.headers', res.headers)

            //pm
            let pm = genPm()

            //returnType
            let returnType = get(res, `headers['return-type']`, '')
            // console.log('returnType', returnType)

            //returnMsg
            let returnMsg = get(res, `headers['return-msg']`, '')
            // console.log('returnMsg', returnMsg)

            //check
            if (returnType === 'error') {
                pm.reject(returnMsg)
                return pm
            }

            //contentDisposition
            let contentDisposition = get(res, `headers['content-disposition']`, '')
            // console.log('contentDisposition', contentDisposition)

            //filename
            let filename = getFilenameByHeader(contentDisposition)
            filename = b642str(filename) //headers內對中文支援度不佳須用base64傳, 此處解析提取後須反轉
            // console.log('filename', filename)

            //streamRecv
            let streamRecv = get(res, 'data')

            if (env === 'browser') {

                //browser通過createObjectURL接收stream與a.href+a.click()接收檔案
                try {
                    let url = URL.createObjectURL(streamRecv)
                    let a = document.createElement('a')
                    a.href = url
                    a.download = filename // 動態設置檔名
                    document.body.appendChild(a)
                    a.click()
                    a.remove()
                    URL.revokeObjectURL(url)
                    pm.resolve(filename)
                }
                catch (err) {
                    console.log(err)
                    pm.reject(err)
                }

            }
            else {

                //nodejs通過fs與stream接收檔案, stream出錯只會觸發error事件, 此處try catch為攔截其他非stream程式碼錯誤
                try {

                    //path, fs, 使用動態import供nodejs使用, 否則會被webpack偵測報錯
                    let path = await import('path')
                    let fs = await import('fs')

                    //fdDownload, 只有nodejs下載才使用fdDownload
                    let fdDownload = get(opt, 'fdDownload', '')
                    fs.mkdirSync(fdDownload, { recursive: true }) //須使用mkdirSync, 不要用fsIsFolder與fsCreateFolder避免編譯
                    // console.log('fdDownload', fdDownload)

                    //fp
                    let fp = path.resolve(fdDownload, filename)
                    // console.log('fp', fp)

                    //streamWriter
                    let streamWriter = fs.createWriteStream(fp)

                    //pipe
                    streamRecv.pipe(streamWriter)

                    //finish
                    streamWriter.on('finish', () => {
                        pm.resolve(fp)
                    })

                    //error
                    streamWriter.on('error', (err) => {
                        pm.reject(err)
                    })

                }
                catch (err) {
                    pm.reject(err)
                }

            }

            return pm
        }

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

                //check download
                if (type === 'download') {
                    await downloadStream(res)
                        .then((_res) => {
                            pm.resolve(_res)
                        })
                        .catch((_err) => {
                            pm.reject(_err)
                        })
                    return
                }

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

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

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

                //check
                if (!iseobj(data)) {
                    // console.log('data is not an effective object', data)
                    eeEmit('error', `data is not an effective object`)
                    pm.reject(`data is not an effective object`)
                    return
                }

                //分離伺服器資料的success或error
                if (haskey(data, 'success')) {
                    pm.resolve(data.success)
                }
                else if (haskey(data, 'error')) {
                    eeEmit('error', data.error)
                    pm.reject(data.error)
                }
                else {
                    // console.log('invalid data', data)
                    eeEmit('error', `invalid data`)
                    pm.reject(`invalid data`)
                }

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

                //data
                let data = null

                //statusText, err
                let statusText = get(res, 'response.statusText') || get(res, 'message')
                let err = get(res, 'response.data') || get(res, 'stack')
                // console.log(`get(res, 'response.statusText')`, get(res, 'response.statusText'))
                // console.log(`get(res, 'message')`, get(res, 'message'))
                // console.log(`get(res, 'response.data')`, get(res, 'response.data'))
                // console.log(`get(res, 'stack')`, 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)
                    eeEmit('error', res)
                    data = 'Can not connect to server.'
                }
                if (data === 'Network Error') {
                    data = `Network Error. Make sure your space of hard drive is large enough or blocking by browser plugins.`
                }
                // console.log('data', data)

                pm.reject(data)
            })

        return pm
    }

    //sendPkg
    async function sendPkg(type, data, cbProgress) {

        //bb
        let bb = null
        try {

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

            //u8a to blob(in browser) or buffer(in nodejs)
            bb = u8arr2bb(u8a)
            // console.log('bb', bb)

        }
        catch (err) {
            return Promise.reject(err)
        }

        //send
        let res = await send(type, bb, { dataType: 'blob', cbProgress })

        return res
    }

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

        //fun
        let fun = pmConvertResolve(sendPkg)

        //sendPkg
        let r = await fun(type, data, cbProgress)

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

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

    //calcHash
    async function calcHash(inp) {

        //bb
        let bb = null
        if (env === 'browser') {
            bb = inp
        }
        else {
            //於nodejs時, 因尚無法提供檔名上傳, 故會是readFileSync讀入的buffer, 再轉成new Blob([buffer]), 供getFileXxHash使用
            bb = new Blob([inp])
        }

        //hash
        let hash = await getFileXxHash(bb)

        return hash
    }

    //sendDataSlice
    async function sendDataSlice(fileTotalName, bb, cbProgress) {

        //n
        let n = 0
        if (n === 0) {
            try {
                n = bb.size //for Blob
                n = cint(n)
            }
            catch (err) {}
        }
        if (n === 0) {
            try {
                n = bb.length //for ArrayBuffer //nodejs用fs讀有檔案大小上限, 除非改傳入檔名用stream讀, 否則無法支援超大檔
                n = cint(n)
            }
            catch (err) {}
        }
        if (n === 0) {
            // eeEmit('error', `can not get size of bb`)
            // return Promise.reject(`can not get size of bb`)
            n = 1 //最小給1, 使能支援無大小檔案上傳
        }
        // console.log('n', n)

        //fileTotalSize
        let fileTotalSize = n

        //chunkTotal
        let chunkTotal = Math.ceil(fileTotalSize / sizeSlice)
        // console.log('chunkTotal', chunkTotal)

        //progCount, progWeightSlice
        let progCount = 0
        let progWeightSlice = 0.99 //上傳階段進度使用99%
        // let progWeightMerge = 0.01 //合併階段進度使用1%

        //cbProgressSlice
        let cbProgressSlice = (msg) => {
            let perc = msg.prog
            let dir = msg.m
            if (dir === 'upload' && perc === 100) {
                progCount++
                let r = progCount / chunkTotal
                let prog = r * progWeightSlice * 100
                let psiz = r * fileTotalSize
                cbProgress({ prog, p: psiz, m: 'upload' })
            }
        }

        //cbProgressMerge
        let cbProgressMerge = (msg) => {
            let perc = msg.prog
            let dir = msg.m
            if (dir === 'download' && perc === 100) {
                cbProgress({ prog: 100, p: fileTotalSize, m: 'upload' })
            }
        }

        //fileTotalHash
        // console.log(`calc hash for fileTotalSize[${fileTotalSize}]...`)
        let fileTotalHash = await calcHash(bb)
        // console.log(`calc hash for fileTotalSize[${fileTotalSize}] done`, fileTotalHash)

        //send check-total-hash
        // console.log(`send check-total-hash...`)
        let resUpCkt = await send('upload-controller', { mode: 'check-total-hash', fileHash: fileTotalHash, filename: fileTotalName, fileSize: fileTotalSize }, { dataType: 'json' })
        // console.log('resUpCkt', resUpCkt)
        // resUpCkt {
        //   path: 'uploadTemp\\2429b7ef08ce6ba9',
        //   bAllExist: false,
        //   bAllSize: false,
        //   bAllHash: false,
        //   bSls: true,
        //   slks: [
        //      0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,
        //   ]
        // }

        //check
        if (resUpCkt.bAllHash) {
            // console.log('已有上傳大檔')

            //cbProgressMerge
            cbProgressMerge({ prog: 100, m: 'download' }) //觸發上傳完畢後之下載回應, 故m須為download

            //resMg, 直接回傳
            let resMg = {
                filename: fileTotalName,
                path: resUpCkt.path,
            }

            return resMg
        }

        //針對伺服器上已有切片檔案計算hash與比對
        if (resUpCkt.slks.length > 0) {
            // console.log('receive slks...', resUpCkt.slks[0], size(resUpCkt.slks))

            //fileSliceHashs
            let fileSliceHashs = []
            // let n = Math.max(resUpCkt.slks.length, 1)
            // let nr = Math.floor(n / 100)
            for (let k = 0; k < resUpCkt.slks.length; k++) {
                // if (k % nr === 0) {
                //     console.log(`calc hash for slices`, round(k / resUpCkt.slks.length * 100, 1), '%')
                // }

                //i
                let i = resUpCkt.slks[k]

                //start
                let start = i * sizeSlice

                //end
                let end = Math.min(start + sizeSlice, fileTotalSize)

                //chunk
                let chunk = bb.slice(start, end)

                //fileSliceHash
                let fileSliceHash = await calcHash(chunk)
                // console.log('fileSliceHash', fileSliceHash)

                //push
                fileSliceHashs.push({
                    i,
                    h: fileSliceHash,
                })

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

            //send check-slices-hash
            // console.log(`send check-slices-hash...`)
            let resUpCks = await send('upload-controller', { mode: 'check-slices-hash', fileHash: fileTotalHash, fileSliceHashs }, { dataType: 'json' })
            // console.log('resUpCks', resUpCks)
            // resUpCks {
            //   slks: [
            //      0,  1,  2,  3,  4,  5,  6,  7,  8,  9, 10, 11,
            //     12, 13, 14, 15, 16, 17, 18, 19, 20, 21, 22, 23,
            //     24, 25, 26, 27, 28, 29, 30, 31, 32, 33, 34, 35,
            //     36, 37, 38, 39, 40, 41, 42, 43, 44, 45, 46, 47,
            //     48, 49, 50, 51, 52, 53
            //   ]
            // }

            //update, 伺服器針對各切片計算hash與比對, 回傳resUpCks.slks代表hash一致的切片編號, 非一致hash的切片則須重傳, 尚未傳切片亦須繼續傳
            resUpCkt.slks = resUpCks.slks

        }

        //packageId
        let packageId = fileTotalHash
        // console.log('packageId', packageId)

        //upload slice
        // console.log(`upload slice...`)
        for (let i = 0; i < chunkTotal; i++) {

            //check
            if (resUpCkt.slks.indexOf(i) >= 0) {
                // console.log('已有上傳切片檔')
                cbProgressSlice({ prog: 100, m: 'upload' }) //直接觸發更新進度
                continue
            }

            //start
            let start = i * sizeSlice

            //end
            let end = Math.min(start + sizeSlice, fileTotalSize)

            //chunk
            let chunk = bb.slice(start, end)

            //send slice
            let hd = { //用header傳key與value時, key不分大小寫, 故使用kebabCase
                'chunk-index': i,
                'chunk-total': chunkTotal,
                'package-id': packageId,
            }
            await send('slice', chunk, { headers: hd, dataType: 'blob', cbProgress: cbProgressSlice })
            // console.log('resSl', resSl)

        }

        //send merge-slices-push
        // console.log(`send merge-slices-push...`)
        let resUpMgp = await send('upload-controller', { mode: 'merge-slices-push', fileHash: fileTotalHash, chunkTotal }, { dataType: 'json' })
        // console.log('resUpMgp', resUpMgp)

        //checkMerging
        let checkMerging = () => {
            let pm = genPm()

            //queueId
            let queueId = resUpMgp.queueId
            // console.log('queueId', queueId)

            let t = setInterval(() => {

                //send merge-slices-get
                // console.log(`send merge-slices-get...`)
                send('upload-controller', { mode: 'merge-slices-get', fileHash: fileTotalHash, filename: fileTotalName, queueId }, { dataType: 'json' })
                    .then((res) => {
                        // console.log('res', res)
                        // res => {
                        //   queueId,
                        //   state,
                        //   filename,
                        //   path,
                        //   msg: ...
                        // }

                        //check
                        if (res.state === 'success') {

                            //clearInterval
                            clearInterval(t)

                            //cbProgressMerge
                            cbProgressMerge({ prog: 100, m: 'download' }) //觸發上傳完畢後之下載回應, 故m須為download

                            //resolve, state為'success'時提取msg回傳
                            pm.resolve(res.msg)

                        }
                        else if (res.state === 'error') {
                            console.log('merge-slices-get error', res)

                            //clearInterval
                            clearInterval(t)

                            //reject, state為'error'時會於msg提供錯誤訊息
                            pm.resolve(res.msg)

                        }

                    })
                    .catch((err) => {
                        console.log('merge-slices-get catch', err)
                        //可能發生網路斷訊錯誤, 不clearInterval, 持續輪循測試合併大檔之狀態
                    })

            }, 2000)

            return pm
        }

        //checkMerging
        let resMg = await checkMerging()
        // console.log('resMg', resMg)

        // console.log(`upload slice done`)
        return resMg
    }

    //execute
    async function execute(func, input, cbProgress) {

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

        //sendData
        let state = ''
        let res = null
        await sendData('basic', msg, cbProgress)
            .then((msg) => {
                // console.log('msg', msg)

                //check, 若為字串為錯誤訊息
                if (isestr(msg)) {
                    state = 'error'
                    res = msg
                    return
                }

                //check, 若為非物件為非預期錯誤
                if (!iseobj(msg)) {
                    console.log('msg is not an effective object', msg)
                    state = 'error'
                    res = 'msg is not an effective object'
                    return
                }

                //check, 若不存在output為非預期錯誤, msg格式為{func,input,output}但input會刪除
                if (!haskey(msg, 'output')) {
                    console.log('invalid msg.output', msg)
                    state = 'error'
                    res = 'invalid msg.output'
                    return
                }

                state = 'success'
                res = msg.output
            })
            .catch((msg) => {
                state = 'error'
                res = msg
            })

        //check
        if (state === '') {
            // console.log('invalid state', r)
            eeEmit('error', `invalid state`)
            return Promise.reject('invalid state')
        }

        //check
        if (state === 'error') {
            // console.log('send data error', r)
            // eeEmit('error', res) //一般錯誤會嘗試n次, 每次也都會emit, 故此處不再基於已知state='error'時再emit
            return Promise.reject(res)
        }

        return res
    }

    //upload
    async function upload(filename, input, cbProgress) {

        //bb
        let bb = input

        return sendDataSlice(filename, bb, cbProgress)
    }

    //downloadNodejs
    async function downloadNodejs(fileId, cbProgress, opt = {}) {

        //send download
        let msg = { fileId }
        let resMg = await send('download', msg, { ...opt, dataType: 'json', cbProgress })
        // console.log('resMg', resMg)

        return resMg
    }

    //downloadBrowser
    async function downloadBrowser(fileId, cbProgress, opt = {}) {
        //交由瀏覽器下載與管理故無法監聽進度, 不使用cbProgress

        //token
        let token = getToken()
        if (ispm(token)) {
            token = await token
        }
        // console.log('token', token)

        //send download-get-filename
        let msg = { fileId }
        let resMg = await send('download-get-filename', msg, { dataType: 'json' })
        // console.log('resMg', resMg)

        //filename
        let filename = get(resMg, 'filename', '')
        // console.log('filename', filename)

        //urlUse
        let urlUse = getUrlUse('download-get')
        // console.log('urlUse', urlUse)

        //url
        let url = `${urlUse}?fileId=${fileId}&token=${token}`
        // console.log('url', url)

        //透過a元素打url下載, 讓瀏覽器認定為直接下載模式, 由瀏覽器展示下載進度與排入正在下載清單
        let a = document.createElement('a')
        a.href = url
        a.download = filename
        a.click()

        return filename
    }

    //download
    async function download(fileId, cbProgress, opt = {}) {
        if (env === 'browser') {
            return downloadBrowser(fileId, cbProgress, opt)
        }
        else {
            return downloadNodejs(fileId, cbProgress, opt)
        }
    }

    //save
    ee.execute = execute
    ee.upload = upload
    ee.download = download

    return ee
}


export default WConverhpClient