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
 * setTimeout(() => {
 *     let t = setInterval(() => {
 *         i += d
 *         console.log(i, 'ms')
 *         if (i > 2500) {
 *             clearInterval(t)
 *         }
 *     }, d)
 * }, 1)
 *
 * let n = 0
 * function test(rin) {
 *     console.log(i, 'ms', 'test in:', rin)
 *     return new Promise(function(resolve, reject) {
 *         setTimeout(() => {
 *             n++
 *             let rout = `${rin}:${n}`
 *             console.log(i, 'ms', 'test out:', rout)
 *             resolve(rout)
 *         }, 500)
 *     })
 * }
 *
 * setTimeout(() => {
 *     setTimeout(() => {
 *         dbc(async() => {
 *             return test('a')
 *         }, 'a')
 *     }, 1)
 *     //1ms, a進入排程, 但a尚未發呆300ms故無法執行
 *     setTimeout(() => {
 *         dbc(async() => {
 *             return test('b')
 *         }, 'b')
 *     }, 100)
 *     //100ms, b進入排程, 因b尚未發呆300ms故無法執行
 *     //400ms, 因b已發呆300ms故開始執行, 此時也取消a, 400ms開始, 至900ms結束
 *     setTimeout(() => {
 *         dbc(async() => {
 *             return test('c')
 *         }, 'c')
 *     }, 500)
 *     setTimeout(() => {
 *         dbc(async() => {
 *             return test('d')
 *         }, 'd')
 *     }, 800)
 *     //500與800ms, c與d皆進入排程, 但b還在執行中故無法執行c與d
 *     //900ms, b執行完, 故取最後d開始執行, c已取消, 900ms開始, 至1400ms結束
 *     //1400ms, d執行完, 無任何排程等待
 *     setTimeout(() => {
 *         dbc(async() => {
 *             return test('e')
 *         }, 'e')
 *     }, 1500)
 *     //1500ms, e進入排程, 目前無任何排程, 但因e尚未發呆300ms故無法執行
 *     //1800ms, 因e已發呆300ms故開始執行, 1800ms開始, 至2300ms結束
 *     //2300ms, e執行完
 * }, 20)
 * // 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
 * // 900 ms test in: d
 * // 1000 ms
 * // 1100 ms
 * // 1200 ms
 * // 1300 ms
 * // 1400 ms
 * // 1400 ms test out: d:2
 * // 1500 ms
 * // 1600 ms
 * // 1700 ms
 * // 1800 ms
 * // 1800 ms test in: e
 * // 1900 ms
 * // 2000 ms
 * // 2100 ms
 * // 2200 ms
 * // 2300 ms
 * // 2300 ms test out: e:3
 * // 2400 ms
 * // 2500 ms
 * // 2600 ms
 *
 */
function pmDebounce(ms = 300) {

    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)

        function detect() {

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

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

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

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

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

                //b
                let b = false
                if (tLast === null) {
                    b = true
                }
                else {
                    let tDiff = Date.now() - tLast
                    b = tDiff > ms
                }

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

                    //state
                    state = 'doing'
                    // console.log('state1', state)

                    //update
                    tLast = Date.now()

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

                    //執行最後的任務
                    let r = m.func(...m.input)
                    if (ispm(r)) {
                        r
                            .then((res) => {
                                m.output = res
                            })
                            .catch((err) => {
                                m.output = { err }
                            })
                            .finally(() => {
                                state = 'wait'
                                // console.log('state2a', state, 'q', q)
                            })
                    }
                    else {
                        m.output = r
                        state = 'wait'
                        // console.log('state2b', state, 'q', q)
                    }

                }

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

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

        }

        function run(func, ...input) {

            //check
            if (!isfun(func)) {
                console.log('func is not a function')
                return
            }

            //first tLast
            if (tLast === null) {
                tLast = Date.now()
            }
            else if (state === 'wait') {
                tLast = Date.now()
            }

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

            //detect
            detect()

        }

        return run
    }

    return new ClsDebounce(ms)
}


export default pmDebounce