@fsai-flow/core 0.0.5 → 0.1.1
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/dist/index.d.ts +17 -0
- package/dist/index.js +61 -0
- package/dist/lib/ActiveWebhooks.d.ts +59 -0
- package/dist/lib/ActiveWebhooks.js +177 -0
- package/dist/lib/ActiveWorkflows.d.ts +87 -0
- package/dist/lib/ActiveWorkflows.js +465 -0
- package/dist/lib/BinaryDataManager/FileSystem.d.ts +26 -0
- package/dist/lib/BinaryDataManager/FileSystem.js +180 -0
- package/dist/lib/BinaryDataManager/index.d.ts +21 -0
- package/dist/lib/BinaryDataManager/index.js +129 -0
- package/dist/lib/ChangeCase.d.ts +9 -0
- package/dist/lib/ChangeCase.js +43 -0
- package/dist/lib/Constants.d.ts +14 -0
- package/dist/lib/Constants.js +18 -0
- package/dist/lib/Credentials.d.ts +27 -0
- package/dist/lib/Credentials.js +88 -0
- package/dist/lib/FileSystem.d.ts +26 -0
- package/dist/lib/FileSystem.js +180 -0
- package/dist/lib/InputConnectionDataLegacy.d.ts +2 -0
- package/dist/lib/InputConnectionDataLegacy.js +72 -0
- package/dist/lib/Interfaces.d.ts +147 -0
- package/dist/lib/Interfaces.js +2 -0
- package/dist/lib/LoadNodeParameterOptions.d.ts +39 -0
- package/dist/lib/LoadNodeParameterOptions.js +152 -0
- package/dist/lib/NodeExecuteFunctions.d.ts +225 -0
- package/dist/lib/NodeExecuteFunctions.js +2467 -0
- package/dist/lib/NodesLoader/constants.d.ts +5 -0
- package/dist/lib/NodesLoader/constants.js +105 -0
- package/dist/lib/NodesLoader/custom-directory-loader.d.ts +9 -0
- package/dist/lib/NodesLoader/custom-directory-loader.js +35 -0
- package/dist/lib/NodesLoader/directory-loader.d.ts +66 -0
- package/dist/lib/NodesLoader/directory-loader.js +367 -0
- package/dist/lib/NodesLoader/index.d.ts +5 -0
- package/dist/lib/NodesLoader/index.js +11 -0
- package/dist/lib/NodesLoader/lazy-package-directory-loader.d.ts +7 -0
- package/dist/lib/NodesLoader/lazy-package-directory-loader.js +44 -0
- package/dist/lib/NodesLoader/load-class-in-isolation.d.ts +1 -0
- package/dist/lib/NodesLoader/load-class-in-isolation.js +17 -0
- package/dist/lib/NodesLoader/package-directory-loader.d.ts +17 -0
- package/dist/lib/NodesLoader/package-directory-loader.js +92 -0
- package/dist/lib/NodesLoader/types.d.ts +14 -0
- package/dist/lib/NodesLoader/types.js +2 -0
- package/dist/lib/RedisLeaderElectionManager.d.ts +53 -0
- package/dist/lib/RedisLeaderElectionManager.js +279 -0
- package/dist/lib/RequestTypes.d.ts +58 -0
- package/dist/lib/RequestTypes.js +8 -0
- package/dist/lib/UserSettings.d.ts +80 -0
- package/dist/lib/UserSettings.js +269 -0
- package/dist/lib/WorkflowExecute.d.ts +53 -0
- package/dist/lib/WorkflowExecute.js +906 -0
- package/dist/lib/index.d.ts +21 -0
- package/dist/lib/index.js +129 -0
- package/dist/utils/crypto.d.ts +1 -0
- package/dist/utils/crypto.js +7 -0
- package/package.json +52 -52
- package/dist/README.md +0 -31
- package/dist/package.json +0 -54
- package/eslint.config.js +0 -19
- package/jest.config.ts +0 -10
- package/project.json +0 -19
- package/src/index.ts +0 -28
- package/src/lib/ActiveWebhooks.ts +0 -245
- package/src/lib/ActiveWorkflows.ts +0 -575
- package/src/lib/BinaryDataManager/FileSystem.ts +0 -214
- package/src/lib/BinaryDataManager/index.ts +0 -187
- package/src/lib/ChangeCase.ts +0 -45
- package/src/lib/Constants.ts +0 -16
- package/src/lib/Credentials.ts +0 -108
- package/src/lib/FileSystem.ts +0 -214
- package/src/lib/InputConnectionDataLegacy.ts +0 -123
- package/src/lib/Interfaces.ts +0 -338
- package/src/lib/LoadNodeParameterOptions.ts +0 -235
- package/src/lib/NodeExecuteFunctions.ts +0 -3700
- package/src/lib/NodesLoader/constants.ts +0 -112
- package/src/lib/NodesLoader/custom-directory-loader.ts +0 -31
- package/src/lib/NodesLoader/directory-loader.ts +0 -458
- package/src/lib/NodesLoader/index.ts +0 -5
- package/src/lib/NodesLoader/lazy-package-directory-loader.ts +0 -55
- package/src/lib/NodesLoader/load-class-in-isolation.ts +0 -19
- package/src/lib/NodesLoader/package-directory-loader.ts +0 -107
- package/src/lib/NodesLoader/types.ts +0 -14
- package/src/lib/RedisLeaderElectionManager.ts +0 -334
- package/src/lib/UserSettings.ts +0 -292
- package/src/lib/WorkflowExecute.ts +0 -1128
- package/src/lib/index.ts +0 -187
- package/src/utils/crypto.ts +0 -5
- package/tests/Credentials.test.ts +0 -88
- package/tests/Helpers.ts +0 -808
- package/tests/WorkflowExecute.test.ts +0 -1242
- package/tsconfig.json +0 -41
- package/tsconfig.lib.json +0 -10
- package/tsconfig.spec.json +0 -14
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.loadClassInIsolation = void 0;
|
|
4
|
+
const node_vm_1 = require("node:vm");
|
|
5
|
+
const Constants_1 = require("../Constants");
|
|
6
|
+
const context = (0, node_vm_1.createContext)({ require });
|
|
7
|
+
const loadClassInIsolation = (filePath, className) => {
|
|
8
|
+
const normalizedPath = process.platform === "win32" ? filePath.replace(/\\/g, "/") : filePath;
|
|
9
|
+
// Note: Skip the isolation because it breaks nock mocks in tests
|
|
10
|
+
if (Constants_1.inTest) {
|
|
11
|
+
// eslint-disable-next-line @typescript-eslint/no-unsafe-call, @typescript-eslint/no-var-requires, @typescript-eslint/no-unsafe-member-access
|
|
12
|
+
return new (require(normalizedPath)[className])();
|
|
13
|
+
}
|
|
14
|
+
const script = new node_vm_1.Script(`new (require('${normalizedPath}').${className})()`);
|
|
15
|
+
return script.runInContext(context);
|
|
16
|
+
};
|
|
17
|
+
exports.loadClassInIsolation = loadClassInIsolation;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { DirectoryLoader } from "./directory-loader";
|
|
2
|
+
import type { n8n } from "./types";
|
|
3
|
+
/**
|
|
4
|
+
* Loader for source files of nodes and credentials located in a package dir,
|
|
5
|
+
* e.g. /nodes-base or community packages.
|
|
6
|
+
*/
|
|
7
|
+
export declare class PackageDirectoryLoader extends DirectoryLoader {
|
|
8
|
+
packageJson: n8n.PackageJson;
|
|
9
|
+
packageName: string;
|
|
10
|
+
constructor(directory: string, excludeNodes?: string[], includeNodes?: string[]);
|
|
11
|
+
private extractNodeTypes;
|
|
12
|
+
loadAll(): Promise<void>;
|
|
13
|
+
private inferSupportedNodes;
|
|
14
|
+
private parseJSON;
|
|
15
|
+
protected readJSONSync<T>(file: string): T;
|
|
16
|
+
protected readJSON<T>(file: string): Promise<T>;
|
|
17
|
+
}
|
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.PackageDirectoryLoader = void 0;
|
|
4
|
+
const node_fs_1 = require("node:fs");
|
|
5
|
+
const promises_1 = require("node:fs/promises");
|
|
6
|
+
const workflow_1 = require("@fsai-flow/workflow");
|
|
7
|
+
const directory_loader_1 = require("./directory-loader");
|
|
8
|
+
/**
|
|
9
|
+
* Loader for source files of nodes and credentials located in a package dir,
|
|
10
|
+
* e.g. /nodes-base or community packages.
|
|
11
|
+
*/
|
|
12
|
+
class PackageDirectoryLoader extends directory_loader_1.DirectoryLoader {
|
|
13
|
+
constructor(directory, excludeNodes = [], includeNodes = []) {
|
|
14
|
+
super(directory, excludeNodes, includeNodes);
|
|
15
|
+
this.packageJson = this.readJSONSync("package.json");
|
|
16
|
+
this.packageName = this.packageJson.name;
|
|
17
|
+
this.excludeNodes = this.extractNodeTypes(excludeNodes);
|
|
18
|
+
this.includeNodes = this.extractNodeTypes(includeNodes);
|
|
19
|
+
}
|
|
20
|
+
extractNodeTypes(fullNodeTypes) {
|
|
21
|
+
return fullNodeTypes
|
|
22
|
+
.map((fullNodeType) => fullNodeType.split("."))
|
|
23
|
+
.filter(([packageName]) => packageName === this.packageName)
|
|
24
|
+
.map(([_, nodeType]) => nodeType);
|
|
25
|
+
}
|
|
26
|
+
async loadAll() {
|
|
27
|
+
const { n8n } = this.packageJson;
|
|
28
|
+
if (!n8n)
|
|
29
|
+
return;
|
|
30
|
+
const { nodes, credentials } = n8n;
|
|
31
|
+
if (Array.isArray(nodes)) {
|
|
32
|
+
for (const nodePath of nodes) {
|
|
33
|
+
this.loadNodeFromFile(nodePath);
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
if (Array.isArray(credentials)) {
|
|
37
|
+
for (const credentialPath of credentials) {
|
|
38
|
+
this.loadCredentialFromFile(credentialPath);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
this.inferSupportedNodes();
|
|
42
|
+
console.debug(`Loaded all credentials and nodes from ${this.packageName}`, {
|
|
43
|
+
credentials: credentials?.length ?? 0,
|
|
44
|
+
nodes: nodes?.length ?? 0,
|
|
45
|
+
});
|
|
46
|
+
}
|
|
47
|
+
inferSupportedNodes() {
|
|
48
|
+
const knownCredentials = this.known.credentials;
|
|
49
|
+
for (const { type: credentialType } of Object.values(this.credentialTypes)) {
|
|
50
|
+
const supportedNodes = knownCredentials[credentialType.name].supportedNodes ?? [];
|
|
51
|
+
if (supportedNodes.length > 0 && credentialType.httpRequestNode) {
|
|
52
|
+
credentialType.httpRequestNode.hidden = true;
|
|
53
|
+
}
|
|
54
|
+
credentialType.supportedNodes = supportedNodes;
|
|
55
|
+
if (!credentialType.iconUrl && !credentialType.icon) {
|
|
56
|
+
for (const supportedNode of supportedNodes) {
|
|
57
|
+
const nodeDescription = this.nodeTypes[supportedNode]?.type.description;
|
|
58
|
+
if (!nodeDescription)
|
|
59
|
+
continue;
|
|
60
|
+
if (nodeDescription.icon) {
|
|
61
|
+
credentialType.icon = nodeDescription.icon;
|
|
62
|
+
credentialType.iconColor = nodeDescription.iconColor;
|
|
63
|
+
break;
|
|
64
|
+
}
|
|
65
|
+
if (nodeDescription.iconUrl) {
|
|
66
|
+
credentialType.iconUrl = nodeDescription.iconUrl;
|
|
67
|
+
break;
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
parseJSON(fileString, filePath) {
|
|
74
|
+
try {
|
|
75
|
+
return (0, workflow_1.jsonParse)(fileString);
|
|
76
|
+
}
|
|
77
|
+
catch (_error) {
|
|
78
|
+
throw new Error(`Failed to parse JSON, ${filePath}`);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
readJSONSync(file) {
|
|
82
|
+
const filePath = this.resolvePath(file);
|
|
83
|
+
const fileString = (0, node_fs_1.readFileSync)(filePath, "utf8");
|
|
84
|
+
return this.parseJSON(fileString, filePath);
|
|
85
|
+
}
|
|
86
|
+
async readJSON(file) {
|
|
87
|
+
const filePath = this.resolvePath(file);
|
|
88
|
+
const fileString = await (0, promises_1.readFile)(filePath, "utf8");
|
|
89
|
+
return this.parseJSON(fileString, filePath);
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
exports.PackageDirectoryLoader = PackageDirectoryLoader;
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
import { type RedisOptions } from "ioredis";
|
|
2
|
+
export interface RedisLeaderElectionCallbacks {
|
|
3
|
+
onStartedLeading: () => void;
|
|
4
|
+
onStoppedLeading: () => void;
|
|
5
|
+
onNewLeader?: (identity: string) => void;
|
|
6
|
+
}
|
|
7
|
+
export declare class RedisLeaderElectionManager {
|
|
8
|
+
private redis;
|
|
9
|
+
private isLeader;
|
|
10
|
+
private lockKey;
|
|
11
|
+
private nodeId;
|
|
12
|
+
private lockTTL;
|
|
13
|
+
private renewalInterval;
|
|
14
|
+
private renewalTimer?;
|
|
15
|
+
private callbacks;
|
|
16
|
+
constructor(lockKey: string, redisConfig: string | RedisOptions, callbacks: RedisLeaderElectionCallbacks);
|
|
17
|
+
/**
|
|
18
|
+
* Wait for Redis connection to be ready
|
|
19
|
+
*/
|
|
20
|
+
private waitForRedisConnection;
|
|
21
|
+
/**
|
|
22
|
+
* Start the leader election process
|
|
23
|
+
*/
|
|
24
|
+
start(): Promise<void>;
|
|
25
|
+
/**
|
|
26
|
+
* Stop the leader election process
|
|
27
|
+
*/
|
|
28
|
+
stop(): Promise<void>;
|
|
29
|
+
/**
|
|
30
|
+
* Check if this node is currently the leader
|
|
31
|
+
*/
|
|
32
|
+
getIsLeader(): boolean;
|
|
33
|
+
/**
|
|
34
|
+
* Try to acquire leadership
|
|
35
|
+
*/
|
|
36
|
+
private tryAcquireLeadership;
|
|
37
|
+
/**
|
|
38
|
+
* Renew the leadership lock
|
|
39
|
+
*/
|
|
40
|
+
private renewLock;
|
|
41
|
+
/**
|
|
42
|
+
* Release the leadership lock
|
|
43
|
+
*/
|
|
44
|
+
private releaseLock;
|
|
45
|
+
/**
|
|
46
|
+
* Start the renewal/retry loop
|
|
47
|
+
*/
|
|
48
|
+
private startRenewalLoop;
|
|
49
|
+
/**
|
|
50
|
+
* Generate a unique node identifier
|
|
51
|
+
*/
|
|
52
|
+
private generateNodeId;
|
|
53
|
+
}
|
|
@@ -0,0 +1,279 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.RedisLeaderElectionManager = void 0;
|
|
7
|
+
const workflow_1 = require("@fsai-flow/workflow");
|
|
8
|
+
const ioredis_1 = __importDefault(require("ioredis"));
|
|
9
|
+
class RedisLeaderElectionManager {
|
|
10
|
+
constructor(lockKey, redisConfig, callbacks) {
|
|
11
|
+
this.isLeader = false;
|
|
12
|
+
this.lockTTL = 30000; // 30 seconds in milliseconds
|
|
13
|
+
this.renewalInterval = 10000; // 10 seconds in milliseconds
|
|
14
|
+
this.lockKey = `leader:${lockKey}`;
|
|
15
|
+
this.nodeId = this.generateNodeId();
|
|
16
|
+
this.callbacks = callbacks;
|
|
17
|
+
// Initialize Redis connection
|
|
18
|
+
if (typeof redisConfig === "string") {
|
|
19
|
+
this.redis = new ioredis_1.default(redisConfig);
|
|
20
|
+
}
|
|
21
|
+
else {
|
|
22
|
+
this.redis = new ioredis_1.default(redisConfig);
|
|
23
|
+
}
|
|
24
|
+
// Handle Redis connection events
|
|
25
|
+
this.redis.on("connect", () => {
|
|
26
|
+
workflow_1.LoggerProxy.info(`Redis connected for leader election: ${this.lockKey}`);
|
|
27
|
+
});
|
|
28
|
+
this.redis.on("ready", () => {
|
|
29
|
+
workflow_1.LoggerProxy.info(`Redis ready for leader election: ${this.lockKey}`);
|
|
30
|
+
});
|
|
31
|
+
this.redis.on("error", (error) => {
|
|
32
|
+
workflow_1.LoggerProxy.error(`Redis connection error for leader election: ${error instanceof Error ? error.message : String(error)}`);
|
|
33
|
+
});
|
|
34
|
+
}
|
|
35
|
+
/**
|
|
36
|
+
* Wait for Redis connection to be ready
|
|
37
|
+
*/
|
|
38
|
+
async waitForRedisConnection() {
|
|
39
|
+
return new Promise((resolve, reject) => {
|
|
40
|
+
if (this.redis.status === "ready") {
|
|
41
|
+
workflow_1.LoggerProxy.debug(`Redis already ready for ${this.lockKey}`);
|
|
42
|
+
resolve();
|
|
43
|
+
return;
|
|
44
|
+
}
|
|
45
|
+
let isSettled = false;
|
|
46
|
+
let timeoutId = null;
|
|
47
|
+
// Cleanup function to prevent memory leaks
|
|
48
|
+
const cleanup = () => {
|
|
49
|
+
if (isSettled)
|
|
50
|
+
return; // Already cleaned up
|
|
51
|
+
isSettled = true;
|
|
52
|
+
// Clear timeout safely
|
|
53
|
+
if (timeoutId !== null) {
|
|
54
|
+
clearTimeout(timeoutId);
|
|
55
|
+
timeoutId = null;
|
|
56
|
+
}
|
|
57
|
+
// Remove event listeners safely
|
|
58
|
+
try {
|
|
59
|
+
this.redis.off("ready", onReady);
|
|
60
|
+
this.redis.off("error", onError);
|
|
61
|
+
}
|
|
62
|
+
catch (error) {
|
|
63
|
+
// Ignore cleanup errors - Redis connection might be destroyed
|
|
64
|
+
workflow_1.LoggerProxy.debug(`Cleanup warning for ${this.lockKey}: ${error}`);
|
|
65
|
+
}
|
|
66
|
+
};
|
|
67
|
+
const onReady = () => {
|
|
68
|
+
cleanup();
|
|
69
|
+
workflow_1.LoggerProxy.debug(`Redis connection established for ${this.lockKey}`);
|
|
70
|
+
resolve();
|
|
71
|
+
};
|
|
72
|
+
const onError = (error) => {
|
|
73
|
+
cleanup();
|
|
74
|
+
workflow_1.LoggerProxy.error(`Redis connection failed for ${this.lockKey}: ${error.message}`);
|
|
75
|
+
reject(error);
|
|
76
|
+
};
|
|
77
|
+
const onTimeout = () => {
|
|
78
|
+
cleanup();
|
|
79
|
+
reject(new Error(`Redis connection timeout after 10 seconds for ${this.lockKey}`));
|
|
80
|
+
};
|
|
81
|
+
// Set up timeout
|
|
82
|
+
timeoutId = setTimeout(onTimeout, 10000); // 10 second timeout
|
|
83
|
+
// Set up event listeners
|
|
84
|
+
this.redis.once("ready", onReady);
|
|
85
|
+
this.redis.once("error", onError);
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
/**
|
|
89
|
+
* Start the leader election process
|
|
90
|
+
*/
|
|
91
|
+
async start() {
|
|
92
|
+
workflow_1.LoggerProxy.info(`🚀 Starting Redis leader election for ${this.nodeId} on key ${this.lockKey}`);
|
|
93
|
+
workflow_1.LoggerProxy.info(`🔧 Leader election config: TTL=${this.lockTTL}ms, RenewalInterval=${this.renewalInterval}ms`);
|
|
94
|
+
// Wait for Redis connection to be ready
|
|
95
|
+
workflow_1.LoggerProxy.info(`⏳ ${this.nodeId} waiting for Redis connection...`);
|
|
96
|
+
await this.waitForRedisConnection();
|
|
97
|
+
workflow_1.LoggerProxy.info(`✅ Redis connection ready for ${this.nodeId}`);
|
|
98
|
+
// Try to acquire leadership immediately
|
|
99
|
+
workflow_1.LoggerProxy.info(`🎯 ${this.nodeId} making initial leadership attempt...`);
|
|
100
|
+
await this.tryAcquireLeadership();
|
|
101
|
+
// Start the renewal/retry loop
|
|
102
|
+
this.startRenewalLoop();
|
|
103
|
+
workflow_1.LoggerProxy.info(`✅ Leader election process started for ${this.nodeId}`);
|
|
104
|
+
}
|
|
105
|
+
/**
|
|
106
|
+
* Stop the leader election process
|
|
107
|
+
*/
|
|
108
|
+
async stop() {
|
|
109
|
+
workflow_1.LoggerProxy.info(`🛑 Stopping Redis leader election for ${this.nodeId}`);
|
|
110
|
+
// Stop renewal timer
|
|
111
|
+
if (this.renewalTimer) {
|
|
112
|
+
workflow_1.LoggerProxy.debug(`⏹️ Stopping leadership monitoring loop for ${this.nodeId}`);
|
|
113
|
+
clearInterval(this.renewalTimer);
|
|
114
|
+
this.renewalTimer = undefined;
|
|
115
|
+
}
|
|
116
|
+
// Release leadership if we have it
|
|
117
|
+
if (this.isLeader) {
|
|
118
|
+
workflow_1.LoggerProxy.info(`👋 ${this.nodeId} releasing leadership voluntarily for ${this.lockKey}`);
|
|
119
|
+
await this.releaseLock();
|
|
120
|
+
}
|
|
121
|
+
// Close Redis connection
|
|
122
|
+
workflow_1.LoggerProxy.debug(`🔌 Disconnecting Redis for ${this.nodeId}`);
|
|
123
|
+
this.redis.disconnect();
|
|
124
|
+
workflow_1.LoggerProxy.info(`✅ Leader election stopped for ${this.nodeId}`);
|
|
125
|
+
}
|
|
126
|
+
/**
|
|
127
|
+
* Check if this node is currently the leader
|
|
128
|
+
*/
|
|
129
|
+
getIsLeader() {
|
|
130
|
+
return this.isLeader;
|
|
131
|
+
}
|
|
132
|
+
/**
|
|
133
|
+
* Try to acquire leadership
|
|
134
|
+
*/
|
|
135
|
+
async tryAcquireLeadership() {
|
|
136
|
+
try {
|
|
137
|
+
// Check if Redis connection is ready before attempting operations
|
|
138
|
+
if (this.redis.status !== "ready") {
|
|
139
|
+
workflow_1.LoggerProxy.warn(`Redis not ready for ${this.nodeId}, current status: ${this.redis.status}`);
|
|
140
|
+
return false;
|
|
141
|
+
}
|
|
142
|
+
workflow_1.LoggerProxy.debug(`Node ${this.nodeId} attempting to acquire leadership for ${this.lockKey}`);
|
|
143
|
+
// Use SET with NX (not exists) and PX (expire in milliseconds)
|
|
144
|
+
const result = await this.redis.set(this.lockKey, this.nodeId, "PX", this.lockTTL, "NX");
|
|
145
|
+
if (result === "OK") {
|
|
146
|
+
// Successfully acquired leadership
|
|
147
|
+
if (!this.isLeader) {
|
|
148
|
+
this.isLeader = true;
|
|
149
|
+
workflow_1.LoggerProxy.info(`🏆 Node ${this.nodeId} ACQUIRED LEADERSHIP for ${this.lockKey} (TTL: ${this.lockTTL}ms)`);
|
|
150
|
+
this.callbacks.onStartedLeading();
|
|
151
|
+
}
|
|
152
|
+
else {
|
|
153
|
+
workflow_1.LoggerProxy.debug(`Node ${this.nodeId} renewed leadership lock for ${this.lockKey}`);
|
|
154
|
+
}
|
|
155
|
+
return true;
|
|
156
|
+
}
|
|
157
|
+
// Failed to acquire leadership - check who is the current leader
|
|
158
|
+
const currentLeader = await this.redis.get(this.lockKey);
|
|
159
|
+
const lockTTL = await this.redis.pttl(this.lockKey);
|
|
160
|
+
if (currentLeader) {
|
|
161
|
+
if (currentLeader !== this.nodeId) {
|
|
162
|
+
workflow_1.LoggerProxy.debug(`👑 Node ${this.nodeId} sees ${currentLeader} is the current leader for ${this.lockKey} (TTL: ${lockTTL}ms)`);
|
|
163
|
+
if (this.callbacks.onNewLeader) {
|
|
164
|
+
this.callbacks.onNewLeader(currentLeader);
|
|
165
|
+
}
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
else {
|
|
169
|
+
workflow_1.LoggerProxy.debug(`Node ${this.nodeId} found no current leader for ${this.lockKey}, but failed to acquire lock`);
|
|
170
|
+
}
|
|
171
|
+
// Lost leadership if we previously had it
|
|
172
|
+
if (this.isLeader) {
|
|
173
|
+
this.isLeader = false;
|
|
174
|
+
workflow_1.LoggerProxy.info(`📉 Node ${this.nodeId} LOST LEADERSHIP for ${this.lockKey}`);
|
|
175
|
+
this.callbacks.onStoppedLeading();
|
|
176
|
+
}
|
|
177
|
+
return false;
|
|
178
|
+
}
|
|
179
|
+
catch (error) {
|
|
180
|
+
workflow_1.LoggerProxy.error(`❌ Error during leader election for ${this.nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
181
|
+
// If we had leadership and there's an error, assume we lost it
|
|
182
|
+
if (this.isLeader) {
|
|
183
|
+
this.isLeader = false;
|
|
184
|
+
workflow_1.LoggerProxy.info(`💥 Node ${this.nodeId} LOST LEADERSHIP due to error: ${this.lockKey}`);
|
|
185
|
+
this.callbacks.onStoppedLeading();
|
|
186
|
+
}
|
|
187
|
+
return false;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
/**
|
|
191
|
+
* Renew the leadership lock
|
|
192
|
+
*/
|
|
193
|
+
async renewLock() {
|
|
194
|
+
try {
|
|
195
|
+
// Check if Redis connection is ready before attempting operations
|
|
196
|
+
if (this.redis.status !== "ready") {
|
|
197
|
+
workflow_1.LoggerProxy.warn(`Redis not ready for ${this.nodeId} renewal, current status: ${this.redis.status}`);
|
|
198
|
+
return false;
|
|
199
|
+
}
|
|
200
|
+
workflow_1.LoggerProxy.debug(`🔄 Node ${this.nodeId} attempting to renew leadership for ${this.lockKey}`);
|
|
201
|
+
// Use Lua script to atomically check ownership and renew
|
|
202
|
+
const script = `
|
|
203
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
204
|
+
return redis.call("pexpire", KEYS[1], ARGV[2])
|
|
205
|
+
else
|
|
206
|
+
return 0
|
|
207
|
+
end
|
|
208
|
+
`;
|
|
209
|
+
const result = await this.redis.eval(script, 1, this.lockKey, this.nodeId, this.lockTTL.toString());
|
|
210
|
+
if (result === 1) {
|
|
211
|
+
workflow_1.LoggerProxy.debug(`✅ Node ${this.nodeId} successfully renewed leadership for ${this.lockKey} (TTL: ${this.lockTTL}ms)`);
|
|
212
|
+
return true;
|
|
213
|
+
}
|
|
214
|
+
workflow_1.LoggerProxy.warn(`⚠️ Node ${this.nodeId} failed to renew leadership for ${this.lockKey} - lock no longer owned by this node`);
|
|
215
|
+
return false;
|
|
216
|
+
}
|
|
217
|
+
catch (error) {
|
|
218
|
+
workflow_1.LoggerProxy.error(`❌ Error renewing leadership lock for ${this.nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Release the leadership lock
|
|
224
|
+
*/
|
|
225
|
+
async releaseLock() {
|
|
226
|
+
try {
|
|
227
|
+
// Use Lua script to atomically check ownership and delete
|
|
228
|
+
const script = `
|
|
229
|
+
if redis.call("get", KEYS[1]) == ARGV[1] then
|
|
230
|
+
return redis.call("del", KEYS[1])
|
|
231
|
+
else
|
|
232
|
+
return 0
|
|
233
|
+
end
|
|
234
|
+
`;
|
|
235
|
+
await this.redis.eval(script, 1, this.lockKey, this.nodeId);
|
|
236
|
+
workflow_1.LoggerProxy.info(`Node ${this.nodeId} released leadership lock for ${this.lockKey}`);
|
|
237
|
+
}
|
|
238
|
+
catch (error) {
|
|
239
|
+
workflow_1.LoggerProxy.error(`Error releasing lock for ${this.nodeId}: ${error instanceof Error ? error.message : String(error)}`);
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
/**
|
|
243
|
+
* Start the renewal/retry loop
|
|
244
|
+
*/
|
|
245
|
+
startRenewalLoop() {
|
|
246
|
+
workflow_1.LoggerProxy.info(`Starting leadership monitoring loop for ${this.nodeId} (check every ${this.renewalInterval}ms)`);
|
|
247
|
+
this.renewalTimer = setInterval(async () => {
|
|
248
|
+
if (this.isLeader) {
|
|
249
|
+
// Leader: Try to renew the lock
|
|
250
|
+
workflow_1.LoggerProxy.debug(`⏰ [LEADER] Node ${this.nodeId} periodic leadership renewal check`);
|
|
251
|
+
const renewed = await this.renewLock();
|
|
252
|
+
if (!renewed) {
|
|
253
|
+
// Failed to renew - we lost leadership
|
|
254
|
+
this.isLeader = false;
|
|
255
|
+
workflow_1.LoggerProxy.info(`💔 [LEADER] Node ${this.nodeId} LOST LEADERSHIP (failed to renew) for ${this.lockKey}`);
|
|
256
|
+
this.callbacks.onStoppedLeading();
|
|
257
|
+
}
|
|
258
|
+
else {
|
|
259
|
+
// Successfully renewed - keeping leadership
|
|
260
|
+
//Logger.info(`🔒 [LEADER] Node ${this.nodeId} RENEWED LEADERSHIP - staying active for ${this.lockKey}`);
|
|
261
|
+
}
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
// Follower: Try to acquire leadership
|
|
265
|
+
workflow_1.LoggerProxy.debug(`⏰ [FOLLOWER] Node ${this.nodeId} checking for available leadership`);
|
|
266
|
+
await this.tryAcquireLeadership();
|
|
267
|
+
}
|
|
268
|
+
}, this.renewalInterval);
|
|
269
|
+
}
|
|
270
|
+
/**
|
|
271
|
+
* Generate a unique node identifier
|
|
272
|
+
*/
|
|
273
|
+
generateNodeId() {
|
|
274
|
+
return (process.env["POD_NAME"] ||
|
|
275
|
+
process.env["HOSTNAME"] ||
|
|
276
|
+
`node-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
exports.RedisLeaderElectionManager = RedisLeaderElectionManager;
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Local replacement types for the deprecated `request` and `request-promise-native` packages.
|
|
3
|
+
*
|
|
4
|
+
* These interfaces provide type compatibility for function signatures that previously
|
|
5
|
+
* referenced types from those packages. The actual HTTP implementation uses axios.
|
|
6
|
+
*/
|
|
7
|
+
import type * as http from "node:http";
|
|
8
|
+
import type * as url from "node:url";
|
|
9
|
+
export interface RequestOptions {
|
|
10
|
+
uri?: string | url.Url;
|
|
11
|
+
url?: string | url.Url;
|
|
12
|
+
method?: string;
|
|
13
|
+
headers?: Record<string, unknown>;
|
|
14
|
+
body?: unknown;
|
|
15
|
+
json?: unknown;
|
|
16
|
+
qs?: Record<string, unknown>;
|
|
17
|
+
form?: Record<string, unknown>;
|
|
18
|
+
formData?: Record<string, unknown>;
|
|
19
|
+
encoding?: string | null;
|
|
20
|
+
timeout?: number;
|
|
21
|
+
followRedirect?: boolean;
|
|
22
|
+
followAllRedirects?: boolean;
|
|
23
|
+
maxRedirects?: number;
|
|
24
|
+
proxy?: string | {
|
|
25
|
+
host: string;
|
|
26
|
+
port: number;
|
|
27
|
+
auth?: {
|
|
28
|
+
username: string;
|
|
29
|
+
password: string;
|
|
30
|
+
};
|
|
31
|
+
protocol?: string;
|
|
32
|
+
};
|
|
33
|
+
auth?: {
|
|
34
|
+
user?: string;
|
|
35
|
+
username?: string;
|
|
36
|
+
pass?: string;
|
|
37
|
+
password?: string;
|
|
38
|
+
bearer?: string;
|
|
39
|
+
};
|
|
40
|
+
agent?: http.Agent;
|
|
41
|
+
agentOptions?: Record<string, unknown>;
|
|
42
|
+
rejectUnauthorized?: boolean;
|
|
43
|
+
strictSSL?: boolean;
|
|
44
|
+
gzip?: boolean;
|
|
45
|
+
[key: string]: unknown;
|
|
46
|
+
}
|
|
47
|
+
export interface OptionsWithUri extends RequestOptions {
|
|
48
|
+
uri: string | url.Url;
|
|
49
|
+
}
|
|
50
|
+
export interface OptionsWithUrl extends RequestOptions {
|
|
51
|
+
url: string | url.Url;
|
|
52
|
+
}
|
|
53
|
+
export interface RequestPromiseOptions extends RequestOptions {
|
|
54
|
+
simple?: boolean;
|
|
55
|
+
resolveWithFullResponse?: boolean;
|
|
56
|
+
transform?: (body: unknown, response: unknown, resolveWithFullResponse?: boolean) => unknown;
|
|
57
|
+
transform2xxOnly?: boolean;
|
|
58
|
+
}
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Local replacement types for the deprecated `request` and `request-promise-native` packages.
|
|
4
|
+
*
|
|
5
|
+
* These interfaces provide type compatibility for function signatures that previously
|
|
6
|
+
* referenced types from those packages. The actual HTTP implementation uses axios.
|
|
7
|
+
*/
|
|
8
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import { type IUserSettings } from "..";
|
|
2
|
+
/**
|
|
3
|
+
* Creates the user settings if they do not exist yet
|
|
4
|
+
*
|
|
5
|
+
* @export
|
|
6
|
+
*/
|
|
7
|
+
export declare function prepareUserSettings(): Promise<IUserSettings>;
|
|
8
|
+
/**
|
|
9
|
+
* Returns the encryption key which is used to encrypt
|
|
10
|
+
* the credentials.
|
|
11
|
+
*
|
|
12
|
+
* @export
|
|
13
|
+
* @returns
|
|
14
|
+
*/
|
|
15
|
+
export declare function getEncryptionKey(): Promise<string | undefined>;
|
|
16
|
+
/**
|
|
17
|
+
* Returns the instance ID
|
|
18
|
+
*
|
|
19
|
+
* @export
|
|
20
|
+
* @returns
|
|
21
|
+
*/
|
|
22
|
+
export declare function getInstanceId(): Promise<string>;
|
|
23
|
+
/**
|
|
24
|
+
* Adds/Overwrite the given settings in the currently
|
|
25
|
+
* saved user settings
|
|
26
|
+
*
|
|
27
|
+
* @export
|
|
28
|
+
* @param {IUserSettings} addSettings The settings to add/overwrite
|
|
29
|
+
* @param {string} [settingsPath] Optional settings file path
|
|
30
|
+
* @returns {Promise<IUserSettings>}
|
|
31
|
+
*/
|
|
32
|
+
export declare function addToUserSettings(addSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings>;
|
|
33
|
+
/**
|
|
34
|
+
* Writes a user settings file
|
|
35
|
+
*
|
|
36
|
+
* @export
|
|
37
|
+
* @param {IUserSettings} userSettings The settings to write
|
|
38
|
+
* @param {string} [settingsPath] Optional settings file path
|
|
39
|
+
* @returns {Promise<IUserSettings>}
|
|
40
|
+
*/
|
|
41
|
+
export declare function writeUserSettings(userSettings: IUserSettings, settingsPath?: string): Promise<IUserSettings>;
|
|
42
|
+
/**
|
|
43
|
+
* Returns the content of the user settings
|
|
44
|
+
*
|
|
45
|
+
* @export
|
|
46
|
+
* @returns {UserSettings}
|
|
47
|
+
*/
|
|
48
|
+
export declare function getUserSettings(settingsPath?: string, ignoreCache?: boolean): Promise<IUserSettings | undefined>;
|
|
49
|
+
/**
|
|
50
|
+
* Returns the path to the user settings
|
|
51
|
+
*
|
|
52
|
+
* @export
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
export declare function getUserSettingsPath(): string;
|
|
56
|
+
/**
|
|
57
|
+
* Retruns the path to the n8n folder in which all n8n
|
|
58
|
+
* related data gets saved
|
|
59
|
+
*
|
|
60
|
+
* @export
|
|
61
|
+
* @returns {string}
|
|
62
|
+
*/
|
|
63
|
+
export declare function getUserN8nFolderPath(): string;
|
|
64
|
+
/**
|
|
65
|
+
* Returns the path to the n8n user folder with the custom
|
|
66
|
+
* extensions like nodes and credentials
|
|
67
|
+
*
|
|
68
|
+
* @export
|
|
69
|
+
* @returns {string}
|
|
70
|
+
*/
|
|
71
|
+
export declare function getUserN8nFolderCustomExtensionPath(): string;
|
|
72
|
+
/**
|
|
73
|
+
* Returns the home folder path of the user if
|
|
74
|
+
* none can be found it falls back to the current
|
|
75
|
+
* working directory
|
|
76
|
+
*
|
|
77
|
+
* @export
|
|
78
|
+
* @returns {string}
|
|
79
|
+
*/
|
|
80
|
+
export declare function getUserHome(): string;
|