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.
This commit is contained in:
Matthew Raymer
2025-08-25 13:03:06 +00:00
parent a11443dc3a
commit 77a4c60656
4 changed files with 1208 additions and 200 deletions

298
src/utils/errorHandler.ts Normal file
View File

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

View File

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