@fsai-flow/core 0.0.4 → 0.1.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.
Files changed (136) hide show
  1. package/dist/index.d.ts +17 -0
  2. package/dist/index.js +61 -0
  3. package/dist/{src/lib → lib}/ActiveWebhooks.d.ts +1 -1
  4. package/dist/lib/ActiveWebhooks.js +177 -0
  5. package/dist/{src/lib → lib}/ActiveWorkflows.d.ts +3 -3
  6. package/dist/lib/ActiveWorkflows.js +465 -0
  7. package/dist/{src/lib → lib}/BinaryDataManager/FileSystem.d.ts +1 -1
  8. package/dist/lib/BinaryDataManager/FileSystem.js +180 -0
  9. package/dist/{src/lib → lib}/BinaryDataManager/index.d.ts +2 -2
  10. package/dist/lib/BinaryDataManager/index.js +129 -0
  11. package/dist/{src/lib → lib}/ChangeCase.js +11 -11
  12. package/dist/lib/Constants.js +18 -0
  13. package/dist/{src/lib → lib}/Credentials.d.ts +1 -1
  14. package/dist/{src/lib → lib}/Credentials.js +7 -8
  15. package/dist/{src/lib → lib}/FileSystem.d.ts +1 -1
  16. package/dist/lib/FileSystem.js +180 -0
  17. package/dist/{src/lib → lib}/InputConnectionDataLegacy.d.ts +1 -1
  18. package/dist/lib/InputConnectionDataLegacy.js +72 -0
  19. package/dist/{src/lib → lib}/Interfaces.d.ts +47 -48
  20. package/dist/{src/lib → lib}/Interfaces.js +0 -1
  21. package/dist/{src/lib → lib}/LoadNodeParameterOptions.d.ts +1 -1
  22. package/dist/lib/LoadNodeParameterOptions.js +152 -0
  23. package/dist/{src/lib → lib}/NodeExecuteFunctions.d.ts +9 -10
  24. package/dist/lib/NodeExecuteFunctions.js +2467 -0
  25. package/dist/{src/lib → lib}/NodesLoader/constants.d.ts +1 -1
  26. package/dist/lib/NodesLoader/constants.js +105 -0
  27. package/dist/{src/lib → lib}/NodesLoader/custom-directory-loader.d.ts +1 -1
  28. package/dist/lib/NodesLoader/custom-directory-loader.js +35 -0
  29. package/dist/{src/lib → lib}/NodesLoader/directory-loader.d.ts +1 -1
  30. package/dist/{src/lib → lib}/NodesLoader/directory-loader.js +80 -38
  31. package/dist/lib/NodesLoader/index.d.ts +5 -0
  32. package/dist/{src/lib → lib}/NodesLoader/index.js +5 -6
  33. package/dist/{src/lib → lib}/NodesLoader/lazy-package-directory-loader.d.ts +1 -1
  34. package/dist/lib/NodesLoader/lazy-package-directory-loader.js +44 -0
  35. package/dist/{src/lib → lib}/NodesLoader/load-class-in-isolation.js +6 -11
  36. package/dist/{src/lib → lib}/NodesLoader/package-directory-loader.d.ts +2 -2
  37. package/dist/{src/lib → lib}/NodesLoader/package-directory-loader.js +28 -36
  38. package/dist/{src/lib → lib}/NodesLoader/types.js +0 -1
  39. package/dist/{src/lib → lib}/RedisLeaderElectionManager.d.ts +1 -1
  40. package/dist/lib/RedisLeaderElectionManager.js +279 -0
  41. package/dist/lib/RequestTypes.d.ts +58 -0
  42. package/dist/lib/RequestTypes.js +8 -0
  43. package/dist/{src/lib → lib}/UserSettings.d.ts +1 -1
  44. package/dist/lib/UserSettings.js +269 -0
  45. package/dist/{src/lib → lib}/WorkflowExecute.d.ts +4 -4
  46. package/dist/{src/lib → lib}/WorkflowExecute.js +230 -178
  47. package/dist/{src/lib → lib}/index.d.ts +2 -2
  48. package/dist/lib/index.js +129 -0
  49. package/dist/{src/utils → utils}/crypto.js +2 -3
  50. package/package.json +59 -52
  51. package/dist/README.md +0 -31
  52. package/dist/package.json +0 -54
  53. package/dist/src/index.d.ts +0 -16
  54. package/dist/src/index.js +0 -30
  55. package/dist/src/index.js.map +0 -1
  56. package/dist/src/lib/ActiveWebhooks.js +0 -184
  57. package/dist/src/lib/ActiveWebhooks.js.map +0 -1
  58. package/dist/src/lib/ActiveWorkflows.js +0 -456
  59. package/dist/src/lib/ActiveWorkflows.js.map +0 -1
  60. package/dist/src/lib/BinaryDataManager/FileSystem.js +0 -179
  61. package/dist/src/lib/BinaryDataManager/FileSystem.js.map +0 -1
  62. package/dist/src/lib/BinaryDataManager/index.js +0 -146
  63. package/dist/src/lib/BinaryDataManager/index.js.map +0 -1
  64. package/dist/src/lib/ChangeCase.js.map +0 -1
  65. package/dist/src/lib/Constants.js +0 -19
  66. package/dist/src/lib/Constants.js.map +0 -1
  67. package/dist/src/lib/Credentials.js.map +0 -1
  68. package/dist/src/lib/FileSystem.js +0 -179
  69. package/dist/src/lib/FileSystem.js.map +0 -1
  70. package/dist/src/lib/InputConnectionDataLegacy.js +0 -79
  71. package/dist/src/lib/InputConnectionDataLegacy.js.map +0 -1
  72. package/dist/src/lib/Interfaces.js.map +0 -1
  73. package/dist/src/lib/LoadNodeParameterOptions.js +0 -150
  74. package/dist/src/lib/LoadNodeParameterOptions.js.map +0 -1
  75. package/dist/src/lib/NodeExecuteFunctions.js +0 -2479
  76. package/dist/src/lib/NodeExecuteFunctions.js.map +0 -1
  77. package/dist/src/lib/NodesLoader/constants.js +0 -106
  78. package/dist/src/lib/NodesLoader/constants.js.map +0 -1
  79. package/dist/src/lib/NodesLoader/custom-directory-loader.js +0 -36
  80. package/dist/src/lib/NodesLoader/custom-directory-loader.js.map +0 -1
  81. package/dist/src/lib/NodesLoader/directory-loader.js.map +0 -1
  82. package/dist/src/lib/NodesLoader/index.d.ts +0 -5
  83. package/dist/src/lib/NodesLoader/index.js.map +0 -1
  84. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js +0 -52
  85. package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js.map +0 -1
  86. package/dist/src/lib/NodesLoader/load-class-in-isolation.js.map +0 -1
  87. package/dist/src/lib/NodesLoader/package-directory-loader.js.map +0 -1
  88. package/dist/src/lib/NodesLoader/types.js.map +0 -1
  89. package/dist/src/lib/RedisLeaderElectionManager.js +0 -294
  90. package/dist/src/lib/RedisLeaderElectionManager.js.map +0 -1
  91. package/dist/src/lib/UserSettings.js +0 -261
  92. package/dist/src/lib/UserSettings.js.map +0 -1
  93. package/dist/src/lib/WorkflowExecute.js.map +0 -1
  94. package/dist/src/lib/index.js +0 -146
  95. package/dist/src/lib/index.js.map +0 -1
  96. package/dist/src/utils/crypto.js.map +0 -1
  97. package/eslint.config.js +0 -19
  98. package/jest.config.ts +0 -10
  99. package/project.json +0 -19
  100. package/src/index.ts +0 -28
  101. package/src/lib/ActiveWebhooks.ts +0 -245
  102. package/src/lib/ActiveWorkflows.ts +0 -575
  103. package/src/lib/BinaryDataManager/FileSystem.ts +0 -214
  104. package/src/lib/BinaryDataManager/index.ts +0 -187
  105. package/src/lib/ChangeCase.ts +0 -45
  106. package/src/lib/Constants.ts +0 -16
  107. package/src/lib/Credentials.ts +0 -108
  108. package/src/lib/FileSystem.ts +0 -214
  109. package/src/lib/InputConnectionDataLegacy.ts +0 -123
  110. package/src/lib/Interfaces.ts +0 -338
  111. package/src/lib/LoadNodeParameterOptions.ts +0 -235
  112. package/src/lib/NodeExecuteFunctions.ts +0 -3700
  113. package/src/lib/NodesLoader/constants.ts +0 -112
  114. package/src/lib/NodesLoader/custom-directory-loader.ts +0 -31
  115. package/src/lib/NodesLoader/directory-loader.ts +0 -458
  116. package/src/lib/NodesLoader/index.ts +0 -5
  117. package/src/lib/NodesLoader/lazy-package-directory-loader.ts +0 -55
  118. package/src/lib/NodesLoader/load-class-in-isolation.ts +0 -19
  119. package/src/lib/NodesLoader/package-directory-loader.ts +0 -107
  120. package/src/lib/NodesLoader/types.ts +0 -14
  121. package/src/lib/RedisLeaderElectionManager.ts +0 -334
  122. package/src/lib/UserSettings.ts +0 -292
  123. package/src/lib/WorkflowExecute.ts +0 -1128
  124. package/src/lib/index.ts +0 -187
  125. package/src/utils/crypto.ts +0 -5
  126. package/tests/Credentials.test.ts +0 -88
  127. package/tests/Helpers.ts +0 -808
  128. package/tests/WorkflowExecute.test.ts +0 -1242
  129. package/tsconfig.json +0 -41
  130. package/tsconfig.lib.json +0 -10
  131. package/tsconfig.spec.json +0 -14
  132. /package/dist/{src/lib → lib}/ChangeCase.d.ts +0 -0
  133. /package/dist/{src/lib → lib}/Constants.d.ts +0 -0
  134. /package/dist/{src/lib → lib}/NodesLoader/load-class-in-isolation.d.ts +0 -0
  135. /package/dist/{src/lib → lib}/NodesLoader/types.d.ts +0 -0
  136. /package/dist/{src/utils → utils}/crypto.d.ts +0 -0
@@ -1,575 +0,0 @@
1
- /* eslint-disable no-continue */
2
- /* eslint-disable no-await-in-loop */
3
- /* eslint-disable no-restricted-syntax */
4
- import { CronJob } from 'cron';
5
-
6
- import {
7
- IGetExecutePollFunctions,
8
- IGetExecuteTriggerFunctions,
9
- INode,
10
- IPollResponse,
11
- ITriggerResponse,
12
- IWorkflowExecuteAdditionalData,
13
- LoggerProxy as Logger,
14
- Workflow,
15
- WorkflowActivateMode,
16
- WorkflowExecuteMode,
17
- } from '@fsai-flow/workflow';
18
-
19
- import { secureRandomNumber } from '../utils/crypto';
20
-
21
- import { RedisLeaderElectionManager } from './RedisLeaderElectionManager';
22
-
23
- import type Redis from 'ioredis';
24
-
25
- // eslint-disable-next-line import/no-cycle
26
- import { ITriggerTime, IWorkflowData } from '..';
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
- }
37
-
38
- export class ActiveWorkflows {
39
- private workflowData: {
40
- [key: string]: IWorkflowData;
41
- } = {};
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
-
64
- /**
65
- * Returns if the workflow is active
66
- *
67
- * @param {string} id The id of the workflow to check
68
- * @returns {boolean}
69
- * @memberof ActiveWorkflows
70
- */
71
- isActive(id: string): boolean {
72
- // eslint-disable-next-line no-prototype-builtins
73
- return this.workflowData.hasOwnProperty(id);
74
- }
75
-
76
- /**
77
- * Returns the ids of the currently active workflows
78
- *
79
- * @returns {string[]}
80
- * @memberof ActiveWorkflows
81
- */
82
- allActiveWorkflows(): string[] {
83
- return Object.keys(this.workflowData);
84
- }
85
-
86
- /**
87
- * Returns the Workflow data for the workflow with
88
- * the given id if it is currently active
89
- *
90
- * @param {string} id
91
- * @returns {(WorkflowData | undefined)}
92
- * @memberof ActiveWorkflows
93
- */
94
- get(id: string): IWorkflowData | undefined {
95
- return this.workflowData[id];
96
- }
97
-
98
- /**
99
- * Makes a workflow active
100
- *
101
- * @param {string} id The id of the workflow to activate
102
- * @param {Workflow} workflow The workflow to activate
103
- * @param {IWorkflowExecuteAdditionalData} additionalData The additional data which is needed to run workflows
104
- * @returns {Promise<void>}
105
- * @memberof ActiveWorkflows
106
- */
107
- async add(
108
- id: string,
109
- workflow: Workflow,
110
- additionalData: IWorkflowExecuteAdditionalData,
111
- mode: WorkflowExecuteMode,
112
- activation: WorkflowActivateMode,
113
- getTriggerFunctions: IGetExecuteTriggerFunctions,
114
- getPollFunctions: IGetExecutePollFunctions,
115
- ): Promise<void> {
116
- this.workflowData[id] = {};
117
- const triggerNodes = workflow.getTriggerNodes();
118
-
119
- let triggerResponse: ITriggerResponse | undefined;
120
- this.workflowData[id].triggerResponses = [];
121
- for (const triggerNode of triggerNodes) {
122
- triggerResponse = await workflow.runTrigger(
123
- triggerNode,
124
- getTriggerFunctions,
125
- additionalData,
126
- mode,
127
- activation,
128
- );
129
- if (triggerResponse !== undefined) {
130
- // If a response was given save it
131
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
132
- this.workflowData[id].triggerResponses!.push(triggerResponse);
133
- }
134
- }
135
-
136
- const pollNodes = workflow.getPollNodes();
137
- if (pollNodes.length) {
138
- this.workflowData[id].pollResponses = [];
139
- for (const pollNode of pollNodes) {
140
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
141
- this.workflowData[id].pollResponses!.push(
142
- await this.activatePolling(
143
- pollNode,
144
- workflow,
145
- additionalData,
146
- getPollFunctions,
147
- mode,
148
- activation,
149
- ),
150
- );
151
- }
152
- }
153
- }
154
-
155
- /**
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
288
- *
289
- * @param {INode} node
290
- * @param {Workflow} workflow
291
- * @param {IWorkflowExecuteAdditionalData} additionalData
292
- * @param {IGetExecutePollFunctions} getPollFunctions
293
- * @returns {Promise<IPollResponse>}
294
- * @memberof ActiveWorkflows
295
- */
296
- async activatePolling(
297
- node: INode,
298
- workflow: Workflow,
299
- additionalData: IWorkflowExecuteAdditionalData,
300
- getPollFunctions: IGetExecutePollFunctions,
301
- mode: WorkflowExecuteMode,
302
- activation: WorkflowActivateMode,
303
- ): Promise<IPollResponse> {
304
- const pollFunctions = getPollFunctions(workflow, node, additionalData, mode, activation);
305
-
306
- // Get polling configuration
307
- const pollTimes = pollFunctions.getNodeParameter('pollTimes') as unknown as {
308
- item: ITriggerTime[];
309
- };
310
-
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[] {
419
- const parameterOrder = [
420
- 'second', 'minute', 'hour', 'dayOfMonth', 'month', 'weekday'
421
- ];
422
-
423
- const cronTimes: string[] = [];
424
- if (pollTimes.item !== undefined) {
425
- for (const item of pollTimes.item) {
426
- let cronTime: string[];
427
-
428
- if (item.mode === 'custom') {
429
- cronTimes.push((item['cronExpression'] as string).trim());
430
- continue;
431
- }
432
- if (item.mode === 'everyMinute') {
433
- cronTimes.push(`${secureRandomNumber(1, 60).toString()} * * * * *`);
434
- continue;
435
- }
436
- if (item.mode === 'everyX') {
437
- if (item['unit'] === 'minutes') {
438
- cronTimes.push(`${secureRandomNumber(1, 60).toString()} */${item['value']} * * * *`);
439
- } else if (item['unit'] === 'hours') {
440
- cronTimes.push(`${secureRandomNumber(1, 60).toString()} 0 */${item['value']} * * *`);
441
- } else if (item['unit'] === 'seconds') {
442
- cronTimes.push(`*/${item['value']} * * * * *`);
443
- }
444
- continue;
445
- }
446
-
447
- cronTime = [];
448
- for (const parameterName of parameterOrder) {
449
- if (item[parameterName] !== undefined) {
450
- cronTime.push(item[parameterName] as string);
451
- } else if (parameterName === 'second') {
452
- cronTime.push(secureRandomNumber(1, 60).toString());
453
- } else {
454
- cronTime.push('*');
455
- }
456
- }
457
- cronTimes.push(cronTime.join(' '));
458
- }
459
- }
460
- return cronTimes;
461
- }
462
-
463
- /**
464
- * Validate cron times (same as original)
465
- */
466
- private validateCronTimes(cronTimes: string[]): void {
467
- for (const cronTime of cronTimes) {
468
- const cronTimeParts = cronTime.split(' ');
469
-
470
- if (cronTimeParts.length === 6 && cronTimeParts[0].includes('/')) {
471
- const secondsInterval = parseInt(cronTimeParts[0].replace('*/', ''), 10);
472
- if (secondsInterval < 1) {
473
- throw new Error('The polling interval is too short. It has to be at least 1 second!');
474
- }
475
- } else if (cronTimeParts.length > 0 && cronTimeParts[0].includes('*') && !cronTimeParts[0].includes('/')) {
476
- throw new Error('The polling interval is too short. It has to be at least a minute!');
477
- }
478
- }
479
- }
480
-
481
- /**
482
- * Makes a workflow inactive
483
- *
484
- * @param {string} id The id of the workflow to deactivate
485
- * @returns {Promise<void>}
486
- * @memberof ActiveWorkflows
487
- */
488
- async remove(id: string): Promise<void> {
489
- if (!this.isActive(id)) {
490
- // Workflow is currently not registered
491
- throw new Error(
492
- `The workflow with the id "${id}" is currently not active and can so not be removed`,
493
- );
494
- }
495
-
496
- const workflowData = this.workflowData[id];
497
-
498
- if (workflowData.triggerResponses) {
499
- for (const triggerResponse of workflowData.triggerResponses) {
500
- if (triggerResponse.closeFunction) {
501
- try {
502
- await triggerResponse.closeFunction();
503
- } catch (error: any) {
504
- Logger.error(
505
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
506
- `There was a problem deactivating trigger of workflow "${id}": "${error.message}"`,
507
- {
508
- workflowId: id,
509
- },
510
- );
511
- }
512
- }
513
- }
514
- }
515
-
516
- if (workflowData.pollResponses) {
517
- for (const pollResponse of workflowData.pollResponses) {
518
- if (pollResponse.closeFunction) {
519
- try {
520
- await pollResponse.closeFunction();
521
- } catch (error:any) {
522
- Logger.error(
523
- // eslint-disable-next-line @typescript-eslint/no-unsafe-member-access, @typescript-eslint/restrict-template-expressions
524
- `There was a problem deactivating polling trigger of workflow "${id}": "${error.message}"`,
525
- {
526
- workflowId: id,
527
- },
528
- );
529
- }
530
- }
531
- }
532
- }
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
-
573
- delete this.workflowData[id];
574
- }
575
- }