WOrmLmdb.mjs

import { open, ABORT } from 'lmdb'
// import mingo from 'mingo' //mingo內未更新import寫法, 會導致ERR_UNSUPPORTED_DIR_IMPORT, 故須改用require引入使用
import mingo from './reqMingo.js'
import size from 'lodash-es/size.js'
import get from 'lodash-es/get.js'
import each from 'lodash-es/each.js'
import map from 'lodash-es/map.js'
import merge from 'lodash-es/merge.js'
import isEqual from 'lodash-es/isEqual.js'
import cloneDeep from 'lodash-es/cloneDeep.js'
import isestr from 'wsemi/src/isestr.mjs'
import isarr from 'wsemi/src/isarr.mjs'
import isearr from 'wsemi/src/isearr.mjs'
import iseobj from 'wsemi/src/iseobj.mjs'
import isbol from 'wsemi/src/isbol.mjs'
import haskey from 'wsemi/src/haskey.mjs'
import evem from 'wsemi/src/evem.mjs'
import genIDSeq from 'wsemi/src/genIDSeq.mjs'
import pmSeries from 'wsemi/src/pmSeries.mjs'
import waitFun from 'wsemi/src/waitFun.mjs'


/**
 * 操作資料庫(LMDB)
 *
 * 回傳物件為EventEmitter,除各操作函數外另發出change與error兩事件,供呼叫端於單一處集中觀察資料異動與失敗。
 * 事件僅為附加通知,其所送出之資訊皆另有正規管道(操作結果經resolve、整批性錯誤經reject、逐筆失敗經該筆之err欄位),
 * 故不監聽亦能取得完整資訊,且監聽與否不改變任何操作之回傳值。
 *
 * change事件,參數為(mode, data, res),於資料實際異動成功後發出:
 * mode為操作別字串,可為'insert'、'insertBulk'、'save'、'del'、'delAll';save內若逐筆走自動插入則該筆另發出mode為'insert'之事件。
 * data為本次操作之輸入數據,delAll固定為null。
 * res為本次操作之回傳結果。
 * 逐筆函數以整批為單位發出一次而不逐筆發出,select與selectByPk不發出本事件。
 *
 * error事件,參數為(mode, data, err),於操作發生錯誤時發出:
 * mode為操作別字串,可為'select'、'selectByPk'、'insert'、'insertBulk'、'save'、'del'、'delAll'。
 * data為本次操作之輸入數據,無輸入數據者為null。
 * err為錯誤訊息字串,內容與正規管道所送出者一致。
 * 整批性錯誤於reject之前發出;逐筆失敗於該筆結果定案後發出,每筆一次。
 * 註: 逐筆失敗時整批仍resolve,故收到本事件不表示該次呼叫失敗;正常結果(如查無數據、主鍵未命中、全數已存在)不發出本事件。
 *
 * @class
 * @param {Object} [opt={}] 輸入設定物件,預設{}
 * @param {String} [opt.url='_db'] 輸入資料庫用資料夾字串,預設'_db'
 * @param {String} [opt.db='worm'] 輸入使用資料庫名稱字串,預設'worm'
 * @param {String} [opt.cl='test'] 輸入使用資料表名稱字串,預設'test'
 * @param {Boolean} [opt.useCache=false] 輸入是否使用select快取,適用於單程序操作,預設false
 * @param {Boolean} [opt.autoGenPk=true] 輸入insert與save於數據未帶有效主鍵(本套件為id欄位)時是否自動產生主鍵值,預設true;給false代表主鍵改由呼叫端自備,套件不產生亦不檢查其唯一性與格式,未帶有效主鍵者將reject
 * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
 */
function WOrmLmdb(opt = {}) {

    //_cache
    let _cache = null

    //url
    let url = get(opt, 'url')
    if (!isestr(url)) {
        url = './_db'
    }

    //db
    let db = get(opt, 'db')
    if (!isestr(db)) {
        db = 'worm'
    }

    //cl
    let cl = get(opt, 'cl')
    if (!isestr(cl)) {
        cl = 'test'
    }

    //useCache
    let useCache = get(opt, 'useCache')
    if (!isbol(useCache)) {
        useCache = false
    }

    //autoGenPk, 主鍵由誰產生為整個資料表之政策, 故為建構層設定, 不可於insert與save之option逐次覆寫,
    //否則同一資料表將混入兩種來源之主鍵而難以追溯
    let autoGenPk = get(opt, 'autoGenPk')
    if (!isbol(autoGenPk)) {
        autoGenPk = true
    }

    //storage
    let storage = `${url}/${db}/${cl}`
    // console.log('storage',storage)

    // client
    let client = open({
        path: storage,
        compression: true,
        useVersions: true,
    })

    //ee
    let ee = evem()

    //getData
    let getData = async() => {

        //check
        if (useCache && isarr(_cache)) {
            return cloneDeep(_cache) //與外部使用數據脫勾
        }

        //waitOpen
        await waitOpen()
        // console.log('client.status',client.status)

        //ltdt
        let ltdt = []
        for await (let { value: dt } of client.getRange()) {
            // console.log('dt',dt)
            ltdt.push(dt)
        }

        if (useCache) {
            _cache = ltdt
            return cloneDeep(ltdt) //與外部使用數據脫勾
        }
        return ltdt
    }

    //waitOpen, 等待client開啟, 因getRange與get於client關閉時會拋錯
    //closed為終態: LMDB env一旦關閉不會自行回到open, 續等必然逾時, 故直接拋出而非空轉至逾時
    //各函數入口已有procClosed守門, 此處之終態判斷為縱深防禦, 攔[操作進行中被close]之競態
    //等待參數收斂: 開啟中為毫秒級過渡, 50次x200ms=10秒已極寬裕, 不使非終態之等待拖至waitFun預設之200秒
    let waitOpen = async () => {
        if (client.status === 'closed') {
            throw new Error('client is closed, instance can not be used after close()')
        }
        await waitFun(() => {
            return client.status === 'open'
        }, { attemptNum: 50, timeInterval: 200 })
    }

    //procClosed, 各函數入口之終態守門: close()後再操作屬整批性錯誤, 立即reject令誤用當場可見
    //不可靜默回正常空值(如null或未命中), 否則[已關閉]與[查無資料]同形,
    //去重類呼叫端將把每筆判為未存在而fail-open, 整批重送下游昂貴動作
    let procClosed = (mode, data) => {
        let msg = `can not ${mode} by closed client, instance can not be used after close()`
        emitError(mode, data, msg)
        return Promise.reject(new Error(msg))
    }

    //emitChange, 資料實際異動成功後發出, 事件僅為附加通知不承擔回傳義務
    //一律包try/catch, 令訂閱函數自身拋錯不影響本次操作之結果
    let emitChange = (mode, data, res) => {
        try {
            ee.emit('change', mode, data, res)
        }
        catch (err) {
            console.log(err)
        }
    }

    //emitError, 操作發生錯誤時發出, 錯誤訊息一律轉為字串
    //此try/catch除攔訂閱函數之例外外另有必要: Node之EventEmitter於'error'無監聽者時會將錯誤直接拋出,
    //若不攔則同一次操作會因呼叫端有無註冊監聽而走向不同結果
    let emitError = (mode, data, err) => {
        try {
            ee.emit('error', mode, data, getErrMsg(err))
        }
        catch (errEmit) {
            console.log(errEmit)
        }
    }

    //getErrMsg, 取錯誤訊息字串, 供逐筆結果之err欄位與error事件使用
    let getErrMsg = (err) => {
        let m = get(err, 'message')
        if (isestr(m)) {
            return m
        }
        return String(err)
    }

    //procPk, 依autoGenPk處理各數據之主鍵, 供insert與save共用
    //autoGenPk為true時補值; 為false時不補值, 未帶有效主鍵者屬呼叫端未履行契約, 拋出為整批性錯誤,
    //且檢查於任何寫入之前完成, 故不會有部份筆數已寫入而整批失敗之情形
    let procPk = (data, fnName) => {
        if (autoGenPk) {
            return map(data, function(v) {
                if (!isestr(v.id)) {
                    v.id = genIDSeq()
                }
                return v
            })
        }
        each(data, function(v, k) {
            if (!isestr(get(v, 'id'))) {
                throw new Error(`can not ${fnName} by data[${k}] without valid id when autoGenPk is false`)
            }
        })
        return data
    }

    //getValue
    let getValue = async (key) => {
        let value = null
        try {
            value = await client.get(key)
        }
        catch (err) {
            // if (err.notFound) {
            //     console.log('資料不存在')
            // }
            // else {
            //     console.log('其他錯誤:', err)
            // }
        }
        return value
    }

    /**
     * 查詢數據
     *
     * @memberOf WOrmLmdb
     * @param {Object} [find={}] 輸入查詢條件物件
     * @returns {Promise} 回傳Promise,resolve回傳數據,reject回傳錯誤訊息
     */
    async function select(find = {}) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('select', null)
        }
        let isErr = false
        let res = null

        try {

            //ltdt
            let ltdt = await getData()
            // console.log('select ltdt',ltdt)

            //filter
            if (iseobj(find)) {

                //q
                let q = new mingo.Query(find)
                // console.log('q', q)

                //find
                res = q.find(ltdt).all()
                // console.log('select(find) ltdt',res)

            }
            else {
                res = ltdt
            }

            //check
            if (!isarr(res)) {
                isErr = true
                res = `can not select by find[${JSON.stringify(find)}]`
            }

        }
        catch (err) {
            isErr = true
            res = err
        }
        // console.log('res', res)

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('select', null, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 由主鍵查詢單筆數據,因直接由LMDB取值,不需如select提取全表數據再過濾,故數據量大時效能較佳
     * 註: 本套件之主鍵欄位固定為id,尚未支援指定其他欄位為主鍵
     *
     * @memberOf WOrmLmdb
     * @param {String} pk 輸入主鍵值字串,本套件之主鍵欄位為id
     * @returns {Promise} 回傳Promise,resolve回傳數據物件,若無此主鍵則回傳null,reject回傳錯誤訊息
     */
    async function selectByPk(pk) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('selectByPk', null)
        }
        let isErr = false
        let res = null

        try {

            //check
            if (!isestr(pk)) {
                //未給有效主鍵值視為查無數據
                return null
            }

            //waitOpen
            await waitOpen()

            //查找資料表內pk
            let v = await getValue(pk) //不會有catch

            //check, 判定基準與insert、save、del內對既有數據之認定一致
            if (iseobj(v)) {
                res = v
            }
            else {
                //不存在此主鍵, 回傳null
                res = null
            }

        }
        catch (err) {
            isErr = true
            res = err
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('selectByPk', null, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 插入數據
     *
     * @memberOf WOrmLmdb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {Boolean} [option.returnList=false] 輸入是否改回傳逐筆結果布林值,預設false。給true時回傳與輸入等長保序之陣列,各筆為{n,nInserted,ok},nInserted為1即該筆為新增;聚合計數只回答有幾筆是新的,逐筆結果方能回答是哪幾筆,供下游僅對新資料執行昂貴動作。回傳形式由呼叫點靜態決定,共用之結果處理程式碼不可跨不同取值之呼叫點混用
     * @returns {Promise} 回傳Promise,resolve依returnList回傳插入結果:預設回傳聚合物件{n,nInserted,ok},n為輸入筆數、nInserted為實際插入筆數;returnList為true時回傳逐筆陣列,各筆n恆為1(主鍵命中或經插入)、ok恆為1(insert之錯誤皆屬整批性而reject,不進逐筆);reject回傳錯誤訊息
     */
    async function insert(data, option = {}) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('insert', data)
        }
        let isErr = false

        //returnList
        let returnList = get(option, 'returnList')
        if (!isbol(returnList)) {
            returnList = false
        }

        //check
        if (!iseobj(data) && !isearr(data)) {
            if (returnList) {
                //輸入無效, 對齊save與del之T5規定回傳空陣列
                return []
            }
            return {
                n: 0,
                nInserted: 0,
                ok: 1,
            }
        }

        //cloneDeep, 與外部數據脫勾
        data = cloneDeep(data)

        //res
        let res = null
        try {

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //check id
            data = procPk(data, 'insert')

            //each
            //ifNoExists, 由LMDB於同一寫交易內原子完成[檢查v.id未存在]與[寫入], 併發時同一v.id僅有一次成功,
            //同批含重複v.id時亦僅首筆成功, 故不逐筆await而一次送出, 由LMDB寫執行緒依序處理並合併為批次寫交易
            //注意: 回調函數須為同步函數, 且client.put須於回調函數內直接呼叫,
            //若將client.put置於await之後, 該寫入會逸出條件批次而變成無條件寫入, 並使回傳值與實際狀態不一致
            let nAll = size(data)
            let ltdone = await Promise.all(map(data, function(v) {
                // console.log(v)
                return client.ifNoExists(v.id, () => {
                    client.put(v.id, v)
                })
            }))

            //nPush
            let nPush = 0
            each(ltdone, function(done) {
                if (done) {
                    //未存在v.id, 已寫入
                    nPush++
                }
                else {
                    //已存在v.id則不push
                }
            })

            //res
            if (returnList) {
                //逐筆結果, 與輸入等長保序, 元素沿用逐筆結果之家族形狀(同save與del之元素)
                //n恆為1(主鍵命中既有或經插入而產生), ok恆為1(insert之錯誤皆屬整批性而reject, 不進逐筆),
                //資訊由nInserted承載, filter(v=>v.nInserted===1)之長度必等於聚合模式之nInserted
                res = map(ltdone, function(done) {
                    return {
                        n: 1,
                        nInserted: done ? 1 : 0,
                        ok: 1,
                    }
                })
            }
            else {
                res = {
                    n: nAll,
                    nInserted: nPush,
                    ok: 1,
                }
            }

        }
        catch (err) {
            isErr = true
            res = err
        }

        //update, 不能保證插入多少, 一律重設快取
        _cache = null

        //emit, 於change可能須使用select, 故須放在重設快取之後
        if (!isErr) {
            emitChange('insert', data, res)
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('insert', data, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 批次插入數據,全批視為一個單位,全部插入成功或一筆都不寫入
     * 註: 本函數非insert之加速版,兩者衝突政策不同。insert於主鍵已存在時跳過該筆而整批ok為1,
     * 本函數則整批reject且不寫入任何一筆;同批含重複主鍵者亦視為衝突。確無衝突時兩者結果相同
     * 註: 於本套件不會較insert快,因insert本即以Promise.all一次送出全部條件寫入而非逐筆await,
     * 提供本函數係為與其他w-orm系列套件維持同一組函數,令呼叫端得於各套件間替換而不須改寫呼叫
     *
     * @memberOf WOrmLmdb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @returns {Promise} 回傳Promise,resolve回傳插入結果物件{n,nInserted,ok},n為輸入筆數、nInserted成功時恆等於n,任一筆主鍵已存在則reject回傳錯誤訊息
     */
    async function insertBulk(data) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('insertBulk', data)
        }
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return {
                n: 0,
                nInserted: 0,
                ok: 1,
            }
        }

        //cloneDeep, 與外部數據脫勾
        data = cloneDeep(data)

        //res
        let res = null
        try {

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //check id
            data = procPk(data, 'insertBulk')

            //nAll
            let nAll = size(data)

            //childTransaction, 全有全無由交易回滾保證
            //註: 非同步之client.transaction於中止時並不回滾, 實測其內之put仍會落盤, 故此處須用childTransaction
            //註: 交易內之client.get讀得到同一交易稍早之put, 故同批含重複主鍵者亦會於此被偵測為衝突
            //註: 存在與否採鍵層判定, 與insert之ifNoExists一致
            let pkConflict = null
            await client.childTransaction(() => {
                for (let v of data) {
                    if (client.get(v.id) !== undefined) {
                        pkConflict = v.id
                        return ABORT
                    }
                    client.put(v.id, v)
                }
            })

            //check, 衝突則整批視為失敗, 交易已回滾故無任何寫入
            if (isestr(pkConflict)) {
                throw new Error(`can not insertBulk by existed id[${pkConflict}]`)
            }

            //res, 未衝突則全數插入, 故nInserted恆等於n
            res = {
                n: nAll,
                nInserted: nAll,
                ok: 1,
            }

        }
        catch (err) {
            isErr = true
            res = err
        }

        //update, 不能保證插入多少, 一律重設快取
        _cache = null

        //emit, 於change可能須使用select, 故須放在重設快取之後
        if (!isErr) {
            emitChange('insertBulk', data, res)
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('insertBulk', data, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 儲存數據
     *
     * @memberOf WOrmLmdb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @param {Object} [option={}] 輸入設定物件,預設為{}
     * @param {boolean} [option.autoInsert=true] 輸入是否於儲存時發現原本無數據,則自動改以插入處理,預設為true
     * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之儲存結果陣列,各筆為{n,nInserted,nModified,ok},n為主鍵命中筆數,單筆失敗者ok為0並附err,reject回傳錯誤訊息
     */
    async function save(data, option = {}) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('save', data)
        }
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return []
        }

        //cloneDeep, 與外部數據脫勾
        data = cloneDeep(data)

        //autoInsert
        let autoInsert = get(option, 'autoInsert', true)

        //res
        let res = null
        try {

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //check id
            data = procPk(data, 'save')

            //pmSeries
            res = await pmSeries(data, async(v) => {

                //rest
                let rest = null

                //inserted, 供emit使用
                let inserted = false

                try {

                    //快速路徑, 先不開寫交易直接預讀, 內容相同者本就不須寫入, 可省去開啟寫交易之成本
                    //預讀值縱使已被其他寫入者更動而過期亦不影響正確性, 因內容相同時本次save等價於無操作,
                    //無操作可視為於預讀當下即已完成, 其後他人之寫入結果與[本次save先執行再輪到他人]相同
                    //註: 預讀僅用於判斷是否略過寫入, 不用於決定寫入內容, 寫入內容一律由交易內讀到之現值決定
                    let vp = await getValue(v.id) //不會有catch
                    if (iseobj(vp) && isEqual(merge({}, vp, v), vp)) {
                        //合併後與現值相同不須寫入, 惟已命中v.id故n為1
                        return {
                            n: 1,
                            nInserted: 0,
                            nModified: 0,
                            ok: 1,
                        }
                    }

                    //existed, updated
                    let existed = false
                    let updated = false

                    //transaction, 由LMDB於同一寫交易內原子完成[查找v.id]與[合併寫入],
                    //併發時不會有兩方各自讀到同一舊值再各自寫入而導致遺失更新
                    //注意: 回調函數須為同步函數, client.get於寫交易內為同步取值, 不可使用await,
                    //若將client.get或client.put置於await之後, 該操作會逸出本交易而失去原子性
                    await client.transaction(() => {

                        //查找資料表內v.id
                        let vv = client.get(v.id)

                        //existed
                        existed = iseobj(vv)

                        //check
                        if (existed) {
                            //已存在v.id

                            //vm, 合併後之結果
                            let vm = merge({}, vv, v)

                            //判定基準為[合併後結果與現值相同], 而非[待寫入物件與現值全等],
                            //令nModified忠實反映資料庫端是否真的寫入, 僅給部份欄位且值皆相同者不應回報已修改
                            if (isEqual(vm, vv)) {
                                //合併後與現值相同不須寫入
                            }
                            else {
                                //合併後與現值不同須更新

                                //put
                                client.put(v.id, vm)

                                updated = true
                            }
                        }
                        else {
                            //內容不存在, 若autoInsert則須插入
                            if (autoInsert) {

                                //put
                                client.put(v.id, v)

                                inserted = true
                            }
                        }

                    })

                    //rest, nInserted與nModified恆存在, 無對應行為者給0
                    if (inserted) {
                        rest = {
                            n: 1,
                            nInserted: 1,
                            nModified: 0,
                            ok: 1,
                        }
                    }
                    else if (updated) {
                        rest = {
                            n: 1,
                            nInserted: 0,
                            nModified: 1,
                            ok: 1,
                        }
                    }
                    else {
                        //未寫入, 有二:
                        // 已存在v.id而內容相同不更新, 已命中故n為1
                        // 不存在v.id且未開啟autoInsert, 未命中故n為0
                        //n為v.id之命中筆數, 與nModified[實際更新筆數]分屬不同語義, 不可混用
                        rest = {
                            n: existed ? 1 : 0,
                            nInserted: 0,
                            nModified: 0,
                            ok: 1,
                        }
                    }

                }
                catch (err) {

                    //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
                    rest = {
                        n: 1,
                        nInserted: 0,
                        nModified: 0,
                        ok: 0,
                        err: getErrMsg(err),
                    }

                }

                //emit, 原autoInsert係轉呼叫insert故會發出insert事件, 此處維持既有行為,
                //須於交易外發出, 避免外部訂閱函數於寫交易內執行
                if (inserted) {

                    //update, 不能保證插入多少, 一律重設快取
                    _cache = null

                    //emit
                    emitChange('insert', [v], rest)

                }

                //emit, 逐筆失敗於該筆結果定案後發出, 每筆一次
                //此事件之發出不表示整批失敗, 整批仍resolve, 該筆以ok為0回報
                if (rest.ok === 0) {
                    emitError('save', [v], rest.err)
                }

                return rest
            })

        }
        catch (err) {
            isErr = true
            res = err
        }

        //update, 不能保證變更多少, 一律重設快取
        _cache = null

        //emit, 於change可能須使用select, 故須放在重設快取之後
        //逐筆失敗之error事件已於各筆定案時發出, 故必早於本整批change
        if (!isErr) {
            emitChange('save', data, res)
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('save', data, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 刪除數據
     *
     * @memberOf WOrmLmdb
     * @param {Object|Array} data 輸入數據物件或陣列
     * @returns {Promise} 回傳Promise,resolve回傳與輸入等長之刪除結果陣列,各筆為{n,nDeleted,ok},n為主鍵命中筆數,單筆失敗或未給有效id者ok為0並附err,reject回傳錯誤訊息
     */
    async function del(data) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('del', data)
        }
        let isErr = false

        //check
        if (!iseobj(data) && !isearr(data)) {
            return []
        }

        //cloneDeep, 與外部數據脫勾
        data = cloneDeep(data)

        //res
        let res = null
        try {

            //check
            if (!isarr(data)) {
                data = [data]
            }

            //pmSeries
            res = await pmSeries(data, async(v) => {

                //rest
                let rest = null

                try {

                    //id
                    let id = get(v, 'id', '')

                    //check
                    if (!isestr(id)) {
                        //未給有效v.id視為該筆數據有問題而無法處理, 以ok為0並附err回報,
                        //與[已給v.id但查無數據]之ok為1有別, 二者須可由ok分辨
                        //註: 此處不可直接return, 否則會跳過本回調末尾之error事件發出
                        rest = {
                            n: 0,
                            nDeleted: 0,
                            ok: 0,
                            err: `can not delete by invalid id[${id}]`,
                        }
                    }
                    else {

                        //查找資料表內v.id
                        let vv = await getValue(id) //不會有catch

                        //check
                        if (iseobj(vv)) {
                            //已存在v.id則須刪除

                            //del
                            await client.del(id)

                            //rest
                            rest = {
                                n: 1,
                                nDeleted: 1,
                                ok: 1,
                            }

                        }
                        else {
                            //不存在v.id則不刪除, 未命中故n為0
                            rest = {
                                n: 0,
                                nDeleted: 0,
                                ok: 1,
                            }

                        }

                    }

                }
                catch (err) {

                    //本筆失敗不中斷整批, 以ok為0並附err回報, 由呼叫端逐筆檢查
                    rest = {
                        n: 1,
                        nDeleted: 0,
                        ok: 0,
                        err: getErrMsg(err),
                    }

                }

                //emit, 逐筆失敗於該筆結果定案後發出, 每筆一次
                //此事件之發出不表示整批失敗, 整批仍resolve, 該筆以ok為0回報
                if (rest.ok === 0) {
                    emitError('del', [v], rest.err)
                }

                return rest
            })

        }
        catch (err) {
            isErr = true
            res = err
        }

        //update, 不能保證刪除多少, 一律重設快取
        _cache = null

        //emit, 於change可能須使用select, 故須放在重設快取之後
        //逐筆失敗之error事件已於各筆定案時發出, 故必早於本整批change
        if (!isErr) {
            emitChange('del', data, res)
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('del', data, res)

            return Promise.reject(res)
        }

        return res
    }

    /**
     * 刪除全部數據,需與del分開,避免未傳數據導致直接刪除全表
     *
     * @memberOf WOrmLmdb
     * @param {Object} [find={}] 輸入刪除條件物件
     * @returns {Promise} 回傳Promise,resolve回傳刪除結果物件{n,nDeleted,ok},n與nDeleted同為實際刪除筆數,reject回傳錯誤訊息
     */
    async function delAll(find = {}) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (client.status === 'closed') {
            return procClosed('delAll', null)
        }
        let isErr = false

        //res
        let res = null
        try {

            //ltdt
            let ltdt = await getData()

            //filter
            let nAll = size(ltdt)
            let nDel = 0
            if (iseobj(find)) {

                //q
                let q = new mingo.Query(find)
                // console.log('q', q)

                //find
                let _res = q.find(ltdt).all()
                // console.log('_res', _res)

                //nDel
                nDel = size(_res)
                // console.log('nDel', nDel)

                if (nDel === 0) {
                    //未有find結果等於不刪除
                }
                else if (nAll === nDel) {
                    //全在find結果內等於全部刪除

                    //empty
                    for (let v of ltdt) {
                        await client.del(v.id)
                    }

                }
                else {
                    //部份在find結果內

                    //_kp
                    let _kp = {}
                    each(_res, (v, k) => {
                        _kp[v.id] = { k, v }
                    })

                    //del
                    for (let v of ltdt) {
                        if (haskey(_kp, v.id)) {
                            //在find結果內代表須刪除
                            await client.del(v.id)
                        }
                    }

                }
            }
            else {

                //nDel
                nDel = nAll

                //empty
                for (let v of ltdt) {
                    await client.del(v.id)
                }

            }

            //res, n為實際刪除筆數而非全表筆數, 故與nDeleted同值
            res = {
                n: nDel,
                nDeleted: nDel,
                ok: 1,
            }

        }
        catch (err) {
            isErr = true
            res = err
        }

        //update, 不能保證刪除多少, 一律重設快取
        _cache = null

        //emit, 於change可能須使用select, 故須放在重設快取之後
        if (!isErr) {
            emitChange('delAll', null, res)
        }

        //check
        if (isErr) {

            //emit, 整批性錯誤須於reject之前發出
            emitError('delAll', null, res)

            return Promise.reject(res)
        }

        return res
    }

    //close, 關閉內部LMDB env並釋放檔案鎖, close後該實例即報廢, 須重新new WOrmLmdb,
    //再呼叫任何操作函數將立即reject(訊息明示closed), 不與[查無資料]同形
    let close = async () => {
        if (client && client.status !== 'closed') {
            await client.close() //lmdb root database之close(), 回傳Promise
        }
    }

    //save
    ee.select = select
    ee.selectByPk = selectByPk
    ee.insert = insert
    ee.insertBulk = insertBulk
    ee.save = save
    ee.del = del
    ee.delAll = delAll
    ee.close = close

    return ee
}


export default WOrmLmdb