@redpanda-data/docs-extensions-and-macros 4.12.5 → 4.13.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.adoc +33 -1064
- package/bin/doc-tools-mcp.js +720 -0
- package/bin/doc-tools.js +1050 -50
- package/bin/mcp-tools/antora.js +153 -0
- package/bin/mcp-tools/cache.js +89 -0
- package/bin/mcp-tools/cloud-regions.js +127 -0
- package/bin/mcp-tools/content-review.js +196 -0
- package/bin/mcp-tools/crd-docs.js +153 -0
- package/bin/mcp-tools/frontmatter.js +138 -0
- package/bin/mcp-tools/generated-docs-review.js +887 -0
- package/bin/mcp-tools/helm-docs.js +152 -0
- package/bin/mcp-tools/index.js +245 -0
- package/bin/mcp-tools/job-queue.js +468 -0
- package/bin/mcp-tools/mcp-validation.js +266 -0
- package/bin/mcp-tools/metrics-docs.js +146 -0
- package/bin/mcp-tools/openapi.js +174 -0
- package/bin/mcp-tools/prompt-discovery.js +283 -0
- package/bin/mcp-tools/property-docs.js +157 -0
- package/bin/mcp-tools/rpcn-docs.js +113 -0
- package/bin/mcp-tools/rpk-docs.js +141 -0
- package/bin/mcp-tools/telemetry.js +211 -0
- package/bin/mcp-tools/utils.js +131 -0
- package/bin/mcp-tools/versions.js +168 -0
- package/cli-utils/convert-doc-links.js +1 -1
- package/cli-utils/github-token.js +58 -0
- package/cli-utils/self-managed-docs-branch.js +2 -2
- package/cli-utils/setup-mcp.js +313 -0
- package/docker-compose/25.1/transactions.md +1 -1
- package/docker-compose/transactions.md +1 -1
- package/extensions/DEVELOPMENT.adoc +464 -0
- package/extensions/README.adoc +124 -0
- package/extensions/REFERENCE.adoc +768 -0
- package/extensions/USER_GUIDE.adoc +339 -0
- package/extensions/generate-rp-connect-info.js +3 -4
- package/extensions/version-fetcher/get-latest-console-version.js +38 -27
- package/extensions/version-fetcher/get-latest-redpanda-helm-version-from-operator.js +1 -1
- package/extensions/version-fetcher/get-latest-redpanda-version.js +65 -54
- package/extensions/version-fetcher/retry-util.js +88 -0
- package/extensions/version-fetcher/set-latest-version.js +6 -3
- package/macros/DEVELOPMENT.adoc +377 -0
- package/macros/README.adoc +105 -0
- package/macros/REFERENCE.adoc +222 -0
- package/macros/USER_GUIDE.adoc +220 -0
- package/macros/rp-connect-components.js +6 -6
- package/package.json +12 -3
- package/tools/bundle-openapi.js +20 -10
- package/tools/cloud-regions/generate-cloud-regions.js +1 -1
- package/tools/fetch-from-github.js +18 -4
- package/tools/gen-rpk-ascii.py +3 -1
- package/tools/generate-cli-docs.js +325 -0
- package/tools/get-console-version.js +4 -2
- package/tools/get-redpanda-version.js +4 -2
- package/tools/metrics/metrics.py +19 -7
- package/tools/property-extractor/Makefile +7 -1
- package/tools/property-extractor/cloud_config.py +4 -4
- package/tools/property-extractor/constant_resolver.py +11 -11
- package/tools/property-extractor/property_extractor.py +18 -16
- package/tools/property-extractor/topic_property_extractor.py +2 -2
- package/tools/property-extractor/transformers.py +7 -7
- package/tools/property-extractor/type_definition_extractor.py +4 -4
- package/tools/redpanda-connect/README.adoc +1 -1
- package/tools/redpanda-connect/generate-rpcn-connector-docs.js +5 -3
|
@@ -0,0 +1,468 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MCP Tools - Background Job Queue
|
|
3
|
+
*
|
|
4
|
+
* Manages long-running documentation generation jobs with progress tracking
|
|
5
|
+
* and streaming updates via MCP notifications.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
const { spawn } = require('child_process');
|
|
9
|
+
const { randomUUID } = require('crypto');
|
|
10
|
+
const { DEFAULT_COMMAND_TIMEOUT } = require('./utils');
|
|
11
|
+
|
|
12
|
+
// Job status enum
|
|
13
|
+
const JobStatus = {
|
|
14
|
+
PENDING: 'pending',
|
|
15
|
+
RUNNING: 'running',
|
|
16
|
+
COMPLETED: 'completed',
|
|
17
|
+
FAILED: 'failed'
|
|
18
|
+
};
|
|
19
|
+
|
|
20
|
+
// In-memory job storage
|
|
21
|
+
const jobs = new Map();
|
|
22
|
+
|
|
23
|
+
// Reference to MCP server for sending notifications
|
|
24
|
+
let mcpServer = null;
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Initialize the job queue with MCP server reference
|
|
28
|
+
* @param {Object} server - MCP server instance
|
|
29
|
+
*/
|
|
30
|
+
function initializeJobQueue(server) {
|
|
31
|
+
mcpServer = server;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
/**
|
|
35
|
+
* Create a new background job
|
|
36
|
+
* @param {string} toolName - Name of the tool
|
|
37
|
+
* @param {string|Array} command - Command to execute (array format preferred for security)
|
|
38
|
+
* @param {Object} options - Execution options
|
|
39
|
+
* @returns {string} Job ID
|
|
40
|
+
*/
|
|
41
|
+
function createJob(toolName, command, options = {}) {
|
|
42
|
+
const jobId = randomUUID();
|
|
43
|
+
|
|
44
|
+
const job = {
|
|
45
|
+
id: jobId,
|
|
46
|
+
tool: toolName,
|
|
47
|
+
command,
|
|
48
|
+
status: JobStatus.PENDING,
|
|
49
|
+
createdAt: new Date().toISOString(),
|
|
50
|
+
startedAt: null,
|
|
51
|
+
completedAt: null,
|
|
52
|
+
progress: 0,
|
|
53
|
+
progressMessage: 'Job queued',
|
|
54
|
+
output: '',
|
|
55
|
+
error: null,
|
|
56
|
+
result: null
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
jobs.set(jobId, job);
|
|
60
|
+
|
|
61
|
+
// Start job execution immediately (can be changed to queue-based if needed)
|
|
62
|
+
executeJob(jobId, command, options);
|
|
63
|
+
|
|
64
|
+
return jobId;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/**
|
|
68
|
+
* Parse a command string into executable and arguments
|
|
69
|
+
* Handles basic quoted arguments safely
|
|
70
|
+
*/
|
|
71
|
+
function parseCommand(command) {
|
|
72
|
+
const args = [];
|
|
73
|
+
let current = '';
|
|
74
|
+
let inQuotes = false;
|
|
75
|
+
let quoteChar = '';
|
|
76
|
+
|
|
77
|
+
for (let i = 0; i < command.length; i++) {
|
|
78
|
+
const char = command[i];
|
|
79
|
+
|
|
80
|
+
if (!inQuotes && (char === '"' || char === "'")) {
|
|
81
|
+
inQuotes = true;
|
|
82
|
+
quoteChar = char;
|
|
83
|
+
} else if (inQuotes && char === quoteChar) {
|
|
84
|
+
inQuotes = false;
|
|
85
|
+
quoteChar = '';
|
|
86
|
+
} else if (!inQuotes && char === ' ') {
|
|
87
|
+
if (current.trim()) {
|
|
88
|
+
args.push(current.trim());
|
|
89
|
+
current = '';
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
current += char;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
if (current.trim()) {
|
|
97
|
+
args.push(current.trim());
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return args;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
/**
|
|
104
|
+
* Validate that an executable is safe to run
|
|
105
|
+
* Whitelist known safe executables
|
|
106
|
+
*/
|
|
107
|
+
function isValidExecutable(executable) {
|
|
108
|
+
const allowedExecutables = [
|
|
109
|
+
'npx',
|
|
110
|
+
'node',
|
|
111
|
+
'npm',
|
|
112
|
+
'yarn',
|
|
113
|
+
'doc-tools',
|
|
114
|
+
'helm-docs',
|
|
115
|
+
'crd-ref-docs',
|
|
116
|
+
'git',
|
|
117
|
+
'make',
|
|
118
|
+
'docker',
|
|
119
|
+
'timeout'
|
|
120
|
+
];
|
|
121
|
+
|
|
122
|
+
// Allow absolute paths to known tools in common locations
|
|
123
|
+
const allowedPaths = [
|
|
124
|
+
'/usr/bin/',
|
|
125
|
+
'/usr/local/bin/',
|
|
126
|
+
'/bin/',
|
|
127
|
+
'/opt/homebrew/bin/'
|
|
128
|
+
];
|
|
129
|
+
|
|
130
|
+
// Check if it's a whitelisted executable
|
|
131
|
+
if (allowedExecutables.includes(executable)) {
|
|
132
|
+
return true;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Check if it's an absolute path to a whitelisted location
|
|
136
|
+
if (executable.startsWith('/')) {
|
|
137
|
+
return allowedPaths.some(path =>
|
|
138
|
+
executable.startsWith(path) &&
|
|
139
|
+
allowedExecutables.some(exe => executable.endsWith(`/${exe}`) || executable.endsWith(`/${exe}.exe`))
|
|
140
|
+
);
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return false;
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Execute a job in the background
|
|
148
|
+
* @param {string} jobId - Job ID
|
|
149
|
+
* @param {string|Array} command - Command to execute (string will be parsed, array is preferred)
|
|
150
|
+
* @param {Object} options - Execution options
|
|
151
|
+
*/
|
|
152
|
+
async function executeJob(jobId, command, options = {}) {
|
|
153
|
+
const job = jobs.get(jobId);
|
|
154
|
+
if (!job) return;
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
job.status = JobStatus.RUNNING;
|
|
158
|
+
job.startedAt = new Date().toISOString();
|
|
159
|
+
updateJobProgress(jobId, 10, 'Starting execution...');
|
|
160
|
+
|
|
161
|
+
const cwd = options.cwd || process.cwd();
|
|
162
|
+
const timeout = options.timeout || DEFAULT_COMMAND_TIMEOUT;
|
|
163
|
+
|
|
164
|
+
let executable, args;
|
|
165
|
+
|
|
166
|
+
if (Array.isArray(command)) {
|
|
167
|
+
// Preferred: pre-parsed array [executable, ...args]
|
|
168
|
+
[executable, ...args] = command;
|
|
169
|
+
} else if (typeof command === 'string') {
|
|
170
|
+
// Legacy string command - use safer parsing
|
|
171
|
+
// Basic parsing that handles simple quoted arguments
|
|
172
|
+
const parsedArgs = parseCommand(command);
|
|
173
|
+
[executable, ...args] = parsedArgs;
|
|
174
|
+
} else {
|
|
175
|
+
throw new Error('Command must be a string or array');
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
// Validate executable to prevent injection
|
|
179
|
+
if (!isValidExecutable(executable)) {
|
|
180
|
+
throw new Error(`Invalid executable: ${executable}`);
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
const childProcess = spawn(executable, args, {
|
|
184
|
+
cwd,
|
|
185
|
+
shell: false, // Explicitly disable shell to prevent injection
|
|
186
|
+
stdio: ['ignore', 'pipe', 'pipe']
|
|
187
|
+
});
|
|
188
|
+
|
|
189
|
+
let stdout = '';
|
|
190
|
+
let stderr = '';
|
|
191
|
+
let timedOut = false;
|
|
192
|
+
|
|
193
|
+
// Set up timeout
|
|
194
|
+
const timeoutHandle = setTimeout(() => {
|
|
195
|
+
timedOut = true;
|
|
196
|
+
childProcess.kill('SIGTERM');
|
|
197
|
+
job.error = `Job timed out after ${timeout}ms`;
|
|
198
|
+
job.status = JobStatus.FAILED;
|
|
199
|
+
job.completedAt = new Date().toISOString();
|
|
200
|
+
job.result = {
|
|
201
|
+
success: false,
|
|
202
|
+
error: job.error,
|
|
203
|
+
stdout: stdout.trim(),
|
|
204
|
+
stderr: stderr.trim()
|
|
205
|
+
};
|
|
206
|
+
updateJobProgress(jobId, 100, 'Job timed out');
|
|
207
|
+
}, timeout);
|
|
208
|
+
|
|
209
|
+
// Capture stdout
|
|
210
|
+
childProcess.stdout.on('data', (data) => {
|
|
211
|
+
const chunk = data.toString();
|
|
212
|
+
stdout += chunk;
|
|
213
|
+
job.output = stdout;
|
|
214
|
+
|
|
215
|
+
// Parse progress from output if available
|
|
216
|
+
parseProgressFromOutput(jobId, chunk);
|
|
217
|
+
});
|
|
218
|
+
|
|
219
|
+
// Capture stderr
|
|
220
|
+
childProcess.stderr.on('data', (data) => {
|
|
221
|
+
stderr += data.toString();
|
|
222
|
+
});
|
|
223
|
+
|
|
224
|
+
// Handle completion
|
|
225
|
+
childProcess.on('close', (code) => {
|
|
226
|
+
clearTimeout(timeoutHandle);
|
|
227
|
+
|
|
228
|
+
// If job already timed out, don't overwrite the timeout error
|
|
229
|
+
if (timedOut) {
|
|
230
|
+
return;
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
job.completedAt = new Date().toISOString();
|
|
234
|
+
|
|
235
|
+
if (code === 0) {
|
|
236
|
+
job.status = JobStatus.COMPLETED;
|
|
237
|
+
job.progress = 100;
|
|
238
|
+
job.progressMessage = 'Completed successfully';
|
|
239
|
+
job.result = {
|
|
240
|
+
success: true,
|
|
241
|
+
output: stdout.trim(),
|
|
242
|
+
command
|
|
243
|
+
};
|
|
244
|
+
updateJobProgress(jobId, 100, 'Completed successfully');
|
|
245
|
+
} else {
|
|
246
|
+
job.status = JobStatus.FAILED;
|
|
247
|
+
job.error = stderr || `Command exited with code ${code}`;
|
|
248
|
+
job.result = {
|
|
249
|
+
success: false,
|
|
250
|
+
error: job.error,
|
|
251
|
+
stdout: stdout.trim(),
|
|
252
|
+
stderr: stderr.trim(),
|
|
253
|
+
exitCode: code
|
|
254
|
+
};
|
|
255
|
+
updateJobProgress(jobId, 100, `Failed with exit code ${code}`);
|
|
256
|
+
}
|
|
257
|
+
});
|
|
258
|
+
|
|
259
|
+
// Handle errors
|
|
260
|
+
childProcess.on('error', (err) => {
|
|
261
|
+
clearTimeout(timeoutHandle);
|
|
262
|
+
|
|
263
|
+
// If job already timed out, don't overwrite the timeout error
|
|
264
|
+
if (timedOut) {
|
|
265
|
+
return;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
job.status = JobStatus.FAILED;
|
|
269
|
+
job.error = err.message;
|
|
270
|
+
job.completedAt = new Date().toISOString();
|
|
271
|
+
job.result = {
|
|
272
|
+
success: false,
|
|
273
|
+
error: err.message
|
|
274
|
+
};
|
|
275
|
+
updateJobProgress(jobId, 100, `Error: ${err.message}`);
|
|
276
|
+
});
|
|
277
|
+
} catch (err) {
|
|
278
|
+
// Catch synchronous errors (validation failures, etc.)
|
|
279
|
+
// Record them on the job instead of throwing
|
|
280
|
+
job.status = JobStatus.FAILED;
|
|
281
|
+
job.error = err.message;
|
|
282
|
+
job.completedAt = new Date().toISOString();
|
|
283
|
+
job.result = {
|
|
284
|
+
success: false,
|
|
285
|
+
error: err.message
|
|
286
|
+
};
|
|
287
|
+
updateJobProgress(jobId, 100, `Error: ${err.message}`);
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/**
|
|
292
|
+
* Parse progress information from command output
|
|
293
|
+
* @param {string} jobId - Job ID
|
|
294
|
+
* @param {string} output - Output chunk
|
|
295
|
+
*/
|
|
296
|
+
function parseProgressFromOutput(jobId, output) {
|
|
297
|
+
const job = jobs.get(jobId);
|
|
298
|
+
if (!job) return;
|
|
299
|
+
|
|
300
|
+
// Look for common progress patterns
|
|
301
|
+
const patterns = [
|
|
302
|
+
// Percentage: "Progress: 45%", "45%", "[45%]"
|
|
303
|
+
/(?:progress[:\s]*)?(\d+)%/i,
|
|
304
|
+
// Step indicators: "Step 3/5", "3 of 5"
|
|
305
|
+
/(?:step\s+)?(\d+)\s*(?:\/|of)\s*(\d+)/i,
|
|
306
|
+
// Processing indicators: "Processing file 3 of 10"
|
|
307
|
+
/processing.*?(\d+)\s*of\s*(\d+)/i,
|
|
308
|
+
// Cloning/downloading indicators
|
|
309
|
+
/(?:cloning|downloading|fetching)/i,
|
|
310
|
+
// Building indicators
|
|
311
|
+
/(?:building|compiling|generating)/i,
|
|
312
|
+
// Analyzing indicators
|
|
313
|
+
/(?:analyzing|parsing|extracting)/i
|
|
314
|
+
];
|
|
315
|
+
|
|
316
|
+
for (const pattern of patterns) {
|
|
317
|
+
const match = output.match(pattern);
|
|
318
|
+
if (match) {
|
|
319
|
+
if (match.length === 2) {
|
|
320
|
+
// Percentage match
|
|
321
|
+
const percentage = parseInt(match[1]);
|
|
322
|
+
if (percentage >= 0 && percentage <= 100) {
|
|
323
|
+
updateJobProgress(jobId, percentage, output.trim().split('\n').pop());
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
} else if (match.length === 3) {
|
|
327
|
+
// Step match (for example, "3/5")
|
|
328
|
+
const current = parseInt(match[1]);
|
|
329
|
+
const total = parseInt(match[2]);
|
|
330
|
+
const percentage = Math.round((current / total) * 100);
|
|
331
|
+
updateJobProgress(jobId, percentage, output.trim().split('\n').pop());
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
// If we find action keywords but no percentage, estimate progress based on job runtime
|
|
338
|
+
const actionKeywords = ['cloning', 'downloading', 'fetching', 'building', 'compiling', 'generating', 'analyzing', 'parsing', 'extracting'];
|
|
339
|
+
const hasAction = actionKeywords.some(keyword => output.toLowerCase().includes(keyword));
|
|
340
|
+
|
|
341
|
+
if (hasAction && job.progress < 90) {
|
|
342
|
+
// Gradually increase progress for long-running jobs
|
|
343
|
+
const elapsed = new Date() - new Date(job.startedAt);
|
|
344
|
+
const estimatedTotal = DEFAULT_COMMAND_TIMEOUT;
|
|
345
|
+
const estimatedProgress = Math.min(90, Math.round((elapsed / estimatedTotal) * 100));
|
|
346
|
+
|
|
347
|
+
if (estimatedProgress > job.progress) {
|
|
348
|
+
updateJobProgress(jobId, estimatedProgress, output.trim().split('\n').pop());
|
|
349
|
+
}
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
|
|
353
|
+
/**
|
|
354
|
+
* Update job progress and send notification
|
|
355
|
+
* @param {string} jobId - Job ID
|
|
356
|
+
* @param {number} progress - Progress percentage (0-100)
|
|
357
|
+
* @param {string} message - Progress message
|
|
358
|
+
*/
|
|
359
|
+
function updateJobProgress(jobId, progress, message) {
|
|
360
|
+
const job = jobs.get(jobId);
|
|
361
|
+
if (!job) return;
|
|
362
|
+
|
|
363
|
+
job.progress = Math.min(100, Math.max(0, progress));
|
|
364
|
+
job.progressMessage = message;
|
|
365
|
+
|
|
366
|
+
// Send MCP notification if server is initialized
|
|
367
|
+
if (mcpServer) {
|
|
368
|
+
try {
|
|
369
|
+
mcpServer.notification({
|
|
370
|
+
method: 'notifications/progress',
|
|
371
|
+
params: {
|
|
372
|
+
progressToken: jobId,
|
|
373
|
+
progress: job.progress,
|
|
374
|
+
total: 100,
|
|
375
|
+
message: message
|
|
376
|
+
}
|
|
377
|
+
});
|
|
378
|
+
} catch (err) {
|
|
379
|
+
// Ignore notification errors - they shouldn't stop the job
|
|
380
|
+
console.error(`Failed to send progress notification: ${err.message}`);
|
|
381
|
+
}
|
|
382
|
+
}
|
|
383
|
+
}
|
|
384
|
+
|
|
385
|
+
/**
|
|
386
|
+
* Get job status
|
|
387
|
+
* @param {string} jobId - Job ID
|
|
388
|
+
* @returns {Object|null} Job status or null if not found
|
|
389
|
+
*/
|
|
390
|
+
function getJob(jobId) {
|
|
391
|
+
const job = jobs.get(jobId);
|
|
392
|
+
if (!job) return null;
|
|
393
|
+
|
|
394
|
+
return {
|
|
395
|
+
id: job.id,
|
|
396
|
+
tool: job.tool,
|
|
397
|
+
status: job.status,
|
|
398
|
+
progress: job.progress,
|
|
399
|
+
progressMessage: job.progressMessage,
|
|
400
|
+
createdAt: job.createdAt,
|
|
401
|
+
startedAt: job.startedAt,
|
|
402
|
+
completedAt: job.completedAt,
|
|
403
|
+
error: job.error,
|
|
404
|
+
result: job.result
|
|
405
|
+
};
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Get all jobs
|
|
410
|
+
* @param {Object} filter - Optional filter
|
|
411
|
+
* @returns {Array} List of jobs
|
|
412
|
+
*/
|
|
413
|
+
function listJobs(filter = {}) {
|
|
414
|
+
const jobList = Array.from(jobs.values());
|
|
415
|
+
|
|
416
|
+
return jobList
|
|
417
|
+
.filter(job => {
|
|
418
|
+
if (filter.status && job.status !== filter.status) return false;
|
|
419
|
+
if (filter.tool && job.tool !== filter.tool) return false;
|
|
420
|
+
return true;
|
|
421
|
+
})
|
|
422
|
+
.map(job => ({
|
|
423
|
+
id: job.id,
|
|
424
|
+
tool: job.tool,
|
|
425
|
+
status: job.status,
|
|
426
|
+
progress: job.progress,
|
|
427
|
+
progressMessage: job.progressMessage,
|
|
428
|
+
createdAt: job.createdAt,
|
|
429
|
+
startedAt: job.startedAt,
|
|
430
|
+
completedAt: job.completedAt
|
|
431
|
+
}))
|
|
432
|
+
.sort((a, b) => new Date(b.createdAt) - new Date(a.createdAt));
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
/**
|
|
436
|
+
* Clean up old completed/failed jobs
|
|
437
|
+
* @param {number} maxAge - Maximum age in milliseconds (default: 1 hour)
|
|
438
|
+
*/
|
|
439
|
+
function cleanupOldJobs(maxAge = 60 * 60 * 1000) {
|
|
440
|
+
const now = Date.now();
|
|
441
|
+
let removed = 0;
|
|
442
|
+
|
|
443
|
+
for (const [jobId, job] of jobs.entries()) {
|
|
444
|
+
if (job.status === JobStatus.COMPLETED || job.status === JobStatus.FAILED) {
|
|
445
|
+
const jobTime = new Date(job.completedAt || job.createdAt).getTime();
|
|
446
|
+
if (now - jobTime > maxAge) {
|
|
447
|
+
jobs.delete(jobId);
|
|
448
|
+
removed++;
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
return removed;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
// Clean up old jobs every 10 minutes
|
|
457
|
+
setInterval(() => {
|
|
458
|
+
cleanupOldJobs();
|
|
459
|
+
}, 10 * 60 * 1000);
|
|
460
|
+
|
|
461
|
+
module.exports = {
|
|
462
|
+
JobStatus,
|
|
463
|
+
initializeJobQueue,
|
|
464
|
+
createJob,
|
|
465
|
+
getJob,
|
|
466
|
+
listJobs,
|
|
467
|
+
cleanupOldJobs
|
|
468
|
+
};
|