voltjs-framework 1.0.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.
Files changed (60) hide show
  1. package/LICENSE +21 -0
  2. package/README.md +1265 -0
  3. package/bin/volt.js +139 -0
  4. package/package.json +56 -0
  5. package/src/api/graphql.js +399 -0
  6. package/src/api/rest.js +204 -0
  7. package/src/api/websocket.js +285 -0
  8. package/src/cli/build.js +111 -0
  9. package/src/cli/create.js +371 -0
  10. package/src/cli/db.js +106 -0
  11. package/src/cli/dev.js +114 -0
  12. package/src/cli/generate.js +278 -0
  13. package/src/cli/lint.js +172 -0
  14. package/src/cli/routes.js +118 -0
  15. package/src/cli/start.js +42 -0
  16. package/src/cli/test.js +138 -0
  17. package/src/core/app.js +701 -0
  18. package/src/core/config.js +232 -0
  19. package/src/core/middleware.js +133 -0
  20. package/src/core/plugins.js +88 -0
  21. package/src/core/react-renderer.js +244 -0
  22. package/src/core/renderer.js +337 -0
  23. package/src/core/router.js +183 -0
  24. package/src/database/index.js +461 -0
  25. package/src/database/migration.js +192 -0
  26. package/src/database/model.js +285 -0
  27. package/src/database/query.js +394 -0
  28. package/src/database/seeder.js +89 -0
  29. package/src/index.js +156 -0
  30. package/src/security/auth.js +425 -0
  31. package/src/security/cors.js +80 -0
  32. package/src/security/csrf.js +125 -0
  33. package/src/security/encryption.js +110 -0
  34. package/src/security/helmet.js +103 -0
  35. package/src/security/index.js +75 -0
  36. package/src/security/rateLimit.js +119 -0
  37. package/src/security/sanitizer.js +113 -0
  38. package/src/security/xss.js +110 -0
  39. package/src/ui/component.js +224 -0
  40. package/src/ui/reactive.js +503 -0
  41. package/src/ui/template.js +448 -0
  42. package/src/utils/cache.js +216 -0
  43. package/src/utils/collection.js +772 -0
  44. package/src/utils/cron.js +213 -0
  45. package/src/utils/date.js +223 -0
  46. package/src/utils/events.js +181 -0
  47. package/src/utils/excel.js +482 -0
  48. package/src/utils/form.js +547 -0
  49. package/src/utils/hash.js +121 -0
  50. package/src/utils/http.js +461 -0
  51. package/src/utils/logger.js +186 -0
  52. package/src/utils/mail.js +347 -0
  53. package/src/utils/paginator.js +179 -0
  54. package/src/utils/pdf.js +417 -0
  55. package/src/utils/queue.js +199 -0
  56. package/src/utils/schema.js +985 -0
  57. package/src/utils/sms.js +243 -0
  58. package/src/utils/storage.js +348 -0
  59. package/src/utils/string.js +236 -0
  60. package/src/utils/validation.js +318 -0
@@ -0,0 +1,417 @@
1
+ /**
2
+ * VoltJS PDF Generator
3
+ *
4
+ * Generate PDF documents from HTML or programmatically.
5
+ * Uses raw PDF spec — zero external dependencies.
6
+ *
7
+ * @example
8
+ * const { PDF } = require('voltjs');
9
+ *
10
+ * const pdf = new PDF();
11
+ * pdf.addPage()
12
+ * .setFont('Helvetica', 16)
13
+ * .text('Hello VoltJS!', 50, 700)
14
+ * .setFont('Helvetica', 12)
15
+ * .text('Generated with VoltJS PDF', 50, 670)
16
+ * .line(50, 660, 550, 660)
17
+ * .save('output.pdf');
18
+ */
19
+
20
+ 'use strict';
21
+
22
+ const fs = require('fs');
23
+
24
+ class PDF {
25
+ constructor(options = {}) {
26
+ this._objects = [];
27
+ this._pages = [];
28
+ this._currentPage = null;
29
+ this._fonts = new Map();
30
+ this._pageWidth = options.width || 612; // US Letter
31
+ this._pageHeight = options.height || 792;
32
+ this._margin = options.margin || 50;
33
+ this._fontSize = 12;
34
+ this._fontName = 'Helvetica';
35
+
36
+ // Register standard fonts
37
+ this._registerStandardFonts();
38
+ }
39
+
40
+ /** Add a new page */
41
+ addPage(options = {}) {
42
+ const width = options.width || this._pageWidth;
43
+ const height = options.height || this._pageHeight;
44
+
45
+ this._currentPage = {
46
+ width,
47
+ height,
48
+ content: [],
49
+ };
50
+ this._pages.push(this._currentPage);
51
+ return this;
52
+ }
53
+
54
+ /** Set font */
55
+ setFont(name, size) {
56
+ this._fontName = name || 'Helvetica';
57
+ this._fontSize = size || 12;
58
+ if (this._currentPage) {
59
+ this._currentPage.content.push(`/F1 ${this._fontSize} Tf`);
60
+ }
61
+ return this;
62
+ }
63
+
64
+ /** Add text to current page */
65
+ text(str, x, y, options = {}) {
66
+ if (!this._currentPage) this.addPage();
67
+
68
+ const escapedStr = this._escapeString(str);
69
+ const alignment = options.align || 'left';
70
+
71
+ this._currentPage.content.push('BT');
72
+ this._currentPage.content.push(`/F1 ${this._fontSize} Tf`);
73
+
74
+ if (options.color) {
75
+ const rgb = this._hexToRgb(options.color);
76
+ this._currentPage.content.push(`${rgb.r} ${rgb.g} ${rgb.b} rg`);
77
+ }
78
+
79
+ this._currentPage.content.push(`${x} ${y} Td`);
80
+ this._currentPage.content.push(`(${escapedStr}) Tj`);
81
+ this._currentPage.content.push('ET');
82
+
83
+ return this;
84
+ }
85
+
86
+ /** Add a line */
87
+ line(x1, y1, x2, y2, options = {}) {
88
+ if (!this._currentPage) this.addPage();
89
+
90
+ const lineWidth = options.width || 1;
91
+ const content = this._currentPage.content;
92
+
93
+ if (options.color) {
94
+ const rgb = this._hexToRgb(options.color);
95
+ content.push(`${rgb.r} ${rgb.g} ${rgb.b} RG`);
96
+ }
97
+
98
+ content.push(`${lineWidth} w`);
99
+ content.push(`${x1} ${y1} m`);
100
+ content.push(`${x2} ${y2} l`);
101
+ content.push('S');
102
+
103
+ return this;
104
+ }
105
+
106
+ /** Add a rectangle */
107
+ rect(x, y, width, height, options = {}) {
108
+ if (!this._currentPage) this.addPage();
109
+ const content = this._currentPage.content;
110
+
111
+ if (options.fill) {
112
+ const rgb = this._hexToRgb(options.fill);
113
+ content.push(`${rgb.r} ${rgb.g} ${rgb.b} rg`);
114
+ }
115
+ if (options.stroke) {
116
+ const rgb = this._hexToRgb(options.stroke);
117
+ content.push(`${rgb.r} ${rgb.g} ${rgb.b} RG`);
118
+ }
119
+
120
+ content.push(`${x} ${y} ${width} ${height} re`);
121
+ content.push(options.fill && options.stroke ? 'B' : options.fill ? 'f' : 'S');
122
+
123
+ return this;
124
+ }
125
+
126
+ /** Add a circle */
127
+ circle(cx, cy, radius, options = {}) {
128
+ if (!this._currentPage) this.addPage();
129
+ const content = this._currentPage.content;
130
+
131
+ if (options.fill) {
132
+ const rgb = this._hexToRgb(options.fill);
133
+ content.push(`${rgb.r} ${rgb.g} ${rgb.b} rg`);
134
+ }
135
+
136
+ // Approximate circle with bezier curves
137
+ const k = 0.5523;
138
+ const kr = k * radius;
139
+
140
+ content.push(`${cx + radius} ${cy} m`);
141
+ content.push(`${cx + radius} ${cy + kr} ${cx + kr} ${cy + radius} ${cx} ${cy + radius} c`);
142
+ content.push(`${cx - kr} ${cy + radius} ${cx - radius} ${cy + kr} ${cx - radius} ${cy} c`);
143
+ content.push(`${cx - radius} ${cy - kr} ${cx - kr} ${cy - radius} ${cx} ${cy - radius} c`);
144
+ content.push(`${cx + kr} ${cy - radius} ${cx + radius} ${cy - kr} ${cx + radius} ${cy} c`);
145
+ content.push(options.fill ? 'f' : 'S');
146
+
147
+ return this;
148
+ }
149
+
150
+ /** Add a table */
151
+ table(data, x, y, options = {}) {
152
+ if (!this._currentPage) this.addPage();
153
+
154
+ const cellWidth = options.cellWidth || 100;
155
+ const cellHeight = options.cellHeight || 25;
156
+ const headerBg = options.headerBg || '#667eea';
157
+ const headers = options.headers || (data.length > 0 ? Object.keys(data[0]) : []);
158
+
159
+ let currentY = y;
160
+
161
+ // Draw header
162
+ headers.forEach((header, i) => {
163
+ this.rect(x + i * cellWidth, currentY, cellWidth, cellHeight, { fill: headerBg, stroke: '#333333' });
164
+ this.setFont('Helvetica', 10);
165
+ this._currentPage.content.push('1 1 1 rg'); // White text
166
+ this.text(String(header), x + i * cellWidth + 5, currentY + 8);
167
+ this._currentPage.content.push('0 0 0 rg'); // Reset to black
168
+ });
169
+
170
+ currentY -= cellHeight;
171
+
172
+ // Draw data rows
173
+ for (const row of data) {
174
+ headers.forEach((header, i) => {
175
+ this.rect(x + i * cellWidth, currentY, cellWidth, cellHeight, { stroke: '#cccccc' });
176
+ this.setFont('Helvetica', 9);
177
+ this.text(String(row[header] ?? ''), x + i * cellWidth + 5, currentY + 8);
178
+ });
179
+ currentY -= cellHeight;
180
+ }
181
+
182
+ return this;
183
+ }
184
+
185
+ /** Add header section */
186
+ header(title, subtitle = '') {
187
+ if (!this._currentPage) this.addPage();
188
+
189
+ const y = this._pageHeight - this._margin;
190
+
191
+ // Header bar
192
+ this.rect(0, y - 10, this._pageWidth, 60, { fill: '#667eea' });
193
+ this._currentPage.content.push('1 1 1 rg');
194
+ this.setFont('Helvetica', 20);
195
+ this.text(title, this._margin, y + 15);
196
+
197
+ if (subtitle) {
198
+ this.setFont('Helvetica', 10);
199
+ this.text(subtitle, this._margin, y);
200
+ }
201
+
202
+ this._currentPage.content.push('0 0 0 rg');
203
+ return this;
204
+ }
205
+
206
+ /** Add paragraph (auto-wrapping text) */
207
+ paragraph(str, x, y, maxWidth, options = {}) {
208
+ if (!this._currentPage) this.addPage();
209
+
210
+ const fontSize = options.fontSize || this._fontSize;
211
+ const lineHeight = options.lineHeight || fontSize * 1.5;
212
+ const charWidth = fontSize * 0.5; // Approximate
213
+ const maxChars = Math.floor(maxWidth / charWidth);
214
+
215
+ const words = str.split(' ');
216
+ let line = '';
217
+ let currentY = y;
218
+
219
+ for (const word of words) {
220
+ if ((line + ' ' + word).length > maxChars) {
221
+ this.setFont(this._fontName, fontSize);
222
+ this.text(line.trim(), x, currentY);
223
+ currentY -= lineHeight;
224
+ line = word;
225
+ } else {
226
+ line += (line ? ' ' : '') + word;
227
+ }
228
+ }
229
+ if (line) {
230
+ this.setFont(this._fontName, fontSize);
231
+ this.text(line.trim(), x, currentY);
232
+ }
233
+
234
+ return this;
235
+ }
236
+
237
+ /** Generate PDF buffer */
238
+ toBuffer() {
239
+ return Buffer.from(this._build(), 'binary');
240
+ }
241
+
242
+ /** Save to file */
243
+ save(filePath) {
244
+ fs.writeFileSync(filePath, this.toBuffer());
245
+ return filePath;
246
+ }
247
+
248
+ // ===== INTERNAL PDF GENERATION =====
249
+
250
+ _build() {
251
+ const objects = [];
252
+ let objectNum = 0;
253
+
254
+ const addObj = (content) => {
255
+ objectNum++;
256
+ objects.push({ num: objectNum, content });
257
+ return objectNum;
258
+ };
259
+
260
+ // Catalog
261
+ const catalogNum = addObj('<<\n/Type /Catalog\n/Pages 2 0 R\n>>');
262
+
263
+ // Pages parent (will be filled later)
264
+ const pagesNum = addObj(''); // Placeholder
265
+
266
+ // Font
267
+ const fontNum = addObj('<<\n/Type /Font\n/Subtype /Type1\n/BaseFont /Helvetica\n>>');
268
+
269
+ // Build pages
270
+ const pageNums = [];
271
+ for (const page of this._pages) {
272
+ // Page content stream
273
+ const stream = page.content.join('\n');
274
+ const streamLength = Buffer.byteLength(stream, 'binary');
275
+ const streamNum = addObj(
276
+ `<<\n/Length ${streamLength}\n>>\nstream\n${stream}\nendstream`
277
+ );
278
+
279
+ // Page object
280
+ const pageNum = addObj(
281
+ `<<\n/Type /Page\n/Parent ${pagesNum} 0 R\n/MediaBox [0 0 ${page.width} ${page.height}]\n/Contents ${streamNum} 0 R\n/Resources <<\n/Font <<\n/F1 ${fontNum} 0 R\n>>\n>>\n>>`
282
+ );
283
+ pageNums.push(pageNum);
284
+ }
285
+
286
+ // Update pages parent
287
+ objects[pagesNum - 1].content = `<<\n/Type /Pages\n/Kids [${pageNums.map(n => `${n} 0 R`).join(' ')}]\n/Count ${pageNums.length}\n>>`;
288
+
289
+ // Build PDF
290
+ let pdf = '%PDF-1.4\n';
291
+ const offsets = [];
292
+
293
+ for (const obj of objects) {
294
+ offsets.push(pdf.length);
295
+ pdf += `${obj.num} 0 obj\n${obj.content}\nendobj\n`;
296
+ }
297
+
298
+ // Cross-reference table
299
+ const xrefOffset = pdf.length;
300
+ pdf += `xref\n0 ${objects.length + 1}\n`;
301
+ pdf += '0000000000 65535 f \n';
302
+ for (const offset of offsets) {
303
+ pdf += `${String(offset).padStart(10, '0')} 00000 n \n`;
304
+ }
305
+
306
+ // Trailer
307
+ pdf += `trailer\n<<\n/Size ${objects.length + 1}\n/Root ${catalogNum} 0 R\n>>\nstartxref\n${xrefOffset}\n%%EOF`;
308
+
309
+ return pdf;
310
+ }
311
+
312
+ _escapeString(str) {
313
+ return String(str)
314
+ .replace(/\\/g, '\\\\')
315
+ .replace(/\(/g, '\\(')
316
+ .replace(/\)/g, '\\)');
317
+ }
318
+
319
+ _hexToRgb(hex) {
320
+ const cleaned = hex.replace('#', '');
321
+ const r = parseInt(cleaned.substring(0, 2), 16) / 255;
322
+ const g = parseInt(cleaned.substring(2, 4), 16) / 255;
323
+ const b = parseInt(cleaned.substring(4, 6), 16) / 255;
324
+ return { r: r.toFixed(3), g: g.toFixed(3), b: b.toFixed(3) };
325
+ }
326
+
327
+ _registerStandardFonts() {
328
+ this._fonts.set('Helvetica', '/Helvetica');
329
+ this._fonts.set('Times', '/Times-Roman');
330
+ this._fonts.set('Courier', '/Courier');
331
+ }
332
+
333
+ // ===== STATIC HELPERS =====
334
+
335
+ /** Quick PDF from HTML-like content */
336
+ static fromData(data, options = {}) {
337
+ const pdf = new PDF(options);
338
+ const page = pdf.addPage();
339
+
340
+ let y = pdf._pageHeight - pdf._margin - 50;
341
+ const title = options.title || 'Report';
342
+
343
+ pdf.header(title, options.subtitle || new Date().toLocaleString());
344
+
345
+ if (Array.isArray(data) && data.length > 0 && typeof data[0] === 'object') {
346
+ y -= 80;
347
+ pdf.table(data, pdf._margin, y, {
348
+ headers: options.headers || Object.keys(data[0]),
349
+ cellWidth: options.cellWidth || Math.floor((pdf._pageWidth - pdf._margin * 2) / Object.keys(data[0]).length),
350
+ });
351
+ }
352
+
353
+ return pdf;
354
+ }
355
+
356
+ /** Generate PDF invoice */
357
+ static invoice(invoiceData) {
358
+ const pdf = new PDF();
359
+ pdf.addPage();
360
+
361
+ const w = pdf._pageWidth;
362
+ const h = pdf._pageHeight;
363
+ const m = pdf._margin;
364
+ let y = h - m;
365
+
366
+ // Header
367
+ pdf.setFont('Helvetica', 24);
368
+ pdf.text('INVOICE', m, y);
369
+
370
+ pdf.setFont('Helvetica', 10);
371
+ y -= 30;
372
+ pdf.text(`Invoice #: ${invoiceData.number || '001'}`, m, y);
373
+ y -= 15;
374
+ pdf.text(`Date: ${invoiceData.date || new Date().toLocaleDateString()}`, m, y);
375
+ y -= 15;
376
+ pdf.text(`Due: ${invoiceData.dueDate || 'On Receipt'}`, m, y);
377
+
378
+ // From
379
+ y -= 30;
380
+ pdf.setFont('Helvetica', 12);
381
+ pdf.text('From:', m, y);
382
+ pdf.setFont('Helvetica', 10);
383
+ y -= 15;
384
+ pdf.text(invoiceData.from?.name || 'Company', m, y);
385
+ y -= 12;
386
+ pdf.text(invoiceData.from?.address || '', m, y);
387
+
388
+ // To
389
+ y -= 30;
390
+ pdf.setFont('Helvetica', 12);
391
+ pdf.text('Bill To:', m, y);
392
+ pdf.setFont('Helvetica', 10);
393
+ y -= 15;
394
+ pdf.text(invoiceData.to?.name || 'Client', m, y);
395
+ y -= 12;
396
+ pdf.text(invoiceData.to?.address || '', m, y);
397
+
398
+ // Items table
399
+ y -= 30;
400
+ if (invoiceData.items && invoiceData.items.length > 0) {
401
+ pdf.table(invoiceData.items, m, y, {
402
+ headers: ['description', 'quantity', 'price', 'total'],
403
+ cellWidth: (w - m * 2) / 4,
404
+ });
405
+ }
406
+
407
+ // Total
408
+ y -= (invoiceData.items?.length || 0) * 25 + 50;
409
+ pdf.setFont('Helvetica', 14);
410
+ const total = invoiceData.items?.reduce((sum, item) => sum + (item.total || 0), 0) || 0;
411
+ pdf.text(`Total: $${total.toFixed(2)}`, w - m - 150, y);
412
+
413
+ return pdf;
414
+ }
415
+ }
416
+
417
+ module.exports = { PDF };
@@ -0,0 +1,199 @@
1
+ /**
2
+ * VoltJS Queue
3
+ *
4
+ * In-process job queue with retries, delays, priorities, and concurrency control.
5
+ *
6
+ * @example
7
+ * const { Queue } = require('voltjs');
8
+ *
9
+ * const queue = new Queue('emails', { concurrency: 3, retries: 3 });
10
+ *
11
+ * queue.process(async (job) => {
12
+ * await sendEmail(job.data);
13
+ * });
14
+ *
15
+ * queue.add({ to: 'user@example.com', subject: 'Hello' }, { priority: 'high' });
16
+ */
17
+
18
+ 'use strict';
19
+
20
+ const crypto = require('crypto');
21
+ const { EventEmitter } = require('events');
22
+
23
+ class Queue extends EventEmitter {
24
+ constructor(name, options = {}) {
25
+ super();
26
+ this.name = name;
27
+ this.concurrency = options.concurrency || 1;
28
+ this.maxRetries = options.retries || 3;
29
+ this.retryDelay = options.retryDelay || 1000; // ms
30
+ this.timeout = options.timeout || 30000; // 30s
31
+
32
+ this.jobs = [];
33
+ this.processing = 0;
34
+ this.handler = null;
35
+ this.paused = false;
36
+
37
+ this.stats = { completed: 0, failed: 0, retried: 0, total: 0 };
38
+ this._delayed = [];
39
+ }
40
+
41
+ /** Add a job to the queue */
42
+ add(data, options = {}) {
43
+ const job = {
44
+ id: options.id || crypto.randomBytes(8).toString('hex'),
45
+ data,
46
+ priority: this._priorityValue(options.priority || 'normal'),
47
+ retries: 0,
48
+ maxRetries: options.retries || this.maxRetries,
49
+ delay: options.delay || 0,
50
+ createdAt: Date.now(),
51
+ status: 'pending',
52
+ };
53
+
54
+ this.stats.total++;
55
+
56
+ if (job.delay > 0) {
57
+ const timer = setTimeout(() => {
58
+ this._enqueue(job);
59
+ this._delayed = this._delayed.filter(d => d.id !== job.id);
60
+ }, job.delay);
61
+ if (timer.unref) timer.unref();
62
+ this._delayed.push({ id: job.id, timer });
63
+ } else {
64
+ this._enqueue(job);
65
+ }
66
+
67
+ this.emit('added', job);
68
+ return job;
69
+ }
70
+
71
+ /** Add multiple jobs */
72
+ addBulk(items, options = {}) {
73
+ return items.map(data => this.add(data, options));
74
+ }
75
+
76
+ /** Register job processor */
77
+ process(handler) {
78
+ this.handler = handler;
79
+ this._tick();
80
+ }
81
+
82
+ /** Pause the queue */
83
+ pause() {
84
+ this.paused = true;
85
+ this.emit('paused');
86
+ }
87
+
88
+ /** Resume the queue */
89
+ resume() {
90
+ this.paused = false;
91
+ this.emit('resumed');
92
+ this._tick();
93
+ }
94
+
95
+ /** Get queue size */
96
+ size() {
97
+ return this.jobs.length;
98
+ }
99
+
100
+ /** Get queue status */
101
+ status() {
102
+ return {
103
+ name: this.name,
104
+ pending: this.jobs.length,
105
+ processing: this.processing,
106
+ delayed: this._delayed.length,
107
+ paused: this.paused,
108
+ ...this.stats,
109
+ };
110
+ }
111
+
112
+ /** Clear all pending jobs */
113
+ clear() {
114
+ this.jobs = [];
115
+ this._delayed.forEach(d => clearTimeout(d.timer));
116
+ this._delayed = [];
117
+ this.emit('cleared');
118
+ }
119
+
120
+ /** Remove a specific job by ID */
121
+ remove(id) {
122
+ const idx = this.jobs.findIndex(j => j.id === id);
123
+ if (idx >= 0) {
124
+ this.jobs.splice(idx, 1);
125
+ return true;
126
+ }
127
+ return false;
128
+ }
129
+
130
+ // ===== INTERNAL =====
131
+
132
+ _enqueue(job) {
133
+ // Insert sorted by priority (higher priority = processed first)
134
+ let inserted = false;
135
+ for (let i = 0; i < this.jobs.length; i++) {
136
+ if (job.priority > this.jobs[i].priority) {
137
+ this.jobs.splice(i, 0, job);
138
+ inserted = true;
139
+ break;
140
+ }
141
+ }
142
+ if (!inserted) this.jobs.push(job);
143
+ this._tick();
144
+ }
145
+
146
+ async _tick() {
147
+ if (this.paused || !this.handler) return;
148
+ if (this.processing >= this.concurrency) return;
149
+ if (this.jobs.length === 0) return;
150
+
151
+ const job = this.jobs.shift();
152
+ job.status = 'processing';
153
+ this.processing++;
154
+
155
+ try {
156
+ // Timeout wrapper
157
+ const result = await Promise.race([
158
+ this.handler(job),
159
+ new Promise((_, reject) =>
160
+ setTimeout(() => reject(new Error(`Job ${job.id} timed out`)), this.timeout)
161
+ ),
162
+ ]);
163
+
164
+ job.status = 'completed';
165
+ job.result = result;
166
+ this.stats.completed++;
167
+ this.emit('completed', job);
168
+
169
+ } catch (error) {
170
+ job.retries++;
171
+
172
+ if (job.retries <= job.maxRetries) {
173
+ job.status = 'retrying';
174
+ job.lastError = error.message;
175
+ this.stats.retried++;
176
+ this.emit('retrying', job, error);
177
+
178
+ // Exponential backoff
179
+ const delay = this.retryDelay * Math.pow(2, job.retries - 1);
180
+ setTimeout(() => this._enqueue(job), delay);
181
+ } else {
182
+ job.status = 'failed';
183
+ job.error = error.message;
184
+ this.stats.failed++;
185
+ this.emit('failed', job, error);
186
+ }
187
+ } finally {
188
+ this.processing--;
189
+ this._tick(); // Process next
190
+ }
191
+ }
192
+
193
+ _priorityValue(priority) {
194
+ const map = { low: 1, normal: 5, high: 10, critical: 20 };
195
+ return typeof priority === 'number' ? priority : (map[priority] || 5);
196
+ }
197
+ }
198
+
199
+ module.exports = { Queue };