glide-mq 0.4.0 → 0.4.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.
@@ -0,0 +1,474 @@
1
+ #!/usr/bin/env node
2
+ import express from 'express';
3
+ import { Queue, Worker, QueueEvents } from 'glide-mq';
4
+ import chalk from 'chalk';
5
+
6
+ const app = express();
7
+ const PORT = 3000;
8
+
9
+ // Connection config
10
+ const connection = {
11
+ addresses: [{ host: 'localhost', port: 6379 }],
12
+ clusterMode: false,
13
+ };
14
+
15
+ // Queue registry for dashboard
16
+ const queueRegistry = [
17
+ 'orders',
18
+ 'payments',
19
+ 'inventory',
20
+ 'shipping',
21
+ 'notifications',
22
+ 'analytics',
23
+ 'recommendations',
24
+ 'reports',
25
+ 'dead-letter',
26
+ 'priority-tasks'
27
+ ];
28
+
29
+ // Create queue instances for dashboard
30
+ const queues = queueRegistry.reduce((acc, name) => {
31
+ acc[name] = new Queue(name, { connection });
32
+ return acc;
33
+ }, {} as Record<string, Queue>);
34
+
35
+ // Middleware for CORS (if dashboard is on different port)
36
+ app.use((req, res, next) => {
37
+ res.header('Access-Control-Allow-Origin', '*');
38
+ res.header('Access-Control-Allow-Headers', 'Origin, X-Requested-With, Content-Type, Accept');
39
+ res.header('Access-Control-Allow-Methods', 'GET, POST, PUT, DELETE, OPTIONS');
40
+ next();
41
+ });
42
+
43
+ app.use(express.json());
44
+
45
+ // Dashboard API endpoints
46
+ app.get('/api/queues', async (req, res) => {
47
+ const queueData = await Promise.all(
48
+ Object.entries(queues).map(async ([name, queue]) => {
49
+ const counts = await queue.getJobCounts();
50
+ const metrics = await queue.getMetrics('completed', 'failed');
51
+
52
+ return {
53
+ name,
54
+ counts,
55
+ metrics: {
56
+ completed: metrics.completed || { count: 0, prevCount: 0 },
57
+ failed: metrics.failed || { count: 0, prevCount: 0 }
58
+ },
59
+ isPaused: await queue.isPaused()
60
+ };
61
+ })
62
+ );
63
+
64
+ res.json(queueData);
65
+ });
66
+
67
+ // Get specific queue details
68
+ app.get('/api/queues/:name', async (req, res) => {
69
+ const { name } = req.params;
70
+ const queue = queues[name];
71
+
72
+ if (!queue) {
73
+ return res.status(404).json({ error: 'Queue not found' });
74
+ }
75
+
76
+ const counts = await queue.getJobCounts();
77
+ const jobs = await queue.getJobs(
78
+ ['waiting', 'active', 'completed', 'failed', 'delayed'],
79
+ 0,
80
+ 20
81
+ );
82
+
83
+ res.json({
84
+ name,
85
+ counts,
86
+ jobs: jobs.map(job => ({
87
+ id: job.id,
88
+ name: job.name,
89
+ data: job.data,
90
+ opts: job.opts,
91
+ progress: job.progress,
92
+ attemptsMade: job.attemptsMade,
93
+ failedReason: job.failedReason,
94
+ finishedOn: job.finishedOn,
95
+ processedOn: job.processedOn,
96
+ timestamp: job.timestamp
97
+ }))
98
+ });
99
+ });
100
+
101
+ // Get specific job
102
+ app.get('/api/queues/:queueName/jobs/:jobId', async (req, res) => {
103
+ const { queueName, jobId } = req.params;
104
+ const queue = queues[queueName];
105
+
106
+ if (!queue) {
107
+ return res.status(404).json({ error: 'Queue not found' });
108
+ }
109
+
110
+ const job = await queue.getJob(jobId);
111
+ if (!job) {
112
+ return res.status(404).json({ error: 'Job not found' });
113
+ }
114
+
115
+ const state = await job.getState();
116
+ const logs = await job.getLogs();
117
+
118
+ res.json({
119
+ id: job.id,
120
+ name: job.name,
121
+ data: job.data,
122
+ opts: job.opts,
123
+ progress: job.progress,
124
+ attemptsMade: job.attemptsMade,
125
+ failedReason: job.failedReason,
126
+ finishedOn: job.finishedOn,
127
+ processedOn: job.processedOn,
128
+ timestamp: job.timestamp,
129
+ state,
130
+ logs: logs.logs,
131
+ returnvalue: job.returnvalue
132
+ });
133
+ });
134
+
135
+ // Pause queue
136
+ app.post('/api/queues/:name/pause', async (req, res) => {
137
+ const { name } = req.params;
138
+ const queue = queues[name];
139
+
140
+ if (!queue) {
141
+ return res.status(404).json({ error: 'Queue not found' });
142
+ }
143
+
144
+ await queue.pause();
145
+ res.json({ success: true, message: `Queue ${name} paused` });
146
+ });
147
+
148
+ // Resume queue
149
+ app.post('/api/queues/:name/resume', async (req, res) => {
150
+ const { name } = req.params;
151
+ const queue = queues[name];
152
+
153
+ if (!queue) {
154
+ return res.status(404).json({ error: 'Queue not found' });
155
+ }
156
+
157
+ await queue.resume();
158
+ res.json({ success: true, message: `Queue ${name} resumed` });
159
+ });
160
+
161
+ // Add job to queue
162
+ app.post('/api/queues/:name/jobs', async (req, res) => {
163
+ const { name } = req.params;
164
+ const { jobName, data, opts } = req.body;
165
+ const queue = queues[name];
166
+
167
+ if (!queue) {
168
+ return res.status(404).json({ error: 'Queue not found' });
169
+ }
170
+
171
+ const job = await queue.add(jobName || 'manual-job', data || {}, opts || {});
172
+ res.json({ success: true, jobId: job.id });
173
+ });
174
+
175
+ // Retry failed job
176
+ app.post('/api/queues/:queueName/jobs/:jobId/retry', async (req, res) => {
177
+ const { queueName, jobId } = req.params;
178
+ const queue = queues[queueName];
179
+
180
+ if (!queue) {
181
+ return res.status(404).json({ error: 'Queue not found' });
182
+ }
183
+
184
+ const job = await queue.getJob(jobId);
185
+ if (!job) {
186
+ return res.status(404).json({ error: 'Job not found' });
187
+ }
188
+
189
+ await job.retry();
190
+ res.json({ success: true, message: `Job ${jobId} retried` });
191
+ });
192
+
193
+ // Remove job
194
+ app.delete('/api/queues/:queueName/jobs/:jobId', async (req, res) => {
195
+ const { queueName, jobId } = req.params;
196
+ const queue = queues[queueName];
197
+
198
+ if (!queue) {
199
+ return res.status(404).json({ error: 'Queue not found' });
200
+ }
201
+
202
+ const job = await queue.getJob(jobId);
203
+ if (!job) {
204
+ return res.status(404).json({ error: 'Job not found' });
205
+ }
206
+
207
+ await job.remove();
208
+ res.json({ success: true, message: `Job ${jobId} removed` });
209
+ });
210
+
211
+ // Drain queue
212
+ app.post('/api/queues/:name/drain', async (req, res) => {
213
+ const { name } = req.params;
214
+ const queue = queues[name];
215
+
216
+ if (!queue) {
217
+ return res.status(404).json({ error: 'Queue not found' });
218
+ }
219
+
220
+ await queue.drain();
221
+ res.json({ success: true, message: `Queue ${name} drained` });
222
+ });
223
+
224
+ // Obliterate queue
225
+ app.post('/api/queues/:name/obliterate', async (req, res) => {
226
+ const { name } = req.params;
227
+ const queue = queues[name];
228
+
229
+ if (!queue) {
230
+ return res.status(404).json({ error: 'Queue not found' });
231
+ }
232
+
233
+ await queue.obliterate({ force: true });
234
+ res.json({ success: true, message: `Queue ${name} obliterated` });
235
+ });
236
+
237
+ // SSE endpoint for real-time updates
238
+ app.get('/api/events', (req, res) => {
239
+ res.writeHead(200, {
240
+ 'Content-Type': 'text/event-stream',
241
+ 'Cache-Control': 'no-cache',
242
+ 'Connection': 'keep-alive'
243
+ });
244
+
245
+ // Setup event listeners for all queues
246
+ const eventSources = queueRegistry.map(name => {
247
+ const events = new QueueEvents(name, { connection });
248
+
249
+ events.on('added', ({ jobId }) => {
250
+ res.write(`data: ${JSON.stringify({ queue: name, event: 'added', jobId })}\n\n`);
251
+ });
252
+
253
+ events.on('completed', ({ jobId, returnvalue }) => {
254
+ res.write(`data: ${JSON.stringify({ queue: name, event: 'completed', jobId, returnvalue })}\n\n`);
255
+ });
256
+
257
+ events.on('failed', ({ jobId, failedReason }) => {
258
+ res.write(`data: ${JSON.stringify({ queue: name, event: 'failed', jobId, failedReason })}\n\n`);
259
+ });
260
+
261
+ events.on('progress', ({ jobId, data }) => {
262
+ res.write(`data: ${JSON.stringify({ queue: name, event: 'progress', jobId, progress: data })}\n\n`);
263
+ });
264
+
265
+ events.on('stalled', ({ jobId }) => {
266
+ res.write(`data: ${JSON.stringify({ queue: name, event: 'stalled', jobId })}\n\n`);
267
+ });
268
+
269
+ return events;
270
+ });
271
+
272
+ // Heartbeat
273
+ const heartbeat = setInterval(() => {
274
+ res.write(`data: ${JSON.stringify({ event: 'heartbeat', timestamp: Date.now() })}\n\n`);
275
+ }, 30000);
276
+
277
+ // Cleanup on close
278
+ req.on('close', () => {
279
+ clearInterval(heartbeat);
280
+ eventSources.forEach(events => events.close());
281
+ });
282
+ });
283
+
284
+ // Serve a basic HTML dashboard (placeholder until @glidemq/dashboard is available)
285
+ app.get('/', (req, res) => {
286
+ res.send(`
287
+ <!DOCTYPE html>
288
+ <html>
289
+ <head>
290
+ <title>glide-mq Dashboard</title>
291
+ <style>
292
+ * { margin: 0; padding: 0; box-sizing: border-box; }
293
+ body {
294
+ font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', Arial, sans-serif;
295
+ background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
296
+ min-height: 100vh;
297
+ display: flex;
298
+ align-items: center;
299
+ justify-content: center;
300
+ }
301
+ .container {
302
+ background: white;
303
+ border-radius: 20px;
304
+ box-shadow: 0 20px 60px rgba(0,0,0,0.3);
305
+ padding: 40px;
306
+ max-width: 800px;
307
+ width: 90%;
308
+ }
309
+ h1 {
310
+ color: #333;
311
+ margin-bottom: 10px;
312
+ font-size: 2.5em;
313
+ }
314
+ .subtitle {
315
+ color: #666;
316
+ margin-bottom: 30px;
317
+ font-size: 1.1em;
318
+ }
319
+ .status {
320
+ background: #f0f4f8;
321
+ border-radius: 10px;
322
+ padding: 20px;
323
+ margin-bottom: 20px;
324
+ }
325
+ .status-item {
326
+ display: flex;
327
+ justify-content: space-between;
328
+ margin: 10px 0;
329
+ padding: 10px;
330
+ background: white;
331
+ border-radius: 5px;
332
+ }
333
+ .label {
334
+ font-weight: 600;
335
+ color: #555;
336
+ }
337
+ .value {
338
+ color: #667eea;
339
+ font-weight: bold;
340
+ }
341
+ .instructions {
342
+ background: #fef3c7;
343
+ border: 2px solid #f59e0b;
344
+ border-radius: 10px;
345
+ padding: 20px;
346
+ margin-top: 30px;
347
+ }
348
+ .instructions h3 {
349
+ color: #92400e;
350
+ margin-bottom: 10px;
351
+ }
352
+ .instructions p {
353
+ color: #78350f;
354
+ line-height: 1.6;
355
+ }
356
+ .instructions code {
357
+ background: #fed7aa;
358
+ padding: 2px 6px;
359
+ border-radius: 3px;
360
+ font-family: 'Courier New', monospace;
361
+ }
362
+ .endpoint-list {
363
+ margin-top: 20px;
364
+ }
365
+ .endpoint {
366
+ background: white;
367
+ padding: 8px 12px;
368
+ margin: 5px 0;
369
+ border-radius: 5px;
370
+ font-family: monospace;
371
+ color: #059669;
372
+ }
373
+ </style>
374
+ </head>
375
+ <body>
376
+ <div class="container">
377
+ <h1>glide-mq Dashboard</h1>
378
+ <p class="subtitle">High-performance message queue monitoring</p>
379
+
380
+ <div class="status">
381
+ <h2>API Status</h2>
382
+ <div class="status-item">
383
+ <span class="label">Server</span>
384
+ <span class="value">Running on port ${PORT}</span>
385
+ </div>
386
+ <div class="status-item">
387
+ <span class="label">Valkey Connection</span>
388
+ <span class="value">Connected</span>
389
+ </div>
390
+ <div class="status-item">
391
+ <span class="label">Queues Registered</span>
392
+ <span class="value">${queueRegistry.length}</span>
393
+ </div>
394
+ </div>
395
+
396
+ <div class="instructions">
397
+ <h3>Dashboard Setup</h3>
398
+ <p>
399
+ This server provides REST API endpoints for the glide-mq dashboard.
400
+ Once <code>@glidemq/dashboard</code> is available, install it with:
401
+ </p>
402
+ <p style="margin: 15px 0;">
403
+ <code>npm install @glidemq/dashboard</code>
404
+ </p>
405
+ <p>
406
+ Then the dashboard UI will be available at this URL.
407
+ For now, you can use the API endpoints directly:
408
+ </p>
409
+
410
+ <div class="endpoint-list">
411
+ <div class="endpoint">GET /api/queues</div>
412
+ <div class="endpoint">GET /api/queues/:name</div>
413
+ <div class="endpoint">GET /api/queues/:name/jobs/:id</div>
414
+ <div class="endpoint">POST /api/queues/:name/jobs</div>
415
+ <div class="endpoint">POST /api/queues/:name/pause</div>
416
+ <div class="endpoint">POST /api/queues/:name/resume</div>
417
+ <div class="endpoint">GET /api/events (SSE)</div>
418
+ </div>
419
+ </div>
420
+ </div>
421
+
422
+ <script>
423
+ // Auto-refresh queue counts
424
+ async function updateStatus() {
425
+ try {
426
+ const response = await fetch('/api/queues');
427
+ const queues = await response.json();
428
+ console.log('Queue status:', queues);
429
+ } catch (err) {
430
+ console.error('Failed to fetch queue status:', err);
431
+ }
432
+ }
433
+
434
+ // Connect to SSE for real-time updates
435
+ const eventSource = new EventSource('/api/events');
436
+ eventSource.onmessage = (event) => {
437
+ const data = JSON.parse(event.data);
438
+ console.log('Real-time event:', data);
439
+ };
440
+
441
+ // Update every 5 seconds
442
+ setInterval(updateStatus, 5000);
443
+ updateStatus();
444
+ </script>
445
+ </body>
446
+ </html>
447
+ `);
448
+ });
449
+
450
+ // Start server
451
+ app.listen(PORT, () => {
452
+ console.log(chalk.green(`[OK] Dashboard server running at http://localhost:${PORT}`));
453
+ console.log(chalk.cyan('[INFO] API endpoints available at http://localhost:' + PORT + '/api'));
454
+ console.log(chalk.yellow('[WARN] Full dashboard UI pending @glidemq/dashboard package'));
455
+ console.log(chalk.gray('\nAvailable endpoints:'));
456
+ console.log(chalk.gray(' GET /api/queues - List all queues'));
457
+ console.log(chalk.gray(' GET /api/queues/:name - Queue details'));
458
+ console.log(chalk.gray(' GET /api/queues/:n/jobs/:id - Job details'));
459
+ console.log(chalk.gray(' POST /api/queues/:name/jobs - Add job'));
460
+ console.log(chalk.gray(' POST /api/queues/:name/pause - Pause queue'));
461
+ console.log(chalk.gray(' POST /api/queues/:name/resume - Resume queue'));
462
+ console.log(chalk.gray(' GET /api/events - SSE stream'));
463
+ });
464
+
465
+ // Graceful shutdown
466
+ process.on('SIGINT', async () => {
467
+ console.log(chalk.yellow('\n[WARN] Shutting down dashboard server...'));
468
+
469
+ // Close all queues
470
+ await Promise.all(Object.values(queues).map(q => q.close()));
471
+
472
+ console.log(chalk.green('[OK] Dashboard server stopped.'));
473
+ process.exit(0);
474
+ });