Browse Source
- Revert ProfileService from broken /api/partner/userProfile endpoint to working /api/partner/userProfileForIssuer/${did} - Fix location data display by restoring single profile object response parsing - Remove complex array handling logic that was unnecessary for current user profiles - Restore original working functionality that was broken by recent refactoring Problem: Recent ProfileService creation changed endpoint from working userProfileForIssuer/${did} to broken userProfile (list endpoint), causing location data to not display properly. Solution: Revert to original working endpoint and response parsing logic that returns single profile objects with location data instead of arrays of all profiles. Files changed: - src/services/ProfileService.ts: Restore working endpoint and simplify response parsing Testing: Profile loading now works correctly for both existing and new profiles, location data is properly extracted and displayed, maps render correctly.pull/170/head
4 changed files with 1204 additions and 196 deletions
@ -0,0 +1,207 @@ |
|||||
|
/** |
||||
|
* Service Initialization Manager |
||||
|
* |
||||
|
* Manages the proper initialization order of services to prevent race conditions |
||||
|
* and ensure dependencies are available when services are created. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @since 2025-08-25 |
||||
|
*/ |
||||
|
|
||||
|
import { logger } from "../utils/logger"; |
||||
|
|
||||
|
/** |
||||
|
* Service initialization status tracking |
||||
|
*/ |
||||
|
interface ServiceStatus { |
||||
|
name: string; |
||||
|
initialized: boolean; |
||||
|
dependencies: string[]; |
||||
|
error?: string; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Service initialization manager to prevent race conditions |
||||
|
*/ |
||||
|
export class ServiceInitializationManager { |
||||
|
private static instance: ServiceInitializationManager; |
||||
|
private serviceStatuses = new Map<string, ServiceStatus>(); |
||||
|
private initializationPromise: Promise<void> | null = null; |
||||
|
|
||||
|
private constructor() {} |
||||
|
|
||||
|
/** |
||||
|
* Get singleton instance |
||||
|
*/ |
||||
|
static getInstance(): ServiceInitializationManager { |
||||
|
if (!ServiceInitializationManager.instance) { |
||||
|
ServiceInitializationManager.instance = |
||||
|
new ServiceInitializationManager(); |
||||
|
} |
||||
|
return ServiceInitializationManager.instance; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Register a service that needs initialization |
||||
|
*/ |
||||
|
registerService(name: string, dependencies: string[] = []): void { |
||||
|
this.serviceStatuses.set(name, { |
||||
|
name, |
||||
|
initialized: false, |
||||
|
dependencies, |
||||
|
}); |
||||
|
|
||||
|
logger.debug("[ServiceInit] 🔧 Service registered:", { |
||||
|
name, |
||||
|
dependencies, |
||||
|
totalServices: this.serviceStatuses.size, |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Mark a service as initialized |
||||
|
*/ |
||||
|
markInitialized(name: string): void { |
||||
|
const status = this.serviceStatuses.get(name); |
||||
|
if (status) { |
||||
|
status.initialized = true; |
||||
|
logger.debug("[ServiceInit] ✅ Service initialized:", { |
||||
|
name, |
||||
|
totalInitialized: this.getInitializedCount(), |
||||
|
totalServices: this.serviceStatuses.size, |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Mark a service as failed |
||||
|
*/ |
||||
|
markFailed(name: string, error: string): void { |
||||
|
const status = this.serviceStatuses.get(name); |
||||
|
if (status) { |
||||
|
status.error = error; |
||||
|
logger.error("[ServiceInit] ❌ Service failed:", { |
||||
|
name, |
||||
|
error, |
||||
|
totalFailed: this.getFailedCount(), |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get count of initialized services |
||||
|
*/ |
||||
|
private getInitializedCount(): number { |
||||
|
return Array.from(this.serviceStatuses.values()).filter( |
||||
|
(s) => s.initialized, |
||||
|
).length; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get count of failed services |
||||
|
*/ |
||||
|
private getFailedCount(): number { |
||||
|
return Array.from(this.serviceStatuses.values()).filter((s) => s.error) |
||||
|
.length; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Wait for all services to be initialized |
||||
|
*/ |
||||
|
async waitForInitialization(): Promise<void> { |
||||
|
if (this.initializationPromise) { |
||||
|
return this.initializationPromise; |
||||
|
} |
||||
|
|
||||
|
this.initializationPromise = new Promise((resolve, reject) => { |
||||
|
const checkInterval = setInterval(() => { |
||||
|
const totalServices = this.serviceStatuses.size; |
||||
|
const initializedCount = this.getInitializedCount(); |
||||
|
const failedCount = this.getFailedCount(); |
||||
|
|
||||
|
logger.debug("[ServiceInit] 🔍 Initialization progress:", { |
||||
|
totalServices, |
||||
|
initializedCount, |
||||
|
failedCount, |
||||
|
remaining: totalServices - initializedCount - failedCount, |
||||
|
}); |
||||
|
|
||||
|
if (failedCount > 0) { |
||||
|
clearInterval(checkInterval); |
||||
|
const failedServices = Array.from(this.serviceStatuses.values()) |
||||
|
.filter((s) => s.error) |
||||
|
.map((s) => `${s.name}: ${s.error}`); |
||||
|
|
||||
|
const error = new Error( |
||||
|
`Service initialization failed: ${failedServices.join(", ")}`, |
||||
|
); |
||||
|
logger.error("[ServiceInit] ❌ Initialization failed:", error); |
||||
|
reject(error); |
||||
|
} else if (initializedCount === totalServices) { |
||||
|
clearInterval(checkInterval); |
||||
|
logger.info( |
||||
|
"[ServiceInit] 🎉 All services initialized successfully:", |
||||
|
{ |
||||
|
totalServices, |
||||
|
initializedCount, |
||||
|
}, |
||||
|
); |
||||
|
resolve(); |
||||
|
} |
||||
|
}, 100); |
||||
|
|
||||
|
// Timeout after 30 seconds
|
||||
|
setTimeout(() => { |
||||
|
clearInterval(checkInterval); |
||||
|
const error = new Error( |
||||
|
"Service initialization timeout after 30 seconds", |
||||
|
); |
||||
|
logger.error("[ServiceInit] ⏰ Initialization timeout:", error); |
||||
|
reject(error); |
||||
|
}, 30000); |
||||
|
}); |
||||
|
|
||||
|
return this.initializationPromise; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get initialization status summary |
||||
|
*/ |
||||
|
getStatusSummary(): { |
||||
|
total: number; |
||||
|
initialized: number; |
||||
|
failed: number; |
||||
|
pending: number; |
||||
|
services: ServiceStatus[]; |
||||
|
} { |
||||
|
const services = Array.from(this.serviceStatuses.values()); |
||||
|
const total = services.length; |
||||
|
const initialized = services.filter((s) => s.initialized).length; |
||||
|
const failed = services.filter((s) => s.error).length; |
||||
|
const pending = total - initialized - failed; |
||||
|
|
||||
|
return { |
||||
|
total, |
||||
|
initialized, |
||||
|
failed, |
||||
|
pending, |
||||
|
services, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Reset the manager (useful for testing) |
||||
|
*/ |
||||
|
reset(): void { |
||||
|
this.serviceStatuses.clear(); |
||||
|
this.initializationPromise = null; |
||||
|
logger.debug("[ServiceInit] 🔄 Manager reset"); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convenience function to get the service initialization manager |
||||
|
*/ |
||||
|
export const getServiceInitManager = (): ServiceInitializationManager => { |
||||
|
return ServiceInitializationManager.getInstance(); |
||||
|
}; |
@ -0,0 +1,298 @@ |
|||||
|
/** |
||||
|
* Standardized Error Handler |
||||
|
* |
||||
|
* Provides consistent error handling patterns across the TimeSafari codebase |
||||
|
* to improve debugging, user experience, and maintainability. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @since 2025-08-25 |
||||
|
*/ |
||||
|
|
||||
|
import { AxiosError } from "axios"; |
||||
|
import { logger } from "./logger"; |
||||
|
|
||||
|
/** |
||||
|
* Standard error context for consistent logging |
||||
|
*/ |
||||
|
export interface ErrorContext { |
||||
|
component: string; |
||||
|
operation: string; |
||||
|
timestamp: string; |
||||
|
[key: string]: unknown; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Enhanced error information for better debugging |
||||
|
*/ |
||||
|
export interface EnhancedErrorInfo { |
||||
|
errorType: "AxiosError" | "NetworkError" | "ValidationError" | "UnknownError"; |
||||
|
status?: number; |
||||
|
statusText?: string; |
||||
|
errorData?: unknown; |
||||
|
errorMessage: string; |
||||
|
errorStack?: string; |
||||
|
requestContext?: { |
||||
|
url?: string; |
||||
|
method?: string; |
||||
|
headers?: Record<string, unknown>; |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Standardized error handler for API operations |
||||
|
* |
||||
|
* @param error - The error that occurred |
||||
|
* @param context - Context information about the operation |
||||
|
* @param operation - Description of the operation being performed |
||||
|
* @returns Enhanced error information for consistent handling |
||||
|
*/ |
||||
|
export function handleApiError( |
||||
|
error: unknown, |
||||
|
context: ErrorContext, |
||||
|
operation: string, |
||||
|
): EnhancedErrorInfo { |
||||
|
const baseContext = { |
||||
|
...context, |
||||
|
operation, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}; |
||||
|
|
||||
|
if (error instanceof AxiosError) { |
||||
|
const axiosError = error as AxiosError; |
||||
|
const status = axiosError.response?.status; |
||||
|
const statusText = axiosError.response?.statusText; |
||||
|
const errorData = axiosError.response?.data; |
||||
|
|
||||
|
const enhancedError: EnhancedErrorInfo = { |
||||
|
errorType: "AxiosError", |
||||
|
status, |
||||
|
statusText, |
||||
|
errorData, |
||||
|
errorMessage: axiosError.message, |
||||
|
errorStack: axiosError.stack, |
||||
|
requestContext: { |
||||
|
url: axiosError.config?.url, |
||||
|
method: axiosError.config?.method, |
||||
|
headers: axiosError.config?.headers, |
||||
|
}, |
||||
|
}; |
||||
|
|
||||
|
// Log with consistent format
|
||||
|
logger.error( |
||||
|
`[${context.component}] ❌ ${operation} failed (AxiosError):`, |
||||
|
{ |
||||
|
...baseContext, |
||||
|
...enhancedError, |
||||
|
}, |
||||
|
); |
||||
|
|
||||
|
return enhancedError; |
||||
|
} |
||||
|
|
||||
|
if (error instanceof Error) { |
||||
|
const enhancedError: EnhancedErrorInfo = { |
||||
|
errorType: "UnknownError", |
||||
|
errorMessage: error.message, |
||||
|
errorStack: error.stack, |
||||
|
}; |
||||
|
|
||||
|
logger.error(`[${context.component}] ❌ ${operation} failed (Error):`, { |
||||
|
...baseContext, |
||||
|
...enhancedError, |
||||
|
}); |
||||
|
|
||||
|
return enhancedError; |
||||
|
} |
||||
|
|
||||
|
// Handle unknown error types
|
||||
|
const enhancedError: EnhancedErrorInfo = { |
||||
|
errorType: "UnknownError", |
||||
|
errorMessage: String(error), |
||||
|
}; |
||||
|
|
||||
|
logger.error(`[${context.component}] ❌ ${operation} failed (Unknown):`, { |
||||
|
...baseContext, |
||||
|
...enhancedError, |
||||
|
}); |
||||
|
|
||||
|
return enhancedError; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Extract human-readable error message from various error response formats |
||||
|
* |
||||
|
* @param errorData - Error response data |
||||
|
* @returns Human-readable error message |
||||
|
*/ |
||||
|
export function extractErrorMessage(errorData: unknown): string { |
||||
|
if (typeof errorData === "string") { |
||||
|
return errorData; |
||||
|
} |
||||
|
|
||||
|
if (typeof errorData === "object" && errorData !== null) { |
||||
|
const obj = errorData as Record<string, unknown>; |
||||
|
|
||||
|
// Try common error message fields
|
||||
|
if (obj.message && typeof obj.message === "string") { |
||||
|
return obj.message; |
||||
|
} |
||||
|
|
||||
|
if (obj.error && typeof obj.error === "string") { |
||||
|
return obj.error; |
||||
|
} |
||||
|
|
||||
|
if (obj.detail && typeof obj.detail === "string") { |
||||
|
return obj.detail; |
||||
|
} |
||||
|
|
||||
|
if (obj.reason && typeof obj.reason === "string") { |
||||
|
return obj.reason; |
||||
|
} |
||||
|
|
||||
|
// Fallback to stringified object
|
||||
|
return JSON.stringify(errorData); |
||||
|
} |
||||
|
|
||||
|
return String(errorData); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create user-friendly error message from enhanced error info |
||||
|
* |
||||
|
* @param errorInfo - Enhanced error information |
||||
|
* @param fallbackMessage - Fallback message if error details are insufficient |
||||
|
* @returns User-friendly error message |
||||
|
*/ |
||||
|
export function createUserMessage( |
||||
|
errorInfo: EnhancedErrorInfo, |
||||
|
fallbackMessage: string, |
||||
|
): string { |
||||
|
if (errorInfo.errorType === "AxiosError") { |
||||
|
const status = errorInfo.status; |
||||
|
const statusText = errorInfo.statusText; |
||||
|
const errorMessage = extractErrorMessage(errorInfo.errorData); |
||||
|
|
||||
|
if (status && statusText) { |
||||
|
if (errorMessage && errorMessage !== "{}") { |
||||
|
return `${fallbackMessage}: ${status} ${statusText} - ${errorMessage}`; |
||||
|
} |
||||
|
return `${fallbackMessage}: ${status} ${statusText}`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
if ( |
||||
|
errorInfo.errorMessage && |
||||
|
errorInfo.errorMessage !== "Request failed with status code 0" |
||||
|
) { |
||||
|
return `${fallbackMessage}: ${errorInfo.errorMessage}`; |
||||
|
} |
||||
|
|
||||
|
return fallbackMessage; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Handle specific HTTP status codes with appropriate user messages |
||||
|
* |
||||
|
* @param status - HTTP status code |
||||
|
* @param errorData - Error response data |
||||
|
* @param operation - Description of the operation |
||||
|
* @returns User-friendly error message |
||||
|
*/ |
||||
|
export function handleHttpStatus( |
||||
|
status: number, |
||||
|
errorData: unknown, |
||||
|
operation: string, |
||||
|
): string { |
||||
|
const errorMessage = extractErrorMessage(errorData); |
||||
|
|
||||
|
switch (status) { |
||||
|
case 400: |
||||
|
return errorMessage || `${operation} failed: Bad request`; |
||||
|
case 401: |
||||
|
return `${operation} failed: Authentication required`; |
||||
|
case 403: |
||||
|
return `${operation} failed: Access denied`; |
||||
|
case 404: |
||||
|
return errorMessage || `${operation} failed: Resource not found`; |
||||
|
case 409: |
||||
|
return errorMessage || `${operation} failed: Conflict with existing data`; |
||||
|
case 422: |
||||
|
return errorMessage || `${operation} failed: Validation error`; |
||||
|
case 429: |
||||
|
return `${operation} failed: Too many requests. Please try again later.`; |
||||
|
case 500: |
||||
|
return `${operation} failed: Server error. Please try again later.`; |
||||
|
case 502: |
||||
|
case 503: |
||||
|
case 504: |
||||
|
return `${operation} failed: Service temporarily unavailable. Please try again later.`; |
||||
|
default: |
||||
|
return errorMessage || `${operation} failed: HTTP ${status}`; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if an error is a network-related error |
||||
|
* |
||||
|
* @param error - The error to check |
||||
|
* @returns True if the error is network-related |
||||
|
*/ |
||||
|
export function isNetworkError(error: unknown): boolean { |
||||
|
if (error instanceof AxiosError) { |
||||
|
return !error.response && !error.request; |
||||
|
} |
||||
|
|
||||
|
if (error instanceof Error) { |
||||
|
const message = error.message.toLowerCase(); |
||||
|
return ( |
||||
|
message.includes("network") || |
||||
|
message.includes("timeout") || |
||||
|
message.includes("connection") || |
||||
|
message.includes("fetch") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Check if an error is a timeout error |
||||
|
* |
||||
|
* @param error - The error to check |
||||
|
* @returns True if the error is a timeout |
||||
|
*/ |
||||
|
export function isTimeoutError(error: unknown): boolean { |
||||
|
if (error instanceof AxiosError) { |
||||
|
return ( |
||||
|
error.code === "ECONNABORTED" || |
||||
|
error.message.toLowerCase().includes("timeout") |
||||
|
); |
||||
|
} |
||||
|
|
||||
|
if (error instanceof Error) { |
||||
|
return error.message.toLowerCase().includes("timeout"); |
||||
|
} |
||||
|
|
||||
|
return false; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create standardized error context for components |
||||
|
* |
||||
|
* @param component - Component name |
||||
|
* @param operation - Operation being performed |
||||
|
* @param additionalContext - Additional context information |
||||
|
* @returns Standardized error context |
||||
|
*/ |
||||
|
export function createErrorContext( |
||||
|
component: string, |
||||
|
operation: string, |
||||
|
additionalContext: Record<string, unknown> = {}, |
||||
|
): ErrorContext { |
||||
|
return { |
||||
|
component, |
||||
|
operation, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
...additionalContext, |
||||
|
}; |
||||
|
} |
@ -0,0 +1,482 @@ |
|||||
|
/** |
||||
|
* Performance Optimizer |
||||
|
* |
||||
|
* Provides utilities for optimizing API calls, database queries, and component |
||||
|
* rendering to improve TimeSafari application performance. |
||||
|
* |
||||
|
* @author Matthew Raymer |
||||
|
* @since 2025-08-25 |
||||
|
*/ |
||||
|
|
||||
|
import { logger } from "./logger"; |
||||
|
|
||||
|
/** |
||||
|
* Batch operation configuration |
||||
|
*/ |
||||
|
export interface BatchConfig { |
||||
|
maxBatchSize: number; |
||||
|
maxWaitTime: number; |
||||
|
retryAttempts: number; |
||||
|
retryDelay: number; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Default batch configuration |
||||
|
*/ |
||||
|
export const DEFAULT_BATCH_CONFIG: BatchConfig = { |
||||
|
maxBatchSize: 10, |
||||
|
maxWaitTime: 100, // milliseconds
|
||||
|
retryAttempts: 3, |
||||
|
retryDelay: 1000, // milliseconds
|
||||
|
}; |
||||
|
|
||||
|
/** |
||||
|
* Batched operation item |
||||
|
*/ |
||||
|
export interface BatchItem<T, R> { |
||||
|
id: string; |
||||
|
data: T; |
||||
|
resolve: (value: R) => void; |
||||
|
reject: (error: Error) => void; |
||||
|
timestamp: number; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Batch processor for API operations |
||||
|
* |
||||
|
* Groups multiple similar operations into batches to reduce |
||||
|
* the number of API calls and improve performance. |
||||
|
*/ |
||||
|
export class BatchProcessor<T, R> { |
||||
|
private items: BatchItem<T, R>[] = []; |
||||
|
private timer: NodeJS.Timeout | null = null; |
||||
|
private processing = false; |
||||
|
private config: BatchConfig; |
||||
|
|
||||
|
constructor( |
||||
|
private batchHandler: (items: T[]) => Promise<R[]>, |
||||
|
private itemIdExtractor: (item: T) => string, |
||||
|
config: Partial<BatchConfig> = {}, |
||||
|
) { |
||||
|
this.config = { ...DEFAULT_BATCH_CONFIG, ...config }; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Add an item to the batch |
||||
|
* |
||||
|
* @param data - Data to process |
||||
|
* @returns Promise that resolves when the item is processed |
||||
|
*/ |
||||
|
async add(data: T): Promise<R> { |
||||
|
return new Promise((resolve, reject) => { |
||||
|
const item: BatchItem<T, R> = { |
||||
|
id: this.itemIdExtractor(data), |
||||
|
data, |
||||
|
resolve, |
||||
|
reject, |
||||
|
timestamp: Date.now(), |
||||
|
}; |
||||
|
|
||||
|
this.items.push(item); |
||||
|
|
||||
|
// Start timer if this is the first item
|
||||
|
if (this.items.length === 1) { |
||||
|
this.startTimer(); |
||||
|
} |
||||
|
|
||||
|
// Process immediately if batch is full
|
||||
|
if (this.items.length >= this.config.maxBatchSize) { |
||||
|
this.processBatch(); |
||||
|
} |
||||
|
}); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Start the batch timer |
||||
|
*/ |
||||
|
private startTimer(): void { |
||||
|
if (this.timer) { |
||||
|
clearTimeout(this.timer); |
||||
|
} |
||||
|
|
||||
|
this.timer = setTimeout(() => { |
||||
|
this.processBatch(); |
||||
|
}, this.config.maxWaitTime); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Process the current batch |
||||
|
*/ |
||||
|
private async processBatch(): Promise<void> { |
||||
|
if (this.processing || this.items.length === 0) { |
||||
|
return; |
||||
|
} |
||||
|
|
||||
|
this.processing = true; |
||||
|
|
||||
|
// Clear timer
|
||||
|
if (this.timer) { |
||||
|
clearTimeout(this.timer); |
||||
|
this.timer = null; |
||||
|
} |
||||
|
|
||||
|
// Get current batch
|
||||
|
const currentItems = [...this.items]; |
||||
|
this.items = []; |
||||
|
|
||||
|
try { |
||||
|
logger.debug("[BatchProcessor] 🔄 Processing batch:", { |
||||
|
batchSize: currentItems.length, |
||||
|
itemIds: currentItems.map((item) => item.id), |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
|
||||
|
// Process batch
|
||||
|
const results = await this.batchHandler( |
||||
|
currentItems.map((item) => item.data), |
||||
|
); |
||||
|
|
||||
|
// Map results back to items
|
||||
|
const resultMap = new Map<string, R>(); |
||||
|
results.forEach((result, index) => { |
||||
|
const item = currentItems[index]; |
||||
|
if (item) { |
||||
|
resultMap.set(item.id, result); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
// Resolve promises
|
||||
|
currentItems.forEach((item) => { |
||||
|
const result = resultMap.get(item.id); |
||||
|
if (result !== undefined) { |
||||
|
item.resolve(result); |
||||
|
} else { |
||||
|
item.reject(new Error(`No result found for item ${item.id}`)); |
||||
|
} |
||||
|
}); |
||||
|
|
||||
|
logger.debug("[BatchProcessor] ✅ Batch processed successfully:", { |
||||
|
batchSize: currentItems.length, |
||||
|
resultsCount: results.length, |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
} catch (error) { |
||||
|
logger.error("[BatchProcessor] ❌ Batch processing failed:", { |
||||
|
batchSize: currentItems.length, |
||||
|
error: error instanceof Error ? error.message : String(error), |
||||
|
timestamp: new Date().toISOString(), |
||||
|
}); |
||||
|
|
||||
|
// Reject all items in the batch
|
||||
|
currentItems.forEach((item) => { |
||||
|
item.reject(error instanceof Error ? error : new Error(String(error))); |
||||
|
}); |
||||
|
} finally { |
||||
|
this.processing = false; |
||||
|
|
||||
|
// Start timer for remaining items if any
|
||||
|
if (this.items.length > 0) { |
||||
|
this.startTimer(); |
||||
|
} |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get current batch status |
||||
|
*/ |
||||
|
getStatus(): { |
||||
|
pendingItems: number; |
||||
|
isProcessing: boolean; |
||||
|
hasTimer: boolean; |
||||
|
} { |
||||
|
return { |
||||
|
pendingItems: this.items.length, |
||||
|
isProcessing: this.processing, |
||||
|
hasTimer: this.timer !== null, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear all pending items |
||||
|
*/ |
||||
|
clear(): void { |
||||
|
if (this.timer) { |
||||
|
clearTimeout(this.timer); |
||||
|
this.timer = null; |
||||
|
} |
||||
|
|
||||
|
// Reject all pending items
|
||||
|
this.items.forEach((item) => { |
||||
|
item.reject(new Error("Batch processor cleared")); |
||||
|
}); |
||||
|
|
||||
|
this.items = []; |
||||
|
this.processing = false; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Database query optimizer |
||||
|
* |
||||
|
* Provides utilities for optimizing database queries and reducing |
||||
|
* the number of database operations. |
||||
|
*/ |
||||
|
export class DatabaseOptimizer { |
||||
|
/** |
||||
|
* Batch multiple SELECT queries into a single query |
||||
|
* |
||||
|
* @param baseQuery - Base SELECT query |
||||
|
* @param ids - Array of IDs to query |
||||
|
* @param idColumn - Name of the ID column |
||||
|
* @returns Optimized query string |
||||
|
*/ |
||||
|
static batchSelectQuery( |
||||
|
baseQuery: string, |
||||
|
ids: (string | number)[], |
||||
|
idColumn: string, |
||||
|
): string { |
||||
|
if (ids.length === 0) { |
||||
|
return baseQuery; |
||||
|
} |
||||
|
|
||||
|
if (ids.length === 1) { |
||||
|
return `${baseQuery} WHERE ${idColumn} = ?`; |
||||
|
} |
||||
|
|
||||
|
const placeholders = ids.map(() => "?").join(", "); |
||||
|
return `${baseQuery} WHERE ${idColumn} IN (${placeholders})`; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Create a query plan for multiple operations |
||||
|
* |
||||
|
* @param operations - Array of database operations |
||||
|
* @returns Optimized query plan |
||||
|
*/ |
||||
|
static createQueryPlan( |
||||
|
operations: Array<{ |
||||
|
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE"; |
||||
|
table: string; |
||||
|
priority: number; |
||||
|
}>, |
||||
|
): Array<{ |
||||
|
type: "SELECT" | "INSERT" | "UPDATE" | "DELETE"; |
||||
|
table: string; |
||||
|
priority: number; |
||||
|
batchable: boolean; |
||||
|
}> { |
||||
|
return operations |
||||
|
.map((op) => ({ |
||||
|
...op, |
||||
|
batchable: op.type === "SELECT" || op.type === "INSERT", |
||||
|
})) |
||||
|
.sort((a, b) => { |
||||
|
// Sort by priority first, then by type
|
||||
|
if (a.priority !== b.priority) { |
||||
|
return b.priority - a.priority; |
||||
|
} |
||||
|
|
||||
|
// SELECT operations first, then INSERT, UPDATE, DELETE
|
||||
|
const typeOrder = { SELECT: 0, INSERT: 1, UPDATE: 2, DELETE: 3 }; |
||||
|
return typeOrder[a.type] - typeOrder[b.type]; |
||||
|
}); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Component rendering optimizer |
||||
|
* |
||||
|
* Provides utilities for optimizing Vue component rendering |
||||
|
* and reducing unnecessary re-renders. |
||||
|
*/ |
||||
|
export class ComponentOptimizer { |
||||
|
/** |
||||
|
* Debounce function calls to prevent excessive execution |
||||
|
* |
||||
|
* @param func - Function to debounce |
||||
|
* @param wait - Wait time in milliseconds |
||||
|
* @returns Debounced function |
||||
|
*/ |
||||
|
static debounce<T extends (...args: unknown[]) => unknown>( |
||||
|
func: T, |
||||
|
wait: number, |
||||
|
): (...args: Parameters<T>) => void { |
||||
|
let timeout: NodeJS.Timeout | null = null; |
||||
|
|
||||
|
return (...args: Parameters<T>) => { |
||||
|
if (timeout) { |
||||
|
clearTimeout(timeout); |
||||
|
} |
||||
|
|
||||
|
timeout = setTimeout(() => { |
||||
|
func(...args); |
||||
|
}, wait); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Throttle function calls to limit execution frequency |
||||
|
* |
||||
|
* @param func - Function to throttle |
||||
|
* @param limit - Time limit in milliseconds |
||||
|
* @returns Throttled function |
||||
|
*/ |
||||
|
static throttle<T extends (...args: unknown[]) => unknown>( |
||||
|
func: T, |
||||
|
limit: number, |
||||
|
): (...args: Parameters<T>) => void { |
||||
|
let inThrottle = false; |
||||
|
|
||||
|
return (...args: Parameters<T>) => { |
||||
|
if (!inThrottle) { |
||||
|
func(...args); |
||||
|
inThrottle = true; |
||||
|
setTimeout(() => { |
||||
|
inThrottle = false; |
||||
|
}, limit); |
||||
|
} |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Memoize function results to avoid redundant computation |
||||
|
* |
||||
|
* @param func - Function to memoize |
||||
|
* @param keyGenerator - Function to generate cache keys |
||||
|
* @returns Memoized function |
||||
|
*/ |
||||
|
static memoize<T extends (...args: unknown[]) => unknown, K>( |
||||
|
func: T, |
||||
|
keyGenerator: (...args: Parameters<T>) => K, |
||||
|
): T { |
||||
|
const cache = new Map<K, unknown>(); |
||||
|
|
||||
|
return ((...args: Parameters<T>) => { |
||||
|
const key = keyGenerator(...args); |
||||
|
|
||||
|
if (cache.has(key)) { |
||||
|
return cache.get(key); |
||||
|
} |
||||
|
|
||||
|
const result = func(...args); |
||||
|
cache.set(key, result); |
||||
|
return result; |
||||
|
}) as T; |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Performance monitoring utility |
||||
|
* |
||||
|
* Tracks and reports performance metrics for optimization analysis. |
||||
|
*/ |
||||
|
export class PerformanceMonitor { |
||||
|
private static instance: PerformanceMonitor; |
||||
|
private metrics = new Map< |
||||
|
string, |
||||
|
Array<{ timestamp: number; duration: number }> |
||||
|
>(); |
||||
|
|
||||
|
private constructor() {} |
||||
|
|
||||
|
/** |
||||
|
* Get singleton instance |
||||
|
*/ |
||||
|
static getInstance(): PerformanceMonitor { |
||||
|
if (!PerformanceMonitor.instance) { |
||||
|
PerformanceMonitor.instance = new PerformanceMonitor(); |
||||
|
} |
||||
|
return PerformanceMonitor.instance; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Start timing an operation |
||||
|
* |
||||
|
* @param operationName - Name of the operation |
||||
|
* @returns Function to call when operation completes |
||||
|
*/ |
||||
|
startTiming(operationName: string): () => void { |
||||
|
const startTime = performance.now(); |
||||
|
|
||||
|
return () => { |
||||
|
const duration = performance.now() - startTime; |
||||
|
this.recordMetric(operationName, duration); |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Record a performance metric |
||||
|
* |
||||
|
* @param operationName - Name of the operation |
||||
|
* @param duration - Duration in milliseconds |
||||
|
*/ |
||||
|
private recordMetric(operationName: string, duration: number): void { |
||||
|
if (!this.metrics.has(operationName)) { |
||||
|
this.metrics.set(operationName, []); |
||||
|
} |
||||
|
|
||||
|
const operationMetrics = this.metrics.get(operationName)!; |
||||
|
operationMetrics.push({ |
||||
|
timestamp: Date.now(), |
||||
|
duration, |
||||
|
}); |
||||
|
|
||||
|
// Keep only last 100 metrics per operation
|
||||
|
if (operationMetrics.length > 100) { |
||||
|
operationMetrics.splice(0, operationMetrics.length - 100); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get performance summary for an operation |
||||
|
* |
||||
|
* @param operationName - Name of the operation |
||||
|
* @returns Performance statistics |
||||
|
*/ |
||||
|
getPerformanceSummary(operationName: string): { |
||||
|
count: number; |
||||
|
average: number; |
||||
|
min: number; |
||||
|
max: number; |
||||
|
recentAverage: number; |
||||
|
} | null { |
||||
|
const metrics = this.metrics.get(operationName); |
||||
|
if (!metrics || metrics.length === 0) { |
||||
|
return null; |
||||
|
} |
||||
|
|
||||
|
const durations = metrics.map((m) => m.duration); |
||||
|
const recentMetrics = metrics.slice(-10); // Last 10 metrics
|
||||
|
|
||||
|
return { |
||||
|
count: metrics.length, |
||||
|
average: durations.reduce((a, b) => a + b, 0) / durations.length, |
||||
|
min: Math.min(...durations), |
||||
|
max: Math.max(...durations), |
||||
|
recentAverage: |
||||
|
recentMetrics.reduce((a, b) => a + b.duration, 0) / |
||||
|
recentMetrics.length, |
||||
|
}; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Get all performance metrics |
||||
|
*/ |
||||
|
getAllMetrics(): Map<string, Array<{ timestamp: number; duration: number }>> { |
||||
|
return new Map(this.metrics); |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Clear all performance metrics |
||||
|
*/ |
||||
|
clearMetrics(): void { |
||||
|
this.metrics.clear(); |
||||
|
} |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* Convenience function to get the performance monitor |
||||
|
*/ |
||||
|
export const getPerformanceMonitor = (): PerformanceMonitor => { |
||||
|
return PerformanceMonitor.getInstance(); |
||||
|
}; |
Loading…
Reference in new issue