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.
- package/LICENSE +21 -0
- package/README.md +1265 -0
- package/bin/volt.js +139 -0
- package/package.json +56 -0
- package/src/api/graphql.js +399 -0
- package/src/api/rest.js +204 -0
- package/src/api/websocket.js +285 -0
- package/src/cli/build.js +111 -0
- package/src/cli/create.js +371 -0
- package/src/cli/db.js +106 -0
- package/src/cli/dev.js +114 -0
- package/src/cli/generate.js +278 -0
- package/src/cli/lint.js +172 -0
- package/src/cli/routes.js +118 -0
- package/src/cli/start.js +42 -0
- package/src/cli/test.js +138 -0
- package/src/core/app.js +701 -0
- package/src/core/config.js +232 -0
- package/src/core/middleware.js +133 -0
- package/src/core/plugins.js +88 -0
- package/src/core/react-renderer.js +244 -0
- package/src/core/renderer.js +337 -0
- package/src/core/router.js +183 -0
- package/src/database/index.js +461 -0
- package/src/database/migration.js +192 -0
- package/src/database/model.js +285 -0
- package/src/database/query.js +394 -0
- package/src/database/seeder.js +89 -0
- package/src/index.js +156 -0
- package/src/security/auth.js +425 -0
- package/src/security/cors.js +80 -0
- package/src/security/csrf.js +125 -0
- package/src/security/encryption.js +110 -0
- package/src/security/helmet.js +103 -0
- package/src/security/index.js +75 -0
- package/src/security/rateLimit.js +119 -0
- package/src/security/sanitizer.js +113 -0
- package/src/security/xss.js +110 -0
- package/src/ui/component.js +224 -0
- package/src/ui/reactive.js +503 -0
- package/src/ui/template.js +448 -0
- package/src/utils/cache.js +216 -0
- package/src/utils/collection.js +772 -0
- package/src/utils/cron.js +213 -0
- package/src/utils/date.js +223 -0
- package/src/utils/events.js +181 -0
- package/src/utils/excel.js +482 -0
- package/src/utils/form.js +547 -0
- package/src/utils/hash.js +121 -0
- package/src/utils/http.js +461 -0
- package/src/utils/logger.js +186 -0
- package/src/utils/mail.js +347 -0
- package/src/utils/paginator.js +179 -0
- package/src/utils/pdf.js +417 -0
- package/src/utils/queue.js +199 -0
- package/src/utils/schema.js +985 -0
- package/src/utils/sms.js +243 -0
- package/src/utils/storage.js +348 -0
- package/src/utils/string.js +236 -0
- package/src/utils/validation.js +318 -0
|
@@ -0,0 +1,213 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS Cron
|
|
3
|
+
*
|
|
4
|
+
* Cron-like scheduled task runner with human-readable syntax.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { Cron } = require('voltjs');
|
|
8
|
+
*
|
|
9
|
+
* const cron = new Cron();
|
|
10
|
+
* cron.schedule('cleanup', '0 * * * *', async () => { // Every hour
|
|
11
|
+
* await cleanupOldFiles();
|
|
12
|
+
* });
|
|
13
|
+
* cron.schedule('report', '0 9 * * 1', sendWeeklyReport); // Mon 9am
|
|
14
|
+
*
|
|
15
|
+
* cron.start();
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
'use strict';
|
|
19
|
+
|
|
20
|
+
class Cron {
|
|
21
|
+
constructor() {
|
|
22
|
+
this.tasks = new Map();
|
|
23
|
+
this.running = false;
|
|
24
|
+
this._timer = null;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Schedule a task */
|
|
28
|
+
schedule(name, expression, handler, options = {}) {
|
|
29
|
+
const parsed = Cron.parse(expression);
|
|
30
|
+
|
|
31
|
+
this.tasks.set(name, {
|
|
32
|
+
name,
|
|
33
|
+
expression,
|
|
34
|
+
parsed,
|
|
35
|
+
handler,
|
|
36
|
+
enabled: options.enabled !== false,
|
|
37
|
+
runOnStart: options.runOnStart || false,
|
|
38
|
+
lastRun: null,
|
|
39
|
+
nextRun: null,
|
|
40
|
+
runCount: 0,
|
|
41
|
+
errors: 0,
|
|
42
|
+
});
|
|
43
|
+
|
|
44
|
+
return this;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Remove a task */
|
|
48
|
+
remove(name) {
|
|
49
|
+
this.tasks.delete(name);
|
|
50
|
+
return this;
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/** Enable a task */
|
|
54
|
+
enable(name) {
|
|
55
|
+
const task = this.tasks.get(name);
|
|
56
|
+
if (task) task.enabled = true;
|
|
57
|
+
return this;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Disable a task */
|
|
61
|
+
disable(name) {
|
|
62
|
+
const task = this.tasks.get(name);
|
|
63
|
+
if (task) task.enabled = false;
|
|
64
|
+
return this;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
/** Start the scheduler */
|
|
68
|
+
start() {
|
|
69
|
+
if (this.running) return;
|
|
70
|
+
this.running = true;
|
|
71
|
+
|
|
72
|
+
// Run-on-start tasks
|
|
73
|
+
for (const task of this.tasks.values()) {
|
|
74
|
+
if (task.runOnStart && task.enabled) {
|
|
75
|
+
this._runTask(task);
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Check every second
|
|
80
|
+
this._timer = setInterval(() => this._tick(), 1000);
|
|
81
|
+
if (this._timer.unref) this._timer.unref();
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/** Stop the scheduler */
|
|
86
|
+
stop() {
|
|
87
|
+
this.running = false;
|
|
88
|
+
if (this._timer) {
|
|
89
|
+
clearInterval(this._timer);
|
|
90
|
+
this._timer = null;
|
|
91
|
+
}
|
|
92
|
+
return this;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/** Get status of all tasks */
|
|
96
|
+
status() {
|
|
97
|
+
const tasks = [];
|
|
98
|
+
for (const task of this.tasks.values()) {
|
|
99
|
+
tasks.push({
|
|
100
|
+
name: task.name,
|
|
101
|
+
expression: task.expression,
|
|
102
|
+
enabled: task.enabled,
|
|
103
|
+
lastRun: task.lastRun,
|
|
104
|
+
runCount: task.runCount,
|
|
105
|
+
errors: task.errors,
|
|
106
|
+
});
|
|
107
|
+
}
|
|
108
|
+
return { running: this.running, tasks };
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
/** Run a task immediately by name */
|
|
112
|
+
async run(name) {
|
|
113
|
+
const task = this.tasks.get(name);
|
|
114
|
+
if (!task) throw new Error(`Task not found: ${name}`);
|
|
115
|
+
return this._runTask(task);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/** Convenience helpers */
|
|
119
|
+
everyMinute(name, handler) { return this.schedule(name, '* * * * *', handler); }
|
|
120
|
+
every5Minutes(name, handler) { return this.schedule(name, '*/5 * * * *', handler); }
|
|
121
|
+
every15Minutes(name, handler) { return this.schedule(name, '*/15 * * * *', handler); }
|
|
122
|
+
every30Minutes(name, handler) { return this.schedule(name, '*/30 * * * *', handler); }
|
|
123
|
+
hourly(name, handler) { return this.schedule(name, '0 * * * *', handler); }
|
|
124
|
+
daily(name, handler) { return this.schedule(name, '0 0 * * *', handler); }
|
|
125
|
+
weekly(name, handler) { return this.schedule(name, '0 0 * * 0', handler); }
|
|
126
|
+
monthly(name, handler) { return this.schedule(name, '0 0 1 * *', handler); }
|
|
127
|
+
|
|
128
|
+
// ===== PARSING =====
|
|
129
|
+
|
|
130
|
+
/** Parse cron expression: minute hour day month weekday */
|
|
131
|
+
static parse(expression) {
|
|
132
|
+
const parts = expression.trim().split(/\s+/);
|
|
133
|
+
if (parts.length !== 5) {
|
|
134
|
+
throw new Error(`Invalid cron expression: ${expression} (need 5 fields: min hour day month weekday)`);
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
minute: Cron._parseField(parts[0], 0, 59),
|
|
139
|
+
hour: Cron._parseField(parts[1], 0, 23),
|
|
140
|
+
day: Cron._parseField(parts[2], 1, 31),
|
|
141
|
+
month: Cron._parseField(parts[3], 1, 12),
|
|
142
|
+
weekday: Cron._parseField(parts[4], 0, 6),
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/** Check if now matches the cron pattern */
|
|
147
|
+
static matches(parsed, date) {
|
|
148
|
+
return (
|
|
149
|
+
parsed.minute.includes(date.getMinutes()) &&
|
|
150
|
+
parsed.hour.includes(date.getHours()) &&
|
|
151
|
+
parsed.day.includes(date.getDate()) &&
|
|
152
|
+
parsed.month.includes(date.getMonth() + 1) &&
|
|
153
|
+
parsed.weekday.includes(date.getDay())
|
|
154
|
+
);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
static _parseField(field, min, max) {
|
|
158
|
+
if (field === '*') {
|
|
159
|
+
return Array.from({ length: max - min + 1 }, (_, i) => min + i);
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
const values = new Set();
|
|
163
|
+
|
|
164
|
+
for (const part of field.split(',')) {
|
|
165
|
+
if (part.includes('/')) {
|
|
166
|
+
// Step: */5 or 1-30/5
|
|
167
|
+
const [range, step] = part.split('/');
|
|
168
|
+
const stepNum = parseInt(step);
|
|
169
|
+
const [start, end] = range === '*' ? [min, max] : range.split('-').map(Number);
|
|
170
|
+
for (let i = start; i <= (end || max); i += stepNum) {
|
|
171
|
+
values.add(i);
|
|
172
|
+
}
|
|
173
|
+
} else if (part.includes('-')) {
|
|
174
|
+
// Range: 1-5
|
|
175
|
+
const [start, end] = part.split('-').map(Number);
|
|
176
|
+
for (let i = start; i <= end; i++) values.add(i);
|
|
177
|
+
} else {
|
|
178
|
+
// Single value
|
|
179
|
+
values.add(parseInt(part));
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
|
|
183
|
+
return [...values];
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
// ===== INTERNAL =====
|
|
187
|
+
|
|
188
|
+
_tick() {
|
|
189
|
+
const now = new Date();
|
|
190
|
+
// Only check at second 0 to avoid double-firing
|
|
191
|
+
if (now.getSeconds() !== 0) return;
|
|
192
|
+
|
|
193
|
+
for (const task of this.tasks.values()) {
|
|
194
|
+
if (!task.enabled) continue;
|
|
195
|
+
if (Cron.matches(task.parsed, now)) {
|
|
196
|
+
this._runTask(task);
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
|
|
201
|
+
async _runTask(task) {
|
|
202
|
+
try {
|
|
203
|
+
task.lastRun = new Date();
|
|
204
|
+
task.runCount++;
|
|
205
|
+
await task.handler();
|
|
206
|
+
} catch (error) {
|
|
207
|
+
task.errors++;
|
|
208
|
+
console.error(`[Cron] Task "${task.name}" failed:`, error.message);
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
module.exports = { Cron };
|
|
@@ -0,0 +1,223 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS DateHelper
|
|
3
|
+
*
|
|
4
|
+
* Date manipulation and formatting without external dependencies.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { DateHelper } = require('voltjs');
|
|
8
|
+
*
|
|
9
|
+
* DateHelper.format(new Date(), 'YYYY-MM-DD HH:mm:ss');
|
|
10
|
+
* DateHelper.ago(new Date('2024-01-01')); // "6 months ago"
|
|
11
|
+
* DateHelper.add(new Date(), 7, 'days');
|
|
12
|
+
* DateHelper.diff(date1, date2, 'hours');
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
class DateHelper {
|
|
18
|
+
/** Format a date string: YYYY, MM, DD, HH, mm, ss, ddd, MMM */
|
|
19
|
+
static format(date, fmt = 'YYYY-MM-DD HH:mm:ss') {
|
|
20
|
+
const d = new Date(date);
|
|
21
|
+
const days = ['Sun', 'Mon', 'Tue', 'Wed', 'Thu', 'Fri', 'Sat'];
|
|
22
|
+
const months = ['Jan', 'Feb', 'Mar', 'Apr', 'May', 'Jun', 'Jul', 'Aug', 'Sep', 'Oct', 'Nov', 'Dec'];
|
|
23
|
+
const fullMonths = ['January', 'February', 'March', 'April', 'May', 'June', 'July', 'August', 'September', 'October', 'November', 'December'];
|
|
24
|
+
const fullDays = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
25
|
+
|
|
26
|
+
const pad = (n) => String(n).padStart(2, '0');
|
|
27
|
+
|
|
28
|
+
return fmt
|
|
29
|
+
.replace('YYYY', d.getFullYear())
|
|
30
|
+
.replace('YY', String(d.getFullYear()).slice(-2))
|
|
31
|
+
.replace('MMMM', fullMonths[d.getMonth()])
|
|
32
|
+
.replace('MMM', months[d.getMonth()])
|
|
33
|
+
.replace('MM', pad(d.getMonth() + 1))
|
|
34
|
+
.replace('DD', pad(d.getDate()))
|
|
35
|
+
.replace('dddd', fullDays[d.getDay()])
|
|
36
|
+
.replace('ddd', days[d.getDay()])
|
|
37
|
+
.replace('HH', pad(d.getHours()))
|
|
38
|
+
.replace('hh', pad(d.getHours() % 12 || 12))
|
|
39
|
+
.replace('mm', pad(d.getMinutes()))
|
|
40
|
+
.replace('ss', pad(d.getSeconds()))
|
|
41
|
+
.replace('A', d.getHours() >= 12 ? 'PM' : 'AM')
|
|
42
|
+
.replace('a', d.getHours() >= 12 ? 'pm' : 'am');
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/** Relative time: "2 hours ago", "in 3 days" */
|
|
46
|
+
static ago(date) {
|
|
47
|
+
const now = Date.now();
|
|
48
|
+
const d = new Date(date).getTime();
|
|
49
|
+
const diff = now - d;
|
|
50
|
+
const abs = Math.abs(diff);
|
|
51
|
+
const future = diff < 0;
|
|
52
|
+
|
|
53
|
+
const seconds = Math.floor(abs / 1000);
|
|
54
|
+
const minutes = Math.floor(seconds / 60);
|
|
55
|
+
const hours = Math.floor(minutes / 60);
|
|
56
|
+
const days = Math.floor(hours / 24);
|
|
57
|
+
const weeks = Math.floor(days / 7);
|
|
58
|
+
const months = Math.floor(days / 30);
|
|
59
|
+
const years = Math.floor(days / 365);
|
|
60
|
+
|
|
61
|
+
let text;
|
|
62
|
+
if (seconds < 5) text = 'just now';
|
|
63
|
+
else if (seconds < 60) text = `${seconds} seconds`;
|
|
64
|
+
else if (minutes === 1) text = '1 minute';
|
|
65
|
+
else if (minutes < 60) text = `${minutes} minutes`;
|
|
66
|
+
else if (hours === 1) text = '1 hour';
|
|
67
|
+
else if (hours < 24) text = `${hours} hours`;
|
|
68
|
+
else if (days === 1) text = '1 day';
|
|
69
|
+
else if (days < 7) text = `${days} days`;
|
|
70
|
+
else if (weeks === 1) text = '1 week';
|
|
71
|
+
else if (weeks < 4) text = `${weeks} weeks`;
|
|
72
|
+
else if (months === 1) text = '1 month';
|
|
73
|
+
else if (months < 12) text = `${months} months`;
|
|
74
|
+
else if (years === 1) text = '1 year';
|
|
75
|
+
else text = `${years} years`;
|
|
76
|
+
|
|
77
|
+
if (text === 'just now') return text;
|
|
78
|
+
return future ? `in ${text}` : `${text} ago`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
/** Add time to a date */
|
|
82
|
+
static add(date, amount, unit = 'days') {
|
|
83
|
+
const d = new Date(date);
|
|
84
|
+
switch (unit) {
|
|
85
|
+
case 'seconds': case 'second': d.setSeconds(d.getSeconds() + amount); break;
|
|
86
|
+
case 'minutes': case 'minute': d.setMinutes(d.getMinutes() + amount); break;
|
|
87
|
+
case 'hours': case 'hour': d.setHours(d.getHours() + amount); break;
|
|
88
|
+
case 'days': case 'day': d.setDate(d.getDate() + amount); break;
|
|
89
|
+
case 'weeks': case 'week': d.setDate(d.getDate() + amount * 7); break;
|
|
90
|
+
case 'months': case 'month': d.setMonth(d.getMonth() + amount); break;
|
|
91
|
+
case 'years': case 'year': d.setFullYear(d.getFullYear() + amount); break;
|
|
92
|
+
}
|
|
93
|
+
return d;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Subtract time from a date */
|
|
97
|
+
static subtract(date, amount, unit = 'days') {
|
|
98
|
+
return DateHelper.add(date, -amount, unit);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
/** Get difference between two dates */
|
|
102
|
+
static diff(date1, date2, unit = 'days') {
|
|
103
|
+
const ms = Math.abs(new Date(date1) - new Date(date2));
|
|
104
|
+
switch (unit) {
|
|
105
|
+
case 'milliseconds': case 'ms': return ms;
|
|
106
|
+
case 'seconds': case 'second': return Math.floor(ms / 1000);
|
|
107
|
+
case 'minutes': case 'minute': return Math.floor(ms / 60000);
|
|
108
|
+
case 'hours': case 'hour': return Math.floor(ms / 3600000);
|
|
109
|
+
case 'days': case 'day': return Math.floor(ms / 86400000);
|
|
110
|
+
case 'weeks': case 'week': return Math.floor(ms / 604800000);
|
|
111
|
+
case 'months': case 'month': return Math.floor(ms / 2592000000);
|
|
112
|
+
case 'years': case 'year': return Math.floor(ms / 31536000000);
|
|
113
|
+
default: return ms;
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Check if a date is today */
|
|
118
|
+
static isToday(date) {
|
|
119
|
+
const d = new Date(date);
|
|
120
|
+
const today = new Date();
|
|
121
|
+
return d.toDateString() === today.toDateString();
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/** Check if a date is in the past */
|
|
125
|
+
static isPast(date) { return new Date(date) < new Date(); }
|
|
126
|
+
|
|
127
|
+
/** Check if a date is in the future */
|
|
128
|
+
static isFuture(date) { return new Date(date) > new Date(); }
|
|
129
|
+
|
|
130
|
+
/** Check if two dates are the same day */
|
|
131
|
+
static isSameDay(d1, d2) {
|
|
132
|
+
return new Date(d1).toDateString() === new Date(d2).toDateString();
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Get start of day/week/month/year */
|
|
136
|
+
static startOf(date, unit = 'day') {
|
|
137
|
+
const d = new Date(date);
|
|
138
|
+
switch (unit) {
|
|
139
|
+
case 'day': d.setHours(0, 0, 0, 0); break;
|
|
140
|
+
case 'week': d.setDate(d.getDate() - d.getDay()); d.setHours(0, 0, 0, 0); break;
|
|
141
|
+
case 'month': d.setDate(1); d.setHours(0, 0, 0, 0); break;
|
|
142
|
+
case 'year': d.setMonth(0, 1); d.setHours(0, 0, 0, 0); break;
|
|
143
|
+
}
|
|
144
|
+
return d;
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
/** Get end of day/week/month/year */
|
|
148
|
+
static endOf(date, unit = 'day') {
|
|
149
|
+
const d = new Date(date);
|
|
150
|
+
switch (unit) {
|
|
151
|
+
case 'day': d.setHours(23, 59, 59, 999); break;
|
|
152
|
+
case 'week': d.setDate(d.getDate() + (6 - d.getDay())); d.setHours(23, 59, 59, 999); break;
|
|
153
|
+
case 'month': d.setMonth(d.getMonth() + 1, 0); d.setHours(23, 59, 59, 999); break;
|
|
154
|
+
case 'year': d.setMonth(11, 31); d.setHours(23, 59, 59, 999); break;
|
|
155
|
+
}
|
|
156
|
+
return d;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/** Get ISO week number */
|
|
160
|
+
static weekNumber(date) {
|
|
161
|
+
const d = new Date(date);
|
|
162
|
+
d.setHours(0, 0, 0, 0);
|
|
163
|
+
d.setDate(d.getDate() + 3 - (d.getDay() + 6) % 7);
|
|
164
|
+
const week1 = new Date(d.getFullYear(), 0, 4);
|
|
165
|
+
return 1 + Math.round(((d - week1) / 86400000 - 3 + (week1.getDay() + 6) % 7) / 7);
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Check if year is a leap year */
|
|
169
|
+
static isLeapYear(year) {
|
|
170
|
+
return (year % 4 === 0 && year % 100 !== 0) || year % 400 === 0;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
/** Get days in month */
|
|
174
|
+
static daysInMonth(year, month) {
|
|
175
|
+
return new Date(year, month, 0).getDate();
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Parse a date from various formats */
|
|
179
|
+
static parse(str) {
|
|
180
|
+
// Try ISO format first
|
|
181
|
+
const d = new Date(str);
|
|
182
|
+
if (!isNaN(d)) return d;
|
|
183
|
+
|
|
184
|
+
// Try DD/MM/YYYY
|
|
185
|
+
const dmy = str.match(/^(\d{1,2})[\/\-](\d{1,2})[\/\-](\d{4})$/);
|
|
186
|
+
if (dmy) return new Date(parseInt(dmy[3]), parseInt(dmy[2]) - 1, parseInt(dmy[1]));
|
|
187
|
+
|
|
188
|
+
// Try relative: "5 days ago", "in 3 hours"
|
|
189
|
+
const agoMatch = str.match(/^(\d+)\s+(second|minute|hour|day|week|month|year)s?\s+ago$/i);
|
|
190
|
+
if (agoMatch) return DateHelper.subtract(new Date(), parseInt(agoMatch[1]), agoMatch[2] + 's');
|
|
191
|
+
|
|
192
|
+
const inMatch = str.match(/^in\s+(\d+)\s+(second|minute|hour|day|week|month|year)s?$/i);
|
|
193
|
+
if (inMatch) return DateHelper.add(new Date(), parseInt(inMatch[1]), inMatch[2] + 's');
|
|
194
|
+
|
|
195
|
+
return null;
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/** ISO string (UTC) */
|
|
199
|
+
static toISO(date) { return new Date(date).toISOString(); }
|
|
200
|
+
|
|
201
|
+
/** Unix timestamp (seconds) */
|
|
202
|
+
static toUnix(date) { return Math.floor(new Date(date).getTime() / 1000); }
|
|
203
|
+
|
|
204
|
+
/** From Unix timestamp */
|
|
205
|
+
static fromUnix(timestamp) { return new Date(timestamp * 1000); }
|
|
206
|
+
|
|
207
|
+
/** Current timestamp in ms */
|
|
208
|
+
static now() { return Date.now(); }
|
|
209
|
+
|
|
210
|
+
/** Generate date range */
|
|
211
|
+
static range(start, end, step = 'day') {
|
|
212
|
+
const dates = [];
|
|
213
|
+
let current = new Date(start);
|
|
214
|
+
const endDate = new Date(end);
|
|
215
|
+
while (current <= endDate) {
|
|
216
|
+
dates.push(new Date(current));
|
|
217
|
+
current = DateHelper.add(current, 1, step);
|
|
218
|
+
}
|
|
219
|
+
return dates;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
module.exports = { DateHelper };
|
|
@@ -0,0 +1,181 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* VoltJS EventBus
|
|
3
|
+
*
|
|
4
|
+
* Typed event emitter with async support, wildcard patterns, and once listeners.
|
|
5
|
+
*
|
|
6
|
+
* @example
|
|
7
|
+
* const { EventBus } = require('voltjs');
|
|
8
|
+
*
|
|
9
|
+
* const bus = new EventBus();
|
|
10
|
+
* bus.on('user:created', (user) => console.log('Created:', user));
|
|
11
|
+
* bus.on('user:*', (data) => console.log('User event:', data));
|
|
12
|
+
* bus.emit('user:created', { id: 1, name: 'Jane' });
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
'use strict';
|
|
16
|
+
|
|
17
|
+
class EventBus {
|
|
18
|
+
constructor() {
|
|
19
|
+
this._listeners = new Map();
|
|
20
|
+
this._onceListeners = new Map();
|
|
21
|
+
this._wildcardListeners = [];
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/** Register an event listener */
|
|
25
|
+
on(event, handler) {
|
|
26
|
+
if (event.includes('*')) {
|
|
27
|
+
const pattern = new RegExp('^' + event.replace(/\*/g, '.*') + '$');
|
|
28
|
+
this._wildcardListeners.push({ pattern, handler, original: event });
|
|
29
|
+
return this;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
if (!this._listeners.has(event)) {
|
|
33
|
+
this._listeners.set(event, []);
|
|
34
|
+
}
|
|
35
|
+
this._listeners.get(event).push(handler);
|
|
36
|
+
return this;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/** Register a one-time listener */
|
|
40
|
+
once(event, handler) {
|
|
41
|
+
if (!this._onceListeners.has(event)) {
|
|
42
|
+
this._onceListeners.set(event, []);
|
|
43
|
+
}
|
|
44
|
+
this._onceListeners.get(event).push(handler);
|
|
45
|
+
return this;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/** Remove a listener */
|
|
49
|
+
off(event, handler) {
|
|
50
|
+
if (handler) {
|
|
51
|
+
const listeners = this._listeners.get(event);
|
|
52
|
+
if (listeners) {
|
|
53
|
+
this._listeners.set(event, listeners.filter(h => h !== handler));
|
|
54
|
+
}
|
|
55
|
+
const once = this._onceListeners.get(event);
|
|
56
|
+
if (once) {
|
|
57
|
+
this._onceListeners.set(event, once.filter(h => h !== handler));
|
|
58
|
+
}
|
|
59
|
+
this._wildcardListeners = this._wildcardListeners.filter(
|
|
60
|
+
w => !(w.original === event && w.handler === handler)
|
|
61
|
+
);
|
|
62
|
+
} else {
|
|
63
|
+
this._listeners.delete(event);
|
|
64
|
+
this._onceListeners.delete(event);
|
|
65
|
+
}
|
|
66
|
+
return this;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/** Emit an event (supports async handlers) */
|
|
70
|
+
async emit(event, ...args) {
|
|
71
|
+
const results = [];
|
|
72
|
+
|
|
73
|
+
// Exact match listeners
|
|
74
|
+
const listeners = this._listeners.get(event) || [];
|
|
75
|
+
for (const handler of listeners) {
|
|
76
|
+
results.push(await handler(...args));
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// Once listeners
|
|
80
|
+
const onceListeners = this._onceListeners.get(event) || [];
|
|
81
|
+
this._onceListeners.delete(event);
|
|
82
|
+
for (const handler of onceListeners) {
|
|
83
|
+
results.push(await handler(...args));
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Wildcard listeners
|
|
87
|
+
for (const { pattern, handler } of this._wildcardListeners) {
|
|
88
|
+
if (pattern.test(event)) {
|
|
89
|
+
results.push(await handler(...args, event));
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
return results;
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
/** Emit synchronously (no await) */
|
|
97
|
+
emitSync(event, ...args) {
|
|
98
|
+
const listeners = this._listeners.get(event) || [];
|
|
99
|
+
for (const handler of listeners) handler(...args);
|
|
100
|
+
|
|
101
|
+
const onceListeners = this._onceListeners.get(event) || [];
|
|
102
|
+
this._onceListeners.delete(event);
|
|
103
|
+
for (const handler of onceListeners) handler(...args);
|
|
104
|
+
|
|
105
|
+
for (const { pattern, handler } of this._wildcardListeners) {
|
|
106
|
+
if (pattern.test(event)) handler(...args, event);
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/** Wait for an event to fire */
|
|
111
|
+
waitFor(event, timeout = 30000) {
|
|
112
|
+
return new Promise((resolve, reject) => {
|
|
113
|
+
const timer = setTimeout(() => {
|
|
114
|
+
this.off(event, handler);
|
|
115
|
+
reject(new Error(`Timeout waiting for event: ${event}`));
|
|
116
|
+
}, timeout);
|
|
117
|
+
|
|
118
|
+
const handler = (...args) => {
|
|
119
|
+
clearTimeout(timer);
|
|
120
|
+
resolve(args.length === 1 ? args[0] : args);
|
|
121
|
+
};
|
|
122
|
+
|
|
123
|
+
this.once(event, handler);
|
|
124
|
+
});
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
/** Check if event has listeners */
|
|
128
|
+
hasListeners(event) {
|
|
129
|
+
const exact = (this._listeners.get(event) || []).length > 0;
|
|
130
|
+
const once = (this._onceListeners.get(event) || []).length > 0;
|
|
131
|
+
const wildcard = this._wildcardListeners.some(w => w.pattern.test(event));
|
|
132
|
+
return exact || once || wildcard;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
/** Get listener count for an event */
|
|
136
|
+
listenerCount(event) {
|
|
137
|
+
let count = (this._listeners.get(event) || []).length;
|
|
138
|
+
count += (this._onceListeners.get(event) || []).length;
|
|
139
|
+
count += this._wildcardListeners.filter(w => w.pattern.test(event)).length;
|
|
140
|
+
return count;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/** Get all registered event names */
|
|
144
|
+
eventNames() {
|
|
145
|
+
const names = new Set([
|
|
146
|
+
...this._listeners.keys(),
|
|
147
|
+
...this._onceListeners.keys(),
|
|
148
|
+
...this._wildcardListeners.map(w => w.original),
|
|
149
|
+
]);
|
|
150
|
+
return [...names];
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
/** Remove all listeners */
|
|
154
|
+
removeAll() {
|
|
155
|
+
this._listeners.clear();
|
|
156
|
+
this._onceListeners.clear();
|
|
157
|
+
this._wildcardListeners = [];
|
|
158
|
+
return this;
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
/** Create a namespaced event bus */
|
|
162
|
+
namespace(prefix) {
|
|
163
|
+
const self = this;
|
|
164
|
+
return {
|
|
165
|
+
on: (event, handler) => self.on(`${prefix}:${event}`, handler),
|
|
166
|
+
once: (event, handler) => self.once(`${prefix}:${event}`, handler),
|
|
167
|
+
off: (event, handler) => self.off(`${prefix}:${event}`, handler),
|
|
168
|
+
emit: (event, ...args) => self.emit(`${prefix}:${event}`, ...args),
|
|
169
|
+
};
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
/** Pipe events from another event bus */
|
|
173
|
+
pipe(source, events) {
|
|
174
|
+
for (const event of events) {
|
|
175
|
+
source.on(event, (...args) => this.emit(event, ...args));
|
|
176
|
+
}
|
|
177
|
+
return this;
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
module.exports = { EventBus };
|