@kumori/aurora-backend-handler 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,1833 @@
1
+ import { v7 as uuidv7 } from "uuid";
2
+ import { environment } from "./environment";
3
+ import { Account, Channel, ClusterToken, Environment, Instance, Link, MarketplaceService, Notification, Organization, Plan, PlanProvider, Platform, Registry, Resource, Service, Tenant, tenantRole, Token, Usage, User, UserData } from "@hestekumori/aurora-interfaces";
4
+ import { loadMarketplaceItemsForTenant } from "./api/marketplace-api-service";
5
+ import { getReporting } from "./api/reporting-api-service";
6
+ import { parseKeyPath } from "./utils/utils";
7
+ import {
8
+ handleTenantEvent,
9
+ handleTenantOperationSuccess,
10
+ } from "./helpers/tenant-helper";
11
+ import {
12
+ handleUserEvent,
13
+ handleUserOperationError,
14
+ } from "./helpers/user-helper";
15
+ import {
16
+ handleEnvironmentEvent,
17
+ handleEnvironmentOperationError,
18
+ handleEnvironmentOperationSuccess,
19
+ } from "./helpers/environment-helper";
20
+ import { handleRevisionEvent, processRevisionData } from "./helpers/revision-helper";
21
+ import {
22
+ handleServiceEvent,
23
+ handleServiceOperationError,
24
+ handleServiceOperationSuccess,
25
+ mapChannelsFromApiData,
26
+ } from "./helpers/service-helper";
27
+ import {
28
+ handleAccountEvent,
29
+ handleAccountOperationError,
30
+ handleAccountOperationSuccess,
31
+ } from "./helpers/account-helper";
32
+ import {
33
+ handleCAEvent,
34
+ handleCertificateEvent,
35
+ handleDomainEvent,
36
+ handlePortEvent,
37
+ handleSecretEvent,
38
+ handleVolumeEvent,
39
+ processResourceResult,
40
+ } from "./helpers/resource-helper";
41
+ import {
42
+ handlePlanInstanceEvent,
43
+ updateUserPlansAfterPlanEvent,
44
+ } from "./helpers/plan-helper";
45
+ import { handleRegistryEvent } from "./helpers/registry-helper";
46
+ import {
47
+ handleTokenEvent,
48
+ handleTokenOperationSuccess,
49
+ } from "./helpers/token-helper";
50
+ import { handleLinkEvent } from "./helpers/link-helper";
51
+ import { eventHelper } from "./backend-handler";
52
+
53
+ let WebSocketClass: any;
54
+
55
+ interface WSMessage {
56
+ messageId: string;
57
+ payload: any;
58
+ topic: string;
59
+ type: "request" | "multipartrequest" | "success" | "error" | "event";
60
+ error?: {
61
+ code?: string;
62
+ message?: string;
63
+ content?: string;
64
+ };
65
+ }
66
+ interface Revision {
67
+ service: string;
68
+ revision: string;
69
+ usage: Usage;
70
+ status: {
71
+ code: string;
72
+ message: string;
73
+ timestamp: string;
74
+ args: string[];
75
+ };
76
+ errorCode?: string;
77
+ errorMsg?: string;
78
+ createdAt?: string;
79
+ }
80
+ interface PendingOperation {
81
+ resolve: Function;
82
+ reject: Function;
83
+ action: string;
84
+ entityType: string;
85
+ entityName: string;
86
+ originalData?: any;
87
+ responsePayload?: any;
88
+ }
89
+ if (typeof window === "undefined") {
90
+ WebSocketClass = require("ws");
91
+ } else {
92
+ WebSocketClass = WebSocket;
93
+ }
94
+ interface Role {
95
+ name: string;
96
+ instances: Instance[];
97
+ logo?: string;
98
+ category?: string;
99
+ version?: string;
100
+ description?: string;
101
+ resource?: Resource[];
102
+ parameters?: { [key: string]: string }[];
103
+ registry?: string;
104
+ imageTag?: string;
105
+ entrypoint?: string;
106
+ cmd?: string;
107
+ scalling?: {
108
+ cpu: {
109
+ up: string;
110
+ down: string;
111
+ };
112
+ memory: {
113
+ up: string;
114
+ down: string;
115
+ };
116
+ instances: {
117
+ max: number;
118
+ min: number;
119
+ };
120
+ histeresys: string;
121
+ };
122
+ hsize?: number;
123
+ }
124
+
125
+ /**
126
+ * Global WebSocket client instance and pending requests
127
+ */
128
+ let wsConnection: WebSocket | null = null;
129
+ let pendingRequests = new Map<string, PendingOperation>();
130
+ let currentToken: string | null = null;
131
+ let connectionPromise: Promise<WebSocket> | null = null;
132
+ let userData: UserData = {
133
+ id: "",
134
+ name: "",
135
+ surname: "",
136
+ provider: [],
137
+ notificationsEnabled: "",
138
+ organizations: [],
139
+ clusterTokens: [],
140
+ tokens: [],
141
+ tenants: [],
142
+ axebowPlan: "freemium",
143
+ companyName: "",
144
+ rol: "",
145
+ };
146
+ let user: User = new User(userData);
147
+ let tenantsMap = new Map<string, Tenant>();
148
+ let clusterTokensMap = new Map<string, ClusterToken>();
149
+ let servicesMap = new Map<string, Service>();
150
+ let organizationsMap = new Map<string, Organization>();
151
+ let environmentsMap = new Map<string, Environment>();
152
+ let accountsMap = new Map<string, Account>();
153
+ let revisionsMap = new Map<string, Revision>();
154
+ let roleMap = new Map<string, Role[]>();
155
+ let tokenMap = new Map<string, Token>();
156
+ let platformInfo: Platform | null = null;
157
+ let isPlatformInfoLoading = false;
158
+ let isPlatformInfoLoaded = false;
159
+ const pendingDomains: Resource[] = [];
160
+ let globalEntityName: string | null = null;
161
+ let pendingEnvironments: Array<{ tenant: string; env: Environment }> = [];
162
+ let pendingRevisionErrors: Array<{ service: string; revision: Revision }> = [];
163
+ let pendingCloudProviderUpdates: Array<{
164
+ environmentId: string;
165
+ accountId: string;
166
+ }> = [];
167
+ let plansMap = new Map<string, Plan>();
168
+ let pendingRegistries: Array<{ tenant: string; registry: Registry }> = [];
169
+ let secretsMap = new Map<string, any>();
170
+ let tempProviders: Record<string, string> = {};
171
+ let pendingProjects: Array<{ tenant: string; project: string }> = [];
172
+ let isLoadingReporting = false;
173
+ const REPORTING_ITERATIONS = 5;
174
+ const REPORTING_INTERVAL = 1000;
175
+ let hasLoadedReportingOnce = false;
176
+
177
+ /**
178
+ * Helper function to safely stringify error objects
179
+ */
180
+ const safeStringifyError = (error: any): string => {
181
+ if (typeof error === "string") {
182
+ return error;
183
+ }
184
+
185
+ if (error && typeof error === "object") {
186
+ try {
187
+ if (error.message) return error.message;
188
+ if (error.error) {
189
+ if (typeof error.error === "string") return error.error;
190
+ if (error.error.message) return error.error.message;
191
+ if (error.error.content) return error.error.content;
192
+ if (error.error.description) return error.error.description;
193
+ return JSON.stringify(error.error);
194
+ }
195
+ if (error.content) return error.content;
196
+ if (error.description) return error.description;
197
+ return JSON.stringify(error);
198
+ } catch (stringifyError) {
199
+ return `Error object could not be stringified: ${error.toString()}`;
200
+ }
201
+ }
202
+
203
+ return String(error);
204
+ };
205
+
206
+ /**
207
+ * Initialize global WebSocket connection
208
+ * @param token Authorization token
209
+ * @returns Promise that resolves to WebSocket instance
210
+ */
211
+ export const initializeGlobalWebSocketClient = async (
212
+ token: string,
213
+ type?: string,
214
+ name?: string,
215
+ previousData?: UserData,
216
+ ): Promise<WebSocket> => {
217
+ userData.notifications = previousData?.notifications;
218
+ globalEntityName = name || null;
219
+ if (
220
+ wsConnection &&
221
+ wsConnection.readyState === WebSocket.OPEN &&
222
+ currentToken === token
223
+ ) {
224
+ return wsConnection;
225
+ }
226
+
227
+ if (connectionPromise) {
228
+ try {
229
+ return await connectionPromise;
230
+ } catch (error) {
231
+ connectionPromise = null;
232
+ }
233
+ }
234
+
235
+ if (wsConnection && currentToken !== token) {
236
+ wsConnection.close();
237
+ wsConnection = null;
238
+ platformInfo = null;
239
+ isPlatformInfoLoaded = false;
240
+ isPlatformInfoLoading = false;
241
+ }
242
+ const baseUrl = environment.apiServer.baseUrl.replace("http", "ws");
243
+ const wsUrl = `${baseUrl}/api/${environment.apiServer.apiVersion}/ws`;
244
+
245
+ connectionPromise = new Promise((resolve, reject) => {
246
+ try {
247
+ if (typeof window === "undefined") {
248
+ wsConnection = new WebSocketClass(wsUrl, {
249
+ headers: {
250
+ authorization: `Bearer ${token}`,
251
+ },
252
+ });
253
+ } else {
254
+ wsConnection = new WebSocketClass(wsUrl);
255
+ }
256
+
257
+ wsConnection?.addEventListener("open", async () => {
258
+ currentToken = token;
259
+ connectionPromise = null;
260
+
261
+ resolve(wsConnection!);
262
+ });
263
+
264
+ wsConnection?.addEventListener("message", (event) => {
265
+ try {
266
+ const rawMessage = typeof window === "undefined" ? event : event.data;
267
+ const message: WSMessage = JSON.parse(rawMessage.toString());
268
+
269
+ if (message.type === "success" || message.type === "error") {
270
+ const pending = pendingRequests.get(message.messageId);
271
+ if (pending) {
272
+ pendingRequests.delete(message.messageId);
273
+ if (message.type === "success") {
274
+ handleOperationSuccess(
275
+ {
276
+ ...pending,
277
+ action:
278
+ message.messageId.split(":")[1].split("/")[0] ||
279
+ pending.action,
280
+ entityType:
281
+ message.payload?.data?.id?.kind ||
282
+ (Array.isArray(message.payload?.data) &&
283
+ message.payload.data[0]?.id?.kind) ||
284
+ pending.entityType,
285
+ entityName:
286
+ message.payload?.data?.id?.name ||
287
+ globalEntityName ||
288
+ pending.entityName,
289
+ originalData: pending.originalData,
290
+ responsePayload: message.payload,
291
+ },
292
+ message,
293
+ );
294
+ pending.resolve(message);
295
+ } else {
296
+ console.error("WebSocket error response:", {
297
+ messageId: message.messageId,
298
+ fullMessage: message,
299
+ });
300
+ handleOperationError(
301
+ {
302
+ ...pending,
303
+ action:
304
+ message.messageId.split("ACTION:")[1].split("/ON")[0] ||
305
+ pending.action,
306
+ entityType:
307
+ message.payload?.data?.id?.kind ||
308
+ pending.entityType ||
309
+ "unknown",
310
+ entityName:
311
+ message.payload?.data?.id?.name ||
312
+ pending.entityName ||
313
+ globalEntityName ||
314
+ "unknown",
315
+ originalData: pending.originalData,
316
+ },
317
+ message,
318
+ );
319
+ pending.reject(message);
320
+ }
321
+ }
322
+ }
323
+
324
+ if (message.type === "event") {
325
+ handleEvent(message);
326
+ } else if (
327
+ message.type === "error" &&
328
+ message.error?.code === "_unknown_user_" &&
329
+ message.error?.content !== "Unknown proposed user."
330
+ ) {
331
+ userData.status = "unknown";
332
+ user = new User(userData);
333
+ eventHelper.user.publish.loaded(user);
334
+ }
335
+ } catch (error) {
336
+ console.error("Error parsing WebSocket message:", error, {
337
+ rawMessage: typeof window === "undefined" ? event : event.data,
338
+ });
339
+ }
340
+ });
341
+
342
+ wsConnection?.addEventListener("close", () => {
343
+ wsConnection = null;
344
+ currentToken = null;
345
+ connectionPromise = null;
346
+ platformInfo = null;
347
+ isPlatformInfoLoaded = false;
348
+ isPlatformInfoLoading = false;
349
+
350
+ pendingRequests.forEach((pending, messageId) => {
351
+ pending.reject(new Error("WebSocket connection closed"));
352
+ pendingRequests.delete(messageId);
353
+ });
354
+ });
355
+
356
+ wsConnection?.addEventListener("error", (error) => {
357
+ console.error("Global WebSocket error:", error);
358
+ connectionPromise = null;
359
+ reject(error);
360
+ });
361
+ } catch (error) {
362
+ connectionPromise = null;
363
+ reject(error);
364
+ }
365
+ });
366
+
367
+ return connectionPromise;
368
+ };
369
+
370
+ /**
371
+ * Function to make WebSocket requests using the global connection
372
+ * @param topic Request topic (group:method)
373
+ * @param payload Request payload
374
+ * @param timeout Request timeout in milliseconds
375
+ * @returns The response data
376
+ */
377
+ export const makeGlobalWebSocketRequest = async (
378
+ topic: string,
379
+ payload: any = {},
380
+ timeout = 30000,
381
+ petitionAction: string = "",
382
+ petitionInfo: string = "",
383
+ entityType: string = "",
384
+ originalData?: any,
385
+ ): Promise<any> => {
386
+ if (!wsConnection || wsConnection.readyState !== WebSocket.OPEN) {
387
+ throw new Error(
388
+ "Global WebSocket connection not established. Call initializeGlobalWebSocketClient first.",
389
+ );
390
+ }
391
+
392
+ const requestId = uuidv7();
393
+ const messageId =
394
+ "ACTION:" +
395
+ petitionAction +
396
+ "/ON:/" +
397
+ petitionInfo +
398
+ "/" +
399
+ Date.now() +
400
+ "-" +
401
+ requestId;
402
+ const request = {
403
+ messageId: messageId,
404
+ type: "request",
405
+ topic,
406
+ payload,
407
+ };
408
+
409
+ return new Promise((resolve, reject) => {
410
+ const timeoutId = setTimeout(() => {
411
+ const pending = pendingRequests.get(messageId);
412
+ if (pending) {
413
+ pendingRequests.delete(messageId);
414
+ handleOperationError(
415
+ {
416
+ ...pending,
417
+ action: petitionAction,
418
+ entityType: entityType,
419
+ entityName: petitionInfo,
420
+ },
421
+ new Error(`Request timeout for ${topic}`),
422
+ );
423
+ }
424
+ reject(new Error(`Request timeout for ${topic}`));
425
+ }, timeout);
426
+
427
+ const operation: PendingOperation = {
428
+ resolve: (response: any) => {
429
+ clearTimeout(timeoutId);
430
+ resolve(response.payload || response);
431
+ },
432
+ reject: (error: any) => {
433
+ clearTimeout(timeoutId);
434
+ const errorMessage = safeStringifyError(error);
435
+ console.error(`WebSocket request failed for ${topic}:`, {
436
+ originalError: error,
437
+ errorMessage,
438
+ messageId,
439
+ topic,
440
+ payload,
441
+ });
442
+ reject(error);
443
+ },
444
+ action: petitionAction,
445
+ entityType: entityType,
446
+ entityName: petitionInfo,
447
+ originalData: {
448
+ ...originalData,
449
+ payload: payload,
450
+ },
451
+ };
452
+
453
+ pendingRequests.set(messageId, operation);
454
+ wsConnection!.send(JSON.stringify(request));
455
+ });
456
+ };
457
+
458
+ /**
459
+ * Helper function for WebSocket requests with retry logic using global connection
460
+ * @param topic Request topic
461
+ * @param payload Request payload
462
+ * @param maxRetries Maximum number of retries
463
+ * @param timeout Request timeout
464
+ * @returns The response data
465
+ */
466
+ export const makeGlobalWebSocketRequestWithRetry = async (
467
+ topic: string,
468
+ payload: any = {},
469
+ maxRetries: number = 3,
470
+ timeout = 30000,
471
+ petitionAction: string = "",
472
+ petitionInfo: string = "",
473
+ entityType: string = "",
474
+ originalData?: any,
475
+ ): Promise<any> => {
476
+ let lastError: Error | null = null;
477
+
478
+ for (let attempt = 1; attempt <= maxRetries; attempt++) {
479
+ try {
480
+ const result = await makeGlobalWebSocketRequest(
481
+ topic,
482
+ payload,
483
+ timeout,
484
+ petitionAction,
485
+ petitionInfo,
486
+ entityType,
487
+ originalData,
488
+ );
489
+ return result;
490
+ } catch (error) {
491
+ lastError = error as Error;
492
+ console.warn(`Attempt ${attempt} failed for ${topic}:`, {
493
+ error,
494
+ errorMessage:
495
+ error instanceof Error ? error.message : safeStringifyError(error),
496
+ });
497
+
498
+ if (attempt < maxRetries) {
499
+ const delay = Math.min(1000 * Math.pow(2, attempt - 1), 5000);
500
+ await new Promise((resolve) => setTimeout(resolve, delay));
501
+ }
502
+ }
503
+ }
504
+
505
+ throw (
506
+ lastError || new Error(`Failed after ${maxRetries} attempts for ${topic}`)
507
+ );
508
+ };
509
+
510
+ /**
511
+ * Get the current WebSocket connection status
512
+ * @returns Connection status and token info
513
+ */
514
+ export const getWebSocketStatus = () => {
515
+ return {
516
+ connected: wsConnection?.readyState === WebSocket.OPEN,
517
+ readyState: wsConnection?.readyState,
518
+ currentToken: currentToken ? "***" + currentToken.slice(-4) : null,
519
+ pendingRequests: pendingRequests.size,
520
+ platformInfoLoaded: isPlatformInfoLoaded,
521
+ };
522
+ };
523
+
524
+ /**
525
+ * Close the global WebSocket connection
526
+ */
527
+ export const closeGlobalWebSocketConnection = () => {
528
+ if (wsConnection) {
529
+ wsConnection.close();
530
+ wsConnection = null;
531
+ currentToken = null;
532
+ connectionPromise = null;
533
+ }
534
+
535
+ platformInfo = null;
536
+ isPlatformInfoLoaded = false;
537
+ isPlatformInfoLoading = false;
538
+
539
+ pendingRequests.forEach((pending, messageId) => {
540
+ pending.reject(new Error("WebSocket connection manually closed"));
541
+ pendingRequests.delete(messageId);
542
+ });
543
+ };
544
+
545
+ const rebuildHierarchy = () => {
546
+ const preservedUserData = { ...userData };
547
+
548
+ tenantsMap.forEach((tenant) => {
549
+ tenant.accounts = Array.from(accountsMap.values()).filter((account) => {
550
+ account.environments = Array.from(environmentsMap.values())
551
+ .filter((env) => env.account === account.id)
552
+ .map((env) => env.name);
553
+ return account.tenant === tenant.id;
554
+ });
555
+
556
+ tenant.environments = Array.from(environmentsMap.values()).filter(
557
+ (env) => env.tenant === tenant.id,
558
+ );
559
+
560
+ tenant.services = Array.from(servicesMap.values()).filter(
561
+ (service) => service.tenant === tenant.id,
562
+ );
563
+
564
+ for (let i = pendingProjects.length - 1; i >= 0; i--) {
565
+ const { tenant: tenantId, project } = pendingProjects[i];
566
+ if (tenantId === tenant.id) {
567
+ if (!tenant.projects) {
568
+ tenant.projects = [];
569
+ }
570
+ if (!tenant.projects.includes(project)) {
571
+ tenant.projects.push(project);
572
+ }
573
+ pendingProjects.splice(i, 1);
574
+ }
575
+ }
576
+ });
577
+
578
+ userData = {
579
+ id: preservedUserData.id,
580
+ name: preservedUserData.name,
581
+ surname: preservedUserData.surname,
582
+ provider: preservedUserData.provider.map((p) => ({ ...p })),
583
+ notificationsEnabled: preservedUserData.notificationsEnabled,
584
+ organizations: Array.from(organizationsMap.values()),
585
+ clusterTokens: Array.from(clusterTokensMap.values()),
586
+ tokens: preservedUserData.tokens || [],
587
+ tenants: Array.from(tenantsMap.values()),
588
+ axebowPlan: preservedUserData.axebowPlan,
589
+ companyName: preservedUserData.companyName,
590
+ rol: preservedUserData.rol,
591
+ platform: preservedUserData.platform,
592
+ status: preservedUserData.status,
593
+ notifications: preservedUserData.notifications || [],
594
+ plans: preservedUserData.plans || [],
595
+ };
596
+ };
597
+ const handleEvent = async (message: WSMessage) => {
598
+ if (message.payload.deleted === true) {
599
+ await handleDeleteEvent(message);
600
+ } else {
601
+ const kind = message.payload.record.id.kind;
602
+ const eventData = message.payload.record;
603
+ const entityId = eventData.id.name;
604
+ const hasRequestingServices =
605
+ eventData.status?.requesting_services &&
606
+ Object.keys(eventData.status.requesting_services).length > 0;
607
+ const parentParts = eventData.id.parent
608
+ ? parseKeyPath(eventData.id.parent.name)
609
+ : {};
610
+ switch (kind) {
611
+ case "user":
612
+ const userEventResult = handleUserEvent({
613
+ eventData,
614
+ tenantsMap,
615
+ currentUserData: userData,
616
+ currentUserNotifications: user.notifications || [],
617
+ plansMap,
618
+ parseKeyPath,
619
+ eventHelper,
620
+ });
621
+ userData = userEventResult.userData;
622
+ tempProviders = userEventResult.tempProviders;
623
+ userEventResult.deletedTenantKeys.forEach((key) => {
624
+ tenantsMap.delete(key);
625
+ });
626
+ break;
627
+ case "environment":
628
+ const envEventResult = handleEnvironmentEvent({
629
+ entityId,
630
+ eventData,
631
+ parentParts,
632
+ accountsMap,
633
+ environmentsMap,
634
+ pendingCloudProviderUpdates,
635
+ });
636
+ if (envEventResult.pendingCloudProviderUpdate) {
637
+ pendingCloudProviderUpdates.push(
638
+ envEventResult.pendingCloudProviderUpdate,
639
+ );
640
+ }
641
+ environmentsMap.set(entityId, envEventResult.environment);
642
+ const envTenant = tenantsMap.get(envEventResult.tenantId);
643
+ if (!envTenant) {
644
+ pendingEnvironments.push({
645
+ tenant: envEventResult.tenantId,
646
+ env: envEventResult.environment,
647
+ });
648
+ }
649
+ break;
650
+ case "revision":
651
+ const revisionResult = handleRevisionEvent({
652
+ entityId,
653
+ eventData,
654
+ parentParts,
655
+ servicesMap,
656
+ revisionsMap,
657
+ roleMap,
658
+ environmentsMap,
659
+ accountsMap,
660
+ });
661
+ revisionsMap.set(revisionResult.revisionKey, revisionResult.revision);
662
+ if (revisionResult.roles.length > 0) {
663
+ roleMap.set(revisionResult.serviceId, revisionResult.roles);
664
+ }
665
+ if (revisionResult.updatedService) {
666
+ servicesMap.set(
667
+ revisionResult.serviceId,
668
+ revisionResult.updatedService,
669
+ );
670
+ }
671
+ if (revisionResult.updatedEnvironment) {
672
+ const envId = revisionResult.updatedService?.environment;
673
+ if (envId) {
674
+ environmentsMap.set(envId, revisionResult.updatedEnvironment);
675
+ }
676
+ }
677
+ if (revisionResult.updatedAccount) {
678
+ const accId = revisionResult.updatedService?.account;
679
+ if (accId) {
680
+ accountsMap.set(accId, revisionResult.updatedAccount);
681
+ }
682
+ }
683
+ if (revisionResult.pendingRevisionError) {
684
+ pendingRevisionErrors.push(revisionResult.pendingRevisionError);
685
+ }
686
+ if (revisionResult.serviceDeploymentErrorEvent) {
687
+ eventHelper.service.publish.deploymentError(
688
+ revisionResult.serviceDeploymentErrorEvent,
689
+ );
690
+ }
691
+ if (
692
+ revisionResult.shouldFetchChannels &&
693
+ revisionResult.channelsFetchInfo
694
+ ) {
695
+ getChannelsInfo(
696
+ revisionResult.channelsFetchInfo.serviceId,
697
+ revisionResult.channelsFetchInfo.tenantId,
698
+ )
699
+ .then((channelsInfo) => {})
700
+ .catch((error) => {
701
+ console.error("[ws] Error fetching channels info for revision", {
702
+ revisionServiceId: revisionResult.channelsFetchInfo?.serviceId,
703
+ error,
704
+ });
705
+ });
706
+ }
707
+ break;
708
+ case "service":
709
+ const serviceResult = handleServiceEvent({
710
+ entityId,
711
+ eventData,
712
+ parentParts,
713
+ servicesMap,
714
+ revisionsMap,
715
+ roleMap,
716
+ tenantsMap,
717
+ pendingRevisionErrors,
718
+ pendingProjects,
719
+ });
720
+ pendingRevisionErrors.length = 0;
721
+ pendingRevisionErrors.push(
722
+ ...serviceResult.updatedPendingRevisionErrors,
723
+ );
724
+ servicesMap.set(serviceResult.serviceFullKey, serviceResult.service);
725
+ const projectLabel = eventData.meta?.labels?.project;
726
+ if (projectLabel) {
727
+ const tenant = tenantsMap.get(parentParts.tenant);
728
+ if (tenant) {
729
+ if (!tenant.projects) {
730
+ tenant.projects = [];
731
+ }
732
+ if (!tenant.projects.includes(projectLabel)) {
733
+ tenant.projects.push(projectLabel);
734
+ tenantsMap.set(parentParts.tenant, tenant);
735
+ }
736
+ } else if (serviceResult.pendingProject) {
737
+ pendingProjects.push(serviceResult.pendingProject);
738
+ }
739
+ }
740
+ if (serviceResult.wasDeployed) {
741
+ eventHelper.service.publish.deployed(serviceResult.service);
742
+ }
743
+ break;
744
+ case "account":
745
+ const accountResult = handleAccountEvent({
746
+ entityId,
747
+ eventData,
748
+ parentParts,
749
+ accountsMap,
750
+ });
751
+
752
+ if (!message.payload.deleted) {
753
+ accountsMap.set(entityId, accountResult.account);
754
+ updateEnvironmentsCloudProvider(entityId, eventData.spec.api);
755
+ }
756
+ break;
757
+
758
+ case "tenant":
759
+ const newTenant: Tenant = handleTenantEvent(
760
+ entityId,
761
+ eventData,
762
+ userData,
763
+ );
764
+ tenantsMap.set(entityId, newTenant);
765
+ loadMarketplaceItemsForTenant(entityId, "") //TODO Move to helper
766
+ .then((items) => {
767
+ const tenant = tenantsMap.get(entityId);
768
+ if (tenant) {
769
+ tenant.marketplaceItems = items;
770
+ tenantsMap.set(entityId, tenant);
771
+ }
772
+ })
773
+ .catch((err) => {
774
+ console.error(
775
+ `Failed to load marketplace items for ${entityId}:`,
776
+ err,
777
+ );
778
+ });
779
+
780
+ for (let i = pendingEnvironments.length - 1; i >= 0; i--) {
781
+ const { tenant, env } = pendingEnvironments[i];
782
+ if (tenant === entityId) {
783
+ pendingEnvironments.splice(i, 1);
784
+ }
785
+ }
786
+ for (let i = pendingDomains.length - 1; i >= 0; i--) {
787
+ const pd = pendingDomains[i];
788
+ if (pd.tenant === newTenant.id) {
789
+ newTenant.resources.push(pd);
790
+ pendingDomains.splice(i, 1);
791
+ }
792
+ }
793
+ for (let i = pendingRegistries.length - 1; i >= 0; i--) {
794
+ const { tenant, registry } = pendingRegistries[i];
795
+ if (tenant === entityId) {
796
+ newTenant.registry.push(registry);
797
+ pendingRegistries.splice(i, 1);
798
+ }
799
+ }
800
+ break;
801
+
802
+ case "domain":
803
+ const domainResult = handleDomainEvent(
804
+ eventData,
805
+ parentParts,
806
+ tenantsMap,
807
+ hasRequestingServices,
808
+ );
809
+ processResourceResult(
810
+ domainResult,
811
+ tenantsMap,
812
+ pendingDomains,
813
+ eventHelper,
814
+ );
815
+ break;
816
+
817
+ case "port":
818
+ const portResult = handlePortEvent(
819
+ eventData,
820
+ parentParts,
821
+ tenantsMap,
822
+ hasRequestingServices,
823
+ );
824
+ processResourceResult(
825
+ portResult,
826
+ tenantsMap,
827
+ pendingDomains,
828
+ eventHelper,
829
+ );
830
+ break;
831
+
832
+ case "secret":
833
+ const secretResult = handleSecretEvent(
834
+ eventData,
835
+ parentParts,
836
+ tenantsMap,
837
+ secretsMap,
838
+ );
839
+ secretResult.secretsToStore.forEach(({ key, value }) =>
840
+ secretsMap.set(key, value),
841
+ );
842
+ processResourceResult(
843
+ secretResult,
844
+ tenantsMap,
845
+ pendingDomains,
846
+ eventHelper,
847
+ );
848
+ break;
849
+
850
+ case "certificate":
851
+ const certificateResult = handleCertificateEvent(
852
+ eventData,
853
+ parentParts,
854
+ tenantsMap,
855
+ );
856
+ processResourceResult(
857
+ certificateResult,
858
+ tenantsMap,
859
+ pendingDomains,
860
+ eventHelper,
861
+ );
862
+ break;
863
+
864
+ case "ca":
865
+ const caResult = handleCAEvent(eventData, parentParts, tenantsMap);
866
+ processResourceResult(
867
+ caResult,
868
+ tenantsMap,
869
+ pendingDomains,
870
+ eventHelper,
871
+ );
872
+ break;
873
+
874
+ case "volume":
875
+ const volumeResult = handleVolumeEvent(
876
+ eventData,
877
+ parentParts,
878
+ tenantsMap,
879
+ );
880
+ processResourceResult(
881
+ volumeResult,
882
+ tenantsMap,
883
+ pendingDomains,
884
+ eventHelper,
885
+ );
886
+ break;
887
+ case "provideruser":
888
+ const providerUserId = eventData.id.name;
889
+ const providerEmail = eventData.spec.claims?.email || "";
890
+ for (const [providerName, providerId] of Object.entries(
891
+ tempProviders,
892
+ )) {
893
+ if (providerId === providerUserId) {
894
+ const existingIndex = userData.provider.findIndex(
895
+ (p) => p.name === providerName,
896
+ );
897
+
898
+ if (existingIndex !== -1) {
899
+ userData.provider[existingIndex] = {
900
+ ...userData.provider[existingIndex],
901
+ email: providerEmail,
902
+ };
903
+ } else {
904
+ userData.provider.push({
905
+ name: providerName,
906
+ email: providerEmail,
907
+ });
908
+ }
909
+ user = new User(
910
+ userData,
911
+ Array.from(tenantsMap.values()),
912
+ Array.from(organizationsMap.values()),
913
+ platformInfo || undefined,
914
+ );
915
+ break;
916
+ }
917
+ }
918
+ break;
919
+ case "planinstance":
920
+ const planResult = handlePlanInstanceEvent({
921
+ entityId,
922
+ eventData,
923
+ parentParts,
924
+ });
925
+
926
+ plansMap.set(planResult.planKey, planResult.plan);
927
+ const userPlansResult = updateUserPlansAfterPlanEvent({
928
+ newPlan: planResult.plan,
929
+ userData,
930
+ userId: userData.id,
931
+ });
932
+
933
+ if (userPlansResult.shouldUpdate) {
934
+ userData.plans = userPlansResult.updatedPlans;
935
+ user = new User(
936
+ userData,
937
+ Array.from(tenantsMap.values()),
938
+ Array.from(organizationsMap.values()),
939
+ user.platform,
940
+ );
941
+ eventHelper.user.publish.loaded(user);
942
+ }
943
+ break;
944
+ case "dregistry":
945
+ const registryResult = handleRegistryEvent({
946
+ eventData,
947
+ parentParts,
948
+ tenantsMap,
949
+ secretsMap
950
+ });
951
+
952
+ if (registryResult.tenantFound && registryResult.updatedTenant) {
953
+ tenantsMap.set(registryResult.tenantId, registryResult.updatedTenant);
954
+ } else if (registryResult.pendingRegistry) {
955
+ pendingRegistries.push(registryResult.pendingRegistry);
956
+ }
957
+ break;
958
+ case "token":
959
+ const tokenResult = handleTokenEvent({
960
+ eventData,
961
+ tokenMap,
962
+ userData,
963
+ });
964
+
965
+ if (tokenResult.isUserToken) {
966
+ userData.tokens = tokenResult.updatedUserTokens;
967
+ } else if (tokenResult.isClusterToken && tokenResult.clusterToken) {
968
+ clusterTokensMap.clear();
969
+ clusterTokensMap.set(tokenResult.tokenId, tokenResult.clusterToken);
970
+ }
971
+ break;
972
+ case "cluster":
973
+ const clusterName = eventData.spec.cluster.name;
974
+ break;
975
+ case "link":
976
+ const linkResult = handleLinkEvent({
977
+ eventData,
978
+ servicesMap,
979
+ });
980
+
981
+ if (linkResult.updatedServerService) {
982
+ servicesMap.set(
983
+ linkResult.linkServiceServer,
984
+ linkResult.updatedServerService,
985
+ );
986
+ }
987
+
988
+ if (linkResult.updatedClientService) {
989
+ servicesMap.set(
990
+ linkResult.linkServiceClient,
991
+ linkResult.updatedClientService,
992
+ );
993
+ }
994
+ break;
995
+ }
996
+ setTimeout(async () => {
997
+ rebuildHierarchy();
998
+ await updateUserWithPlatformInfo();
999
+ }, 500);
1000
+ }
1001
+ };
1002
+ const loadPlatformInfo = async (): Promise<Platform | null> => {
1003
+ if (isPlatformInfoLoaded && platformInfo) {
1004
+ return platformInfo;
1005
+ }
1006
+
1007
+ if (isPlatformInfoLoading) {
1008
+ let attempts = 0;
1009
+ while (isPlatformInfoLoading && attempts < 100) {
1010
+ await new Promise((resolve) => setTimeout(resolve, 100));
1011
+ attempts++;
1012
+ }
1013
+ return platformInfo;
1014
+ }
1015
+
1016
+ isPlatformInfoLoading = true;
1017
+
1018
+ try {
1019
+ const platformResponse = await makeGlobalWebSocketRequest(
1020
+ "misc:info",
1021
+ {},
1022
+ 30000,
1023
+ "GET",
1024
+ "PLATFORM",
1025
+ );
1026
+
1027
+ if (platformResponse) {
1028
+ platformInfo = platformResponse as Platform;
1029
+ isPlatformInfoLoaded = true;
1030
+ }
1031
+ } catch (err) {
1032
+ console.error("Error loading platform info:", err);
1033
+ platformInfo = null;
1034
+ } finally {
1035
+ isPlatformInfoLoading = false;
1036
+ }
1037
+
1038
+ return platformInfo;
1039
+ };
1040
+
1041
+ const updateUserWithPlatformInfo = async () => {
1042
+ try {
1043
+ const platform = await loadPlatformInfo();
1044
+ const mapTenants = Array.from(tenantsMap.values());
1045
+
1046
+ const existingTenants = user.tenants;
1047
+ const combinedTenantsMap = new Map<string, Tenant>();
1048
+ existingTenants.forEach((tenant) => {
1049
+ combinedTenantsMap.set(tenant.id, tenant);
1050
+ });
1051
+ mapTenants.forEach((tenant) => {
1052
+ combinedTenantsMap.set(tenant.id, tenant);
1053
+ });
1054
+ user = new User(
1055
+ userData,
1056
+ Array.from(tenantsMap.values()),
1057
+ Array.from(organizationsMap.values()),
1058
+ platform || undefined,
1059
+ );
1060
+
1061
+ eventHelper.user.publish.loaded(user);
1062
+ if (
1063
+ environmentsMap.size > 0 &&
1064
+ !isLoadingReporting &&
1065
+ !hasLoadedReportingOnce
1066
+ ) {
1067
+ setTimeout(() => {
1068
+ if (!isLoadingReporting && !hasLoadedReportingOnce) {
1069
+ loadAllEnvironmentsReporting().catch((err) => {
1070
+ console.error("Error in background reporting loading:", err);
1071
+ });
1072
+ }
1073
+ }, 2000);
1074
+ }
1075
+ } catch (err) {
1076
+ console.error("Error updating user with platform info:", err);
1077
+ user = new User(
1078
+ userData,
1079
+ Array.from(tenantsMap.values()),
1080
+ Array.from(organizationsMap.values()),
1081
+ );
1082
+ eventHelper.user.publish.loaded(user);
1083
+ }
1084
+ };
1085
+ export const getReferenceDomain = () => {
1086
+ return user.platform?.referenceDomain.replace(/\./g, "-");
1087
+ };
1088
+ const handleOperationSuccess = (operation: PendingOperation, response: any) => {
1089
+ const { action, entityType, entityName, originalData } = operation;
1090
+ switch (entityType.toLowerCase()) {
1091
+ case "tenant":
1092
+ if (action === "DELETE") {
1093
+ tenantsMap.delete(entityName);
1094
+ } else {
1095
+ if (originalData) {
1096
+ if (action === "CREATE") {
1097
+ const eventData = response.payload.data;
1098
+ const newTenant: Tenant = handleTenantOperationSuccess(
1099
+ entityName,
1100
+ eventData,
1101
+ userData,
1102
+ );
1103
+ tenantsMap.set(entityName, newTenant);
1104
+ eventHelper.tenant.publish.created(newTenant);
1105
+ } else if (action === "UPDATE") {
1106
+ const updatedTenant = { ...originalData, status: "active" };
1107
+ tenantsMap.set(entityName, updatedTenant);
1108
+ eventHelper.tenant.publish.updated(updatedTenant);
1109
+ }
1110
+ }
1111
+ }
1112
+ break;
1113
+
1114
+ case "service":
1115
+ const svcSuccessResult = handleServiceOperationSuccess({
1116
+ action,
1117
+ entityName,
1118
+ originalData,
1119
+ responsePayload: operation.responsePayload,
1120
+ servicesMap,
1121
+ revisionsMap,
1122
+ roleMap,
1123
+ });
1124
+
1125
+ if (action === "GET_CHANNELS") {
1126
+ let serviceId: string;
1127
+ let serviceName: string;
1128
+
1129
+ const payloadTenant = operation.originalData?.payload?.tenant;
1130
+ const payloadService = operation.originalData?.payload?.service;
1131
+
1132
+ if (payloadTenant && payloadService) {
1133
+ serviceId = `${payloadTenant}/${payloadService}`;
1134
+ serviceName = payloadService;
1135
+ } else {
1136
+ const entityNameParts = operation.entityName.split("/");
1137
+ if (entityNameParts.length === 2) {
1138
+ serviceId = operation.entityName;
1139
+ serviceName = entityNameParts[1];
1140
+ } else {
1141
+ console.warn(
1142
+ `Cannot determine serviceId for GET_CHANNELS. EntityName: ${operation.entityName}`,
1143
+ );
1144
+ break;
1145
+ }
1146
+ }
1147
+
1148
+ const service = servicesMap.get(serviceId);
1149
+ if (service) {
1150
+ const channelsInfo = mapChannelsFromApiData(
1151
+ response.payload.data,
1152
+ serviceName,
1153
+ );
1154
+ service.serverChannels = channelsInfo.serverChannels;
1155
+ service.clientChannels = channelsInfo.clientChannels;
1156
+ service.duplexChannels = channelsInfo.duplexChannels;
1157
+ servicesMap.set(serviceId, service);
1158
+ } else {
1159
+ console.warn(
1160
+ `Service ${serviceId} not found when processing GET_CHANNELS response`,
1161
+ );
1162
+ }
1163
+ } else if (action === "GET_REVISION") {
1164
+ if (
1165
+ svcSuccessResult.processRevisionData &&
1166
+ svcSuccessResult.revisionData
1167
+ ) {
1168
+ try {
1169
+ const updatedService = processRevisionData(
1170
+ svcSuccessResult.updatedService!,
1171
+ svcSuccessResult.revisionData,
1172
+ );
1173
+
1174
+ if (svcSuccessResult.revisionData.revision_error) {
1175
+ updatedService.status =
1176
+ svcSuccessResult.revisionData.revision_error;
1177
+ updatedService.error = {
1178
+ code:
1179
+ svcSuccessResult.revisionData.revision_error.code ||
1180
+ "UNKNOWN_ERROR",
1181
+ message:
1182
+ svcSuccessResult.revisionData.revision_error.message ||
1183
+ "Unknown error occurred",
1184
+ };
1185
+ }
1186
+
1187
+ servicesMap.set(svcSuccessResult.serviceId, updatedService);
1188
+
1189
+ if (updatedService.role && updatedService.role.length > 0) {
1190
+ roleMap.set(svcSuccessResult.serviceId, updatedService.role);
1191
+ }
1192
+ } catch (error) {
1193
+ console.error(
1194
+ `Error processing revision data for ${svcSuccessResult.serviceId}:`,
1195
+ error,
1196
+ );
1197
+ const service = servicesMap.get(svcSuccessResult.serviceId);
1198
+ if (service) {
1199
+ const resetService = { ...service, role: [] };
1200
+ servicesMap.set(svcSuccessResult.serviceId, resetService);
1201
+ roleMap.delete(svcSuccessResult.serviceId);
1202
+ }
1203
+ }
1204
+ } else if (
1205
+ svcSuccessResult.updatedService &&
1206
+ !svcSuccessResult.processRevisionData
1207
+ ) {
1208
+ servicesMap.set(
1209
+ svcSuccessResult.serviceId,
1210
+ svcSuccessResult.updatedService,
1211
+ );
1212
+ roleMap.delete(svcSuccessResult.serviceId);
1213
+ console.warn(
1214
+ `GET_REVISION response without solution for ${svcSuccessResult.serviceId}, roles reset`,
1215
+ );
1216
+ }
1217
+ } else if (svcSuccessResult.updatedService) {
1218
+ servicesMap.set(
1219
+ svcSuccessResult.serviceId,
1220
+ svcSuccessResult.updatedService,
1221
+ );
1222
+ if (svcSuccessResult.eventType === "deployed") {
1223
+ eventHelper.service.publish.deployed(svcSuccessResult.updatedService);
1224
+ } else if (svcSuccessResult.eventType === "updated") {
1225
+ eventHelper.service.publish.updated(svcSuccessResult.updatedService);
1226
+ } else if (svcSuccessResult.eventType === "deleting") {
1227
+ eventHelper.service.publish.updated(svcSuccessResult.updatedService);
1228
+ }
1229
+ }
1230
+ break;
1231
+ case "account":
1232
+ const accSuccessResult = handleAccountOperationSuccess({
1233
+ action,
1234
+ entityName,
1235
+ originalData: originalData as Account,
1236
+ });
1237
+
1238
+ if (accSuccessResult.shouldDelete) {
1239
+ const acc = accountsMap.get(entityName);
1240
+ if (acc) {
1241
+ accountsMap.delete(entityName);
1242
+ eventHelper.notification.publish.creation(
1243
+ accSuccessResult.notification,
1244
+ );
1245
+ }
1246
+ } else if (accSuccessResult.updatedAccount) {
1247
+ accountsMap.set(entityName, accSuccessResult.updatedAccount);
1248
+ if (accSuccessResult.eventType === "created") {
1249
+ eventHelper.account.publish.created(accSuccessResult.updatedAccount);
1250
+ } else if (accSuccessResult.eventType === "updated") {
1251
+ eventHelper.account.publish.updated(accSuccessResult.updatedAccount);
1252
+ }
1253
+ eventHelper.notification.publish.creation(
1254
+ accSuccessResult.notification,
1255
+ );
1256
+ }
1257
+ break;
1258
+
1259
+ case "environment":
1260
+ const envSuccessResult = handleEnvironmentOperationSuccess({
1261
+ action,
1262
+ entityName,
1263
+ originalData: originalData as Environment,
1264
+ });
1265
+
1266
+ if (envSuccessResult.shouldDelete) {
1267
+ environmentsMap.delete(entityName);
1268
+ } else if (envSuccessResult.updatedEnvironment) {
1269
+ environmentsMap.set(entityName, envSuccessResult.updatedEnvironment);
1270
+ if (envSuccessResult.notification) {
1271
+ eventHelper.notification.publish.creation(
1272
+ envSuccessResult.notification,
1273
+ );
1274
+ }
1275
+ if (envSuccessResult.eventType === "created") {
1276
+ eventHelper.environment.publish.created(
1277
+ envSuccessResult.updatedEnvironment,
1278
+ );
1279
+ } else if (envSuccessResult.eventType === "updated") {
1280
+ eventHelper.environment.publish.updated(
1281
+ envSuccessResult.updatedEnvironment,
1282
+ );
1283
+ }
1284
+ }
1285
+ break;
1286
+ case "planprovider":
1287
+ if (action === "GET") {
1288
+ const providers: PlanProvider[] = response.payload.data.map(
1289
+ (item: any) => ({
1290
+ name: item.id.name,
1291
+ }),
1292
+ );
1293
+ eventHelper.planProviders.publish.plansLoaded(providers);
1294
+ }
1295
+ break;
1296
+ case "token":
1297
+ const tokenSuccessResult = handleTokenOperationSuccess({
1298
+ responsePayload: response.payload,
1299
+ });
1300
+ tokenMap.set(tokenSuccessResult.token.name, tokenSuccessResult.token);
1301
+ break;
1302
+ case "user":
1303
+ if (action === "DELETE") {
1304
+ userData = handleUserOperationError();
1305
+ user = new User(userData);
1306
+ }
1307
+ break;
1308
+ }
1309
+
1310
+ setTimeout(async () => {
1311
+ rebuildHierarchy();
1312
+ await updateUserWithPlatformInfo();
1313
+ }, 100);
1314
+ };
1315
+
1316
+ const handleOperationError = (operation: PendingOperation, error: any) => {
1317
+ const { action, entityType, entityName, originalData } = operation;
1318
+
1319
+ console.error(
1320
+ `Operation ${action} on ${entityType} '${entityName}' failed:`,
1321
+ error,
1322
+ );
1323
+ switch (entityType.toLowerCase()) {
1324
+ case "tenant":
1325
+ if (action === "CREATE") {
1326
+ eventHelper.tenant.publish.creationError(originalData);
1327
+ } else if (action === "UPDATE") {
1328
+ eventHelper.tenant.publish.updateError(originalData);
1329
+ } else if (action === "DELETE") {
1330
+ eventHelper.tenant.publish.deletionError(originalData);
1331
+ }
1332
+ break;
1333
+
1334
+ case "service":
1335
+ const svcErrorResult = handleServiceOperationError({
1336
+ action,
1337
+ entityName,
1338
+ originalData,
1339
+ error,
1340
+ servicesMap,
1341
+ roleMap,
1342
+ });
1343
+ if (svcErrorResult.eventType === "deploymentError") {
1344
+ eventHelper.service.publish.deploymentError(originalData);
1345
+ } else if (svcErrorResult.eventType === "updateError") {
1346
+ eventHelper.service.publish.updateError(originalData);
1347
+ } else if (svcErrorResult.eventType === "deletionError") {
1348
+ eventHelper.service.publish.deletionError(originalData);
1349
+ }
1350
+ if (svcErrorResult.shouldResetRoles) {
1351
+ const service = servicesMap.get(svcErrorResult.serviceId);
1352
+ if (service) {
1353
+ const resetService = { ...service, role: [] };
1354
+ servicesMap.set(svcErrorResult.serviceId, resetService);
1355
+ roleMap.delete(svcErrorResult.serviceId);
1356
+ console.warn(
1357
+ `GET_REVISION failed for ${svcErrorResult.serviceId}, roles have been reset`,
1358
+ );
1359
+ }
1360
+ }
1361
+ break;
1362
+ case "account":
1363
+ const accErrorResult = handleAccountOperationError({
1364
+ action,
1365
+ entityName,
1366
+ originalData: originalData as Account,
1367
+ error,
1368
+ });
1369
+
1370
+ if (accErrorResult.shouldDelete) {
1371
+ accountsMap.delete(entityName);
1372
+ } else if (accErrorResult.updatedAccount) {
1373
+ accountsMap.set(entityName, accErrorResult.updatedAccount);
1374
+ }
1375
+ if (accErrorResult.eventType === "creationError") {
1376
+ eventHelper.account.publish.creationError(originalData);
1377
+ } else if (accErrorResult.eventType === "updateError") {
1378
+ eventHelper.account.publish.updateError(originalData);
1379
+ } else if (accErrorResult.eventType === "deletionError") {
1380
+ eventHelper.account.publish.deletionError(originalData);
1381
+ }
1382
+ eventHelper.notification.publish.creation(accErrorResult.notification);
1383
+ break;
1384
+ case "environment":
1385
+ const envErrorResult = handleEnvironmentOperationError({
1386
+ action,
1387
+ entityName,
1388
+ originalData: originalData as Environment,
1389
+ error,
1390
+ });
1391
+ if (envErrorResult.eventType === "creationError") {
1392
+ eventHelper.environment.publish.creationError(originalData);
1393
+ } else if (envErrorResult.eventType === "updateError") {
1394
+ eventHelper.environment.publish.updateError(originalData);
1395
+ } else if (envErrorResult.eventType === "deletionError") {
1396
+ eventHelper.environment.publish.deletionError(originalData);
1397
+ }
1398
+ eventHelper.notification.publish.creation(envErrorResult.notification);
1399
+ environmentsMap.set(entityName, envErrorResult.updatedEnvironment);
1400
+ break;
1401
+ }
1402
+ };
1403
+ const handleDeleteEvent = async (message: WSMessage) => {
1404
+ const keyParts = parseKeyPath(message.payload.key);
1405
+ const tenantName = keyParts.tenant;
1406
+ if (keyParts.service && !keyParts.revision) {
1407
+ const serviceName = `${keyParts.tenant}/${keyParts.service}`;
1408
+ const deletedService = servicesMap.get(serviceName);
1409
+ if (deletedService) {
1410
+ servicesMap.delete(serviceName);
1411
+ const revisionsToDelete: string[] = [];
1412
+ revisionsMap.forEach((revision, key) => {
1413
+ if (key.startsWith(serviceName)) {
1414
+ revisionsToDelete.push(key);
1415
+ }
1416
+ });
1417
+ revisionsToDelete.forEach((key) => revisionsMap.delete(key));
1418
+ roleMap.delete(serviceName);
1419
+ eventHelper.service.publish.deleted(deletedService);
1420
+ } else {
1421
+ console.warn(`Service ${serviceName} not found in map for deletion`);
1422
+ }
1423
+ }
1424
+ if (keyParts.environment && !keyParts.service) {
1425
+ const environmentName = keyParts.environment;
1426
+ const deletedEnv = environmentsMap.get(environmentName)!;
1427
+ eventHelper.environment.publish.deleted(deletedEnv);
1428
+ const envNotification: Notification = {
1429
+ type: "success",
1430
+ subtype: "environment-deleted",
1431
+ date: Date.now().toString(),
1432
+ callToAction: false,
1433
+ status: "unread",
1434
+ data: {
1435
+ environment: deletedEnv.name,
1436
+ account: deletedEnv.account,
1437
+ tenant: deletedEnv.tenant,
1438
+ },
1439
+ };
1440
+ eventHelper.notification.publish.creation(envNotification);
1441
+ environmentsMap.delete(environmentName);
1442
+ }
1443
+ if (keyParts.account && !keyParts.environment) {
1444
+ const accountName = keyParts.account;
1445
+ const deletedAccount = accountsMap.get(accountName)!;
1446
+ eventHelper.account.publish.deleted(deletedAccount);
1447
+ const accountNotification: Notification = {
1448
+ type: "success",
1449
+ subtype: "account-deleted",
1450
+ date: Date.now().toString(),
1451
+ callToAction: false,
1452
+ status: "unread",
1453
+ data: {
1454
+ account: deletedAccount.name,
1455
+ tenant: deletedAccount.tenant,
1456
+ },
1457
+ };
1458
+ eventHelper.notification.publish.creation(accountNotification);
1459
+ accountsMap.delete(accountName);
1460
+ }
1461
+ if (keyParts.domain) {
1462
+ const domainName = keyParts.domain;
1463
+ const domainTenant = tenantsMap.get(tenantName);
1464
+ if (domainTenant) {
1465
+ const existingIndex = domainTenant.resources.findIndex(
1466
+ (res) => res.name === domainName && res.type === "domain",
1467
+ );
1468
+ if (existingIndex !== -1) {
1469
+ eventHelper.resource.publish.deleted(
1470
+ domainTenant.resources[existingIndex],
1471
+ );
1472
+ domainTenant.resources.splice(existingIndex, 1);
1473
+ tenantsMap.set(tenantName, domainTenant);
1474
+ } else {
1475
+ console.warn(`Intento de eliminar recurso inexistente: ${domainName}`);
1476
+ }
1477
+ }
1478
+ }
1479
+ if (keyParts.port) {
1480
+ const portName = keyParts.port;
1481
+ const portTenant = tenantsMap.get(tenantName);
1482
+ if (portTenant) {
1483
+ const existingIndex = portTenant.resources.findIndex(
1484
+ (res) => res.name === portName && res.type === "port",
1485
+ );
1486
+ if (existingIndex !== -1) {
1487
+ eventHelper.resource.publish.deleted(
1488
+ portTenant.resources[existingIndex],
1489
+ );
1490
+ portTenant.resources.splice(existingIndex, 1);
1491
+ tenantsMap.set(tenantName, portTenant);
1492
+ } else {
1493
+ console.warn(`Intento de eliminar recurso inexistente: ${portTenant}`);
1494
+ }
1495
+ }
1496
+ }
1497
+ if (keyParts.secret) {
1498
+ const secretName = keyParts.secret;
1499
+ const secretTenant = tenantsMap.get(tenantName);
1500
+ if (secretTenant) {
1501
+ const existingIndex = secretTenant.resources.findIndex(
1502
+ (res) => res.name === secretName && res.type === "secret",
1503
+ );
1504
+ if (existingIndex !== -1) {
1505
+ eventHelper.resource.publish.deleted(
1506
+ secretTenant.resources[existingIndex],
1507
+ );
1508
+ secretTenant.resources.splice(existingIndex, 1);
1509
+ tenantsMap.set(tenantName, secretTenant);
1510
+ } else {
1511
+ console.warn(`Intento de eliminar recurso inexistente: ${secretName}`);
1512
+ }
1513
+ }
1514
+ }
1515
+ if (keyParts.volume) {
1516
+ const volumeName = keyParts.volume;
1517
+ const volumeTenant = tenantsMap.get(tenantName);
1518
+ if (volumeTenant) {
1519
+ const existingIndex = volumeTenant.resources.findIndex(
1520
+ (res) => res.name === volumeName && res.type === "volume",
1521
+ );
1522
+ if (existingIndex !== -1) {
1523
+ eventHelper.resource.publish.deleted(
1524
+ volumeTenant.resources[existingIndex],
1525
+ );
1526
+ volumeTenant.resources.splice(existingIndex, 1);
1527
+ tenantsMap.set(tenantName, volumeTenant);
1528
+ } else {
1529
+ console.warn(`Intento de eliminar recurso inexistente: ${volumeName}`);
1530
+ }
1531
+ }
1532
+ }
1533
+ if (keyParts.ca) {
1534
+ const caName = keyParts.ca;
1535
+ const caTenant = tenantsMap.get(tenantName);
1536
+ if (caTenant) {
1537
+ const existingIndex = caTenant.resources.findIndex(
1538
+ (res) => res.name === caName && res.type === "ca",
1539
+ );
1540
+ if (existingIndex !== -1) {
1541
+ eventHelper.resource.publish.deleted(caTenant.resources[existingIndex]);
1542
+ caTenant.resources.splice(existingIndex, 1);
1543
+ tenantsMap.set(tenantName, caTenant);
1544
+ } else {
1545
+ console.warn(`Intento de eliminar recurso inexistente: ${caName}`);
1546
+ }
1547
+ }
1548
+ }
1549
+ if (keyParts.certificate) {
1550
+ const certName = keyParts.certificate;
1551
+ const certTenant = tenantsMap.get(tenantName);
1552
+ if (certTenant) {
1553
+ const existingIndex = certTenant.resources.findIndex(
1554
+ (res) => res.name === certName && res.type === "certificate",
1555
+ );
1556
+ if (existingIndex !== -1) {
1557
+ eventHelper.resource.publish.deleted(
1558
+ certTenant.resources[existingIndex],
1559
+ );
1560
+ certTenant.resources.splice(existingIndex, 1);
1561
+ tenantsMap.set(tenantName, certTenant);
1562
+ } else {
1563
+ console.warn(`Intento de eliminar recurso inexistente: ${certName}`);
1564
+ }
1565
+ }
1566
+ }
1567
+ if (keyParts.dregistry) {
1568
+ const registryName = keyParts.dregistry;
1569
+ const registryTenant = tenantsMap.get(tenantName);
1570
+
1571
+ if (registryTenant) {
1572
+ const existingIndex = registryTenant.registry.findIndex(
1573
+ (reg) => reg.domain === registryName,
1574
+ );
1575
+
1576
+ if (existingIndex !== -1) {
1577
+ const deletedRegistry = registryTenant.registry[existingIndex];
1578
+ const deleteRegistryBody = {
1579
+ tenant: registryTenant,
1580
+ registry: deletedRegistry,
1581
+ };
1582
+ eventHelper.tenant?.publish?.deleteRegistry(deleteRegistryBody);
1583
+ registryTenant.registry.splice(existingIndex, 1);
1584
+ tenantsMap.set(tenantName, registryTenant);
1585
+ const registryNotification: Notification = {
1586
+ type: "success",
1587
+ subtype: "registry-deleted",
1588
+ date: Date.now().toString(),
1589
+ status: "unread",
1590
+ callToAction: false,
1591
+ data: {
1592
+ registry: deletedRegistry,
1593
+ tenant: registryTenant,
1594
+ },
1595
+ };
1596
+ eventHelper.notification.publish.creation(registryNotification);
1597
+ } else {
1598
+ console.warn(
1599
+ `Attempt to delete non-existent registry: ${registryName}`,
1600
+ );
1601
+ }
1602
+ }
1603
+ }
1604
+ if (keyParts.link) {
1605
+ const linkId = keyParts.link;
1606
+ let deletedLinkInfo: {
1607
+ origin: string;
1608
+ target: string;
1609
+ originChannel: string;
1610
+ targetChannel: string;
1611
+ tenant: string;
1612
+ } = {
1613
+ origin: "",
1614
+ target: "",
1615
+ originChannel: "",
1616
+ targetChannel: "",
1617
+ tenant: "",
1618
+ };
1619
+
1620
+ servicesMap.forEach((service, serviceId) => {
1621
+ const linkIndex = service.links.findIndex((link) => link.name === linkId);
1622
+ if (linkIndex !== -1) {
1623
+ const deletedLink = service.links[linkIndex];
1624
+
1625
+ if (!deletedLinkInfo) {
1626
+ deletedLinkInfo = {
1627
+ origin: deletedLink.origin,
1628
+ target: deletedLink.target,
1629
+ originChannel: deletedLink.originChannel || "",
1630
+ targetChannel: deletedLink.targetChannel || "",
1631
+ tenant: service.tenant,
1632
+ };
1633
+ }
1634
+
1635
+ service.links.splice(linkIndex, 1);
1636
+ servicesMap.set(serviceId, service);
1637
+ }
1638
+ });
1639
+
1640
+ if (deletedLinkInfo) {
1641
+ const unlinkNotification: Notification = {
1642
+ type: "success",
1643
+ subtype: "service-unlinked",
1644
+ date: Date.now().toString(),
1645
+ status: "unread",
1646
+ callToAction: false,
1647
+ data: {
1648
+ originService: deletedLinkInfo.origin,
1649
+ targetService: deletedLinkInfo.target,
1650
+ originChannel: deletedLinkInfo.originChannel,
1651
+ targetChannel: deletedLinkInfo.targetChannel,
1652
+ tenant: deletedLinkInfo.tenant,
1653
+ },
1654
+ };
1655
+ eventHelper.notification.publish.creation(unlinkNotification);
1656
+ }
1657
+ }
1658
+ if (keyParts.token && keyParts.user) {
1659
+ const tokenId = keyParts.token;
1660
+ const tokenIndex = userData.tokens.findIndex((t) => t.name === tokenId);
1661
+ if (tokenIndex !== -1) {
1662
+ const deletedToken = userData.tokens[tokenIndex];
1663
+ userData.tokens.splice(tokenIndex, 1);
1664
+
1665
+ const tokenNotification: Notification = {
1666
+ type: "success",
1667
+ subtype: "token-deleted",
1668
+ date: Date.now().toString(),
1669
+ status: "unread",
1670
+ callToAction: false,
1671
+ data: {
1672
+ tokenName: deletedToken.name,
1673
+ tenant: deletedToken.tenant,
1674
+ },
1675
+ };
1676
+ eventHelper.notification.publish.creation(tokenNotification);
1677
+ }
1678
+ }
1679
+ rebuildHierarchy();
1680
+ await updateUserWithPlatformInfo();
1681
+ };
1682
+ const getChannelsInfo = async (
1683
+ serviceId: string,
1684
+ tenantId: string,
1685
+ ): Promise<{
1686
+ serverChannels: Channel[];
1687
+ clientChannels: Channel[];
1688
+ duplexChannels: Channel[];
1689
+ }> => {
1690
+ try {
1691
+ const service = servicesMap.get(`${tenantId}/${serviceId}`);
1692
+ const response = await makeGlobalWebSocketRequest(
1693
+ "service:report_channels_service",
1694
+ {
1695
+ tenant: tenantId,
1696
+ service: serviceId,
1697
+ },
1698
+ 30000,
1699
+ "GET_CHANNELS",
1700
+ `${tenantId}/${serviceId}`,
1701
+ "service",
1702
+ service,
1703
+ );
1704
+ return mapChannelsFromApiData(response.data, serviceId);
1705
+ } catch (error) {
1706
+ console.error(`Error getting channels for service ${serviceId}:`, error);
1707
+ return {
1708
+ serverChannels: [],
1709
+ clientChannels: [],
1710
+ duplexChannels: [],
1711
+ };
1712
+ }
1713
+ };
1714
+
1715
+ const updateEnvironmentsCloudProvider = (
1716
+ accountId: string,
1717
+ cloudProviderName: string,
1718
+ ) => {
1719
+ environmentsMap.forEach((environment, envId) => {
1720
+ if (environment.account === accountId) {
1721
+ environment.cloudProvider = cloudProviderName;
1722
+ environmentsMap.set(envId, environment);
1723
+ }
1724
+ });
1725
+ pendingCloudProviderUpdates = pendingCloudProviderUpdates.filter(
1726
+ (pending) => pending.accountId !== accountId,
1727
+ );
1728
+ };
1729
+
1730
+ const loadReportingForEnvironment = async (environment: Environment) => {
1731
+ try {
1732
+ const reportingData = await getReporting(environment);
1733
+ if (
1734
+ reportingData !== undefined &&
1735
+ reportingData !== null &&
1736
+ Array.isArray(reportingData)
1737
+ ) {
1738
+ const updatedEnvironment = environmentsMap.get(environment.id);
1739
+
1740
+ if (!updatedEnvironment) {
1741
+ console.warn(`Environment ${environment.id} not found in map`);
1742
+ return;
1743
+ }
1744
+
1745
+ if (!updatedEnvironment.usage.current.cpuConsuption) {
1746
+ updatedEnvironment.usage.current.cpuConsuption = [];
1747
+ }
1748
+ const cpuValues = reportingData.map((dataPoint: any) => {
1749
+ const fromTime = Date.parse(dataPoint.From);
1750
+ const toTime = Date.parse(dataPoint.To);
1751
+ const durationMs = toTime - fromTime;
1752
+ const milliCores =
1753
+ durationMs > 0 ? dataPoint.VCPUMillisMs / durationMs : 0;
1754
+ return milliCores;
1755
+ });
1756
+
1757
+ updatedEnvironment.usage.current.cpuConsuption.push(...cpuValues);
1758
+
1759
+ if (
1760
+ updatedEnvironment.usage.current.cpuConsuption.length >
1761
+ REPORTING_ITERATIONS
1762
+ ) {
1763
+ updatedEnvironment.usage.current.cpuConsuption =
1764
+ updatedEnvironment.usage.current.cpuConsuption.slice(
1765
+ -REPORTING_ITERATIONS,
1766
+ );
1767
+ }
1768
+
1769
+ environmentsMap.set(environment.id, updatedEnvironment);
1770
+ }
1771
+ } catch (error) {
1772
+ console.error(
1773
+ `Error loading reporting for environment ${environment.name}:`,
1774
+ error,
1775
+ );
1776
+ }
1777
+ };
1778
+
1779
+ const loadAllEnvironmentsReporting = async () => {
1780
+ if (isLoadingReporting || hasLoadedReportingOnce) {
1781
+ return;
1782
+ }
1783
+ isLoadingReporting = true;
1784
+ try {
1785
+ const environments = Array.from(environmentsMap.values());
1786
+ await Promise.all(
1787
+ environments.map((env) => loadReportingForEnvironment(env)),
1788
+ );
1789
+ hasLoadedReportingOnce = true;
1790
+ } catch (error) {
1791
+ console.error("Error loading reporting for all environments:", error);
1792
+ isLoadingReporting = false;
1793
+ } finally {
1794
+ isLoadingReporting = false;
1795
+ }
1796
+ };
1797
+ export const updateUserComplete = (updatedUserData: UserData): User => {
1798
+ try {
1799
+ const preservedNotifications =
1800
+ userData.notifications || updatedUserData.notifications || [];
1801
+ userData = {
1802
+ id: updatedUserData.id || userData.id,
1803
+ name: updatedUserData.name || userData.name,
1804
+ surname: updatedUserData.surname || userData.surname,
1805
+ provider: updatedUserData.provider || userData.provider || [],
1806
+ notificationsEnabled:
1807
+ updatedUserData.notificationsEnabled ||
1808
+ userData.notificationsEnabled ||
1809
+ "",
1810
+ organizations: updatedUserData.organizations || [],
1811
+ clusterTokens: updatedUserData.clusterTokens || [],
1812
+ tokens: updatedUserData.tokens || [],
1813
+ tenants: updatedUserData.tenants || [],
1814
+ axebowPlan:
1815
+ updatedUserData.axebowPlan || userData.axebowPlan || "freemium",
1816
+ companyName: updatedUserData.companyName || userData.companyName || "",
1817
+ rol: updatedUserData.rol || userData.rol || "",
1818
+ notifications: preservedNotifications,
1819
+ plans: updatedUserData.plans,
1820
+ status: updatedUserData.status,
1821
+ };
1822
+ user = new User(
1823
+ userData,
1824
+ Array.from(tenantsMap.values()),
1825
+ Array.from(organizationsMap.values()),
1826
+ platformInfo || undefined,
1827
+ );
1828
+ return user;
1829
+ } catch (error) {
1830
+ console.error("Error updating user in websocket-manager:", error);
1831
+ return user;
1832
+ }
1833
+ };