@arela/uploader 1.0.15 ā 1.0.17
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/package.json +3 -1
- package/src/commands/IdentifyCommand.js +36 -1
- package/src/commands/PollWorkerCommand.js +414 -0
- package/src/commands/PropagateCommand.js +38 -1
- package/src/commands/PushCommand.js +34 -1
- package/src/commands/ScanCommand.js +37 -1
- package/src/commands/WorkerCommand.js +334 -0
- package/src/config/config.js +86 -4
- package/src/index.js +42 -0
- package/src/services/PipelineApiService.js +268 -0
|
@@ -28,9 +28,11 @@ export class ScanCommand {
|
|
|
28
28
|
* @param {boolean} options.countFirst - Count files first for percentage-based progress
|
|
29
29
|
* @param {string} options.api - API target: 'default', 'agencia', or 'cliente'
|
|
30
30
|
* @param {boolean} options.stream - Use streaming file discovery (default: true, use --no-stream to disable)
|
|
31
|
+
* @param {Function} options.onProgress - Optional callback for progress updates (percent, message)
|
|
31
32
|
*/
|
|
32
33
|
async execute(options = {}) {
|
|
33
34
|
const startTime = Date.now();
|
|
35
|
+
this.onProgress = options.onProgress || null;
|
|
34
36
|
|
|
35
37
|
try {
|
|
36
38
|
// Validate scan configuration
|
|
@@ -115,8 +117,20 @@ export class ScanCommand {
|
|
|
115
117
|
totalSize: 0,
|
|
116
118
|
};
|
|
117
119
|
|
|
118
|
-
|
|
120
|
+
// Report initial progress
|
|
121
|
+
this.#reportProgress(
|
|
122
|
+
0,
|
|
123
|
+
`Starting scan of ${registrations.length} directories`,
|
|
124
|
+
);
|
|
125
|
+
|
|
126
|
+
for (let i = 0; i < registrations.length; i++) {
|
|
127
|
+
const reg = registrations[i];
|
|
119
128
|
logger.info(`\nš Scanning: ${reg.directory.label || 'root'}`);
|
|
129
|
+
this.#reportProgress(
|
|
130
|
+
Math.round((i / registrations.length) * 100),
|
|
131
|
+
`Scanning directory ${i + 1}/${registrations.length}: ${reg.directory.label || 'root'}`,
|
|
132
|
+
);
|
|
133
|
+
|
|
120
134
|
const stats = await this.#streamScanDirectory(
|
|
121
135
|
reg.directory.path,
|
|
122
136
|
scanConfig,
|
|
@@ -141,6 +155,12 @@ export class ScanCommand {
|
|
|
141
155
|
const duration = ((Date.now() - startTime) / 1000).toFixed(2);
|
|
142
156
|
const filesPerSec = (totalStats.filesScanned / duration).toFixed(2);
|
|
143
157
|
|
|
158
|
+
// Report completion
|
|
159
|
+
this.#reportProgress(
|
|
160
|
+
100,
|
|
161
|
+
`Scan completed: ${totalStats.filesScanned} files in ${duration}s`,
|
|
162
|
+
);
|
|
163
|
+
|
|
144
164
|
logger.success('\nā
Scan completed successfully!');
|
|
145
165
|
logger.info(`\nš Scan Statistics:`);
|
|
146
166
|
logger.info(` Directories scanned: ${registrations.length}`);
|
|
@@ -649,6 +669,22 @@ export class ScanCommand {
|
|
|
649
669
|
|
|
650
670
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
|
651
671
|
}
|
|
672
|
+
|
|
673
|
+
/**
|
|
674
|
+
* Report progress to callback if available (for worker mode)
|
|
675
|
+
* @private
|
|
676
|
+
* @param {number} percent - Progress percentage (0-100)
|
|
677
|
+
* @param {string} message - Progress message
|
|
678
|
+
*/
|
|
679
|
+
#reportProgress(percent, message) {
|
|
680
|
+
if (this.onProgress && typeof this.onProgress === 'function') {
|
|
681
|
+
try {
|
|
682
|
+
this.onProgress(percent, message);
|
|
683
|
+
} catch (error) {
|
|
684
|
+
logger.debug(`Progress callback error: ${error.message}`);
|
|
685
|
+
}
|
|
686
|
+
}
|
|
687
|
+
}
|
|
652
688
|
}
|
|
653
689
|
|
|
654
690
|
export default new ScanCommand();
|
|
@@ -0,0 +1,334 @@
|
|
|
1
|
+
import { Worker } from 'bullmq';
|
|
2
|
+
|
|
3
|
+
import logger from '../services/LoggingService.js';
|
|
4
|
+
|
|
5
|
+
import appConfig from '../config/config.js';
|
|
6
|
+
import ErrorHandler from '../errors/ErrorHandler.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Worker Command Handler
|
|
10
|
+
* Runs arela-uploader as a BullMQ worker, listening for jobs from the API
|
|
11
|
+
*
|
|
12
|
+
* This enables UI-controlled execution of:
|
|
13
|
+
* - scan: Filesystem scanning and file cataloging
|
|
14
|
+
* - identify: Document type detection (pedimento-simplificado)
|
|
15
|
+
* - propagate: Spread arela_path to related files
|
|
16
|
+
* - push: Upload files to storage
|
|
17
|
+
*/
|
|
18
|
+
export class WorkerCommand {
|
|
19
|
+
constructor() {
|
|
20
|
+
this.errorHandler = new ErrorHandler(logger);
|
|
21
|
+
this.workers = new Map();
|
|
22
|
+
this.isShuttingDown = false;
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Execute the worker command
|
|
27
|
+
* @param {Object} options - Command options
|
|
28
|
+
* @param {string} options.queues - Comma-separated list of queues to listen to
|
|
29
|
+
* @param {number} options.concurrency - Number of concurrent jobs per queue
|
|
30
|
+
*/
|
|
31
|
+
async execute(options = {}) {
|
|
32
|
+
try {
|
|
33
|
+
// Load configuration
|
|
34
|
+
const redisConfig = appConfig.getRedisConfig();
|
|
35
|
+
const workerConfig = appConfig.getWorkerConfig();
|
|
36
|
+
|
|
37
|
+
// Determine which queues to listen to
|
|
38
|
+
const queueNames = options.queues
|
|
39
|
+
? options.queues.split(',').map((q) => q.trim())
|
|
40
|
+
: workerConfig.queues;
|
|
41
|
+
|
|
42
|
+
const concurrency =
|
|
43
|
+
parseInt(options.concurrency) || workerConfig.concurrency;
|
|
44
|
+
|
|
45
|
+
logger.info('š§ Starting Arela Worker');
|
|
46
|
+
logger.info(`š” Redis: ${redisConfig.host}:${redisConfig.port}`);
|
|
47
|
+
logger.info(`š Queues: ${queueNames.join(', ')}`);
|
|
48
|
+
logger.info(`ā” Concurrency: ${concurrency}`);
|
|
49
|
+
|
|
50
|
+
// Setup graceful shutdown handlers
|
|
51
|
+
this.#setupShutdownHandlers();
|
|
52
|
+
|
|
53
|
+
// Start workers for each queue
|
|
54
|
+
for (const queueName of queueNames) {
|
|
55
|
+
await this.#startWorker(queueName, redisConfig, concurrency);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
logger.success('\nā
Worker is running. Press Ctrl+C to stop.\n');
|
|
59
|
+
|
|
60
|
+
// Keep the process alive
|
|
61
|
+
await this.#keepAlive();
|
|
62
|
+
} catch (error) {
|
|
63
|
+
logger.error('ā Worker failed to start:', error.message);
|
|
64
|
+
throw error;
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Start a worker for a specific queue
|
|
70
|
+
* @private
|
|
71
|
+
*/
|
|
72
|
+
async #startWorker(queueName, redisConfig, concurrency) {
|
|
73
|
+
const processor = this.#getProcessor(queueName);
|
|
74
|
+
|
|
75
|
+
if (!processor) {
|
|
76
|
+
logger.warn(`ā ļø No processor found for queue: ${queueName}`);
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
const worker = new Worker(queueName, processor, {
|
|
81
|
+
connection: redisConfig,
|
|
82
|
+
concurrency,
|
|
83
|
+
lockDuration: appConfig.getWorkerConfig().lockDuration,
|
|
84
|
+
stalledInterval: appConfig.getWorkerConfig().stalledInterval,
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
// Event handlers
|
|
88
|
+
worker.on('completed', (job) => {
|
|
89
|
+
logger.success(`ā
Job ${job.id} completed on ${queueName}`);
|
|
90
|
+
});
|
|
91
|
+
|
|
92
|
+
worker.on('failed', (job, err) => {
|
|
93
|
+
logger.error(`ā Job ${job?.id} failed on ${queueName}: ${err.message}`);
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
worker.on('progress', (job, progress) => {
|
|
97
|
+
logger.info(`š Job ${job.id} progress: ${JSON.stringify(progress)}`);
|
|
98
|
+
});
|
|
99
|
+
|
|
100
|
+
worker.on('error', (err) => {
|
|
101
|
+
logger.error(`š“ Worker error on ${queueName}: ${err.message}`);
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
this.workers.set(queueName, worker);
|
|
105
|
+
logger.info(` ā Worker started for queue: ${queueName}`);
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
/**
|
|
109
|
+
* Get the processor function for a queue
|
|
110
|
+
* @private
|
|
111
|
+
*/
|
|
112
|
+
#getProcessor(queueName) {
|
|
113
|
+
const processors = {
|
|
114
|
+
'pipeline-scan': this.#processScanJob.bind(this),
|
|
115
|
+
'pipeline-identify': this.#processIdentifyJob.bind(this),
|
|
116
|
+
'pipeline-propagate': this.#processPropagateJob.bind(this),
|
|
117
|
+
'pipeline-push': this.#processPushJob.bind(this),
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
return processors[queueName];
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Process a scan job
|
|
125
|
+
* @private
|
|
126
|
+
*/
|
|
127
|
+
async #processScanJob(job) {
|
|
128
|
+
logger.info(`š Processing scan job ${job.id}`);
|
|
129
|
+
|
|
130
|
+
const { ScanCommand } = await import('./ScanCommand.js');
|
|
131
|
+
const scanCommand = new ScanCommand();
|
|
132
|
+
|
|
133
|
+
// Create progress callback for the job
|
|
134
|
+
const onProgress = (percent, message) => {
|
|
135
|
+
job.updateProgress({ percent, message });
|
|
136
|
+
job.log(message);
|
|
137
|
+
};
|
|
138
|
+
|
|
139
|
+
try {
|
|
140
|
+
// Extract options from job data
|
|
141
|
+
const options = {
|
|
142
|
+
api: job.data.apiTarget || 'default',
|
|
143
|
+
countFirst: job.data.countFirst || false,
|
|
144
|
+
stream: job.data.stream !== false,
|
|
145
|
+
// Pass progress callback
|
|
146
|
+
onProgress,
|
|
147
|
+
};
|
|
148
|
+
|
|
149
|
+
// Override scan config if provided in job
|
|
150
|
+
if (job.data.scanConfig) {
|
|
151
|
+
this.#overrideScanConfig(job.data.scanConfig);
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
const result = await scanCommand.execute(options);
|
|
155
|
+
return { success: true, ...result };
|
|
156
|
+
} catch (error) {
|
|
157
|
+
logger.error(`Scan job ${job.id} failed: ${error.message}`);
|
|
158
|
+
throw error;
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
/**
|
|
163
|
+
* Process an identify job
|
|
164
|
+
* @private
|
|
165
|
+
*/
|
|
166
|
+
async #processIdentifyJob(job) {
|
|
167
|
+
logger.info(`š Processing identify job ${job.id}`);
|
|
168
|
+
|
|
169
|
+
const { IdentifyCommand } = await import('./IdentifyCommand.js');
|
|
170
|
+
const identifyCommand = new IdentifyCommand();
|
|
171
|
+
|
|
172
|
+
const onProgress = (percent, message) => {
|
|
173
|
+
job.updateProgress({ percent, message });
|
|
174
|
+
job.log(message);
|
|
175
|
+
};
|
|
176
|
+
|
|
177
|
+
try {
|
|
178
|
+
const options = {
|
|
179
|
+
api: job.data.apiTarget || 'default',
|
|
180
|
+
batchSize: job.data.batchSize || 100,
|
|
181
|
+
showStats: job.data.showStats || false,
|
|
182
|
+
onProgress,
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (job.data.scanConfig) {
|
|
186
|
+
this.#overrideScanConfig(job.data.scanConfig);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const result = await identifyCommand.execute(options);
|
|
190
|
+
return { success: true, ...result };
|
|
191
|
+
} catch (error) {
|
|
192
|
+
logger.error(`Identify job ${job.id} failed: ${error.message}`);
|
|
193
|
+
throw error;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Process a propagate job
|
|
199
|
+
* @private
|
|
200
|
+
*/
|
|
201
|
+
async #processPropagateJob(job) {
|
|
202
|
+
logger.info(`š Processing propagate job ${job.id}`);
|
|
203
|
+
|
|
204
|
+
const { PropagateCommand } = await import('./PropagateCommand.js');
|
|
205
|
+
|
|
206
|
+
const onProgress = (percent, message) => {
|
|
207
|
+
job.updateProgress({ percent, message });
|
|
208
|
+
job.log(message);
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
try {
|
|
212
|
+
const options = {
|
|
213
|
+
api: job.data.apiTarget || 'default',
|
|
214
|
+
batchSize: job.data.batchSize || 50,
|
|
215
|
+
showStats: job.data.showStats || false,
|
|
216
|
+
onProgress,
|
|
217
|
+
};
|
|
218
|
+
|
|
219
|
+
if (job.data.scanConfig) {
|
|
220
|
+
this.#overrideScanConfig(job.data.scanConfig);
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
const propagateCommand = new PropagateCommand(options);
|
|
224
|
+
const result = await propagateCommand.execute();
|
|
225
|
+
return { success: true, ...result };
|
|
226
|
+
} catch (error) {
|
|
227
|
+
logger.error(`Propagate job ${job.id} failed: ${error.message}`);
|
|
228
|
+
throw error;
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Process a push job
|
|
234
|
+
* @private
|
|
235
|
+
*/
|
|
236
|
+
async #processPushJob(job) {
|
|
237
|
+
logger.info(`š¤ Processing push job ${job.id}`);
|
|
238
|
+
|
|
239
|
+
const { PushCommand } = await import('./PushCommand.js');
|
|
240
|
+
const pushCommand = new PushCommand();
|
|
241
|
+
|
|
242
|
+
const onProgress = (percent, message) => {
|
|
243
|
+
job.updateProgress({ percent, message });
|
|
244
|
+
job.log(message);
|
|
245
|
+
};
|
|
246
|
+
|
|
247
|
+
try {
|
|
248
|
+
const options = {
|
|
249
|
+
api: job.data.apiTarget || 'default',
|
|
250
|
+
scanApi: job.data.scanApi,
|
|
251
|
+
pushApi: job.data.pushApi,
|
|
252
|
+
batchSize: job.data.batchSize || 100,
|
|
253
|
+
uploadBatchSize: job.data.uploadBatchSize || 10,
|
|
254
|
+
rfcs: job.data.rfcs,
|
|
255
|
+
years: job.data.years,
|
|
256
|
+
folderStructure: job.data.folderStructure,
|
|
257
|
+
autoOrganize: job.data.autoOrganize !== false,
|
|
258
|
+
showStats: job.data.showStats || false,
|
|
259
|
+
onProgress,
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
if (job.data.scanConfig) {
|
|
263
|
+
this.#overrideScanConfig(job.data.scanConfig);
|
|
264
|
+
}
|
|
265
|
+
|
|
266
|
+
const result = await pushCommand.execute(options);
|
|
267
|
+
return { success: true, ...result };
|
|
268
|
+
} catch (error) {
|
|
269
|
+
logger.error(`Push job ${job.id} failed: ${error.message}`);
|
|
270
|
+
throw error;
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Override scan configuration from job data
|
|
276
|
+
* @private
|
|
277
|
+
*/
|
|
278
|
+
#overrideScanConfig(scanConfig) {
|
|
279
|
+
// Temporarily override environment variables for this job
|
|
280
|
+
if (scanConfig.companySlug) {
|
|
281
|
+
process.env.ARELA_COMPANY_SLUG = scanConfig.companySlug;
|
|
282
|
+
}
|
|
283
|
+
if (scanConfig.serverId) {
|
|
284
|
+
process.env.ARELA_SERVER_ID = scanConfig.serverId;
|
|
285
|
+
}
|
|
286
|
+
if (scanConfig.basePath) {
|
|
287
|
+
process.env.UPLOAD_BASE_PATH = scanConfig.basePath;
|
|
288
|
+
process.env.ARELA_BASE_PATH_LABEL = scanConfig.basePath;
|
|
289
|
+
}
|
|
290
|
+
if (scanConfig.directoryLevel !== undefined) {
|
|
291
|
+
process.env.SCAN_DIRECTORY_LEVEL = String(scanConfig.directoryLevel);
|
|
292
|
+
}
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
/**
|
|
296
|
+
* Setup graceful shutdown handlers
|
|
297
|
+
* @private
|
|
298
|
+
*/
|
|
299
|
+
#setupShutdownHandlers() {
|
|
300
|
+
const shutdown = async (signal) => {
|
|
301
|
+
if (this.isShuttingDown) return;
|
|
302
|
+
this.isShuttingDown = true;
|
|
303
|
+
|
|
304
|
+
logger.info(`\nš Received ${signal}. Shutting down workers...`);
|
|
305
|
+
|
|
306
|
+
// Close all workers gracefully
|
|
307
|
+
const closePromises = [];
|
|
308
|
+
for (const [queueName, worker] of this.workers) {
|
|
309
|
+
logger.info(` ā³ Closing worker: ${queueName}`);
|
|
310
|
+
closePromises.push(worker.close());
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
await Promise.all(closePromises);
|
|
314
|
+
logger.info('ā
All workers closed gracefully');
|
|
315
|
+
process.exit(0);
|
|
316
|
+
};
|
|
317
|
+
|
|
318
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
319
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
320
|
+
}
|
|
321
|
+
|
|
322
|
+
/**
|
|
323
|
+
* Keep the process alive while workers are running
|
|
324
|
+
* @private
|
|
325
|
+
*/
|
|
326
|
+
async #keepAlive() {
|
|
327
|
+
return new Promise((resolve) => {
|
|
328
|
+
// This promise never resolves, keeping the process alive
|
|
329
|
+
// The process will exit via the shutdown handlers
|
|
330
|
+
});
|
|
331
|
+
}
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
export default new WorkerCommand();
|
package/src/config/config.js
CHANGED
|
@@ -22,6 +22,8 @@ class Config {
|
|
|
22
22
|
this.performance = this.#loadPerformanceConfig();
|
|
23
23
|
this.logging = this.#loadLoggingConfig();
|
|
24
24
|
this.watch = this.#loadWatchConfig();
|
|
25
|
+
this.redis = this.#loadRedisConfig();
|
|
26
|
+
this.worker = this.#loadWorkerConfig();
|
|
25
27
|
}
|
|
26
28
|
|
|
27
29
|
/**
|
|
@@ -34,10 +36,10 @@ class Config {
|
|
|
34
36
|
const __dirname = path.dirname(__filename);
|
|
35
37
|
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
36
38
|
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf-8'));
|
|
37
|
-
return packageJson.version || '1.0.
|
|
39
|
+
return packageJson.version || '1.0.17';
|
|
38
40
|
} catch (error) {
|
|
39
41
|
console.warn('ā ļø Could not read package.json version, using fallback');
|
|
40
|
-
return '1.0.
|
|
42
|
+
return '1.0.17';
|
|
41
43
|
}
|
|
42
44
|
}
|
|
43
45
|
|
|
@@ -78,6 +80,11 @@ class Config {
|
|
|
78
80
|
token:
|
|
79
81
|
process.env.ARELA_API_CLIENTE_TOKEN || process.env.ARELA_API_TOKEN,
|
|
80
82
|
},
|
|
83
|
+
// Dynamic targets - add more as needed via ARELA_API_{NAME}_URL
|
|
84
|
+
ktj: {
|
|
85
|
+
baseUrl: process.env.ARELA_API_KTJ_URL || process.env.ARELA_API_URL,
|
|
86
|
+
token: process.env.ARELA_API_KTJ_TOKEN || process.env.ARELA_API_TOKEN,
|
|
87
|
+
},
|
|
81
88
|
},
|
|
82
89
|
// Current active target (can be changed at runtime)
|
|
83
90
|
activeTarget: process.env.ARELA_API_TARGET || 'default',
|
|
@@ -118,7 +125,7 @@ class Config {
|
|
|
118
125
|
* @param {string} target - API target: 'default', 'agencia', or 'cliente'
|
|
119
126
|
*/
|
|
120
127
|
setApiTarget(target) {
|
|
121
|
-
const validTargets = ['default', 'agencia', 'cliente'];
|
|
128
|
+
const validTargets = ['default', 'agencia', 'cliente', 'ktj'];
|
|
122
129
|
if (!validTargets.includes(target.toLowerCase())) {
|
|
123
130
|
throw new Error(
|
|
124
131
|
`Invalid API target '${target}'. Must be one of: ${validTargets.join(', ')}`,
|
|
@@ -135,7 +142,7 @@ class Config {
|
|
|
135
142
|
* @param {string} targetTarget - Target API target (for writing data)
|
|
136
143
|
*/
|
|
137
144
|
setCrossTenantTargets(sourceTarget, targetTarget) {
|
|
138
|
-
const validTargets = ['default', 'agencia', 'cliente'];
|
|
145
|
+
const validTargets = ['default', 'agencia', 'cliente', 'ktj'];
|
|
139
146
|
|
|
140
147
|
if (!validTargets.includes(sourceTarget.toLowerCase())) {
|
|
141
148
|
throw new Error(
|
|
@@ -496,6 +503,81 @@ class Config {
|
|
|
496
503
|
return !!(this.supabase.url && this.supabase.key && this.supabase.bucket);
|
|
497
504
|
}
|
|
498
505
|
|
|
506
|
+
/**
|
|
507
|
+
* Load Redis configuration for BullMQ worker mode
|
|
508
|
+
* @private
|
|
509
|
+
*/
|
|
510
|
+
#loadRedisConfig() {
|
|
511
|
+
return {
|
|
512
|
+
host: process.env.REDIS_HOST || 'localhost',
|
|
513
|
+
port: parseInt(process.env.REDIS_PORT) || 6379,
|
|
514
|
+
password: process.env.REDIS_PASSWORD || undefined,
|
|
515
|
+
db: parseInt(process.env.REDIS_DB) || 0,
|
|
516
|
+
maxRetriesPerRequest: null, // Required by BullMQ
|
|
517
|
+
};
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
/**
|
|
521
|
+
* Load worker configuration
|
|
522
|
+
* @private
|
|
523
|
+
*/
|
|
524
|
+
#loadWorkerConfig() {
|
|
525
|
+
const queues = process.env.WORKER_QUEUES?.split(',')
|
|
526
|
+
.map((q) => q.trim())
|
|
527
|
+
.filter(Boolean) || [
|
|
528
|
+
'pipeline-scan',
|
|
529
|
+
'pipeline-identify',
|
|
530
|
+
'pipeline-propagate',
|
|
531
|
+
'pipeline-push',
|
|
532
|
+
];
|
|
533
|
+
|
|
534
|
+
return {
|
|
535
|
+
queues,
|
|
536
|
+
concurrency: parseInt(process.env.WORKER_CONCURRENCY) || 1,
|
|
537
|
+
lockDuration: parseInt(process.env.WORKER_LOCK_DURATION) || 30000,
|
|
538
|
+
stalledInterval: parseInt(process.env.WORKER_STALLED_INTERVAL) || 30000,
|
|
539
|
+
// HTTP Polling mode configuration
|
|
540
|
+
poll: {
|
|
541
|
+
enabled: process.env.WORKER_POLL_MODE === 'true',
|
|
542
|
+
interval: parseInt(process.env.WORKER_POLL_INTERVAL) || 5000,
|
|
543
|
+
apiTarget: process.env.WORKER_POLL_API_TARGET || 'agencia',
|
|
544
|
+
},
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
|
|
548
|
+
/**
|
|
549
|
+
* Get Redis configuration
|
|
550
|
+
* @returns {Object} Redis connection options for ioredis/BullMQ
|
|
551
|
+
*/
|
|
552
|
+
getRedisConfig() {
|
|
553
|
+
return this.redis;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
/**
|
|
557
|
+
* Get worker configuration
|
|
558
|
+
* @returns {Object} Worker settings
|
|
559
|
+
*/
|
|
560
|
+
getWorkerConfig() {
|
|
561
|
+
return this.worker;
|
|
562
|
+
}
|
|
563
|
+
|
|
564
|
+
/**
|
|
565
|
+
* Get the server ID for this instance
|
|
566
|
+
* Used for worker job assignment in poll mode
|
|
567
|
+
* @returns {string|null} Server ID or null if not configured
|
|
568
|
+
*/
|
|
569
|
+
getServerId() {
|
|
570
|
+
return process.env.ARELA_SERVER_ID || null;
|
|
571
|
+
}
|
|
572
|
+
|
|
573
|
+
/**
|
|
574
|
+
* Check if worker mode is available (Redis configured)
|
|
575
|
+
* @returns {boolean}
|
|
576
|
+
*/
|
|
577
|
+
isWorkerModeAvailable() {
|
|
578
|
+
return !!(this.redis.host && this.redis.port);
|
|
579
|
+
}
|
|
580
|
+
|
|
499
581
|
/**
|
|
500
582
|
* Validate configuration for the requested mode
|
|
501
583
|
* @param {boolean} forceSupabase - Whether to force Supabase mode
|
package/src/index.js
CHANGED
|
@@ -2,11 +2,13 @@
|
|
|
2
2
|
import { Command } from 'commander';
|
|
3
3
|
|
|
4
4
|
import identifyCommand from './commands/IdentifyCommand.js';
|
|
5
|
+
import pollWorkerCommand from './commands/PollWorkerCommand.js';
|
|
5
6
|
import PropagateCommand from './commands/PropagateCommand.js';
|
|
6
7
|
import PushCommand from './commands/PushCommand.js';
|
|
7
8
|
import scanCommand from './commands/ScanCommand.js';
|
|
8
9
|
import UploadCommand from './commands/UploadCommand.js';
|
|
9
10
|
import watchCommand from './commands/WatchCommand.js';
|
|
11
|
+
import workerCommand from './commands/WorkerCommand.js';
|
|
10
12
|
import appConfig from './config/config.js';
|
|
11
13
|
import ErrorHandler from './errors/ErrorHandler.js';
|
|
12
14
|
import logger from './services/LoggingService.js';
|
|
@@ -522,6 +524,46 @@ class ArelaUploaderCLI {
|
|
|
522
524
|
}
|
|
523
525
|
});
|
|
524
526
|
|
|
527
|
+
// ============================================================================
|
|
528
|
+
// WORKER MODE - BullMQ job processor
|
|
529
|
+
// ============================================================================
|
|
530
|
+
|
|
531
|
+
// Worker command - process jobs from BullMQ queues
|
|
532
|
+
this.program
|
|
533
|
+
.command('worker')
|
|
534
|
+
.description('š§ Run as BullMQ worker to process pipeline jobs from UI')
|
|
535
|
+
.option(
|
|
536
|
+
'--queues <queues>',
|
|
537
|
+
'Comma-separated queues to listen to (default: all pipeline queues)',
|
|
538
|
+
)
|
|
539
|
+
.option(
|
|
540
|
+
'-c, --concurrency <number>',
|
|
541
|
+
'Number of concurrent jobs per queue',
|
|
542
|
+
'1',
|
|
543
|
+
)
|
|
544
|
+
.option(
|
|
545
|
+
'--poll',
|
|
546
|
+
'Use HTTP polling mode instead of BullMQ (for environments without Redis)',
|
|
547
|
+
)
|
|
548
|
+
.option(
|
|
549
|
+
'--api <target>',
|
|
550
|
+
'API target for polling (e.g., agencia, cliente, ktj)',
|
|
551
|
+
'agencia',
|
|
552
|
+
)
|
|
553
|
+
.action(async (options) => {
|
|
554
|
+
try {
|
|
555
|
+
if (options.poll) {
|
|
556
|
+
// HTTP polling mode - for Windows Server or environments without Redis
|
|
557
|
+
await pollWorkerCommand.execute(options);
|
|
558
|
+
} else {
|
|
559
|
+
// BullMQ mode - default for environments with Redis
|
|
560
|
+
await workerCommand.execute(options);
|
|
561
|
+
}
|
|
562
|
+
} catch (error) {
|
|
563
|
+
this.errorHandler.handleFatalError(error, { command: 'worker' });
|
|
564
|
+
}
|
|
565
|
+
});
|
|
566
|
+
|
|
525
567
|
// Version command (already handled by program.version())
|
|
526
568
|
|
|
527
569
|
// Help command
|