UNPKG

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