WSyncWebdataClient.mjs

import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import genPm from 'wsemi/src/genPm.mjs'
import evem from 'wsemi/src/evem.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import ispint from 'wsemi/src/ispint.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import cint from 'wsemi/src/cint.mjs'
import delay from 'wsemi/src/delay.mjs'
import isWindow from 'wsemi/src/isWindow.mjs'


/**
 * 瀏覽器端之資料同步器
 *
 * @class
 * @param {Object} instWConverClient 輸入通訊服務實體物件,可使用例如WConverhpClient等建立
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {Boolean} [opt.autoPollingTableTagsForActive=false] 輸入是否當使用者活躍時自動進行輪詢布林值,預設false
 * @param {Integer} [opt.pollingDelayTime=2000] 輸入輪詢強制延遲時間整數,單位ms,預設2000
 * @returns {Object} 回傳前端資料同步物件,可監聽事件refreshTags、refreshState、refreshTable、getData、error,可使用函數setTableTags、updateTableTags、pollingTableTags
 * @example
 *
 * //前後端共用範例
 * import w from 'wsemi'
 * import WSyncWebdataServer from './src/WSyncWebdataServer.mjs'
 * import WSyncWebdataClient from './src/WSyncWebdataClient.mjs'
 *
 * let ms = []
 *
 * let ntg = 0
 * let genTag = () => {
 *     ntg++
 *     return `tag-${ntg}`
 * }
 *
 * //-------- 後端 ---------
 *
 * //EventEmitter, 模擬後端推播至前端
 * let ee = w.evem()
 *
 * //instWConverServer
 * let instWConverServer = w.evem()
 *
 * //wsds
 * let wsds = new WSyncWebdataServer(
 *     instWConverServer,
 *     {
 *         // fpTableTags: 'tableTags-sync-webdata.json',
 *         genTag,
 *     },
 * )
 *
 * //tableTagsSrv
 * let tableTagsSrv = {
 *     tabA: 'tag-0-a',
 *     tabB: 'tag-0-b',
 * }
 *
 * //initTableTags
 * wsds.initTableTags(tableTagsSrv)
 *
 * //readTableTags
 * console.log('server: nowTableTags', wsds.readTableTags())
 * ms.push({ 'server nowTableTags': JSON.stringify(wsds.readTableTags()) })
 *
 * //updateTableTag
 * setTimeout(() => {
 *     wsds.updateTableTag('tabA')
 *     ms.push({ 'server updateTableTag': 'tabA' })
 * }, 500)
 * setTimeout(() => {
 *     wsds.updateTableTag('tabB')
 *     ms.push({ 'server updateTableTag': 'tabB' })
 * }, 750)
 * setTimeout(() => {
 *     wsds.updateTableTag('tabA')
 *     ms.push({ 'server updateTableTag': 'tabA' })
 * }, 1000)
 *
 * //changeTableTags
 * wsds.on('changeTableTags', (nowTableTags) => {
 *     console.log('server: changeTableTags', nowTableTags)
 *
 *     //server push
 *     console.log('server: push')
 *     ms.push({ 'server changeTableTags': JSON.stringify(nowTableTags) })
 *     ee.emit('push', nowTableTags)
 *
 * })
 *
 * //error
 * wsds.on('error', (err) => {
 *     console.log('server: error', err)
 * })
 *
 * //getAPIData, 模擬後端提供API
 * let tabsCount = {
 *     tabA: 0,
 *     tabB: 0,
 * }
 * async function getAPIData(tableName) {
 *     ms.push({ 'server call getAPIData before': tableName })
 *     return new Promise((resolve, reject) => {
 *         setTimeout(() => {
 *             tabsCount[tableName] += 1
 *             ms.push({ 'server call getAPIData after': `table[${tableName}] = ${tabsCount[tableName]}` })
 *             resolve(`table[${tableName}] = ${tabsCount[tableName]}`)
 *         }, 100)
 *     })
 * }
 *
 * //-------- 前端 ---------
 *
 * //instWConverClient
 * let instWConverClient = w.evem()
 *
 * //wsdc
 * let wsdc = new WSyncWebdataClient(instWConverClient, {})
 *
 * //tableTagsCl
 * let tableTagsCl = {
 *     tabA: 'tag-0-a',
 *     tabB: 'tag-0-b',
 * }
 *
 * //setTableTags, 模擬前端將tableTags預先或有變更即儲存至localStorage, 啟動時有既有tableTags
 * wsdc.setTableTags(tableTagsCl)
 * ms.push({ 'clinet setTableTags': JSON.stringify(tableTagsCl) })
 *
 * //push, 模擬前端接收後端推播有變更tableTags
 * ee.on('push', (nowTableTags) => {
 *
 *     //updateTableTags
 *     wsdc.updateTableTags(nowTableTags)
 *     ms.push({ 'clinet updateTableTags': JSON.stringify(nowTableTags) })
 *
 * })
 *
 * //refreshState
 * wsdc.on('refreshState', (msg) => {
 *     console.log('client: refreshState needToRefresh', msg.needToRefresh)
 * })
 *
 * //refreshTable
 * wsdc.on('refreshTable', (input) => {
 *     // console.log('client: refreshTable', input)
 *
 *     //收到有變更tableTags, 模擬呼叫後端API
 *     console.log('client: getAPIData before: ' + input.tableName)
 *     ms.push({ 'clinet call getAPIData before': input.tableName })
 *     getAPIData(input.tableName)
 *         .then((data) => {
 *             console.log('client: getAPIData after: ' + data)
 *             ms.push({ 'clinet call getAPIData after': data })
 *             input.pm.resolve(data) //Use pm.resolve to retrieve the data, and pm.reject the message when get an error.
 *         })
 *         .catch((err) => {
 *             input.pm.reject(err)
 *         })
 *
 * })
 *
 * //getData
 * wsdc.on('getData', (data) => {
 *     console.log('client: getData', data)
 *     //前端取得數據
 * })
 *
 * //beforeUpdateTableTags, afterUpdateTableTags
 * wsdc.on('beforeUpdateTableTags', (msg) => {
 *     // console.log('client: beforeUpdateTableTags', msg)
 * })
 * wsdc.on('afterUpdateTableTags', (msg) => {
 *     // console.log('client: afterUpdateTableTags', msg)
 * })
 *
 * //error
 * wsdc.on('error', (err) => {
 *     console.log('client: error', err)
 * })
 *
 * setTimeout(() => {
 *     console.log('ms', ms)
 * }, 2000)
 *
 * // server: nowTableTags { tabA: 'tag-0-a', tabB: 'tag-0-b' }
 * // server: changeTableTags { tabA: 'tag-1', tabB: 'tag-0-b' }
 * // server: push
 * // client: refreshState needToRefresh true
 * // client: getAPIData before: tabA
 * // client: getAPIData after: table[tabA] = 1
 * // client: getData { tableName: 'tabA', timeTag: 'tag-1', data: 'table[tabA] = 1' }
 * // server: changeTableTags { tabA: 'tag-1', tabB: 'tag-2' }
 * // server: push
 * // client: refreshState needToRefresh true
 * // client: getAPIData before: tabB
 * // client: getAPIData after: table[tabB] = 1
 * // client: getData { tableName: 'tabB', timeTag: 'tag-2', data: 'table[tabB] = 1' }
 * // server: changeTableTags { tabA: 'tag-3', tabB: 'tag-2' }
 * // server: push
 * // client: refreshState needToRefresh true
 * // client: getAPIData before: tabA
 * // client: getAPIData after: table[tabA] = 2
 * // client: getData { tableName: 'tabA', timeTag: 'tag-3', data: 'table[tabA] = 2' }
 * // ms [
 * //   { 'server nowTableTags': '{"tabA":"tag-0-a","tabB":"tag-0-b"}' },
 * //   { 'clinet setTableTags': '{"tabA":"tag-0-a","tabB":"tag-0-b"}' },
 * //   { 'server updateTableTag': 'tabA' },
 * //   { 'server changeTableTags': '{"tabA":"tag-1","tabB":"tag-0-b"}' },
 * //   { 'clinet updateTableTags': '{"tabA":"tag-1","tabB":"tag-0-b"}' },
 * //   { 'clinet call getAPIData before': 'tabA' },
 * //   { 'server call getAPIData before': 'tabA' },
 * //   { 'server updateTableTag': 'tabB' },
 * //   { 'server call getAPIData after': 'table[tabA] = 1' },
 * //   { 'clinet call getAPIData after': 'table[tabA] = 1' },
 * //   { 'server changeTableTags': '{"tabA":"tag-1","tabB":"tag-2"}' },
 * //   { 'clinet updateTableTags': '{"tabA":"tag-1","tabB":"tag-2"}' },
 * //   { 'clinet call getAPIData before': 'tabB' },
 * //   { 'server call getAPIData before': 'tabB' },
 * //   { 'server updateTableTag': 'tabA' },
 * //   { 'server call getAPIData after': 'table[tabB] = 1' },
 * //   { 'clinet call getAPIData after': 'table[tabB] = 1' },
 * //   { 'server changeTableTags': '{"tabA":"tag-3","tabB":"tag-2"}' },
 * //   { 'clinet updateTableTags': '{"tabA":"tag-3","tabB":"tag-2"}' },
 * //   { 'clinet call getAPIData before': 'tabA' },
 * //   { 'server call getAPIData before': 'tabA' },
 * //   { 'server call getAPIData after': 'table[tabA] = 2' },
 * //   { 'clinet call getAPIData after': 'table[tabA] = 2' }
 * // ]
 *
 */
function WSyncWebdataClient(instWConverClient, opt = {}) {
    let nowTableTags = {}
    let isPolling = false

    //check
    if (!iseobj(instWConverClient)) {
        console.log('instWConverClient is not an effective object, and set instWConverClient to an EventEmitter')
        instWConverClient = evem()
    }
    if (!haskey(instWConverClient, 'emit')) {
        throw new Error(`instWConverClient is not an EventEmitter`)
    }

    //autoPollingTableTagsForActive
    let autoPollingTableTagsForActive = get(opt, 'autoPollingTableTagsForActive')
    if (!isbol(autoPollingTableTagsForActive)) {
        autoPollingTableTagsForActive = false //若使用後端broadcast就不用自動輪循
    }

    //pollingDelayTime
    let pollingDelayTime = get(opt, 'pollingDelayTime')
    if (!ispint(pollingDelayTime)) {
        pollingDelayTime = 2000
    }
    pollingDelayTime = cint(pollingDelayTime)

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

    /**
     * 直接設定各資料表時間資料
     *
     * @memberof WSyncWebdataClient
     * @param {Object} tableTags 輸入各資料表時間戳物件
     * @example
     *
     * let tableTags = {...}
     * wsdc.setTableTags(tableTags)
     *
     */
    function setTableTags(tableTags = {}) {
        nowTableTags = tableTags
    }

    /**
     * 主動更新指定資料表之時間戳,當有新的資料表時間戳資料時調用此函數進行更新
     *
     * @memberof WSyncWebdataClient
     * @param {Object} tableTags 輸入各資料表時間戳物件
     * @example
     *
     * let tableTags = {...}
     * wsdc.updateTableTags(tableTags)
     *
     */
    async function updateTableTags(tableTags = {}) {
        let pms = []

        //emit
        eeEmit('beforeUpdateTableTags', {
            oldTableTags: cloneDeep(nowTableTags),
            newTableTags: cloneDeep(tableTags),
        })

        //needToRefresh
        let needToRefresh = false
        each(tableTags, (v, k) => {

            //原有更新時間戳
            let vv = nowTableTags[k]

            //check
            if (vv !== v) {
                needToRefresh = true
            }

        })

        //emit,
        eeEmit('refreshState', {
            needToRefresh,
            oldTableTags: cloneDeep(nowTableTags),
            newTableTags: cloneDeep(tableTags),
        })

        //確認各tags的時間戳
        each(tableTags, (v, k) => {

            //原有更新時間戳
            let vv = nowTableTags[k]

            //check
            if (vv !== v) {

                //pm
                let pm = genPm()

                //tt
                let tt = { tableName: k, timeTag: v, pm }

                //push
                pms.push(pm)

                //emit事件, 外部事件on收到通知時打API去撈指定表數據, 外部由tt.pm回傳成功取得數據或失敗
                eeEmit('refreshTable', tt)

                //wait
                pm
                    .then((data) => {

                        //save
                        nowTableTags[k] = v

                        //emit, 外部事件on可收到API回傳之數據
                        eeEmit('getData', { tableName: k, timeTag: v, data })

                    })
                    .catch((err) => {

                        //emit
                        eeEmit('error', {
                            msg: 'can not get table data: ' + k,
                            err,
                        })

                    })

            }

        })

        //Promise.all
        await Promise.all(pms) //不會有catch

        //emit
        eeEmit('afterUpdateTableTags', {
            oldTableTags: cloneDeep(nowTableTags),
            newTableTags: cloneDeep(tableTags),
        })

    }

    /**
     * 主動觸發輪詢更新各資料表之時間戳
     *
     * @memberof WSyncWebdataClient
     * @example
     *
     * wsdc.pollingTableTags()
     *
     */
    async function pollingTableTags() {

        //check
        if (isPolling) {
            return
        }
        isPolling = true

        //pm
        let pm = genPm()

        //emit, 外部事件on收到通知時打API去撈各資料表時間戳, 需通過pm回傳成功取得數據或失敗
        eeEmit('refreshTags', { pm })

        //wait
        let tableTags = await pm
            .catch((err) => {

                //emit
                eeEmit('error', {
                    msg: 'can not get tags data',
                    err,
                })

            })

        //check
        if (tableTags) {

            //updateTableTags
            await updateTableTags(tableTags) //不會有catch

        }

        //強制延遲
        await delay(pollingDelayTime)

        //恢復isPolling
        isPolling = false

    }

    //pollingTableTags
    if (autoPollingTableTagsForActive) {

        //mouseover
        if (isWindow()) {
            window.addEventListener('mouseover', (e) => {
                pollingTableTags()
            }, false)
            window.addEventListener('touchmove', (e) => {
                pollingTableTags()
            }, false)
        }

    }

    //save
    instWConverClient.setTableTags = setTableTags
    instWConverClient.updateTableTags = updateTableTags
    instWConverClient.pollingTableTags = pollingTableTags

    return instWConverClient
}


export default WSyncWebdataClient