@hahnpro/flow-sdk 5.0.0-0 → 5.0.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 (98) hide show
  1. package/LICENSE +1 -1
  2. package/README.md +1 -1
  3. package/dist/FlowApplication.d.ts +53 -14
  4. package/dist/FlowApplication.js +266 -85
  5. package/dist/FlowElement.d.ts +12 -4
  6. package/dist/FlowElement.js +47 -48
  7. package/dist/FlowEvent.d.ts +7 -1
  8. package/dist/FlowEvent.js +5 -5
  9. package/dist/FlowLogger.d.ts +9 -6
  10. package/dist/FlowLogger.js +17 -18
  11. package/dist/FlowModule.d.ts +1 -1
  12. package/dist/FlowModule.js +3 -3
  13. package/dist/RpcClient.d.ts +5 -3
  14. package/dist/RpcClient.js +23 -9
  15. package/dist/TestModule.js +6 -6
  16. package/dist/amqp.d.ts +14 -0
  17. package/dist/amqp.js +13 -0
  18. package/dist/extra-validators.js +15 -8
  19. package/dist/flow.interface.d.ts +7 -1
  20. package/dist/flow.interface.js +7 -0
  21. package/dist/index.d.ts +2 -1
  22. package/dist/index.js +2 -1
  23. package/dist/rpc_server.py +89 -77
  24. package/dist/unit-decorators.d.ts +39 -0
  25. package/dist/unit-decorators.js +157 -0
  26. package/dist/unit-utils.d.ts +8 -0
  27. package/dist/unit-utils.js +144 -0
  28. package/dist/units.d.ts +31 -0
  29. package/dist/units.js +570 -0
  30. package/dist/utils.d.ts +5 -1
  31. package/dist/utils.js +40 -7
  32. package/package.json +35 -33
  33. package/dist/api/Queue.d.ts +0 -15
  34. package/dist/api/Queue.js +0 -25
  35. package/dist/api/api.d.ts +0 -22
  36. package/dist/api/api.interface.d.ts +0 -14
  37. package/dist/api/api.interface.js +0 -2
  38. package/dist/api/api.js +0 -38
  39. package/dist/api/asset.interface.d.ts +0 -45
  40. package/dist/api/asset.interface.js +0 -2
  41. package/dist/api/asset.service.d.ts +0 -8
  42. package/dist/api/asset.service.js +0 -18
  43. package/dist/api/content.interface.d.ts +0 -33
  44. package/dist/api/content.interface.js +0 -2
  45. package/dist/api/content.service.d.ts +0 -9
  46. package/dist/api/content.service.js +0 -22
  47. package/dist/api/data.interface.d.ts +0 -29
  48. package/dist/api/data.interface.js +0 -2
  49. package/dist/api/data.service.d.ts +0 -15
  50. package/dist/api/data.service.js +0 -50
  51. package/dist/api/endpoint.interface.d.ts +0 -18
  52. package/dist/api/endpoint.interface.js +0 -2
  53. package/dist/api/endpoint.service.d.ts +0 -7
  54. package/dist/api/endpoint.service.js +0 -14
  55. package/dist/api/http.service.d.ts +0 -29
  56. package/dist/api/http.service.js +0 -96
  57. package/dist/api/index.d.ts +0 -11
  58. package/dist/api/index.js +0 -15
  59. package/dist/api/mock/api.mock.d.ts +0 -102
  60. package/dist/api/mock/api.mock.js +0 -81
  61. package/dist/api/mock/asset.mock.service.d.ts +0 -9
  62. package/dist/api/mock/asset.mock.service.js +0 -18
  63. package/dist/api/mock/content.mock.service.d.ts +0 -9
  64. package/dist/api/mock/content.mock.service.js +0 -21
  65. package/dist/api/mock/data.mock.service.d.ts +0 -11
  66. package/dist/api/mock/data.mock.service.js +0 -53
  67. package/dist/api/mock/endpoint.mock.service.d.ts +0 -10
  68. package/dist/api/mock/endpoint.mock.service.js +0 -15
  69. package/dist/api/mock/index.d.ts +0 -7
  70. package/dist/api/mock/index.js +0 -10
  71. package/dist/api/mock/secret.mock.service.d.ts +0 -5
  72. package/dist/api/mock/secret.mock.service.js +0 -11
  73. package/dist/api/mock/task.mock.service.d.ts +0 -8
  74. package/dist/api/mock/task.mock.service.js +0 -16
  75. package/dist/api/mock/timeseries.mock.service.d.ts +0 -18
  76. package/dist/api/mock/timeseries.mock.service.js +0 -70
  77. package/dist/api/mock/user.mock.service.d.ts +0 -7
  78. package/dist/api/mock/user.mock.service.js +0 -12
  79. package/dist/api/secret.interface.d.ts +0 -9
  80. package/dist/api/secret.interface.js +0 -2
  81. package/dist/api/secret.service.d.ts +0 -6
  82. package/dist/api/secret.service.js +0 -10
  83. package/dist/api/sidriveiq.interface.d.ts +0 -104
  84. package/dist/api/sidriveiq.interface.js +0 -2
  85. package/dist/api/sidriveiq.service.d.ts +0 -31
  86. package/dist/api/sidriveiq.service.js +0 -97
  87. package/dist/api/task.interface.d.ts +0 -26
  88. package/dist/api/task.interface.js +0 -2
  89. package/dist/api/task.service.d.ts +0 -7
  90. package/dist/api/task.service.js +0 -13
  91. package/dist/api/timeseries.interface.d.ts +0 -51
  92. package/dist/api/timeseries.interface.js +0 -2
  93. package/dist/api/timeseries.service.d.ts +0 -17
  94. package/dist/api/timeseries.service.js +0 -38
  95. package/dist/api/user.interface.d.ts +0 -3
  96. package/dist/api/user.interface.js +0 -2
  97. package/dist/api/user.service.d.ts +0 -7
  98. package/dist/api/user.service.js +0 -21
package/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2020 Hahn PRO
3
+ Copyright (c) 2021 Hahn PRO
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -1,3 +1,3 @@
1
1
  # `@hahnpro/flow-sdk`
2
2
 
3
- https://gitlab.com/hahnpro/flow
3
+ https://github.com/hahnprojects/flow
@@ -1,34 +1,73 @@
1
1
  import 'reflect-metadata';
2
- import { AmqpConnection, Nack } from '@golevelup/nestjs-rabbitmq';
3
- import { CloudEvent } from 'cloudevents';
2
+ import { API, HttpClient, MockAPI } from '@hahnpro/hpc-api';
3
+ import { ConsumeMessage } from 'amqplib';
4
+ import { AmqpConnectionManager } from 'amqp-connection-manager';
4
5
  import { PartialObserver } from 'rxjs';
5
- import { API } from './api';
6
+ import { AmqpConnection, AmqpConnectionConfig } from './amqp';
6
7
  import { ClassType, Flow, FlowElementContext } from './flow.interface';
7
8
  import { FlowEvent } from './FlowEvent';
8
9
  import { Logger } from './FlowLogger';
9
10
  import { RpcClient } from './RpcClient';
11
+ interface FlowAppConfig {
12
+ logger?: Logger;
13
+ amqpConfig?: AmqpConnectionConfig;
14
+ amqpConnection?: AmqpConnectionManager;
15
+ apiClient?: HttpClient;
16
+ skipApi?: boolean;
17
+ explicitInit?: boolean;
18
+ mockApi?: MockAPI;
19
+ }
10
20
  export declare class FlowApplication {
11
- private amqpConnection?;
12
- api: API;
21
+ private modules;
22
+ private flow;
23
+ private _api;
24
+ private _rpcClient;
25
+ private amqpChannel;
26
+ private readonly amqpConnection;
27
+ private readonly baseLogger;
13
28
  private context;
14
29
  private declarations;
15
30
  private elements;
16
- private logger;
17
- private properties;
18
- private _rpcClient;
31
+ private initialized;
32
+ private readonly logger;
19
33
  private outputStreamMap;
20
- constructor(modules: Array<ClassType<any>>, flow: Flow, logger?: Logger, amqpConnection?: AmqpConnection, skipApi?: boolean);
34
+ private outputQueueMetrics;
35
+ private performanceMap;
36
+ private properties;
37
+ private readonly skipApi;
38
+ private readonly apiClient?;
39
+ constructor(modules: ClassType<any>[], flow: Flow, config?: FlowAppConfig);
40
+ constructor(modules: ClassType<any>[], flow: Flow, baseLogger?: Logger, amqpConnection?: AmqpConnection, skipApi?: boolean, explicitInit?: boolean);
41
+ get rpcClient(): RpcClient;
42
+ get api(): API;
43
+ init(): Promise<void>;
44
+ private publishLifecycleEvent;
45
+ private setQueueMetrics;
46
+ private updateMetrics;
21
47
  subscribe: (streamId: string, observer: PartialObserver<FlowEvent>) => import("rxjs").Subscription;
22
48
  emit: (event: FlowEvent) => void;
49
+ emitPartial: (completeEvent: FlowEvent, partialEvent: FlowEvent) => void;
23
50
  getProperties(): Record<string, any>;
24
- onMessage: (event: CloudEvent) => Promise<Nack | undefined>;
25
- publishEvent: (event: FlowEvent) => Promise<void>;
26
- get rpcClient(): RpcClient;
27
- destroy(): Promise<void>;
51
+ onMessage: (msg: ConsumeMessage) => Promise<void>;
52
+ /**
53
+ * Publish a flow event to the amqp flowlogs exchange.
54
+ * If the event size exceeds the limit it will be truncated
55
+ */
56
+ publishEvent: (event: FlowEvent) => Promise<boolean>;
57
+ /**
58
+ * Calls onDestroy lifecycle method on all flow elements,
59
+ * closes amqp connection after allowing logs to be processed and published
60
+ * then exits process
61
+ */
62
+ destroy(exitCode?: number): Promise<void>;
63
+ /**
64
+ * Returns rxjs subject for the specified stream id.
65
+ * A new subject will be created if one doesn't exist yet.
66
+ */
28
67
  private getOutputStream;
29
- private truncate;
30
68
  }
31
69
  export interface Context extends FlowElementContext {
32
70
  app?: FlowApplication;
33
71
  logger?: Logger;
34
72
  }
73
+ export {};
@@ -3,93 +3,181 @@ Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.FlowApplication = void 0;
4
4
  const tslib_1 = require("tslib");
5
5
  require("reflect-metadata");
6
- const nestjs_rabbitmq_1 = require("@golevelup/nestjs-rabbitmq");
6
+ const hpc_api_1 = require("@hahnpro/hpc-api");
7
+ const cloudevents_1 = require("cloudevents");
8
+ const crypto_1 = require("crypto");
7
9
  const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
10
+ const perf_hooks_1 = require("perf_hooks");
8
11
  const rxjs_1 = require("rxjs");
9
12
  const operators_1 = require("rxjs/operators");
10
- const util_1 = require("util");
11
- const uuid_1 = require("uuid");
12
- const api_1 = require("./api");
13
+ const amqp_1 = require("./amqp");
14
+ const flow_interface_1 = require("./flow.interface");
13
15
  const FlowLogger_1 = require("./FlowLogger");
14
16
  const RpcClient_1 = require("./RpcClient");
15
- const MAX_EVENT_SIZE_BYTES = 512 * 1024;
17
+ const utils_1 = require("./utils");
18
+ const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024;
19
+ const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
20
+ const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
16
21
  class FlowApplication {
17
- constructor(modules, flow, logger, amqpConnection, skipApi = false) {
18
- this.amqpConnection = amqpConnection;
22
+ constructor(modules, flow, baseLoggerOrConfig, amqpConnection, skipApi, explicitInit, mockApi) {
23
+ this.modules = modules;
24
+ this.flow = flow;
19
25
  this.declarations = {};
20
26
  this.elements = {};
21
- this.outputStreamMap = {};
27
+ this.initialized = false;
28
+ this.outputStreamMap = new Map();
29
+ this.outputQueueMetrics = new Map();
30
+ this.performanceMap = new Map();
31
+ this.publishLifecycleEvent = (element, flowEventId, eventType, data = {}) => {
32
+ if (!this.amqpChannel) {
33
+ return;
34
+ }
35
+ try {
36
+ const { flowId, deploymentId, id: elementId, functionFqn, inputStreamId } = element.getMetadata();
37
+ const event = new cloudevents_1.CloudEvent({
38
+ source: `flows/${flowId}/deployments/${deploymentId}/elements/${elementId}`,
39
+ type: eventType,
40
+ data: {
41
+ flowEventId,
42
+ functionFqn,
43
+ inputStreamId,
44
+ ...data,
45
+ },
46
+ time: new Date().toISOString(),
47
+ });
48
+ const message = event.toJSON();
49
+ return this.amqpChannel.publish('flow', 'lifecycle', message);
50
+ }
51
+ catch (err) {
52
+ this.logger.error(err);
53
+ }
54
+ };
55
+ this.setQueueMetrics = (id) => {
56
+ const metrics = this.outputQueueMetrics.get(id) || { size: 0, lastAdd: 0, lastRemove: Date.now(), warnings: 0 };
57
+ const secsProcessing = Math.round((metrics.lastAdd - metrics.lastRemove) / 1000);
58
+ metrics.size++;
59
+ metrics.lastAdd = Date.now();
60
+ if (secsProcessing >= WARN_EVENT_PROCESSING_SEC * (metrics.warnings + 1)) {
61
+ this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events and the last event has been processing for ${secsProcessing}s`);
62
+ metrics.warnings++;
63
+ }
64
+ else if (metrics.size % WARN_EVENT_QUEUE_SIZE === 0) {
65
+ this.logger.warn(`Input stream "${id}" has ${metrics.size} queued events`);
66
+ }
67
+ this.outputQueueMetrics.set(id, metrics);
68
+ };
69
+ this.updateMetrics = (id) => {
70
+ const metrics = this.outputQueueMetrics.get(id);
71
+ if (metrics) {
72
+ metrics.size = metrics.size > 0 ? metrics.size - 1 : 0;
73
+ metrics.lastRemove = Date.now();
74
+ metrics.warnings = 0;
75
+ this.outputQueueMetrics.set(id, metrics);
76
+ }
77
+ };
22
78
  this.subscribe = (streamId, observer) => this.getOutputStream(streamId).subscribe(observer);
23
79
  this.emit = (event) => {
24
- var _a;
25
80
  if (event) {
26
81
  try {
27
82
  this.publishEvent(event);
28
- this.getOutputStream(event.getStreamId()).next(event);
83
+ if (this.outputStreamMap.has(event.getStreamId())) {
84
+ this.getOutputStream(event.getStreamId()).next(event);
85
+ }
29
86
  }
30
87
  catch (err) {
31
- (_a = this.logger) === null || _a === void 0 ? void 0 : _a.error(err);
88
+ this.logger.error(err);
89
+ }
90
+ }
91
+ };
92
+ this.emitPartial = (completeEvent, partialEvent) => {
93
+ try {
94
+ if (completeEvent && this.outputStreamMap.has(completeEvent.getStreamId())) {
95
+ this.getOutputStream(completeEvent.getStreamId()).next(completeEvent);
96
+ }
97
+ if (partialEvent) {
98
+ this.publishEvent(partialEvent);
32
99
  }
33
100
  }
101
+ catch (err) {
102
+ this.logger.error(err);
103
+ }
34
104
  };
35
- this.onMessage = async (event) => {
36
- var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l;
105
+ this.onMessage = async (msg) => {
106
+ let event;
107
+ try {
108
+ event = JSON.parse(msg.content.toString());
109
+ }
110
+ catch (err) {
111
+ this.logger.error(err);
112
+ return;
113
+ }
37
114
  if (event.type === 'com.flowstudio.deployment.update') {
38
115
  try {
39
116
  const flow = event.data;
40
117
  if (!flow) {
41
- return new nestjs_rabbitmq_1.Nack(false);
118
+ this.amqpChannel.nack(msg, false, false);
119
+ return;
42
120
  }
43
121
  let context = {};
44
122
  if (flow.context) {
45
- this.context = Object.assign(Object.assign({}, this.context), flow.context);
123
+ this.context = { ...this.context, ...flow.context };
46
124
  context = this.context;
47
125
  }
48
126
  if (flow.properties) {
49
127
  this.properties = flow.properties;
50
128
  for (const element of Object.values(this.elements)) {
51
- (_a = element.onFlowPropertiesChanged) === null || _a === void 0 ? void 0 : _a.call(element, flow.properties);
129
+ element.onFlowPropertiesChanged?.(flow.properties);
52
130
  }
53
131
  }
54
132
  if (Object.keys(context).length > 0) {
55
133
  for (const element of flow.elements || []) {
56
- context = Object.assign(Object.assign({}, context), { name: element.name });
57
- (_c = (_b = this.elements) === null || _b === void 0 ? void 0 : _b[element.id]) === null || _c === void 0 ? void 0 : _c.onContextChanged(context);
134
+ context = { ...context, name: element.name };
135
+ this.elements?.[element.id]?.onContextChanged(context);
58
136
  }
59
137
  }
60
138
  for (const element of flow.elements || []) {
61
- (_e = (_d = this.elements) === null || _d === void 0 ? void 0 : _d[element.id]) === null || _e === void 0 ? void 0 : _e.onPropertiesChanged(element.properties);
139
+ this.elements?.[element.id]?.onPropertiesChanged(element.properties);
62
140
  }
63
141
  const statusEvent = {
64
- eventId: uuid_1.v4(),
142
+ eventId: (0, crypto_1.randomUUID)(),
65
143
  eventTime: new Date().toISOString(),
66
144
  eventType: 'com.hahnpro.event.health',
67
145
  contentType: 'application/json',
68
146
  data: { deploymentId: this.context.deploymentId, status: 'updated' },
69
147
  };
70
- (_f = this.amqpConnection) === null || _f === void 0 ? void 0 : _f.publish('deployment', 'health', statusEvent).catch((err) => this.logger.error(err));
148
+ try {
149
+ this.amqpChannel?.publish('deployment', 'health', statusEvent);
150
+ }
151
+ catch (err) {
152
+ this.logger.error(err);
153
+ }
71
154
  }
72
155
  catch (err) {
73
156
  this.logger.error(err);
74
157
  const statusEvent = {
75
- eventId: uuid_1.v4(),
158
+ eventId: (0, crypto_1.randomUUID)(),
76
159
  eventTime: new Date().toISOString(),
77
160
  eventType: 'com.hahnpro.event.health',
78
161
  contentType: 'application/json',
79
162
  data: { deploymentId: this.context.deploymentId, status: 'updating failed' },
80
163
  };
81
- (_g = this.amqpConnection) === null || _g === void 0 ? void 0 : _g.publish('deployment', 'health', statusEvent).catch((err) => this.logger.error(err));
164
+ try {
165
+ this.amqpChannel?.publish('deployment', 'health', statusEvent);
166
+ }
167
+ catch (e) {
168
+ this.logger.error(e);
169
+ }
82
170
  }
83
171
  }
84
172
  else if (event.type === 'com.flowstudio.deployment.message') {
85
173
  const data = event.data;
86
- const elementId = data === null || data === void 0 ? void 0 : data.elementId;
174
+ const elementId = data?.elementId;
87
175
  if (elementId) {
88
- (_k = (_j = (_h = this.elements) === null || _h === void 0 ? void 0 : _h[elementId]) === null || _j === void 0 ? void 0 : _j.onMessage) === null || _k === void 0 ? void 0 : _k.call(_j, data);
176
+ this.elements?.[elementId]?.onMessage?.(data);
89
177
  }
90
178
  else {
91
179
  for (const element of Object.values(this.elements)) {
92
- (_l = element === null || element === void 0 ? void 0 : element.onMessage) === null || _l === void 0 ? void 0 : _l.call(element, data);
180
+ element?.onMessage?.(data);
93
181
  }
94
182
  }
95
183
  }
@@ -97,131 +185,224 @@ class FlowApplication {
97
185
  this.destroy();
98
186
  }
99
187
  else {
100
- return new nestjs_rabbitmq_1.Nack(false);
188
+ this.amqpChannel.nack(msg, false, false);
101
189
  }
102
- return undefined;
103
190
  };
104
191
  this.publishEvent = (event) => {
105
- if (!this.amqpConnection) {
192
+ if (!this.amqpChannel) {
106
193
  return;
107
194
  }
108
195
  try {
109
196
  const message = event.format();
110
- if (object_sizeof_1.default(message) > MAX_EVENT_SIZE_BYTES) {
111
- message.data = this.truncate(message.data);
197
+ if ((0, object_sizeof_1.default)(message) > MAX_EVENT_SIZE_BYTES) {
198
+ message.data = (0, utils_1.truncate)(message.data);
112
199
  }
113
- return this.amqpConnection.publish('flowlogs', '', message);
200
+ return this.amqpChannel.publish('flowlogs', '', message);
114
201
  }
115
202
  catch (err) {
116
203
  this.logger.error(err);
117
204
  }
118
205
  };
206
+ if (baseLoggerOrConfig && !baseLoggerOrConfig.log) {
207
+ const config = baseLoggerOrConfig;
208
+ this.baseLogger = config.logger;
209
+ this.amqpConnection = config.amqpConnection || (0, amqp_1.createAmqpConnection)(config.amqpConfig);
210
+ this.skipApi = config.skipApi || false;
211
+ explicitInit = config.explicitInit || false;
212
+ this._api = config.mockApi || null;
213
+ this.apiClient = config.apiClient;
214
+ }
215
+ else {
216
+ this.baseLogger = baseLoggerOrConfig;
217
+ this.amqpConnection = amqpConnection?.managedConnection;
218
+ this.skipApi = skipApi || false;
219
+ explicitInit = explicitInit || false;
220
+ this._api = mockApi || null;
221
+ }
222
+ this.logger = new FlowLogger_1.FlowLogger({ id: 'none', functionFqn: 'FlowApplication', ...flow?.context }, this.baseLogger || undefined, this.publishEvent);
223
+ process.once('uncaughtException', (err) => {
224
+ this.logger.error('Uncaught exception!');
225
+ this.logger.error(err);
226
+ this.destroy(1);
227
+ });
228
+ process.on('unhandledRejection', (reason) => {
229
+ this.logger.error('Unhandled promise rejection!');
230
+ this.logger.error(reason);
231
+ });
119
232
  process.on('SIGTERM', () => {
120
- this.logger.log('Flow Application is terminating.');
121
- this.destroy().finally(() => process.exit(0));
233
+ this.logger.log('Flow Deployment is terminating');
234
+ this.destroy(0);
122
235
  });
123
- this.context = Object.assign({}, flow.context);
124
- this.properties = flow.properties || {};
125
- this.logger = logger || FlowLogger_1.defaultLogger;
236
+ if (explicitInit !== true) {
237
+ this.init();
238
+ }
239
+ }
240
+ get rpcClient() {
241
+ if (!this._rpcClient && this.amqpConnection) {
242
+ this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection, this.logger);
243
+ }
244
+ return this._rpcClient;
245
+ }
246
+ get api() {
247
+ return this._api;
248
+ }
249
+ async init() {
250
+ if (this.initialized)
251
+ return;
252
+ this.context = { ...this.flow.context };
253
+ this.properties = this.flow.properties || {};
126
254
  try {
127
- if (!skipApi) {
128
- this.api = new api_1.API();
255
+ if (!this.skipApi && !(this._api instanceof hpc_api_1.MockAPI)) {
256
+ this._api = new hpc_api_1.API(this.apiClient);
129
257
  }
130
258
  }
131
259
  catch (err) {
132
- this.logger.error((err === null || err === void 0 ? void 0 : err.message) || err);
260
+ this.logger.error(err?.message || err);
133
261
  }
134
- for (const module of modules) {
262
+ const logErrorAndExit = async (err) => {
263
+ this.logger.error(new Error(err));
264
+ await this.destroy(1);
265
+ };
266
+ this.amqpChannel = this.amqpConnection?.createChannel({
267
+ json: true,
268
+ setup: async (channel) => {
269
+ try {
270
+ await channel.assertExchange('deployment', 'direct', { durable: true });
271
+ await channel.assertExchange('flowlogs', 'fanout', { durable: true });
272
+ await channel.assertExchange('flow', 'direct', { durable: true });
273
+ }
274
+ catch (e) {
275
+ await logErrorAndExit(`Could not assert exchanges: ${e}`);
276
+ }
277
+ try {
278
+ const queue = await channel.assertQueue(null, { durable: false, exclusive: true });
279
+ await channel.bindQueue(queue.queue, 'deployment', this.context.deploymentId);
280
+ await channel.consume(queue.queue, (msg) => this.onMessage(msg));
281
+ }
282
+ catch (err) {
283
+ await logErrorAndExit(`Could not subscribe to deployment exchange: ${err}`);
284
+ }
285
+ },
286
+ });
287
+ this.amqpChannel && (await this.amqpChannel.waitForConnect());
288
+ for (const module of this.modules) {
135
289
  const moduleName = Reflect.getMetadata('module:name', module);
136
290
  const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
137
291
  if (!moduleName || !moduleDeclarations || !Array.isArray(moduleDeclarations)) {
138
- throw new Error(`FlowModule (${module.name}) metadata is missing or invalid.`);
292
+ await logErrorAndExit(`FlowModule (${module.name}) metadata is missing or invalid`);
293
+ return;
139
294
  }
140
295
  for (const declaration of moduleDeclarations) {
141
296
  const functionFqn = Reflect.getMetadata('element:functionFqn', declaration);
142
297
  if (!functionFqn) {
143
- throw new Error(`FlowFunction (${declaration.name}) metadata is missing or invalid.`);
298
+ await logErrorAndExit(`FlowFunction (${declaration.name}) metadata is missing or invalid`);
299
+ return;
144
300
  }
145
301
  this.declarations[`${moduleName}.${functionFqn}`] = declaration;
146
302
  }
147
303
  }
148
- for (const element of flow.elements) {
304
+ for (const element of this.flow.elements) {
149
305
  const { id, name, properties, module, functionFqn } = element;
150
306
  try {
151
- const context = Object.assign(Object.assign({}, this.context), { id, name, logger, app: this });
307
+ const context = { ...this.context, id, name, logger: this.baseLogger, app: this };
152
308
  this.elements[id] = new this.declarations[`${module}.${functionFqn}`](context, properties);
153
309
  }
154
310
  catch (err) {
155
- console.error(err);
156
- throw new Error(`Could not create FlowElement for ${module}.${functionFqn}`);
311
+ await logErrorAndExit(`Could not create FlowElement for ${module}.${functionFqn}`);
312
+ return;
157
313
  }
158
314
  }
159
- for (const connection of flow.connections) {
315
+ for (const connection of this.flow.connections) {
160
316
  const { source, target, sourceStream = 'default', targetStream = 'default' } = connection;
161
317
  if (!source || !target) {
162
318
  continue;
163
319
  }
164
- const streamId = `${source}.${sourceStream}`;
320
+ const sourceStreamId = `${source}.${sourceStream}`;
321
+ const targetStreamId = `${target}.${targetStream}`;
165
322
  const element = this.elements[target];
166
323
  if (!element || !element.constructor) {
167
- throw new Error(target + ' has not been initialized');
324
+ await logErrorAndExit(`${target} has not been initialized`);
325
+ return;
168
326
  }
169
327
  const streamHandler = Reflect.getMetadata(`stream:${targetStream}`, element.constructor);
170
328
  if (!streamHandler || !element[streamHandler]) {
171
- throw new Error(`${target} does not implement a handler for ${targetStream}`);
329
+ await logErrorAndExit(`${target} does not implement a handler for ${targetStream}`);
330
+ return;
172
331
  }
173
332
  const streamOptions = Reflect.getMetadata(`stream:options:${targetStream}`, element.constructor) || {};
174
333
  const concurrent = streamOptions.concurrent || 1;
175
- const outputStream = this.getOutputStream(streamId);
334
+ const outputStream = this.getOutputStream(sourceStreamId);
176
335
  outputStream
177
- .pipe(operators_1.mergeMap((event) => element[streamHandler](event), concurrent), operators_1.catchError((err, observable) => {
178
- element.handleApiError(err);
179
- return observable;
336
+ .pipe((0, operators_1.tap)(() => this.setQueueMetrics(targetStreamId)), (0, operators_1.mergeMap)(async (event) => {
337
+ const eventId = event.getId();
338
+ this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.ACTIVATED);
339
+ this.performanceMap.set(eventId, perf_hooks_1.performance.eventLoopUtilization());
340
+ const start = perf_hooks_1.performance.now();
341
+ try {
342
+ await element[streamHandler](event);
343
+ const duration = Math.ceil(perf_hooks_1.performance.now() - start);
344
+ this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.COMPLETED, { duration });
345
+ }
346
+ catch (err) {
347
+ const duration = Math.ceil(perf_hooks_1.performance.now() - start);
348
+ this.publishLifecycleEvent(element, eventId, flow_interface_1.LifecycleEvent.TERMINATED, { duration });
349
+ try {
350
+ element.handleApiError(err);
351
+ }
352
+ catch (e) {
353
+ this.logger.error(err);
354
+ }
355
+ }
356
+ return event;
357
+ }, concurrent), (0, operators_1.tap)((event) => {
358
+ this.updateMetrics(targetStreamId);
359
+ let elu = this.performanceMap.get(event.getId());
360
+ if (elu) {
361
+ this.performanceMap.delete(event.getId());
362
+ elu = perf_hooks_1.performance.eventLoopUtilization(elu);
363
+ if (elu.utilization > 0.75 && elu.active > 2000) {
364
+ this.logger.warn(`High event loop utilization detected for ${targetStreamId} with event ${event.getId()}! Handler was active for ${Number(elu.active).toFixed(2)}ms with a utilization of ${Number(elu.utilization * 100).toFixed(2)}%. Consider refactoring or move tasks to a worker thread.`);
365
+ }
366
+ }
180
367
  }))
181
368
  .subscribe();
182
369
  }
183
- if (this.amqpConnection) {
184
- this.amqpConnection.createSubscriber((msg) => this.onMessage(msg), {
185
- exchange: 'deployment',
186
- routingKey: this.context.deploymentId,
187
- queueOptions: { durable: false, exclusive: true },
188
- });
189
- }
370
+ this.initialized = true;
371
+ this.logger.log('Flow Deployment is running');
190
372
  }
191
373
  getProperties() {
192
374
  return this.properties;
193
375
  }
194
- get rpcClient() {
195
- var _a;
196
- if (!((_a = this.amqpConnection) === null || _a === void 0 ? void 0 : _a.managedConnection)) {
197
- throw new Error('No AMQP connection available');
376
+ async destroy(exitCode = 0) {
377
+ try {
378
+ try {
379
+ for (const element of Object.values(this.elements)) {
380
+ element?.onDestroy?.();
381
+ }
382
+ this._rpcClient && (await this._rpcClient.close());
383
+ }
384
+ catch (err) {
385
+ this.logger.error(err);
386
+ }
387
+ await (0, utils_1.delay)(250);
388
+ this.amqpConnection && (await this.amqpConnection.close());
198
389
  }
199
- if (!this._rpcClient) {
200
- this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection.managedConnection);
390
+ catch (err) {
391
+ console.error(err);
201
392
  }
202
- return this._rpcClient;
203
- }
204
- async destroy() {
205
- var _a, _b;
206
- for (const element of Object.values(this.elements)) {
207
- (_a = element === null || element === void 0 ? void 0 : element.onDestroy) === null || _a === void 0 ? void 0 : _a.call(element);
393
+ finally {
394
+ if (process.env.NODE_ENV !== 'test') {
395
+ process.exit(exitCode);
396
+ }
208
397
  }
209
- await ((_b = this._rpcClient) === null || _b === void 0 ? void 0 : _b.close());
210
398
  }
211
399
  getOutputStream(id) {
212
- const stream = this.outputStreamMap[id];
400
+ const stream = this.outputStreamMap.get(id);
213
401
  if (!stream) {
214
- this.outputStreamMap[id] = new rxjs_1.Subject();
215
- return this.outputStreamMap[id];
402
+ this.outputStreamMap.set(id, new rxjs_1.Subject());
403
+ return this.outputStreamMap.get(id);
216
404
  }
217
405
  return stream;
218
406
  }
219
- truncate(msg) {
220
- let truncated = util_1.inspect(msg, { depth: 4, maxStringLength: 1000 });
221
- if (truncated.startsWith("'") && truncated.endsWith("'")) {
222
- truncated = truncated.substring(1, truncated.length - 1);
223
- }
224
- return truncated;
225
- }
226
407
  }
227
408
  exports.FlowApplication = FlowApplication;
@@ -1,6 +1,6 @@
1
1
  import { PythonShell } from 'python-shell';
2
- import { API } from './api';
3
- import { ClassType, FlowElementContext, DeploymentMessage, FlowContext } from './flow.interface';
2
+ import { API } from '@hahnpro/hpc-api';
3
+ import { ClassType, DeploymentMessage, FlowContext, FlowElementContext } from './flow.interface';
4
4
  import { Context } from './FlowApplication';
5
5
  import { FlowEvent } from './FlowEvent';
6
6
  import { FlowLogger } from './FlowLogger';
@@ -14,6 +14,7 @@ export declare abstract class FlowElement<T = any> {
14
14
  protected properties: T;
15
15
  private readonly app?;
16
16
  private readonly rpcRoutingKey;
17
+ private stopPropagateStream;
17
18
  constructor({ app, logger, ...metadata }: Context, properties?: unknown, propertiesClassType?: ClassType<T>, whitelist?: boolean);
18
19
  get flowProperties(): Record<string, any>;
19
20
  onDestroy?: () => void;
@@ -21,17 +22,24 @@ export declare abstract class FlowElement<T = any> {
21
22
  onFlowPropertiesChanged?: (properties: Record<string, any>) => void;
22
23
  onContextChanged: (context: Partial<FlowContext>) => void;
23
24
  onPropertiesChanged: (properties: T) => void;
25
+ getMetadata: () => FlowElementContext;
24
26
  protected setProperties: (properties: T) => void;
25
27
  handleApiError: (error: any) => void;
28
+ /**
29
+ * @deprecated since version 4.8.0, will be removed in 5.0.0, use emitEvent(...) instead
30
+ */
26
31
  protected emitOutput(data?: any, outputId?: string, time?: Date): FlowEvent;
32
+ protected emitEvent(data: Record<string, any>, inputEvent: FlowEvent, outputId?: string, time?: Date): FlowEvent;
27
33
  protected validateProperties<P>(classType: ClassType<P>, properties?: any, whitelist?: boolean): P;
34
+ protected logValidationErrors(error: any, parent?: string): void;
28
35
  protected validateEventData<E>(classType: ClassType<E>, event: FlowEvent, whitelist?: boolean): E;
29
- protected interpolate(text: string, ...templateVariables: any): string;
36
+ protected interpolate: (value: any, ...templateVariables: any) => any;
30
37
  protected callRpcFunction(functionName: string, ...args: any[]): Promise<unknown>;
31
- protected runPyRpcScript(scriptPath: string): PythonShell;
38
+ protected runPyRpcScript(scriptPath: string, ...args: (string | boolean | number)[]): PythonShell;
32
39
  }
33
40
  export declare function InputStream(id?: string, options?: {
34
41
  concurrent?: number;
42
+ stopPropagation?: boolean;
35
43
  }): MethodDecorator;
36
44
  export declare function FlowFunction(fqn: string): ClassDecorator;
37
45
  export declare abstract class FlowResource<T = any> extends FlowElement<T> {