import get from 'lodash-es/get.js'
import evem from './evem.mjs'
import waitFun from './waitFun.mjs'
import haskey from './haskey.mjs'
import ispint from './ispint.mjs'
import cint from './cint.mjs'
/**
* 非同步狀態旗標快取,含TTL與互斥/同步原語,可用於在單一process內協調非同步動作之執行順序與資源獨占
*
* Unit Test: {@link https://github.com/yuda-lyu/wsemi/blob/master/test/cacheSt.test.mjs Github}
* @memberOf wsemi
* @param {Object} [opt={}] 輸入設定物件,預設{}
* @param {Integer} [opt.timeExpire=1800000] 輸入TTL過期時間整數,單位為毫秒ms,key寫入超過此時間後將被內部偵測週期自動刪除,預設30分鐘(1800000ms)
* @param {Integer} [opt.timeDetect=2000] 輸入TTL偵測週期整數,單位為毫秒ms,預設2000
* @returns {Object} 回傳事件物件,內含上述對外方法可呼叫,亦提供on用於監聽各方法之事件
* @example
*
* let test1 = async () => {
* let ms = []
*
* let cs = cacheSt({ timeExpire: 10000 })
*
* //案例: mutex取得與釋放
* let b1 = cs.checkWithSet('myLock') //第一次取得lock, 回true
* ms.push({ '1st checkWithSet': b1 })
* let b2 = cs.checkWithSet('myLock') //第二次嘗試, 已被占用回false
* ms.push({ '2nd checkWithSet': b2 })
* await cs.del('myLock') //釋放lock
* let b3 = cs.checkWithSet('myLock') //釋放後可再取
* ms.push({ 'after del checkWithSet': b3 })
*
* cs.clear() //結束時停止TTL偵測timer
* console.log('ms', ms)
* return ms
* }
* await test1()
* // ms [
* // { '1st checkWithSet': true },
* // { '2nd checkWithSet': false },
* // { 'after del checkWithSet': true }
* // ]
*
* let test2 = async () => {
* let ms = []
*
* let cs = cacheSt({ timeExpire: 10000 })
*
* //案例: 多key原子占用並執行函數, 結束自動釋放
* let r = await cs.setWithFree(['lockA', 'lockB'], async () => {
* ms.push({ inside: 'fn running' })
* return 'done'
* })
* ms.push({ result: r })
* ms.push({ 'lockA after': await cs.check('lockA') }) //已自動釋放
* ms.push({ 'lockB after': await cs.check('lockB') }) //已自動釋放
*
* cs.clear()
* console.log('ms', ms)
* return ms
* }
* await test2()
* // ms [
* // { inside: 'fn running' },
* // { result: 'done' },
* // { 'lockA after': false },
* // { 'lockB after': false }
* // ]
*
* let test3 = async () => {
* let ms = []
*
* let cs = cacheSt({ timeExpire: 10000 })
*
* //案例: 等待key出現 (條件變數模式)
* setTimeout(() => {
* cs.set('ready', 'data-x')
* }, 200)
*
* await cs.waitExist('ready', { attemptNum: 30, timeInterval: 100 }) //3秒內等候ready出現
* let v = await cs.get('ready')
* ms.push({ 'after waitExist': v })
*
* cs.clear()
* console.log('ms', ms)
* return ms
* }
* await test3()
* // ms [
* // { 'after waitExist': 'data-x' }
* // ]
*
*/
function cacheSt(opt = {}) {
let timeExpire = get(opt, 'timeExpire', null)
if (!ispint(timeExpire)) {
timeExpire = 30 * 60 * 1000
}
timeExpire = cint(timeExpire)
let timeDetect = get(opt, 'timeDetect', null)
if (!ispint(timeDetect)) {
timeDetect = 2000
}
timeDetect = cint(timeDetect)
let ev = evem()
let _gs = {}
let _set = async (key, value = true) => {
_gs[key] = {
time: Date.now(),
value,
}
let msg = `set: ${key}`
ev.emit('set', { state: 'success', msg })
}
let _get = async (key) => {
let r = _gs[key]
let value = r === undefined ? null : r.value
let msg = `get: ${key}`
ev.emit('get', { state: 'success', msg })
return value
}
let _check = async (key) => {
let exist = haskey(_gs, key)
let msg = `check: ${key} = ${exist}`
ev.emit('check', { state: 'success', msg })
return exist
}
let _checkWithSet = (key) => {
if (haskey(_gs, key)) {
let msg = `checkWithSet: key in use: ${key}`
ev.emit('checkWithSet', { state: 'error', msg })
return false
}
_gs[key] = {
time: Date.now(),
value: true,
}
let msg = `checkWithSet: ${key}`
ev.emit('checkWithSet', { state: 'success', msg })
return true
}
let _setWithFree = async (keys, fn) => {
ev.emit('setWithFree', { state: 'start' })
let _ks = []
for (let key of keys) {
if (_checkWithSet(key)) {
_ks.push(key)
}
else {
//回滾已占之 key
for (let k of _ks) {
delete _gs[k]
}
let msg = `setWithFree: key in use: ${key}`
ev.emit('setWithFree', { state: 'error', msg })
return Promise.reject(msg)
}
}
try {
let r = await fn()
let msg = `setWithFree: ${keys.join(', ')}`
ev.emit('setWithFree', { state: 'success', msg })
return r
}
catch (err) {
let msg = `setWithFree: fn threw: ${(err && err.message) || err}`
ev.emit('setWithFree', { state: 'error', msg })
throw err
}
finally {
for (let k of _ks) {
delete _gs[k]
}
}
}
let _del = async (key) => {
delete _gs[key]
let msg = `del: ${key}`
ev.emit('del', { state: 'success', msg })
}
let _waitExist = async (key, optWait = {}) => {
let attemptNum = get(optWait, 'attemptNum', null)
if (!ispint(attemptNum)) {
attemptNum = 30
}
attemptNum = cint(attemptNum)
let timeInterval = get(optWait, 'timeInterval', null)
if (!ispint(timeInterval)) {
timeInterval = 100
}
timeInterval = cint(timeInterval)
ev.emit('waitExist', { state: 'start' })
//waitFun 達 attemptNum 會 reject 'exceeded attemptNum[N]', 須包 catch 才能拋出自家較明確之 timeout 訊息
try {
await waitFun(() => haskey(_gs, key), { attemptNum, timeInterval })
}
catch (e) {
let msg = `waitExist timeout (${attemptNum * timeInterval}ms): ${key}`
ev.emit('waitExist', { state: 'error', msg })
return Promise.reject(msg)
}
let msg = `waitExist resolved: ${key}`
ev.emit('waitExist', { state: 'success', msg })
}
let _waitNotExist = async (key, optWait = {}) => {
let attemptNum = get(optWait, 'attemptNum', null)
if (!ispint(attemptNum)) {
attemptNum = 30
}
attemptNum = cint(attemptNum)
let timeInterval = get(optWait, 'timeInterval', null)
if (!ispint(timeInterval)) {
timeInterval = 100
}
timeInterval = cint(timeInterval)
ev.emit('waitNotExist', { state: 'start' })
//同 _waitExist, 包 catch 才能拋出自家較明確之 timeout 訊息
try {
await waitFun(() => !(haskey(_gs, key)), { attemptNum, timeInterval })
}
catch (e) {
let msg = `waitNotExist timeout (${attemptNum * timeInterval}ms): ${key}`
ev.emit('waitNotExist', { state: 'error', msg })
return Promise.reject(msg)
}
let msg = `waitNotExist resolved: ${key}`
ev.emit('waitNotExist', { state: 'success', msg })
}
let t = setInterval(() => {
ev.emit('detect', { state: 'start' })
let now = Date.now()
for (let key of Object.keys(_gs)) {
let r = _gs[key]
if (r && (now - r.time) > timeExpire) {
delete _gs[key]
}
}
ev.emit('detect', { state: 'finish' })
}, timeDetect)
let clear = () => {
clearInterval(t)
}
//save
ev.checkWithSet = _checkWithSet
ev.setWithFree = _setWithFree
ev.set = _set
ev.get = _get
ev.check = _check
ev.del = _del
ev.waitExist = _waitExist
ev.waitNotExist = _waitNotExist
ev.clear = clear
return ev
}
export default cacheSt