WOrmLevel.mjs

import { Level } from 'level'
import { Query } from 'mingo'
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'


/**
 * 操作資料庫(Level)
 *
 * 回傳物件為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,故收到本事件不表示該次呼叫失敗;正常結果(如查無數據、主鍵未命中、全數已存在)不發出本事件。
 *
 * 註: 本套件之主鍵欄位固定為id,尚未支援指定其他欄位為主鍵。
 * 註: level無條件寫入、無比較並交換亦無交易,故insert之[檢查主鍵不存在與寫入]與save之[查找主鍵與更新或插入],
 * 其原子性由本套件之序列化佇列提供;而LevelDB對資料庫目錄為OS層之獨佔鎖,
 * 同一目錄無從由第二個實例或第二個行程開啟,故行程內序列化即等同全域互斥,詳見README之併發保證宣告。
 * 註: 因獨佔鎖之故,用畢須以close()釋放,否則同目錄無法再被開啟。
 *
 * @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、insertBulk與save於數據未帶有效主鍵(本套件為id欄位)時是否自動產生主鍵值,預設true;給false代表主鍵改由呼叫端自備,套件不產生亦不檢查其唯一性與格式,未帶有效主鍵者將reject
 * @returns {Object} 回傳操作資料庫物件,各事件功能詳見說明
 */
function WOrmLevel(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, 主鍵由誰產生為整個資料表之政策, 故為建構層設定, 不可於各寫入函數之option逐次覆寫,
    //否則同一資料表將混入兩種來源之主鍵而難以追溯
    let autoGenPk = get(opt, 'autoGenPk')
    if (!isbol(autoGenPk)) {
        autoGenPk = true
    }

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

    //client
    let client = new Level(storage, { valueEncoding: 'json' })

    //ee
    let ee = evem() //採eventemitter3, 其於'error'無監聽者時僅回傳false而不拋出, 故操作行為不因監聽者之有無而改變

    //gqueue, 本實例之序列化佇列, 詳見下方serialize說明
    let gqueue = Promise.resolve()

    /**
     * 序列化本實例之操作
     *
     * level無條件寫入、無比較並交換亦無交易,故insert之[檢查主鍵不存在與寫入]與save之[查找主鍵與更新或插入],
     * 其原子性只能由序列化達成。採Promise鏈而非[布林旗標配合輪詢等待],係因後者之[檢查旗標]與[設定旗標]之間隔有await,
     * 並非原子之test-and-set,多個等候者之輪詢若落在同一批timer觸發即會同時進入臨界區;
     * 且輪詢函數逾時後為resolve而非reject,等滿即照樣放行。Promise鏈為嚴格FIFO且無逾時放行,由結構保證互斥。
     * select亦納入序列化,令其不致與寫入之批次替換交錯。
     *
     * 註: 佇列掛於實例層即足夠。LevelDB對資料庫目錄為OS層之獨佔鎖,同一目錄無從由第二個實例或第二個行程開啟
     * (實測後開者一律以LEVEL_DATABASE_NOT_OPEN失敗),故本佇列所涵蓋者即為該目錄之全部操作。
     */
    let serialize = (fn) => {

        //pmPrev, 恆為已攔截錯誤之Promise, 故前一次操作失敗不會中斷佇列
        let pmPrev = gqueue

        //pm
        let pm = pmPrev.then(() => {
            return fn()
        })

        //update, 存入已攔截錯誤者, 避免本次reject成為unhandled
        gqueue = pm.catch(() => {})

        return pm
    }

    //waitOpen, 等待client開啟, 因get、getMany、iterator、batch於client未開啟時會拋錯
    //closed為終態: 已關閉或開啟失敗(如目錄被其他行程鎖住)之client不會自行回到open, 續等必然逾時,
    //故等待條件納入終態並於等待後複檢, 令開啟失敗者於數百毫秒內失敗而非空轉至逾時
    //各函數入口已有procClosed守門, 此處為縱深防禦, 攔[操作進行中被close]之競態
    let waitOpen = async () => {
        await waitFun(() => {
            return client.status === 'open' || client.status === 'closed'
        }, { attemptNum: 50, timeInterval: 200 })
        if (client.status !== 'open') {
            throw new Error(`client is closed, instance can not be used after close() or failed to open`)
        }
    }

    //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))
    }

    //isClosed, 判定client是否已為終態
    let isClosed = () => {
        return client.status === 'closed' || client.status === 'closing'
    }

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

    //emitError, 操作發生錯誤時發出, 錯誤訊息一律轉為字串
    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、insertBulk與save共用
    //autoGenPk為true時補值; 為false時不補值, 未帶有效主鍵者屬呼叫端未履行契約, 拋出為整批性錯誤,
    //且檢查於任何寫入之前完成, 故不會有部份筆數已寫入而整批失敗之情形
    let procPk = (data, fnName) => {
        if (autoGenPk) {
            return map(data, function(v) {
                if (!isestr(get(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
    }

    //getKpValues, 一次取回各主鍵之現值, 回傳[主鍵值 => 現值]之對照表, 未命中者不收錄
    //供insert、insertBulk、save、del共用, 令[既有數據之認定]於各函數一致(皆以id為鍵直讀)
    let getKpValues = async(ids) => {
        let kp = {}
        if (size(ids) === 0) {
            return kp
        }
        let vs = await client.getMany(ids)
        each(ids, (id, k) => {
            let v = vs[k]
            if (v !== undefined) {
                kp[id] = v
            }
        })
        return kp
    }

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

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

        //waitOpen
        await waitOpen()

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

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

    /**
     * 查詢數據
     *
     * @memberOf WOrmLevel
     * @param {Object} [find={}] 輸入查詢條件物件
     * @returns {Promise} 回傳Promise,resolve回傳數據陣列,無符合數據回傳空陣列,reject回傳錯誤訊息
     */
    async function selectCore(find = {}) {
        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 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 = new Error(`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
    }
    async function select(find = {}) {

        //check client, closed為終態, 於任何檢查與讀取之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        if (isClosed()) {
            return procClosed('select', null)
        }

        return serialize(() => selectCore(find))
    }

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

        //res
        let res = null
        try {

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

            //waitOpen
            await waitOpen()

            //查找資料表內pk, 判定基準與insert、insertBulk、save、del內對既有數據之認定一致(皆以id為鍵直讀)
            let v = await client.get(pk)

            //check
            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
    }
    async function selectByPk(pk) {

        //check client, 守門須置於[主鍵值無效回null]之判定之前, 否則[已關閉]與[查無資料]同形
        if (isClosed()) {
            return procClosed('selectByPk', null)
        }

        return serialize(() => selectByPkCore(pk))
    }

    /**
     * 插入數據,僅於主鍵不存在時寫入,已存在者跳過且不覆寫
     *
     * @memberOf WOrmLevel
     * @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 insertCore(data, option = {}) {
        let isErr = false

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

        //check
        if (!iseobj(data) && !isearr(data)) {
            if (returnList) {
                //輸入無效, 對齊save與del之空陣列規定
                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')

            //waitOpen
            await waitOpen()

            //kp, 一次取回各主鍵之現值
            let kp = await getKpValues(map(data, 'id'))

            //each
            let nAll = size(data)
            let ltdone = []
            let ops = []
            each(data, (v) => {

                //check
                if (!haskey(kp, v.id)) {
                    //未存在v.id

                    //push
                    ops.push({ type: 'put', key: v.id, value: v })

                    //update, 須同步更新kp, 否則同批含重複主鍵者會全數寫入而僅存最末筆
                    kp[v.id] = v

                    ltdone.push(true)
                }
                else {
                    //已存在v.id(含同批重複之非首筆)則不寫入
                    ltdone.push(false)
                }

            })

            //write, 未有須插入者即不寫入, 全數已存在屬正常結果
            //batch為LevelDB之WriteBatch, 各筆之[檢查主鍵不存在]已於本臨界區內完成, 與寫入不會被他者交錯
            if (size(ops) > 0) {
                await client.batch(ops)
            }

            //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: size(ops),
                    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
    }
    async function insert(data, option = {}) {

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

        return serialize(() => insertCore(data, option))
    }

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

            //waitOpen
            await waitOpen()

            //kp, 一次取回各主鍵之現值
            let kp = await getKpValues(map(data, 'id'))

            //nAll
            let nAll = size(data)

            //check, 主鍵檢查於任何寫入之前一次完成, 任一筆衝突即整批中止,
            //同批含重複主鍵者亦於此被偵測為衝突(kp隨檢查同步更新)
            let pkConflict = null
            each(data, (v) => {
                if (haskey(kp, v.id)) {
                    pkConflict = v.id
                    return false //中止each
                }
                kp[v.id] = v
            })

            //check
            if (pkConflict !== null) {
                throw new Error(`can not insertBulk by existed id[${pkConflict}]`)
            }

            //write, 全部通過檢查方寫入, 且寫入為單次batch(LevelDB之WriteBatch為原子, 不會被拆為多次送出),
            //故全有全無成立: 檢查失敗時尚未寫入任何一筆, 無須交易亦無須補償動作
            await client.batch(map(data, function(v) {
                return { type: 'put', key: v.id, value: v }
            }))

            //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
    }
    async function insertBulk(data) {

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

        return serialize(() => insertBulkCore(data))
    }

    /**
     * 儲存數據,以主鍵為準更新既有數據,未給之欄位保留;主鍵不存在且autoInsert為true時改為插入
     *
     * @memberOf WOrmLevel
     * @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 saveCore(data, option = {}) {
        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')

            //waitOpen
            await waitOpen()

            //kp, 一次取回各主鍵之現值, 隨逐筆處理同步更新, 令同批同主鍵之後筆得見前筆之結果
            let kp = await getKpValues(map(data, 'id'))

            //pmSeries
            let ops = []
            res = await pmSeries(data, async(v) => {

                //rest
                let rest = null

                try {

                    //查找資料表內v.id
                    let existed = haskey(kp, v.id)

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

                        //vv, 現值
                        let vv = get(kp, v.id)

                        //vm, 合併後之結果, 另建新物件以免污染原數據
                        let vm = merge({}, vv, v)

                        //判定基準為[合併後結果與現值相同], 而非[待寫入物件與現值全等],
                        //令nModified忠實反映是否真的寫入, 僅給部份欄位且值皆相同者不應回報已修改
                        if (isEqual(vm, vv)) {
                            //合併後與現值相同不須寫入, 惟已命中v.id故n為1
                            rest = {
                                n: 1,
                                nInserted: 0,
                                nModified: 0,
                                ok: 1,
                            }
                        }
                        else {
                            //合併後與現值不同須更新

                            //update
                            ops.push({ type: 'put', key: v.id, value: vm })
                            kp[v.id] = vm

                            rest = {
                                n: 1,
                                nInserted: 0,
                                nModified: 1,
                                ok: 1,
                            }
                        }

                    }
                    else {
                        //不存在v.id, 若autoInsert則須插入
                        if (autoInsert) {

                            //push
                            ops.push({ type: 'put', key: v.id, value: v })
                            kp[v.id] = v

                            rest = {
                                n: 1,
                                nInserted: 1,
                                nModified: 0,
                                ok: 1,
                            }

                        }
                        else {
                            //未命中且未開啟autoInsert, 未命中故n為0
                            rest = {
                                n: 0,
                                nInserted: 0,
                                nModified: 0,
                                ok: 1,
                            }
                        }
                    }

                }
                catch (err) {

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

                }

                return rest
            })

            //write, 逐筆判定與寫入同於本臨界區內完成, 不會與他者交錯
            if (size(ops) > 0) {
                await client.batch(ops)
            }

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

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

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

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

            //emit, 逐筆插入另發出mode為insert之事件, 供呼叫端區辨新增與更新
            //逐筆結果與輸入等長保序, 故由nInserted即可定位係哪幾筆走插入, 不須另行記錄
            each(res, (rest, k) => {
                if (get(rest, 'nInserted') === 1) {
                    emitChange('insert', [get(data, k)], rest)
                }
            })

            //emit, 逐筆失敗之error事件已於上方發出, 故必早於本整批change
            emitChange('save', data, res)

        }

        //check
        if (isErr) {

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

            return Promise.reject(res)
        }

        return res
    }
    async function save(data, option = {}) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        //不可降為逐筆ok:0之resolve, 否則呼叫端只看Promise是否reject即誤判整批成功
        if (isClosed()) {
            return procClosed('save', data)
        }

        return serialize(() => saveCore(data, option))
    }

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

            //waitOpen
            await waitOpen()

            //ids, 僅取具有效主鍵值者, 不可將無效主鍵送進查詢(del不受autoGenPk影響, 於任一設定下皆不補值)
            let ids = []
            each(data, (v) => {
                let id = get(v, 'id', '')
                if (isestr(id)) {
                    ids.push(id)
                }
            })

            //kp, 一次取回各主鍵之現值
            let kp = await getKpValues(ids)

            //pmSeries
            let ops = []
            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分辨
                        //註: 亦不可將無效主鍵送進查詢條件, 故於此直接定案
                        rest = {
                            n: 0,
                            nDeleted: 0,
                            ok: 0,
                            err: `can not delete by invalid id[${id}]`,
                        }
                    }
                    else {

                        //check
                        if (haskey(kp, id)) {
                            //已存在v.id則須刪除

                            //push
                            ops.push({ type: 'del', key: id })

                            //update, 須同步移除, 否則同批重複主鍵者會重複計入nDeleted
                            delete kp[id]

                            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),
                    }

                }

                return rest
            })

            //write
            if (size(ops) > 0) {
                await client.batch(ops)
            }

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

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

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

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

            //emit, 逐筆失敗之error事件已於上方發出, 故必早於本整批change
            emitChange('del', data, res)

        }

        //check
        if (isErr) {

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

            return Promise.reject(res)
        }

        return res
    }
    async function del(data) {

        //check client, closed為終態, 於任何檢查與寫入之前先行攔下(T4: 實例已關閉屬整批性錯誤)
        //不可回[{n:0,nDeleted:0,ok:1}]之正常未命中結果, 否則[已關閉]與[查無資料]同形而fail-open
        if (isClosed()) {
            return procClosed('del', data)
        }

        return serialize(() => delCore(data))
    }

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

        //res
        let res = null
        try {

            //waitOpen
            await waitOpen()

            //filter, 一律由資料庫直讀而不使用快取, 避免依過期數據決定刪除對象
            let nDel = 0
            if (iseobj(find)) {

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

                //ops, 以與select同一條件逐筆判定
                let ops = []
                for await (let [key, dt] of client.iterator()) {
                    if (q.test(dt)) {
                        ops.push({ type: 'del', key })
                    }
                }

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

                //del, 無命中即不寫入, 屬正常結果
                if (nDel > 0) {
                    await client.batch(ops)
                }

            }
            else {
                //find未給或為空物件時刪除全部數據

                //nDel, 先計數方能回報實際刪除筆數
                let ks = await client.keys().all()
                nDel = size(ks)

                //clear
                if (nDel > 0) {
                    await client.clear()
                }

            }

            //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
    }
    async function delAll(find = {}) {

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

        return serialize(() => delAllCore(find))
    }

    /**
     * 關閉資料庫,因LevelDB對資料庫目錄為OS層之獨佔鎖,用畢須關閉方能令該目錄再被開啟
     * 註: 關閉為終態,關閉後之各操作一律以整批性錯誤reject,不可再以本實例操作
     *
     * @memberOf WOrmLevel
     * @returns {Promise} 回傳Promise,resolve回傳undefined,reject回傳錯誤訊息
     */
    async function close() {

        //check, 已關閉則不重複關閉
        if (client.status === 'closed') {
            return
        }

        //close, 納入佇列, 令已排隊之操作皆完成後方關閉
        return serialize(async() => {
            await client.close()
            _cache = null
        })
    }

    //bind
    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 WOrmLevel