var __defProp = Object.defineProperty; var __getOwnPropDesc = Object.getOwnPropertyDescriptor; var __getOwnPropNames = Object.getOwnPropertyNames; var __hasOwnProp = Object.prototype.hasOwnProperty; var __export = (target, all) => { for (var name in all) __defProp(target, name, { get: all[name], enumerable: true }); }; var __copyProps = (to, from, except, desc) => { if (from && typeof from === "object" || typeof from === "function") { for (let key of __getOwnPropNames(from)) if (!__hasOwnProp.call(to, key) && key !== except) __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable }); } return to; }; var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod); // src/index.ts var src_exports = {}; __export(src_exports, { IndexGenerator: () => IndexGenerator, base62CharSet: () => base62CharSet, generateJitteredKeyBetween: () => generateJitteredKeyBetween, generateKeyBetween: () => generateKeyBetween, generateNJitteredKeysBetween: () => generateNJitteredKeysBetween, generateNKeysBetween: () => generateNKeysBetween, indexCharacterSet: () => indexCharacterSet }); module.exports = __toCommonJS(src_exports); // src/charSet.ts function indexCharacterSet(options) { const dicts = createCharSetDicts(options.chars); const limits = integerLimits( dicts, options.firstPositive, options.mostPositive, options.mostNegative ); const jitterRange = options.jitterRange ?? Math.floor(Math.pow(dicts.length, 3) / 5); const paddingRange = paddingDict(jitterRange, dicts.length); return { chars: options.chars, byChar: dicts.byChar, byCode: dicts.byCode, length: dicts.length, first: dicts.byCode[0], last: dicts.byCode[dicts.length - 1], firstPositive: limits.firstPositive, mostPositive: limits.mostPositive, firstNegative: limits.firstNegative, mostNegative: limits.mostNegative, jitterRange, paddingDict: paddingRange }; } function createCharSetDicts(charSet) { const byCode = {}; const byChar = {}; const length = charSet.length; for (let i = 0; i < length; i++) { const char = charSet[i]; byCode[i] = char; byChar[char] = i; } return { byCode, byChar, length }; } function integerLimits(dicts, firstPositive, mostPositive, mostNegative) { const firstPositiveIndex = firstPositive ? dicts.byChar[firstPositive] : Math.ceil(dicts.length / 2); const mostPositiveIndex = mostPositive ? dicts.byChar[mostPositive] : dicts.length - 1; const mostNegativeIndex = mostNegative ? dicts.byChar[mostNegative] : 0; if (firstPositiveIndex === void 0 || mostPositiveIndex === void 0 || mostNegativeIndex === void 0) { throw new Error("invalid charSet"); } if (mostPositiveIndex - firstPositiveIndex < 3) { throw new Error( "mostPositive must be at least 3 characters away from neutral" ); } if (firstPositiveIndex - mostNegativeIndex < 3) { throw new Error( "mostNegative must be at least 3 characters away from neutral" ); } return { firstPositive: dicts.byCode[firstPositiveIndex], mostPositive: dicts.byCode[mostPositiveIndex], firstNegative: dicts.byCode[firstPositiveIndex - 1], mostNegative: dicts.byCode[mostNegativeIndex] }; } function paddingDict(jitterRange, charSetLength) { const paddingDict2 = {}; let distance = 0; for (let i = 0; i < 100; i++) { paddingDict2[i] = Math.pow(charSetLength, i); if (paddingDict2[i] > jitterRange) { break; } } return paddingDict2; } var _base62CharSet = null; function base62CharSet() { if (_base62CharSet) return _base62CharSet; return _base62CharSet = indexCharacterSet({ // Base62 are all the alphanumeric characters, database and user friendly // For shorter strings and more room you could opt for more characters chars: "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz", // This gives us nice human readable keys to start with a0 a1 etc firstPositive: "a", mostPositive: "z", mostNegative: "A" }); } // src/integerLength.ts function distanceBetween(a, b, charSet) { const indexA = charSet.byChar[a]; const indexB = charSet.byChar[b]; return Math.abs(indexA - indexB); } function integerLength(head, charSet) { const firstChar = head[0]; if (firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) { throw new Error("invalid firstChar on key"); } if (firstChar === charSet.mostPositive) { const firstLevel = distanceBetween(firstChar, charSet.firstPositive, charSet) + 1; return firstLevel + integerLengthFromSecondLevel(head.slice(1), "positive", charSet); } if (firstChar === charSet.mostNegative) { const firstLevel = distanceBetween(firstChar, charSet.firstNegative, charSet) + 1; return firstLevel + integerLengthFromSecondLevel(head.slice(1), "negative", charSet); } const isPositiveRange = firstChar >= charSet.firstPositive; if (isPositiveRange) { return distanceBetween(firstChar, charSet.firstPositive, charSet) + 2; } else { return distanceBetween(firstChar, charSet.firstNegative, charSet) + 2; } } function integerLengthFromSecondLevel(key, direction, charSet) { const firstChar = key[0]; if (firstChar > charSet.mostPositive || firstChar < charSet.mostNegative) { throw new Error("invalid firstChar on key"); } if (firstChar === charSet.mostPositive && direction === "positive") { const totalPositiveRoom = distanceBetween(firstChar, charSet.mostNegative, charSet) + 1; return totalPositiveRoom + integerLengthFromSecondLevel(key.slice(1), direction, charSet); } if (firstChar === charSet.mostNegative && direction === "negative") { const totalNegativeRoom = distanceBetween(firstChar, charSet.mostPositive, charSet) + 1; return totalNegativeRoom + integerLengthFromSecondLevel(key.slice(1), direction, charSet); } if (direction === "positive") { return distanceBetween(firstChar, charSet.mostNegative, charSet) + 2; } else { return distanceBetween(firstChar, charSet.mostPositive, charSet) + 2; } } // src/padToSameLength.ts function makeSameLength(a, b, pad, fillChar, forceLength) { const max = forceLength ?? Math.max(a.length, b.length); if (pad === "start") { return [a.padStart(max, fillChar), b.padStart(max, fillChar)]; } return [a.padEnd(max, fillChar), b.padEnd(max, fillChar)]; } // src/keyAsNumber.ts function midPoint(lower, upper, charSet) { let [paddedLower, paddedUpper] = makeSameLength( lower, upper, "end", charSet.first ); let distance = lexicalDistance(paddedLower, paddedUpper, charSet); if (distance === 1) { paddedLower = paddedLower.padEnd(paddedLower.length + 1, charSet.first); distance = charSet.length; } const mid = encodeToCharSet(Math.floor(distance / 2), charSet); return addCharSetKeys(paddedLower, mid, charSet); } function lexicalDistance(a, b, charSet) { const [lower, upper] = makeSameLength(a, b, "end", charSet.first).sort(); const distance = subtractCharSetKeys(upper, lower, charSet); return decodeCharSetToNumber(distance, charSet); } function addCharSetKeys(a, b, charSet) { const base = charSet.length; const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first); const result = []; let carry = 0; for (let i = paddedA.length - 1; i >= 0; i--) { const digitA = charSet.byChar[paddedA[i]]; const digitB = charSet.byChar[paddedB[i]]; const sum = digitA + digitB + carry; carry = Math.floor(sum / base); const remainder = sum % base; result.unshift(charSet.byCode[remainder]); } if (carry > 0) { result.unshift(charSet.byCode[carry]); } return result.join(""); } function subtractCharSetKeys(a, b, charSet, stripLeadingZeros = true) { const base = charSet.length; const [paddedA, paddedB] = makeSameLength(a, b, "start", charSet.first); const result = []; let borrow = 0; for (let i = paddedA.length - 1; i >= 0; i--) { let digitA = charSet.byChar[paddedA[i]]; const digitB = charSet.byChar[paddedB[i]] + borrow; if (digitA < digitB) { borrow = 1; digitA += base; } else { borrow = 0; } const difference = digitA - digitB; result.unshift(charSet.byCode[difference]); } if (borrow > 0) { throw new Error( "Subtraction result is negative. Ensure a is greater than or equal to b." ); } while (stripLeadingZeros && result.length > 1 && result[0] === charSet.first) { result.shift(); } return result.join(""); } function incrementKey(key, charSet) { return addCharSetKeys(key, charSet.byCode[1], charSet); } function decrementKey(key, charSet) { return subtractCharSetKeys(key, charSet.byCode[1], charSet, false); } function encodeToCharSet(int, charSet) { if (int === 0) { return charSet.byCode[0]; } let res = ""; const max = charSet.length; while (int > 0) { res = charSet.byCode[int % max] + res; int = Math.floor(int / max); } return res; } function decodeCharSetToNumber(key, charSet) { let res = 0; const length = key.length; const max = charSet.length; for (let i = 0; i < length; i++) { res += charSet.byChar[key[i]] * Math.pow(max, length - i - 1); } return res; } // src/integer.ts function startKey(charSet) { return charSet.firstPositive + charSet.byCode[0]; } function validInteger(integer, charSet) { const length = integerLength(integer, charSet); return length === integer.length; } function validateOrderKey(orderKey, charSet) { getIntegerPart(orderKey, charSet); } function getIntegerPart(orderKey, charSet) { const head = integerHead(orderKey, charSet); const integerPartLength = integerLength(head, charSet); if (integerPartLength > orderKey.length) { throw new Error("invalid order key length: " + orderKey); } return orderKey.slice(0, integerPartLength); } function validateInteger(integer, charSet) { if (!validInteger(integer, charSet)) { throw new Error("invalid integer length: " + integer); } } function incrementInteger(integer, charSet) { validateInteger(integer, charSet); const [head, digs] = splitInteger(integer, charSet); const anyNonMaxedDigit = digs.split("").some((d) => d !== charSet.byCode[charSet.length - 1]); if (anyNonMaxedDigit) { const newDigits = incrementKey(digs, charSet); return head + newDigits; } const nextHead = incrementIntegerHead(head, charSet); return startOnNewHead(nextHead, "lower", charSet); } function decrementInteger(integer, charSet) { validateInteger(integer, charSet); const [head, digs] = splitInteger(integer, charSet); const anyNonLimitDigit = digs.split("").some((d) => d !== charSet.byCode[0]); if (anyNonLimitDigit) { const newDigits = decrementKey(digs, charSet); return head + newDigits; } const nextHead = decrementIntegerHead(head, charSet); return startOnNewHead(nextHead, "upper", charSet); } function integerHead(integer, charSet) { let i = 0; if (integer[0] === charSet.mostPositive) { while (integer[i] === charSet.mostPositive) { i = i + 1; } } if (integer[0] === charSet.mostNegative) { while (integer[i] === charSet.mostNegative) { i = i + 1; } } return integer.slice(0, i + 1); } function splitInteger(integer, charSet) { const head = integerHead(integer, charSet); const tail = integer.slice(head.length); return [head, tail]; } function incrementIntegerHead(head, charSet) { const inPositiveRange = head >= charSet.firstPositive; const nextHead = incrementKey(head, charSet); const headIsLimitMax = head[head.length - 1] === charSet.mostPositive; const nextHeadIsLimitMax = nextHead[nextHead.length - 1] === charSet.mostPositive; if (inPositiveRange && nextHeadIsLimitMax) { return nextHead + charSet.mostNegative; } if (!inPositiveRange && headIsLimitMax) { return head.slice(0, head.length - 1); } return nextHead; } function decrementIntegerHead(head, charSet) { const inPositiveRange = head >= charSet.firstPositive; const headIsLimitMin = head[head.length - 1] === charSet.mostNegative; if (inPositiveRange && headIsLimitMin) { const nextLevel = head.slice(0, head.length - 1); return decrementKey(nextLevel, charSet); } if (!inPositiveRange && headIsLimitMin) { return head + charSet.mostPositive; } return decrementKey(head, charSet); } function startOnNewHead(head, limit, charSet) { const newLength = integerLength(head, charSet); const fillChar = limit === "upper" ? charSet.byCode[charSet.length - 1] : charSet.byCode[0]; return head + fillChar.repeat(newLength - head.length); } // src/jittering.ts function jitterString(orderKey, charSet) { const shift = encodeToCharSet( Math.floor(Math.random() * charSet.jitterRange), charSet ); return addCharSetKeys(orderKey, shift, charSet); } function padAndJitterString(orderKey, numberOfChars, charSet) { const paddedKey = orderKey.padEnd( orderKey.length + numberOfChars, charSet.first ); return jitterString(paddedKey, charSet); } function paddingNeededForJitter(orderKey, b, charSet) { const integer = getIntegerPart(orderKey, charSet); const nextInteger = incrementInteger(integer, charSet); let needed = 0; if (b !== null) { const distanceToB = lexicalDistance(orderKey, b, charSet); if (distanceToB < charSet.jitterRange + 1) { needed = Math.max(needed, paddingNeededForDistance(distanceToB, charSet)); } } const distanceToNextInteger = lexicalDistance(orderKey, nextInteger, charSet); if (distanceToNextInteger < charSet.jitterRange + 1) { needed = Math.max( needed, paddingNeededForDistance(distanceToNextInteger, charSet) ); } return needed; } function paddingNeededForDistance(distance, charSet) { const gap = charSet.jitterRange - distance; const firstBigger = Object.entries(charSet.paddingDict).find( ([_key, value]) => { return value > gap; } ); return firstBigger ? parseInt(firstBigger[0]) : 0; } // src/generateKeyBetween.ts function generateKeyBetween(lower, upper, charSet = base62CharSet()) { if (lower !== null) { validateOrderKey(lower, charSet); } if (upper !== null) { validateOrderKey(upper, charSet); } if (lower === null && upper === null) { return startKey(charSet); } if (lower === null) { const integer = getIntegerPart(upper, charSet); return decrementInteger(integer, charSet); } if (upper === null) { const integer = getIntegerPart(lower, charSet); return incrementInteger(integer, charSet); } if (lower >= upper) { throw new Error(lower + " >= " + upper); } return midPoint(lower, upper, charSet); } function generateNKeysBetween(a, b, n, charSet = base62CharSet()) { return spreadGeneratorResults( a, b, n, charSet, generateKeyBetween, generateNKeysBetween ); } function generateJitteredKeyBetween(lower, upper, charSet = base62CharSet()) { const key = generateKeyBetween(lower, upper, charSet); const paddingNeeded = paddingNeededForJitter(key, upper, charSet); if (paddingNeeded) { return padAndJitterString(key, paddingNeeded, charSet); } return jitterString(key, charSet); } function generateNJitteredKeysBetween(lower, upper, n, charSet = base62CharSet()) { return spreadGeneratorResults( lower, upper, n, charSet, generateJitteredKeyBetween, generateNJitteredKeysBetween ); } function spreadGeneratorResults(lower, upper, n, charSet, generateKey, generateNKeys) { if (n === 0) { return []; } if (n === 1) { return [generateKey(lower, upper, charSet)]; } if (upper == null) { let newUpper = generateKey(lower, upper, charSet); const result = [newUpper]; for (let i = 0; i < n - 1; i++) { newUpper = generateKey(newUpper, upper, charSet); result.push(newUpper); } return result; } if (lower == null) { let newLower = generateKey(lower, upper, charSet); const result = [newLower]; for (let i = 0; i < n - 1; i++) { newLower = generateKey(lower, newLower, charSet); result.push(newLower); } result.reverse(); return result; } const mid = Math.floor(n / 2); const midOrderKey = generateKey(lower, upper, charSet); return [ ...generateNKeys(lower, midOrderKey, mid, charSet), midOrderKey, ...generateNKeys(midOrderKey, upper, n - mid - 1, charSet) ]; } // src/IndexGenerator.ts var IndexGenerator = class { charSet; useJitter; list; useGroups; groupIdLength; constructor(list, options = {}) { this.charSet = options.charSet ?? base62CharSet(); this.useJitter = options.useJitter ?? true; this.list = list; this.useGroups = !!options.groupIdLength && options.groupIdLength > 0; this.groupIdLength = options.groupIdLength ?? 0; } /** * Updates the list that the generator uses to generate keys. * The generator will not mutate the internal list when generating keys. */ updateList(list) { this.list = [...list].sort(); } /** * Generate any number of keys at the start of the list (before the first key). * Optionally you can supply a groupId to generate keys at the start of a specific group. */ nKeysStart(n, groupId) { this.validateGroupId(groupId); return this.generateNKeysBetween( null, this.firstOfGroup(groupId), n, groupId ); } /** * Generate a single key at the start of the list (before the first key). * Optionally you can supply a groupId to generate a key at the start of a specific group. */ keyStart(groupId) { this.validateGroupId(groupId); return this.nKeysStart(1, groupId)[0]; } /** * Generate any number of keys at the end of the list (after the last key). * Optionally you can supply a groupId to generate keys at the end of a specific group. */ nKeysEnd(n, groupId) { this.validateGroupId(groupId); return this.generateNKeysBetween( this.lastOfGroup(groupId), null, n, groupId ); } /** * Generate a single key at the end of the list (after the last key). * Optionally you can supply a groupId to generate a key at the end of a specific group. */ keyEnd(groupId) { this.validateGroupId(groupId); return this.nKeysEnd(1, groupId)[0]; } /** * Generate any number of keys behind a specific key and in front of the next key. * GroupId will be inferred from the orderKey if working with groups */ nKeysAfter(orderKey, n) { const keyAfter = this.getKeyAfter(orderKey); return this.generateNKeysBetween( orderKey, keyAfter, n, this.groupId(orderKey) ); } /** * Generate a single key behind a specific key and in front of the next key. * GroupId will be inferred from the orderKey if working with groups */ keyAfter(orderKey) { return this.nKeysAfter(orderKey, 1)[0]; } /** * Generate any number of keys in front of a specific key and behind the previous key. * GroupId will be inferred from the orderKey if working with groups */ nKeysBefore(orderKey, n) { const keyBefore = this.getKeyBefore(orderKey); return this.generateNKeysBetween( keyBefore, orderKey, n, this.groupId(orderKey) ); } /** * Generate a single key in front of a specific key and behind the previous key. * GroupId will be inferred from the orderKey if working with groups */ keyBefore(orderKey) { return this.nKeysBefore(orderKey, 1)[0]; } /** * private function responsible for calling the correct generate function */ generateNKeysBetween(lowerKey, upperKey, n, groupId) { const lower = this.groupLessKey(lowerKey); const upper = this.groupLessKey(upperKey); const keys = this.useJitter ? generateNJitteredKeysBetween(lower, upper, n, this.charSet) : generateNKeysBetween(lower, upper, n, this.charSet); return !groupId ? keys : keys.map((key) => groupId + key); } /** * get the key before the supplied orderKey, if it exists and is in the same group */ getKeyBefore(orderKey) { const index = this.list.indexOf(orderKey); if (index === -1) { throw new Error(`orderKey is not in the list`); } const before = this.list[index - 1]; return !!before && this.isSameGroup(orderKey, before) ? before : null; } /** * get the key after the supplied orderKey, if it exists and is in the same group */ getKeyAfter(orderKey) { const index = this.list.indexOf(orderKey); if (index === -1) { throw new Error(`orderKey is not in the list`); } const after = this.list[index + 1]; return !!after && this.isSameGroup(orderKey, after) ? after : null; } /** * get the first key of the group (or the first key of the list if not using groups) */ firstOfGroup(groupId) { if (!this.useGroups) return this.list[0] ?? null; const first = this.list.find((key) => this.isPartOfGroup(key, groupId)); return first ?? null; } /** * get the last key of the group (or the last key of the list if not using groups) */ lastOfGroup(groupId) { if (!this.useGroups) return this.list[this.list.length - 1] ?? null; const allGroupItems = this.list.filter( (key) => this.isPartOfGroup(key, groupId) ); const last = allGroupItems[allGroupItems.length - 1]; return last ?? null; } /** * throw an error if the groupId is invalid or supplied when not using groups */ validateGroupId(groupId) { if (!this.useGroups) { if (groupId) { console.warn("groupId should not used when not using groups"); } return; } if (!groupId) { throw new Error("groupId is required when using groups"); } if (groupId.length !== this.groupIdLength) { throw new Error(`groupId must be the lenght supplied in the options`); } } /** * get the groupId from the orderKey */ groupId(orderKey) { if (!this.useGroups) return void 0; return this.splitIntoGroupIdAndOrderKey(orderKey)[0]; } /** * remove the groupId from the orderKey */ groupLessKey(orderKey) { if (!this.useGroups) return orderKey; return this.splitIntoGroupIdAndOrderKey(orderKey)[1]; } /** * split the orderKey into groupId and key * if not using groups, orderKey will be the same as key */ splitIntoGroupIdAndOrderKey(orderKey) { if (!this.useGroups || !orderKey) { return [void 0, orderKey]; } const groupId = orderKey.substring(0, this.groupIdLength); const key = orderKey.substring(this.groupIdLength); return [groupId, key]; } /** * check if two keys are in the same group * if not using groups, keys will always be in the same group */ isSameGroup(a, b) { if (!this.useGroups) return true; const [aGroupId] = this.splitIntoGroupIdAndOrderKey(a); const [bGroupId] = this.splitIntoGroupIdAndOrderKey(b); return aGroupId === bGroupId; } /** * check if the key is part of the group * if not using groups, key will always be part of the group */ isPartOfGroup(orderKey, groupId) { if (!this.useGroups) return true; const [keyGroupId] = this.splitIntoGroupIdAndOrderKey(orderKey); return keyGroupId === groupId; } }; // Annotate the CommonJS export names for ESM import in node: 0 && (module.exports = { IndexGenerator, base62CharSet, generateJitteredKeyBetween, generateKeyBetween, generateNJitteredKeysBetween, generateNKeysBetween, indexCharacterSet });