@fsai-flow/core 0.0.1 → 0.0.3

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 (86) hide show
  1. package/README.md +22 -2
  2. package/package.json +16 -6
  3. package/src/index.ts +1 -0
  4. package/src/lib/ActiveWorkflows.ts +326 -55
  5. package/src/lib/NodeExecuteFunctions.ts +0 -4
  6. package/src/lib/RedisLeaderElectionManager.ts +252 -0
  7. package/src/lib/WorkflowExecute.ts +26 -2
  8. package/tsconfig.base.json +28 -0
  9. package/tsconfig.json +1 -1
  10. package/dist/README.md +0 -11
  11. package/dist/package.json +0 -44
  12. package/dist/src/index.d.ts +0 -15
  13. package/dist/src/index.js +0 -29
  14. package/dist/src/index.js.map +0 -1
  15. package/dist/src/lib/ActiveWebhooks.d.ts +0 -59
  16. package/dist/src/lib/ActiveWebhooks.js +0 -184
  17. package/dist/src/lib/ActiveWebhooks.js.map +0 -1
  18. package/dist/src/lib/ActiveWorkflows.d.ts +0 -58
  19. package/dist/src/lib/ActiveWorkflows.js +0 -244
  20. package/dist/src/lib/ActiveWorkflows.js.map +0 -1
  21. package/dist/src/lib/BinaryDataManager/FileSystem.d.ts +0 -26
  22. package/dist/src/lib/BinaryDataManager/FileSystem.js +0 -179
  23. package/dist/src/lib/BinaryDataManager/FileSystem.js.map +0 -1
  24. package/dist/src/lib/BinaryDataManager/index.d.ts +0 -21
  25. package/dist/src/lib/BinaryDataManager/index.js +0 -146
  26. package/dist/src/lib/BinaryDataManager/index.js.map +0 -1
  27. package/dist/src/lib/ChangeCase.d.ts +0 -9
  28. package/dist/src/lib/ChangeCase.js +0 -43
  29. package/dist/src/lib/ChangeCase.js.map +0 -1
  30. package/dist/src/lib/Constants.d.ts +0 -14
  31. package/dist/src/lib/Constants.js +0 -19
  32. package/dist/src/lib/Constants.js.map +0 -1
  33. package/dist/src/lib/Credentials.d.ts +0 -27
  34. package/dist/src/lib/Credentials.js +0 -89
  35. package/dist/src/lib/Credentials.js.map +0 -1
  36. package/dist/src/lib/FileSystem.d.ts +0 -26
  37. package/dist/src/lib/FileSystem.js +0 -179
  38. package/dist/src/lib/FileSystem.js.map +0 -1
  39. package/dist/src/lib/InputConnectionDataLegacy.d.ts +0 -2
  40. package/dist/src/lib/InputConnectionDataLegacy.js +0 -79
  41. package/dist/src/lib/InputConnectionDataLegacy.js.map +0 -1
  42. package/dist/src/lib/Interfaces.d.ts +0 -148
  43. package/dist/src/lib/Interfaces.js +0 -3
  44. package/dist/src/lib/Interfaces.js.map +0 -1
  45. package/dist/src/lib/LoadNodeParameterOptions.d.ts +0 -39
  46. package/dist/src/lib/LoadNodeParameterOptions.js +0 -150
  47. package/dist/src/lib/LoadNodeParameterOptions.js.map +0 -1
  48. package/dist/src/lib/NodeExecuteFunctions.d.ts +0 -226
  49. package/dist/src/lib/NodeExecuteFunctions.js +0 -2483
  50. package/dist/src/lib/NodeExecuteFunctions.js.map +0 -1
  51. package/dist/src/lib/NodesLoader/constants.d.ts +0 -5
  52. package/dist/src/lib/NodesLoader/constants.js +0 -106
  53. package/dist/src/lib/NodesLoader/constants.js.map +0 -1
  54. package/dist/src/lib/NodesLoader/custom-directory-loader.d.ts +0 -9
  55. package/dist/src/lib/NodesLoader/custom-directory-loader.js +0 -36
  56. package/dist/src/lib/NodesLoader/custom-directory-loader.js.map +0 -1
  57. package/dist/src/lib/NodesLoader/directory-loader.d.ts +0 -66
  58. package/dist/src/lib/NodesLoader/directory-loader.js +0 -325
  59. package/dist/src/lib/NodesLoader/directory-loader.js.map +0 -1
  60. package/dist/src/lib/NodesLoader/index.d.ts +0 -5
  61. package/dist/src/lib/NodesLoader/index.js +0 -12
  62. package/dist/src/lib/NodesLoader/index.js.map +0 -1
  63. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.d.ts +0 -7
  64. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js +0 -52
  65. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js.map +0 -1
  66. package/dist/src/lib/NodesLoader/load-class-in-isolation.d.ts +0 -1
  67. package/dist/src/lib/NodesLoader/load-class-in-isolation.js +0 -22
  68. package/dist/src/lib/NodesLoader/load-class-in-isolation.js.map +0 -1
  69. package/dist/src/lib/NodesLoader/package-directory-loader.d.ts +0 -17
  70. package/dist/src/lib/NodesLoader/package-directory-loader.js +0 -100
  71. package/dist/src/lib/NodesLoader/package-directory-loader.js.map +0 -1
  72. package/dist/src/lib/NodesLoader/types.d.ts +0 -14
  73. package/dist/src/lib/NodesLoader/types.js +0 -3
  74. package/dist/src/lib/NodesLoader/types.js.map +0 -1
  75. package/dist/src/lib/UserSettings.d.ts +0 -80
  76. package/dist/src/lib/UserSettings.js +0 -261
  77. package/dist/src/lib/UserSettings.js.map +0 -1
  78. package/dist/src/lib/WorkflowExecute.d.ts +0 -53
  79. package/dist/src/lib/WorkflowExecute.js +0 -835
  80. package/dist/src/lib/WorkflowExecute.js.map +0 -1
  81. package/dist/src/lib/index.d.ts +0 -21
  82. package/dist/src/lib/index.js +0 -146
  83. package/dist/src/lib/index.js.map +0 -1
  84. package/dist/src/utils/crypto.d.ts +0 -1
  85. package/dist/src/utils/crypto.js +0 -8
  86. package/dist/src/utils/crypto.js.map +0 -1
package/README.md CHANGED
@@ -1,6 +1,14 @@
1
- # core
1
+ # @fsai-flow/core
2
2
 
3
- This library was generated with [Nx](https://nx.dev).
3
+ This library is a dependency for [FSAI-Flow](https://github.com/your-org/flowx) that contains base classes and types used throughout the application.
4
+
5
+ ## Installation
6
+
7
+ This project is published to the npm registry. To use it in your project:
8
+
9
+ ```bash
10
+ npm install @fsai-flow/core
11
+ ```
4
12
 
5
13
  ## Building
6
14
 
@@ -9,3 +17,15 @@ Run `nx build core` to build the library.
9
17
  ## Running unit tests
10
18
 
11
19
  Run `nx test core` to execute the unit tests via [Jest](https://jestjs.io).
20
+
21
+ ## Contributing
22
+
23
+ To contribute to this project:
24
+
25
+ 1. Clone this repository
26
+ 2. Make your changes
27
+ 3. Open a Pull Request
28
+
29
+ ## Publishing
30
+
31
+ Maintainers can publish a new version to the npm package registry.
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@fsai-flow/core",
3
- "version": "0.0.1",
4
- "license": "PolyForm Noncommercial License 1.0.0",
3
+ "version": "0.0.3",
5
4
  "dependencies": {
6
- "@fsai-flow/workflow": "workspace:^",
5
+ "@fsai-flow/workflow": "0.0.2",
7
6
  "client-oauth2": "^4.3.3",
8
7
  "cron": "~3.3.0",
9
8
  "crypto-js": "~4.2.0",
10
9
  "file-type": "^16.0.0",
11
- "form-data": "^4.0.0",
10
+ "form-data": "4.0.4",
12
11
  "https-proxy-agent": "^7.0.6",
12
+ "ioredis": "^5.3.2",
13
13
  "lodash": "^4.17.21",
14
14
  "mime-types": "^2.1.27",
15
15
  "oauth-1.0a": "^2.2.6",
@@ -20,7 +20,7 @@
20
20
  "simple-oauth2": "^5.1.0",
21
21
  "tslib": "^2.3.0",
22
22
  "uuid": "^11.0.3",
23
- "fast-glob": "catalog:"
23
+ "fast-glob": "3.2.12"
24
24
  },
25
25
  "devDependencies": {
26
26
  "@types/crypto-js": "^4.0.1",
@@ -35,10 +35,20 @@
35
35
  "@types/simple-oauth2": "^5.0.7",
36
36
  "@types/uuid": "^10.0.0",
37
37
  "axios": "^1.7.9",
38
+ "ioredis": "^5.3.2",
38
39
  "jsonc-eslint-parser": "^2.4.0",
39
40
  "typescript": "~5.7.2"
40
41
  },
41
42
  "type": "commonjs",
42
43
  "main": "dist/src/index",
43
- "types": "dist/src/index.d.ts"
44
+ "types": "dist/src/index.d.ts",
45
+ "overrides": {
46
+ "request": {
47
+ "form-data": "2.5.4",
48
+ "tough-cookie": "4.1.3"
49
+ },
50
+ "request-promise-native": {
51
+ "tough-cookie": "4.1.3"
52
+ }
53
+ }
44
54
  }
package/src/index.ts CHANGED
@@ -15,6 +15,7 @@ export * from './lib/BinaryDataManager';
15
15
  export * from './lib/Constants';
16
16
  export * from './lib/Credentials';
17
17
  export * from './lib/Interfaces';
18
+ export * from './lib/RedisLeaderElectionManager';
18
19
  export * from './lib/LoadNodeParameterOptions';
19
20
  export * from './lib/NodeExecuteFunctions';
20
21
  export * from './lib/WorkflowExecute';
@@ -18,14 +18,49 @@ import {
18
18
 
19
19
  import { secureRandomNumber } from '../utils/crypto';
20
20
 
21
+ import { RedisLeaderElectionManager } from './RedisLeaderElectionManager';
22
+
23
+ import type Redis from 'ioredis';
24
+
21
25
  // eslint-disable-next-line import/no-cycle
22
26
  import { ITriggerTime, IWorkflowData } from '../../src';
27
+ import { RedisOptions } from 'ioredis';
28
+
29
+ interface IPollingWorkflow {
30
+ workflow: Workflow;
31
+ node: INode;
32
+ cronTimes: string[];
33
+ timezone: string;
34
+ executeTrigger: () => Promise<void>;
35
+ activeCronJobs: CronJob[];
36
+ }
23
37
 
24
38
  export class ActiveWorkflows {
25
39
  private workflowData: {
26
40
  [key: string]: IWorkflowData;
27
41
  } = {};
28
42
 
43
+ private redisConfig: string | RedisOptions | undefined;
44
+ private instanceLeaderElection: RedisLeaderElectionManager | null = null;
45
+ private pollingWorkflows: Map<string, IPollingWorkflow> = new Map();
46
+ private isLeader = false;
47
+ private isInitializingLeaderElection = false;
48
+ private leaderElectionInitialized = false;
49
+ private leaderElectionEnabled = false;
50
+
51
+ constructor(redisConfig?: string | RedisOptions) {
52
+ this.redisConfig = redisConfig;
53
+ this.leaderElectionEnabled = redisConfig !== undefined;
54
+
55
+ if (!this.leaderElectionEnabled) {
56
+ Logger.info('📴 Redis leader election disabled - workflows will run on all instances');
57
+ this.isLeader = true; // In non-queue mode, every instance is a "leader"
58
+ this.leaderElectionInitialized = true;
59
+ }
60
+ }
61
+
62
+
63
+
29
64
  /**
30
65
  * Returns if the workflow is active
31
66
  *
@@ -118,7 +153,138 @@ export class ActiveWorkflows {
118
153
  }
119
154
 
120
155
  /**
121
- * Activates polling for the given node
156
+ * Initialize global leader election (called once per instance)
157
+ */
158
+ private async initializeLeaderElection(): Promise<void> {
159
+ // Skip leader election if disabled
160
+ if (!this.leaderElectionEnabled) {
161
+ Logger.debug('📴 Leader election disabled - skipping initialization');
162
+ return;
163
+ }
164
+
165
+ // Prevent race condition - only one initialization at a time
166
+ if (this.leaderElectionInitialized || this.isInitializingLeaderElection) {
167
+ Logger.debug('⏳ Leader election already initialized or in progress, waiting...');
168
+
169
+ // Wait for initialization to complete
170
+ while (this.isInitializingLeaderElection) {
171
+ await new Promise(resolve => setTimeout(resolve, 100));
172
+ }
173
+ return;
174
+ }
175
+
176
+ this.isInitializingLeaderElection = true;
177
+
178
+ try {
179
+ const nodeId = process.env['POD_NAME'] || process.env['HOSTNAME'] || `flowx-instance-${Math.random().toString(36).substr(2, 9)}`;
180
+ const lockName = 'global-polling-leadership';
181
+
182
+ Logger.info(`🌟 Initializing global polling leadership for instance ${nodeId}`);
183
+
184
+ this.instanceLeaderElection = new RedisLeaderElectionManager(
185
+ lockName,
186
+ this.redisConfig!, // We know it's not null because leaderElectionEnabled is true
187
+ {
188
+ onStartedLeading: () => {
189
+ Logger.info(`🏆 Instance ${nodeId} became GLOBAL POLLING LEADER - starting all registered workflows (current count: ${this.pollingWorkflows.size})`);
190
+ this.isLeader = true;
191
+ // Set initialized flag before starting workflows to prevent timing issue
192
+ this.leaderElectionInitialized = true;
193
+ this.startAllPollingWorkflows();
194
+ },
195
+ onStoppedLeading: () => {
196
+ Logger.info(`📉 Instance ${nodeId} lost GLOBAL POLLING LEADERSHIP - stopping all workflows`);
197
+ this.isLeader = false;
198
+ this.stopAllPollingWorkflows();
199
+ },
200
+ onNewLeader: (identity: string) => {
201
+ //Logger.info(`👑 New global polling leader: ${identity}`);
202
+ }
203
+ }
204
+ );
205
+
206
+ await this.instanceLeaderElection.start();
207
+ // Only set this if we didn't become leader during start() - prevent double-setting
208
+ if (!this.leaderElectionInitialized) {
209
+ this.leaderElectionInitialized = true;
210
+ }
211
+
212
+ Logger.info(`✅ Global leader election initialized for ${nodeId}`);
213
+ } finally {
214
+ this.isInitializingLeaderElection = false;
215
+ }
216
+ }
217
+
218
+ /**
219
+ * Start all registered polling workflows (called when becoming leader)
220
+ */
221
+ private startAllPollingWorkflows(): void {
222
+ Logger.debug(`🔍 startAllPollingWorkflows called - isLeader: ${this.isLeader}, initialized: ${this.leaderElectionInitialized}, workflows: ${this.pollingWorkflows.size}`);
223
+
224
+ // Safety check: Only start if we're truly the leader and initialization is complete
225
+ if (!this.isLeader || !this.leaderElectionInitialized) {
226
+ Logger.warn(`⚠️ Cannot start workflows - not leader (isLeader: ${this.isLeader}, initialized: ${this.leaderElectionInitialized})`);
227
+ return;
228
+ }
229
+
230
+ Logger.info(`🚀 Starting all ${this.pollingWorkflows.size} registered polling workflows`);
231
+ Logger.debug(`📋 Registered workflow keys: ${Array.from(this.pollingWorkflows.keys()).join(', ')}`);
232
+
233
+ let startedCount = 0;
234
+ for (const [workflowKey, pollingWorkflow] of this.pollingWorkflows) {
235
+ try {
236
+ // Skip if already running (double-check safety)
237
+ if (pollingWorkflow.activeCronJobs.length > 0) {
238
+ Logger.warn(`⚠️ Workflow ${workflowKey} already has active cron jobs - skipping`);
239
+ continue;
240
+ }
241
+
242
+ Logger.info(`⏰ Starting cron jobs for workflow "${pollingWorkflow.workflow.name}", node "${pollingWorkflow.node.name}" (${pollingWorkflow.cronTimes.length} schedules)`);
243
+
244
+ // Create and start cron jobs for this workflow
245
+ pollingWorkflow.activeCronJobs = pollingWorkflow.cronTimes.map(cronTime =>
246
+ new CronJob(cronTime, pollingWorkflow.executeTrigger, undefined, true, pollingWorkflow.timezone)
247
+ );
248
+
249
+ startedCount++;
250
+ Logger.info(`✅ Started ${pollingWorkflow.activeCronJobs.length} cron jobs for workflow "${pollingWorkflow.workflow.name}"`);
251
+ } catch (error: any) {
252
+ Logger.error(`❌ Failed to start polling for ${workflowKey}: ${error.message}`);
253
+ }
254
+ }
255
+
256
+ Logger.info(`🎯 Successfully started ${startedCount} of ${this.pollingWorkflows.size} polling workflows`);
257
+ }
258
+
259
+ /**
260
+ * Stop all polling workflows (called when losing leadership)
261
+ */
262
+ private stopAllPollingWorkflows(): void {
263
+ Logger.info(`🛑 Stopping all ${this.pollingWorkflows.size} polling workflows`);
264
+
265
+ for (const [workflowKey, pollingWorkflow] of this.pollingWorkflows) {
266
+ try {
267
+ // Skip if already stopped (safety check)
268
+ if (pollingWorkflow.activeCronJobs.length === 0) {
269
+ Logger.debug(`⏭️ Workflow ${workflowKey} already stopped - skipping`);
270
+ continue;
271
+ }
272
+
273
+ Logger.info(`⏹️ Stopping cron jobs for workflow "${pollingWorkflow.workflow.name}", node "${pollingWorkflow.node.name}"`);
274
+
275
+ // Stop all cron jobs for this workflow
276
+ pollingWorkflow.activeCronJobs.forEach(job => job.stop());
277
+ pollingWorkflow.activeCronJobs = [];
278
+
279
+ Logger.debug(`✅ Stopped polling for ${workflowKey}`);
280
+ } catch (error: any) {
281
+ Logger.error(`❌ Failed to stop polling for ${workflowKey}: ${error.message}`);
282
+ }
283
+ }
284
+ }
285
+
286
+ /**
287
+ * Activates polling for the given node with global Redis leader election
122
288
  *
123
289
  * @param {INode} node
124
290
  * @param {Workflow} workflow
@@ -137,27 +303,128 @@ export class ActiveWorkflows {
137
303
  ): Promise<IPollResponse> {
138
304
  const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
139
305
 
306
+ // Get polling configuration
140
307
  const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
141
308
  item: ITriggerTime[];
142
309
  };
143
310
 
144
- // Define the order the cron-time-parameter appear
311
+ // Build cron times
312
+ const cronTimes = this.buildCronTimes(pollTimes);
313
+ const timezone = pollFunctions.getTimezone();
314
+
315
+ // Validate cron times
316
+ this.validateCronTimes(cronTimes);
317
+
318
+ // Create unique workflow key
319
+ const workflowKey = `${workflow.id}-${node.name}`;
320
+
321
+ // Check if this workflow is already registered (prevents duplicate registrations)
322
+ if (this.pollingWorkflows.has(workflowKey)) {
323
+ Logger.warn(`⚠️ Workflow ${workflowKey} is already registered - returning existing closeFunction`);
324
+ const existingWorkflow = this.pollingWorkflows.get(workflowKey)!;
325
+ return {
326
+ closeFunction: async () => {
327
+ Logger.info(`🗑️ Removing duplicate registration for: "${workflow.name}" (node: ${node.name})`);
328
+ // Just log - don't actually remove since this is a duplicate
329
+ }
330
+ };
331
+ }
332
+
333
+ // Create the polling execution function
334
+ const executeTrigger = async () => {
335
+ try {
336
+ Logger.info(`Polling trigger initiated for workflow "${workflow.name}"`, {
337
+ workflowName: workflow.name,
338
+ workflowId: workflow.id,
339
+ });
340
+ const pollResponse = await workflow.runPoll(node, pollFunctions);
341
+
342
+ if (pollResponse !== null) {
343
+ // eslint-disable-next-line no-underscore-dangle
344
+ pollFunctions.__emit(pollResponse);
345
+ }
346
+ } catch (error: any) {
347
+ Logger.error(
348
+ `Polling trigger failed for workflow "${workflow.name}"`,
349
+ {
350
+ workflowName: workflow.name,
351
+ workflowId: workflow.id,
352
+ nodeName: node.name,
353
+ error: error.message,
354
+ statusCode: error.statusCode,
355
+ httpCode: error.httpCode,
356
+ description: error.description,
357
+ cause: error.cause?.message,
358
+ }
359
+ );
360
+ }
361
+ };
362
+
363
+ // Register this polling workflow
364
+ const pollingWorkflow: IPollingWorkflow = {
365
+ workflow,
366
+ node,
367
+ cronTimes,
368
+ timezone,
369
+ executeTrigger,
370
+ activeCronJobs: []
371
+ };
372
+
373
+ this.pollingWorkflows.set(workflowKey, pollingWorkflow);
374
+ Logger.info(`📋 Registered polling workflow: "${workflow.name}" (node: ${node.name}) [Key: ${workflowKey}] - Total workflows: ${this.pollingWorkflows.size}`);
375
+
376
+ // Initialize global leader election if enabled
377
+ await this.initializeLeaderElection();
378
+
379
+ // Start workflow if we're the leader (or if leader election is disabled)
380
+ if (this.isLeader && this.leaderElectionInitialized && pollingWorkflow.activeCronJobs.length === 0) {
381
+ const reason = this.leaderElectionEnabled ? 'Leader established' : 'Leader election disabled';
382
+ Logger.info(`🔥 Post-registration start: "${workflow.name}" [Key: ${workflowKey}] - ${reason}, starting immediately`);
383
+ try {
384
+ pollingWorkflow.activeCronJobs = pollingWorkflow.cronTimes.map(cronTime =>
385
+ new CronJob(cronTime, pollingWorkflow.executeTrigger, undefined, true, pollingWorkflow.timezone)
386
+ );
387
+ Logger.info(`✅ Post-registration: Started ${pollingWorkflow.activeCronJobs.length} cron jobs for "${workflow.name}"`);
388
+ } catch (error: any) {
389
+ Logger.error(`❌ Failed to start newly registered workflow ${workflowKey}: ${error.message}`);
390
+ }
391
+ } else {
392
+ const leaderElectionStatus = this.leaderElectionEnabled ? 'enabled' : 'disabled';
393
+ Logger.debug(`⏳ Post-registration wait: "${workflow.name}" [Key: ${workflowKey}] (isLeader: ${this.isLeader}, initialized: ${this.leaderElectionInitialized}, activeCronJobs: ${pollingWorkflow.activeCronJobs.length}, leaderElection: ${leaderElectionStatus})`);
394
+ }
395
+
396
+ // Return cleanup function
397
+ return {
398
+ closeFunction: async () => {
399
+ Logger.info(`🗑️ Removing polling workflow: "${workflow.name}" (node: ${node.name})`);
400
+
401
+ // Stop cron jobs for this workflow
402
+ const workflow_to_remove = this.pollingWorkflows.get(workflowKey);
403
+ if (workflow_to_remove) {
404
+ workflow_to_remove.activeCronJobs.forEach(job => job.stop());
405
+ }
406
+
407
+ // Remove from registered workflows
408
+ this.pollingWorkflows.delete(workflowKey);
409
+
410
+ Logger.debug(`✅ Removed polling workflow ${workflowKey}`);
411
+ }
412
+ };
413
+ }
414
+
415
+ /**
416
+ * Extract cron time building logic (same as original)
417
+ */
418
+ private buildCronTimes(pollTimes: { item: ITriggerTime[] }): string[] {
145
419
  const parameterOrder = [
146
- 'second', // 0 - 59
147
- 'minute', // 0 - 59
148
- 'hour', // 0 - 23
149
- 'dayOfMonth', // 1 - 31
150
- 'month', // 0 - 11(Jan - Dec)
151
- 'weekday', // 0 - 6(Sun - Sat)
420
+ 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'weekday'
152
421
  ];
153
422
 
154
- // Get all the trigger times
155
423
  const cronTimes: string[] = [];
156
- let cronTime: string[];
157
- let parameterName: string;
158
424
  if (pollTimes.item !== undefined) {
159
425
  for (const item of pollTimes.item) {
160
- cronTime = [];
426
+ let cronTime: string[];
427
+
161
428
  if (item.mode === 'custom') {
162
429
  cronTimes.push((item['cronExpression'] as string).trim());
163
430
  continue;
@@ -177,51 +444,29 @@ export class ActiveWorkflows {
177
444
  continue;
178
445
  }
179
446
 
180
- for (parameterName of parameterOrder) {
447
+ cronTime = [];
448
+ for (const parameterName of parameterOrder) {
181
449
  if (item[parameterName] !== undefined) {
182
- // Value is set so use it
183
450
  cronTime.push(item[parameterName] as string);
184
451
  } else if (parameterName === 'second') {
185
- // For seconds we use by default a random one to make sure to
186
- // balance the load a little bit over time
187
452
  cronTime.push(secureRandomNumber(1, 60).toString());
188
453
  } else {
189
- // For all others set "any"
190
454
  cronTime.push('*');
191
455
  }
192
456
  }
193
-
194
457
  cronTimes.push(cronTime.join(' '));
195
458
  }
196
459
  }
460
+ return cronTimes;
461
+ }
197
462
 
198
- // The trigger function to execute when the cron-time got reached
199
- const executeTrigger = async () => {
200
- // eslint-disable-next-line @typescript-eslint/restrict-template-expressions
201
- Logger.debug(`Polling trigger initiated for workflow "${workflow.name}"`, {
202
- workflowName: workflow.name,
203
- workflowId: workflow.id,
204
- });
205
- const pollResponse = await workflow.runPoll(node, pollFunctions);
206
-
207
- if (pollResponse !== null) {
208
- // eslint-disable-next-line no-underscore-dangle
209
- pollFunctions.__emit(pollResponse);
210
- }
211
- };
212
-
213
- // Execute the trigger directly to be able to know if it works
214
- await executeTrigger();
215
-
216
- const timezone = pollFunctions.getTimezone();
217
-
218
- // Start the cron-jobs
219
- const cronJobs: CronJob[] = [];
220
- // eslint-disable-next-line @typescript-eslint/no-shadow
463
+ /**
464
+ * Validate cron times (same as original)
465
+ */
466
+ private validateCronTimes(cronTimes: string[]): void {
221
467
  for (const cronTime of cronTimes) {
222
468
  const cronTimeParts = cronTime.split(' ');
223
469
 
224
- // Verificar se é polling em segundos
225
470
  if (cronTimeParts.length === 6 && cronTimeParts[0].includes('/')) {
226
471
  const secondsInterval = parseInt(cronTimeParts[0].replace('*/', ''), 10);
227
472
  if (secondsInterval < 1) {
@@ -230,20 +475,7 @@ export class ActiveWorkflows {
230
475
  } else if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*') && !cronTimeParts[0].includes('/')) {
231
476
  throw new Error('The polling interval is too short. It has to be at least a minute!');
232
477
  }
233
-
234
- cronJobs.push(new CronJob(cronTime, executeTrigger, undefined, true, timezone));
235
- }
236
-
237
- // Stop the cron-jobs
238
- async function closeFunction() {
239
- for (const cronJob of cronJobs) {
240
- cronJob.stop();
241
- }
242
478
  }
243
-
244
- return {
245
- closeFunction,
246
- };
247
479
  }
248
480
 
249
481
  /**
@@ -299,6 +531,45 @@ export class ActiveWorkflows {
299
531
  }
300
532
  }
301
533
 
534
+ // Remove polling workflows from the global map (closeFunction will handle stopping cron jobs)
535
+ const pollingWorkflowsToRemove: string[] = [];
536
+ for (const [workflowKey, pollingWorkflow] of this.pollingWorkflows) {
537
+ if (pollingWorkflow.workflow.id === id) {
538
+ pollingWorkflowsToRemove.push(workflowKey);
539
+ }
540
+ }
541
+
542
+ for (const workflowKey of pollingWorkflowsToRemove) {
543
+ const pollingWorkflow = this.pollingWorkflows.get(workflowKey);
544
+ if (pollingWorkflow) {
545
+ Logger.info(`🗑️ Removing polling workflow: "${pollingWorkflow.workflow.name}" (node: ${pollingWorkflow.node.name})`);
546
+
547
+ // Stop cron jobs for this workflow
548
+ pollingWorkflow.activeCronJobs.forEach(job => job.stop());
549
+
550
+ // Remove from registered workflows
551
+ this.pollingWorkflows.delete(workflowKey);
552
+
553
+ Logger.debug(`✅ Removed polling workflow ${workflowKey}`);
554
+ }
555
+ }
556
+
557
+ // If no more polling workflows are registered and leader election is enabled, stop it
558
+ if (this.pollingWorkflows.size === 0 && this.instanceLeaderElection && this.leaderElectionEnabled) {
559
+ Logger.info(`📴 No more polling workflows - stopping global leader election`);
560
+ try {
561
+ await this.instanceLeaderElection.stop();
562
+ this.instanceLeaderElection = null;
563
+ this.isLeader = false;
564
+ this.leaderElectionInitialized = false;
565
+ } catch (error: any) {
566
+ Logger.error(
567
+ // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
568
+ `There was a problem stopping global leader election: "${error.message}"`
569
+ );
570
+ }
571
+ }
572
+
302
573
  delete this.workflowData[id];
303
574
  }
304
575
  }
@@ -899,7 +899,6 @@ export async function getCurrentOAuth2AccessToken(
899
899
  client: { id: clientId, secret: clientSecret },
900
900
  auth: { tokenHost: authUrl, tokenPath: accessTokenUrl },
901
901
  http: { agent: httpAgent },
902
- options: { authorizationMethod: 'body' },
903
902
  };
904
903
 
905
904
  if (grantType === 'authorizationCode') {
@@ -999,9 +998,6 @@ export async function requestOAuth2(
999
998
  http: {
1000
999
  agent: credentials['httpAgent'],
1001
1000
  },
1002
- options: {
1003
- authorizationMethod: 'body',
1004
- },
1005
1001
  };
1006
1002
 
1007
1003
  const client = new AuthorizationCode(config);