import { v7 as uuidv7 } from "uuid"; import { environment } from "./environment"; import { Account, Channel, ClusterToken, Environment, errors, Instance, MarketplaceItem, Notification, Organization, Plan, PlanProvider, Platform, Registry, Resource, Service, Tenant, Token, User, UserData, } from "@kumori/aurora-interfaces"; import { loadMarketplaceItemsForTenant } from "./api/marketplace-api-service"; import { getReporting } from "./api/reporting-api-service"; import { parseKeyPath } from "./utils/utils"; import { handleTenantEvent, handleTenantOperationSuccess, } from "./helpers/tenant-helper"; import { handleUserEvent, handleUserOperationError, } from "./helpers/user-helper"; import { handleEnvironmentEvent, handleEnvironmentOperationError, handleEnvironmentOperationSuccess, } from "./helpers/environment-helper"; import { handleRevisionEvent, processRevisionData, } from "./helpers/revision-helper"; import { handleServiceEvent, handleServiceOperationError, handleServiceOperationSuccess, mapChannelsFromApiData, } from "./helpers/service-helper"; import { handleAccountEvent, handleAccountOperationError, handleAccountOperationSuccess, syncAccountStatusFromNotifications, } from "./helpers/account-helper"; import { handleCAEvent, handleCertificateEvent, handleDomainEvent, handlePortEvent, handleSecretEvent, handleVolumeEvent, processResourceResult, setResourceDeleting, } from "./helpers/resource-helper"; import { handlePlanInstanceEvent, updateUserPlansAfterPlanEvent, } from "./helpers/plan-helper"; import { handleRegistryEvent } from "./helpers/registry-helper"; import { handleTokenEvent, handleTokenOperationSuccess, } from "./helpers/token-helper"; import { handleLinkEvent } from "./helpers/link-helper"; import { eventHelper } from "./backend-handler"; import { Revision } from "@kumori/aurora-interfaces/interfaces/revision-interface"; import { requestRevisionData } from "./api/service-api-service"; import { processMarketplaceSchemaResponse } from "./helpers/marketplace-helper"; let WebSocketClass: any; interface WSMessage { messageId: string; payload: any; topic: string; type: "request" | "multipartrequest" | "success" | "error" | "event"; error?: { code?: string; message?: string; content?: string; }; } interface RevisionWs { service: string; revision: Revision; } interface PendingOperation { resolve: Function; reject: Function; action: string; entityType: string; entityName: string; originalData?: any; responsePayload?: any; } if (typeof window === "undefined") { WebSocketClass = require("ws"); } else { WebSocketClass = WebSocket; } interface Role { name: string; instances: Instance[]; logo?: string; category?: string; version?: string; description?: string; resource?: Resource[]; parameters?: { [key: string]: string }[]; registry?: string; imageTag?: string; entrypoint?: string; cmd?: string; scalling?: { cpu: { up: string; down: string; }; memory: { up: string; down: string; }; instances: { max: number; min: number; }; histeresys: string; }; hsize?: number; } /** * Global WebSocket client instance and pending requests */ let wsConnection: WebSocket | null = null; let pendingRequests = new Map(); let purgingEnvironments = new Set(); let currentToken: string | null = null; let connectionPromise: Promise | null = null; let userData: UserData = { id: "", name: "", surname: "", provider: [], notificationsEnabled: "", organizations: [], clusterTokens: [], tokens: [], tenants: [], axebowPlan: "freemium", companyName: "", rol: "", }; let user: User = new User(userData); let tenantsMap = new Map(); let clusterTokensMap = new Map(); let servicesMap = new Map(); let organizationsMap = new Map(); let environmentsMap = new Map(); let accountsMap = new Map(); let revisionsMap = new Map(); let roleMap = new Map(); let tokenMap = new Map(); let platformInfo: Platform | null = null; let isPlatformInfoLoading = false; let isPlatformInfoLoaded = false; const pendingDomains: Resource[] = []; let globalEntityName: string | null = null; let pendingEnvironments: Array<{ tenant: string; env: Environment }> = []; let pendingRevisionErrors: Array<{ service: string; revision: Revision }> = []; let pendingCloudProviderUpdates: Array<{ environmentId: string; accountId: string; }> = []; let plansMap = new Map(); let pendingRegistries: Array<{ tenant: string; registry: Registry }> = []; let secretsMap = new Map(); let tempProviders: Record = {}; let pendingProjects: Array<{ tenant: string; project: string }> = []; let isLoadingReporting = false; const REPORTING_ITERATIONS = 5; const REPORTING_INTERVAL = 1000; let hasLoadedReportingOnce = false; let recentlyUpdatedServices = new Map(); let pendingTenantStatus = new Map(); /** * Helper function to safely stringify error objects */ const safeStringifyError = (error: any): string => { if (typeof error === "string") { return error; } if (error && typeof error === "object") { try { if (error.message) return error.message; if (error.error) { if (typeof error.error === "string") return error.error; if (error.error.message) return error.error.message; if (error.error.content) return error.error.content; if (error.error.description) return error.error.description; return JSON.stringify(error.error); } if (error.content) return error.content; if (error.description) return error.description; return JSON.stringify(error); } catch (stringifyError) { return `Error object could not be stringified: ${error.toString()}`; } } return String(error); }; /** * Initialize global WebSocket connection * @param token Authorization token * @returns Promise that resolves to WebSocket instance */ export const initializeGlobalWebSocketClient = async ( token: string, type?: string, name?: string, previousData?: UserData, ): Promise => { userData.notifications = previousData?.notifications; globalEntityName = name || null; if ( wsConnection && wsConnection.readyState === WebSocket.OPEN && currentToken === token ) { return wsConnection; } if (connectionPromise) { try { return await connectionPromise; } catch (error) { connectionPromise = null; } } if (wsConnection && currentToken !== token) { wsConnection.close(); wsConnection = null; platformInfo = null; isPlatformInfoLoaded = false; isPlatformInfoLoading = false; } const baseUrl = environment.apiServer.baseUrl.replace("http", "ws"); const wsUrl = `${baseUrl}/api/${environment.apiServer.apiVersion}/ws`; connectionPromise = new Promise((resolve, reject) => { try { if (typeof window === "undefined") { wsConnection = new WebSocketClass(wsUrl, { headers: { // authorization: `Bearer ${token}`, }, credentials: "include", }); } else { wsConnection = new WebSocketClass(wsUrl); } wsConnection?.addEventListener("open", async () => { currentToken = token; connectionPromise = null; resolve(wsConnection!); }); wsConnection?.addEventListener("message", (event) => { try { const rawMessage = typeof window === "undefined" ? event : event.data; const message: WSMessage = JSON.parse(rawMessage.toString()); if (message.type === "success" || message.type === "error") { const pending = pendingRequests.get(message.messageId); if (pending) { pendingRequests.delete(message.messageId); if (message.type === "success") { handleOperationSuccess( { ...pending, action: message.messageId.split(":")[1].split("/")[0] || pending.action, entityType: message.payload?.data?.id?.kind || (Array.isArray(message.payload?.data) && message.payload.data[0]?.id?.kind) || pending.entityType, entityName: message.payload?.data?.id?.name || globalEntityName || pending.entityName, originalData: pending.originalData, responsePayload: message.payload, }, message, ); pending.resolve(message); } else { console.error("WebSocket error response:", { messageId: message.messageId, fullMessage: message, }); handleOperationError( { ...pending, action: message.messageId.split("ACTION:")[1].split("/ON")[0] || pending.action, entityType: message.payload?.data?.id?.kind || pending.entityType || "unknown", entityName: message.payload?.data?.id?.name || pending.entityName || globalEntityName || "unknown", originalData: pending.originalData, }, message, ); pending.reject(message); } } } if (message.type === "event") { handleEvent(message); } else if ( message.type === "error" && message.error?.code === "_unknown_user_" && message.error?.content !== "Unknown proposed user." ) { userData.status = "unknown"; user = new User(userData); eventHelper.user.publish.loaded(user); } } catch (error) { console.error("Error parsing WebSocket message:", error, { rawMessage: typeof window === "undefined" ? event : event.data, }); } }); wsConnection?.addEventListener("close", () => { wsConnection = null; currentToken = null; connectionPromise = null; platformInfo = null; isPlatformInfoLoaded = false; isPlatformInfoLoading = false; pendingRequests.forEach((pending, messageId) => { pending.reject(new Error("WebSocket connection closed")); pendingRequests.delete(messageId); }); }); wsConnection?.addEventListener("error", (error) => { console.error("Global WebSocket error:", error); userData.status = "notlogged"; user = new User(userData); eventHelper.user.publish.loaded(user); connectionPromise = null; reject(error); }); } catch (error) { connectionPromise = null; reject(error); } }); return connectionPromise; }; /** * Function to make WebSocket requests using the global connection * @param topic Request topic (group:method) * @param payload Request payload * @param timeout Request timeout in milliseconds * @returns The response data */ export const makeGlobalWebSocketRequest = async ( topic: string, payload: any = {}, timeout = 30000, petitionAction: string = "", petitionInfo: string = "", entityType: string = "", originalData?: any, ): Promise => { if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) { throw new Error( "Global WebSocket connection not established. Call initializeGlobalWebSocketClient first.", ); } const requestId = uuidv7(); const messageId = "ACTION:" + petitionAction + "/ON:/" + petitionInfo + "/" + Date.now() + "-" + requestId; const request = { messageId: messageId, type: "request", topic, payload, }; let purgeEnvKey: string | null = null; if (petitionAction === "CLEAN" && entityType === "environment" && originalData) { purgeEnvKey = originalData.tenant && originalData.account ? `${originalData.tenant}/${originalData.account}/${petitionInfo}` : petitionInfo; purgingEnvironments.add(purgeEnvKey); } return new Promise((resolve, reject) => { const timeoutId = setTimeout(() => { const pending = pendingRequests.get(messageId); if (pending) { pendingRequests.delete(messageId); handleOperationError( { ...pending, action: petitionAction, entityType: entityType, entityName: petitionInfo, }, new Error(`Request timeout for ${topic}`), ); } if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey); const timeoutError = Object.assign( new Error(`Request timeout for ${topic}`), { isTimeout: true }, ); reject(timeoutError); }, timeout); const operation: PendingOperation = { resolve: (response: any) => { clearTimeout(timeoutId); if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey); resolve(response.payload || response); }, reject: (error: any) => { clearTimeout(timeoutId); if (purgeEnvKey) purgingEnvironments.delete(purgeEnvKey); const errorMessage = safeStringifyError(error); console.error(`WebSocket request failed for ${topic}:`, { originalError: error, errorMessage, messageId, topic, payload, }); reject(error); }, action: petitionAction, entityType: entityType, entityName: petitionInfo, originalData: { ...originalData, payload: payload, }, }; pendingRequests.set(messageId, operation); wsConnection!.send(JSON.stringify(request)); }); }; /** * Helper function for WebSocket requests with retry logic using global connection * @param topic Request topic * @param payload Request payload * @param maxRetries Maximum number of retries * @param timeout Request timeout * @returns The response data */ export const makeGlobalWebSocketRequestWithRetry = async ( topic: string, payload: any = {}, maxRetries: number = 3, timeout = 30000, petitionAction: string = "", petitionInfo: string = "", entityType: string = "", originalData?: any, ): Promise => { let lastError: Error | null = null; for (let attempt = 1; attempt <= maxRetries; attempt++) { try { const result = await makeGlobalWebSocketRequest( topic, payload, timeout, petitionAction, petitionInfo, entityType, originalData, ); return result; } catch (error) { lastError = error as Error; console.warn(`Attempt ${attempt} failed for ${topic}:`, { error, errorMessage: error instanceof Error ? error.message : safeStringifyError(error), }); if (attempt < maxRetries) { const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000); await new Promise((resolve) => setTimeout(resolve, delay)); } } } throw ( lastError || new Error(`Failed after ${maxRetries} attempts for ${topic}`) ); }; export const markEnvironmentAsPurging = (envKey: string) => { purgingEnvironments.add(envKey); const env = environmentsMap.get(envKey); if (env) { const updatedEnv: Environment = { ...env, status: { code: "PURGING", message: `Environment ${env.name} is being purged.`, timestamp: new Date().toISOString(), }, }; environmentsMap.set(envKey, updatedEnv); eventHelper.environment.publish.updated(updatedEnv); } }; /** * Get the current WebSocket connection status * @returns Connection status and token info */ export const getWebSocketStatus = () => { return { connected: wsConnection?.readyState === WebSocket.OPEN, readyState: wsConnection?.readyState, currentToken: currentToken ? "***" + currentToken.slice(-4) : null, pendingRequests: pendingRequests.size, platformInfoLoaded: isPlatformInfoLoaded, }; }; /** * Close the global WebSocket connection */ export const closeGlobalWebSocketConnection = () => { if (wsConnection) { wsConnection.close(); wsConnection = null; currentToken = null; connectionPromise = null; } platformInfo = null; isPlatformInfoLoaded = false; isPlatformInfoLoading = false; pendingRequests.forEach((pending, messageId) => { pending.reject(new Error("WebSocket connection manually closed")); pendingRequests.delete(messageId); }); }; const rebuildHierarchy = () => { const preservedUserData = { ...userData }; tenantsMap.forEach((tenant) => { tenant.accounts = Array.from(accountsMap.values()).filter((account) => { account.environments = Array.from(environmentsMap.values()) .filter((env) => env.account === account.id && env.tenant === account.tenant) .map((env) => env.name); return account.tenant === tenant.id; }); tenant.environments = Array.from(environmentsMap.values()).filter( (env) => env.tenant === tenant.id, ); tenant.services = Array.from(servicesMap.values()).filter( (service) => service.tenant === tenant.id, ); for (let i = pendingProjects.length - 1; i >= 0; i--) { const { tenant: tenantId, project } = pendingProjects[i]; if (tenantId === tenant.id) { if (!tenant.projects) { tenant.projects = []; } if (!tenant.projects.includes(project)) { tenant.projects.push(project); } pendingProjects.splice(i, 1); } } }); userData = { id: preservedUserData.id, name: preservedUserData.name, surname: preservedUserData.surname, provider: preservedUserData.provider.map((p) => ({ ...p })), notificationsEnabled: preservedUserData.notificationsEnabled, organizations: Array.from(organizationsMap.values()), clusterTokens: Array.from(clusterTokensMap.values()), tokens: tokenMap.size > 0 ? (preservedUserData.tokens || []).filter(t => tokenMap.has(t.name)) : (preservedUserData.tokens || []), tenants: Array.from(tenantsMap.values()), axebowPlan: preservedUserData.axebowPlan, companyName: preservedUserData.companyName, rol: preservedUserData.rol, platform: preservedUserData.platform, status: preservedUserData.status, notifications: preservedUserData.notifications || [], plans: preservedUserData.plans || [], discoveryMethod: preservedUserData.discoveryMethod, }; }; const handleEvent = async (message: WSMessage) => { if (message.payload.deleted === true) { await handleDeleteEvent(message); } else { const kind = message.payload.record.id.kind; const eventData = message.payload.record; const entityId = eventData.id.name; const hasRequestingServices = eventData.status?.requesting_services && Object.keys(eventData.status.requesting_services).length > 0; const parentParts = eventData.id.parent ? parseKeyPath(eventData.id.parent.name) : {}; switch (kind) { case "user": const previousTokens = userData.tokens || []; const userEventResult = handleUserEvent({ eventData, tenantsMap, currentUserData: userData, currentUserNotifications: user.notifications || [], plansMap, parseKeyPath, eventHelper, }); Object.entries(eventData.status.tenants).forEach( ([tenantId, tenantInfo]: [string, any]) => { if (tenantInfo.status === "rejected") { const tenant = tenantsMap.get(tenantId); if (tenant) { tenantsMap.set(tenantId, { ...tenant, status: "leaving" }); } else { pendingTenantStatus.set(tenantId, "leaving"); } } }, ); userData = userEventResult.userData; if (!userData.tokens?.length && previousTokens.length) { userData.tokens = previousTokens; } tempProviders = userEventResult.tempProviders; userEventResult.deletedTenantKeys.forEach((key) => { tenantsMap.delete(key); }); syncAccountStatusFromNotifications(userData.notifications, accountsMap); break; case "environment": const envEventResult = handleEnvironmentEvent({ entityId, eventData, parentParts, accountsMap, environmentsMap, pendingCloudProviderUpdates, }); if (envEventResult.pendingCloudProviderUpdate) { pendingCloudProviderUpdates.push( envEventResult.pendingCloudProviderUpdate, ); } const envSetKey = parentParts.tenant && parentParts.account ? `${parentParts.tenant}/${parentParts.account}/${entityId}` : entityId; let envToStore = envEventResult.environment; if (envToStore.status?.code?.startsWith("DELETING") && purgingEnvironments.has(envSetKey)) { envToStore = { ...envToStore, status: { code: "PURGING", message: `Environment ${entityId} is being purged.`, timestamp: new Date().toISOString(), }, }; } environmentsMap.set(envSetKey, envToStore); const envTenant = tenantsMap.get(envEventResult.tenantId); if (!envTenant) { pendingEnvironments.push({ tenant: envEventResult.tenantId, env: envEventResult.environment, }); } break; case "revision": const revisionResult = handleRevisionEvent({ entityId, eventData, parentParts, servicesMap, revisionsMap, roleMap, environmentsMap, accountsMap, }); revisionsMap.set(revisionResult.revisionKey, revisionResult.revision); if (revisionResult.roles.length > 0) { roleMap.set(revisionResult.serviceId, revisionResult.roles); } let resolvedRevisionService: Service | null = null; if (revisionResult.updatedService) { resolvedRevisionService = resolveServiceStatus( revisionResult.updatedService, ); servicesMap.set(revisionResult.serviceId, resolvedRevisionService); } if (revisionResult.updatedEnvironment) { const envId = revisionResult.updatedService?.environment; const envRevTenant = revisionResult.updatedEnvironment.tenant; const envRevAccount = revisionResult.updatedEnvironment.account; const envRevKey = envRevTenant && envRevAccount && envId ? `${envRevTenant}/${envRevAccount}/${envId}` : envId; if (envRevKey) environmentsMap.set(envRevKey, revisionResult.updatedEnvironment); } if (revisionResult.updatedAccount) { const accId = revisionResult.updatedService?.account; const accRevTenant = revisionResult.updatedAccount.tenant; const accRevKey = accRevTenant && accId ? `${accRevTenant}/${accId}` : accId; if (accRevKey) accountsMap.set(accRevKey, revisionResult.updatedAccount); } if (revisionResult.pendingRevisionError) { pendingRevisionErrors.push(revisionResult.pendingRevisionError); } if ( revisionResult.serviceDeploymentErrorEvent && resolvedRevisionService && resolvedRevisionService.error && resolvedRevisionService.status?.code !== "REMOVING_SERVICE" && String(entityId) === String(revisionResult.updatedService?.currentRevision) ) { eventHelper.service.publish.deploymentError(resolvedRevisionService); } if ( revisionResult.shouldFetchChannels && revisionResult.channelsFetchInfo ) { getChannelsInfo( revisionResult.channelsFetchInfo.serviceId, revisionResult.channelsFetchInfo.tenantId, ).catch((error) => { console.error( "[ws] Error fetching channels info for revision", error, ); }); } break; case "service": const serviceResult = handleServiceEvent({ entityId, eventData, parentParts, servicesMap, revisionsMap, roleMap, tenantsMap, pendingRevisionErrors, pendingProjects, }); pendingRevisionErrors.length = 0; pendingRevisionErrors.push( ...serviceResult.updatedPendingRevisionErrors, ); const resolvedService = resolveServiceStatus(serviceResult.service); servicesMap.set(serviceResult.serviceFullKey, resolvedService); const projectLabel = eventData.meta?.labels?.project; if (projectLabel) { const tenant = tenantsMap.get(parentParts.tenant); if (tenant) { if (!tenant.projects) tenant.projects = []; if (!tenant.projects.includes(projectLabel)) { tenant.projects.push(projectLabel); tenantsMap.set(parentParts.tenant, tenant); } } else if (serviceResult.pendingProject) { pendingProjects.push(serviceResult.pendingProject); } } if (serviceResult.wasDeployed) { const isBeingDeleted = !!eventData.meta?.deleted; if (!isBeingDeleted) { eventHelper.service.publish.deployed(resolvedService); } } const serviceState = eventData.status?.state?.code; if (serviceState === "SERVICE_READY") { const serviceKey = `${parentParts.tenant}/${entityId}`; const serviceToRefresh = recentlyUpdatedServices.get(serviceKey); if (serviceToRefresh) { recentlyUpdatedServices.delete(serviceKey); requestRevisionData(serviceToRefresh, "").catch((err) => { console.error( `[ws] Error fetching revision data after SERVICE_READY for ${serviceKey}:`, err, ); }); } } break; case "account": const accountResult = handleAccountEvent({ entityId, eventData, parentParts, accountsMap, }); if (!message.payload.deleted) { const accountKey = parentParts.tenant ? `${parentParts.tenant}/${entityId}` : entityId; accountsMap.set(accountKey, accountResult.account); updateEnvironmentsCloudProvider(entityId, eventData.spec.api, parentParts.tenant); } break; case "tenant": const existingTenant = tenantsMap.get(entityId); const newTenant: Tenant = handleTenantEvent( entityId, eventData, userData, ); if ( existingTenant && existingTenant.role && newTenant.role && existingTenant.role !== newTenant.role ) { const roleNotification: Notification = { type: "info", subtype: errors.tenant.roleChanged.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { tenant: newTenant.name, oldRole: existingTenant.role, newRole: newTenant.role, }, }; eventHelper.notification.publish.creation(roleNotification); } if (existingTenant) { if ( !newTenant.resources?.length && existingTenant.resources?.length ) { newTenant.resources = existingTenant.resources; } if (!newTenant.registry?.length && existingTenant.registry?.length) { newTenant.registry = existingTenant.registry; } if (!newTenant.services?.length && existingTenant.services?.length) { newTenant.services = existingTenant.services; } if ( !newTenant.environments?.length && existingTenant.environments?.length ) { newTenant.environments = existingTenant.environments; } if (!newTenant.projects?.length && existingTenant.projects?.length) { newTenant.projects = existingTenant.projects; } } tenantsMap.set(entityId, newTenant); if (pendingTenantStatus.has(entityId)) { const pendingStatus = pendingTenantStatus.get(entityId)!; tenantsMap.set(entityId, { ...newTenant, status: pendingStatus }); pendingTenantStatus.delete(entityId); } loadMarketplaceItemsForTenant(entityId, "") //TODO Move to helper .then((items) => { const tenant = tenantsMap.get(entityId); if (tenant) { tenant.marketplaceItems = items; tenantsMap.set(entityId, tenant); } }) .catch((err) => { console.error( `Failed to load marketplace items for ${entityId}:`, err, ); }); for (let i = pendingEnvironments.length - 1; i >= 0; i--) { const { tenant, env } = pendingEnvironments[i]; if (tenant === entityId) { pendingEnvironments.splice(i, 1); } } for (let i = pendingDomains.length - 1; i >= 0; i--) { const pd = pendingDomains[i]; if (pd.tenant === newTenant.id) { newTenant.resources.push(pd); pendingDomains.splice(i, 1); } } for (let i = pendingRegistries.length - 1; i >= 0; i--) { const { tenant, registry } = pendingRegistries[i]; if (tenant === entityId) { newTenant.registry.push(registry); pendingRegistries.splice(i, 1); } } break; case "domain": const domainResult = handleDomainEvent( eventData, parentParts, tenantsMap, hasRequestingServices, ); processResourceResult( domainResult, tenantsMap, pendingDomains, eventHelper, ); break; case "port": const portResult = handlePortEvent( eventData, parentParts, tenantsMap, hasRequestingServices, ); processResourceResult( portResult, tenantsMap, pendingDomains, eventHelper, ); break; case "secret": const secretResult = handleSecretEvent( eventData, parentParts, tenantsMap, secretsMap, hasRequestingServices, ); secretResult.secretsToStore.forEach(({ key, value }) => secretsMap.set(key, value), ); processResourceResult( secretResult, tenantsMap, pendingDomains, eventHelper, ); break; case "certificate": const certificateResult = handleCertificateEvent( eventData, parentParts, tenantsMap, hasRequestingServices, ); processResourceResult( certificateResult, tenantsMap, pendingDomains, eventHelper, ); break; case "ca": const caResult = handleCAEvent( eventData, parentParts, tenantsMap, hasRequestingServices, ); processResourceResult( caResult, tenantsMap, pendingDomains, eventHelper, ); break; case "volume": const volumeResult = handleVolumeEvent( eventData, parentParts, tenantsMap, ); processResourceResult( volumeResult, tenantsMap, pendingDomains, eventHelper, ); break; case "provideruser": const providerUserId = eventData.id.name; const providerEmail = eventData.spec.claims?.email || ""; for (const [providerName, providerId] of Object.entries( tempProviders, )) { if (providerId === providerUserId) { const existingIndex = userData.provider.findIndex( (p) => p.name === providerName, ); if (existingIndex !== -1) { userData.provider[existingIndex] = { ...userData.provider[existingIndex], email: providerEmail, }; } else { userData.provider.push({ name: providerName, email: providerEmail, }); } user = new User( userData, Array.from(tenantsMap.values()), Array.from(organizationsMap.values()), platformInfo || undefined, ); break; } } break; case "planinstance": const planResult = handlePlanInstanceEvent({ entityId, eventData, parentParts, }); plansMap.set(planResult.planKey, planResult.plan); const userPlansResult = updateUserPlansAfterPlanEvent({ newPlan: planResult.plan, userData, userId: userData.id, }); if (userPlansResult.shouldUpdate) { userData.plans = userPlansResult.updatedPlans; user = new User( userData, Array.from(tenantsMap.values()), Array.from(organizationsMap.values()), user.platform, ); eventHelper.user.publish.loaded(user); } break; case "dregistry": const registryResult = handleRegistryEvent({ eventData, parentParts, tenantsMap, secretsMap, }); if (registryResult.tenantFound && registryResult.updatedTenant) { tenantsMap.set(registryResult.tenantId, registryResult.updatedTenant); } else if (registryResult.pendingRegistry) { pendingRegistries.push(registryResult.pendingRegistry); } break; case "token": const tokenResult = handleTokenEvent({ eventData, tokenMap, userData, }); if (tokenResult.isUserToken) { userData.tokens = tokenResult.updatedUserTokens; } else if (tokenResult.isClusterToken && tokenResult.clusterToken) { clusterTokensMap.clear(); clusterTokensMap.set(tokenResult.tokenId, tokenResult.clusterToken); } break; case "cluster": const clusterName = eventData.spec.cluster.name; break; case "link": const linkResult = handleLinkEvent({ eventData, servicesMap, }); if (linkResult.updatedServerService) { servicesMap.set( linkResult.linkServiceServer, linkResult.updatedServerService, ); } if (linkResult.updatedClientService) { servicesMap.set( linkResult.linkServiceClient, linkResult.updatedClientService, ); } break; } setTimeout(async () => { rebuildHierarchy(); await updateUserWithPlatformInfo(); }, 500); } }; const loadPlatformInfo = async (): Promise => { if (isPlatformInfoLoaded && platformInfo) { return platformInfo; } if (isPlatformInfoLoading) { let attempts = 0; while (isPlatformInfoLoading && attempts < 100) { await new Promise((resolve) => setTimeout(resolve, 100)); attempts++; } return platformInfo; } isPlatformInfoLoading = true; try { const platformResponse = await makeGlobalWebSocketRequest( "misc:info", {}, 30000, "GET", "PLATFORM", ); if (platformResponse) { platformInfo = platformResponse as Platform; isPlatformInfoLoaded = true; } } catch (err) { console.error("Error loading platform info:", err); platformInfo = null; } finally { isPlatformInfoLoading = false; } return platformInfo; }; const updateUserWithPlatformInfo = async () => { try { const platform = await loadPlatformInfo(); const mapTenants = Array.from(tenantsMap.values()); const existingTenants = user.tenants; const combinedTenantsMap = new Map(); existingTenants.forEach((tenant) => { combinedTenantsMap.set(tenant.id, tenant); }); mapTenants.forEach((tenant) => { combinedTenantsMap.set(tenant.id, tenant); }); user = new User( userData, Array.from(tenantsMap.values()), Array.from(organizationsMap.values()), platform || undefined, ); eventHelper.user.publish.loaded(user); if ( environmentsMap.size > 0 && !isLoadingReporting && !hasLoadedReportingOnce ) { setTimeout(() => { if (!isLoadingReporting && !hasLoadedReportingOnce) { loadAllEnvironmentsReporting().catch((err) => { console.error("Error in background reporting loading:", err); }); } }, 2000); } } catch (err) { console.error("Error updating user with platform info:", err); user = new User( userData, Array.from(tenantsMap.values()), Array.from(organizationsMap.values()), ); eventHelper.user.publish.loaded(user); } }; export const getReferenceDomain = () => { return user.platform?.referenceDomain.replace(/\./g, "-"); }; const handleOperationSuccess = (operation: PendingOperation, response: any) => { const { action, entityType, entityName, originalData } = operation; if (action === "UPDATE_CONFIG") { const tenant = originalData?.payload?.tenant; const serviceName = originalData?.payload?.service; if (tenant && serviceName) { const serviceKey = `${tenant}/${serviceName}`; const service = servicesMap.get(serviceKey); if (service) { recentlyUpdatedServices.set(serviceKey, service); } } } switch (entityType.toLowerCase()) { case "tenant": if (action === "DELETE") { tenantsMap.delete(entityName); } else { if (originalData) { if (action === "CREATE") { const eventData = response.payload.data; const newTenant: Tenant = handleTenantOperationSuccess( entityName, eventData, userData, ); tenantsMap.set(entityName, newTenant); eventHelper.tenant.publish.created(newTenant); } else if (action === "UPDATE") { const existingTenant = tenantsMap.get(entityName); const updatedTenant = { ...existingTenant, ...originalData, status: "active", }; tenantsMap.set(entityName, updatedTenant); eventHelper.tenant.publish.updated(updatedTenant); } } } break; case "service": const svcSuccessResult = handleServiceOperationSuccess({ action, entityName, originalData, responsePayload: operation.responsePayload, servicesMap, revisionsMap, roleMap, }); if (action === "GET_CHANNELS") { let serviceId: string; let serviceName: string; const payloadTenant = operation.originalData?.payload?.tenant; const payloadService = operation.originalData?.payload?.service; if (payloadTenant && payloadService) { serviceId = `${payloadTenant}/${payloadService}`; serviceName = payloadService; } else { const entityNameParts = operation.entityName.split("/"); if (entityNameParts.length === 2) { serviceId = operation.entityName; serviceName = entityNameParts[1]; } else { console.warn( `Cannot determine serviceId for GET_CHANNELS. EntityName: ${operation.entityName}`, ); break; } } const service = servicesMap.get(serviceId); if (service) { const channelsInfo = mapChannelsFromApiData( response.payload.data, serviceName, service.resources, ); service.serverChannels = channelsInfo.serverChannels; service.clientChannels = channelsInfo.clientChannels; service.duplexChannels = channelsInfo.duplexChannels; servicesMap.set(serviceId, service); } else { console.warn( `Service ${serviceId} not found when processing GET_CHANNELS response`, ); } } else if (action === "GET_REVISION") { if ( svcSuccessResult.processRevisionData && svcSuccessResult.revisionData ) { try { let updatedService = processRevisionData( svcSuccessResult.updatedService!, svcSuccessResult.revisionData, revisionsMap, svcSuccessResult.serviceId, ); if (svcSuccessResult.revisionData.revision_error) { updatedService.status = svcSuccessResult.revisionData.revision_error; updatedService.error = { code: svcSuccessResult.revisionData.revision_error.code || "UNKNOWN_ERROR", message: svcSuccessResult.revisionData.revision_error.message || "Unknown error occurred", }; } const resources = updatedService.resources || []; if (resources.length > 0) { const applyIsPublic = (channels: Channel[]): Channel[] => channels.map((ch) => ({ ...ch, isPublic: ch.isPublic || resources.some( (r) => (r.type === "port" || r.type === "domain") && (r.name === `${ch.name}_port` || r.name === `${ch.name}_domain`), ), })); updatedService = { ...updatedService, serverChannels: applyIsPublic(updatedService.serverChannels || []), clientChannels: applyIsPublic(updatedService.clientChannels || []), duplexChannels: applyIsPublic(updatedService.duplexChannels || []), }; } servicesMap.set(svcSuccessResult.serviceId, updatedService); if (updatedService.role && updatedService.role.length > 0) { roleMap.set(svcSuccessResult.serviceId, updatedService.role); } } catch (error) { console.error( `Error processing revision data for ${svcSuccessResult.serviceId}:`, error, ); const service = servicesMap.get(svcSuccessResult.serviceId); if (service) { const resetService = { ...service, role: [] }; servicesMap.set(svcSuccessResult.serviceId, resetService); roleMap.delete(svcSuccessResult.serviceId); } } } else if ( svcSuccessResult.updatedService && !svcSuccessResult.processRevisionData ) { servicesMap.set( svcSuccessResult.serviceId, svcSuccessResult.updatedService, ); roleMap.delete(svcSuccessResult.serviceId); console.warn( `GET_REVISION response without solution for ${svcSuccessResult.serviceId}, roles reset`, ); } } else if (svcSuccessResult.updatedService) { servicesMap.set( svcSuccessResult.serviceId, svcSuccessResult.updatedService, ); if ( svcSuccessResult.eventType === "deployed" || operation.action !== "DELETE" ) { eventHelper.service.publish.deployed(svcSuccessResult.updatedService); } else if (svcSuccessResult.eventType === "updated") { eventHelper.service.publish.updated(svcSuccessResult.updatedService); } else if (svcSuccessResult.eventType === "deleting") { eventHelper.service.publish.updated(svcSuccessResult.updatedService); } } break; case "account": const accSuccessResult = handleAccountOperationSuccess({ action, entityName, originalData: originalData as Account, }); const accTenant = (originalData as Account)?.tenant; const accSuccessKey = accTenant ? `${accTenant}/${entityName}` : entityName; if (accSuccessResult.shouldDelete) { const acc = accountsMap.get(accSuccessKey); if (acc) { accountsMap.delete(accSuccessKey); eventHelper.notification.publish.creation( accSuccessResult.notification, ); } } else if (accSuccessResult.updatedAccount) { accountsMap.set(accSuccessKey, accSuccessResult.updatedAccount); if (accSuccessResult.eventType === "created") { eventHelper.account.publish.created(accSuccessResult.updatedAccount); } else if (accSuccessResult.eventType === "updated") { eventHelper.account.publish.updated(accSuccessResult.updatedAccount); } eventHelper.notification.publish.creation( accSuccessResult.notification, ); } break; case "environment": const envSuccessResult = handleEnvironmentOperationSuccess({ action, entityName, originalData: originalData as Environment, }); const envOrigData = originalData as Environment; const envSuccessKey = envOrigData?.tenant && envOrigData?.account ? `${envOrigData.tenant}/${envOrigData.account}/${entityName}` : entityName; if (envSuccessResult.shouldDelete) { environmentsMap.delete(envSuccessKey); } else if (envSuccessResult.updatedEnvironment) { environmentsMap.set(envSuccessKey, envSuccessResult.updatedEnvironment); if (envSuccessResult.notification) { eventHelper.notification.publish.creation( envSuccessResult.notification, ); } if (envSuccessResult.eventType === "created") { eventHelper.environment.publish.created( envSuccessResult.updatedEnvironment, ); } else if (envSuccessResult.eventType === "updated") { eventHelper.environment.publish.updated( envSuccessResult.updatedEnvironment, ); } } break; case "planprovider": if (action === "GET") { const providers: PlanProvider[] = response.payload.data.map( (item: any) => ({ name: item.id.name, }), ); eventHelper.planProviders.publish.plansLoaded(providers); } break; case "token": const tokenSuccessResult = handleTokenOperationSuccess({ responsePayload: response.payload, }); tokenMap.set(tokenSuccessResult.token.name, tokenSuccessResult.token); break; case "user": if (action === "DELETE") { userData = handleUserOperationError(); user = new User(userData); } break; case "resource": if (action === "DELETE") { const tenant = originalData?.payload?.tenant; const resourceName = originalData?.payload?.resource; const resourceType = originalData?.payload?.kind; if (tenant && resourceName && resourceType) { setResourceDeleting(tenantsMap, tenant, resourceName, resourceType); } } break; } setTimeout(async () => { rebuildHierarchy(); await updateUserWithPlatformInfo(); }, 100); }; const handleOperationError = (operation: PendingOperation, error: any) => { const { action, entityType, entityName, originalData } = operation; console.error( `Operation ${action} on ${entityType} '${entityName}' failed:`, error, ); switch (entityType.toLowerCase()) { case "tenant": if (action === "CREATE") { eventHelper.tenant.publish.creationError(originalData); } else if (action === "UPDATE") { eventHelper.tenant.publish.updateError(originalData); } else if (action === "DELETE") { eventHelper.tenant.publish.deletionError(originalData); } break; case "service": const svcErrorResult = handleServiceOperationError({ action, entityName, originalData, error, servicesMap, roleMap, }); if (svcErrorResult.eventType === "deploymentError") { eventHelper.service.publish.deploymentError(originalData); } else if (svcErrorResult.eventType === "updateError") { eventHelper.service.publish.updateError(originalData); } else if (svcErrorResult.eventType === "deletionError") { eventHelper.service.publish.deletionError(originalData); } if (svcErrorResult.shouldResetRoles) { const service = servicesMap.get(svcErrorResult.serviceId); if (service) { const resetService = { ...service, role: [] }; servicesMap.set(svcErrorResult.serviceId, resetService); roleMap.delete(svcErrorResult.serviceId); console.warn( `GET_REVISION failed for ${svcErrorResult.serviceId}, roles have been reset`, ); } } break; case "account": const accErrorResult = handleAccountOperationError({ action, entityName, originalData: originalData as Account, error, }); const accErrorTenant = (originalData as Account)?.tenant; const accErrorKey = accErrorTenant ? `${accErrorTenant}/${entityName}` : entityName; if (accErrorResult.shouldDelete) { accountsMap.delete(accErrorKey); } else if (accErrorResult.updatedAccount) { accountsMap.set(accErrorKey, accErrorResult.updatedAccount); } if (accErrorResult.eventType === "creationError") { eventHelper.account.publish.creationError(originalData); } else if (accErrorResult.eventType === "updateError") { eventHelper.account.publish.updateError(originalData); } else if (accErrorResult.eventType === "deletionError") { eventHelper.account.publish.deletionError(originalData); } break; case "environment": const envErrorResult = handleEnvironmentOperationError({ action, entityName, originalData: originalData as Environment, error, }); if (envErrorResult.eventType === "creationError") { eventHelper.environment.publish.creationError(originalData); } else if (envErrorResult.eventType === "updateError") { eventHelper.environment.publish.updateError(originalData); } else if (envErrorResult.eventType === "deletionError") { eventHelper.environment.publish.deletionError(originalData); } eventHelper.notification.publish.creation(envErrorResult.notification); const envErrOrigData = originalData as Environment; const envErrorKey = envErrOrigData?.tenant && envErrOrigData?.account ? `${envErrOrigData.tenant}/${envErrOrigData.account}/${entityName}` : entityName; environmentsMap.set(envErrorKey, envErrorResult.updatedEnvironment); break; } setTimeout(async () => { rebuildHierarchy(); await updateUserWithPlatformInfo(); }, 100); }; const handleDeleteEvent = async (message: WSMessage) => { const keyParts = parseKeyPath(message.payload.key); const tenantName = keyParts.tenant; if (keyParts.service && !keyParts.revision) { const serviceName = `${keyParts.tenant}/${keyParts.service}`; const deletedService = servicesMap.get(serviceName); if (deletedService) { servicesMap.delete(serviceName); const revisionsToDelete: string[] = []; revisionsMap.forEach((revision, key) => { if (key.startsWith(serviceName)) { revisionsToDelete.push(key); } }); revisionsToDelete.forEach((key) => revisionsMap.delete(key)); roleMap.delete(serviceName); eventHelper.service.publish.deleted(deletedService); } else { console.warn(`Service ${serviceName} not found in map for deletion`); } } if (keyParts.environment && !keyParts.service) { const environmentName = keyParts.environment; const envDeleteKey = tenantName && keyParts.account ? `${tenantName}/${keyParts.account}/${environmentName}` : environmentName; const deletedEnv = environmentsMap.get(envDeleteKey)!; eventHelper.environment.publish.deleted(deletedEnv); const envNotification: Notification = { type: "success", subtype: errors.environment.deleted.subtype, date: Date.now().toString(), callToAction: false, status: "unread", data: { environment: deletedEnv.name, account: deletedEnv.account, tenant: deletedEnv.tenant, }, }; eventHelper.notification.publish.creation(envNotification); environmentsMap.delete(envDeleteKey); } if (keyParts.account && !keyParts.environment) { const accountName = keyParts.account; const accountDeleteKey = tenantName ? `${tenantName}/${accountName}` : accountName; const deletedAccount = accountsMap.get(accountDeleteKey)!; eventHelper.account.publish.deleted(deletedAccount); const accountNotification: Notification = { type: "success", subtype: errors.account.deleted.subtype, date: Date.now().toString(), callToAction: false, status: "unread", data: { account: deletedAccount.name, tenant: deletedAccount.tenant, }, }; eventHelper.notification.publish.creation(accountNotification); accountsMap.delete(accountDeleteKey); } if (keyParts.domain) { const domainName = keyParts.domain; const domainTenant = tenantsMap.get(tenantName); if (domainTenant) { const existingIndex = domainTenant.resources.findIndex( (res) => res.name === domainName && res.type === "domain", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted( domainTenant.resources[existingIndex], ); domainTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, domainTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${domainName}`); } } } if (keyParts.port) { const portName = keyParts.port; const portTenant = tenantsMap.get(tenantName); if (portTenant) { const existingIndex = portTenant.resources.findIndex( (res) => res.name === portName && res.type === "port", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted( portTenant.resources[existingIndex], ); portTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, portTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${portTenant}`); } } } if (keyParts.secret) { const secretName = keyParts.secret; const secretTenant = tenantsMap.get(tenantName); if (secretTenant) { const existingIndex = secretTenant.resources.findIndex( (res) => res.name === secretName && res.type === "secret", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted( secretTenant.resources[existingIndex], ); secretTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, secretTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${secretName}`); } } } if (keyParts.volume) { const volumeName = keyParts.volume; const volumeTenant = tenantsMap.get(tenantName); if (volumeTenant) { const existingIndex = volumeTenant.resources.findIndex( (res) => res.name === volumeName && res.type === "volume", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted( volumeTenant.resources[existingIndex], ); volumeTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, volumeTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${volumeName}`); } } } if (keyParts.ca) { const caName = keyParts.ca; const caTenant = tenantsMap.get(tenantName); if (caTenant) { const existingIndex = caTenant.resources.findIndex( (res) => res.name === caName && res.type === "ca", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted(caTenant.resources[existingIndex]); caTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, caTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${caName}`); } } } if (keyParts.certificate) { const certName = keyParts.certificate; const certTenant = tenantsMap.get(tenantName); if (certTenant) { const existingIndex = certTenant.resources.findIndex( (res) => res.name === certName && res.type === "certificate", ); if (existingIndex !== -1) { eventHelper.resource.publish.deleted( certTenant.resources[existingIndex], ); certTenant.resources.splice(existingIndex, 1); tenantsMap.set(tenantName, certTenant); } else { console.warn(`Intento de eliminar recurso inexistente: ${certName}`); } } } if (keyParts.dregistry) { const registryName = keyParts.dregistry; const registryTenant = tenantsMap.get(tenantName); if (registryTenant) { const existingIndex = registryTenant.registry.findIndex( (reg) => reg.domain === registryName, ); if (existingIndex !== -1) { const deletedRegistry = registryTenant.registry[existingIndex]; const deleteRegistryBody = { tenant: registryTenant, registry: deletedRegistry, }; eventHelper.tenant?.publish?.deleteRegistry(deleteRegistryBody); registryTenant.registry.splice(existingIndex, 1); tenantsMap.set(tenantName, registryTenant); const registryNotification: Notification = { type: "success", subtype: errors.registry.deleted.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { registry: deletedRegistry, tenant: registryTenant, }, }; eventHelper.notification.publish.creation(registryNotification); } else { console.warn( `Attempt to delete non-existent registry: ${registryName}`, ); } } } if (keyParts.link) { const linkId = keyParts.link; let deletedLinkInfo: { origin: string; target: string; originChannel: string; targetChannel: string; tenant: string; } = { origin: "", target: "", originChannel: "", targetChannel: "", tenant: "", }; servicesMap.forEach((service, serviceId) => { const linkIndex = service.links.findIndex((link) => link.name === linkId); if (linkIndex !== -1) { const deletedLink = service.links[linkIndex]; if (!deletedLinkInfo) { deletedLinkInfo = { origin: deletedLink.origin, target: deletedLink.target, originChannel: deletedLink.originChannel || "", targetChannel: deletedLink.targetChannel || "", tenant: service.tenant, }; } service.links.splice(linkIndex, 1); servicesMap.set(serviceId, service); } }); if (deletedLinkInfo) { const unlinkNotification: Notification = { type: "success", subtype: errors.service.unlinked.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { originService: deletedLinkInfo.origin, targetService: deletedLinkInfo.target, originChannel: deletedLinkInfo.originChannel, targetChannel: deletedLinkInfo.targetChannel, tenant: deletedLinkInfo.tenant, }, userError: true, }; eventHelper.notification.publish.creation(unlinkNotification); } } if (keyParts.token && keyParts.user) { const tokenId = keyParts.token; const tokenIndex = userData.tokens.findIndex((t) => t.name === tokenId); if (tokenIndex !== -1) { const deletedToken = userData.tokens[tokenIndex]; userData.tokens.splice(tokenIndex, 1); tokenMap.delete(tokenId); const tokenNotification: Notification = { type: "success", subtype: errors.user.tokenDeleted.subtype, date: Date.now().toString(), status: "unread", callToAction: false, data: { tokenName: deletedToken.name, tenant: deletedToken.tenant, }, }; eventHelper.notification.publish.creation(tokenNotification); } } rebuildHierarchy(); await updateUserWithPlatformInfo(); }; const getChannelsInfo = async ( serviceId: string, tenantId: string, ): Promise<{ serverChannels: Channel[]; clientChannels: Channel[]; duplexChannels: Channel[]; }> => { try { const service = servicesMap.get(`${tenantId}/${serviceId}`); const response = await makeGlobalWebSocketRequest( "service:report_channels_service", { tenant: tenantId, service: serviceId, }, 30000, "GET_CHANNELS", `${tenantId}/${serviceId}`, "service", service, ); return mapChannelsFromApiData(response.data, serviceId); } catch (error) { console.error(`Error getting channels for service ${serviceId}:`, error); return { serverChannels: [], clientChannels: [], duplexChannels: [], }; } }; const updateEnvironmentsCloudProvider = ( accountId: string, cloudProviderName: string, tenantId?: string, ) => { environmentsMap.forEach((environment, envId) => { if (environment.account === accountId && (!tenantId || environment.tenant === tenantId)) { environment.cloudProvider = cloudProviderName; environmentsMap.set(envId, environment); } }); pendingCloudProviderUpdates = pendingCloudProviderUpdates.filter( (pending) => pending.accountId !== accountId, ); }; const loadReportingForEnvironment = async (environment: Environment) => { try { const reportingData = await getReporting(environment); if ( reportingData !== undefined && reportingData !== null && Array.isArray(reportingData) ) { const envReportKey = environment.tenant && environment.account ? `${environment.tenant}/${environment.account}/${environment.id}` : environment.id; const updatedEnvironment = environmentsMap.get(envReportKey); if (!updatedEnvironment) { console.warn(`Environment ${environment.id} not found in map`); return; } if (!updatedEnvironment.usage.current.cpuConsuption) { updatedEnvironment.usage.current.cpuConsuption = []; } const cpuValues = reportingData.map((dataPoint: any) => { const fromTime = Date.parse(dataPoint.From); const toTime = Date.parse(dataPoint.To); const durationMs = toTime - fromTime; const milliCores = durationMs > 0 ? dataPoint.VCPUMillisMs / durationMs : 0; return milliCores; }); updatedEnvironment.usage.current.cpuConsuption.push(...cpuValues); if ( updatedEnvironment.usage.current.cpuConsuption.length > REPORTING_ITERATIONS ) { updatedEnvironment.usage.current.cpuConsuption = updatedEnvironment.usage.current.cpuConsuption.slice( -REPORTING_ITERATIONS, ); } environmentsMap.set(envReportKey, updatedEnvironment); } } catch (error) { console.error( `Error loading reporting for environment ${environment.name}:`, error, ); } }; const loadAllEnvironmentsReporting = async () => { if (isLoadingReporting || hasLoadedReportingOnce) { return; } isLoadingReporting = true; try { const environments = Array.from(environmentsMap.values()); await Promise.all( environments.map((env) => loadReportingForEnvironment(env)), ); hasLoadedReportingOnce = true; } catch (error) { console.error("Error loading reporting for all environments:", error); isLoadingReporting = false; } finally { isLoadingReporting = false; } }; export const updateUserComplete = (updatedUserData: UserData): User => { try { const preservedNotifications = userData.notifications || updatedUserData.notifications || []; const incomingTokenNames = new Set( (updatedUserData.tokens || []).map((t) => t.name) ); const localOnlyTokens = (userData.tokens || []).filter( (t) => !incomingTokenNames.has(t.name) ); const mergedTokens = [...(updatedUserData.tokens || []), ...localOnlyTokens] .filter((t, index, arr) => arr.findIndex(x => x.name === t.name) === index) .filter(t => tokenMap.size === 0 || tokenMap.has(t.name)); userData = { id: updatedUserData.id || userData.id, name: updatedUserData.name || userData.name, surname: updatedUserData.surname || userData.surname, provider: updatedUserData.provider || userData.provider || [], notificationsEnabled: updatedUserData.notificationsEnabled || userData.notificationsEnabled || "", organizations: updatedUserData.organizations || [], clusterTokens: updatedUserData.clusterTokens || [], tokens: mergedTokens, tenants: updatedUserData.tenants || [], axebowPlan: updatedUserData.axebowPlan || userData.axebowPlan || "freemium", companyName: updatedUserData.companyName || userData.companyName || "", rol: updatedUserData.rol || userData.rol || "", notifications: preservedNotifications, plans: updatedUserData.plans, status: updatedUserData.status, discoveryMethod: updatedUserData.discoveryMethod || userData.discoveryMethod, }; user = new User( userData, Array.from(tenantsMap.values()), Array.from(organizationsMap.values()), platformInfo || undefined, ); return user; } catch (error) { console.error("Error updating user in websocket-manager:", error); return user; } }; const resolveServiceStatus = (service: Service): Service => { if (service.status?.code === "REMOVING_SERVICE") { return service; } const revisionKey = `/tenant/${service.tenant}/service/${service.name}/revision/${service.currentRevision}`; const currentRevision = revisionsMap.get(revisionKey); if (!currentRevision) { if (service.error) { return service; } return service; } if (currentRevision.errorCode) { return { ...service, status: currentRevision.status, error: { code: currentRevision.errorCode, message: currentRevision.errorMsg || "", timestamp: currentRevision.status?.timestamp, }, }; } return { ...service, error: undefined, status: currentRevision.status, }; }; export const fetchAndStoreMarketplaceSchema = async ( item: MarketplaceItem, ): Promise => { const tenantsWithItem: string[] = []; tenantsMap.forEach((tenantEntry, tenantId) => { const hasItem = (tenantEntry.marketplaceItems || []).some( (i) => i.name === item.name && i.domain === item.domain && i.artifact === item.artifact, ); if (hasItem) tenantsWithItem.push(tenantId); }); if (tenantsWithItem.length === 0) return; await Promise.all( tenantsWithItem.map(async (tenantId) => { try { const response = await makeGlobalWebSocketRequest( "marketplace:artifact_schema", { tenant: tenantId, module: item.name, version: item.version, domain: item.domain, artifact: item.artifact, }, 30000, "GET", tenantId, ); const data = response?.data; if (!data) return; const nameKeys = Object.keys(data); const schema = processMarketplaceSchemaResponse(data[nameKeys[0]], nameKeys[0]); const tenantEntry = tenantsMap.get(tenantId); if (tenantEntry) { tenantEntry.marketplaceItems = tenantEntry.marketplaceItems.map((i) => i.name === item.name && i.domain === item.domain && i.artifact === item.artifact ? { ...i, schema } : i, ); tenantsMap.set(tenantId, tenantEntry); } } catch (error) { console.error(`Error loading schema for tenant ${tenantId}:`, error); } }), ); rebuildHierarchy(); await updateUserWithPlatformInfo(); };