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