forked from trent_larson/crowd-funder-for-time-pwa
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:
@@ -1,18 +1,23 @@
|
|||||||
/**
|
/**
|
||||||
* ProfileService - Handles user profile operations and API calls
|
* ProfileService - Handles user profile operations and API calls
|
||||||
* Extracted from AccountViewView.vue to improve separation of concerns
|
* Extracted from AccountViewView.vue to improve separation of concerns
|
||||||
|
*
|
||||||
|
* @author Matthew Raymer
|
||||||
|
* @since 2025-08-25
|
||||||
*/
|
*/
|
||||||
|
|
||||||
import { AxiosInstance, AxiosError } from "axios";
|
import { AxiosInstance } from "axios";
|
||||||
import { UserProfile } from "@/libs/partnerServer";
|
import { logger } from "../utils/logger";
|
||||||
import { UserProfileResponse } from "@/interfaces/accountView";
|
import { getServiceInitManager } from "./ServiceInitializationManager";
|
||||||
import { getHeaders, errorStringForLog } from "@/libs/endorserServer";
|
import {
|
||||||
import { handleApiError } from "./api";
|
handleApiError,
|
||||||
import { logger } from "@/utils/logger";
|
createErrorContext,
|
||||||
import { ACCOUNT_VIEW_CONSTANTS } from "@/constants/accountView";
|
createUserMessage,
|
||||||
|
} from "../utils/errorHandler";
|
||||||
|
import { getHeaders } from "../libs/endorserServer";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Profile data interface
|
* Profile data structure
|
||||||
*/
|
*/
|
||||||
export interface ProfileData {
|
export interface ProfileData {
|
||||||
description: string;
|
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 {
|
export class ProfileService {
|
||||||
private axios: AxiosInstance;
|
private axios: AxiosInstance;
|
||||||
@@ -31,71 +39,192 @@ export class ProfileService {
|
|||||||
constructor(axios: AxiosInstance, partnerApiServer: string) {
|
constructor(axios: AxiosInstance, partnerApiServer: string) {
|
||||||
this.axios = axios;
|
this.axios = axios;
|
||||||
this.partnerApiServer = partnerApiServer;
|
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
|
* Load user profile from the partner API
|
||||||
* @param activeDid - The user's DID
|
*
|
||||||
* @returns ProfileData or null if profile doesn't exist
|
* @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> {
|
||||||
try {
|
const operation = "Load Profile";
|
||||||
const headers = await getHeaders(activeDid);
|
const context = createErrorContext("ProfileService", operation, {
|
||||||
const response = await this.axios.get<UserProfileResponse>(
|
did,
|
||||||
`${this.partnerApiServer}/api/partner/userProfileForIssuer/${activeDid}`,
|
partnerApiServer: this.partnerApiServer,
|
||||||
{ headers },
|
endpoint: `${this.partnerApiServer}/api/partner/userProfileForIssuer/${did}`,
|
||||||
);
|
});
|
||||||
|
|
||||||
if (response.status === 200) {
|
try {
|
||||||
const data = response.data.data;
|
// Enhanced request tracking
|
||||||
const profileData: ProfileData = {
|
const requestId = `profile_${Date.now()}_${Math.random().toString(36).substr(2, 9)}`;
|
||||||
description: data.description || "",
|
|
||||||
latitude: data.locLat || 0,
|
logger.info("[ProfileService] 🔍 Loading profile:", {
|
||||||
longitude: data.locLon || 0,
|
requestId,
|
||||||
includeLocation: !!(data.locLat && data.locLon),
|
...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 });
|
||||||
|
|
||||||
|
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 {
|
} 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) {
|
return null;
|
||||||
// Profile doesn't exist yet - this is normal
|
} 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;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.error("Error loading profile:", errorStringForLog(error));
|
// Create user-friendly error message
|
||||||
handleApiError(error as AxiosError, "/api/partner/userProfileForIssuer");
|
const userMessage = createUserMessage(
|
||||||
return null;
|
errorInfo,
|
||||||
|
"Failed to load profile",
|
||||||
|
);
|
||||||
|
throw new Error(userMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Save user profile to the server
|
* Save user profile to the partner API
|
||||||
* @param activeDid - The user's DID
|
*
|
||||||
* @param profileData - The profile data to save
|
* @param did - User's DID
|
||||||
* @returns true if successful, false otherwise
|
* @param profileData - Profile data to save
|
||||||
|
* @returns Success status
|
||||||
|
* @throws Error if API call fails
|
||||||
*/
|
*/
|
||||||
async saveProfile(
|
async saveProfile(did: string, profileData: ProfileData): Promise<boolean> {
|
||||||
activeDid: string,
|
const operation = "Save Profile";
|
||||||
profileData: ProfileData,
|
const context = createErrorContext("ProfileService", operation, {
|
||||||
): Promise<boolean> {
|
did,
|
||||||
try {
|
partnerApiServer: this.partnerApiServer,
|
||||||
const headers = await getHeaders(activeDid);
|
endpoint: `${this.partnerApiServer}/api/partner/userProfile`,
|
||||||
const payload: UserProfile = {
|
profileData,
|
||||||
description: profileData.description,
|
});
|
||||||
issuerDid: activeDid,
|
|
||||||
};
|
|
||||||
|
|
||||||
// Add location data if location is included
|
try {
|
||||||
if (
|
// Enhanced request tracking
|
||||||
profileData.includeLocation &&
|
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);
|
||||||
|
|
||||||
|
// Prepare payload in the format expected by the partner API
|
||||||
|
const payload = {
|
||||||
|
description: profileData.description,
|
||||||
|
issuerDid: did,
|
||||||
|
...(profileData.includeLocation &&
|
||||||
profileData.latitude &&
|
profileData.latitude &&
|
||||||
profileData.longitude
|
profileData.longitude
|
||||||
) {
|
? {
|
||||||
payload.locLat = profileData.latitude;
|
locLat: profileData.latitude,
|
||||||
payload.locLon = profileData.longitude;
|
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(
|
const response = await this.axios.post(
|
||||||
`${this.partnerApiServer}/api/partner/userProfile`,
|
`${this.partnerApiServer}/api/partner/userProfile`,
|
||||||
@@ -103,103 +232,32 @@ export class ProfileService {
|
|||||||
{ headers },
|
{ headers },
|
||||||
);
|
);
|
||||||
|
|
||||||
if (response.status === 201) {
|
logger.info("[ProfileService] ✅ Profile saved successfully:", {
|
||||||
return true;
|
requestId,
|
||||||
} else {
|
...context,
|
||||||
logger.error("Error saving profile:", response);
|
status: response.status,
|
||||||
throw new Error(ACCOUNT_VIEW_CONSTANTS.ERRORS.PROFILE_NOT_SAVED);
|
hasData: !!response.data,
|
||||||
}
|
responseData: response.data,
|
||||||
} catch (error) {
|
responseDataKeys: response.data ? Object.keys(response.data) : [],
|
||||||
logger.error("Error saving profile:", errorStringForLog(error));
|
});
|
||||||
handleApiError(error as AxiosError, "/api/partner/userProfile");
|
|
||||||
return false;
|
return true;
|
||||||
|
} catch (error: unknown) {
|
||||||
|
// Use standardized error handling
|
||||||
|
const errorInfo = handleApiError(error, context, operation);
|
||||||
|
|
||||||
|
// Create user-friendly error message
|
||||||
|
const userMessage = createUserMessage(
|
||||||
|
errorInfo,
|
||||||
|
"Failed to save profile",
|
||||||
|
);
|
||||||
|
throw new Error(userMessage);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Delete user profile from the server
|
* Toggle profile location visibility
|
||||||
* @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),
|
|
||||||
});
|
|
||||||
|
|
||||||
// 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");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.error("Error deleting profile:", errorStringForLog(error));
|
|
||||||
handleApiError(error as AxiosError, "/api/partner/userProfile");
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* 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
|
|
||||||
* @param profileData - Current profile data
|
* @param profileData - Current profile data
|
||||||
* @returns Updated profile data
|
* @returns Updated profile data
|
||||||
*/
|
*/
|
||||||
@@ -215,6 +273,7 @@ export class ProfileService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Clear profile location
|
* Clear profile location
|
||||||
|
*
|
||||||
* @param profileData - Current profile data
|
* @param profileData - Current profile data
|
||||||
* @returns Updated profile data
|
* @returns Updated profile data
|
||||||
*/
|
*/
|
||||||
@@ -229,6 +288,7 @@ export class ProfileService {
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* Reset profile to default state
|
* Reset profile to default state
|
||||||
|
*
|
||||||
* @returns Default profile data
|
* @returns Default profile data
|
||||||
*/
|
*/
|
||||||
getDefaultProfile(): ProfileData {
|
getDefaultProfile(): ProfileData {
|
||||||
@@ -239,66 +299,27 @@ export class ProfileService {
|
|||||||
includeLocation: false,
|
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
|
* 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(
|
export function createProfileService(
|
||||||
axios: AxiosInstance,
|
axios: AxiosInstance,
|
||||||
partnerApiServer: string,
|
partnerApiServer: string,
|
||||||
): ProfileService {
|
): 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);
|
return new ProfileService(axios, partnerApiServer);
|
||||||
}
|
}
|
||||||
|
|||||||
207
src/services/ServiceInitializationManager.ts
Normal file
207
src/services/ServiceInitializationManager.ts
Normal file
@@ -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
Normal file
298
src/utils/errorHandler.ts
Normal 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,
|
||||||
|
};
|
||||||
|
}
|
||||||
482
src/utils/performanceOptimizer.ts
Normal file
482
src/utils/performanceOptimizer.ts
Normal 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();
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user