pmDebounce.mjs

import ispint from './ispint.mjs'
import ispm from './ispm.mjs'
import isfun from './isfun.mjs'
import cint from './cint.mjs'


/**
 * 非同步函數進行防抖
 *
 * 同時僅會執行一個佇列(非同步函數),到達防抖時間才會取最後一個佇列執行,待其執行完畢,若期間有新佇列進來則儲存,待執行完畢後取最後佇列出來執行
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/pmDebounce.test.mjs Github}
 * @memberOf wsemi
 * @param {Integer} [ms=300] 輸入未有調用的時間區間,為正整數,預設300ms
 * @returns {Function} 回傳Function,輸入為非同步函數與其輸入,會推入佇列後並循序等待執行,回傳為Promise,resolve回傳成功結果而reject回傳失敗訊息
 * @example
 *
 * let dbc = pmDebounce(300)
 *
 * let i = 0
 * let d = 100
 * if (true) {
 *     console.log(i, 'ms')
 *     let t = setInterval(() => { //dbc內時間比較快
 *         i += d
 *         if (i % 100 === 0) {
 *             console.log(i, 'ms')
 *         }
 *         if (i >= 2000) {
 *             clearInterval(t)
 *         }
 *     }, d)
 * }
 *
 * let ms = []
 *
 * let n = 0
 * function test(rin) {
 *     console.log(i, 'ms', 'test in:', rin)
 *     ms.push(`test in: ${rin}`)
 *     return new Promise(function(resolve, reject) {
 *         setTimeout(() => {
 *             n++
 *             let rout = `${rin}:${n}`
 *             console.log(i, 'ms', 'test out:', rout)
 *             ms.push(`test out: ${rout}`)
 *             resolve(rout)
 *         }, 500)
 *     })
 * }
 *
 * if (true) {
 *
 *     setTimeout(() => {
 *         dbc(async(inp) => {
 *             return test(inp)
 *         }, 'a') //無阻攔時1->300發呆, 300->800執行
 *     }, 1)
 *     //1ms, a進入排程, 但a尚未發呆300ms, 故a無法執行
 *     setTimeout(() => {
 *         dbc(async(inp) => {
 *             return test(inp)
 *         }, 'b') //無阻攔時200->500發呆, 500->1000執行
 *     }, 200)
 *     //200ms, b進入排程, 同時排程前面還有a, 故只會取最末b出來判識(a被強制剔除), 因b尚未發呆完300ms故b無法執行
 *     setTimeout(() => {
 *         dbc(async(inp) => {
 *             return test(inp)
 *         }, 'c') //無阻攔時600->900發呆, 900->1400執行
 *     }, 600)
 *     //500ms, b已發呆完開始執行
 *     //600ms, c進入排程, 因b已於500ms開始執行(b執行為500->1000), c進入排程只能等待
 *     setTimeout(() => {
 *         dbc(async(inp) => {
 *             return test(inp)
 *         }, 'd') //無阻攔時900->1200發呆, 1200->1700執行
 *     }, 900)
 *     //900ms, d進入排程, 因b執行尚未結束, d雖可進入排程, 同時排程前面還有c, 故c與d皆為等待
 *     //1000ms, b已執行完, 取最末d出來判識(c被強制剔除), d須發呆300ms才能開始執行
 *     //1200ms, d已發呆完, 開始執行(d執行為1200->1700)
 *     //1700ms, d執行完
 *
 * }
 * //0 ms
 * //100 ms
 * //200 ms
 * //300 ms
 * //400 ms
 * //400 ms test in: b
 * //500 ms
 * //600 ms
 * //700 ms
 * //800 ms
 * //900 ms
 * //900 ms test out: b:1
 * //1000 ms
 * //1100 ms
 * //1200 ms
 * //1200 ms test in: d
 * //1300 ms
 * //1400 ms
 * //1500 ms
 * //1600 ms
 * //1700 ms
 * //1700 ms test out: d:2
 * //1800 ms
 * //1900 ms
 * //2000 ms
 * setTimeout(() => {
 *     console.log(ms)
 *     // => [ 'test in: b', 'test out: b:1', 'test in: d', 'test out: d:2' ]
 * }, 2200) //實測setInterval因多次執行會比較不準, 故顯示2000ms結束, 實際為2200ms之後
 *
 */
function pmDebounce(ms = 300) {

    // function ClsDebounceEasy(ms) { //簡易法, 因執行階段中無法儲存推入任務會被視為強制取消, 不合需求

    //     //timer
    //     let t = null

    //     //pm
    //     let pm = Promise.resolve()

    //     //run
    //     let run = (func, ...input) => {

    //         //clearTimeout
    //         clearTimeout(t)

    //         // console.log('call fun')
    //         return new Promise((resolve, reject) => {
    //             // console.log('call in Promise', ...input, ms)

    //             //setTimeout
    //             t = setTimeout(async () => {
    //                 // console.log('exec fun start', ...input, ms)

    //                 try {
    //                     let res = func(...input)
    //                     if (ispm(res)) {
    //                         res = await pm
    //                     }
    //                     resolve(res)
    //                 }
    //                 catch (err) {
    //                     reject(err)
    //                 }
    //                 // console.log('exec fun end')

    //             }, ms)
    //         })
    //     }

    //     return run
    // }

    function ClsDebounce(ms) {
        let q = [] //queue
        let t = null //timer
        let tLast = null
        let state = 'wait'

        //ms
        if (!ispint(ms)) {
            ms = 300
        }
        ms = cint(ms)

        // //_tNow
        // let _tNow = Date.now()
        // let getIt = () => {
        //     return Date.now() - _tNow
        // }

        function detect() {

            //check
            if (t !== null) {
                return
            }

            //check
            if (state !== 'wait') {
                return
            }
            // console.log('start detect')

            //setInterval
            t = setInterval(() => {
                //console.log('q', q)

                //check
                if (tLast === null) {
                    throw new Error(`invalid tLast`)
                }

                //check
                if (state !== 'wait') {
                    return
                }

                //check
                if (state === 'wait' && q.length === 0) {
                    clearInterval(t)
                    t = null
                    tLast = null
                    return
                }

                //tNow
                let tNow = Date.now()
                // console.log('ii', getIt())

                //tDiff
                let tDiff = tNow - tLast

                //b
                let b = tDiff > ms

                //check
                if (b) { //超過指定延時則呼叫指定func

                    //state
                    state = 'doing'

                    //update, 直接使用前面計算時間差之時間點
                    tLast = tNow

                    //取最後的任務與清空佇列
                    let m = q.pop()
                    q = []
                    // console.log('m', m)

                    //執行最末任務
                    // console.log('fun start ii', getIt(), 'ms', tDiff, m.input)
                    let r = m.func(...m.input)
                    if (ispm(r)) {
                        r
                            .then((res) => {
                                m.output = res
                            })
                            .catch((err) => {
                                m.output = { err }
                            })
                            .finally(() => {
                                state = 'wait'

                                //update, 因func計算時間可能較長, 得重新更新時間點
                                tLast = Date.now()

                                // console.log('fun(async) end ii', getIt(), 'ms', m.input)
                            })
                    }
                    else {
                        m.output = r
                        state = 'wait'

                        // console.log('fun(sync) end_i', getIt(), 'ms', m.input)
                    }

                }

                //clear
                if (state === 'wait' && q.length === 0) {
                    clearInterval(t)
                    t = null
                    tLast = null
                }

            }, 20) //20ms偵測, 啟動後跑timer, 無佇列則會停止減耗

        }

        function run(func, ...input) {

            //check
            if (!isfun(func)) {
                console.log('func is not a function')
                return
            }
            // console.log('call run ii', Date.now() - _tNow, 'ms', input)

            //first tLast
            if (tLast === null) {
                tLast = Date.now()
            }
            else if (state === 'wait') {
                tLast = Date.now()
            }
            else {
                // console.log('state', state)
                //不更新tLast
            }

            //push
            q.push({
                func,
                input,
                output: null,
            })
            // console.log('push q', q)

            //detect
            detect()

        }

        return run
    }

    return new ClsDebounce(ms)
}


export default pmDebounce