@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,627 @@
1
+
2
+ import { Channel, Service, Tenant, Usage } from "@hestekumori/aurora-interfaces";
3
+ import { getTimestamp } from "../utils/utils";
4
+
5
+ interface Role {
6
+ name: string;
7
+ instances: any[];
8
+ logo?: string;
9
+ category?: string;
10
+ version?: string;
11
+ description?: string;
12
+ resource?: any[];
13
+ parameters?: { [key: string]: string }[];
14
+ registry?: string;
15
+ imageTag?: string;
16
+ entrypoint?: string;
17
+ cmd?: string;
18
+ scalling?: {
19
+ cpu: { up: string; down: string };
20
+ memory: { up: string; down: string };
21
+ instances: { max: number; min: number };
22
+ histeresys: string;
23
+ };
24
+ hsize?: number;
25
+ }
26
+
27
+ interface Revision {
28
+ service: string;
29
+ revision: string;
30
+ usage: Usage;
31
+ status: {
32
+ code: string;
33
+ message: string;
34
+ timestamp: string;
35
+ args: string[];
36
+ };
37
+ errorCode?: string;
38
+ errorMsg?: string;
39
+ createdAt?: string;
40
+ }
41
+
42
+ interface HandleServiceEventParams {
43
+ entityId: string;
44
+ eventData: any;
45
+ parentParts: { [entity: string]: string };
46
+ servicesMap: Map<string, Service>;
47
+ revisionsMap: Map<string, Revision>;
48
+ roleMap: Map<string, Role[]>;
49
+ tenantsMap: Map<string, Tenant>;
50
+ pendingRevisionErrors: Array<{ service: string; revision: Revision }>;
51
+ pendingProjects: Array<{ tenant: string; project: string }>;
52
+ }
53
+
54
+ interface HandleServiceEventResult {
55
+ service: Service;
56
+ serviceFullKey: string;
57
+ wasDeployed: boolean;
58
+ pendingProject: { tenant: string; project: string } | null;
59
+ updatedPendingRevisionErrors: Array<{ service: string; revision: Revision }>;
60
+ }
61
+ interface HandleServiceOperationSuccessParams {
62
+ action: string;
63
+ entityName: string;
64
+ originalData: any;
65
+ responsePayload: any;
66
+ servicesMap: Map<string, Service>;
67
+ revisionsMap: Map<string, Revision>;
68
+ roleMap: Map<string, Role[]>;
69
+ }
70
+
71
+ interface HandleServiceOperationSuccessResult {
72
+ updatedService: Service | null;
73
+ shouldDelete: boolean;
74
+ eventType: "deployed" | "updated" | "deleting" | null;
75
+ processRevisionData: boolean;
76
+ revisionData: any;
77
+ serviceId: string;
78
+ }
79
+
80
+ interface HandleServiceOperationErrorParams {
81
+ action: string;
82
+ entityName: string;
83
+ originalData: any;
84
+ error: any;
85
+ servicesMap: Map<string, Service>;
86
+ roleMap: Map<string, Role[]>;
87
+ }
88
+
89
+ interface HandleServiceOperationErrorResult {
90
+ eventType:
91
+ | "deploymentError"
92
+ | "updateError"
93
+ | "deletionError"
94
+ | "revisionError"
95
+ | null;
96
+ serviceId: string;
97
+ shouldResetRoles: boolean;
98
+ }
99
+
100
+ /**
101
+ * Parse key path to extract entity names
102
+ */
103
+ const parseKeyPath = (key: string): { [entity: string]: string } => {
104
+ if (!key) {
105
+ return {};
106
+ }
107
+ const cleanKey = key?.startsWith("/") ? key.slice(1) : key;
108
+ const parts = cleanKey.split("/");
109
+ const result: { [entity: string]: string } = {};
110
+ for (let i = 0; i < parts.length; i += 2) {
111
+ if (parts[i] && parts[i + 1]) {
112
+ const entityType = parts[i];
113
+ const entityName = parts[i + 1];
114
+ result[entityType] = entityName;
115
+ }
116
+ }
117
+ return result;
118
+ };
119
+
120
+ /**
121
+ * Get default usage object
122
+ */
123
+ const getDefaultUsage = (): Usage => ({
124
+ current: {
125
+ cpu: 0,
126
+ memory: 0,
127
+ storage: 0,
128
+ volatileStorage: 0,
129
+ nonReplicatedStorage: 0,
130
+ persistentStorage: 0,
131
+ },
132
+ limit: {
133
+ cpu: { max: 0, min: 0 },
134
+ memory: { max: 0, min: 0 },
135
+ storage: { max: 0, min: 0 },
136
+ volatileStorage: { max: 0, min: 0 },
137
+ nonReplicatedStorage: { max: 0, min: 0 },
138
+ persistentStorage: { max: 0, min: 0 },
139
+ },
140
+ cost: 0,
141
+ });
142
+
143
+ /**
144
+ * Collect revisions for a service from revisionsMap
145
+ */
146
+ const collectServiceRevisions = (
147
+ entityId: string,
148
+ revisionsMap: Map<string, Revision>,
149
+ ): string[] => {
150
+ const serviceRevisions: string[] = [];
151
+ revisionsMap.forEach((revision, key) => {
152
+ if (revision.service === entityId) {
153
+ serviceRevisions.push(revision.revision);
154
+ }
155
+ });
156
+ return serviceRevisions;
157
+ };
158
+
159
+ /**
160
+ * Determine final status and error based on timestamps
161
+ */
162
+ const determineFinalStatusAndError = (
163
+ existingService: Service | undefined,
164
+ eventData: any,
165
+ pendingRevisionErrors: Array<{ service: string; revision: Revision }>,
166
+ entityId: string,
167
+ ): {
168
+ finalStatus: any;
169
+ finalError: any;
170
+ pendingErrorIndex: number;
171
+ } => {
172
+ const incomingStatus = eventData.status.state;
173
+ const incomingTs = getTimestamp(incomingStatus.timestamp);
174
+ const currentTs = getTimestamp(existingService?.status?.timestamp);
175
+
176
+ const isNewer = !existingService || incomingTs > currentTs;
177
+ let finalStatus = existingService?.status;
178
+ let finalError = existingService?.error;
179
+
180
+ if (isNewer) {
181
+ finalStatus = incomingStatus;
182
+
183
+ if (eventData.status.error) {
184
+ finalError = eventData.status.error;
185
+ } else {
186
+ finalError = undefined;
187
+ }
188
+ }
189
+ const pendingErrorIndex = pendingRevisionErrors.findIndex(
190
+ (pending) => pending.service === entityId,
191
+ );
192
+
193
+ if (pendingErrorIndex !== -1) {
194
+ const pendingError = pendingRevisionErrors[pendingErrorIndex];
195
+ const pendingTs = getTimestamp(pendingError.revision.status.timestamp);
196
+ const currentDecisionTs = getTimestamp(finalStatus?.timestamp);
197
+
198
+ if (pendingTs > currentDecisionTs) {
199
+ finalStatus = pendingError.revision.status;
200
+ finalError = {
201
+ code: pendingError.revision.errorCode || "",
202
+ message: pendingError.revision.errorMsg || "",
203
+ timestamp: pendingError.revision.status.timestamp || "",
204
+ };
205
+ }
206
+ }
207
+
208
+ return { finalStatus, finalError, pendingErrorIndex };
209
+ };
210
+ /**
211
+ * Handles the "service" event from WebSocket messages
212
+ * Processes service data updates and manages project labels
213
+ */
214
+ export const handleServiceEvent = ({
215
+ entityId,
216
+ eventData,
217
+ parentParts,
218
+ servicesMap,
219
+ revisionsMap,
220
+ roleMap,
221
+ tenantsMap,
222
+ pendingRevisionErrors,
223
+ pendingProjects,
224
+ }: HandleServiceEventParams): HandleServiceEventResult => {
225
+ const serviceFullKey = `${parentParts.tenant}/${entityId}`;
226
+ const serviceTenantId = parentParts.tenant;
227
+ const currentRevisionKey = `${serviceFullKey}-${eventData.status.revision}`;
228
+ const currentRevision = revisionsMap.get(currentRevisionKey);
229
+
230
+ const projectLabel = eventData.meta?.labels?.project;
231
+ const serviceRoles = roleMap.get(serviceFullKey) || [];
232
+ const serviceUsage = currentRevision?.usage || getDefaultUsage();
233
+ const serviceRevisions = collectServiceRevisions(entityId, revisionsMap);
234
+ const environmentPath = eventData.spec.environment;
235
+ const pathParts = parseKeyPath(environmentPath);
236
+ const existingService = servicesMap.get(serviceFullKey);
237
+ const { finalStatus, finalError, pendingErrorIndex } =
238
+ determineFinalStatusAndError(
239
+ existingService,
240
+ eventData,
241
+ pendingRevisionErrors,
242
+ entityId,
243
+ );
244
+ const updatedPendingRevisionErrors = [...pendingRevisionErrors];
245
+ if (pendingErrorIndex !== -1) {
246
+ updatedPendingRevisionErrors.splice(pendingErrorIndex, 1);
247
+ }
248
+ const baseServiceData = {
249
+ tenant: serviceTenantId,
250
+ account: eventData.spec.metadata.targetAccount || pathParts.account,
251
+ environment:
252
+ eventData.spec.metadata.targetEnvironment || pathParts.environment,
253
+ name: entityId,
254
+ revisions: [...serviceRevisions].sort((a, b) => Number(b) - Number(a)),
255
+ status: finalStatus || eventData.status.state,
256
+ error: finalError,
257
+ role: serviceRoles.length > 0 ? serviceRoles : existingService?.role || [],
258
+ usage: serviceUsage,
259
+ lastDeployed: eventData.spec.ctstamp,
260
+ project: projectLabel || existingService?.project || "",
261
+ currentRevision:
262
+ eventData.status.revision ||
263
+ (serviceRevisions.length > 0
264
+ ? Math.max(...serviceRevisions.map(Number)).toString()
265
+ : ""),
266
+ startedAt: currentRevision?.createdAt || existingService?.startedAt || "",
267
+ };
268
+ let newService: Service;
269
+
270
+ if (existingService) {
271
+ newService = {
272
+ ...existingService,
273
+ ...baseServiceData,
274
+ };
275
+ } else {
276
+ newService = {
277
+ id: serviceFullKey,
278
+ logo: "",
279
+ description: "",
280
+ links: [],
281
+ resources: [],
282
+ parameters: [],
283
+ minReplicas: 0,
284
+ maxReplicas: 0,
285
+ registry: "",
286
+ imageName: "",
287
+ entrypoint: "",
288
+ cmd: "",
289
+ serverChannels: [],
290
+ clientChannels: [],
291
+ duplexChannels: [],
292
+ cloudProvider: "",
293
+ ...baseServiceData,
294
+ };
295
+ }
296
+ const oldStatusCode = existingService?.status?.code;
297
+ const newStatusCode = newService.status.code;
298
+
299
+ const wasDeployed =
300
+ existingService !== undefined &&
301
+ oldStatusCode !== "SERVICE_READY" &&
302
+ newStatusCode === "SERVICE_READY" &&
303
+ eventData.status.deployed === true;
304
+ let pendingProject: { tenant: string; project: string } | null = null;
305
+
306
+ if (projectLabel) {
307
+ const tenant = tenantsMap.get(serviceTenantId);
308
+ if (!tenant) {
309
+ const existingPending = pendingProjects.find(
310
+ (pending) =>
311
+ pending.tenant === serviceTenantId &&
312
+ pending.project === projectLabel,
313
+ );
314
+ if (!existingPending) {
315
+ pendingProject = {
316
+ tenant: serviceTenantId,
317
+ project: projectLabel,
318
+ };
319
+ }
320
+ }
321
+ }
322
+
323
+ return {
324
+ service: newService,
325
+ serviceFullKey,
326
+ wasDeployed,
327
+ pendingProject,
328
+ updatedPendingRevisionErrors,
329
+ };
330
+ };
331
+
332
+ /**
333
+ * Handles successful service operations
334
+ */
335
+ export const handleServiceOperationSuccess = ({
336
+ action,
337
+ entityName,
338
+ originalData,
339
+ responsePayload,
340
+ servicesMap,
341
+ revisionsMap,
342
+ roleMap,
343
+ }: HandleServiceOperationSuccessParams): HandleServiceOperationSuccessResult => {
344
+ const serviceId = originalData
345
+ ? `${originalData.tenant}/${entityName}`
346
+ : entityName;
347
+
348
+ if (action === "GET_CHANNELS") {
349
+ return {
350
+ updatedService: null,
351
+ shouldDelete: false,
352
+ eventType: null,
353
+ processRevisionData: false,
354
+ revisionData: null,
355
+ serviceId,
356
+ };
357
+ }
358
+
359
+ if (action === "GET_REVISION") {
360
+ const revisionData = responsePayload?.data;
361
+ const service = servicesMap.get(serviceId);
362
+
363
+ if (!service) {
364
+ return {
365
+ updatedService: null,
366
+ shouldDelete: false,
367
+ eventType: null,
368
+ processRevisionData: false,
369
+ revisionData: null,
370
+ serviceId,
371
+ };
372
+ }
373
+
374
+ if (!revisionData?.solution) {
375
+ const resetService = {
376
+ ...service,
377
+ role: [],
378
+ };
379
+ return {
380
+ updatedService: resetService,
381
+ shouldDelete: false,
382
+ eventType: null,
383
+ processRevisionData: false,
384
+ revisionData: null,
385
+ serviceId,
386
+ };
387
+ }
388
+ return {
389
+ updatedService: service,
390
+ shouldDelete: false,
391
+ eventType: null,
392
+ processRevisionData: true,
393
+ revisionData,
394
+ serviceId,
395
+ };
396
+ }
397
+
398
+ if (action === "DELETE") {
399
+ const existingService = servicesMap.get(serviceId);
400
+
401
+ if (existingService) {
402
+ const updatedService = {
403
+ ...existingService,
404
+ status: {
405
+ code: "REMOVING_SERVICE",
406
+ message: `Service ${entityName} is being removed`,
407
+ args: [],
408
+ timestamp: new Date().toISOString(),
409
+ },
410
+ };
411
+ return {
412
+ updatedService,
413
+ shouldDelete: false,
414
+ eventType: "deleting",
415
+ processRevisionData: false,
416
+ revisionData: null,
417
+ serviceId,
418
+ };
419
+ }
420
+
421
+ return {
422
+ updatedService: null,
423
+ shouldDelete: false,
424
+ eventType: null,
425
+ processRevisionData: false,
426
+ revisionData: null,
427
+ serviceId,
428
+ };
429
+ }
430
+ if (originalData) {
431
+ const updatedService = {
432
+ ...originalData,
433
+ status: {
434
+ code: "SERVICE_READY",
435
+ message: `Service (${entityName}) is ready.`,
436
+ args: [],
437
+ timestamp: new Date().toISOString(),
438
+ },
439
+ };
440
+
441
+ let eventType: "deployed" | "updated" | null = null;
442
+ if (action === "CREATE") {
443
+ eventType = "deployed";
444
+ } else if (action === "UPDATE") {
445
+ eventType = "updated";
446
+ }
447
+
448
+ return {
449
+ updatedService,
450
+ shouldDelete: false,
451
+ eventType,
452
+ processRevisionData: false,
453
+ revisionData: null,
454
+ serviceId,
455
+ };
456
+ }
457
+
458
+ return {
459
+ updatedService: null,
460
+ shouldDelete: false,
461
+ eventType: null,
462
+ processRevisionData: false,
463
+ revisionData: null,
464
+ serviceId,
465
+ };
466
+ };
467
+
468
+ /**
469
+ * Handles failed service operations
470
+ */
471
+ export const handleServiceOperationError = ({
472
+ action,
473
+ entityName,
474
+ originalData,
475
+ error,
476
+ servicesMap,
477
+ roleMap,
478
+ }: HandleServiceOperationErrorParams): HandleServiceOperationErrorResult => {
479
+ const serviceId = originalData
480
+ ? `${originalData.tenant}/${entityName}`
481
+ : entityName;
482
+
483
+ if (action === "CREATE") {
484
+ return {
485
+ eventType: "deploymentError",
486
+ serviceId,
487
+ shouldResetRoles: false,
488
+ };
489
+ }
490
+
491
+ if (action === "UPDATE") {
492
+ return {
493
+ eventType: "updateError",
494
+ serviceId,
495
+ shouldResetRoles: false,
496
+ };
497
+ }
498
+
499
+ if (action === "DELETE") {
500
+ return {
501
+ eventType: "deletionError",
502
+ serviceId,
503
+ shouldResetRoles: false,
504
+ };
505
+ }
506
+
507
+ if (action === "GET_REVISION") {
508
+ const service = servicesMap.get(serviceId);
509
+ if (service) {
510
+ return {
511
+ eventType: "revisionError",
512
+ serviceId,
513
+ shouldResetRoles: true,
514
+ };
515
+ }
516
+ }
517
+
518
+ return {
519
+ eventType: null,
520
+ serviceId,
521
+ shouldResetRoles: false,
522
+ };
523
+ };
524
+ export const mapChannelsFromApiData = (
525
+ apiData: any,
526
+ entityId: string,
527
+ ): {
528
+ serverChannels: Channel[];
529
+ clientChannels: Channel[];
530
+ duplexChannels: Channel[];
531
+ } => {
532
+ const serverChannels: Channel[] = [];
533
+ const clientChannels: Channel[] = [];
534
+ const duplexChannels: Channel[] = [];
535
+ const inboundSuffix = "_inbound.inbound";
536
+
537
+ const publicChannelNames = new Set<string>();
538
+
539
+ if (apiData.server) {
540
+ Object.keys(apiData.server).forEach((channelName) => {
541
+ if (channelName.endsWith(inboundSuffix)) {
542
+ const baseName = channelName.slice(0, -inboundSuffix.length);
543
+ if (apiData.server[baseName]) {
544
+ publicChannelNames.add(baseName);
545
+ }
546
+ }
547
+ });
548
+ }
549
+
550
+ if (apiData.client) {
551
+ Object.keys(apiData.client).forEach((channelName) => {
552
+ if (channelName.endsWith(inboundSuffix)) {
553
+ const baseName = channelName.slice(0, -inboundSuffix.length);
554
+ if (apiData.client[baseName]) {
555
+ publicChannelNames.add(baseName);
556
+ }
557
+ }
558
+ });
559
+ }
560
+
561
+ if (apiData.duplex) {
562
+ Object.keys(apiData.duplex).forEach((channelName) => {
563
+ if (channelName.endsWith(inboundSuffix)) {
564
+ const baseName = channelName.slice(0, -inboundSuffix.length);
565
+ if (apiData.duplex[baseName]) {
566
+ publicChannelNames.add(baseName);
567
+ }
568
+ }
569
+ });
570
+ }
571
+ if (apiData.server) {
572
+ Object.entries(apiData.server).forEach(
573
+ ([channelName, channelData]: [string, any]) => {
574
+ if (channelName.endsWith(inboundSuffix)) {
575
+ return;
576
+ }
577
+ serverChannels.push({
578
+ name: channelName,
579
+ from: entityId,
580
+ to: "",
581
+ protocol: channelData.protocol as "http" | "tcp" | "https",
582
+ port: channelData.port,
583
+ portNum: channelData.port,
584
+ isPublic: publicChannelNames.has(channelName),
585
+ });
586
+ },
587
+ );
588
+ }
589
+ if (apiData.client) {
590
+ Object.entries(apiData.client).forEach(
591
+ ([channelName, channelData]: [string, any]) => {
592
+ if (channelName.endsWith(inboundSuffix)) {
593
+ return;
594
+ }
595
+ clientChannels.push({
596
+ name: channelName,
597
+ from: entityId,
598
+ to: "",
599
+ protocol: channelData.protocol as "http" | "tcp" | "https",
600
+ port: channelData.port,
601
+ portNum: channelData.port,
602
+ isPublic: publicChannelNames.has(channelName),
603
+ });
604
+ },
605
+ );
606
+ }
607
+ if (apiData.duplex) {
608
+ Object.entries(apiData.duplex).forEach(
609
+ ([channelName, channelData]: [string, any]) => {
610
+ if (channelName.endsWith(inboundSuffix)) {
611
+ return;
612
+ }
613
+ duplexChannels.push({
614
+ name: channelName,
615
+ from: entityId,
616
+ to: "",
617
+ protocol: channelData.protocol as "http" | "tcp" | "https",
618
+ port: channelData.port,
619
+ portNum: channelData.port,
620
+ isPublic: publicChannelNames.has(channelName),
621
+ });
622
+ },
623
+ );
624
+ }
625
+
626
+ return { serverChannels, clientChannels, duplexChannels };
627
+ };