import loGet from 'lodash-es/get.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import evem from './evem.mjs'
import waitFun from './waitFun.mjs'
import isfun from './isfun.mjs'
import haskey from './haskey.mjs'
import ispint from './ispint.mjs'
import isarr from './isarr.mjs'
import cint from './cint.mjs'
/**
* 非同步函數快取
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/cache.test.mjs Github}
* @memberOf wsemi
* @returns {Object} 回傳事件物件,可呼叫事件on、set、get、getProxy、clear、remove。on為監聽事件,需自行監聽message與error事件。set為加入待執行函數,函數結束回傳欲快取的值,set傳入參數依序為key與快取物件,key為唯一識別字串,可使用函數加上輸入參數作為key,因考慮輸入參數可能為大量數據會有效能問題,由開發者自行決定key,而快取物件需設定欄位fun為待執行的非同步函數、inputs為待執行函數fun的傳入參數組、timeExpired為過期時間整數,單位ms,預設5000。get為依照key取得目前快取值。getProxy為合併set與get功能,直接set註冊待執行函數與取值,傳入參數同set,回傳同get。update為強制更新key所屬快取值,同時也會更新該快取之時間至當前。clear為清除key所屬快取的是否執行標記,使該快取視為需重新執行函數取值。remove為直接清除key所屬快取,清除後用set重設
* @example
*
* async function topAsync() {
*
* function test1() {
* return new Promise((resolve, reject) => {
* let ms = []
*
* let oc = cache()
*
* // oc.on('message', function(msg) {
* // console.log('message', msg)
* // })
* // oc.on('error', function(msg) {
* // console.log('error', msg)
* // })
*
* let i = 0
* let j = 0
* function fun(v1, v2) {
* i++
* console.log('call fun, count=' + i)
* ms.push('call fun, count=' + i)
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* j++
* ms.push(v1 + '|' + v2 + ', count=' + j)
* resolve(v1 + '|' + v2 + ', count=' + j)
* }, 300)
* })
* }
*
* oc.set('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 }) //快取1200ms, 但第1次執行就需要300ms, 故執行完畢後只會再保留800ms
* setTimeout(function() {
* //第1次呼叫, 此時沒有快取只能執行取值
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 1st', msg)
* ms.push('fun 1st', msg)
* })
* }, 1)
* setTimeout(function() {
* //第2次呼叫(50ms), 此時第1次呼叫還沒完成(要到300ms), 故get會偵測並等待, 偵測週期為1000ms, 下次偵測是1050ms, 此時第1次快取尚未過期(1200ms), 故1050ms取值時會拿到第1次快取(count=1)
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 2nd', msg)
* ms.push('fun 2nd', msg)
* })
* }, 50)
* setTimeout(function() {
* //第3次呼叫(250ms), 此時第1次呼叫還沒完成(要到300ms), 故get會偵測並等待, 偵測週期為1000ms, 下次偵測是1250ms, 此時第1次快取已過期(1200ms), 故1250ms取值時會重新執行取值(count=2)
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 3rd', msg)
* ms.push('fun 3rd', msg)
* })
* }, 250)
* setTimeout(function() {
* //第4次呼叫(500ms), 此時第1次呼叫已結束(300ms), 且第1次快取(count=1)未過期(要到1200ms), 故get可拿到第1次計算的快取(count=1)
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 4th', msg)
* ms.push('fun 4th', msg)
* })
* }, 500)
* setTimeout(function() {
* //第5次呼叫(1300ms), 此時第1次快取(count=1)已過期(1200ms), 但第3次已重新執行取值(1250~1550ms執行, 2450ms過期), 故get會偵測並等待, 偵測週期為1000ms, 下次偵測是2300ms, 且此時第3次所得快取(count=2)尚未過期(2450ms), 此時就會拿到第3次所得快取(count=2)
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 5th', msg)
* ms.push('fun 5th', msg)
* })
* }, 1300)
* setTimeout(function() {
* //第6次呼叫(1600ms), 此時第3次所得快取(count=2)還在有效期(1550ms執行結束, 2450ms過期), 故get會拿到第3次所得快取(count=2)
* oc.get('fun')
* .then(function(msg) {
* console.log('fun 6th', msg)
* ms.push('fun 6th', msg)
* })
* }, 1600)
*
* setTimeout(function() {
* resolve(ms)
* }, 2400)
*
* })
* }
* console.log('test1')
* let r1 = await test1()
* console.log(JSON.stringify(r1))
* // test1
* // call fun, count=1
* // fun 1st inp1|inp2, count=1
* // fun 4th inp1|inp2, count=1
* // fun 2nd inp1|inp2, count=1
* // call fun, count=2
* // fun 3rd inp1|inp2, count=2
* // fun 6th inp1|inp2, count=2
* // fun 5th inp1|inp2, count=2
* // ["call fun, count=1","inp1|inp2, count=1","fun 1st","inp1|inp2, count=1","fun 4th","inp1|inp2, count=1","fun 2nd","inp1|inp2, count=1","call fun, count=2","inp1|inp2, count=2","fun 3rd","inp1|inp2, count=2","fun 6th","inp1|inp2, count=2","fun 5th","inp1|inp2, count=2"]
*
* function test2() {
* return new Promise((resolve, reject) => {
* let ms = []
*
* let oc = cache()
*
* // oc.on('message', function(msg) {
* // console.log('message', msg)
* // })
* // oc.on('error', function(msg) {
* // console.log('error', msg)
* // })
*
* let i = 0
* let j = 0
* function fun(v1, v2) {
* i++
* console.log('call fun, count=' + i)
* ms.push('call fun, count=' + i)
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* j++
* ms.push(v1 + '|' + v2 + ', count=' + j)
* resolve(v1 + '|' + v2 + ', count=' + j)
* }, 300)
* })
* }
*
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 }) //快取1200ms, 但第1次執行就需要300ms, 故執行完畢後只會再保留800ms
* setTimeout(function() {
* //第1次呼叫, 此時沒有快取只能執行取值, 因偵測週期為1000ms故得要1001ms才會回應, 會取得第1次結果(count=1)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 })
* .then(function(msg) {
* console.log('fun 1st', msg)
* ms.push('fun 1st', msg)
* })
* }, 1)
* setTimeout(function() {
* //第2次呼叫, 此時執行中會等待, 因偵測週期為1000ms, 故得等到下次偵測1100ms才會回應, 此時會取得第1次結果(count=1)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 })
* .then(function(msg) {
* console.log('fun 2nd', msg)
* ms.push('fun 2nd', msg)
* })
* }, 100)
* setTimeout(function() {
* //第3次呼叫, 此時已有快取, 故此時500ms就會先回應, 會取得第1次結果(count=1)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 })
* .then(function(msg) {
* console.log('fun 3rd', msg)
* ms.push('fun 3rd', msg)
* })
* }, 500)
* setTimeout(function() {
* //第4次呼叫, 此時第1次快取(count=1)已失效, 會重新呼叫函數取值, 取得第2次結果(count=2)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1200 })
* .then(function(msg) {
* console.log('fun 4th', msg)
* ms.push('fun 4th', msg)
* })
* }, 1300)
*
* setTimeout(function() {
* resolve(ms)
* }, 1700)
*
* })
* }
* console.log('test2')
* let r2 = await test2()
* console.log(JSON.stringify(r2))
* // test2
* // call fun, count=1
* // fun 3rd inp1|inp2, count=1
* // fun 1st inp1|inp2, count=1
* // fun 2nd inp1|inp2, count=1
* // call fun, count=2
* // fun 4th inp1|inp2, count=2
* // ["call fun, count=1","inp1|inp2, count=1","fun 3rd","inp1|inp2, count=1","fun 1st","inp1|inp2, count=1","fun 2nd","inp1|inp2, count=1","call fun, count=2","inp1|inp2, count=2","fun 4th","inp1|inp2, count=2"]
*
* function test3() {
* return new Promise((resolve, reject) => {
* let ms = []
*
* let oc = cache()
*
* // oc.on('message', function(msg) {
* // console.log('message', msg)
* // })
* // oc.on('error', function(msg) {
* // console.log('error', msg)
* // })
*
* let i = 0
* let j = 0
* function fun(v1, v2) {
* i++
* console.log('call fun, count=' + i)
* ms.push('call fun, count=' + i)
* return new Promise(function(resolve, reject) {
* setTimeout(function() {
* j++
* ms.push(v1 + '|' + v2 + ', count=' + j)
* resolve(v1 + '|' + v2 + ', count=' + j)
* }, 300)
* })
* }
*
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 }) //快取1500ms, 但第1次執行就需要300ms, 故執行完畢後只會再保留800ms
* setTimeout(function() {
* //第1次呼叫(延遲1ms), 此時沒有快取只能執行取值, 因偵測週期為1000ms故得要1001ms才會回應, 回應時為被強制更新(1100ms)之前, 會取得第1次結果(count=1)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 })
* .then(function(msg) {
* console.log('fun 1st', msg)
* ms.push('fun 1st', msg)
* })
* }, 1)
* setTimeout(function() {
* //第2次呼叫(延遲200ms), 此時執行中會等待, 因偵測週期為1000ms, 故得等到下次偵測1200ms才會回應, 回應時為被強制更新(1100ms)之後, 此時會取得被強制更新的結果(abc)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 })
* .then(function(msg) {
* console.log('fun 2nd', msg)
* ms.push('fun 2nd', msg)
* })
* }, 200)
* setTimeout(function() {
* //第3次呼叫, 此時已有快取, 故此時500ms就會先回應, 會取得第1次結果(count=1)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 })
* .then(function(msg) {
* console.log('fun 3rd', msg)
* ms.push('fun 3rd', msg)
* })
* }, 500)
* setTimeout(function() {
* //更新快取值(延遲1100ms), 快取值為abc, 快取時間也被更新至此時, 故會重新計算1500ms才會失效
* oc.update('fun', 'abc')
* console.log('fun update', 'abc')
* ms.push('fun update', 'abc')
* }, 1100)
* setTimeout(function() {
* //第4次呼叫(延遲1300ms), 此時會取得被強制更新之快取值(abc), 快取還剩1300ms才失效(也就是在2600ms失效)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 })
* .then(function(msg) {
* console.log('fun 4th', msg)
* ms.push('fun 4th', msg)
* })
* }, 1300)
* setTimeout(function() {
* //第5次呼叫(延遲2700ms), 此時被強制更新之快取值(abc)已失效, 會重新呼叫函數取值, 取得第2次結果(count=2)
* oc.getProxy('fun', { fun, inputs: ['inp1', 'inp2'], timeExpired: 1500 })
* .then(function(msg) {
* console.log('fun 5th', msg)
* ms.push('fun 5th', msg)
* })
* }, 2700)
*
* setTimeout(function() {
* resolve(ms)
* }, 3100)
*
* })
* }
* console.log('test3')
* let r3 = await test3()
* console.log(JSON.stringify(r3))
* // test3
* // call fun, count=1
* // fun 3rd inp1|inp2, count=1
* // fun 1st inp1|inp2, count=1
* // fun update abc
* // fun 2nd abc
* // fun 4th abc
* // call fun, count=2
* // fun 5th inp1|inp2, count=2
* // ["call fun, count=1","inp1|inp2, count=1","fun 3rd","inp1|inp2, count=1","fun 1st","inp1|inp2, count=1","fun update","abc","fun 2nd","abc","fun 4th","abc","call fun, count=2","inp1|inp2, count=2","fun 5th","inp1|inp2, count=2"]
*
* }
* topAsync().catch(() => {})
*
*/
function cache() {
let ev = evem()
let data = {} //快取資料
function emit(mode, data) {
setTimeout(() => { //用timer脫勾
ev.emit(mode, data)
}, 1)
}
function set(key, opt = {}) {
//check
if (haskey(data, key)) {
//可重複設定不報錯
//emit('error', { fun: 'set', key, msg: 'has key' })
return
}
//fun
let fun = loGet(opt, 'fun')
if (!isfun(fun)) {
fun = async () => {}
}
//inputs
let inputs = loGet(opt, 'inputs')
if (!isarr(inputs)) {
inputs = []
}
//timeExpired
let timeExpired = loGet(opt, 'timeExpired')
if (!ispint(timeExpired)) {
timeExpired = 5000
}
timeExpired = cint(timeExpired)
//save
data[key] = {
needExec: true,
fun,
running: false,
inputs,
value: null,
time: null,
timeExpired,
}
emit('message', { fun: 'set', key, timeExpired })
}
async function get(key) {
if (haskey(data, key)) {
let b
//若執行中則強制等待
emit('message', { fun: 'get', key, msg: 'waiting' })
await waitFun(() => {
return !data[key].running
}, { timeInterval: 1000 }) //偵測週期1000ms
//t
let t = Date.now()
//needExec or timeExpired
b = data[key].needExec
let timeDiff = (t - data[key].time)
if (b) {
emit('message', { fun: 'get', key, msg: 'execute first' })
}
else if (data[key].timeExpired > 0 && (timeDiff > data[key].timeExpired)) {
emit('message', { fun: 'get', key, msg: 'execute by timeExpired', timeDiff })
b = true
}
//fun
if (b) {
emit('message', { fun: 'get', key, msg: 'fun start' })
data[key].running = true
data[key].value = await data[key].fun(...data[key].inputs)
.catch((err) => {
emit('error', { fun: 'get', key, msg: err })
})
data[key].needExec = false
data[key].time = t
data[key].running = false
emit('message', { fun: 'get', key, msg: 'fun end' })
}
else {
emit('message', { fun: 'get', key, msg: 'use cache' })
}
return cloneDeep(data[key].value) //若為物件可能受外部調用而被修改到快取區的記憶體, 故得要用cloneDeep複製才回傳
}
else {
emit('error', { fun: 'get', key, msg: 'invalid key' })
return null
}
}
async function getProxy(key, opt = {}) {
set(key, opt)
return get(key)
}
function update(key, value) {
if (haskey(data, key)) {
emit('message', { fun: 'updateValue', key })
data[key].value = value
data[key].time = Date.now()
}
}
function clear(key) {
if (haskey(data, key)) {
emit('message', { fun: 'clear', key })
data[key].needExec = true
}
}
function remove(key) {
if (haskey(data, key)) {
emit('message', { fun: 'remove', key })
delete data[key]
}
}
//save
ev.set = set
ev.getProxy = getProxy
ev.get = get
ev.update = update
ev.clear = clear
ev.remove = remove
return ev
}
export default cache