UNPKG

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
'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) {