import ispint from './ispint.mjs'
import isfun from './isfun.mjs'
import cint from './cint.mjs'
import pm2resolve from './pm2resolve.mjs'
import getTrace from './getTrace.mjs'
/**
* 非同步函數進行防抖
*
* 同時僅會執行一個佇列(非同步函數),到達防抖時間才會取最後一個佇列執行,待其執行完畢,若期間有新佇列進來則儲存,待執行完畢後取最後佇列出來執行
*
* 防抖時間(ms)自最後一次調用起算,任務執行中進來之調用亦計入;每20ms偵測一次,任務執行完畢時若自最後一次調用起已閒置滿ms,即於下一次偵測執行最後之佇列,未滿則只再等剩餘時間(不因任務執行完畢而重新起算)
*
* 任務之結果經pm2resolve正規化,任務同步拋錯或其Promise被reject皆視為執行完畢,以console.log記錄而不外拋(不產生未處理之rejection),不影響後續佇列
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/pmDebounce.test.mjs Github}
* @memberOf wsemi
* @param {Integer} [ms=300] 輸入未有調用的時間區間,為正整數,預設300ms
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Function} [opt.onTrace=null] 輸入觀察回調函數,於內部事件發生當下同步呼叫,傳入一個凍結之事件物件{ type, ... },供測試以事件(而非等待時間)同步與診斷;type為'push'時為任務排入佇列(含size為排入後之佇列長度),'start'時為開始偵測(每20ms檢查一次是否已超過ms未有調用),'run'時為已超過ms未有調用而即將執行最後之任務(含dropped為被捨棄之較早任務數),'done'時為被執行之任務已執行完畢(含ok為是否成功,其後才恢復可執行下一批),'stop'時為佇列已空而停止偵測;回調不帶接收者呼叫,其拋錯與回傳值皆忽略,不影響去除抖動,預設null
* @returns {Function} 回傳Function,輸入為非同步函數與其輸入,會推入佇列後等待執行,呼叫無回傳值,被執行任務之結果與錯誤皆不回傳給呼叫端,須由任務自行處理;輸入非函數時不排入佇列(不影響已排入之任務),以console.log記錄'func is not a function'
* @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皆為等待, 發呆時間自最後一次調用(d, 900ms)起算
* //1000ms, b已執行完, 取最末d出來判識(c被強制剔除), 此時d僅發呆100ms(900->1000)未滿300ms, 故再等剩餘200ms
* //1200ms, d已發呆滿300ms(900->1200), 開始執行(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, opt = {}) {
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)
//trace, 觀察回調(opt.onTrace), 未給時為null
let trace = getTrace(opt)
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
if (trace) {
trace('stop', {})
}
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'
//取最後的任務與清空佇列
let m = q.pop()
if (trace) {
trace('run', { dropped: q.length })
}
q = []
// console.log('m', m)
//執行最末任務, 結果經pm2resolve正規化(同步回傳、同步拋錯、Promise皆同), 其必定resolve, 故必定恢復wait而不停擺, 亦不使計時器回呼拋錯
// console.log('fun start ii', getIt(), 'ms', tDiff, m.input)
pm2resolve(m.func)(...m.input)
.then((res) => {
if (trace) {
trace('done', { ok: res.state === 'success' })
}
m.output = (res.state === 'success') ? res.msg : { err: res.msg }
if (res.state === 'error') {
//比照debounce記錄失敗, 記錄本身亦可能拋錯(如值之util.inspect.custom會拋), 須包try, 否則成為未處理之rejection
try {
console.log('m.func catch', res.msg)
}
catch (err) {}
}
})
.finally(() => {
//恢復wait, 不更新tLast: 防抖時間自最後一次調用起算, 任務完成時已閒置滿ms者於下一次偵測即執行, 未滿者只等剩餘時間
state = 'wait'
// console.log('fun end ii', getIt(), 'ms', m.input)
})
}
}, 20) //20ms偵測, 啟動後跑timer, 無佇列則會停止減耗
if (trace) {
trace('start', {})
}
}
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)
//tLast, 每次調用皆更新為最後一次調用之時間, 任務執行中進來之調用亦計入
tLast = Date.now()
//push
q.push({
func,
input,
output: null,
})
// console.log('push q', q)
if (trace) {
trace('push', { size: q.length })
}
//detect
detect()
}
return run
}
return new ClsDebounce(ms)
}
export default pmDebounce