@hahnpro/flow-sdk 9.4.1 → 9.5.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.
@@ -32,9 +32,6 @@ class FlowApplication {
32
32
  this.outputQueueMetrics = new Map();
33
33
  this.performanceMap = new Map();
34
34
  this.publishLifecycleEvent = async (element, flowEventId, eventType, data = {}) => {
35
- if (!this.amqpChannel) {
36
- return;
37
- }
38
35
  try {
39
36
  const { flowId, deploymentId, id: elementId, functionFqn, inputStreamId } = element.getMetadata();
40
37
  const natsEvent = {
@@ -47,7 +44,7 @@ class FlowApplication {
47
44
  ...data,
48
45
  },
49
46
  };
50
- await (0, nats_1.publishNatsEvent)(this.logger, this.natsConnection, natsEvent, `${nats_1.natsFlowsPrefixFlowDeployment}.flowlifecycle.${deploymentId}`);
47
+ await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent, `${nats_1.natsFlowsPrefixFlowDeployment}.flowlifecycle.${deploymentId}`);
51
48
  }
52
49
  catch (err) {
53
50
  this.logger.error(err);
@@ -141,7 +138,7 @@ class FlowApplication {
141
138
  status: 'updated',
142
139
  },
143
140
  };
144
- await (0, nats_1.publishNatsEvent)(this.logger, this.natsConnection, natsEvent);
141
+ await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
145
142
  }
146
143
  catch (err) {
147
144
  this.logger.error(err);
@@ -154,7 +151,7 @@ class FlowApplication {
154
151
  status: 'updating failed',
155
152
  },
156
153
  };
157
- await (0, nats_1.publishNatsEvent)(this.logger, this.natsConnection, natsEvent);
154
+ await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
158
155
  }
159
156
  }
160
157
  else if (cloudEvent.subject.endsWith('.message')) {
@@ -174,8 +171,8 @@ class FlowApplication {
174
171
  }
175
172
  };
176
173
  this.publishNatsEventFlowlogs = async (event) => {
177
- if (!this.natsConnection || this.natsConnection.isClosed()) {
178
- return;
174
+ if (!this._natsConnection || this._natsConnection.isClosed()) {
175
+ return true;
179
176
  }
180
177
  try {
181
178
  const formatedEvent = event.format();
@@ -188,7 +185,7 @@ class FlowApplication {
188
185
  subject: `${this.context.deploymentId}`,
189
186
  data: formatedEvent,
190
187
  };
191
- await (0, nats_1.publishNatsEvent)(this.logger, this.natsConnection, natsEvent);
188
+ await (0, nats_1.publishNatsEvent)(this.logger, this._natsConnection, natsEvent);
192
189
  return true;
193
190
  }
194
191
  catch (err) {
@@ -315,6 +312,7 @@ class FlowApplication {
315
312
  name: `flow-deployment-${this.context.deploymentId}`,
316
313
  filter_subject: `${nats_1.natsFlowsPrefixFlowDeployment}.${this.context.deploymentId}.*`,
317
314
  inactive_threshold: 10 * 60 * 1_000_000_000,
315
+ deliver_policy: jetstream_1.DeliverPolicy.New,
318
316
  };
319
317
  const consumer = await (0, nats_1.getOrCreateConsumer)(this.logger, this._natsConnection, nats_1.FLOWS_STREAM_NAME, consumerOptions.name, consumerOptions);
320
318
  const handleNatsStatus = async () => {
@@ -453,21 +451,37 @@ class FlowApplication {
453
451
  if (this.amqpConnection) {
454
452
  await this.amqpConnection.close();
455
453
  }
456
- if (this._natsConnection && !this._natsConnection.isClosed()) {
457
- await (0, jetstream_1.jetstreamManager)(this._natsConnection).then((jsm) => {
458
- jsm.consumers
459
- .delete(nats_1.FLOWS_STREAM_NAME, `flow-deployment-${this.context.deploymentId}`)
460
- .then(() => {
461
- this.logger.error(`Deleted consumer for flow deployment ${this.context.deploymentId}`);
462
- })
463
- .catch((err) => {
464
- this.logger.error(`Could not delete consumer for flow deployment ${this.context.deploymentId}: ${err.message}`);
454
+ for (const [id, stream] of this.outputStreamMap.entries()) {
455
+ try {
456
+ stream?.complete();
457
+ }
458
+ catch (err) {
459
+ this.logger.error(`Error completing output stream ${id}: ${err.message}`);
460
+ }
461
+ }
462
+ try {
463
+ await this.natsMessageIterator?.close();
464
+ await this._natsConnection?.drain();
465
+ await this._natsConnection?.close();
466
+ if (this._natsConnection && !this._natsConnection.isClosed()) {
467
+ await (0, jetstream_1.jetstreamManager)(this._natsConnection).then((jsm) => {
468
+ jsm.consumers
469
+ .delete(nats_1.FLOWS_STREAM_NAME, `flow-deployment-${this.context?.deploymentId}`)
470
+ .then(() => {
471
+ this.logger.debug(`Deleted consumer for flow deployment ${this.context?.deploymentId}`);
472
+ })
473
+ .catch((err) => {
474
+ this.logger.error(`Could not delete consumer for flow deployment ${this.context?.deploymentId}: ${err.message}`);
475
+ });
465
476
  });
466
- });
467
- await this._natsConnection.drain();
477
+ }
478
+ }
479
+ catch (err) {
480
+ this.logger.error(err);
468
481
  }
469
- await this.natsMessageIterator?.close();
470
- await this._natsConnection?.close();
482
+ process.removeAllListeners('SIGTERM');
483
+ process.removeAllListeners('uncaughtException');
484
+ process.removeAllListeners('unhandledRejection');
471
485
  }
472
486
  catch (err) {
473
487
  console.error(err);
@@ -8,8 +8,13 @@ export interface Logger {
8
8
  verbose(message: any, metadata?: any): void;
9
9
  }
10
10
  export declare const defaultLogger: Logger;
11
+ export declare enum STACK_TRACE {
12
+ FULL = "full",
13
+ ONLY_LOG_CALL = "only-log-call"
14
+ }
11
15
  export interface LoggerOptions {
12
16
  truncate: boolean;
17
+ stackTrace?: STACK_TRACE;
13
18
  }
14
19
  export declare class FlowLogger implements Logger {
15
20
  private readonly metadata;
@@ -22,5 +27,18 @@ export declare class FlowLogger implements Logger {
22
27
  log: (message: any, options?: LoggerOptions) => void;
23
28
  warn: (message: any, options?: LoggerOptions) => void;
24
29
  verbose: (message: any, options?: LoggerOptions) => void;
30
+ /**
31
+ * Parses a message into a FlowLog object, including optional stack trace information.
32
+ *
33
+ * @details Requirements for the output format of messages:
34
+ * - Necessary for consistent logging and event publishing, because the OpenSearch index expects a specific structure: flat_object.
35
+ * - The current UI expects a `message` property to be present, so we ensure it is always set.
36
+ *
37
+ * @param {any} message - The message to be logged. Can be a string, an object with a `message` property, or any other type.
38
+ * @param {string} level - The log level (e.g., 'error', 'debug', 'warn', 'verbose').
39
+ * @param {LoggerOptions} options - Additional options for logging, such as whether to include a stack trace.
40
+ * @returns {FlowLog} - An object containing the parsed log message and optional stack trace.
41
+ */
42
+ private parseMessageToFlowLog;
25
43
  private publish;
26
44
  }
@@ -1,6 +1,6 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
- exports.FlowLogger = exports.defaultLogger = void 0;
3
+ exports.FlowLogger = exports.STACK_TRACE = exports.defaultLogger = void 0;
4
4
  const FlowEvent_1 = require("./FlowEvent");
5
5
  exports.defaultLogger = {
6
6
  debug: (msg, metadata) => console.debug(msg),
@@ -9,8 +9,13 @@ exports.defaultLogger = {
9
9
  warn: (msg, metadata) => console.warn(msg),
10
10
  verbose: (msg, metadata) => console.log(msg, metadata),
11
11
  };
12
+ var STACK_TRACE;
13
+ (function (STACK_TRACE) {
14
+ STACK_TRACE["FULL"] = "full";
15
+ STACK_TRACE["ONLY_LOG_CALL"] = "only-log-call";
16
+ })(STACK_TRACE || (exports.STACK_TRACE = STACK_TRACE = {}));
12
17
  class FlowLogger {
13
- static getStackTrace() {
18
+ static getStackTrace(stacktrace = STACK_TRACE.FULL) {
14
19
  let stack;
15
20
  try {
16
21
  throw new Error('');
@@ -21,8 +26,14 @@ class FlowLogger {
21
26
  stack = stack
22
27
  .split('\n')
23
28
  .map((line) => line.trim())
24
- .filter((value) => !value.includes('Logger'));
25
- return stack.splice(1).join('\n');
29
+ .filter((value) => value.includes('at ') && !value.includes('Logger'));
30
+ if (stacktrace === STACK_TRACE.ONLY_LOG_CALL && stack.length > 0) {
31
+ stack = stack[0];
32
+ }
33
+ else {
34
+ stack = stack.splice(1).join('\n');
35
+ }
36
+ return stack;
26
37
  }
27
38
  constructor(metadata, logger = exports.defaultLogger, publishEvent) {
28
39
  this.metadata = metadata;
@@ -34,28 +45,49 @@ class FlowLogger {
34
45
  this.warn = (message, options) => this.publish(message, 'warn', options);
35
46
  this.verbose = (message, options) => this.publish(message, 'verbose', options);
36
47
  }
48
+ parseMessageToFlowLog(message, level, options) {
49
+ let flowLogMessage;
50
+ if (!message) {
51
+ flowLogMessage = 'No message provided!';
52
+ }
53
+ else if (typeof message.message === 'string') {
54
+ flowLogMessage = message.message;
55
+ }
56
+ else if (typeof message === 'string') {
57
+ flowLogMessage = message;
58
+ }
59
+ else {
60
+ try {
61
+ flowLogMessage = JSON.stringify(message.message ?? message);
62
+ }
63
+ catch (e) {
64
+ flowLogMessage = 'Error: Could not stringify the message.';
65
+ }
66
+ }
67
+ const flowLog = { message: flowLogMessage };
68
+ if (['error', 'debug', 'warn', 'verbose'].includes(level) || options?.stackTrace) {
69
+ flowLog.stackTrace = FlowLogger.getStackTrace(options?.stackTrace ?? STACK_TRACE.ONLY_LOG_CALL);
70
+ }
71
+ return flowLog;
72
+ }
37
73
  publish(message, level, options) {
74
+ const flowLogData = this.parseMessageToFlowLog(message, level, options);
38
75
  if (this.publishEvent) {
39
- const data = message?.message
40
- ? message
41
- : {
42
- ...message,
43
- message: typeof message === 'string' ? message : JSON.stringify(message),
44
- };
45
- const event = new FlowEvent_1.FlowEvent(this.metadata, data, `flow.log.${level}`);
76
+ const event = new FlowEvent_1.FlowEvent(this.metadata, flowLogData, `flow.log.${level}`);
46
77
  this.publishEvent(event);
47
78
  }
79
+ const messageWithStackTrace = flowLogData.stackTrace ? `${flowLogData.message}\n${flowLogData.stackTrace}` : flowLogData.message;
48
80
  switch (level) {
49
81
  case 'debug':
50
- return this.logger.debug(message, { ...this.metadata, ...options });
82
+ return this.logger.debug(messageWithStackTrace, { ...this.metadata, ...options });
51
83
  case 'error':
52
- return this.logger.error(message, { ...this.metadata, ...options });
84
+ return this.logger.error(messageWithStackTrace, { ...this.metadata, ...options });
53
85
  case 'warn':
54
- return this.logger.warn(message, { ...this.metadata, ...options });
86
+ return this.logger.warn(messageWithStackTrace, { ...this.metadata, ...options });
55
87
  case 'verbose':
56
- return this.logger.verbose(message, { ...this.metadata, ...options });
88
+ return this.logger.verbose(messageWithStackTrace, { ...this.metadata, ...options });
57
89
  default:
58
- this.logger.log(message, { ...this.metadata, ...options });
90
+ this.logger.log(messageWithStackTrace, { ...this.metadata, ...options });
59
91
  }
60
92
  }
61
93
  }
package/dist/nats.js CHANGED
@@ -66,6 +66,7 @@ async function natsEventListener(nc, logger, reconnectHandler) {
66
66
  }
67
67
  async function publishNatsEvent(logger, nc, event, subject) {
68
68
  if (!nc || nc.isClosed()) {
69
+ logger.error('NATS connection is not available, cannot publish event');
69
70
  return;
70
71
  }
71
72
  const cloudEvent = new cloudevents_1.CloudEvent({ datacontenttype: 'application/json', ...event });
package/dist/utils.d.ts CHANGED
@@ -3,6 +3,45 @@ export declare function fillTemplate(value: any, ...templateVariables: any): any
3
3
  export declare function getCircularReplacer(): (key: any, value: any) => any;
4
4
  export declare function toArray(value?: string | string[]): string[];
5
5
  export declare function delay(ms: number): Promise<void>;
6
+ /**
7
+ * Creates a promise that resolves after a specified delay, with support for cancellation via an AbortSignal.
8
+ *
9
+ * @param {number} ms - The delay duration in milliseconds.
10
+ * @param {Object} [options] - Optional configuration.
11
+ * @param {AbortSignal} [options.signal] - An AbortSignal to allow cancellation of the delay.
12
+ *
13
+ * @returns {Promise<void>} A promise that resolves after the specified delay or rejects if aborted.
14
+ *
15
+ * @throws {Error} If the AbortSignal is already aborted or gets aborted during the delay, the promise rejects with an "AbortError".
16
+ *
17
+ * @details Usage:
18
+ * ```typescript
19
+ * @FlowFunction('test.task.LongRunningTask')
20
+ * class LongRunningTask extends FlowTask<Properties> {
21
+ * private readonly abortController = new AbortController();
22
+ *
23
+ * constructor(...) {...}
24
+ *
25
+ * @InputStream()
26
+ * public async loveMeLongTime(event) {
27
+ * try {
28
+ * await delayWithAbort(this.properties.delay, { signal: this.abortController.signal });
29
+ * return this.emitEvent({ foo: 'bar' }, null);
30
+ * } catch (err) {
31
+ * if (err.message === 'AbortError') {
32
+ * return; // Task was aborted
33
+ * }
34
+ * throw err;
35
+ * }
36
+ * }
37
+ *
38
+ * public onDestroy = () => { this.abortController.abort(); };
39
+ * }
40
+ * ```
41
+ */
42
+ export declare function delayWithAbort(ms: number, options?: {
43
+ signal?: AbortSignal;
44
+ }): Promise<void>;
6
45
  export declare function deleteFiles(dir: string, ...filenames: string[]): Promise<void>;
7
46
  export declare function handleApiError(error: any, logger: FlowLogger): void;
8
47
  export declare function runPyScript(scriptPath: string, data: any): Promise<any>;
package/dist/utils.js CHANGED
@@ -4,6 +4,7 @@ exports.fillTemplate = fillTemplate;
4
4
  exports.getCircularReplacer = getCircularReplacer;
5
5
  exports.toArray = toArray;
6
6
  exports.delay = delay;
7
+ exports.delayWithAbort = delayWithAbort;
7
8
  exports.deleteFiles = deleteFiles;
8
9
  exports.handleApiError = handleApiError;
9
10
  exports.runPyScript = runPyScript;
@@ -63,6 +64,19 @@ function toArray(value = []) {
63
64
  function delay(ms) {
64
65
  return new Promise((resolve) => setTimeout(resolve, ms));
65
66
  }
67
+ function delayWithAbort(ms, options) {
68
+ return new Promise((resolve, reject) => {
69
+ if (options?.signal?.aborted) {
70
+ reject(new Error('AbortError'));
71
+ return;
72
+ }
73
+ const timeout = setTimeout(() => resolve(), ms);
74
+ options?.signal?.addEventListener('abort', () => {
75
+ clearTimeout(timeout);
76
+ reject(new Error('AbortError'));
77
+ });
78
+ });
79
+ }
66
80
  async function deleteFiles(dir, ...filenames) {
67
81
  for (const filename of filenames) {
68
82
  await fs_1.promises.unlink((0, path_1.join)(dir, filename)).catch((err) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@hahnpro/flow-sdk",
3
- "version": "9.4.1",
3
+ "version": "9.5.0",
4
4
  "description": "SDK for building Flow Modules",
5
5
  "license": "MIT",
6
6
  "author": {