@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.
- package/README.md +22 -2
- package/package.json +16 -6
- package/src/index.ts +1 -0
- package/src/lib/ActiveWorkflows.ts +326 -55
- package/src/lib/NodeExecuteFunctions.ts +0 -4
- package/src/lib/RedisLeaderElectionManager.ts +252 -0
- package/src/lib/WorkflowExecute.ts +26 -2
- package/tsconfig.base.json +28 -0
- package/tsconfig.json +1 -1
- package/dist/README.md +0 -11
- package/dist/package.json +0 -44
- package/dist/src/index.d.ts +0 -15
- package/dist/src/index.js +0 -29
- package/dist/src/index.js.map +0 -1
- package/dist/src/lib/ActiveWebhooks.d.ts +0 -59
- package/dist/src/lib/ActiveWebhooks.js +0 -184
- package/dist/src/lib/ActiveWebhooks.js.map +0 -1
- package/dist/src/lib/ActiveWorkflows.d.ts +0 -58
- package/dist/src/lib/ActiveWorkflows.js +0 -244
- package/dist/src/lib/ActiveWorkflows.js.map +0 -1
- package/dist/src/lib/BinaryDataManager/FileSystem.d.ts +0 -26
- package/dist/src/lib/BinaryDataManager/FileSystem.js +0 -179
- package/dist/src/lib/BinaryDataManager/FileSystem.js.map +0 -1
- package/dist/src/lib/BinaryDataManager/index.d.ts +0 -21
- package/dist/src/lib/BinaryDataManager/index.js +0 -146
- package/dist/src/lib/BinaryDataManager/index.js.map +0 -1
- package/dist/src/lib/ChangeCase.d.ts +0 -9
- package/dist/src/lib/ChangeCase.js +0 -43
- package/dist/src/lib/ChangeCase.js.map +0 -1
- package/dist/src/lib/Constants.d.ts +0 -14
- package/dist/src/lib/Constants.js +0 -19
- package/dist/src/lib/Constants.js.map +0 -1
- package/dist/src/lib/Credentials.d.ts +0 -27
- package/dist/src/lib/Credentials.js +0 -89
- package/dist/src/lib/Credentials.js.map +0 -1
- package/dist/src/lib/FileSystem.d.ts +0 -26
- package/dist/src/lib/FileSystem.js +0 -179
- package/dist/src/lib/FileSystem.js.map +0 -1
- package/dist/src/lib/InputConnectionDataLegacy.d.ts +0 -2
- package/dist/src/lib/InputConnectionDataLegacy.js +0 -79
- package/dist/src/lib/InputConnectionDataLegacy.js.map +0 -1
- package/dist/src/lib/Interfaces.d.ts +0 -148
- package/dist/src/lib/Interfaces.js +0 -3
- package/dist/src/lib/Interfaces.js.map +0 -1
- package/dist/src/lib/LoadNodeParameterOptions.d.ts +0 -39
- package/dist/src/lib/LoadNodeParameterOptions.js +0 -150
- package/dist/src/lib/LoadNodeParameterOptions.js.map +0 -1
- package/dist/src/lib/NodeExecuteFunctions.d.ts +0 -226
- package/dist/src/lib/NodeExecuteFunctions.js +0 -2483
- package/dist/src/lib/NodeExecuteFunctions.js.map +0 -1
- package/dist/src/lib/NodesLoader/constants.d.ts +0 -5
- package/dist/src/lib/NodesLoader/constants.js +0 -106
- package/dist/src/lib/NodesLoader/constants.js.map +0 -1
- package/dist/src/lib/NodesLoader/custom-directory-loader.d.ts +0 -9
- package/dist/src/lib/NodesLoader/custom-directory-loader.js +0 -36
- package/dist/src/lib/NodesLoader/custom-directory-loader.js.map +0 -1
- package/dist/src/lib/NodesLoader/directory-loader.d.ts +0 -66
- package/dist/src/lib/NodesLoader/directory-loader.js +0 -325
- package/dist/src/lib/NodesLoader/directory-loader.js.map +0 -1
- package/dist/src/lib/NodesLoader/index.d.ts +0 -5
- package/dist/src/lib/NodesLoader/index.js +0 -12
- package/dist/src/lib/NodesLoader/index.js.map +0 -1
- package/dist/src/lib/NodesLoader/lazy-package-directory-loader.d.ts +0 -7
- package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js +0 -52
- package/dist/src/lib/NodesLoader/lazy-package-directory-loader.js.map +0 -1
- package/dist/src/lib/NodesLoader/load-class-in-isolation.d.ts +0 -1
- package/dist/src/lib/NodesLoader/load-class-in-isolation.js +0 -22
- package/dist/src/lib/NodesLoader/load-class-in-isolation.js.map +0 -1
- package/dist/src/lib/NodesLoader/package-directory-loader.d.ts +0 -17
- package/dist/src/lib/NodesLoader/package-directory-loader.js +0 -100
- package/dist/src/lib/NodesLoader/package-directory-loader.js.map +0 -1
- package/dist/src/lib/NodesLoader/types.d.ts +0 -14
- package/dist/src/lib/NodesLoader/types.js +0 -3
- package/dist/src/lib/NodesLoader/types.js.map +0 -1
- package/dist/src/lib/UserSettings.d.ts +0 -80
- package/dist/src/lib/UserSettings.js +0 -261
- package/dist/src/lib/UserSettings.js.map +0 -1
- package/dist/src/lib/WorkflowExecute.d.ts +0 -53
- package/dist/src/lib/WorkflowExecute.js +0 -835
- package/dist/src/lib/WorkflowExecute.js.map +0 -1
- package/dist/src/lib/index.d.ts +0 -21
- package/dist/src/lib/index.js +0 -146
- package/dist/src/lib/index.js.map +0 -1
- package/dist/src/utils/crypto.d.ts +0 -1
- package/dist/src/utils/crypto.js +0 -8
- 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
|
|
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.
|
|
4
|
-
"license": "PolyForm Noncommercial License 1.0.0",
|
|
3
|
+
"version": "0.0.3",
|
|
5
4
|
"dependencies": {
|
|
6
|
-
"@fsai-flow/workflow": "
|
|
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": "
|
|
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": "
|
|
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
|
-
*
|
|
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
|
-
//
|
|
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',
|
|
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
|
-
|
|
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
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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);
|