pmTimeout.mjs

import get from 'lodash-es/get.js'
import genPm from './genPm.mjs'
import ispm from './ispm.mjs'
import isestr from './isestr.mjs'
import isp0int from './isp0int.mjs'


/**
 * 為Promise添加逾時限制
 *
 * 以Promise.race令pm與計時器競速,pm先完成則原樣回傳其resolve或reject之結果,逾時則reject一個Error
 * 該Error之message為'timeout[ms數ms]'(有label時置於前綴),並帶有code為'TIMEOUT'與timeout為逾時毫秒數等欄位,供呼叫端辨別為逾時而非pm自身之錯誤
 * 注意JavaScript無法中止已啟動之Promise,逾時僅代表不再等待其結果,pm內之工作仍會繼續執行至自然結束,若需一併中止內部工作(如子進程)須由pm自行處理
 * 本函數為async function,參數錯誤亦以reject傳出而非同步throw,故可與逾時錯誤一併由.catch或try/catch接住
 *
 * Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/pmTimeout.test.mjs Github}
 * @memberOf wsemi
 * @param {Promise} pm 輸入Promise物件,非Promise時reject錯誤'invalid pm'
 * @param {Integer} ms 輸入逾時毫秒數,須為非負整數,為0代表下一輪事件迴圈即逾時,其餘reject錯誤'invalid ms[ms數]'
 * @param {Object} [opt={}] 輸入設定物件
 * @param {String} [opt.label=''] 輸入識別字串,將置於逾時錯誤訊息之前綴以利辨識來源,非有效字串時回退為空字串,預設''
 * @returns {Promise} 回傳Promise,pm先完成則resolve或reject其結果,逾時則reject一個code為'TIMEOUT'之Error
 * @example
 * //need test in nodejs
 *
 * async function test() {
 *
 *     //pm先完成則原樣回傳其結果
 *     let r1 = await pmTimeout(delay(10).then(() => 'ok'), 1000)
 *     console.log('r1', r1)
 *     // => r1 ok
 *
 *     //逾時則reject一個code為'TIMEOUT'之Error
 *     try {
 *         await pmTimeout(delay(3000), 100)
 *     }
 *     catch (err) {
 *         console.log('r2', err.message, err.code, err.timeout)
 *         // => r2 timeout[100ms] TIMEOUT 100
 *     }
 *
 *     //有label時置於訊息前綴
 *     try {
 *         await pmTimeout(delay(3000), 100, { label: 'fetchUser' })
 *     }
 *     catch (err) {
 *         console.log('r3', err.message)
 *         // => r3 fetchUser timeout[100ms]
 *     }
 *
 *     //pm自身之reject原樣傳出, 其code非'TIMEOUT'故可與逾時區分
 *     try {
 *         await pmTimeout(Promise.reject(new Error('mine')), 1000)
 *     }
 *     catch (err) {
 *         console.log('r4', err.message, err.code)
 *         // => r4 mine undefined
 *     }
 *
 * }
 * test()
 *     .catch((err) => {
 *         console.log(err)
 *     })
 *
 */
async function pmTimeout(pm, ms, opt = {}) {

    //check, pm與ms給錯視為程式撰寫錯誤
    //本函數為async function, 故此處throw會轉為reject傳出, 與pm*家族之錯誤傳遞管道一致(見pmSeries/pmChain)
    if (!ispm(pm)) {
        throw new Error(`invalid pm`)
    }

    //check
    if (!isp0int(ms)) {
        throw new Error(`invalid ms[${ms}]`)
    }

    //label
    let label = get(opt, 'label', null)
    if (!isestr(label)) {
        label = ''
    }

    //msgErr, label為空時不加前綴, 避免訊息出現多餘之前導空白
    let msgErr = `timeout[${ms}ms]`
    if (isestr(label)) {
        msgErr = `${label} ${msgErr}`
    }

    //timeout, 計時器用Promise, 逾時即reject
    let t
    let timeout = genPm()
    t = setTimeout(() => {
        let e = new Error(msgErr)
        e.code = 'TIMEOUT' //供呼叫端辨別為逾時而非pm自身之錯誤
        e.timeout = ms
        timeout.reject(e)
    }, ms)

    //race, 先完成者勝出
    //無論勝負皆須clearTimeout: pm先完成時若不清除, 計時器會持續佔住event loop而延後process結束
    //pm逾時落敗後其自身之reject仍由Promise.race內部掛載之handler接住, 不會產生unhandled rejection
    let r = Promise.race([pm, timeout])
        .finally(() => clearTimeout(t))

    return r
}


export default pmTimeout