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.
- package/README.md +143 -8
- package/demo/README.md +169 -0
- package/demo/dashboard-server.ts +474 -0
- package/demo/index.ts +502 -0
- package/demo/package-lock.json +2051 -0
- package/demo/package.json +26 -0
- package/dist/functions/index.d.ts +2 -2
- package/dist/functions/index.d.ts.map +1 -1
- package/dist/functions/index.js +23 -11
- package/dist/functions/index.js.map +1 -1
- package/dist/graceful-shutdown.d.ts +5 -1
- package/dist/graceful-shutdown.d.ts.map +1 -1
- package/dist/graceful-shutdown.js +31 -11
- package/dist/graceful-shutdown.js.map +1 -1
- package/dist/index.d.ts +1 -0
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js.map +1 -1
- package/package.json +1 -1
|
@@ -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
|
+
});
|