server/lockSave.mjs

import get from 'lodash-es/get.js'
import isestr from 'wsemi/src/isestr.mjs'


/**
 * 有副作用之寫入路徑(如清單儲存 updateUsersList / updateTokensList / updateIpsList)之雙擊防護(後端, ADR-025)
 *
 * 以「操作:操作者 id」於 wsemi cacheSt 原子占位執行 fn, 同一操作者之同一操作處理中再送出即 reject 錯誤 key
 * (預設 'saveInProgress', 可以 opt.errKey 指定; 皆為 i18n key, 見 procLang); 不同操作者、不同操作互不影響;
 * fn 之 reject 原樣 bubble; fn 結束(含失敗)即釋放.
 * 第 2 次不排隊執行: 其內容與第 1 次相同, 排隊後再寫一次會把第 1 次剛建立之列當修改覆寫
 * (依序重送另由各專案之資料層檢查擋, 如 SSO procCore.updateTabItems 之新增列檢查回 'saveNewRowExists').
 * operatorId 可為操作者 id 或「操作者 id + 列識別」(只擋同一列之連發, 同批其他列不受影響).
 *
 * @param {Object} cst 輸入 wsemi cacheSt 實例
 * @returns {Function} 回傳 async (op, operatorId, fn, opt = {}) => fn 之結果, opt.errKey 為占位衝突時 reject 之 key
 */
function createLockSave(cst) {

    let lockSave = async (op, operatorId, fn, opt = {}) => {

        //errKey
        let errKey = get(opt, 'errKey', '')
        if (!isestr(errKey)) {
            errKey = 'saveInProgress'
        }

        let key = `${op}:${operatorId}`
        return await cst.setWithFree([key], fn)
            .catch((err) => {
                //remap setWithFree 之占位衝突 → i18n key; fn 內之 reject 原樣 bubble
                if (err === `setWithFree: key in use: ${key}`) {
                    return Promise.reject(errKey)
                }
                return Promise.reject(err)
            })
    }

    return lockSave
}


export default createLockSave