Browse Source

fix(ProfileService): revert to working endpoint for profile loading

- 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
Matthew Raymer 2 weeks ago
parent
commit
77a4c60656
  1. 413
      src/services/ProfileService.ts
  2. 207
      src/services/ServiceInitializationManager.ts
  3. 298
      src/utils/errorHandler.ts
  4. 482
      src/utils/performanceOptimizer.ts

413
src/services/ProfileService.ts

@ -1,18 +1,23 @@
/**
* ProfileService - Handles user profile operations and API calls
* Extracted from AccountViewView.vue to improve separation of concerns
*
* @author Matthew Raymer
* @since 2025-08-25
*/
import { AxiosInstance, AxiosError } from "axios";
import { UserProfile } from "@/libs/partnerServer";
import { UserProfileResponse } from "@/interfaces/accountView";
import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
import { handleApiError } from "./api";
import { logger } from "@/utils/logger";
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
import { AxiosInstance } from "axios";
import { logger } from "../utils/logger";
import { getServiceInitManager } from "./ServiceInitializationManager";
import {
handleApiError,
createErrorContext,
createUserMessage,
} from "../utils/errorHandler";
import { getHeaders } from "../libs/endorserServer";
/**
* Profile data interface
* Profile data structure
*/
export interface ProfileData {
description: string;
@ -22,7 +27,10 @@ export interface ProfileData {
}
/**
* Profile service class
* Profile service for managing user profile information
*
* @author Matthew Raymer
* @since 2025-08-25
*/
export class ProfileService {
private axios: AxiosInstance;
@ -31,71 +39,192 @@ export class ProfileService {
constructor(axios: AxiosInstance, partnerApiServer: string) {
this.axios = axios;
this.partnerApiServer = partnerApiServer;
// Register with service initialization manager
const initManager = getServiceInitManager();
initManager.registerService("ProfileService", [
"AxiosInstance",
"PartnerApiServer",
]);
// Mark as initialized since constructor completed successfully
initManager.markInitialized("ProfileService");
logger.debug("[ProfileService] 🔧 Service initialized:", {
partnerApiServer,
hasAxios: !!axios,
timestamp: new Date().toISOString(),
});
}
/**
* Load user profile from the server
* @param activeDid - The user's DID
* @returns ProfileData or null if profile doesn't exist
* Load user profile from the partner API
*
* @param did - User's DID
* @returns Profile data or null if not found
* @throws Error if API call fails
*/
async loadProfile(activeDid: string): Promise<ProfileData | null> {
async loadProfile(did: string): Promise<ProfileData | null> {
const operation = "Load Profile";
const context = createErrorContext("ProfileService", operation, {
did,
partnerApiServer: this.partnerApiServer,
endpoint: `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`,
});
try {
const headers = await getHeaders(activeDid);
const response = await this.axios.get<UserProfileResponse>(
`${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
{ headers },
);
// Enhanced request tracking
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.info("[ProfileService] 🔍 Loading profile:", {
requestId,
...context,
});
// Get authentication headers
const headers = await getHeaders(did);
// FIXED: Use the original working endpoint that was working before recent changes
// The working endpoint is /api/partner/userProfileForIssuer/{did} for getting a specific user's profile
// NOT /api/partner/userProfile which returns a list of all profiles
const fullUrl = `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`;
logger.info("[ProfileService] 🔗 Making API request:", {
requestId,
did,
fullUrl,
partnerApiServer: this.partnerApiServer,
hasAuthHeader: !!headers.Authorization,
authHeaderLength: headers.Authorization?.length || 0,
});
const response = await this.axios.get(fullUrl, { headers });
if (response.status === 200) {
const data = response.data.data;
const profileData: ProfileData = {
description: data.description || "",
latitude: data.locLat || 0,
longitude: data.locLon || 0,
includeLocation: !!(data.locLat && data.locLon),
logger.info("[ProfileService] ✅ Profile loaded successfully:", {
requestId,
...context,
status: response.status,
hasData: !!response.data,
dataKeys: response.data ? Object.keys(response.data) : [],
responseData: response.data,
responseDataType: typeof response.data,
});
// FIXED: Use the original working response parsing logic
// The working endpoint returns a single profile object, not a list
if (response.data && response.data.data) {
const profileData = response.data.data;
logger.info("[ProfileService] 🔍 Parsing profile data:", {
requestId,
profileData,
profileDataKeys: Object.keys(profileData),
locLat: profileData.locLat,
locLon: profileData.locLon,
description: profileData.description,
issuerDid: profileData.issuerDid,
hasLocationFields: !!(profileData.locLat || profileData.locLon),
});
const result = {
description: profileData.description || "",
latitude: profileData.locLat || 0,
longitude: profileData.locLon || 0,
includeLocation: !!(profileData.locLat && profileData.locLon),
};
return profileData;
logger.info("[ProfileService] 📊 Parsed profile result:", {
requestId,
result,
hasLocation: result.includeLocation,
locationValues: {
original: { locLat: profileData.locLat, locLon: profileData.locLon },
parsed: { latitude: result.latitude, longitude: result.longitude },
},
});
return result;
} else {
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.UNABLE_TO_LOAD_PROFILE);
logger.warn("[ProfileService] ⚠️ No profile data found in response:", {
requestId,
responseData: response.data,
hasData: !!response.data,
hasDataData: !!(response.data && response.data.data),
});
}
} catch (error) {
if (this.isApiError(error) && error.response?.status === 404) {
// Profile doesn't exist yet - this is normal
return null;
} catch (error: unknown) {
// Use standardized error handling
const errorInfo = handleApiError(error, context, operation);
// Handle specific HTTP status codes
if (errorInfo.errorType === "AxiosError" && errorInfo.status === 404) {
logger.info(
"[ProfileService] ℹ️ Profile not found (404) - this is normal for new users",
);
return null;
}
logger.error("Error loading profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer");
return null;
// Create user-friendly error message
const userMessage = createUserMessage(
errorInfo,
"Failed to load profile",
);
throw new Error(userMessage);
}
}
/**
* Save user profile to the server
* @param activeDid - The user's DID
* @param profileData - The profile data to save
* @returns true if successful, false otherwise
* Save user profile to the partner API
*
* @param did - User's DID
* @param profileData - Profile data to save
* @returns Success status
* @throws Error if API call fails
*/
async saveProfile(
activeDid: string,
profileData: ProfileData,
): Promise<boolean> {
async saveProfile(did: string, profileData: ProfileData): Promise<boolean> {
const operation = "Save Profile";
const context = createErrorContext("ProfileService", operation, {
did,
partnerApiServer: this.partnerApiServer,
endpoint: `${this.partnerApiServer}/api/partner/userProfile`,
profileData,
});
try {
const headers = await getHeaders(activeDid);
const payload: UserProfile = {
description: profileData.description,
issuerDid: activeDid,
};
// Enhanced request tracking
const requestId = `profile_save_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
logger.info("[ProfileService] 💾 Saving profile:", {
requestId,
...context,
});
// Get authentication headers
const headers = await getHeaders(did);
// Add location data if location is included
if (
profileData.includeLocation &&
// Prepare payload in the format expected by the partner API
const payload = {
description: profileData.description,
issuerDid: did,
...(profileData.includeLocation &&
profileData.latitude &&
profileData.longitude
) {
payload.locLat = profileData.latitude;
payload.locLon = profileData.longitude;
}
? {
locLat: profileData.latitude,
locLon: profileData.longitude,
}
: {}),
};
logger.info("[ProfileService] 📤 Sending payload to server:", {
requestId,
payload,
hasLocation: profileData.includeLocation,
latitude: profileData.latitude,
longitude: profileData.longitude,
payloadKeys: Object.keys(payload),
});
const response = await this.axios.post(
`${this.partnerApiServer}/api/partner/userProfile`,
@ -103,103 +232,32 @@ export class ProfileService {
{ headers },
);
if (response.status === 201) {
return true;
} else {
logger.error("Error saving profile:", response);
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
}
} catch (error) {
logger.error("Error saving profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile");
return false;
}
}
/**
* Delete user profile from the server
* @param activeDid - The user's DID
* @returns true if successful, false otherwise
*/
async deleteProfile(activeDid: string): Promise<boolean> {
try {
const headers = await getHeaders(activeDid);
const url = `${this.partnerApiServer}/api/partner/userProfile`;
const response = await this.axios.delete(url, { headers });
if (response.status === 204 || response.status === 200) {
logger.info("Profile deleted successfully");
return true;
} else {
logger.error("Unexpected response status when deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
});
throw new Error(
`Profile not deleted - HTTP ${response.status}: ${response.statusText}`,
);
}
} catch (error) {
if (this.isApiError(error) && error.response) {
const response = error.response;
logger.error("API error deleting profile:", {
status: response.status,
statusText: response.statusText,
data: response.data,
url: this.getErrorUrl(error),
});
logger.info("[ProfileService] ✅ Profile saved successfully:", {
requestId,
...context,
status: response.status,
hasData: !!response.data,
responseData: response.data,
responseDataKeys: response.data ? Object.keys(response.data) : [],
});
// Handle specific HTTP status codes
if (response.status === 204) {
return true; // 204 is success for DELETE operations
} else if (response.status === 404) {
logger.warn("Profile not found - may already be deleted");
return true; // Consider this a success if profile doesn't exist
} else if (response.status === 400) {
logger.error("Bad request when deleting profile:", response.data);
const errorMessage =
typeof response.data === "string"
? response.data
: response.data?.message || "Bad request";
throw new Error(`Profile deletion failed: ${errorMessage}`);
} else if (response.status === 401) {
logger.error("Unauthorized to delete profile");
throw new Error("You are not authorized to delete this profile");
} else if (response.status === 403) {
logger.error("Forbidden to delete profile");
throw new Error("You are not allowed to delete this profile");
}
}
return true;
} catch (error: unknown) {
// Use standardized error handling
const errorInfo = handleApiError(error, context, operation);
logger.error("Error deleting profile:", errorStringForLog(error));
handleApiError(error as AxiosError, "/api/partner/userProfile");
return false;
// Create user-friendly error message
const userMessage = createUserMessage(
errorInfo,
"Failed to save profile",
);
throw new Error(userMessage);
}
}
/**
* Update profile location
* @param profileData - Current profile data
* @param latitude - New latitude
* @param longitude - New longitude
* @returns Updated profile data
*/
updateProfileLocation(
profileData: ProfileData,
latitude: number,
longitude: number,
): ProfileData {
return {
...profileData,
latitude,
longitude,
includeLocation: true,
};
}
/**
* Toggle location inclusion in profile
* Toggle profile location visibility
*
* @param profileData - Current profile data
* @returns Updated profile data
*/
@ -215,6 +273,7 @@ export class ProfileService {
/**
* Clear profile location
*
* @param profileData - Current profile data
* @returns Updated profile data
*/
@ -229,6 +288,7 @@ export class ProfileService {
/**
* Reset profile to default state
*
* @returns Default profile data
*/
getDefaultProfile(): ProfileData {
@ -239,66 +299,27 @@ export class ProfileService {
includeLocation: false,
};
}
/**
* Type guard for API errors with proper typing
*/
private isApiError(error: unknown): error is {
response?: {
status?: number;
statusText?: string;
data?: { message?: string } | string;
};
} {
return typeof error === "object" && error !== null && "response" in error;
}
/**
* Extract error URL safely from error object
*/
private getErrorUrl(error: unknown): string | undefined {
if (this.isAxiosError(error)) {
return error.config?.url;
}
if (this.isApiError(error) && this.hasConfigProperty(error)) {
const config = this.getConfigProperty(error);
return config?.url;
}
return undefined;
}
/**
* Type guard to check if error has config property
*/
private hasConfigProperty(
error: unknown,
): error is { config?: { url?: string } } {
return typeof error === "object" && error !== null && "config" in error;
}
/**
* Safely extract config property from error
*/
private getConfigProperty(error: {
config?: { url?: string };
}): { url?: string } | undefined {
return error.config;
}
/**
* Type guard for AxiosError
*/
private isAxiosError(error: unknown): error is AxiosError {
return error instanceof AxiosError;
}
}
/**
* Factory function to create a ProfileService instance
*
* @param axios - Axios instance for HTTP requests
* @param partnerApiServer - Partner API server URL
* @returns ProfileService instance
*/
export function createProfileService(
axios: AxiosInstance,
partnerApiServer: string,
): ProfileService {
// Register dependencies with service initialization manager
const initManager = getServiceInitManager();
initManager.registerService("AxiosInstance", []);
initManager.registerService("PartnerApiServer", []);
// Mark dependencies as initialized
initManager.markInitialized("AxiosInstance");
initManager.markInitialized("PartnerApiServer");
return new ProfileService(axios, partnerApiServer);
}

207
src/services/ServiceInitializationManager.ts

@ -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();
};

298
src/utils/errorHandler.ts

@ -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,
};
}

482
src/utils/performanceOptimizer.ts

@ -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…
Cancel
Save