@hahnpro/flow-sdk 2025.11.0 → 2025.11.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hahnpro/flow-sdk",
3
- "version": "2025.11.0",
3
+ "version": "2025.11.2",
4
4
  "description": "SDK for building Flow Modules",
5
5
  "license": "MIT",
6
6
  "author": {
@@ -17,12 +17,10 @@
17
17
  "access": "public"
18
18
  },
19
19
  "dependencies": {
20
- "@hahnpro/hpc-api": "2025.11.0",
20
+ "@hahnpro/hpc-api": "2025.11.2",
21
21
  "@nats-io/jetstream": "3.2.0",
22
22
  "@nats-io/nats-core": "3.2.0",
23
23
  "@nats-io/transport-node": "3.2.0",
24
- "amqp-connection-manager": "4.1.15",
25
- "amqplib": "0.10.9",
26
24
  "class-transformer": "0.5.1",
27
25
  "class-validator": "0.14.2",
28
26
  "cloudevents": "10.0.0",
@@ -34,7 +32,6 @@
34
32
  "string-interp": "0.3.6"
35
33
  },
36
34
  "devDependencies": {
37
- "@types/amqplib": "0.10.8",
38
35
  "@types/jest": "30.0.0",
39
36
  "@types/lodash": "4.17.20",
40
37
  "@types/node": "22.18.13",
@@ -46,8 +43,7 @@
46
43
  "axios": "1.13.1",
47
44
  "class-transformer": "0.5.1",
48
45
  "class-validator": "0.14.2",
49
- "lodash": "4.17.21",
50
- "python-shell": "5.x"
46
+ "lodash": "4.17.21"
51
47
  },
52
48
  "engines": {
53
49
  "node": ">=v22"
@@ -1,19 +1,14 @@
1
1
  import 'reflect-metadata';
2
2
  import { API, HttpClientService, MockAPI } from '@hahnpro/hpc-api';
3
3
  import { NatsConnection, ConnectionOptions as NatsConnectionOptions } from '@nats-io/nats-core';
4
- import { AmqpConnectionManager } from 'amqp-connection-manager';
5
4
  import { CloudEvent } from 'cloudevents';
6
5
  import { PartialObserver } from 'rxjs';
7
- import { AmqpConnection, AmqpConnectionConfig } from './amqp';
8
6
  import { ContextManager } from './ContextManager';
9
7
  import { ClassType, Flow, FlowElementContext } from './flow.interface';
10
8
  import { FlowEvent } from './FlowEvent';
11
9
  import { Logger } from './FlowLogger';
12
- import { RpcClient } from './RpcClient';
13
10
  interface FlowAppConfig {
14
11
  logger?: Logger;
15
- amqpConfig?: AmqpConnectionConfig;
16
- amqpConnection?: AmqpConnectionManager;
17
12
  natsConfig?: NatsConnectionOptions;
18
13
  natsConnection?: NatsConnection;
19
14
  apiClient?: HttpClientService;
@@ -25,9 +20,6 @@ export declare class FlowApplication {
25
20
  private modules;
26
21
  private flow;
27
22
  private _api;
28
- private _rpcClient;
29
- private amqpChannel;
30
- private readonly amqpConnection;
31
23
  private readonly natsConnectionConfig?;
32
24
  private _natsConnection?;
33
25
  private readonly baseLogger;
@@ -44,8 +36,7 @@ export declare class FlowApplication {
44
36
  private readonly contextManager;
45
37
  private natsMessageIterator;
46
38
  constructor(modules: ClassType<any>[], flow: Flow, config?: FlowAppConfig);
47
- constructor(modules: ClassType<any>[], flow: Flow, baseLogger?: Logger, amqpConnection?: AmqpConnection, natsConnection?: NatsConnection, skipApi?: boolean, explicitInit?: boolean);
48
- get rpcClient(): RpcClient;
39
+ constructor(modules: ClassType<any>[], flow: Flow, baseLogger?: Logger, natsConnection?: NatsConnection, skipApi?: boolean, explicitInit?: boolean);
49
40
  get api(): API;
50
41
  get natsConnection(): NatsConnection;
51
42
  getContextManager(): ContextManager;
@@ -60,7 +51,7 @@ export declare class FlowApplication {
60
51
  emitPartial: (completeEvent: FlowEvent, partialEvent: FlowEvent) => void;
61
52
  onMessage: (cloudEvent: CloudEvent) => Promise<void>;
62
53
  /**
63
- * Publish a flow event to the amqp flowlogs exchange.
54
+ * Publish a flow event to NATS
64
55
  * If the event size exceeds the limit it will be truncated
65
56
  *
66
57
  * TODO warum darf hier nicht false zurückgegeben werden? -> erzeugt loop
@@ -68,7 +59,7 @@ export declare class FlowApplication {
68
59
  publishNatsEventFlowlogs: (event: FlowEvent) => Promise<boolean>;
69
60
  /**
70
61
  * Calls onDestroy lifecycle method on all flow elements,
71
- * closes amqp connection after allowing logs to be processed and published
62
+ * closes NATS connection after allowing logs to be processed and published
72
63
  * then exits process
73
64
  */
74
65
  destroy(exitCode?: number): Promise<void>;
@@ -11,18 +11,16 @@ const lodash_1 = require("lodash");
11
11
  const object_sizeof_1 = tslib_1.__importDefault(require("object-sizeof"));
12
12
  const rxjs_1 = require("rxjs");
13
13
  const operators_1 = require("rxjs/operators");
14
- const amqp_1 = require("./amqp");
15
14
  const ContextManager_1 = require("./ContextManager");
16
15
  const flow_interface_1 = require("./flow.interface");
17
16
  const FlowLogger_1 = require("./FlowLogger");
18
17
  const nats_1 = require("./nats");
19
- const RpcClient_1 = require("./RpcClient");
20
18
  const utils_1 = require("./utils");
21
19
  const MAX_EVENT_SIZE_BYTES = +process.env.MAX_EVENT_SIZE_BYTES || 512 * 1024; // 512kb
22
20
  const WARN_EVENT_PROCESSING_SEC = +process.env.WARN_EVENT_PROCESSING_SEC || 60;
23
21
  const WARN_EVENT_QUEUE_SIZE = +process.env.WARN_EVENT_QUEUE_SIZE || 100;
24
22
  class FlowApplication {
25
- constructor(modules, flow, baseLoggerOrConfig, amqpConnection, natsConnection, skipApi, explicitInit, mockApi) {
23
+ constructor(modules, flow, baseLoggerOrConfig, natsConnection, skipApi, explicitInit, mockApi) {
26
24
  this.modules = modules;
27
25
  this.flow = flow;
28
26
  this.declarations = {};
@@ -167,13 +165,9 @@ class FlowApplication {
167
165
  }
168
166
  }
169
167
  }
170
- else if (cloudEvent.subject.endsWith('.destroy')) {
171
- // TODO war com.flowstudio.deployment.destroy in RabbitMq: wo wird das jetzt wieder gesendet?
172
- this.destroy();
173
- }
174
168
  };
175
169
  /**
176
- * Publish a flow event to the amqp flowlogs exchange.
170
+ * Publish a flow event to NATS
177
171
  * If the event size exceeds the limit it will be truncated
178
172
  *
179
173
  * TODO warum darf hier nicht false zurückgegeben werden? -> erzeugt loop
@@ -204,7 +198,6 @@ class FlowApplication {
204
198
  if (baseLoggerOrConfig && !baseLoggerOrConfig.log) {
205
199
  const config = baseLoggerOrConfig;
206
200
  this.baseLogger = config.logger;
207
- this.amqpConnection = config.amqpConnection || (0, amqp_1.createAmqpConnection)(config.amqpConfig);
208
201
  this.natsConnectionConfig = config.natsConfig;
209
202
  this._natsConnection = config.natsConnection;
210
203
  this.skipApi = config.skipApi || false;
@@ -214,7 +207,6 @@ class FlowApplication {
214
207
  }
215
208
  else {
216
209
  this.baseLogger = baseLoggerOrConfig;
217
- this.amqpConnection = amqpConnection?.managedConnection;
218
210
  this._natsConnection = natsConnection;
219
211
  this.skipApi = skipApi || false;
220
212
  explicitInit = explicitInit || false;
@@ -239,12 +231,6 @@ class FlowApplication {
239
231
  this.init();
240
232
  }
241
233
  }
242
- get rpcClient() {
243
- if (!this._rpcClient && this.amqpConnection) {
244
- this._rpcClient = new RpcClient_1.RpcClient(this.amqpConnection, this.logger);
245
- }
246
- return this._rpcClient;
247
- }
248
234
  get api() {
249
235
  return this._api;
250
236
  }
@@ -343,20 +329,6 @@ class FlowApplication {
343
329
  await logErrorAndExit(`Could not set up consumer for deployment messages exchanges: ${e}`);
344
330
  }
345
331
  }
346
- this.amqpChannel = this.amqpConnection?.createChannel({
347
- json: true,
348
- setup: async (channel) => {
349
- try {
350
- await channel.assertExchange('flow', 'direct', { durable: true }); // TODO wieso weshalb warum: wo wird das gebraucht?
351
- }
352
- catch (e) {
353
- await logErrorAndExit(`Could not assert exchanges: ${e}`);
354
- }
355
- },
356
- });
357
- if (this.amqpChannel) {
358
- await this.amqpChannel.waitForConnect();
359
- }
360
332
  for (const module of this.modules) {
361
333
  const moduleName = Reflect.getMetadata('module:name', module);
362
334
  const moduleDeclarations = Reflect.getMetadata('module:declarations', module);
@@ -447,7 +419,7 @@ class FlowApplication {
447
419
  }
448
420
  /**
449
421
  * Calls onDestroy lifecycle method on all flow elements,
450
- * closes amqp connection after allowing logs to be processed and published
422
+ * closes NATS connection after allowing logs to be processed and published
451
423
  * then exits process
452
424
  */
453
425
  async destroy(exitCode = 0) {
@@ -456,18 +428,12 @@ class FlowApplication {
456
428
  for (const element of Object.values(this.elements)) {
457
429
  element?.onDestroy?.();
458
430
  }
459
- if (this._rpcClient) {
460
- await this._rpcClient.close();
461
- }
462
431
  }
463
432
  catch (err) {
464
433
  this.logger.error(err);
465
434
  }
466
435
  // allow time for logs to be processed
467
436
  await (0, utils_1.delay)(250);
468
- if (this.amqpConnection) {
469
- await this.amqpConnection.close();
470
- }
471
437
  // Close all output streams
472
438
  for (const [id, stream] of this.outputStreamMap.entries()) {
473
439
  try {
@@ -1,5 +1,4 @@
1
1
  import { API } from '@hahnpro/hpc-api';
2
- import { PythonShell } from 'python-shell';
3
2
  import { ClassType, DeploymentMessage, FlowContext, FlowElementContext } from './flow.interface';
4
3
  import { Context } from './FlowApplication';
5
4
  import { FlowEvent } from './FlowEvent';
@@ -14,7 +13,6 @@ export declare abstract class FlowElement<T = any> {
14
13
  protected properties: T;
15
14
  private propertiesWithPlaceholders;
16
15
  private readonly app?;
17
- private readonly rpcRoutingKey;
18
16
  private stopPropagateStream;
19
17
  constructor({ app, logger, ...metadata }: Context, properties?: unknown, propertiesClassType?: ClassType<T>, whitelist?: boolean);
20
18
  /**
@@ -49,8 +47,6 @@ export declare abstract class FlowElement<T = any> {
49
47
  protected logValidationErrors(error: any, parent?: string): void;
50
48
  protected validateEventData<E>(classType: ClassType<E>, event: FlowEvent, whitelist?: boolean): E;
51
49
  protected interpolate: (value: any, ...templateVariables: any) => any;
52
- protected callRpcFunction(functionName: string, ...args: any[]): Promise<unknown>;
53
- protected runPyRpcScript(scriptPath: string, ...args: (string | boolean | number)[]): PythonShell;
54
50
  }
55
51
  export declare function InputStream(id?: string, options?: {
56
52
  concurrent?: number;
@@ -5,7 +5,6 @@ exports.InputStream = InputStream;
5
5
  exports.FlowFunction = FlowFunction;
6
6
  const class_transformer_1 = require("class-transformer");
7
7
  const class_validator_1 = require("class-validator");
8
- const python_shell_1 = require("python-shell");
9
8
  const FlowEvent_1 = require("./FlowEvent");
10
9
  const FlowLogger_1 = require("./FlowLogger");
11
10
  const utils_1 = require("./utils");
@@ -35,7 +34,6 @@ class FlowElement {
35
34
  this.api = this.app?.api;
36
35
  this.metadata = { ...metadata, functionFqn: this.functionFqn };
37
36
  this.logger = new FlowLogger_1.FlowLogger(this.metadata, logger || undefined, this.app?.publishNatsEventFlowlogs);
38
- this.rpcRoutingKey = (this.metadata.flowId || '') + (this.metadata.deploymentId || '') + this.metadata.id;
39
37
  if (properties) {
40
38
  this.setProperties(properties);
41
39
  }
@@ -115,26 +113,6 @@ class FlowElement {
115
113
  validateEventData(classType, event, whitelist = false) {
116
114
  return this.validateProperties(classType, event.getData(), whitelist);
117
115
  }
118
- async callRpcFunction(functionName, ...args) {
119
- return this.app?.rpcClient.callFunction(this.rpcRoutingKey, functionName, ...args);
120
- }
121
- runPyRpcScript(scriptPath, ...args) {
122
- const options = {
123
- mode: 'text',
124
- pythonOptions: ['-u'],
125
- args: [__dirname, this.rpcRoutingKey, ...args.map((v) => v.toString())],
126
- };
127
- const pyshell = new python_shell_1.PythonShell(scriptPath, options);
128
- pyshell.on('message', (message) => {
129
- this.logger.debug(message);
130
- });
131
- pyshell.end((err) => {
132
- if (err) {
133
- this.logger.error(err);
134
- }
135
- });
136
- return pyshell;
137
- }
138
116
  }
139
117
  exports.FlowElement = FlowElement;
140
118
  function InputStream(id = 'default', options) {
@@ -1,13 +0,0 @@
1
- import type { AmqpConnectionManager } from 'amqp-connection-manager';
2
- import { FlowLogger } from './FlowLogger';
3
- export declare class RpcClient {
4
- private readonly logger?;
5
- private readonly channel;
6
- private openRequests;
7
- constructor(amqpConnection: AmqpConnectionManager, logger?: FlowLogger);
8
- private onMessage;
9
- callFunction: (routingKey: string, functionName: string, ...args: any[]) => Promise<unknown>;
10
- declareFunction: (routingKey: string, name: string) => (...args: any[]) => Promise<unknown>;
11
- close(): Promise<void>;
12
- static formatTrace(stack?: string): string;
13
- }
@@ -1,88 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.RpcClient = void 0;
4
- const tslib_1 = require("tslib");
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;
9
- class RpcClient {
10
- constructor(amqpConnection, logger) {
11
- this.logger = logger;
12
- this.openRequests = new Map();
13
- this.onMessage = (msg) => {
14
- if (this.openRequests.has(msg.properties.correlationId)) {
15
- const { resolve, reject, trace } = this.openRequests.get(msg.properties.correlationId);
16
- const response = JSON.parse(msg.content.toString());
17
- switch (response.type) {
18
- case 'reply':
19
- resolve(response.value);
20
- break;
21
- case 'error': {
22
- const err = new Error(response.message);
23
- if (response.stack) {
24
- const stack = RpcClient.formatTrace(response.stack);
25
- err.stack = 'Remote Stack\n'.concat(stack, '\nLocal Stack\n', trace);
26
- }
27
- else {
28
- err.stack = trace;
29
- }
30
- reject(err);
31
- break;
32
- }
33
- default:
34
- reject(response);
35
- break;
36
- }
37
- }
38
- else {
39
- const message = `received unexpected response correlationID: ${msg.properties.correlationId}`;
40
- /* eslint-disable-next-line no-console */
41
- console.warn(message);
42
- }
43
- };
44
- this.callFunction = (routingKey, functionName, ...args) => {
45
- // in case remote returns error add this to the trace
46
- const stack = new Error('test').stack;
47
- return new Promise((resolve, reject) => {
48
- // save to correlationId-> resolve/reject map
49
- // on return resolve or reject promise
50
- if (MAX_MSG_SIZE || WARN_MSG_SIZE) {
51
- const messageSize = (0, object_sizeof_1.default)(args);
52
- if (messageSize > MAX_MSG_SIZE) {
53
- throw new Error(`Max RPC message size exceeded: ${messageSize} bytes / ${MAX_MSG_SIZE} bytes`);
54
- }
55
- if (messageSize > WARN_MSG_SIZE) {
56
- this.logger?.warn(`Large RPC message size detected: ${messageSize} bytes`);
57
- }
58
- }
59
- const call = { functionName, arguments: args };
60
- const correlationId = (0, crypto_1.randomUUID)();
61
- this.openRequests.set(correlationId, { resolve, reject, trace: RpcClient.formatTrace(stack) });
62
- this.channel
63
- .publish('rpc_direct_exchange', routingKey, call, { correlationId, replyTo: 'amq.rabbitmq.reply-to' })
64
- .catch((err) => reject(err));
65
- });
66
- };
67
- this.declareFunction = (routingKey, name) => {
68
- return (...args) => this.callFunction(routingKey, name, ...args);
69
- };
70
- if (!amqpConnection) {
71
- throw new Error('currently no amqp connection available');
72
- }
73
- this.channel = amqpConnection.createChannel({
74
- json: true,
75
- setup: async (channel) => {
76
- await channel.assertExchange('rpc_direct_exchange', 'direct', { durable: false });
77
- await channel.consume('amq.rabbitmq.reply-to', this.onMessage, { noAck: true });
78
- },
79
- });
80
- }
81
- close() {
82
- return this.channel.close();
83
- }
84
- static formatTrace(stack = '') {
85
- return stack.split('\n').splice(1).join('\n');
86
- }
87
- }
88
- exports.RpcClient = RpcClient;
package/src/lib/amqp.d.ts DELETED
@@ -1,14 +0,0 @@
1
- import { AmqpConnectionManager, ChannelWrapper } from 'amqp-connection-manager';
2
- export interface AmqpConnection {
3
- managedChannel: ChannelWrapper;
4
- managedConnection: AmqpConnectionManager;
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;
package/src/lib/amqp.js DELETED
@@ -1,12 +0,0 @@
1
- "use strict";
2
- Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.createAmqpConnection = createAmqpConnection;
4
- const amqp_connection_manager_1 = require("amqp-connection-manager");
5
- function createAmqpConnection(config) {
6
- if (!config) {
7
- return null;
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
- }
@@ -1,116 +0,0 @@
1
- import asyncio
2
- import json
3
- from asyncio import Future
4
- from functools import partial, wraps
5
- from aio_pika import IncomingMessage, Exchange, Message, connect_robust, ExchangeType
6
- from aio_pika.abc import AbstractRobustExchange
7
- import os
8
-
9
- user = os.getenv("RABBIT_USER", "guest")
10
- password = os.getenv("RABBIT_PASSWORD", "guest")
11
- host = os.getenv("RABBIT_HOST", "localhost")
12
- port = os.getenv("RABBIT_PORT", "5672")
13
- vhost = os.getenv("RABBIT_VHOST", "")
14
- routingKey = os.getenv("RPC_ROUTING_KEY", "rpc")
15
- max_msg_size = int(os.getenv("MAX_RPC_MSG_SIZE_BYTES", "0"))
16
-
17
- remote_procedures = {}
18
- flow_logs_exchange: AbstractRobustExchange
19
-
20
-
21
- def RemoteProcedure(func):
22
- global remote_procedures
23
-
24
- @wraps(func)
25
- def function_wrapper(*args, **kwargs):
26
- return func(*args, **kwargs)
27
-
28
- remote_procedures[func.__name__] = func
29
-
30
- return function_wrapper
31
-
32
-
33
- loop = asyncio.new_event_loop()
34
- asyncio.set_event_loop(loop)
35
-
36
-
37
- async def on_message(exchange: Exchange, message: IncomingMessage):
38
- def callback(future: Future):
39
- try:
40
- res = future.result()
41
- if type(res) is tuple and type(res[1]) is type and issubclass(res[1], json.JSONEncoder):
42
- res = json.dumps(res[0], cls=res[1])
43
- reply1 = {"type": "reply", "value": res}
44
- except Exception as err:
45
- # print(traceback.format_list(traceback.extract_stack(err)))
46
- reply1 = {"type": "error", "message": str(err), "stack": "failed"}
47
-
48
- asyncio.ensure_future(send_reply(exchange, reply1, message), loop=loop)
49
-
50
-
51
- request = json.loads(message.body.decode())
52
-
53
- # call function
54
- if remote_procedures.keys().__contains__(request["functionName"]):
55
- func = remote_procedures.get(request["functionName"])
56
- future = loop.run_in_executor(None, func, *request["arguments"])
57
- future.add_done_callback(callback)
58
-
59
- else:
60
- reply = {
61
- "type": "error",
62
- "message": request["functionName"] + " is not a function",
63
- }
64
- await send_reply(exchange, reply, original_message=message)
65
-
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
-
78
- await exchange.publish(
79
- Message(body=body, correlation_id=original_message.correlation_id),
80
- routing_key=original_message.reply_to,
81
- )
82
-
83
-
84
- async def main(loop, routing_key):
85
- global flow_logs_exchange
86
-
87
- url = "amqp://%s:%s@%s:%s/%s" % (user, password, host, port, vhost)
88
- connection = await connect_robust(
89
- url, loop=loop
90
- )
91
-
92
- channel = await connection.channel()
93
-
94
- dest_exchange = await channel.declare_exchange(name="rpc_direct_exchange", type=ExchangeType.DIRECT)
95
- flow_logs_exchange = await channel.declare_exchange(name='flowlogs', type=ExchangeType.FANOUT, durable=True)
96
-
97
- queue = await channel.declare_queue("", exclusive=True)
98
-
99
- await queue.bind(dest_exchange, routing_key)
100
-
101
- await queue.consume(partial(
102
- on_message, channel.default_exchange)
103
- )
104
-
105
-
106
- def start_consumer(routing_key=routingKey):
107
- loop.create_task(main(loop, routing_key))
108
- loop.run_forever()
109
-
110
- def log(message):
111
- global flow_logs_exchange
112
-
113
- if flow_logs_exchange is not None:
114
- flow_logs_exchange.publish(Message(body=json.dumps(message).encode("utf-8")), "")
115
- else:
116
- print("Connection not established. Call start_consumer first.")