btuid
Version:
Btuid library provides an interface to generate UIDs (Unique Identifiers) that are safe, cryptographically secure, and unpredictable, ensuring that every UUID is unique. The library also supports B+tree indexing, which makes it ideal for use in database s
328 lines (291 loc) • 9.59 kB
text/typescript
// Copyright 2025 Omar Alhaj Ali
// Licensed under the Apache License, Version 2.0 (the "License");
// you may not use this file except in compliance with the License.
// You may obtain a copy of the License at
//
// http://www.apache.org/licenses/LICENSE-2.0
//
// Unless required by applicable law or agreed to in writing, software
// distributed under the License is distributed on an "AS IS" BASIS,
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
// See the License for the specific language governing permissions and
// limitations under the License.
import { randomBytes } from "crypto";
import fs from "fs";
export interface DegreeConfig {
pageSize: number;
keySize: number;
TIDSize: number;
indexTupleDataSize: number;
linePointerSize: number;
addingPaddingSize: number;
degree: number;
}
export interface RestConfigData {
depth: number;
degree: number;
chunkLength: bigint;
indexInCurrentDepth: bigint;
startValue: bigint;
usedIdCount: bigint;
}
export type ConstructorParams = {
degreeConfig?: DegreeConfig;
startValue?: bigint;
displacementRate?: number;
restConfigData?: RestConfigData | null;
DegreeComplications? :number ,
path?: string;
saveTime?: number;
hexLength? :number;
randomLength? :number;
securityKey :string
};
export class BtuidGenerator {
private HEX_LENGTH = 16;
private EXTRALENGTH = 16;
private saveTime: number;
private degreeConfig: DegreeConfig;
private overhead: number;
private entryOverhead: number;
private degree: number = 0;
private usedIdCount: bigint = 0n;
private depth: number = 1; // for split first array
private startValue: bigint;
private length: bigint = 16n ** BigInt(this.HEX_LENGTH);
private chunkLength: bigint;
private indexInCurrentDepth: bigint = 0n;
private chunkCount: bigint = 0n;
private shiftAmount: number[] =[]
private restConfigData: RestConfigData = {
depth: 0,
degree: 0,
chunkLength: 0n,
indexInCurrentDepth: 0n,
startValue: 0n,
usedIdCount: 0n,
};
data = {
depth: 0,
degree: 0,
chunkLength: BigInt(0),
indexInCurrentDepth: BigInt(0),
startValue: BigInt(0),
usedIdCount: BigInt(0),
};
constructor({
degreeConfig = {
pageSize: 8192,
keySize: 16,
TIDSize: 6,
indexTupleDataSize: 4,
linePointerSize: 4,
addingPaddingSize: 2,
degree: 0,
},
startValue = 0n,
displacementRate = 6,
restConfigData = null,
DegreeComplications = 1,
path = "",
saveTime = 86400,
hexLength=16,
randomLength=16,
securityKey = "my key"
}: ConstructorParams) {
this.HEX_LENGTH =hexLength
this.length = 16n ** BigInt(this.HEX_LENGTH);
this.EXTRALENGTH =randomLength
this.saveTime = saveTime;
this.shiftAmount = this.stringToArray16("hello world")
console.log(this.shiftAmount)
this.startValue = startValue;
this.length =
this.length - (this.length * BigInt(displacementRate)) / 1000n;
this.startValue =
startValue + (this.length * BigInt(displacementRate)) / 1000n;
this.degreeConfig = degreeConfig;
this.overhead = this.sumOverhead();
this.entryOverhead = this.degreeConfig.pageSize / this.overhead;
if (degreeConfig.degree == 0)
this.degree = Math.floor(
(this.degreeConfig.pageSize - this.overhead) /
(this.degreeConfig.keySize +
this.degreeConfig.TIDSize +
this.entryOverhead)
);
else this.degree = degreeConfig.degree;
this.degree = DegreeComplications*this.degree
this.chunkCount = BigInt(this.degree * 2) ** BigInt(this.depth);
this.chunkLength = this.length / this.chunkCount;
this.data.degree = this.degree;
this.restConfigData.chunkLength = this.chunkLength;
this.restConfigData.degree = this.degree;
this.restConfigData.startValue = this.startValue;
this.restConfigData.degree = this.degree;
if (!fs.existsSync(path)) {
this.syncSaveObject(`${path}`, this.restConfigData);
}
if (restConfigData) {
this.depth = restConfigData.depth;
this.degree = restConfigData.degree;
this.chunkLength = BigInt(restConfigData.chunkLength);
this.indexInCurrentDepth = BigInt(restConfigData.indexInCurrentDepth);
this.startValue = BigInt(restConfigData.startValue);
this.usedIdCount = BigInt(restConfigData.usedIdCount);
} else {
this.restConfigData = this.readObject(path);
if (this.restConfigData) {
this.depth = this.restConfigData.depth;
this.degree = this.restConfigData.degree;
this.chunkLength = BigInt(this.restConfigData.chunkLength);
this.indexInCurrentDepth = BigInt(
this.restConfigData.indexInCurrentDepth
);
this.startValue = BigInt(this.restConfigData.startValue);
this.usedIdCount = BigInt(this.restConfigData.usedIdCount);
}
}
setInterval(() => {
this.saveObject(`${path}`, this.restConfigData);
}, this.saveTime);
}
private sumOverhead() {
const d = this.degreeConfig;
return (
d.keySize +
d.TIDSize +
d.indexTupleDataSize +
d.linePointerSize +
d.addingPaddingSize
);
}
public getExtraBtuid(): string {
let extra = ''
extra += randomBytes(Math.ceil(this.EXTRALENGTH / 2))
.toString("hex")
.slice(0, this.EXTRALENGTH);
let first =this.bigIntToHex(this.getId())
return this.shiftHexMonotone(first,this.shiftAmount) +'-' +this.shiftHexMonotone( extra,this.shiftAmount);
}
shiftHexMonotone(text: string, shiftAmount: number[]): string {
const ALNUM = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
return Array.from(text).map(ch => {
let code = ch.charCodeAt(0);
if(code >= 97){
code =code -87
}
else if(code>= 48){
code =code -48
}
let shiftMin =0
if(code != 0 )
shiftMin = shiftAmount[code -1]
let shiftMax = shiftAmount[code]
return ALNUM[this.randomInRange(shiftMin,shiftMax)];
}).join("");
}
stringToArray16(input: string): number[] {
const result: number[] = [];
let hash = 0;
// حساب قيمة hash من النص
for (let i = 0; i < input.length; i++) {
hash = (hash * 31 + input.charCodeAt(i)) >>> 0; // >>> 0 لتحويله لـ unsigned int
}
// توليد 16 رقم بناءً على الـ hash
for (let i = 0; i < 16; i++) {
hash = (hash * 1103515245 + 12345) >>> 0; // Linear Congruential Generator
result.push(hash % 63); // لأنك تريد القيم ≤ 62
}
// ترتيب تصاعدي
result.sort((a, b) => a - b);
return result;
}
randomInRange(min: number, max: number) {
return Math.floor(Math.random() * (max - min)) + min;
}
public getBtuid(): string {
return this.bigIntToHex(this.getId());
}
private getId(): bigint {
let start: bigint = 0n;
start = this.getNextInDepthAndIndex(
this.depth,
this.degree,
this.chunkLength,
this.indexInCurrentDepth + 1n,
this.startValue,
this.usedIdCount
);
this.indexInCurrentDepth++;
this.restConfigData.indexInCurrentDepth = this.indexInCurrentDepth;
if (this.indexInCurrentDepth >= this.chunkCount) {
this.indexInCurrentDepth = 0n;
this.restConfigData.indexInCurrentDepth = this.indexInCurrentDepth;
this.usedIdCount++;
this.restConfigData.usedIdCount = this.usedIdCount;
if (this.usedIdCount >= 1) {
this.usedIdCount = 0n;
this.indexInCurrentDepth++;
this.restConfigData.indexInCurrentDepth = this.indexInCurrentDepth;
this.restConfigData.usedIdCount = this.usedIdCount;
this.depth++;
this.restConfigData.depth = this.depth;
this.chunkCount = BigInt(this.degree * 2 - 1) ** BigInt(this.depth);
let pwrPart = BigInt(this.degree * 2) ** BigInt(this.depth);
this.chunkLength = this.length / pwrPart;
this.restConfigData.chunkLength = this.chunkLength;
}
}
return start;
}
private padHex(hex: string): string {
let hexText = hex.padStart(this.HEX_LENGTH, "0");
return hexText;
}
private bigIntToHex(value: bigint): string {
return this.padHex(value.toString(16));
}
private getNextInDepthAndIndex(
depth: number,
degree: number,
chunkLength: bigint,
index: bigint,
startValue: bigint,
usedIdCount: bigint
): bigint {
const usedItemInlastLevel = BigInt((2 * degree - 1) * (depth - 1));
const multiPart = chunkLength * index;
const start: bigint = startValue + usedItemInlastLevel + multiPart;
return start;
}
syncSaveObject(path: string, obj: RestConfigData) {
const jsonString = JSON.stringify(
obj,
(key, value) => (typeof value === "bigint" ? value.toString() : value),
2
);
fs.writeFileSync(`${path}`, jsonString, "utf8");
}
saveObject(path: string, obj: RestConfigData) {
const jsonString = JSON.stringify(
obj,
(key, value) => (typeof value === "bigint" ? value.toString() : value),
2
);
fs.writeFile(`${path}`, jsonString, "utf8", () => {});
}
readObject(path: string) {
if (!fs.existsSync(`${path}`)) {
return null;
}
const fileData = fs.readFileSync(`${path}`, "utf8");
const parsedObject = JSON.parse(fileData, (key, value) => {
if (typeof value === "string" && /^\d+n$/.test(value)) {
return BigInt(value.slice(0, -1));
}
return value;
});
return parsedObject;
}
}