@hahnpro/flow-sdk 9.6.4 → 2025.2.0-beta.1

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.
Files changed (81) hide show
  1. package/CHANGELOG.md +904 -0
  2. package/jest.config.ts +10 -0
  3. package/package.json +7 -20
  4. package/project.json +41 -0
  5. package/src/index.ts +15 -0
  6. package/src/lib/ContextManager.ts +111 -0
  7. package/src/lib/FlowApplication.ts +659 -0
  8. package/src/lib/FlowElement.ts +220 -0
  9. package/src/lib/FlowEvent.ts +73 -0
  10. package/src/lib/FlowLogger.ts +131 -0
  11. package/src/lib/FlowModule.ts +18 -0
  12. package/src/lib/RpcClient.ts +99 -0
  13. package/src/lib/TestModule.ts +14 -0
  14. package/src/lib/__pycache__/rpc_server.cpython-310.pyc +0 -0
  15. package/src/lib/amqp.ts +32 -0
  16. package/src/lib/extra-validators.ts +62 -0
  17. package/src/lib/flow.interface.ts +56 -0
  18. package/{dist/index.d.ts → src/lib/index.ts} +3 -0
  19. package/src/lib/nats.ts +140 -0
  20. package/src/lib/unit-decorators.ts +156 -0
  21. package/src/lib/unit-utils.ts +163 -0
  22. package/src/lib/units.ts +587 -0
  23. package/src/lib/utils.ts +176 -0
  24. package/test/context-manager-purpose.spec.ts +248 -0
  25. package/test/context-manager.spec.ts +55 -0
  26. package/test/context.spec.ts +180 -0
  27. package/test/event.spec.ts +155 -0
  28. package/test/extra-validators.spec.ts +84 -0
  29. package/test/flow-logger.spec.ts +104 -0
  30. package/test/flow.spec.ts +508 -0
  31. package/test/input-stream.decorator.spec.ts +379 -0
  32. package/test/long-rpc.test.py +14 -0
  33. package/test/long-running-rpc.spec.ts +60 -0
  34. package/test/message.spec.ts +57 -0
  35. package/test/mocks/logger.mock.ts +7 -0
  36. package/test/mocks/nats-connection.mock.ts +135 -0
  37. package/test/mocks/nats-prepare.reals-nats.ts +15 -0
  38. package/test/rpc.spec.ts +198 -0
  39. package/test/rpc.test.py +45 -0
  40. package/test/rx.spec.ts +92 -0
  41. package/test/unit-decorator.spec.ts +57 -0
  42. package/test/utils.spec.ts +210 -0
  43. package/test/validation.spec.ts +174 -0
  44. package/tsconfig.json +13 -0
  45. package/tsconfig.lib.json +22 -0
  46. package/tsconfig.spec.json +8 -0
  47. package/LICENSE +0 -21
  48. package/dist/ContextManager.d.ts +0 -40
  49. package/dist/ContextManager.js +0 -77
  50. package/dist/FlowApplication.d.ts +0 -85
  51. package/dist/FlowApplication.js +0 -500
  52. package/dist/FlowElement.d.ts +0 -67
  53. package/dist/FlowElement.js +0 -163
  54. package/dist/FlowEvent.d.ts +0 -25
  55. package/dist/FlowEvent.js +0 -71
  56. package/dist/FlowLogger.d.ts +0 -44
  57. package/dist/FlowLogger.js +0 -94
  58. package/dist/FlowModule.d.ts +0 -7
  59. package/dist/FlowModule.js +0 -13
  60. package/dist/RpcClient.d.ts +0 -13
  61. package/dist/RpcClient.js +0 -84
  62. package/dist/TestModule.d.ts +0 -2
  63. package/dist/TestModule.js +0 -27
  64. package/dist/amqp.d.ts +0 -14
  65. package/dist/amqp.js +0 -12
  66. package/dist/extra-validators.d.ts +0 -1
  67. package/dist/extra-validators.js +0 -51
  68. package/dist/flow.interface.d.ts +0 -48
  69. package/dist/flow.interface.js +0 -9
  70. package/dist/index.js +0 -18
  71. package/dist/nats.d.ts +0 -12
  72. package/dist/nats.js +0 -109
  73. package/dist/unit-decorators.d.ts +0 -39
  74. package/dist/unit-decorators.js +0 -156
  75. package/dist/unit-utils.d.ts +0 -8
  76. package/dist/unit-utils.js +0 -143
  77. package/dist/units.d.ts +0 -31
  78. package/dist/units.js +0 -570
  79. package/dist/utils.d.ts +0 -51
  80. package/dist/utils.js +0 -137
  81. /package/{dist → src/lib}/rpc_server.py +0 -0
@@ -0,0 +1,659 @@
1
+ import 'reflect-metadata';
2
+
3
+ import { EventLoopUtilization, performance } from 'perf_hooks';
4
+
5
+ import { API, HttpClientService, MockAPI } from '@hahnpro/hpc-api';
6
+ import { ConsumeOptions, Consumer, ConsumerMessages, DeliverPolicy, jetstreamManager } from '@nats-io/jetstream';
7
+ import { NatsConnection, ConnectionOptions as NatsConnectionOptions } from '@nats-io/nats-core';
8
+ import { AmqpConnectionManager, Channel, ChannelWrapper } from 'amqp-connection-manager';
9
+ import { CloudEvent } from 'cloudevents';
10
+ import { cloneDeep } from 'lodash';
11
+ import sizeof from 'object-sizeof';
12
+ import { PartialObserver, Subject } from 'rxjs';
13
+ import { mergeMap, tap } from 'rxjs/operators';
14
+
15
+ import { AmqpConnection, AmqpConnectionConfig, createAmqpConnection } from './amqp';
16
+ import { ContextManager } from './ContextManager';
17
+ import { ClassType, DeploymentMessage, Flow, FlowContext, FlowElementContext, LifecycleEvent, StreamOptions } from './flow.interface';
18
+ import type { FlowElement } from './FlowElement';
19
+ import { FlowEvent } from './FlowEvent';
20
+ import { FlowLogger, Logger } from './FlowLogger';
21
+ import {
22
+ createNatsConnection,
23
+ defaultConsumerConfig,
24
+ FLOWS_STREAM_NAME,
25
+ getOrCreateConsumer,
26
+ NatsEvent,
27
+ natsEventListener,
28
+ natsFlowsPrefixFlowDeployment,
29
+ publishNatsEvent,
30
+ } from './nats';
31
+ import { RpcClient } from './RpcClient';
32
+ import { delay, truncate } from './utils';
33
+
34
+ const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024; // 512kb
35
+ const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
36
+ const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
37
+
38
+ interface QueueMetrics {
39
+ size: number;
40
+ lastAdd: number;
41
+ lastRemove: number;
42
+ warnings: number;
43
+ }
44
+
45
+ interface FlowAppConfig {
46
+ logger?: Logger;
47
+ amqpConfig?: AmqpConnectionConfig;
48
+ amqpConnection?: AmqpConnectionManager;
49
+ natsConfig?: NatsConnectionOptions;
50
+ natsConnection?: NatsConnection;
51
+ apiClient?: HttpClientService;
52
+ skipApi?: boolean;
53
+ explicitInit?: boolean;
54
+ mockApi?: MockAPI;
55
+ }
56
+
57
+ export class FlowApplication {
58
+ private _api: API;
59
+ private _rpcClient: RpcClient;
60
+ private amqpChannel: ChannelWrapper;
61
+ private readonly amqpConnection: AmqpConnectionManager;
62
+ private readonly natsConnectionConfig?: NatsConnectionOptions;
63
+ private _natsConnection?: NatsConnection;
64
+ private readonly baseLogger: Logger;
65
+ private context: FlowContext;
66
+ private declarations: Record<string, ClassType<FlowElement>> = {};
67
+ private elements: Record<string, FlowElement> = {};
68
+ private initialized = false;
69
+ private readonly logger: FlowLogger;
70
+ private outputStreamMap = new Map<string, Subject<FlowEvent>>();
71
+ private outputQueueMetrics = new Map<string, QueueMetrics>();
72
+ private performanceMap = new Map<string, EventLoopUtilization>();
73
+ private readonly skipApi: boolean;
74
+ private readonly apiClient?: HttpClientService;
75
+
76
+ private readonly contextManager: ContextManager;
77
+ private natsMessageIterator: ConsumerMessages;
78
+
79
+ constructor(modules: ClassType<any>[], flow: Flow, config?: FlowAppConfig);
80
+ constructor(
81
+ modules: ClassType<any>[],
82
+ flow: Flow,
83
+ baseLogger?: Logger,
84
+ amqpConnection?: AmqpConnection,
85
+ natsConnection?: NatsConnection,
86
+ skipApi?: boolean,
87
+ explicitInit?: boolean,
88
+ );
89
+ constructor(
90
+ private modules: ClassType<any>[],
91
+ private flow: Flow,
92
+ baseLoggerOrConfig?: Logger | FlowAppConfig,
93
+ amqpConnection?: AmqpConnection,
94
+ natsConnection?: NatsConnection,
95
+ skipApi?: boolean,
96
+ explicitInit?: boolean,
97
+ mockApi?: MockAPI,
98
+ ) {
99
+ if (baseLoggerOrConfig && !(baseLoggerOrConfig as Logger).log) {
100
+ const config = baseLoggerOrConfig as FlowAppConfig;
101
+ this.baseLogger = config.logger;
102
+ this.amqpConnection = config.amqpConnection || createAmqpConnection(config.amqpConfig);
103
+ this.natsConnectionConfig = config.natsConfig;
104
+ this._natsConnection = config.natsConnection;
105
+ this.skipApi = config.skipApi || false;
106
+ explicitInit = config.explicitInit || false;
107
+ this._api = config.mockApi || null;
108
+ this.apiClient = config.apiClient;
109
+ } else {
110
+ this.baseLogger = baseLoggerOrConfig as Logger;
111
+ this.amqpConnection = amqpConnection?.managedConnection;
112
+ this._natsConnection = natsConnection;
113
+ this.skipApi = skipApi || false;
114
+ explicitInit = explicitInit || false;
115
+ this._api = mockApi || null;
116
+ }
117
+
118
+ this.logger = new FlowLogger(
119
+ { id: 'none', functionFqn: 'FlowApplication', ...flow?.context },
120
+ this.baseLogger || undefined,
121
+ this.publishNatsEventFlowlogs,
122
+ );
123
+
124
+ this.contextManager = new ContextManager(this.logger, this.flow?.properties);
125
+
126
+ process.once('uncaughtException', (err) => {
127
+ this.logger.error('Uncaught exception!');
128
+ this.logger.error(err);
129
+ this.destroy(1);
130
+ });
131
+ process.on('unhandledRejection', (reason) => {
132
+ this.logger.error('Unhandled promise rejection!');
133
+ this.logger.error(reason);
134
+ });
135
+ process.on('SIGTERM', () => {
136
+ this.logger.log('Flow Deployment is terminating');
137
+ this.destroy(0);
138
+ });
139
+
140
+ if (explicitInit !== true) {
141
+ this.init();
142
+ }
143
+ }
144
+
145
+ get rpcClient(): RpcClient {
146
+ if (!this._rpcClient && this.amqpConnection) {
147
+ this._rpcClient = new RpcClient(this.amqpConnection, this.logger);
148
+ }
149
+ return this._rpcClient;
150
+ }
151
+
152
+ get api(): API {
153
+ return this._api;
154
+ }
155
+
156
+ get natsConnection(): NatsConnection {
157
+ return this._natsConnection;
158
+ }
159
+
160
+ public getContextManager(): ContextManager {
161
+ return this.contextManager;
162
+ }
163
+
164
+ public getProperties() {
165
+ return this.contextManager.getProperties();
166
+ }
167
+
168
+ private async consumeNatsMessagesOfConsumer(consumer: Consumer, consumeOptions: ConsumeOptions) {
169
+ if (this.natsMessageIterator) {
170
+ await this.natsMessageIterator.close();
171
+ }
172
+ this.natsMessageIterator = await consumer.consume(consumeOptions);
173
+ for await (const msg of this.natsMessageIterator) {
174
+ try {
175
+ let event: CloudEvent;
176
+ try {
177
+ event = new CloudEvent(msg.json());
178
+ event.validate();
179
+ } catch (error) {
180
+ this.logger.error('Message is not a valid CloudEvent and will be discarded');
181
+ msg.ack(); // Acknowledge the message to remove it from the queue
182
+ continue;
183
+ }
184
+ await this.onMessage(event);
185
+ msg.ack();
186
+ } catch (error) {
187
+ this.logger.error('Error processing message');
188
+ this.logger.error(error);
189
+ msg.nak(1000);
190
+ }
191
+ }
192
+ }
193
+
194
+ public async init() {
195
+ if (this.initialized) return;
196
+
197
+ this.context = { ...this.flow.context };
198
+ this.contextManager.overwriteAllProperties(this.flow.properties ?? {});
199
+
200
+ try {
201
+ if (!this.skipApi && !(this._api instanceof MockAPI)) {
202
+ const { owner } = this.context;
203
+ // only create real API if it should not be skipped and is not already a mock
204
+ this._api = new API(this.apiClient, { activeOrg: owner?.id });
205
+ }
206
+ } catch (err) {
207
+ this.logger.error(err?.message || err);
208
+ }
209
+
210
+ const logErrorAndExit = async (err: string) => {
211
+ this.logger.error(new Error(err));
212
+ await this.destroy(1);
213
+ };
214
+
215
+ if (!this._natsConnection && this.natsConnectionConfig) {
216
+ try {
217
+ this._natsConnection = await createNatsConnection(this.natsConnectionConfig);
218
+ } catch (err) {
219
+ await logErrorAndExit(`Could not connect to the NATS-Servers: ${err}`);
220
+ }
221
+ }
222
+
223
+ if (this._natsConnection && !this._natsConnection.isClosed() && this.context?.deploymentId !== undefined) {
224
+ try {
225
+ const consumerOptions = {
226
+ ...defaultConsumerConfig,
227
+ name: `flow-deployment-${this.context.deploymentId}`,
228
+ filter_subject: `${natsFlowsPrefixFlowDeployment}.${this.context.deploymentId}.*`,
229
+ inactive_threshold: 10 * 60 * 1_000_000_000, // 10 mins
230
+ deliver_policy: DeliverPolicy.New,
231
+ };
232
+ const consumer = await getOrCreateConsumer(
233
+ this.logger,
234
+ this._natsConnection,
235
+ FLOWS_STREAM_NAME,
236
+ consumerOptions.name,
237
+ consumerOptions,
238
+ );
239
+
240
+ // Recreate consumers on reconnects: NO AWAIT, listen asynchronously
241
+ const handleNatsStatus = async () => {
242
+ try {
243
+ this.logger.debug('ConsumerService: Reconnected to Nats and re-creating non-durable consumers');
244
+ await getOrCreateConsumer(this.logger, this._natsConnection, FLOWS_STREAM_NAME, consumerOptions.name, consumerOptions);
245
+ this.consumeNatsMessagesOfConsumer(consumer, { expires: 10 * 1_000_000_000 /* 10 seconds */ });
246
+ } catch (e) {
247
+ this.logger.error('NATS Status-AsyncIterator is not available, cannot listen. Due to error:');
248
+ this.logger.error(e);
249
+ natsEventListener(this._natsConnection, this.logger, handleNatsStatus);
250
+ }
251
+ };
252
+ natsEventListener(this._natsConnection, this.logger, handleNatsStatus);
253
+
254
+ // NO AWAIT, listen for messages of the consumer asynchronously
255
+ this.consumeNatsMessagesOfConsumer(consumer, { expires: 10 * 1_000_000_000 /* 10 seconds */ });
256
+ } catch (e) {
257
+ await logErrorAndExit(`Could not set up consumer for deployment messages exchanges: ${e}`);
258
+ }
259
+ }
260
+
261
+ this.amqpChannel = this.amqpConnection?.createChannel({
262
+ json: true,
263
+ setup: async (channel: Channel) => {
264
+ try {
265
+ await channel.assertExchange('flow', 'direct', { durable: true }); // TODO wieso weshalb warum: wo wird das gebraucht?
266
+ } catch (e) {
267
+ await logErrorAndExit(`Could not assert exchanges: ${e}`);
268
+ }
269
+ },
270
+ });
271
+
272
+ if (this.amqpChannel) {
273
+ await this.amqpChannel.waitForConnect();
274
+ }
275
+
276
+ for (const module of this.modules) {
277
+ const moduleName = Reflect.getMetadata('module:name', module);
278
+ const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
279
+ if (!moduleName || !moduleDeclarations || !Array.isArray(moduleDeclarations)) {
280
+ await logErrorAndExit(`FlowModule (${module.name}) metadata is missing or invalid`);
281
+ return;
282
+ }
283
+ for (const declaration of moduleDeclarations) {
284
+ const functionFqn = Reflect.getMetadata('element:functionFqn', declaration);
285
+ if (!functionFqn) {
286
+ await logErrorAndExit(`FlowFunction (${declaration.name}) metadata is missing or invalid`);
287
+ return;
288
+ }
289
+ this.declarations[`${moduleName}.${functionFqn}`] = declaration;
290
+ }
291
+ }
292
+
293
+ for (const element of this.flow.elements) {
294
+ const { id, name, properties, module, functionFqn } = element;
295
+ try {
296
+ const context: Context = { ...this.context, id, name, logger: this.baseLogger, app: this };
297
+ this.elements[id] = new this.declarations[`${module}.${functionFqn}`](
298
+ context,
299
+ // run recursively through all properties and interpolate them / replace them with their explicit value
300
+ this.contextManager.replaceAllPlaceholderProperties(properties),
301
+ );
302
+ this.elements[id].setPropertiesWithPlaceholders(cloneDeep(properties));
303
+ } catch (err) {
304
+ await logErrorAndExit(`Could not create FlowElement for ${module}.${functionFqn}`);
305
+ return;
306
+ }
307
+ }
308
+
309
+ for (const connection of this.flow.connections) {
310
+ const { source, target, sourceStream = 'default', targetStream = 'default' } = connection;
311
+ if (!source || !target) {
312
+ continue;
313
+ }
314
+
315
+ const sourceStreamId = `${source}.${sourceStream}`;
316
+ const targetStreamId = `${target}.${targetStream}`;
317
+ const element = this.elements[target];
318
+
319
+ if (!element || !element.constructor) {
320
+ await logErrorAndExit(`${target} has not been initialized`);
321
+ return;
322
+ }
323
+ const streamHandler = Reflect.getMetadata(`stream:${targetStream}`, element.constructor);
324
+ if (!streamHandler || !element[streamHandler]) {
325
+ await logErrorAndExit(`${target} does not implement a handler for ${targetStream}`);
326
+ return;
327
+ }
328
+
329
+ const streamOptions: StreamOptions = Reflect.getMetadata(`stream:options:${targetStream}`, element.constructor) || {};
330
+ const concurrent = streamOptions.concurrent || 1;
331
+
332
+ const outputStream = this.getOutputStream(sourceStreamId);
333
+ outputStream
334
+ .pipe(
335
+ tap(() => this.setQueueMetrics(targetStreamId)),
336
+ mergeMap(async (event: FlowEvent) => {
337
+ const eventId = event.getId();
338
+ this.publishLifecycleEvent(element, eventId, LifecycleEvent.ACTIVATED);
339
+ this.performanceMap.set(eventId, performance.eventLoopUtilization());
340
+ const start = performance.now();
341
+ try {
342
+ await element[streamHandler](event);
343
+ const duration = Math.ceil(performance.now() - start);
344
+ this.publishLifecycleEvent(element, eventId, LifecycleEvent.COMPLETED, { duration });
345
+ } catch (err) {
346
+ const duration = Math.ceil(performance.now() - start);
347
+ this.publishLifecycleEvent(element, eventId, LifecycleEvent.TERMINATED, { duration });
348
+ try {
349
+ element.handleApiError(err);
350
+ } catch (e) {
351
+ this.logger.error(err);
352
+ }
353
+ }
354
+ return event;
355
+ }, concurrent),
356
+ tap((event: FlowEvent) => {
357
+ this.updateMetrics(targetStreamId);
358
+ let elu = this.performanceMap.get(event.getId());
359
+ if (elu) {
360
+ this.performanceMap.delete(event.getId());
361
+ elu = performance.eventLoopUtilization(elu);
362
+ if (elu.utilization > 0.75 && elu.active > 2000) {
363
+ this.logger.warn(
364
+ `High event loop utilization detected for ${targetStreamId} with event ${event.getId()}! Handler was active for ${Number(
365
+ elu.active,
366
+ ).toFixed(2)}ms with a utilization of ${Number(elu.utilization * 100).toFixed(
367
+ 2,
368
+ )}%. Consider refactoring or move tasks to a worker thread.`,
369
+ );
370
+ }
371
+ }
372
+ }),
373
+ )
374
+ .subscribe();
375
+ }
376
+
377
+ this.initialized = true;
378
+ this.logger.log('Flow Deployment is running');
379
+ }
380
+
381
+ private publishLifecycleEvent = async (
382
+ element: FlowElement,
383
+ flowEventId: string,
384
+ eventType: LifecycleEvent,
385
+ data: Record<string, any> = {},
386
+ ) => {
387
+ try {
388
+ const { flowId, deploymentId, id: elementId, functionFqn, inputStreamId } = element.getMetadata();
389
+ const natsEvent: NatsEvent<any> = {
390
+ source: `flows/${flowId}/deployments/${deploymentId}/elements/${elementId}`,
391
+ type: eventType,
392
+ data: {
393
+ flowEventId,
394
+ functionFqn,
395
+ inputStreamId,
396
+ ...data,
397
+ },
398
+ };
399
+ await publishNatsEvent(
400
+ this.logger,
401
+ this._natsConnection,
402
+ natsEvent,
403
+ `${natsFlowsPrefixFlowDeployment}.flowlifecycle.${deploymentId}`,
404
+ );
405
+ } catch (err) {
406
+ this.logger.error(err);
407
+ }
408
+ };
409
+
410
+ private setQueueMetrics = (id: string) => {
411
+ const metrics = this.outputQueueMetrics.get(id) || { size: 0, lastAdd: 0, lastRemove: Date.now(), warnings: 0 };
412
+ const secsProcessing = Math.round((metrics.lastAdd - metrics.lastRemove) / 1000);
413
+ metrics.size++;
414
+ metrics.lastAdd = Date.now();
415
+
416
+ if (secsProcessing >= WARN_EVENT_PROCESSING_SEC * (metrics.warnings + 1)) {
417
+ this.logger.warn(
418
+ `Input stream "${id}" has ${metrics.size} queued events and the last event has been processing for ${secsProcessing}s`,
419
+ );
420
+ metrics.warnings++;
421
+ } else if (metrics.size % WARN_EVENT_QUEUE_SIZE === 0) {
422
+ this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events`);
423
+ }
424
+ this.outputQueueMetrics.set(id, metrics);
425
+ };
426
+
427
+ private updateMetrics = (id: string) => {
428
+ const metrics = this.outputQueueMetrics.get(id);
429
+ if (metrics) {
430
+ metrics.size = metrics.size > 0 ? metrics.size - 1 : 0;
431
+ metrics.lastRemove = Date.now();
432
+ metrics.warnings = 0;
433
+ this.outputQueueMetrics.set(id, metrics);
434
+ }
435
+ };
436
+
437
+ public subscribe = (streamId: string, observer: PartialObserver<FlowEvent>) => this.getOutputStream(streamId).subscribe(observer);
438
+
439
+ public emit = (event: FlowEvent) => {
440
+ if (event) {
441
+ try {
442
+ this.publishNatsEventFlowlogs(event);
443
+ if (this.outputStreamMap.has(event.getStreamId())) {
444
+ this.getOutputStream(event.getStreamId()).next(event);
445
+ }
446
+ } catch (err) {
447
+ this.logger.error(err);
448
+ }
449
+ }
450
+ };
451
+
452
+ public emitPartial = (completeEvent: FlowEvent, partialEvent: FlowEvent) => {
453
+ // send complete event, log only partial event
454
+ try {
455
+ if (completeEvent && this.outputStreamMap.has(completeEvent.getStreamId())) {
456
+ this.getOutputStream(completeEvent.getStreamId()).next(completeEvent);
457
+ }
458
+ if (partialEvent) {
459
+ this.publishNatsEventFlowlogs(partialEvent);
460
+ }
461
+ } catch (err) {
462
+ this.logger.error(err);
463
+ }
464
+ };
465
+
466
+ public onMessage = async (cloudEvent: CloudEvent) => {
467
+ if (cloudEvent.subject.endsWith('.update')) {
468
+ try {
469
+ const flow: Flow = cloudEvent.data;
470
+ if (!flow) {
471
+ return;
472
+ }
473
+
474
+ let context: Partial<FlowElementContext> = {};
475
+ if (flow.context) {
476
+ this.context = { ...this.context, ...flow.context };
477
+ context = this.context;
478
+ }
479
+
480
+ if (flow.properties) {
481
+ this.contextManager.updateFlowProperties(flow.properties);
482
+ for (const element of Object.values(this.elements)) {
483
+ element.replacePlaceholderAndSetProperties();
484
+ element.onFlowPropertiesChanged?.(flow.properties);
485
+ }
486
+ }
487
+
488
+ if (Object.keys(context).length > 0) {
489
+ for (const element of flow.elements || []) {
490
+ context = { ...context, name: element.name };
491
+ this.elements?.[element.id]?.onContextChanged(context);
492
+ }
493
+ }
494
+
495
+ for (const element of flow.elements || []) {
496
+ this.elements?.[element.id]?.setPropertiesWithPlaceholders(cloneDeep(element.properties));
497
+ this.elements?.[element.id]?.onPropertiesChanged(
498
+ this.contextManager.replaceAllPlaceholderProperties(this.elements[element.id].getPropertiesWithPlaceholders()),
499
+ );
500
+ }
501
+
502
+ const natsEvent = {
503
+ source: `hpc/flow-application`,
504
+ type: `${natsFlowsPrefixFlowDeployment}.health`,
505
+ subject: `${this.context.deploymentId}`,
506
+ data: {
507
+ deploymentId: this.context.deploymentId,
508
+ status: 'updated',
509
+ },
510
+ };
511
+ await publishNatsEvent(this.logger, this._natsConnection, natsEvent);
512
+ } catch (err) {
513
+ this.logger.error(err);
514
+
515
+ const natsEvent = {
516
+ source: `hpc/flow-application`,
517
+ type: `${natsFlowsPrefixFlowDeployment}.health`,
518
+ subject: `${this.context.deploymentId}`,
519
+ data: {
520
+ deploymentId: this.context.deploymentId,
521
+ status: 'updating failed',
522
+ },
523
+ };
524
+ await publishNatsEvent(this.logger, this._natsConnection, natsEvent);
525
+ }
526
+ } else if (cloudEvent.subject.endsWith('.message')) {
527
+ const data = cloudEvent.data as DeploymentMessage;
528
+ const elementId = data?.elementId;
529
+ if (elementId) {
530
+ this.elements?.[elementId]?.onMessage?.(data);
531
+ } else {
532
+ for (const element of Object.values(this.elements)) {
533
+ element?.onMessage?.(data);
534
+ }
535
+ }
536
+ } else if (cloudEvent.subject.endsWith('.destroy')) {
537
+ // TODO war com.flowstudio.deployment.destroy in RabbitMq: wo wird das jetzt wieder gesendet?
538
+ this.destroy();
539
+ }
540
+ };
541
+
542
+ /**
543
+ * Publish a flow event to the amqp flowlogs exchange.
544
+ * If the event size exceeds the limit it will be truncated
545
+ *
546
+ * TODO warum darf hier nicht false zurückgegeben werden? -> erzeugt loop
547
+ */
548
+ public publishNatsEventFlowlogs = async (event: FlowEvent): Promise<boolean> => {
549
+ if (!this._natsConnection || this._natsConnection.isClosed()) {
550
+ return true;
551
+ }
552
+
553
+ try {
554
+ const formatedEvent = event.format();
555
+ if (sizeof(formatedEvent) > MAX_EVENT_SIZE_BYTES) {
556
+ formatedEvent.data = truncate(formatedEvent.data);
557
+ }
558
+
559
+ const natsEvent = {
560
+ source: `hpc/flow-application`,
561
+ type: `${natsFlowsPrefixFlowDeployment}.flowlogs`,
562
+ subject: `${this.context.deploymentId}`,
563
+ data: formatedEvent,
564
+ };
565
+
566
+ await publishNatsEvent(this.logger, this._natsConnection, natsEvent);
567
+ return true;
568
+ } catch (err) {
569
+ this.logger.error(err);
570
+ return false;
571
+ }
572
+ };
573
+
574
+ /**
575
+ * Calls onDestroy lifecycle method on all flow elements,
576
+ * closes amqp connection after allowing logs to be processed and published
577
+ * then exits process
578
+ */
579
+ public async destroy(exitCode = 0) {
580
+ try {
581
+ try {
582
+ for (const element of Object.values(this.elements)) {
583
+ element?.onDestroy?.();
584
+ }
585
+ if (this._rpcClient) {
586
+ await this._rpcClient.close();
587
+ }
588
+ } catch (err) {
589
+ this.logger.error(err);
590
+ }
591
+ // allow time for logs to be processed
592
+ await delay(250);
593
+ if (this.amqpConnection) {
594
+ await this.amqpConnection.close();
595
+ }
596
+
597
+ // Close all output streams
598
+ for (const [id, stream] of this.outputStreamMap.entries()) {
599
+ try {
600
+ stream?.complete();
601
+ } catch (err) {
602
+ this.logger.error(`Error completing output stream ${id}: ${err.message}`);
603
+ }
604
+ }
605
+
606
+ // Nats: Delete consumer for flow deployment, stop message listening and close connection
607
+ try {
608
+ await this.natsMessageIterator?.close();
609
+ await this._natsConnection?.drain();
610
+ await this._natsConnection?.close();
611
+
612
+ if (this._natsConnection && !this._natsConnection.isClosed()) {
613
+ await jetstreamManager(this._natsConnection).then((jsm) => {
614
+ jsm.consumers
615
+ .delete(FLOWS_STREAM_NAME, `flow-deployment-${this.context?.deploymentId}`)
616
+ .then(() => {
617
+ this.logger.debug(`Deleted consumer for flow deployment ${this.context?.deploymentId}`);
618
+ })
619
+ .catch((err) => {
620
+ this.logger.error(`Could not delete consumer for flow deployment ${this.context?.deploymentId}: ${err.message}`);
621
+ });
622
+ });
623
+ }
624
+ } catch (err) {
625
+ this.logger.error(err);
626
+ }
627
+
628
+ // remove process listeners
629
+ process.removeAllListeners('SIGTERM');
630
+ process.removeAllListeners('uncaughtException');
631
+ process.removeAllListeners('unhandledRejection');
632
+ } catch (err) {
633
+ /* eslint-disable-next-line no-console */
634
+ console.error(err);
635
+ } finally {
636
+ if (process.env.NODE_ENV !== 'test') {
637
+ process.exit(exitCode);
638
+ }
639
+ }
640
+ }
641
+
642
+ /**
643
+ * Returns rxjs subject for the specified stream id.
644
+ * A new subject will be created if one doesn't exist yet.
645
+ */
646
+ private getOutputStream(id: string) {
647
+ const stream = this.outputStreamMap.get(id);
648
+ if (!stream) {
649
+ this.outputStreamMap.set(id, new Subject<FlowEvent>());
650
+ return this.outputStreamMap.get(id);
651
+ }
652
+ return stream;
653
+ }
654
+ }
655
+
656
+ export interface Context extends FlowElementContext {
657
+ app?: FlowApplication;
658
+ logger?: Logger;
659
+ }