const UB = require('@unitybase/ub')
const App = UB.App
/* global ubs_numcounter ubs_numcounterreserv ubs_settings */
ubs_numcounter.entity.addMethod('getRegnumCounter')
let AUTO_REG_WITH_DELETED_NUMBER_SETTING
/**
* @returns {boolean}
*/
function getAutoRegWithDeletedNumberSetting () {
if (AUTO_REG_WITH_DELETED_NUMBER_SETTING === undefined) {
AUTO_REG_WITH_DELETED_NUMBER_SETTING = ubs_settings.loadKey('ubs.numcounter.autoRegWithDeletedNumber', true)
}
return AUTO_REG_WITH_DELETED_NUMBER_SETTING
}
const REGNUM_CS = App.registerCriticalSection('getRegnumLoosely')
const REGNUM_CACHE_SIZE = 100
/**
* Returns the counter number by mask. Locks the row in the `ubs_numcouner` table until the transaction is committed,
* so other transactions will wait, but guarantees that the returned number is incremented continuously, without gaps.
*
* In case gaps are allowed by business logic, better to use `ubs_numcounter.getRegnumLoosely` - it's lock
* the `ubs_numcouner` very rarely and much faster because of in-memory cache
*
* @function getRegnum
* @memberOf ubs_numcounter_ns.prototype
* @memberOfModule @unitybase/ubs
* @param {string} regKeyValue Registration key mask
* @param {number} [startNum] The starting counter value in case mask not exists
* @param {boolean} [skipReservedNumber=false] When "true" skip loading number from reserve and calculate new number by mask
* @returns {number} Next number for this mask
*/
ubs_numcounter.getRegnum = function (regKeyValue, startNum, skipReservedNumber) {
let counterInData = -1
if (startNum !== 0) startNum = startNum || 1
// Get autoRegWithDeletedNumber from settings if skipReservedNumber is not true
const autoRegWithDeletedNumber = !skipReservedNumber ? getAutoRegWithDeletedNumberSetting() : false
// Get counter from reserved if autoRegWithDeletedNumber set to true in settings
const reservedCounter = (autoRegWithDeletedNumber === true) ? ubs_numcounterreserv.getReservedRegnum(regKeyValue) : -1
if (reservedCounter !== -1) {
counterInData = reservedCounter
} else {
counterInData = getCurrentCounterAndIncrBy(regKeyValue, startNum, 1) // increment in DB
counterInData += 1 // increment result
}
return counterInData
}
/**
* Returns the counter number by mask with possible gaps in the sequence byt faster when `getRegnum`.
* In `-dev` mode fallback to `getRegnum`
*
* @function getRegnumLoosely
* @memberOf ubs_numcounter_ns.prototype
* @memberOfModule @unitybase/ubs
* @param {string} regKeyValue Registration key mask
* @param {number} [startNum=0] The starting counter value in case mask not exists
* @returns {number} Next number for this mask
*/
ubs_numcounter.getRegnumLoosely = function (regKeyValue, startNum = 0) {
let res
App.enterCriticalSection(REGNUM_CS)
try {
let cachedVal = App.memCacheGet('ubs_numcounter.getRegnumLoosely:' + regKeyValue)
if (!cachedVal || (+cachedVal % REGNUM_CACHE_SIZE === 0)) {
// incr DB counter value in separate transaction
App.runInAutonomousTransaction(function getNumCounterAutonomous () {
cachedVal = getCurrentCounterAndIncrBy(regKeyValue, startNum, REGNUM_CACHE_SIZE)
})
}
res = parseInt(cachedVal, 10) + 1
App.memCachePut('ubs_numcounter.getRegnumLoosely:' + regKeyValue, '' + res)
} finally {
App.leaveCriticalSection(REGNUM_CS)
}
return res
}
/**
* Increment counter in DB (or create new if not exists) and return an INITIAL value
*
* @private
* @param {string} regKeyValue
* @param {number} startNum
* @param {number} incr
* @returns {number}
*/
function getCurrentCounterAndIncrBy (regKeyValue, startNum, incr) {
let res
let counterInData = -1
// check number mask exist in ubs_numcounter
let store = UB.Repository('ubs_numcounter')
.attrs(['ID'])
.where('[regKey]', '=', regKeyValue)
.select()
// if mask not exists - add it
if (store.eof) {
counterInData = startNum > 0 ? startNum - 1 : startNum
res = store.run('insert', {
execParams: {
regKey: regKeyValue,
counter: counterInData + incr // insert incremented counter value
}
})
if (!res) throw store.lastError
} else {
// in case mask exist
const IDInData = store.get('ID')
// lock it for update
store.run('update', {
__skipSelectBeforeUpdate: true,
execParams: {
ID: IDInData,
fakeLock: 1
}
})
// retrieve current number
store = UB.Repository('ubs_numcounter')
.attrs(['ID', 'counter'])
.where('ID', '=', IDInData)
.select()
// increment counter
// If counter is not aligned to incr, for example: counter = 107 incr = 100, next returned number is 207
// client will stop on 300, and we do not get an intersection, but loose numbers 109-207 - this is ok, since method is loosely
counterInData = store.get('counter')
if ((incr !== 1) && (counterInData % incr !== 0)) {
// counter not aligned (maybe used in lossless mode before)
// align to incr, for example: counter = 107, incr = 100 -> new value is 200
incr = incr - (counterInData % incr)
}
// and update an incremented counter value
// We can safely use `__skipSelectBeforeUpdate: true` because entity is not audited
res = store.run('update', {
__skipSelectBeforeUpdate: true,
execParams: {
ID: IDInData,
counter: counterInData + incr,
fakeLock: 0
}
})
if (!res) throw store.lastError
}
return counterInData
}
/**
* Generate auto incremental code for specified entity attribute in case
* attribute value in execParams is empty or equal to attribute default value,
* specified in meta file.
*
* Will create a numcounter with code === entity.name and 1 as initial value.
*
* Result value will be left padded by '0' to the length specified in ubs_settings
* To be used in `insert:before` handler as
*
* @example
cdn_profession.on('insert:before', generateAutoIncrementalCode)
function generateAutoIncrementalCode (ctx) {
ubs_numcounter.generateAutoIncrementalCode(ctx, 'code')
}
//or even simple if attribute name is `code`
cdn_profession.on('insert:before', ubs_numcounter.generateAutoIncrementalCode)
* @function generateAutoIncrementalCode
* @memberOf ubs_numcounter_ns.prototype
* @memberOfModule @unitybase/ubs
* @param {ubMethodParams} ctx
* @param {string} ctx.mParams.entity
* @param {TubList|object} ctx.mParams.execParams
* @param {string} [forAttribute='code'] Code of attribute for number generation
*/
ubs_numcounter.generateAutoIncrementalCode = function (ctx, forAttribute = 'code') {
const mParams = ctx.mParams
const execParams = mParams.execParams
if (!execParams) return
const entityCode = mParams.entity
const ubEntity = App.domainInfo.get(entityCode)
const attr = ubEntity.attributes[forAttribute]
const newAttrValue = execParams[forAttribute]
if (execParams && (!newAttrValue || (attr.defaultValue && attr.defaultValue === execParams[forAttribute]))) {
const padTo = ubs_settings.loadKey('ubs.numcounter.autoIncrementalCodeLen', 6)
const newNum = '' + ubs_numcounter.getRegnumLoosely(entityCode, 1)
execParams[forAttribute] = newNum.padStart(padTo, '0')
}
}
/**
* Get counter value by registration key. If both `skipReservedNumber` and `loosely` is true - use loosely non-blocking algo
*
* @function getRegnumCounter
* @memberOf ubs_numcounter_ns.prototype
* @memberOfModule @unitybase/ubs
* @published
* @param {ubMethodParams} ctx
* @param {string} ctx.mParams.execParams.regkey
* @param {boolean} ctx.mParams.execParams.skipReservedNumber Skip loading number from reserve and calculate new number by mask
* @param {boolean} [ctx.mParams.execParams.loosely=false] If true and skipReservedNumber is true - use loosely non-blocking algo
* @returns {boolean}
*/
ubs_numcounter.getRegnumCounter = function (ctx) {
// RegKey caller pass to method
const execParams = ctx.mParams.execParams
const upregKey = execParams.regkey
const skipReservedNumber = execParams.skipReservedNumber || false
if (skipReservedNumber && execParams.loosely) {
ctx.mParams.getRegnumCounter = ubs_numcounter.getRegnumLoosely(upregKey, 1)
} else {
ctx.mParams.getRegnumCounter = ubs_numcounter.getRegnum(upregKey, 1, skipReservedNumber)
}
return true
}