@hahnpro/flow-sdk 4.23.8 → 4.24.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.
@@ -1,36 +1,47 @@
1
1
  import 'reflect-metadata';
2
+ import { API } from '@hahnpro/hpc-api';
3
+ import { AmqpConnectionManager } from 'amqp-connection-manager';
2
4
  import { CloudEvent } from 'cloudevents';
3
5
  import { PartialObserver } from 'rxjs';
4
- import { API } from '@hahnpro/hpc-api';
5
- import { AmqpConnection, Nack } from './amqp';
6
+ import { AmqpConnection, AmqpConnectionConfig, Nack } 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
+ skipApi?: boolean;
16
+ explicitInit?: boolean;
17
+ }
10
18
  export declare class FlowApplication {
11
19
  private modules;
12
20
  private flow;
13
- private readonly baseLogger?;
14
- private amqpConnection?;
15
- private skipApi;
16
- api: API;
21
+ private _api;
22
+ private _rpcClient;
23
+ private amqpChannel;
24
+ private readonly amqpConnection;
25
+ private readonly baseLogger;
17
26
  private context;
18
27
  private declarations;
19
28
  private elements;
29
+ private initialized;
20
30
  private readonly logger;
21
31
  private outputStreamMap;
22
32
  private outputQueueMetrics;
23
33
  private performanceMap;
24
34
  private properties;
25
- private _rpcClient;
26
- private initialized;
35
+ private readonly skipApi;
36
+ constructor(modules: ClassType<any>[], flow: Flow, config?: FlowAppConfig);
27
37
  constructor(modules: ClassType<any>[], flow: Flow, baseLogger?: Logger, amqpConnection?: AmqpConnection, skipApi?: boolean, explicitInit?: boolean);
38
+ get rpcClient(): RpcClient;
39
+ get api(): API;
28
40
  init(): Promise<void>;
29
41
  private publishLifecycleEvent;
30
42
  private setQueueMetrics;
31
43
  private updateMetrics;
32
44
  subscribe: (streamId: string, observer: PartialObserver<FlowEvent>) => import("rxjs").Subscription;
33
- rpcClient: () => RpcClient;
34
45
  emit: (event: FlowEvent) => void;
35
46
  emitPartial: (completeEvent: FlowEvent, partialEvent: FlowEvent) => void;
36
47
  getProperties(): Record<string, any>;
@@ -39,7 +50,7 @@ export declare class FlowApplication {
39
50
  * Publish a flow event to the amqp flowlogs exchange.
40
51
  * If the event size exceeds the limit it will be truncated
41
52
  */
42
- publishEvent: (event: FlowEvent) => void;
53
+ publishEvent: (event: FlowEvent) => Promise<boolean>;
43
54
  /**
44
55
  * Calls onDestroy lifecycle method on all flow elements,
45
56
  * closes amqp connection after allowing logs to be processed and published
@@ -56,3 +67,4 @@ export interface Context extends FlowElementContext {
56
67
  app?: FlowApplication;
57
68
  logger?: Logger;
58
69
  }
70
+ export {};
@@ -3,36 +3,33 @@ 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 hpc_api_1 = require("@hahnpro/hpc-api");
6
7
  const cloudevents_1 = require("cloudevents");
8
+ const crypto_1 = require("crypto");
7
9
  const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
8
10
  const perf_hooks_1 = require("perf_hooks");
9
11
  const rxjs_1 = require("rxjs");
10
12
  const operators_1 = require("rxjs/operators");
11
- const crypto_1 = require("crypto");
12
- const hpc_api_1 = require("@hahnpro/hpc-api");
13
13
  const amqp_1 = require("./amqp");
14
- const utils_1 = require("./utils");
15
14
  const flow_interface_1 = require("./flow.interface");
16
15
  const FlowLogger_1 = require("./FlowLogger");
17
16
  const RpcClient_1 = require("./RpcClient");
17
+ const utils_1 = require("./utils");
18
18
  const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024;
19
19
  const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
20
20
  const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
21
21
  class FlowApplication {
22
- constructor(modules, flow, baseLogger, amqpConnection, skipApi = false, explicitInit = false) {
22
+ constructor(modules, flow, baseLoggerOrConfig, amqpConnection, skipApi, explicitInit) {
23
23
  this.modules = modules;
24
24
  this.flow = flow;
25
- this.baseLogger = baseLogger;
26
- this.amqpConnection = amqpConnection;
27
- this.skipApi = skipApi;
28
25
  this.declarations = {};
29
26
  this.elements = {};
27
+ this.initialized = false;
30
28
  this.outputStreamMap = new Map();
31
29
  this.outputQueueMetrics = new Map();
32
30
  this.performanceMap = new Map();
33
- this.initialized = false;
34
31
  this.publishLifecycleEvent = (element, flowEventId, eventType, data = {}) => {
35
- if (!this.amqpConnection) {
32
+ if (!this.amqpChannel) {
36
33
  return;
37
34
  }
38
35
  try {
@@ -46,7 +43,7 @@ class FlowApplication {
46
43
  time: new Date().toISOString(),
47
44
  });
48
45
  const message = event.toJSON();
49
- return this.amqpConnection.publish('flow', 'lifecycle', message);
46
+ return this.amqpChannel.publish('flow', 'lifecycle', message);
50
47
  }
51
48
  catch (err) {
52
49
  this.logger.error(err);
@@ -76,7 +73,6 @@ class FlowApplication {
76
73
  }
77
74
  };
78
75
  this.subscribe = (streamId, observer) => this.getOutputStream(streamId).subscribe(observer);
79
- this.rpcClient = () => this._rpcClient;
80
76
  this.emit = (event) => {
81
77
  if (event) {
82
78
  try {
@@ -139,7 +135,7 @@ class FlowApplication {
139
135
  data: { deploymentId: this.context.deploymentId, status: 'updated' },
140
136
  };
141
137
  try {
142
- (_f = this.amqpConnection) === null || _f === void 0 ? void 0 : _f.publish('deployment', 'health', statusEvent);
138
+ (_f = this.amqpChannel) === null || _f === void 0 ? void 0 : _f.publish('deployment', 'health', statusEvent);
143
139
  }
144
140
  catch (err) {
145
141
  this.logger.error(err);
@@ -155,7 +151,7 @@ class FlowApplication {
155
151
  data: { deploymentId: this.context.deploymentId, status: 'updating failed' },
156
152
  };
157
153
  try {
158
- (_g = this.amqpConnection) === null || _g === void 0 ? void 0 : _g.publish('deployment', 'health', statusEvent);
154
+ (_g = this.amqpChannel) === null || _g === void 0 ? void 0 : _g.publish('deployment', 'health', statusEvent);
159
155
  }
160
156
  catch (e) {
161
157
  this.logger.error(e);
@@ -183,7 +179,7 @@ class FlowApplication {
183
179
  return undefined;
184
180
  };
185
181
  this.publishEvent = (event) => {
186
- if (!this.amqpConnection) {
182
+ if (!this.amqpChannel) {
187
183
  return;
188
184
  }
189
185
  try {
@@ -191,13 +187,26 @@ class FlowApplication {
191
187
  if ((0, object_sizeof_1.default)(message) > MAX_EVENT_SIZE_BYTES) {
192
188
  message.data = (0, utils_1.truncate)(message.data);
193
189
  }
194
- return this.amqpConnection.publish('flowlogs', '', message);
190
+ return this.amqpChannel.publish('flowlogs', '', message);
195
191
  }
196
192
  catch (err) {
197
193
  this.logger.error(err);
198
194
  }
199
195
  };
200
- this.logger = new FlowLogger_1.FlowLogger(Object.assign({ id: 'none', functionFqn: 'FlowApplication' }, flow === null || flow === void 0 ? void 0 : flow.context), baseLogger || undefined, this.publishEvent);
196
+ if (baseLoggerOrConfig && !baseLoggerOrConfig.log) {
197
+ const config = baseLoggerOrConfig;
198
+ this.baseLogger = config.logger;
199
+ this.amqpConnection = config.amqpConnection || (0, amqp_1.createAmqpConnection)(config.amqpConfig);
200
+ this.skipApi = config.skipApi || false;
201
+ explicitInit = config.explicitInit || false;
202
+ }
203
+ else {
204
+ this.baseLogger = baseLoggerOrConfig;
205
+ this.amqpConnection = amqpConnection === null || amqpConnection === void 0 ? void 0 : amqpConnection.managedConnection;
206
+ this.skipApi = skipApi || false;
207
+ explicitInit = explicitInit || false;
208
+ }
209
+ this.logger = new FlowLogger_1.FlowLogger(Object.assign({ id: 'none', functionFqn: 'FlowApplication' }, flow === null || flow === void 0 ? void 0 : flow.context), this.baseLogger || undefined, this.publishEvent);
201
210
  process.once('uncaughtException', (err) => {
202
211
  this.logger.error('Uncaught exception!');
203
212
  this.logger.error(err);
@@ -215,14 +224,24 @@ class FlowApplication {
215
224
  this.init();
216
225
  }
217
226
  }
227
+ get rpcClient() {
228
+ if (!this._rpcClient && this.amqpConnection) {
229
+ this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection, this.logger);
230
+ }
231
+ return this._rpcClient;
232
+ }
233
+ get api() {
234
+ return this._api;
235
+ }
218
236
  async init() {
237
+ var _a;
219
238
  if (this.initialized)
220
239
  return;
221
240
  this.context = Object.assign({}, this.flow.context);
222
241
  this.properties = this.flow.properties || {};
223
242
  try {
224
243
  if (!this.skipApi) {
225
- this.api = new hpc_api_1.API();
244
+ this._api = new hpc_api_1.API();
226
245
  }
227
246
  }
228
247
  catch (err) {
@@ -232,29 +251,28 @@ class FlowApplication {
232
251
  this.logger.error(new Error(err));
233
252
  await this.destroy(1);
234
253
  };
235
- if (this.amqpConnection) {
236
- try {
237
- await this.amqpConnection.managedChannel.assertExchange('deployment', 'direct', { durable: true });
238
- await this.amqpConnection.managedChannel.assertExchange('flowlogs', 'fanout', { durable: true });
239
- await this.amqpConnection.managedChannel.assertExchange('flow', 'direct', { durable: true });
240
- }
241
- catch (e) {
242
- await logErrorAndExit(`Could not assert exchanges: ${e}`);
243
- return;
244
- }
245
- try {
246
- await this.amqpConnection.createSubscriber((msg) => this.onMessage(msg), {
247
- exchange: 'deployment',
248
- routingKey: this.context.deploymentId,
249
- queueOptions: { durable: false, exclusive: true },
250
- }, 'FlowApplication.onMessage');
251
- }
252
- catch (err) {
253
- await logErrorAndExit(`Could not subscribe to deployment exchange: ${err}`);
254
- return;
255
- }
256
- this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection);
257
- }
254
+ this.amqpChannel = (_a = this.amqpConnection) === null || _a === void 0 ? void 0 : _a.createChannel({
255
+ json: true,
256
+ setup: async (channel) => {
257
+ try {
258
+ await channel.assertExchange('deployment', 'direct', { durable: true });
259
+ await channel.assertExchange('flowlogs', 'fanout', { durable: true });
260
+ await channel.assertExchange('flow', 'direct', { durable: true });
261
+ }
262
+ catch (e) {
263
+ await logErrorAndExit(`Could not assert exchanges: ${e}`);
264
+ }
265
+ try {
266
+ const queue = await channel.assertQueue(null, { durable: false, exclusive: true });
267
+ await channel.bindQueue(queue.queue, 'deployment', this.context.deploymentId);
268
+ await channel.consume(queue.queue, (msg) => this.onMessage(msg));
269
+ }
270
+ catch (err) {
271
+ await logErrorAndExit(`Could not subscribe to deployment exchange: ${err}`);
272
+ }
273
+ },
274
+ });
275
+ this.amqpChannel && (await this.amqpChannel.waitForConnect());
258
276
  for (const module of this.modules) {
259
277
  const moduleName = Reflect.getMetadata('module:name', module);
260
278
  const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
@@ -344,19 +362,19 @@ class FlowApplication {
344
362
  return this.properties;
345
363
  }
346
364
  async destroy(exitCode = 0) {
347
- var _a, _b, _c, _d;
365
+ var _a;
348
366
  try {
349
367
  try {
350
368
  for (const element of Object.values(this.elements)) {
351
369
  (_a = element === null || element === void 0 ? void 0 : element.onDestroy) === null || _a === void 0 ? void 0 : _a.call(element);
352
370
  }
353
- await ((_b = this._rpcClient) === null || _b === void 0 ? void 0 : _b.close());
371
+ this._rpcClient && (await this._rpcClient.close());
354
372
  }
355
373
  catch (err) {
356
374
  this.logger.error(err);
357
375
  }
358
376
  await (0, utils_1.delay)(250);
359
- await ((_d = (_c = this.amqpConnection) === null || _c === void 0 ? void 0 : _c.managedConnection) === null || _d === void 0 ? void 0 : _d.close());
377
+ this.amqpConnection && (await this.amqpConnection.close());
360
378
  }
361
379
  catch (err) {
362
380
  console.error(err);
@@ -93,7 +93,7 @@ class FlowElement {
93
93
  }
94
94
  async callRpcFunction(functionName, ...args) {
95
95
  var _a;
96
- return (_a = this.app) === null || _a === void 0 ? void 0 : _a.rpcClient().callFunction(this.rpcRoutingKey, functionName, ...args);
96
+ return (_a = this.app) === null || _a === void 0 ? void 0 : _a.rpcClient.callFunction(this.rpcRoutingKey, functionName, ...args);
97
97
  }
98
98
  runPyRpcScript(scriptPath, ...args) {
99
99
  const options = {
@@ -1,9 +1,10 @@
1
- import { AmqpConnection } from './amqp';
1
+ import type { AmqpConnectionManager } from 'amqp-connection-manager';
2
+ import { FlowLogger } from './FlowLogger';
2
3
  export declare class RpcClient {
3
- private amqpConnection;
4
- private channel;
4
+ private readonly logger?;
5
+ private readonly channel;
5
6
  private openRequests;
6
- constructor(amqpConnection: AmqpConnection);
7
+ constructor(amqpConnection: AmqpConnectionManager, logger?: FlowLogger);
7
8
  private onMessage;
8
9
  callFunction: (routingKey: string, functionName: string, ...args: any[]) => Promise<unknown>;
9
10
  declareFunction: (routingKey: string, name: string) => (...args: any[]) => Promise<unknown>;
package/dist/RpcClient.js CHANGED
@@ -1,10 +1,14 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.RpcClient = void 0;
4
+ const tslib_1 = require("tslib");
4
5
  const crypto_1 = require("crypto");
6
+ const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
7
+ const MAX_MSG_SIZE = +process.env.MAX_RPC_MSG_SIZE_BYTES;
8
+ const WARN_MSG_SIZE = +process.env.WARN_RPC_MSG_SIZE_BYTES;
5
9
  class RpcClient {
6
- constructor(amqpConnection) {
7
- this.amqpConnection = amqpConnection;
10
+ constructor(amqpConnection, logger) {
11
+ this.logger = logger;
8
12
  this.openRequests = new Map();
9
13
  this.onMessage = (msg) => {
10
14
  if (this.openRequests.has(msg.properties.correlationId)) {
@@ -39,6 +43,16 @@ class RpcClient {
39
43
  this.callFunction = (routingKey, functionName, ...args) => {
40
44
  const stack = new Error('test').stack;
41
45
  return new Promise((resolve, reject) => {
46
+ var _a;
47
+ if (MAX_MSG_SIZE || WARN_MSG_SIZE) {
48
+ const messageSize = (0, object_sizeof_1.default)(args);
49
+ if (messageSize > MAX_MSG_SIZE) {
50
+ throw new Error(`Max RPC message size exceeded: ${messageSize} bytes / ${MAX_MSG_SIZE} bytes`);
51
+ }
52
+ if (messageSize > WARN_MSG_SIZE) {
53
+ (_a = this.logger) === null || _a === void 0 ? void 0 : _a.warn(`Large RPC message size detected: ${messageSize} bytes`);
54
+ }
55
+ }
42
56
  const call = { functionName, arguments: args };
43
57
  const correlationId = (0, crypto_1.randomUUID)();
44
58
  this.openRequests.set(correlationId, { resolve, reject, trace: RpcClient.formatTrace(stack) });
@@ -53,7 +67,7 @@ class RpcClient {
53
67
  if (!amqpConnection) {
54
68
  throw new Error('currently no amqp connection available');
55
69
  }
56
- this.channel = this.amqpConnection.managedConnection.createChannel({
70
+ this.channel = amqpConnection.createChannel({
57
71
  json: true,
58
72
  setup: async (channel) => {
59
73
  await channel.assertExchange('rpc_direct_exchange', 'direct', { durable: false });
package/dist/amqp.d.ts CHANGED
@@ -1,37 +1,19 @@
1
- import type { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager';
2
- import type { Channel, Connection, ConsumeMessage, Options } from 'amqplib';
3
- interface SubscriptionResult {
4
- consumerTag: string;
5
- }
1
+ import { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager';
6
2
  export interface AmqpConnection {
7
- channel: Channel;
8
- connection: Connection;
9
3
  managedChannel: ChannelWrapper;
10
4
  managedConnection: AmqpConnectionManager;
11
- createSubscriber<T>(handler: (msg: T | undefined, rawMessage?: ConsumeMessage) => Promise<any | undefined | void>, msgOptions: MessageHandlerOptions, originalHandlerName: string): Promise<SubscriptionResult>;
12
- publish(exchange: string, routingKey: string, message: any, options?: Options.Publish): void;
13
5
  }
6
+ export interface AmqpConnectionConfig {
7
+ protocol?: string;
8
+ hostname?: string;
9
+ vhost?: string;
10
+ user?: string;
11
+ password?: string;
12
+ port?: number;
13
+ }
14
+ export declare function createAmqpConnection(config: AmqpConnectionConfig): AmqpConnectionManager;
14
15
  export declare class Nack {
15
16
  private readonly _requeue;
16
17
  constructor(_requeue?: boolean);
17
18
  get requeue(): boolean;
18
19
  }
19
- export interface MessageHandlerOptions {
20
- exchange: string;
21
- routingKey: string | string[];
22
- queue?: string;
23
- queueOptions?: QueueOptions;
24
- }
25
- export interface QueueOptions {
26
- durable?: boolean;
27
- exclusive?: boolean;
28
- autoDelete?: boolean;
29
- arguments?: any;
30
- messageTtl?: number;
31
- expires?: number;
32
- deadLetterExchange?: string;
33
- deadLetterRoutingKey?: string;
34
- maxLength?: number;
35
- maxPriority?: number;
36
- }
37
- export {};
package/dist/amqp.js CHANGED
@@ -1,6 +1,16 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.Nack = void 0;
3
+ exports.Nack = exports.createAmqpConnection = void 0;
4
+ const amqp_connection_manager_1 = require("amqp-connection-manager");
5
+ function createAmqpConnection(config) {
6
+ if (!config) {
7
+ return;
8
+ }
9
+ const { protocol = process.env.RABBIT_PROTOCOL || 'amqp', hostname = process.env.RABBIT_HOST || 'localhost', port = +process.env.RABBIT_PORT || 5672, user = process.env.RABBIT_USER || 'guest', password = process.env.RABBIT_PASSWORD || 'guest', vhost = process.env.RABBIT_VHOST || '', } = config;
10
+ const uri = `${protocol}://${user}:${password}@${hostname}:${port}${vhost ? '/' + vhost : ''}`;
11
+ return (0, amqp_connection_manager_1.connect)(uri);
12
+ }
13
+ exports.createAmqpConnection = createAmqpConnection;
4
14
  class Nack {
5
15
  constructor(_requeue = false) {
6
16
  this._requeue = _requeue;
@@ -12,6 +12,7 @@ host = os.getenv("RABBIT_HOST", "localhost")
12
12
  port = os.getenv("RABBIT_PORT", "5672")
13
13
  vhost = os.getenv("RABBIT_VHOST", "")
14
14
  routingKey = os.getenv("RPC_ROUTING_KEY", "rpc")
15
+ max_msg_size = int(os.getenv("MAX_RPC_MSG_SIZE_BYTES", "0"))
15
16
 
16
17
  remote_procedures = {}
17
18
  flow_logs_exchange: AbstractRobustExchange
@@ -64,11 +65,18 @@ async def on_message(exchange: Exchange, message: IncomingMessage):
64
65
 
65
66
 
66
67
  async def send_reply(exchange: Exchange, reply, original_message: Message):
68
+ body = json.dumps(reply).encode("utf-8")
69
+
70
+ if max_msg_size > 0 and len(body) > max_msg_size:
71
+ body = json.dumps(
72
+ {
73
+ "type": "error",
74
+ "message": "Max RPC message size exceeded: " + str(len(body)) + " bytes / " + max_msg_size + " bytes",
75
+ }
76
+ ).encode("utf-8")
77
+
67
78
  await exchange.publish(
68
- Message(
69
- body=json.dumps(reply).encode("utf-8"),
70
- correlation_id=original_message.correlation_id
71
- ),
79
+ Message(body=body, correlation_id=original_message.correlation_id),
72
80
  routing_key=original_message.reply_to,
73
81
  )
74
82
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hahnpro/flow-sdk",
3
- "version": "4.23.8",
3
+ "version": "4.24.0",
4
4
  "description": "SDK for building Flow Modules",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -24,7 +24,7 @@
24
24
  "access": "public"
25
25
  },
26
26
  "dependencies": {
27
- "amqp-connection-manager": "^3.9.0",
27
+ "amqp-connection-manager": "^4.1.11",
28
28
  "amqplib": "^0.10.3",
29
29
  "class-transformer": "0.5.1",
30
30
  "class-validator": "~0.14.0",
@@ -38,9 +38,6 @@
38
38
  "@hahnpro/hpc-api": "3.7.1"
39
39
  },
40
40
  "devDependencies": {
41
- "@golevelup/nestjs-rabbitmq": "^3.5.0",
42
- "@nestjs/common": "^9.3.9",
43
- "@types/amqp-connection-manager": "^2.0.12",
44
41
  "@types/amqplib": "^0.10.1",
45
42
  "@types/jest": "^29.4.2",
46
43
  "@types/lodash": "^4.14.191",