@arela/uploader 1.0.15 → 1.0.16
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 +391 -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 +249 -0
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
import { Agent } from 'http';
|
|
2
|
+
import { Agent as HttpsAgent } from 'https';
|
|
3
|
+
import fetch from 'node-fetch';
|
|
4
|
+
|
|
5
|
+
import appConfig from '../config/config.js';
|
|
6
|
+
import logger from './LoggingService.js';
|
|
7
|
+
|
|
8
|
+
/**
|
|
9
|
+
* Pipeline API Service
|
|
10
|
+
* Handles HTTP communication with the API for pipeline job polling.
|
|
11
|
+
* Used by the poll worker mode as an alternative to Redis/BullMQ.
|
|
12
|
+
*/
|
|
13
|
+
export class PipelineApiService {
|
|
14
|
+
/**
|
|
15
|
+
* @param {string|null} apiTarget - API target for polling: 'agencia' recommended
|
|
16
|
+
*/
|
|
17
|
+
constructor(apiTarget = 'agencia') {
|
|
18
|
+
this.apiTarget = apiTarget;
|
|
19
|
+
const apiConfig = appConfig.getApiConfig(apiTarget);
|
|
20
|
+
this.baseUrl = apiConfig.baseUrl;
|
|
21
|
+
this.token = apiConfig.token;
|
|
22
|
+
|
|
23
|
+
// Get API connection settings
|
|
24
|
+
const maxApiConnections = parseInt(process.env.MAX_API_CONNECTIONS) || 5;
|
|
25
|
+
const connectionTimeout =
|
|
26
|
+
parseInt(process.env.API_CONNECTION_TIMEOUT) || 30000;
|
|
27
|
+
|
|
28
|
+
// Retry configuration
|
|
29
|
+
this.maxRetries = parseInt(process.env.API_MAX_RETRIES) || 3;
|
|
30
|
+
this.retryDelay = parseInt(process.env.API_RETRY_DELAY) || 1000;
|
|
31
|
+
|
|
32
|
+
// Initialize HTTP agents for connection pooling
|
|
33
|
+
this.httpAgent = new Agent({
|
|
34
|
+
keepAlive: true,
|
|
35
|
+
keepAliveMsecs: 30000,
|
|
36
|
+
maxSockets: maxApiConnections,
|
|
37
|
+
timeout: connectionTimeout,
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
this.httpsAgent = new HttpsAgent({
|
|
41
|
+
keepAlive: true,
|
|
42
|
+
keepAliveMsecs: 30000,
|
|
43
|
+
maxSockets: maxApiConnections,
|
|
44
|
+
timeout: connectionTimeout,
|
|
45
|
+
});
|
|
46
|
+
|
|
47
|
+
logger.debug(
|
|
48
|
+
`🔗 Pipeline API Service configured for ${apiTarget} → ${this.baseUrl}`,
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Get the appropriate HTTP agent based on URL protocol
|
|
54
|
+
* @private
|
|
55
|
+
*/
|
|
56
|
+
#getAgent(url) {
|
|
57
|
+
return url.startsWith('https://') ? this.httpsAgent : this.httpAgent;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Make an HTTP request with retry logic
|
|
62
|
+
* @private
|
|
63
|
+
*/
|
|
64
|
+
async #request(method, endpoint, body = null, retries = 0) {
|
|
65
|
+
const url = `${this.baseUrl}${endpoint}`;
|
|
66
|
+
|
|
67
|
+
try {
|
|
68
|
+
const options = {
|
|
69
|
+
method,
|
|
70
|
+
headers: {
|
|
71
|
+
'Content-Type': 'application/json',
|
|
72
|
+
'x-api-key': this.token,
|
|
73
|
+
},
|
|
74
|
+
agent: this.#getAgent(url),
|
|
75
|
+
};
|
|
76
|
+
|
|
77
|
+
if (body) {
|
|
78
|
+
options.body = JSON.stringify(body);
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const response = await fetch(url, options);
|
|
82
|
+
|
|
83
|
+
if (!response.ok) {
|
|
84
|
+
// Handle non-2xx responses
|
|
85
|
+
if (response.status === 404) {
|
|
86
|
+
return null; // Resource not found is not an error for polling
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const errorText = await response.text();
|
|
90
|
+
throw new Error(
|
|
91
|
+
`HTTP ${response.status}: ${response.statusText} - ${errorText}`,
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
// Check if response has content
|
|
96
|
+
const contentType = response.headers.get('content-type');
|
|
97
|
+
if (contentType && contentType.includes('application/json')) {
|
|
98
|
+
return response.json();
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
return { success: true };
|
|
102
|
+
} catch (error) {
|
|
103
|
+
// Retry on network errors
|
|
104
|
+
if (retries < this.maxRetries && this.#isRetryableError(error)) {
|
|
105
|
+
logger.debug(
|
|
106
|
+
`⚠️ Retrying request (${retries + 1}/${this.maxRetries}): ${error.message}`,
|
|
107
|
+
);
|
|
108
|
+
await this.#sleep(this.retryDelay * (retries + 1));
|
|
109
|
+
return this.#request(method, endpoint, body, retries + 1);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
throw error;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
/**
|
|
117
|
+
* Check if error is retryable
|
|
118
|
+
* @private
|
|
119
|
+
*/
|
|
120
|
+
#isRetryableError(error) {
|
|
121
|
+
const retryableCodes = [
|
|
122
|
+
'ECONNRESET',
|
|
123
|
+
'ETIMEDOUT',
|
|
124
|
+
'ECONNREFUSED',
|
|
125
|
+
'ENOTFOUND',
|
|
126
|
+
'EAI_AGAIN',
|
|
127
|
+
];
|
|
128
|
+
return (
|
|
129
|
+
retryableCodes.includes(error.code) ||
|
|
130
|
+
(error.message && error.message.includes('timeout'))
|
|
131
|
+
);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Sleep for specified milliseconds
|
|
136
|
+
* @private
|
|
137
|
+
*/
|
|
138
|
+
#sleep(ms) {
|
|
139
|
+
return new Promise((resolve) => setTimeout(resolve, ms));
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// =====================
|
|
143
|
+
// Public API Methods
|
|
144
|
+
// =====================
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Get the next available job for this server
|
|
148
|
+
* @param {string} serverId - Server identifier
|
|
149
|
+
* @returns {Promise<Object|null>} Job data or null if no jobs available
|
|
150
|
+
*/
|
|
151
|
+
async getNextJob(serverId) {
|
|
152
|
+
try {
|
|
153
|
+
const result = await this.#request(
|
|
154
|
+
'GET',
|
|
155
|
+
`/api/uploader/pipeline/worker/jobs/next?serverId=${encodeURIComponent(serverId)}`,
|
|
156
|
+
);
|
|
157
|
+
return result;
|
|
158
|
+
} catch (error) {
|
|
159
|
+
logger.error(`❌ Failed to get next job: ${error.message}`);
|
|
160
|
+
return null;
|
|
161
|
+
}
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Update job progress
|
|
166
|
+
* @param {string} jobId - Job UUID
|
|
167
|
+
* @param {number} progress - Progress percentage (0-100)
|
|
168
|
+
* @param {string} message - Progress message
|
|
169
|
+
* @param {string} currentFile - Current file being processed
|
|
170
|
+
* @param {string} currentStep - Current pipeline step
|
|
171
|
+
* @returns {Promise<boolean>} Success status
|
|
172
|
+
*/
|
|
173
|
+
async updateProgress(jobId, progress, message, currentFile, currentStep) {
|
|
174
|
+
try {
|
|
175
|
+
await this.#request(
|
|
176
|
+
'PATCH',
|
|
177
|
+
`/api/uploader/pipeline/worker/jobs/${jobId}/progress`,
|
|
178
|
+
{
|
|
179
|
+
progress,
|
|
180
|
+
message,
|
|
181
|
+
currentFile,
|
|
182
|
+
currentStep,
|
|
183
|
+
},
|
|
184
|
+
);
|
|
185
|
+
return true;
|
|
186
|
+
} catch (error) {
|
|
187
|
+
logger.warn(`⚠️ Failed to update progress: ${error.message}`);
|
|
188
|
+
return false;
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
|
|
192
|
+
/**
|
|
193
|
+
* Mark job as completed
|
|
194
|
+
* @param {string} jobId - Job UUID
|
|
195
|
+
* @param {Object} result - Result data
|
|
196
|
+
* @returns {Promise<boolean>} Success status
|
|
197
|
+
*/
|
|
198
|
+
async completeJob(jobId, result = {}) {
|
|
199
|
+
try {
|
|
200
|
+
await this.#request(
|
|
201
|
+
'PATCH',
|
|
202
|
+
`/api/uploader/pipeline/worker/jobs/${jobId}/complete`,
|
|
203
|
+
{
|
|
204
|
+
status: 'completed',
|
|
205
|
+
result,
|
|
206
|
+
},
|
|
207
|
+
);
|
|
208
|
+
logger.success(`✅ Job ${jobId} marked as completed`);
|
|
209
|
+
return true;
|
|
210
|
+
} catch (error) {
|
|
211
|
+
logger.error(`❌ Failed to complete job: ${error.message}`);
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
/**
|
|
217
|
+
* Mark job as failed
|
|
218
|
+
* @param {string} jobId - Job UUID
|
|
219
|
+
* @param {string} errorMessage - Error description
|
|
220
|
+
* @returns {Promise<boolean>} Success status
|
|
221
|
+
*/
|
|
222
|
+
async failJob(jobId, errorMessage) {
|
|
223
|
+
try {
|
|
224
|
+
await this.#request(
|
|
225
|
+
'PATCH',
|
|
226
|
+
`/api/uploader/pipeline/worker/jobs/${jobId}/complete`,
|
|
227
|
+
{
|
|
228
|
+
status: 'failed',
|
|
229
|
+
error: errorMessage,
|
|
230
|
+
},
|
|
231
|
+
);
|
|
232
|
+
logger.error(`❌ Job ${jobId} marked as failed: ${errorMessage}`);
|
|
233
|
+
return true;
|
|
234
|
+
} catch (error) {
|
|
235
|
+
logger.error(`❌ Failed to mark job as failed: ${error.message}`);
|
|
236
|
+
return false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Close connections and cleanup
|
|
242
|
+
*/
|
|
243
|
+
destroy() {
|
|
244
|
+
this.httpAgent.destroy();
|
|
245
|
+
this.httpsAgent.destroy();
|
|
246
|
+
}
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
export default PipelineApiService;
|