UNPKG

nehoid

Version:

Advanced unique ID generation utility with multi-layer encoding, collision detection, and context-aware features

503 lines 21.1 kB
/** * --------------------------------------------------------------------------------------------- * Copyright (c) NEHONIX INC. All rights reserved. * Licensed under the MIT License. See LICENSE in the project root for license information. *-------------------------------------------------------------------------------------------- */ import { Generator } from "./core/generator.js"; import { Validator } from "./core/validator.js"; import { Specialized } from "./core/specialized.js"; import { EncodingPipeline } from "./core/pipeline.js"; import { createMiddleware } from "./integrations/middleware.js"; import { mongooseField, sequelizeField, typeormDecorator, } from "./integrations/database.js"; export { Encoder } from "./core/encoder.js"; export class NehoID { static middleware(...p) { return createMiddleware(...p); } // Core generation methods static generate(options = {}) { const startTime = performance.now(); const id = Generator.generate(options); NehoID.updateStats(startTime); return id; } static async safe(options) { const startTime = performance.now(); try { const id = await Generator.safe(options); NehoID.updateStats(startTime); return id; } catch (error) { NehoID.stats.collisions++; throw error; } } // Specialized generators static uuid() { return Generator.uuid(); } static nanoid(length) { return Generator.nano(length); } static short(length) { return Generator.short(length); } static hex(length) { return Generator.hex(length); } /** * Generates a hierarchical ID with parent-child relationships * @param options Hierarchical ID options * @returns A hierarchical ID */ static hierarchical(options = {}) { return Specialized.hierarchical(options); } /** * Generates a time-ordered ID for chronological sorting * @param options Temporal ID options * @returns A temporal ID with timestamp */ static temporal(options = {}) { return Specialized.temporal(options); } /** * Generates a sequential ID suitable for database use * @param options Sequential ID options * @returns A sequential ID */ static sequential(options) { return Specialized.sequential(options); } // Batch operations static batch(options) { return Generator.batch(options); } // Validation static validate(id, options) { return Validator.validate(id, options); } static validateBatch(ids, options) { return Validator.validateBatch(ids, options); } static healthCheck(id) { return Validator.healthCheck(id); } // Monitoring static startMonitoring() { NehoID.monitoringEnabled = true; } static stopMonitoring() { NehoID.monitoringEnabled = false; } static getStats() { return { ...NehoID.stats }; } /** * Hashes geographic coordinates for privacy * @param latitude Latitude coordinate * @param longitude Longitude coordinate * @returns A hashed string representation of the coordinates */ 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); } static updateStats(startTime) { if (!NehoID.monitoringEnabled) return; NehoID.stats.generated++; const generationTime = performance.now() - startTime; // Update average generation time const prevTotal = parseFloat(NehoID.stats.averageGenerationTime) * (NehoID.stats.generated - 1); NehoID.stats.averageGenerationTime = `${((prevTotal + generationTime) / NehoID.stats.generated).toFixed(2)}ms`; // Update memory usage const used = process.memoryUsage(); NehoID.stats.memoryUsage = `${Math.round((used.heapUsed / 1024 / 1024) * 100) / 100}MB`; } /** * 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 = NehoID.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) { parts.push(options.year.toString()); } else if (options.year === undefined) { // Add current year if not explicitly provided parts.push(new Date().getFullYear().toString()); } // Add custom segments if provided if (options.customSegments) { for (const [key, value] of Object.entries(options.customSegments)) { parts.push(`${key.toUpperCase()}-${value}`); } } // Add a unique identifier at the end const uniquePart = NehoID.nanoid(8); parts.push(uniquePart); // Join all parts with a separator return parts.join("-"); } /** * Migrates IDs from one format to another * @param options Migration options * @returns Promise resolving to an array of migrated IDs */ static migrate(options) { return new Promise((resolve, reject) => { try { const { from, to, preserveOrder = true, batchSize = 100 } = options; // Parse the 'from' format to understand its structure const fromParts = from.split(/[\-_]/); // Parse the 'to' format to understand its structure const toParts = to.split(/[\-_]/); // Validate that the formats have compatible parts if (fromParts.length === 0 || toParts.length === 0) { throw new Error("Invalid format specifications"); } // Function to convert a single ID const convertId = (id) => { // Split the ID into parts based on the 'from' format const idParts = id.split(/[\-_]/); // Validate that the ID has the expected number of parts if (idParts.length !== fromParts.length) { throw new Error(`ID ${id} does not match the 'from' format ${from}`); } // Create a mapping of format parts to actual values const partMap = {}; for (let i = 0; i < fromParts.length; i++) { partMap[fromParts[i]] = idParts[i]; } // Construct the new ID using the 'to' format const newIdParts = toParts.map((part) => { // If the part exists in the mapping, use it if (partMap[part]) { return partMap[part]; } // If the part doesn't exist, generate a new value if (part === "uuid") { return NehoID.uuid(); } else if (part === "nano") { return NehoID.nanoid(); } else if (part === "timestamp") { return Date.now().toString(); } else { // For unknown parts, preserve the format specifier return part; } }); // Join the parts using the separator from the 'to' format const separator = to.includes("-") ? "-" : "_"; return newIdParts.join(separator); }; // Process the IDs in batches if needed const processIds = async (ids) => { const result = []; for (let i = 0; i < ids.length; i += batchSize) { const batch = ids.slice(i, i + batchSize); const convertedBatch = batch.map(convertId); result.push(...convertedBatch); // Allow other operations to proceed between batches if (i + batchSize < ids.length) { await new Promise((resolve) => setTimeout(resolve, 0)); } } return result; }; // Process actual IDs // If no IDs are provided, we'll generate some sample IDs to migrate const sampleIds = []; // Generate sample IDs based on the 'from' format if none are provided if (!options.ids || options.ids.length === 0) { // Generate 10 sample IDs or the specified number const count = options.count || 10; for (let i = 0; i < count; i++) { // Generate a sample ID based on the 'from' format const sampleIdParts = fromParts.map((part) => { if (part === "uuid") { return NehoID.uuid(); } else if (part === "nano") { return NehoID.nanoid(); } else if (part === "timestamp") { return Date.now().toString(); } else if (part === "counter") { return i.toString().padStart(5, "0"); } else { // For unknown parts, use the part name as a placeholder return part; } }); // Join the parts using the separator from the 'from' format const separator = from.includes("-") ? "-" : "_"; sampleIds.push(sampleIdParts.join(separator)); } } else { // Use the provided IDs sampleIds.push(...options.ids); } // Process the IDs processIds(sampleIds) .then((migratedIds) => { resolve(migratedIds); }) .catch((error) => { reject(error); }); } catch (error) { reject(error); } }); } /** * Generates an ID that is compatible with specified platforms * @param options Compatibility options * @returns A compatible ID */ static compatible(options) { const { platform, format, length } = options; // Determine the most restrictive character set based on platforms let allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; // Adjust character set based on platform requirements if (platform.includes("python") || platform.includes("go")) { // Python and Go are more restrictive with certain characters allowedChars = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; } // Further restrict if needed for specific formats if (format === "alphanumeric") { // Keep as is } else if (format === "lowercase") { allowedChars = "abcdefghijklmnopqrstuvwxyz0123456789_"; } else if (format === "uppercase") { allowedChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789_"; } else if (format === "numeric") { allowedChars = "0123456789"; } else if (format === "hex") { allowedChars = "0123456789abcdef"; } // Generate the ID with the specified length and allowed characters let id = ""; const charsLength = allowedChars.length; for (let i = 0; i < length; i++) { id += allowedChars.charAt(Math.floor(Math.random() * charsLength)); } // Ensure the ID doesn't start with a number (important for some languages) if (/^[0-9]/.test(id) && platform.some((p) => ["javascript", "python"].includes(p))) { // Replace the first character with a letter const letters = allowedChars.replace(/[0-9]/g, ""); id = letters.charAt(Math.floor(Math.random() * letters.length)) + id.substring(1); } return id; } } NehoID.monitoringEnabled = false; NehoID.stats = { generated: 0, collisions: 0, averageGenerationTime: "0ms", memoryUsage: "0MB", distributionScore: 1.0, }; // Framework integrations export const NehoIDMiddleware = createMiddleware; export const database = { mongoose: mongooseField, sequelize: sequelizeField, typeorm: typeormDecorator, }; // Export EncodingPipeline class export { EncodingPipeline }; // For CommonJS compatibility, also export as module.exports if available if (typeof module !== "undefined" && module.exports) { module.exports = NehoID; module.exports.default = NehoID; module.exports.NehoID = NehoID; module.exports.middleware = NehoID.middleware; module.exports.database = database; module.exports.EncodingPipeline = EncodingPipeline; } //# sourceMappingURL=index.js.map