millas 0.1.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 +137 -0
- package/bin/millas.js +6 -0
- package/package.json +56 -0
- package/src/admin/Admin.js +617 -0
- package/src/admin/index.js +13 -0
- package/src/admin/resources/AdminResource.js +317 -0
- package/src/auth/Auth.js +254 -0
- package/src/auth/AuthController.js +188 -0
- package/src/auth/AuthMiddleware.js +67 -0
- package/src/auth/Hasher.js +51 -0
- package/src/auth/JwtDriver.js +74 -0
- package/src/auth/RoleMiddleware.js +44 -0
- package/src/cache/Cache.js +231 -0
- package/src/cache/drivers/FileDriver.js +152 -0
- package/src/cache/drivers/MemoryDriver.js +158 -0
- package/src/cache/drivers/NullDriver.js +27 -0
- package/src/cache/index.js +8 -0
- package/src/cli.js +27 -0
- package/src/commands/make.js +61 -0
- package/src/commands/migrate.js +174 -0
- package/src/commands/new.js +50 -0
- package/src/commands/queue.js +92 -0
- package/src/commands/route.js +93 -0
- package/src/commands/serve.js +50 -0
- package/src/container/Application.js +177 -0
- package/src/container/Container.js +281 -0
- package/src/container/index.js +13 -0
- package/src/controller/Controller.js +367 -0
- package/src/errors/HttpError.js +29 -0
- package/src/events/Event.js +39 -0
- package/src/events/EventEmitter.js +151 -0
- package/src/events/Listener.js +46 -0
- package/src/events/index.js +15 -0
- package/src/index.js +93 -0
- package/src/mail/Mail.js +210 -0
- package/src/mail/MailMessage.js +196 -0
- package/src/mail/TemplateEngine.js +150 -0
- package/src/mail/drivers/LogDriver.js +36 -0
- package/src/mail/drivers/MailgunDriver.js +84 -0
- package/src/mail/drivers/SendGridDriver.js +97 -0
- package/src/mail/drivers/SmtpDriver.js +67 -0
- package/src/mail/index.js +19 -0
- package/src/middleware/AuthMiddleware.js +46 -0
- package/src/middleware/CorsMiddleware.js +59 -0
- package/src/middleware/LogMiddleware.js +61 -0
- package/src/middleware/Middleware.js +36 -0
- package/src/middleware/MiddlewarePipeline.js +94 -0
- package/src/middleware/ThrottleMiddleware.js +61 -0
- package/src/orm/drivers/DatabaseManager.js +135 -0
- package/src/orm/fields/index.js +132 -0
- package/src/orm/index.js +19 -0
- package/src/orm/migration/MigrationRunner.js +216 -0
- package/src/orm/migration/ModelInspector.js +338 -0
- package/src/orm/migration/SchemaBuilder.js +173 -0
- package/src/orm/model/Model.js +371 -0
- package/src/orm/query/QueryBuilder.js +197 -0
- package/src/providers/AdminServiceProvider.js +40 -0
- package/src/providers/AuthServiceProvider.js +53 -0
- package/src/providers/CacheStorageServiceProvider.js +71 -0
- package/src/providers/DatabaseServiceProvider.js +45 -0
- package/src/providers/EventServiceProvider.js +34 -0
- package/src/providers/MailServiceProvider.js +51 -0
- package/src/providers/ProviderRegistry.js +82 -0
- package/src/providers/QueueServiceProvider.js +52 -0
- package/src/providers/ServiceProvider.js +45 -0
- package/src/queue/Job.js +135 -0
- package/src/queue/Queue.js +147 -0
- package/src/queue/drivers/DatabaseDriver.js +194 -0
- package/src/queue/drivers/SyncDriver.js +72 -0
- package/src/queue/index.js +16 -0
- package/src/queue/workers/QueueWorker.js +140 -0
- package/src/router/MiddlewareRegistry.js +82 -0
- package/src/router/Route.js +255 -0
- package/src/router/RouteGroup.js +19 -0
- package/src/router/RouteRegistry.js +55 -0
- package/src/router/Router.js +138 -0
- package/src/router/index.js +15 -0
- package/src/scaffold/generator.js +34 -0
- package/src/scaffold/maker.js +272 -0
- package/src/scaffold/templates.js +350 -0
- package/src/storage/Storage.js +170 -0
- package/src/storage/drivers/LocalDriver.js +215 -0
- package/src/storage/index.js +6 -0
|
@@ -0,0 +1,194 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* DatabaseDriver
|
|
5
|
+
*
|
|
6
|
+
* Stores jobs in the `millas_jobs` database table.
|
|
7
|
+
* Requires a database connection configured in config/database.js.
|
|
8
|
+
*
|
|
9
|
+
* Run migration to create the table:
|
|
10
|
+
* millas migrate
|
|
11
|
+
*
|
|
12
|
+
* Start the worker:
|
|
13
|
+
* millas queue:work
|
|
14
|
+
* millas queue:work --queue emails,notifications
|
|
15
|
+
* millas queue:work --sleep 3
|
|
16
|
+
*/
|
|
17
|
+
class DatabaseDriver {
|
|
18
|
+
constructor(config = {}) {
|
|
19
|
+
this._config = config;
|
|
20
|
+
this._connection = config.connection || null;
|
|
21
|
+
this._db = null;
|
|
22
|
+
this._table = config.table || 'millas_jobs';
|
|
23
|
+
this._failedTable = config.failedTable || 'millas_failed_jobs';
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
// ─── Push ─────────────────────────────────────────────────────────────────
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Push a serialized job onto the queue.
|
|
30
|
+
*/
|
|
31
|
+
async push(job) {
|
|
32
|
+
const db = this._getDb();
|
|
33
|
+
const record = job.serialize();
|
|
34
|
+
const runAt = record.delay
|
|
35
|
+
? new Date(Date.now() + record.delay * 1000).toISOString()
|
|
36
|
+
: new Date().toISOString();
|
|
37
|
+
|
|
38
|
+
const [id] = await db(this._table).insert({
|
|
39
|
+
queue: record.queue,
|
|
40
|
+
payload: JSON.stringify(record),
|
|
41
|
+
attempts: 0,
|
|
42
|
+
max_tries: record.tries,
|
|
43
|
+
status: 'pending',
|
|
44
|
+
run_at: runAt,
|
|
45
|
+
created_at: new Date().toISOString(),
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
return { id, status: 'queued', queue: record.queue };
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Pop (used by worker) ─────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Fetch the next available job from the queue.
|
|
55
|
+
* Marks it as 'processing' atomically.
|
|
56
|
+
*/
|
|
57
|
+
async pop(queue = 'default') {
|
|
58
|
+
const db = this._getDb();
|
|
59
|
+
const now = new Date().toISOString();
|
|
60
|
+
|
|
61
|
+
const row = await db(this._table)
|
|
62
|
+
.where('queue', queue)
|
|
63
|
+
.where('status', 'pending')
|
|
64
|
+
.where('run_at', '<=', now)
|
|
65
|
+
.orderBy('run_at', 'asc')
|
|
66
|
+
.first();
|
|
67
|
+
|
|
68
|
+
if (!row) return null;
|
|
69
|
+
|
|
70
|
+
await db(this._table).where('id', row.id).update({
|
|
71
|
+
status: 'processing',
|
|
72
|
+
started_at: now,
|
|
73
|
+
attempts: row.attempts + 1,
|
|
74
|
+
});
|
|
75
|
+
|
|
76
|
+
return { ...row, attempts: row.attempts + 1 };
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
// ─── Complete / Fail ──────────────────────────────────────────────────────
|
|
80
|
+
|
|
81
|
+
async complete(id) {
|
|
82
|
+
await this._getDb()(this._table).where('id', id).update({
|
|
83
|
+
status: 'completed',
|
|
84
|
+
finished_at: new Date().toISOString(),
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async fail(id, error, record) {
|
|
89
|
+
const db = this._getDb();
|
|
90
|
+
const row = await db(this._table).where('id', id).first();
|
|
91
|
+
const maxTries = row?.max_tries || 3;
|
|
92
|
+
const attempts = row?.attempts || 1;
|
|
93
|
+
|
|
94
|
+
if (attempts < maxTries) {
|
|
95
|
+
// Schedule retry with backoff
|
|
96
|
+
const backoff = this._backoff(record?.backoff || 'exponential', attempts);
|
|
97
|
+
const runAt = new Date(Date.now() + backoff * 1000).toISOString();
|
|
98
|
+
|
|
99
|
+
await db(this._table).where('id', id).update({
|
|
100
|
+
status: 'pending',
|
|
101
|
+
run_at: runAt,
|
|
102
|
+
last_error: error.message,
|
|
103
|
+
});
|
|
104
|
+
} else {
|
|
105
|
+
// Move to failed jobs table
|
|
106
|
+
await db(this._failedTable).insert({
|
|
107
|
+
queue: row?.queue,
|
|
108
|
+
payload: row?.payload,
|
|
109
|
+
error: error.message,
|
|
110
|
+
failed_at: new Date().toISOString(),
|
|
111
|
+
});
|
|
112
|
+
await db(this._table).where('id', id).delete();
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
async release(id) {
|
|
117
|
+
await this._getDb()(this._table).where('id', id).update({ status: 'pending' });
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─── Stats ────────────────────────────────────────────────────────────────
|
|
121
|
+
|
|
122
|
+
async size(queue = 'default') {
|
|
123
|
+
const result = await this._getDb()(this._table)
|
|
124
|
+
.where('queue', queue).where('status', 'pending').count('* as count').first();
|
|
125
|
+
return Number(result?.count || 0);
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
async clear(queue = 'default') {
|
|
129
|
+
return this._getDb()(this._table)
|
|
130
|
+
.where('queue', queue).where('status', 'pending').delete();
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
async stats() {
|
|
134
|
+
const db = this._getDb();
|
|
135
|
+
const rows = await db(this._table)
|
|
136
|
+
.select('queue', 'status')
|
|
137
|
+
.count('* as count')
|
|
138
|
+
.groupBy('queue', 'status');
|
|
139
|
+
return rows;
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ─── Schema ───────────────────────────────────────────────────────────────
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Create the jobs tables. Called by migrate.
|
|
146
|
+
*/
|
|
147
|
+
async createTables() {
|
|
148
|
+
const db = this._getDb();
|
|
149
|
+
|
|
150
|
+
const jobsExists = await db.schema.hasTable(this._table);
|
|
151
|
+
if (!jobsExists) {
|
|
152
|
+
await db.schema.createTable(this._table, (t) => {
|
|
153
|
+
t.increments('id');
|
|
154
|
+
t.string('queue', 100).notNullable().defaultTo('default').index();
|
|
155
|
+
t.text('payload').notNullable();
|
|
156
|
+
t.integer('attempts').defaultTo(0);
|
|
157
|
+
t.integer('max_tries').defaultTo(3);
|
|
158
|
+
t.string('status', 20).defaultTo('pending').index();
|
|
159
|
+
t.string('last_error').nullable();
|
|
160
|
+
t.timestamp('run_at').nullable().index();
|
|
161
|
+
t.timestamp('started_at').nullable();
|
|
162
|
+
t.timestamp('finished_at').nullable();
|
|
163
|
+
t.timestamp('created_at').nullable();
|
|
164
|
+
});
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
const failedExists = await db.schema.hasTable(this._failedTable);
|
|
168
|
+
if (!failedExists) {
|
|
169
|
+
await db.schema.createTable(this._failedTable, (t) => {
|
|
170
|
+
t.increments('id');
|
|
171
|
+
t.string('queue', 100).nullable();
|
|
172
|
+
t.text('payload').nullable();
|
|
173
|
+
t.text('error').nullable();
|
|
174
|
+
t.timestamp('failed_at').nullable();
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
180
|
+
|
|
181
|
+
_getDb() {
|
|
182
|
+
if (this._db) return this._db;
|
|
183
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
184
|
+
this._db = DatabaseManager.connection(this._connection);
|
|
185
|
+
return this._db;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
_backoff(strategy, attempt) {
|
|
189
|
+
if (strategy === 'exponential') return Math.min(Math.pow(2, attempt), 3600);
|
|
190
|
+
return 60; // fixed: 60 seconds
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = DatabaseDriver;
|
|
@@ -0,0 +1,72 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* SyncDriver
|
|
5
|
+
*
|
|
6
|
+
* Executes jobs immediately and synchronously in the current process.
|
|
7
|
+
* Default driver — no Redis or DB required.
|
|
8
|
+
*
|
|
9
|
+
* Perfect for:
|
|
10
|
+
* - Local development
|
|
11
|
+
* - Testing
|
|
12
|
+
* - Simple apps that don't need background processing
|
|
13
|
+
*
|
|
14
|
+
* Set QUEUE_DRIVER=sync in .env
|
|
15
|
+
*/
|
|
16
|
+
class SyncDriver {
|
|
17
|
+
constructor() {
|
|
18
|
+
this._processed = [];
|
|
19
|
+
this._failed = [];
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
/**
|
|
23
|
+
* Push and immediately execute a job.
|
|
24
|
+
*/
|
|
25
|
+
async push(job) {
|
|
26
|
+
const record = {
|
|
27
|
+
id: this._id(),
|
|
28
|
+
job: job.constructor.name,
|
|
29
|
+
queue: job._queue || job.constructor.queue || 'default',
|
|
30
|
+
payload: job._getPayload(),
|
|
31
|
+
attempts: 0,
|
|
32
|
+
status: 'processing',
|
|
33
|
+
createdAt: new Date().toISOString(),
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
try {
|
|
37
|
+
await job.handle();
|
|
38
|
+
record.status = 'completed';
|
|
39
|
+
record.finishedAt = new Date().toISOString();
|
|
40
|
+
this._processed.push(record);
|
|
41
|
+
return { id: record.id, status: 'completed' };
|
|
42
|
+
} catch (error) {
|
|
43
|
+
record.status = 'failed';
|
|
44
|
+
record.error = error.message;
|
|
45
|
+
record.failedAt = new Date().toISOString();
|
|
46
|
+
this._failed.push(record);
|
|
47
|
+
|
|
48
|
+
if (typeof job.failed === 'function') {
|
|
49
|
+
await job.failed(error).catch(() => {});
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
throw error;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/**
|
|
57
|
+
* SyncDriver has no persistent queue — nothing to work.
|
|
58
|
+
*/
|
|
59
|
+
async work() {
|
|
60
|
+
return { processed: 0, message: 'SyncDriver: jobs run immediately on dispatch.' };
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
async size(queue = 'default') { return 0; }
|
|
64
|
+
async clear(queue = 'default') { return 0; }
|
|
65
|
+
|
|
66
|
+
processed() { return [...this._processed]; }
|
|
67
|
+
failed() { return [...this._failed]; }
|
|
68
|
+
|
|
69
|
+
_id() { return `sync_${Date.now()}_${Math.random().toString(36).slice(2, 8)}`; }
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
module.exports = SyncDriver;
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const Queue = require('./Queue');
|
|
4
|
+
const Job = require('./Job');
|
|
5
|
+
const QueueWorker = require('./workers/QueueWorker');
|
|
6
|
+
const SyncDriver = require('./drivers/SyncDriver');
|
|
7
|
+
const DatabaseDriver = require('./drivers/DatabaseDriver');
|
|
8
|
+
|
|
9
|
+
module.exports = {
|
|
10
|
+
Queue,
|
|
11
|
+
Job,
|
|
12
|
+
QueueWorker,
|
|
13
|
+
SyncDriver,
|
|
14
|
+
DatabaseDriver,
|
|
15
|
+
dispatch: Queue.dispatch,
|
|
16
|
+
};
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* QueueWorker
|
|
5
|
+
*
|
|
6
|
+
* Polls the queue driver and executes pending jobs.
|
|
7
|
+
* Started by: millas queue:work
|
|
8
|
+
*
|
|
9
|
+
* Usage:
|
|
10
|
+
* const worker = new QueueWorker(driver, registry, options);
|
|
11
|
+
* await worker.start();
|
|
12
|
+
*/
|
|
13
|
+
class QueueWorker {
|
|
14
|
+
constructor(driver, jobRegistry, options = {}) {
|
|
15
|
+
this._driver = driver;
|
|
16
|
+
this._registry = jobRegistry;
|
|
17
|
+
this._queues = options.queues || ['default'];
|
|
18
|
+
this._sleep = options.sleep || 3;
|
|
19
|
+
this._maxJobs = options.maxJobs || Infinity;
|
|
20
|
+
this._handleSignals = options.handleSignals !== false && options.maxJobs === undefined;
|
|
21
|
+
this._silent = options.silent || options.maxJobs !== undefined;
|
|
22
|
+
this._running = false;
|
|
23
|
+
this._processed = 0;
|
|
24
|
+
this._failed = 0;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Start the worker loop.
|
|
29
|
+
*/
|
|
30
|
+
async start() {
|
|
31
|
+
this._running = true;
|
|
32
|
+
|
|
33
|
+
if (!this._silent) {
|
|
34
|
+
console.log(`\n ⚡ Queue worker started`);
|
|
35
|
+
console.log(` Queues: ${this._queues.join(', ')}`);
|
|
36
|
+
console.log(` Sleep: ${this._sleep}s between polls\n`);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
// Only register signal handlers when running as main process
|
|
40
|
+
if (this._handleSignals) {
|
|
41
|
+
process.on('SIGINT', () => this.stop('SIGINT'));
|
|
42
|
+
process.on('SIGTERM', () => this.stop('SIGTERM'));
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
while (this._running && this._processed < this._maxJobs) {
|
|
46
|
+
let worked = false;
|
|
47
|
+
|
|
48
|
+
for (const queue of this._queues) {
|
|
49
|
+
const record = await this._driver.pop(queue);
|
|
50
|
+
if (record) {
|
|
51
|
+
await this._process(record);
|
|
52
|
+
worked = true;
|
|
53
|
+
break;
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (!worked) {
|
|
58
|
+
// If maxJobs is finite and queue is empty, stop
|
|
59
|
+
if (this._maxJobs < Infinity) break;
|
|
60
|
+
await this._sleep_ms(this._sleep * 1000);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (!this._silent) {
|
|
65
|
+
console.log(`\n Worker stopped. Processed: ${this._processed}, Failed: ${this._failed}\n`);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
stop(signal = 'manual') {
|
|
70
|
+
if (!this._silent) console.log(`\n [${signal}] Stopping worker gracefully...`);
|
|
71
|
+
this._running = false;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
stats() {
|
|
75
|
+
return { processed: this._processed, failed: this._failed };
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
// ─── Internal ─────────────────────────────────────────────────────────────
|
|
79
|
+
|
|
80
|
+
async _process(record) {
|
|
81
|
+
let payload;
|
|
82
|
+
try {
|
|
83
|
+
payload = JSON.parse(record.payload);
|
|
84
|
+
} catch {
|
|
85
|
+
console.error(` ✖ Failed to parse job payload (id: ${record.id})`);
|
|
86
|
+
await this._driver.fail(record.id, new Error('Invalid payload'), {});
|
|
87
|
+
this._failed++;
|
|
88
|
+
return;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const JobClass = this._registry.get(payload.class);
|
|
92
|
+
if (!JobClass) {
|
|
93
|
+
console.error(` ✖ Unknown job class: ${payload.class}`);
|
|
94
|
+
await this._driver.fail(record.id, new Error(`Unknown job: ${payload.class}`), payload);
|
|
95
|
+
this._failed++;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
const job = JobClass.deserialize
|
|
100
|
+
? JobClass.deserialize({ ...record, payload: payload.payload || {} }, JobClass)
|
|
101
|
+
: Object.assign(new JobClass(), payload.payload || {});
|
|
102
|
+
|
|
103
|
+
const start = Date.now();
|
|
104
|
+
process.stdout.write(` ⟳ ${payload.class} (id: ${record.id})... `);
|
|
105
|
+
|
|
106
|
+
try {
|
|
107
|
+
await Promise.race([
|
|
108
|
+
job.handle(),
|
|
109
|
+
this._timeout(payload.timeout || 60),
|
|
110
|
+
]);
|
|
111
|
+
|
|
112
|
+
const ms = Date.now() - start;
|
|
113
|
+
console.log(`\x1b[32m✔\x1b[0m ${ms}ms`);
|
|
114
|
+
await this._driver.complete(record.id);
|
|
115
|
+
this._processed++;
|
|
116
|
+
} catch (error) {
|
|
117
|
+
const ms = Date.now() - start;
|
|
118
|
+
console.log(`\x1b[31m✖\x1b[0m ${ms}ms — ${error.message}`);
|
|
119
|
+
await this._driver.fail(record.id, error, payload);
|
|
120
|
+
|
|
121
|
+
if (typeof job.failed === 'function') {
|
|
122
|
+
await job.failed(error).catch(() => {});
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
this._failed++;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
_timeout(seconds) {
|
|
130
|
+
return new Promise((_, reject) =>
|
|
131
|
+
setTimeout(() => reject(new Error(`Job timed out after ${seconds}s`)), seconds * 1000)
|
|
132
|
+
);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
_sleep_ms(ms) {
|
|
136
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
module.exports = QueueWorker;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* MiddlewareRegistry
|
|
5
|
+
*
|
|
6
|
+
* Maps string aliases → middleware handler classes or functions.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* MiddlewareRegistry.register('auth', AuthMiddleware);
|
|
10
|
+
* MiddlewareRegistry.register('throttle', ThrottleMiddleware);
|
|
11
|
+
*
|
|
12
|
+
* Registered automatically by AppServiceProvider (Phase 3+).
|
|
13
|
+
*/
|
|
14
|
+
class MiddlewareRegistry {
|
|
15
|
+
constructor() {
|
|
16
|
+
this._map = {};
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register a middleware alias.
|
|
21
|
+
* @param {string} alias
|
|
22
|
+
* @param {Function|object} handler — class with handle() or raw Express fn
|
|
23
|
+
*/
|
|
24
|
+
register(alias, handler) {
|
|
25
|
+
this._map[alias] = handler;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Resolve a single alias to an Express-compatible function.
|
|
30
|
+
* @param {string|Function} aliasOrFn
|
|
31
|
+
* @returns {Function}
|
|
32
|
+
*/
|
|
33
|
+
resolve(aliasOrFn) {
|
|
34
|
+
if (typeof aliasOrFn === 'function') return aliasOrFn;
|
|
35
|
+
|
|
36
|
+
const Handler = this._map[aliasOrFn];
|
|
37
|
+
if (!Handler) {
|
|
38
|
+
throw new Error(`Middleware "${aliasOrFn}" is not registered.`);
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
// Pre-instantiated object with handle() method (e.g. new ThrottleMiddleware())
|
|
42
|
+
if (typeof Handler === 'object' && Handler !== null && typeof Handler.handle === 'function') {
|
|
43
|
+
return (req, res, next) => {
|
|
44
|
+
const result = Handler.handle(req, res, next);
|
|
45
|
+
if (result && typeof result.catch === 'function') result.catch(next);
|
|
46
|
+
};
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Class with handle() on prototype
|
|
50
|
+
if (typeof Handler === 'function' && Handler.prototype && typeof Handler.prototype.handle === 'function') {
|
|
51
|
+
const instance = new Handler();
|
|
52
|
+
return (req, res, next) => {
|
|
53
|
+
const result = instance.handle(req, res, next);
|
|
54
|
+
if (result && typeof result.catch === 'function') result.catch(next);
|
|
55
|
+
};
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
// Raw Express function
|
|
59
|
+
if (typeof Handler === 'function') return Handler;
|
|
60
|
+
|
|
61
|
+
throw new Error(`Middleware "${aliasOrFn}" must be a function or class with handle().`);
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
/**
|
|
65
|
+
* Resolve an array of aliases/functions.
|
|
66
|
+
* @param {Array} list
|
|
67
|
+
* @returns {Function[]}
|
|
68
|
+
*/
|
|
69
|
+
resolveAll(list = []) {
|
|
70
|
+
return list.map(m => this.resolve(m));
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
has(alias) {
|
|
74
|
+
return Object.prototype.hasOwnProperty.call(this._map, alias);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
all() {
|
|
78
|
+
return { ...this._map };
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = MiddlewareRegistry;
|