@cloudnux/cli 0.11.0 → 0.15.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.
@@ -1,564 +0,0 @@
1
- import fs from "fs/promises";
2
- import path from "path";
3
- import chalk from "chalk";
4
- import consola from "consola";
5
- import logSymbols from "log-symbols";
6
- import Fastify, { FastifyInstance } from "fastify";
7
-
8
- import {
9
- createEventContext,
10
- createScheduleContext,
11
- } from "@anubis/cloud-provider";
12
- <% for(const component of components){ %>
13
- import {
14
- entries as <%=component%>Entries,
15
- registeredEventHandlers as <%=component%>EventHandlers
16
- }
17
- from "./<%=component%>/entrypoint.ts" <% }
18
- %>
19
-
20
- function logEntryURL(method: string, routePath: string){
21
- const bgcolors = {
22
- PUT : chalk.bold.bgYellow.white,
23
- POST : chalk.bold.bgGreen.white,
24
- DELETE : chalk.bold.bgRed.white,
25
- GET : chalk.bold.bgBlue.white,
26
- PATCH : chalk.bold.bgGreenBright.white
27
- };
28
- const colors = {
29
- PUT : chalk.bold.yellow,
30
- POST : chalk.bold.green,
31
- DELETE : chalk.bold.red,
32
- GET : chalk.bold.blue,
33
- PATCH : chalk.bold.greenBright
34
- };
35
-
36
- const prefix = new Array(Math.floor((6 - method.length) / 2)).fill(" ").join("");
37
- const postfix = new Array(6 - method.length - prefix.length).fill(" ").join("");
38
-
39
- consola.log(
40
- logSymbols.info + bgcolors[method](`[${prefix}${method}${postfix}]`) + colors[method](` http://localhost:3000${routePath}`)
41
- );
42
- }
43
-
44
- async function queue(app: FastifyInstance) {
45
-
46
- consola.log(chalk.underline(`\n\rUrls you have for`, chalk.bold(`Queues`)));
47
-
48
- const queues = {};
49
-
50
- const config = {
51
- batchSize: 10, // Max number of messages to collect before processing
52
- batchWindowMs: 500, // Max wait time before processing collected messages
53
- maxRetries: 3, // Maximum retry attempts
54
- parallel: true, // Process messages in parallel or sequentially
55
- maxConcurrent: 5, // Max concurrent processing if parallel is true
56
- retryBackoff: true, // Use exponential backoff for retries
57
- persistence: {
58
- enabled: true, // Enable persistence
59
- directory: './.develop/queue-data', // Directory to store queue data
60
- saveInterval: 60000, // Save queues every minute (in ms)
61
- //TODO: we have issue with esbuild watch if saveonShutdown is true
62
- saveOnShutdown: false, // Save queues on process shutdown
63
- loadOnStartup: true // Load queues on startup
64
- }
65
- };
66
-
67
- <%_ for(const component of components) { _%>
68
- Object.entries(<%=component%>EventHandlers).forEach(([queueName, handler]) => {
69
- queues[queueName] = {
70
- handler,
71
- incoming: [],
72
- processing: [],
73
- dlq: [],
74
- timeoutId: null, // Batch window timeout ID
75
- processingBatch: false, // Flag to prevent concurrent batch processing
76
- activeProcessing: 0 // Counter for active concurrent processing
77
- }
78
- });
79
- <%_ } _%>
80
-
81
- // Initialize persistence
82
- if (config.persistence.enabled) {
83
- await initializePersistence();
84
- }
85
-
86
- // Main batch processing function
87
- async function processBatch(queueName) {
88
- const queueService = queues[queueName];
89
-
90
- // Clear timeout to prevent double processing
91
- if (queueService.timeoutId) {
92
- clearTimeout(queueService.timeoutId);
93
- queueService.timeoutId = null;
94
- }
95
-
96
- // If already processing, reschedule
97
- if (queueService.processingBatch) {
98
- scheduleProcessing(queueName);
99
- return;
100
- }
101
-
102
- try {
103
- queueService.processingBatch = true;
104
-
105
- // Move messages from incoming to processing (up to batchSize)
106
- const messagesToProcess = queueService.incoming.splice(0, config.batchSize);
107
-
108
- if (messagesToProcess.length === 0) {
109
- queueService.processingBatch = false;
110
- return;
111
- }
112
-
113
- // Add to processing queue
114
- queueService.processing.push(...messagesToProcess);
115
-
116
- // Process sequentially
117
- for (const message of messagesToProcess) {
118
- await processMessage(queueName, message);
119
- }
120
-
121
- if (queueService.incoming.length > 0) {
122
- scheduleProcessing(queueName, 0); // Process immediately
123
- }
124
-
125
- // Save queue state after batch processing
126
- if (config.persistence.enabled) {
127
- saveQueueState(queueName);
128
- }
129
- }
130
- finally {
131
- queueService.processingBatch = false;
132
- }
133
- }
134
-
135
- // Process an individual message
136
- async function processMessage(queueName, message) {
137
- const queueService = queues[queueName];
138
-
139
- try {
140
- const context = createEventContext(
141
- message.payload,
142
- message.attributes,
143
- message.id,
144
- message.timestamp,
145
- message.attempts);
146
-
147
- await queueService.handler(context);
148
-
149
- if(context.response.status === "error") {
150
- throw new Error(context.response.body);
151
- }
152
-
153
- // Remove from processing queue on success
154
- queueService.processing = queueService.processing.filter(
155
- msg => msg.id !== message.id
156
- );
157
-
158
- console.log(`Successfully processed message ${message.id} in queue ${queueName} with response `,context.response.body);
159
- } catch (error) {
160
- console.error(`Error processing message ${message.id} in queue ${queueName}:`, error);
161
-
162
- if (message.attempts >= config.maxRetries) {
163
- // Move to DLQ after max retries
164
- queueService.dlq.push({
165
- ...message,
166
- error: error.message,
167
- failedAt: new Date()
168
- });
169
-
170
- // Remove from processing
171
- queueService.processing = queueService.processing.filter(
172
- msg => msg.id !== message.id
173
- );
174
-
175
- // Save DLQ state after message moves to DLQ
176
- if (config.persistence.enabled) {
177
- saveQueueState(queueName);
178
- }
179
- } else {
180
- // Increment attempt counter
181
- const index = queueService.processing.findIndex(m => m.id === message.id);
182
- if (index >= 0) {
183
- queueService.processing[index].attempts += 1;
184
-
185
- // Add backoff delay based on attempt count
186
- if (config.retryBackoff) {
187
- const delayMs = Math.pow(2, queueService.processing[index].attempts) * 100;
188
- queueService.processing[index].nextAttempt = new Date(Date.now() + delayMs);
189
-
190
- // Schedule retry after backoff
191
- setTimeout(() => {
192
- processMessage(queueName, queueService.processing[index]);
193
- }, delayMs);
194
- } else {
195
- // Retry immediately if no backoff
196
- processMessage(queueName, queueService.processing[index]);
197
- }
198
- }
199
- }
200
- }
201
- }
202
-
203
- // Schedule batch processing after a delay
204
- function scheduleProcessing(queueName, overrideDelay = null) {
205
- const queueService = queues[queueName];
206
-
207
- if (!queueService.timeoutId) {
208
- const delay = overrideDelay !== null ? overrideDelay : config.batchWindowMs;
209
- queueService.timeoutId = setTimeout(() => {
210
- processBatch(queueName);
211
- }, delay);
212
- }
213
- }
214
-
215
- // Initialize persistence system
216
- async function initializePersistence() {
217
- try {
218
- // Create persistence directory if it doesn't exist
219
- await fs.mkdir(config.persistence.directory, { recursive: true });
220
-
221
- // Load queue data if enabled
222
- if (config.persistence.loadOnStartup) {
223
- await loadAllQueueStates();
224
- }
225
-
226
- // Set up interval to regularly save queue states
227
- if (config.persistence.saveInterval > 0) {
228
- setInterval(saveAllQueueStates, config.persistence.saveInterval);
229
- }
230
-
231
- // Set up process shutdown handler
232
- if (config.persistence.saveOnShutdown) {
233
- process.on('SIGINT', async () => {
234
- console.log('Saving queue state before shutdown...');
235
- await saveAllQueueStates();
236
- process.exit(0);
237
- });
238
-
239
- process.on('SIGTERM', async () => {
240
- console.log('Saving queue state before shutdown...');
241
- await saveAllQueueStates();
242
- process.exit(0);
243
- });
244
- }
245
-
246
- console.log(`Queue persistence initialized: ${config.persistence.directory}`);
247
- } catch (error) {
248
- console.error('Failed to initialize queue persistence:', error);
249
- }
250
- }
251
-
252
- // Save state for all queues
253
- async function saveAllQueueStates() {
254
- try {
255
- for (const queueName of Object.keys(queues)) {
256
- await saveQueueState(queueName);
257
- }
258
- console.log(`All queue states saved to ${config.persistence.directory}`);
259
- } catch (error) {
260
- console.error('Failed to save all queue states:', error);
261
- }
262
- }
263
-
264
- // Save state for a specific queue
265
- async function saveQueueState(queueName) {
266
- try {
267
- const queueService = queues[queueName];
268
- if (!queueService) return;
269
-
270
- const queueData = {
271
- incoming: queueService.incoming,
272
- processing: queueService.processing,
273
- dlq: queueService.dlq,
274
- savedAt: new Date()
275
- };
276
-
277
- // Use a temporary file to avoid corruption if the process crashes during write
278
- const queueFilePath = path.join(config.persistence.directory, `${queueName}.json`);
279
- const tempFilePath = path.join(config.persistence.directory, `${queueName}.temp.json`);
280
-
281
- // Write to temporary file first
282
- await fs.writeFile(tempFilePath, JSON.stringify(queueData, null, 2), 'utf8');
283
-
284
- // Rename temporary file to the actual file (atomic operation)
285
- await fs.rename(tempFilePath, queueFilePath);
286
-
287
- console.log(`Queue state saved: ${queueName}`);
288
- } catch (error) {
289
- console.error(`Failed to save queue state for ${queueName}:`, error);
290
- }
291
- }
292
-
293
- async function loadAllQueueStates() {
294
- try {
295
- const files = await fs.readdir(config.persistence.directory);
296
- const queueFiles = files.filter(file => file.endsWith('.json') && !file.includes('.temp.'));
297
-
298
- for (const file of queueFiles) {
299
- const queueName = path.basename(file, '.json');
300
- await loadQueueState(queueName);
301
- }
302
-
303
- console.log('All queue states loaded');
304
- } catch (error) {
305
- console.error('Failed to load queue states:', error);
306
- }
307
- }
308
-
309
- // Load state for a specific queue
310
- async function loadQueueState(queueName) {
311
- try {
312
- // Skip if queue doesn't exist
313
- if (!queues[queueName]) {
314
- console.warn(`Skipping load for non-existent queue: ${queueName}`);
315
- return;
316
- }
317
-
318
- const queueFilePath = path.join(config.persistence.directory, `${queueName}.json`);
319
-
320
- try {
321
- const data = await fs.readFile(queueFilePath, 'utf8');
322
- const queueData = JSON.parse(data);
323
-
324
- // Restore date objects (JSON.parse doesn't restore dates)
325
- queueData.incoming.forEach(msg => {
326
- msg.timestamp = new Date(msg.timestamp);
327
- if (msg.nextAttempt) msg.nextAttempt = new Date(msg.nextAttempt);
328
- });
329
-
330
- queueData.processing.forEach(msg => {
331
- msg.timestamp = new Date(msg.timestamp);
332
- if (msg.nextAttempt) msg.nextAttempt = new Date(msg.nextAttempt);
333
- });
334
-
335
- queueData.dlq.forEach(msg => {
336
- msg.timestamp = new Date(msg.timestamp);
337
- if (msg.failedAt) msg.failedAt = new Date(msg.failedAt);
338
- if (msg.nextAttempt) msg.nextAttempt = new Date(msg.nextAttempt);
339
- });
340
-
341
- // Restore queue state
342
- queues[queueName].incoming = queueData.incoming;
343
- queues[queueName].processing = queueData.processing;
344
- queues[queueName].dlq = queueData.dlq;
345
-
346
- console.log(`Queue state loaded: ${queueName} (saved at ${queueData.savedAt})`);
347
-
348
- // Schedule processing for any messages that were in flight
349
- if (queues[queueName].incoming.length > 0) {
350
- scheduleProcessing(queueName);
351
- }
352
-
353
- // Re-process any messages that were in the processing queue
354
- if (queues[queueName].processing.length > 0) {
355
- for (const message of [...queues[queueName].processing]) {
356
- // Skip processing if message has exceeded max retries
357
- if (message.attempts >= config.maxRetries) continue;
358
-
359
- // Process with a slight delay to avoid overwhelming the system on startup
360
- setTimeout(() => {
361
- processMessage(queueName, message);
362
- }, 100 * (Math.random() * 10));
363
- }
364
- }
365
- } catch (error) {
366
- if (error.code === 'ENOENT') {
367
- console.log(`No saved state found for queue: ${queueName}`);
368
- } else {
369
- throw error;
370
- }
371
- }
372
- } catch (error) {
373
- console.error(`Failed to load queue state for ${queueName}:`, error);
374
- }
375
- }
376
-
377
- logEntryURL("GET","/queues/dashboard");
378
- app.get("/dashboard", function(request, reply) {
379
- const summary = {};
380
-
381
- for (const [queueName, queue] of Object.entries(queues)) {
382
- summary[queueName] = {
383
- incoming: queue.incoming.length,
384
- processing: queue.processing.length,
385
- dlq: queue.dlq.length,
386
- isProcessing: queue.processingBatch,
387
- activeProcessing: queue.activeProcessing,
388
- configuration: {
389
- batchSize: config.batchSize,
390
- batchWindowMs: config.batchWindowMs,
391
- parallel: config.parallel,
392
- maxConcurrent: config.maxConcurrent
393
- }
394
- };
395
- }
396
-
397
- return reply
398
- .status(200)
399
- .send(summary);
400
- });
401
-
402
- for(const q of Object.keys(queues))
403
- logEntryURL("GET",`/queues/${q}`);
404
- app.get("/:queue", function(request, reply) {
405
-
406
- const queueName = request.params.queue;
407
-
408
- if(!queues[queueName]) {
409
- return reply
410
- .status(404)
411
- .send({ error: "Queue not found" });
412
- }
413
-
414
- const queue = queues[queueName];
415
- console.log("[queue]", queue.dlq);
416
- return reply
417
- .status(200)
418
- .send({
419
- stats: {
420
- incoming: queue.incoming.length,
421
- processing: queue.processing.length,
422
- dlq: queue.dlq.length,
423
- isProcessing: queue.processingBatch,
424
- activeProcessing: queue.activeProcessing
425
- },
426
- messages: {
427
- incoming: queue.incoming,
428
- processing: queue.processing,
429
- dlq: queue.dlq
430
- }
431
- });
432
- });
433
-
434
- for(const q of Object.keys(queues))
435
- logEntryURL("POST",`/queues/${q}`);
436
- app.post("/:queue", function(request, reply) {
437
-
438
- const queueName = request.params.queue;
439
-
440
- if(!queues[queueName]) {
441
- return reply
442
- .status(404)
443
- .send({ error: "Queue not found" });
444
- }
445
-
446
- const id = Date.now().toString() + Math.random().toString(36).substring(2, 7);
447
-
448
- const message = {
449
- id,
450
- timestamp: new Date(),
451
- attempts: 0,
452
- payload: request.body,
453
- attributes: request.headers,
454
- };
455
-
456
- queues[queueName].incoming.push(message);
457
-
458
- // Schedule processing if not already scheduled
459
- scheduleProcessing(queueName);
460
-
461
- // Trigger immediate processing if we've reached batch size
462
- if (queues[queueName].incoming.length >= config.batchSize) {
463
- clearTimeout(queues[queueName].timeoutId);
464
- queues[queueName].timeoutId = null;
465
- setImmediate(() => processBatch(queueName));
466
- }
467
-
468
- // Save queue state after adding new message
469
- if (config.persistence.enabled) {
470
- saveQueueState(queueName);
471
- }
472
-
473
- return reply
474
- .status(200)
475
- .send({ id, queueName });
476
- });
477
-
478
- for(const q of Object.keys(queues))
479
- logEntryURL("GET",`/queues/${q}/process-dlq`);
480
- app.get("/:queue/process-dlq", function(request, reply) {
481
-
482
- const queueName = request.params.queue;
483
-
484
- if(!queues[queueName]) {
485
- return reply
486
- .status(404)
487
- .send({ error: "Queue not found" });
488
- }
489
-
490
- const queue = queues[queueName];
491
- // Get count of messages before processing
492
- const dlqCount = queue.dlq.length;
493
-
494
- if (dlqCount === 0) {
495
- return reply
496
- .status(200)
497
- .send({
498
- status: "success",
499
- message: "No messages in DLQ to process",
500
- processed: 0
501
- });
502
- }
503
-
504
- // Reset attempt counters and move messages from DLQ to incoming queue
505
- const movedMessages = queue.dlq.map(message => ({
506
- ...message,
507
- attempts: 0, // Reset attempts counter
508
- error: undefined, // Clear error message
509
- failedAt: undefined, // Clear failure timestamp
510
- reprocessed: true, // Mark as reprocessed for tracking
511
- originalId: message.id, // Keep original ID for reference
512
- id: Date.now().toString() + Math.random().toString(36).substring(2, 7) // Create new ID
513
- }));
514
-
515
- queue.incoming.push(...movedMessages);
516
- queue.dlq = []; // Clear the DLQ
517
-
518
- // Schedule processing if not already scheduled
519
- scheduleProcessing(queueName);
520
-
521
- // Save queue state after adding new message
522
- if (config.persistence.enabled) {
523
- saveQueueState(queueName);
524
- }
525
-
526
- return reply
527
- .status(200)
528
- .send({
529
- status: "success",
530
- message: `Moved ${dlqCount} messages from DLQ to processing queue`,
531
- processed: dlqCount
532
- });
533
- });
534
- }
535
-
536
- const fastify = Fastify({
537
- maxParamLength : 1000,
538
- logger: {
539
- transport: {
540
- target: 'pino-pretty',
541
- options: {
542
- colorize: true,
543
- translateTime: 'HH:MM:ss Z',
544
- ignore: 'pid,hostname',
545
- },
546
- }
547
- }
548
- });
549
-
550
- // Register the fastify-raw-body plugin
551
- fastify.register(require('fastify-raw-body'), {
552
- field: 'rawBody',
553
- encoding: 'utf8',
554
- });
555
-
556
-
557
- <% for(const component of components){ %>
558
- fastify.register(<%=component%>Entries, { prefix: "api" });
559
- <%}%>
560
-
561
- fastify.register(queue, {prefix : "queues"});
562
-
563
- fastify.listen({ port: 3000, host : "::" }).then((address) => {
564
- });
@@ -1,7 +0,0 @@
1
- app.route({
2
- method: "GET",
3
- url: `/event/<%= name %>`,
4
- handler: async function (...args) {
5
- await eventBrokerHandler(src["<%= entry.handler %>"], ..args);
6
- }
7
- });
@@ -1,11 +0,0 @@
1
- <%# Template generated a single endpoint entry
2
- input:
3
- - entry : entry to be rendered
4
- %>
5
- app.route({
6
- method: "<%= entry.trigger.options.method %>" as any,
7
- url: `/http<%= $$convertRouteParamstoFastifyRouteTemplate(entry.trigger.options.route) %>`,
8
- handler: async (request, reply) => {
9
- await httpHandler(src["<%= entry.handler %>"], request, reply);
10
- }
11
- });
@@ -1,7 +0,0 @@
1
- app.route({
2
- method: "GET",
3
- url: `/schedule/<%= name %>`,
4
- handler: async function (...args) {
5
- await scheduleHandler(src["<%= entry.handler %>"], ...args);
6
- }
7
- });