nehoid
Version:
NehoID is a high-performance TypeScript library designed for generating, validating, and managing unique identifiers in enterprise-grade applications.
1,376 lines (1,362 loc) • 109 kB
JavaScript
'use strict';
var strulink = require('strulink');
var fflate = require('fflate');
function _interopNamespaceDefault(e) {
var n = Object.create(null);
if (e) {
Object.keys(e).forEach(function (k) {
if (k !== 'default') {
var d = Object.getOwnPropertyDescriptor(e, k);
Object.defineProperty(n, k, d.get ? d : {
enumerable: true,
get: function () { return e[k]; }
});
}
});
}
n.default = e;
return Object.freeze(n);
}
var fflate__namespace = /*#__PURE__*/_interopNamespaceDefault(fflate);
/*---------------------------------------------------------------------------------------------
* Copyright (c) NEHONIX INC. All rights reserved.
* Licensed under the MIT License. See LICENSE in the project root for license information.
*--------------------------------------------------------------------------------------------*/
const __processor__ = {
encode: (input, encoding) => {
return strulink.__strl__.encode(input, encoding);
},
decode: (input, encoding) => {
return strulink.__strl__.decode(input, encoding);
},
encodeMultiple: (input, encodings) => {
return strulink.__strl__.encodeMultiple(input, encodings);
},
encodeMultipleAsync: async (input, encodings) => {
return await strulink.__strl__.encodeMultipleAsync(input, encodings);
},
autoDetectAndDecode: (input) => {
return strulink.__strl__.autoDetectAndDecode(input);
},
};
/**
* Core encoding utilities for data transformation and compression.
*
* The Encoder class provides a comprehensive set of encoding and decoding methods
* supporting multiple algorithms including base64, hex, ROT13, compression schemes,
* and specialized encodings. It serves as the foundation for NehoID's transformation
* capabilities and supports both synchronous and asynchronous operations.
*
* @example
* ```typescript
* // Basic encoding and decoding
* const encoded = await Encoder.encode('hello world', 'base64');
* const decoded = Encoder.decode(encoded, 'base64');
*
* // Multiple encodings in sequence
* const multiEncoded = await Encoder.encode('data', ['base64', 'hex']);
* const multiDecoded = Encoder.decode(multiEncoded, ['hex', 'base64']);
*
* // Compression
* const compressed = Encoder.compress('long text to compress', 'gzip');
* const decompressed = Encoder.decompress(compressed, 'gzip');
* ```
*/
class Encoder {
/**
* Encode a string using one or more encoding schemes asynchronously.
*
* Applies encoding transformations in sequence, where each encoding
* is applied to the result of the previous encoding. This allows for
* complex encoding pipelines to be built programmatically.
*
* @param input - The string to encode
* @param encodings - Single encoding type or array of encoding types to apply in sequence
* @returns Promise that resolves to the encoded string
*
* @example
* ```typescript
* // Single encoding
* const base64Result = await Encoder.encode('hello', 'base64');
* // Output: 'aGVsbG8='
*
* // Multiple encodings
* const result = await Encoder.encode('data', ['base64', 'hex']);
* // Applies base64 first, then hex to the result
*
* // URL-safe encoding
* const urlSafe = await Encoder.encode('user input', 'urlSafeBase64');
* ```
*/
static async encode(input, encodings) {
const encodingArray = Array.isArray(encodings) ? encodings : [encodings];
let result = input;
const enc = await __processor__.encodeMultipleAsync(result, encodingArray);
result = enc.results[enc.results.length - 1].encoded;
return result;
}
/**
* Decode a string using one or more decoding schemes.
*
* Reverses encoding transformations by applying decodings in reverse order.
* Supports both manual specification of decoding types and automatic detection.
*
* @param input - The encoded string to decode
* @param encodings - Single decoding type or array of decoding types to apply in reverse order
* @param opt - Optional decoding configuration
* @param opt.autoDetect - Whether to attempt automatic encoding detection (experimental)
* @returns The decoded string
*
* @example
* ```typescript
* // Single decoding
* const original = Encoder.decode('aGVsbG8=', 'base64');
* // Output: 'hello'
*
* // Multiple decodings (reverse order)
* const result = Encoder.decode(encodedData, ['hex', 'base64']);
* // Decodes hex first, then base64
*
* // With auto-detection
* const decoded = Encoder.decode(encodedStr, 'base64', { autoDetect: true });
* ```
*/
static decode(input, encodings, opt) {
const encodingArray = Array.isArray(encodings) ? encodings : [encodings];
let result = input;
// Decode in reverse order
for (const encoding of encodingArray.reverse()) {
if (opt?.autoDetect) {
result = __processor__.autoDetectAndDecode(result).val();
}
else {
result = __processor__.decode(result, encoding);
}
}
return result;
}
/**
* Compress a string using LZ77 or GZIP-style compression.
*
* Reduces the size of input strings using dictionary-based compression algorithms.
* LZ77 uses sliding window compression, while GZIP uses LZW-style dictionary compression.
* Both methods are lossless and can be reversed using the decompress method.
*
* @param input - The string to compress
* @param method - Compression algorithm to use ('lz77' or 'gzip')
* @returns The compressed and base64-encoded string
*
* @example
* ```typescript
* // LZ77 compression (good for repetitive data)
* const compressed = Encoder.compress('abababababab', 'lz77');
* const decompressed = Encoder.decompress(compressed, 'lz77');
*
* // GZIP compression (good for large texts)
* const text = 'A very long string with lots of repetition and patterns...';
* const gzipped = Encoder.compress(text, 'gzip');
* const original = Encoder.decompress(gzipped, 'gzip');
*
* // Measure compression ratio
* const ratio = gzipped.length / text.length;
* console.log(`Compression ratio: ${ratio.toFixed(2)}`);
* ```
*/
static compress(input, method) {
if (!input)
return "";
try {
const data = new TextEncoder().encode(input);
let compressed;
switch (method) {
case "lz77":
// Deflate is used for LZ77-style compression
compressed = fflate__namespace.deflateSync(data);
break;
case "gzip":
compressed = fflate__namespace.gzipSync(data);
break;
default:
return input;
}
// Convert Uint8Array to Base64 safely
let binary = "";
const len = compressed.byteLength;
for (let i = 0; i < len; i++) {
binary += String.fromCharCode(compressed[i]);
}
return btoa(binary);
}
catch (error) {
console.error(`Compression error (${method}):`, error);
return input;
}
}
/**
* Decompress a string that was compressed using the compress method.
*
* Reverses the compression applied by the compress method, restoring the
* original string. Supports both LZ77 and GZIP decompression algorithms.
*
* @param input - The compressed string to decompress
* @param method - Decompression algorithm to use ('lz77' or 'gzip')
* @returns The decompressed original string
*
* @example
* ```typescript
* // Round-trip compression
* const original = 'This is a long string with repetitive patterns...';
* const compressed = Encoder.compress(original, 'lz77');
* const decompressed = Encoder.decompress(compressed, 'lz77');
* // decompressed === original
*
* // Error handling
* try {
* const result = Encoder.decompress('invalid-compressed-data', 'gzip');
* } catch (error) {
* console.error('Decompression failed:', error);
* }
*
* // Check decompression success
* const result = Encoder.decompress(compressedData, 'lz77');
* if (!result) {
* console.error('Decompression returned empty result');
* }
* ```
*/
static decompress(input, method) {
if (!input)
return "";
try {
// Decode Base64 to Uint8Array safely
const binaryString = atob(input);
const data = new Uint8Array(binaryString.length);
for (let i = 0; i < binaryString.length; i++) {
data[i] = binaryString.charCodeAt(i);
}
let decompressed;
switch (method) {
case "lz77":
decompressed = fflate__namespace.inflateSync(data);
break;
case "gzip":
decompressed = fflate__namespace.gunzipSync(data);
break;
default:
return input;
}
return new TextDecoder().decode(decompressed);
}
catch (error) {
console.error(`Decompression error (${method}):`, error);
return input;
}
}
}
class Generator {
static generateRandomString(length, alphabet) {
return Array.from({ length }, () => alphabet[Math.floor(Math.random() * alphabet.length)]).join("");
}
static generate(options = {}) {
const opts = {
size: 8,
segments: 4,
separator: "-",
encoding: "rawHex",
prefix: "",
includeTimestamp: false,
alphabet: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
compression: "none",
reversible: false,
randomness: 'fast',
includeChecksum: false,
...this.DEFAULT_OPTIONS,
...options
};
const segments = [];
if (opts.includeTimestamp) {
const timestamp = Date.now().toString(36);
segments.push(timestamp);
}
for (let i = 0; i < opts.segments; i++) {
const randomString = this.generateRandomString(opts.size, opts.alphabet);
const multiEnc = __processor__.encodeMultiple(randomString, Array.isArray(opts.encoding) ? opts.encoding : []);
const encoded = Array.isArray(opts.encoding)
? multiEnc.results[multiEnc.results.length - 1].encoded
: __processor__.encode(randomString, opts.encoding);
segments.push(encoded);
}
let id = segments.join(opts.separator);
if (opts.compression !== "none") {
id = Encoder.compress(id, opts.compression);
}
return opts.prefix ? `${opts.prefix}${opts.separator}${id}` : id;
}
static async safe(options) {
let attempts = 0;
let id;
while (attempts < options.maxAttempts) {
id = this.generate();
if (await options.checkFunction(id)) {
return id;
}
attempts++;
if (options.backoffType === "exponential") {
await new Promise((resolve) => setTimeout(resolve, Math.pow(2, attempts)));
}
else {
await new Promise((resolve) => setTimeout(resolve, 100 * attempts));
}
}
throw new Error(`Failed to generate unique ID after ${options.maxAttempts} attempts`);
}
static batch(options) {
const ids = new Set();
const { count, format = "standard", ensureUnique = true } = options;
while (ids.size < count) {
const id = format === "standard"
? this.generate()
: format === "uuid"
? this.uuid()
: format === "nano"
? this.nano()
: this.short();
if (!ensureUnique || !ids.has(id)) {
ids.add(id);
}
}
return Array.from(ids);
}
static uuid() {
return "xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx".replace(/[xy]/g, (c) => {
const r = (Math.random() * 16) | 0;
const v = c === "x" ? r : (r & 0x3) | 0x8;
return v.toString(16);
});
}
static nano(size = 12) {
return this.generateRandomString(size, "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789");
}
static short(length = 8) {
return this.generate({
size: length,
segments: 1,
encoding: "urlSafeBase64",
});
}
static hex(length = 32) {
return this.generateRandomString(length, "0123456789abcdef");
}
}
Generator.DEFAULT_OPTIONS = {
size: 8,
segments: 4,
separator: "-",
encoding: "rawHex",
prefix: "",
includeTimestamp: false,
alphabet: "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789",
compression: "none",
reversible: false,
// New options with defaults
randomness: 'fast',
includeChecksum: false,
};
/**
* Specialized ID generators for NehoID
* Implements hierarchical, temporal, and sequential ID generation
*/
class Specialized {
/**
* Generates a hierarchical ID with parent-child relationships
* @param parent Optional parent ID to create a child under
* @param level Hierarchy level (defaults to 1 if no parent, otherwise parent level + 1)
* @param separator Character to separate hierarchy levels
* @returns A hierarchical ID
*/
static hierarchical(options = {}) {
const { parent, level: specifiedLevel, separator = '/' } = options;
// Determine the level based on parent or specified level
const level = specifiedLevel ?? (parent ? parent.split(separator).length + 1 : 1);
// Generate a random segment for this level
const segment = Generator.short(8);
// If there's a parent, append to it; otherwise, start a new hierarchy
if (parent) {
return `${parent}${separator}${segment}`;
}
else {
return `${level}${separator}${segment}`;
}
}
/**
* Generates a time-ordered ID for chronological sorting
* @param precision Time precision ('ms', 's', 'm', 'h', 'd')
* @param suffix Whether to add a random suffix for uniqueness
* @returns A temporal ID with timestamp
*/
static temporal(options = {}) {
const { precision = 'ms', suffix = true, format = 'hex' } = options;
// Get current timestamp
let timestamp = Date.now();
// Adjust precision
if (precision === 's') {
timestamp = Math.floor(timestamp / 1000);
}
else if (precision === 'm') {
timestamp = Math.floor(timestamp / 60000);
}
else if (precision === 'h') {
timestamp = Math.floor(timestamp / 3600000);
}
else if (precision === 'd') {
timestamp = Math.floor(timestamp / 86400000);
}
// Format timestamp
let formattedTime;
if (format === 'hex') {
formattedTime = timestamp.toString(16).padStart(12, '0');
}
else if (format === 'b36') {
formattedTime = timestamp.toString(36).padStart(8, '0');
}
else {
formattedTime = timestamp.toString();
}
// Add random suffix if requested
if (suffix) {
const randomSuffix = Generator.short(6);
return `${formattedTime}-${randomSuffix}`;
}
else {
return formattedTime;
}
}
/**
* Generates a sequential ID suitable for database use
* @param prefix Optional prefix for the ID
* @param counter Current counter value
* @param padLength Length to pad the counter to
* @returns A sequential ID
*/
static sequential(options) {
const { prefix = '', counter, padLength = 10, suffix = false } = options;
// Format the counter with padding
const formattedCounter = counter.toString().padStart(padLength, '0');
// Add random suffix if requested
if (suffix) {
const randomSuffix = Generator.short(4);
return `${prefix}${formattedCounter}-${randomSuffix}`;
}
else {
return `${prefix}${formattedCounter}`;
}
}
/**
* Generates a temporal ID from a specific timestamp
* @param timestamp Unix timestamp in milliseconds
* @param precision Time precision ('ms', 's', 'm', 'h', 'd')
* @param suffix Whether to add a random suffix for uniqueness
* @param format Timestamp format ('hex' | 'dec' | 'b36')
* @returns A temporal ID with the specified timestamp
*/
static fromTemporal(timestamp, options = {}) {
const { precision = 'ms', suffix = true, format = 'hex' } = options;
// Adjust precision
let adjustedTimestamp = timestamp;
if (precision === 's') {
adjustedTimestamp = Math.floor(timestamp / 1000);
}
else if (precision === 'm') {
adjustedTimestamp = Math.floor(timestamp / 60000);
}
else if (precision === 'h') {
adjustedTimestamp = Math.floor(timestamp / 3600000);
}
else if (precision === 'd') {
adjustedTimestamp = Math.floor(timestamp / 86400000);
}
// Format timestamp
let formattedTime;
if (format === 'hex') {
formattedTime = adjustedTimestamp.toString(16).padStart(12, '0');
}
else if (format === 'b36') {
formattedTime = adjustedTimestamp.toString(36).padStart(8, '0');
}
else {
formattedTime = adjustedTimestamp.toString();
}
// Add random suffix if requested
if (suffix) {
const randomSuffix = Generator.short(6);
return `${formattedTime}-${randomSuffix}`;
}
else {
return formattedTime;
}
}
/**
* Extracts timestamp from a temporal ID
* @param temporalId The temporal ID to extract timestamp from
* @param precision Time precision used in the temporal ID ('ms', 's', 'm', 'h', 'd')
* @param format Timestamp format used in the temporal ID ('hex' | 'dec' | 'b36')
* @returns Unix timestamp in milliseconds
* @throws Error if the temporal ID format is invalid
*/
static fromTemporalToTimestamp(temporalId, options = {}) {
const { precision = 'ms', format = 'hex' } = options;
if (!temporalId || typeof temporalId !== 'string') {
throw new Error('Invalid temporal ID: must be a non-empty string');
}
// Extract timestamp part (before first dash if suffix exists)
const timestampPart = temporalId.split('-')[0];
if (!timestampPart) {
throw new Error('Invalid temporal ID format: missing timestamp part');
}
// Parse timestamp based on format
let timestamp;
try {
if (format === 'hex') {
timestamp = parseInt(timestampPart, 16);
}
else if (format === 'b36') {
timestamp = parseInt(timestampPart, 36);
}
else {
timestamp = parseInt(timestampPart, 10);
}
if (isNaN(timestamp)) {
throw new Error('Invalid timestamp value');
}
}
catch (error) {
throw new Error(`Invalid temporal ID: cannot parse timestamp "${timestampPart}" as ${format}`);
}
// Restore precision
if (precision === 's') {
timestamp *= 1000;
}
else if (precision === 'm') {
timestamp *= 60000;
}
else if (precision === 'h') {
timestamp *= 3600000;
}
else if (precision === 'd') {
timestamp *= 86400000;
}
return timestamp;
}
}
/**
* Specialized ID generation methods for advanced use cases.
*
* This module provides generators for hierarchical relationships, time-ordered IDs,
* and sequential identifiers. These specialized generators are designed for specific
* architectural patterns and data modeling requirements.
*/
class SpecializedGenerators {
/**
* Generates a hierarchical ID with parent-child relationships.
*
* Creates IDs that encode hierarchical structures, making it easy to query
* and navigate tree-like data relationships. The ID format supports efficient
* ancestor and descendant queries in databases.
*
* @param options - Configuration options for hierarchical generation
* @returns A hierarchical ID string with encoded relationship information
*
* @example
* ```typescript
* // Basic hierarchical ID
* const rootId = SpecializedGenerators.hierarchical();
*
* // Child ID with parent reference
* const childId = SpecializedGenerators.hierarchical({
* parentId: rootId,
* level: 1
* });
*
* // Deep hierarchy
* const grandchildId = SpecializedGenerators.hierarchical({
* parentId: childId,
* level: 2,
* maxDepth: 5
* });
* ```
*
* @example
* ```typescript
* // Database queries with hierarchical IDs
* // Find all descendants of a node
* const descendants = await db.collection.find({
* hierarchicalId: { $regex: `^${parentId}` }
* });
*
* // Find direct children
* const children = await db.collection.find({
* parentId: parentId
* });
* ```
*/
static hierarchical(options = {}) {
// Map parentId -> parent and depth -> level for consistency with JSDocs
const mappedOptions = {
parent: options.parent || options.parentId,
level: options.level || options.depth,
separator: options.separator,
};
return Specialized.hierarchical(mappedOptions);
}
/**
* Generates a time-ordered ID for chronological sorting and pagination.
*
* Creates IDs that sort chronologically by embedding timestamp information.
* This enables efficient time-based queries and pagination without requiring
* separate timestamp columns.
*
* @param options - Configuration options for temporal generation
* @returns A temporal ID string with embedded timestamp for natural sorting
*
* @example
* ```typescript
* // Basic temporal ID (current timestamp)
* const eventId = SpecializedGenerators.temporal();
*
* // Custom timestamp
* const pastEventId = SpecializedGenerators.temporal({
* timestamp: new Date('2023-01-01').getTime()
* });
*
* // High precision temporal ID
* const preciseId = SpecializedGenerators.temporal({
* precision: 'nanoseconds',
* includeRandom: true
* });
* ```
*
* @example
* ```typescript
* // Time-based pagination
* const recentEvents = await db.collection.find({
* temporalId: { $gt: lastEventId }
* }).sort({ temporalId: 1 }).limit(50);
*
* // Events from specific time range
* const startTime = SpecializedGenerators.temporal({
* timestamp: Date.now() - (24 * 60 * 60 * 1000) // 24 hours ago
* });
* const todaysEvents = await db.collection.find({
* temporalId: { $gte: startTime }
* });
* ```
*/
static temporal(...options) {
return Specialized.temporal(...options);
}
/**
* Generates a sequential ID suitable for database primary keys.
*
* Creates human-readable sequential identifiers that can replace auto-incrementing
* integers in databases. Supports prefixes, padding, and custom formatting
* for business logic requirements.
*
* @param options - Configuration options for sequential generation
* @returns A formatted sequential ID string
*
* @example
* ```typescript
* // Simple sequential ID
* const id = SpecializedGenerators.sequential({
* counter: 1001
* });
* // Output: '1001'
*
* // With prefix and padding
* const orderId = SpecializedGenerators.sequential({
* prefix: 'ORD',
* counter: 42,
* padLength: 6
* });
* // Output: 'ORD000042'
*
* // With suffix
* const invoiceId = SpecializedGenerators.sequential({
* prefix: 'INV',
* counter: 123,
* suffix: true
* });
* // Output: 'INV123'
* ```
*
* @example
* ```typescript
* // Database schema with sequential IDs
* const userSchema = new mongoose.Schema({
* _id: {
* type: String,
* default: () => SpecializedGenerators.sequential({
* prefix: 'USR',
* counter: await getNextUserCounter()
* })
* },
* name: String
* });
* ```
*/
/**
* Generates a temporal ID from a timestamp.
*
* Converts a Unix timestamp into a temporal ID that can be used for
* chronological sorting and time-based queries. The temporal ID format
* includes encoded timestamp information for efficient database indexing.
*
* @param timestamp - Unix timestamp in milliseconds
* @returns A temporal ID string containing the encoded timestamp
*
* @example
* ```typescript
* // Generate temporal ID from current time
* const temporalId = SpecializedGenerators.fromTemporal(Date.now());
*
* // Generate temporal ID from specific date
* const pastTemporalId = SpecializedGenerators.fromTemporal(
* new Date('2023-01-01').getTime()
* );
*
* // Use in database queries
* const recentEvents = await db.collection.find({
* temporalId: { $gte: SpecializedGenerators.fromTemporal(Date.now() - 86400000) }
* });
* ```
*/
static fromTemporal(timestamp, options = {}) {
return Specialized.fromTemporal(timestamp, options);
}
/**
* Extracts timestamp from a temporal ID.
*
* Reverses the temporal ID generation process to recover the original
* Unix timestamp. This is useful for converting temporal IDs back to
* human-readable dates or for time-based calculations.
*
* @param temporalId - Temporal ID string to extract timestamp from
* @returns Unix timestamp in milliseconds
* @throws {Error} If the temporal ID format is invalid
*
* @example
* ```typescript
* // Extract timestamp from temporal ID
* const temporalId = SpecializedGenerators.fromTemporal(Date.now());
* const timestamp = SpecializedGenerators.fromTemporalToTimestamp(temporalId);
*
* // Convert back to Date object
* const date = new Date(timestamp);
* console.log(date.toISOString());
*
* // Calculate time differences
* const age = Date.now() - timestamp;
* const hoursOld = age / (1000 * 60 * 60);
* ```
*
* @example
* ```typescript
* // Error handling
* try {
* const timestamp = SpecializedGenerators.fromTemporalToTimestamp('invalid-id');
* } catch (error) {
* console.error('Invalid temporal ID format');
* }
* ```
*/
static fromTemporalToTimestamp(temporalId, options = {}) {
return Specialized.fromTemporalToTimestamp(temporalId, options);
}
static sequential(options) {
return Specialized.sequential(options);
}
}
class Validator {
static validate(id, options = {}) {
const { checkFormat = true } = options;
if (!id || typeof id !== "string") {
return false;
}
if (checkFormat) {
// Basic format validation
if (id.length < 8) {
return false;
}
// Check for invalid characters (including Base64 characters for metadata/encoding)
if (/[^A-Za-z0-9_+\/=-]/.test(id)) {
return false;
}
}
return true;
}
static validateBatch(ids, options = {}) {
const result = {
valid: [],
invalid: [],
duplicates: [],
};
const seen = new Set();
for (const id of ids) {
if (!this.validate(id, options)) {
result.invalid.push(id);
continue;
}
if (options.checkCollisions && seen.has(id)) {
result.duplicates.push(id);
continue;
}
result.valid.push(id);
seen.add(id);
}
return result;
}
static healthCheck(id) {
const score = this.calculateHealthScore(id);
const entropy = this.calculateEntropy(id);
const predictability = this.assessPredictability(id);
const recommendations = this.generateRecommendations(score, entropy, id.length);
return {
score,
entropy: entropy > 0.75 ? "high" : entropy > 0.5 ? "medium" : "low",
predictability: predictability < 0.3 ? "low" : predictability < 0.6 ? "medium" : "high",
recommendations,
};
}
static calculateHealthScore(id) {
let score = 1.0;
// Length check
if (id.length < 8)
score *= 0.5;
if (id.length > 32)
score *= 0.9;
// Character variety
const hasUpper = /[A-Z]/.test(id);
const hasLower = /[a-z]/.test(id);
const hasNumber = /[0-9]/.test(id);
const hasSpecial = /[^A-Za-z0-9]/.test(id);
if (!hasUpper)
score *= 0.9;
if (!hasLower)
score *= 0.9;
if (!hasNumber)
score *= 0.9;
if (!hasSpecial)
score *= 0.95;
return Math.min(1, Math.max(0, score));
}
static calculateEntropy(id) {
const charFreq = new Map();
for (const char of id) {
charFreq.set(char, (charFreq.get(char) || 0) + 1);
}
let entropy = 0;
const length = id.length;
for (const freq of charFreq.values()) {
const probability = freq / length;
entropy -= probability * Math.log2(probability);
}
return entropy / Math.log2(charFreq.size);
}
static assessPredictability(id) {
let predictability = 0;
// Check for patterns
const hasRepeatingPattern = /(.+?)\1+/.test(id);
if (hasRepeatingPattern)
predictability += 0.3;
// Check for sequential characters
const hasSequential = /(?:abc|123|xyz)/i.test(id);
if (hasSequential)
predictability += 0.2;
// Check for common words
const hasCommonWords = /(?:test|admin|user|temp)/i.test(id);
if (hasCommonWords)
predictability += 0.4;
return Math.min(1, predictability);
}
static generateRecommendations(score, entropy, length) {
const recommendations = [];
if (score < 0.8) {
if (length < 12)
recommendations.push("increase_length");
if (entropy < 0.6)
recommendations.push("increase_complexity");
}
if (length > 50)
recommendations.push("consider_shorter");
if (entropy < 0.4)
recommendations.push("add_more_variety");
return recommendations;
}
}
Validator.UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i;
Validator.NANO_REGEX = /^[A-Za-z0-9_-]+$/;
/**
* Validation utilities for NehoID identifiers.
*
* This module provides comprehensive validation capabilities for IDs generated by NehoID,
* including format checking, collision detection, batch validation, and health scoring.
* All validation methods are static and can be used independently or through the main NehoID class.
*/
class Validators {
/**
* Validates a single ID against configured rules and formats.
*
* Performs comprehensive validation including format checking, entropy analysis,
* and optional collision detection based on the provided options.
*
* @param id - The ID string to validate
* @param options - Optional validation configuration
* @returns True if the ID passes all validation checks, false otherwise
*
* @example
* ```typescript
* // Basic format validation
* const isValid = Validators.validate('user-abc123def');
*
* // With collision checking
* const isUnique = Validators.validate('user-abc123def', {
* checkCollisions: true
* });
*
* // Full validation with repair
* const isValidAndRepaired = Validators.validate('corrupted-id', {
* checkFormat: true,
* repairCorrupted: true
* });
* ```
*/
static validate(id, options) {
return Validator.validate(id, options);
}
/**
* Validates multiple IDs in a single batch operation.
*
* This method is optimized for validating large numbers of IDs efficiently.
* It processes IDs and categorizes them into valid, invalid, and duplicates.
*
* @param ids - Array of ID strings to validate
* @param options - Optional validation configuration applied to all IDs
* @returns An object containing arrays of valid, invalid, and duplicate IDs
*
* @example
* ```typescript
* const ids = ['user-123', 'admin-456', 'guest-789', 'user-123'];
* const result = Validators.validateBatch(ids, { checkCollisions: true });
* // Output: {
* // valid: ['user-123', 'admin-456'],
* // invalid: ['guest-789'],
* // duplicates: ['user-123']
* // }
* ```
*/
static validateBatch(ids, options) {
return Validator.validateBatch(ids, options);
}
/**
* Performs a comprehensive health check on an ID.
*
* Analyzes the ID's entropy, predictability, and overall quality,
* providing a health score and actionable recommendations for improvement.
*
* @param id - The ID string to analyze
* @returns A health score object containing score, entropy level, predictability, and recommendations
*
* @example
* ```typescript
* const health = Validators.healthCheck('weak-id-123');
* console.log(`Score: ${health.score}`); // 0.3
* console.log(`Entropy: ${health.entropy}`); // 'low'
* console.log('Recommendations:', health.recommendations);
* // Output: ['Consider using a longer ID', 'Add random components']
* ```
*
* @example
* ```typescript
* // Quality assurance for generated IDs
* const newId = NehoID.generate();
* const health = Validators.healthCheck(newId);
*
* if (health.score < 0.7) {
* console.warn('Low quality ID generated:', health.recommendations);
* }
* ```
*/
static healthCheck(id) {
return Validator.healthCheck(id);
}
}
/**
* Monitoring and performance statistics management for NehoID.
*
* This module provides real-time tracking of ID generation performance, collision rates,
* memory usage, and distribution quality. Statistics help optimize generation strategies
* and monitor system health.
*
* @example
* ```typescript
* // Start monitoring
* Monitor.startMonitoring();
*
* // Perform operations
* for (let i = 0; i < 1000; i++) {
* NehoID.generate();
* }
*
* // Get statistics
* const stats = Monitor.getStats();
* console.log(`Generated: ${stats.generated}`);
* console.log(`Avg time: ${stats.averageGenerationTime}`);
* console.log(`Memory: ${stats.memoryUsage}`);
* ```
*/
class Monitor {
/**
* Starts collecting performance and usage statistics.
*
* Enables real-time monitoring of ID generation operations, including
* timing, collision tracking, and memory usage analysis.
*
* @example
* ```typescript
* Monitor.startMonitoring();
*
* // All subsequent ID operations will be tracked
* const ids = NehoID.batch({ count: 100 });
*
* const stats = Monitor.getStats();
* console.log(`Generation time: ${stats.averageGenerationTime}`);
* ```
*/
static startMonitoring() {
Monitor.monitoringEnabled = true;
}
/**
* Stops collecting performance statistics.
*
* Disables monitoring to reduce overhead when statistics are not needed.
* Existing statistics are preserved and can still be retrieved.
*
* @example
* ```typescript
* // Stop monitoring after analysis
* Monitor.stopMonitoring();
*
* // Statistics from previous operations remain available
* const finalStats = Monitor.getStats();
* ```
*/
static stopMonitoring() {
Monitor.monitoringEnabled = false;
}
/**
* Retrieves current monitoring statistics.
*
* Returns a snapshot of all collected performance metrics including
* generation counts, collision rates, timing averages, and memory usage.
*
* @returns A complete statistics object with current metrics
*
* @example
* ```typescript
* const stats = Monitor.getStats();
*
* console.log(`Total IDs generated: ${stats.generated}`);
* console.log(`Collision count: ${stats.collisions}`);
* console.log(`Average generation time: ${stats.averageGenerationTime}`);
* console.log(`Current memory usage: ${stats.memoryUsage}`);
* console.log(`Distribution quality: ${stats.distributionScore}`);
* ```
*/
static getStats() {
return { ...Monitor.stats };
}
/**
* Updates performance statistics after an ID generation operation.
*
* Tracks timing information and updates rolling averages. Only active
* when monitoring is enabled. This method is called automatically
* by generation methods and typically doesn't need manual invocation.
*
* @param startTime - The performance.now() timestamp when generation started
*
* @example
* ```typescript
* // Manual timing (normally done automatically)
* const start = performance.now();
* const id = someGenerationFunction();
* Monitor.updateStats(start);
* ```
*/
static updateStats(startTime) {
if (!Monitor.monitoringEnabled)
return;
Monitor.stats.generated++;
const generationTime = performance.now() - startTime;
// Update average generation time
const prevTotal = parseFloat(Monitor.stats.averageGenerationTime) *
(Monitor.stats.generated - 1);
Monitor.stats.averageGenerationTime = `${((prevTotal + generationTime) /
Monitor.stats.generated).toFixed(2)}ms`;
// Update memory usage
const used = process?.memoryUsage();
Monitor.stats.memoryUsage = `${Math.round((used.heapUsed / 1024 / 1024) * 100) / 100}MB`;
}
/**
* Increments the collision counter.
*
* Tracks instances where ID generation encountered collisions that required
* regeneration. This helps monitor the effectiveness of collision strategies.
*
* @example
* ```typescript
* // Called automatically during collision resolution
* try {
* await NehoID.safe(strategy);
* } catch (error) {
* Monitor.incrementCollisions();
* throw error;
* }
* ```
*/
static incrementCollisions() {
Monitor.stats.collisions++;
}
}
/** @private Whether monitoring is currently active */
Monitor.monitoringEnabled = false;
/** @private Internal statistics storage */
Monitor.stats = {
generated: 0,
collisions: 0,
averageGenerationTime: "0ms",
memoryUsage: "0MB",
distributionScore: 1.0,
};
/**
* Utility methods for NehoID operations.
*
* This module provides helper functions for common ID-related tasks,
* including coordinate hashing for location-based privacy and other
* utility operations that support the core NehoID functionality.
*/
class Utils {
/**
* Hashes geographic coordinates for privacy preservation.
*
* This method reduces the precision of latitude and longitude coordinates
* to protect user privacy while maintaining useful location-based grouping.
* The coordinates are rounded to one decimal place before hashing.
*
* @param latitude - The latitude coordinate (-90 to 90)
* @param longitude - The longitude coordinate (-180 to 180)
* @returns A compact base36 hash string representing the rounded coordinates
*
* @example
* ```typescript
* // Hash coordinates for location-based grouping
* const locationHash = Utils.hashCoordinates(37.7749, -122.4194);
* // Output: '2a3b1c' (example hash)
*
* // Use in ID generation for location-aware IDs
* const id = NehoID.generate({
* prefix: `loc-${locationHash}-`
* });
* ```
*
* @example
* ```typescript
* // Privacy-preserving location clustering
* const users = [
* { id: 1, lat: 37.7749, lng: -122.4194 },
* { id: 2, lat: 37.7750, lng: -122.4195 } // Very close location
* ];
*
* const clusters = users.map(user =>
* Utils.hashCoordinates(user.lat, user.lng)
* );
* // Both users will have the same hash due to rounding
* ```
*/
static hashCoordinates(latitude, longitude) {
// Round coordinates to reduce precision for privacy
const roundedLat = Math.round(latitude * 10) / 10;
const roundedLng = Math.round(longitude * 10) / 10;
// Combine coordinates into a string
const coordString = `${roundedLat},${roundedLng}`;
// Create a hash of the coordinates
let hash = 0;
for (let i = 0; i < coordString.length; i++) {
hash = (hash << 5) - hash + coordString.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
// Convert to a base36 string for compactness
return Math.abs(hash).toString(36);
}
}
/**
* Advanced generation methods for NehoID
*/
class Advanced {
/**
* Generates a context-aware ID that incorporates environmental information
* @param options Context options for ID generation
* @returns A context-aware unique ID
*/
static contextual(options) {
// Default options
const opts = {
includeDevice: options.includeDevice ?? true,
includeTimezone: options.includeTimezone ?? true,
includeBrowser: options.includeBrowser ?? false,
includeScreen: options.includeScreen ?? false,
includeLocation: options.includeLocation ?? false,
userBehavior: options.userBehavior ?? "",
};
// Gather context information
const contextParts = [];
// Add timestamp (always included)
const timestamp = Date.now().toString(36);
contextParts.push(`t${timestamp}`);
// Add timezone if requested
if (opts.includeTimezone) {
const timezoneOffset = new Date().getTimezoneOffset();
const timezone = Math.abs(timezoneOffset).toString(36);
contextParts.push(`z${timezoneOffset >= 0 ? "p" : "n"}${timezone}`);
}
// Add device information if requested
if (opts.includeDevice) {
// Get platform info - works in both Node.js and browser environments
let platform = "";
if (typeof navigator !== "undefined") {
platform = navigator.platform || "unknown";
}
else if (typeof process !== "undefined") {
platform = process.platform || "unknown";
}
// Create a hash of the platform
let hash = 0;
for (let i = 0; i < platform.length; i++) {
hash = (hash << 5) - hash + platform.charCodeAt(i);
hash |= 0; // Convert to 32bit integer
}
contextParts.push(`d${Math.abs(hash).toString(36)}`);
}
// Add browser information if requested (browser environment only)
if (opts.includeBrowser && typeof navigator !== "undefined") {
const userAgent = navigator.userAgent || "unknown";
let hash = 0;
for (let i = 0; i < userAgent.length; i++) {
hash = (hash << 5) - hash + userAgent.charCodeAt(i);
hash |= 0;
}
contextParts.push(`b${Math.abs(hash).toString(36)}`);
}
// Add screen information if requested (browser environment only)
if (opts.includeScreen && typeof window !== "undefined" && window.screen) {
const screenInfo = `${window.screen.width}x${window.screen.height}`;
contextParts.push(`s${screenInfo}`);
}
// Add location information if requested and available
if (opts.includeLocation) {
// In browser environments, use the Geolocation API if available
if (typeof navigator !== "undefined" && navigator.geolocation) {
try {
// Create a promise-based wrapper for the geolocation API
const getPosition = () => {
return new Promise((resolve, reject) => {
navigator.geolocation.getCurrentPosition(resolve, reject, {
enableHighAccuracy: true,
timeout: 5000,
maximumAge: 0,
});
});
};
// Get the current position
getPosition()
.then((position) => {
const { latitude, longitude } = position.coords;
// Hash the coordinates for privacy
const locationHash = Utils.hashCoordinates(latitude, longitude);
contextParts.push(`l${locationHash}`);
})
.catch(() => {
// If geolocation fails, add a fallback indicator
contextParts.push("lna");
});
}
catch (e) {
// If geolocation API is not available or fails, add a fallback indicator
contextParts.push("lna");
}
}
else if (typeof process !== "undefined") {
// In Node.js environments, try to get location from IP
try {
// Use environment variables or configuration for location info
const envLocation = process.env.LOCATION || process.env.REGION || "";
if (envLocation) {
// Hash the location string for consistency
let hash = 0;
for (let i = 0; i < envLocation.length; i++) {
hash = (hash << 5) - hash + envLocation.charCodeAt(i);
hash |= 0;
}
contextParts.push(`l${Math.abs(hash).toString(36)}`);
}
else {
// If no location info is available, add a fallback indicator
contextParts.push("lna");
}
}
catch (e) {
// If location detection fails, add a fallback indicator
contextParts.push("lna");
}
}
else {
// If neither browser nor Node.js environment is detected, add a fallback indicator
contextParts.push("lna");
}
}
// Add user behavior hash if provided
if (opts.userBehavior) {
let hash = 0;
for (let i = 0; i < opts.userBehavior.length; i++) {
hash = (hash << 5) - hash + opts.userBehavior.charCodeAt(i);
hash |= 0;
}
contextParts.push(`u${Math.abs(hash).toString(36)}`);
}
// Generate a random component to ensure uniqueness
const randomPart = Math.random().toString(36).substring(2, 10);
contextParts.push(`r${randomPart}`);
// Join all parts with a separator
return contextParts.join("-");
}
/**
* Generates a semantic ID that incorporates meaningful information
* @param options Semantic options for ID generation
* @returns A semantic unique ID
*/
static semantic(options) {
// Create semantic parts array
const parts = [];
// Add prefix if provided
if (options.prefix) {
parts.push(options.prefix.toUpperCase());
}
// Add region code if provided
if (options.region) {
parts.push(options.region.toUpperCase());
}
// Add department code if provided
if (options.department) {
parts.push(options.department.toUpperCase());
}
// Add year if provided
if (options.year) {