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,71 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const Cache = require('../cache/Cache');
|
|
5
|
+
const Storage = require('../storage/Storage');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* CacheServiceProvider
|
|
9
|
+
*
|
|
10
|
+
* Configures the Cache facade with config/cache.js settings.
|
|
11
|
+
*/
|
|
12
|
+
class CacheServiceProvider extends ServiceProvider {
|
|
13
|
+
register(container) {
|
|
14
|
+
container.instance('Cache', Cache);
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
async boot() {
|
|
18
|
+
let cacheConfig;
|
|
19
|
+
try {
|
|
20
|
+
cacheConfig = require(process.cwd() + '/config/cache');
|
|
21
|
+
} catch {
|
|
22
|
+
cacheConfig = {
|
|
23
|
+
default: process.env.CACHE_DRIVER || 'memory',
|
|
24
|
+
prefix: process.env.CACHE_PREFIX || '',
|
|
25
|
+
drivers: {
|
|
26
|
+
memory: {},
|
|
27
|
+
file: { path: 'storage/cache' },
|
|
28
|
+
null: {},
|
|
29
|
+
},
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
Cache.configure(cacheConfig);
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* StorageServiceProvider
|
|
38
|
+
*
|
|
39
|
+
* Configures the Storage facade with config/storage.js settings.
|
|
40
|
+
*/
|
|
41
|
+
class StorageServiceProvider extends ServiceProvider {
|
|
42
|
+
register(container) {
|
|
43
|
+
container.instance('Storage', Storage);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
async boot() {
|
|
47
|
+
let storageConfig;
|
|
48
|
+
try {
|
|
49
|
+
storageConfig = require(process.cwd() + '/config/storage');
|
|
50
|
+
} catch {
|
|
51
|
+
storageConfig = {
|
|
52
|
+
default: process.env.STORAGE_DRIVER || 'local',
|
|
53
|
+
disks: {
|
|
54
|
+
local: {
|
|
55
|
+
driver: 'local',
|
|
56
|
+
root: 'storage/uploads',
|
|
57
|
+
baseUrl: '/storage',
|
|
58
|
+
},
|
|
59
|
+
public: {
|
|
60
|
+
driver: 'local',
|
|
61
|
+
root: 'public/storage',
|
|
62
|
+
baseUrl: '/storage',
|
|
63
|
+
},
|
|
64
|
+
},
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
Storage.configure(storageConfig);
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
module.exports = { CacheServiceProvider, StorageServiceProvider };
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const DatabaseManager = require('../orm/drivers/DatabaseManager');
|
|
5
|
+
const SchemaBuilder = require('../orm/migration/SchemaBuilder');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* DatabaseServiceProvider
|
|
9
|
+
*
|
|
10
|
+
* Configures the DatabaseManager with the app's database config
|
|
11
|
+
* and registers DB-related bindings into the container.
|
|
12
|
+
*
|
|
13
|
+
* Add to bootstrap/app.js:
|
|
14
|
+
* app.providers([DatabaseServiceProvider, AppServiceProvider])
|
|
15
|
+
*/
|
|
16
|
+
class DatabaseServiceProvider extends ServiceProvider {
|
|
17
|
+
register(container) {
|
|
18
|
+
// Make DatabaseManager available as a singleton in the container
|
|
19
|
+
container.instance('db', DatabaseManager);
|
|
20
|
+
container.instance('DatabaseManager', DatabaseManager);
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async boot(container) {
|
|
24
|
+
// Load the database config
|
|
25
|
+
let dbConfig;
|
|
26
|
+
try {
|
|
27
|
+
dbConfig = require(process.cwd() + '/config/database');
|
|
28
|
+
} catch {
|
|
29
|
+
// Fallback for tests / programmatic use
|
|
30
|
+
dbConfig = {
|
|
31
|
+
default: 'sqlite',
|
|
32
|
+
connections: {
|
|
33
|
+
sqlite: { driver: 'sqlite', database: ':memory:' },
|
|
34
|
+
},
|
|
35
|
+
};
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
DatabaseManager.configure(dbConfig);
|
|
39
|
+
|
|
40
|
+
// Register SchemaBuilder against the default connection
|
|
41
|
+
container.instance('schema', new SchemaBuilder(DatabaseManager.connection()));
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = DatabaseServiceProvider;
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const EventEmitter = require('../events/EventEmitter');
|
|
5
|
+
const { emit } = require('../events/EventEmitter');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* EventServiceProvider
|
|
9
|
+
*
|
|
10
|
+
* Registers the EventEmitter singleton in the container
|
|
11
|
+
* and connects it to the Queue for async listeners.
|
|
12
|
+
*
|
|
13
|
+
* Add to bootstrap/app.js:
|
|
14
|
+
* app.providers([..., EventServiceProvider, AppServiceProvider])
|
|
15
|
+
*
|
|
16
|
+
* Register event → listener mappings in AppServiceProvider.boot():
|
|
17
|
+
* EventEmitter.listen(UserRegistered, [SendWelcomeEmail, NotifyAdmin]);
|
|
18
|
+
*/
|
|
19
|
+
class EventServiceProvider extends ServiceProvider {
|
|
20
|
+
register(container) {
|
|
21
|
+
container.instance('EventEmitter', EventEmitter);
|
|
22
|
+
container.instance('emit', emit);
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
async boot(container) {
|
|
26
|
+
// Connect queue to emitter for async listener support
|
|
27
|
+
if (container.has('Queue')) {
|
|
28
|
+
const Queue = container.make('Queue');
|
|
29
|
+
EventEmitter.setQueue(Queue);
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
module.exports = EventServiceProvider;
|
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const Mail = require('../mail/Mail');
|
|
5
|
+
const MailMessage = require('../mail/MailMessage');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* MailServiceProvider
|
|
9
|
+
*
|
|
10
|
+
* Configures the Mail facade with config/mail.js settings.
|
|
11
|
+
*
|
|
12
|
+
* Add to bootstrap/app.js:
|
|
13
|
+
* app.providers([
|
|
14
|
+
* DatabaseServiceProvider,
|
|
15
|
+
* AuthServiceProvider,
|
|
16
|
+
* MailServiceProvider,
|
|
17
|
+
* AppServiceProvider,
|
|
18
|
+
* ])
|
|
19
|
+
*/
|
|
20
|
+
class MailServiceProvider extends ServiceProvider {
|
|
21
|
+
register(container) {
|
|
22
|
+
container.instance('Mail', Mail);
|
|
23
|
+
container.instance('MailMessage', MailMessage);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
async boot(container) {
|
|
27
|
+
let mailConfig;
|
|
28
|
+
try {
|
|
29
|
+
mailConfig = require(process.cwd() + '/config/mail');
|
|
30
|
+
} catch {
|
|
31
|
+
mailConfig = {
|
|
32
|
+
default: process.env.MAIL_DRIVER || 'log',
|
|
33
|
+
from: { address: 'noreply@millas.dev', name: 'Millas' },
|
|
34
|
+
drivers: {
|
|
35
|
+
log: {},
|
|
36
|
+
smtp: {
|
|
37
|
+
host: process.env.MAIL_HOST || 'localhost',
|
|
38
|
+
port: Number(process.env.MAIL_PORT) || 587,
|
|
39
|
+
username: process.env.MAIL_USERNAME || '',
|
|
40
|
+
password: process.env.MAIL_PASSWORD || '',
|
|
41
|
+
encryption: 'tls',
|
|
42
|
+
},
|
|
43
|
+
},
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
Mail.configure(mailConfig);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
module.exports = MailServiceProvider;
|
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ProviderRegistry
|
|
5
|
+
*
|
|
6
|
+
* Manages the lifecycle of all service providers:
|
|
7
|
+
*
|
|
8
|
+
* 1. Load — require() each provider file
|
|
9
|
+
* 2. Register — call provider.register(container) on all providers
|
|
10
|
+
* 3. Boot — call provider.boot(container, app) on all providers
|
|
11
|
+
*
|
|
12
|
+
* This two-phase approach (register then boot) ensures every binding
|
|
13
|
+
* exists before any provider tries to resolve another.
|
|
14
|
+
*
|
|
15
|
+
* Usage (in bootstrap/app.js):
|
|
16
|
+
*
|
|
17
|
+
* const registry = new ProviderRegistry(container, app);
|
|
18
|
+
* registry.add(AppServiceProvider);
|
|
19
|
+
* registry.add('./providers/DatabaseServiceProvider');
|
|
20
|
+
* await registry.boot();
|
|
21
|
+
*/
|
|
22
|
+
class ProviderRegistry {
|
|
23
|
+
constructor(container, expressApp) {
|
|
24
|
+
this._container = container;
|
|
25
|
+
this._app = expressApp;
|
|
26
|
+
this._providers = [];
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Add a provider class or path to the registry.
|
|
31
|
+
* @param {Function|string} provider — class or file path
|
|
32
|
+
*/
|
|
33
|
+
add(provider) {
|
|
34
|
+
let Cls = provider;
|
|
35
|
+
|
|
36
|
+
if (typeof provider === 'string') {
|
|
37
|
+
Cls = require(provider);
|
|
38
|
+
// Support ES module default export
|
|
39
|
+
if (Cls.default) Cls = Cls.default;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
this._providers.push(new Cls());
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Add multiple providers at once.
|
|
48
|
+
* @param {Array} providers
|
|
49
|
+
*/
|
|
50
|
+
addMany(providers = []) {
|
|
51
|
+
for (const p of providers) this.add(p);
|
|
52
|
+
return this;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* Run the full register → boot lifecycle.
|
|
57
|
+
*/
|
|
58
|
+
async boot() {
|
|
59
|
+
// Phase 1: register all bindings
|
|
60
|
+
for (const provider of this._providers) {
|
|
61
|
+
if (typeof provider.register === 'function') {
|
|
62
|
+
provider.register(this._container);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Phase 2: boot all providers (async-safe)
|
|
67
|
+
for (const provider of this._providers) {
|
|
68
|
+
if (typeof provider.boot === 'function') {
|
|
69
|
+
await provider.boot(this._container, this._app);
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Return a list of provider class names (for debugging).
|
|
76
|
+
*/
|
|
77
|
+
list() {
|
|
78
|
+
return this._providers.map(p => p.constructor.name);
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
module.exports = ProviderRegistry;
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const ServiceProvider = require('./ServiceProvider');
|
|
4
|
+
const Queue = require('../queue/Queue');
|
|
5
|
+
const { dispatch } = require('../queue/Queue');
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* QueueServiceProvider
|
|
9
|
+
*
|
|
10
|
+
* Configures the Queue facade and registers it in the container.
|
|
11
|
+
* Also connects the Mail facade to the queue for async sending.
|
|
12
|
+
*
|
|
13
|
+
* Add to bootstrap/app.js:
|
|
14
|
+
* app.providers([
|
|
15
|
+
* DatabaseServiceProvider,
|
|
16
|
+
* AuthServiceProvider,
|
|
17
|
+
* MailServiceProvider,
|
|
18
|
+
* QueueServiceProvider,
|
|
19
|
+
* AppServiceProvider,
|
|
20
|
+
* ])
|
|
21
|
+
*/
|
|
22
|
+
class QueueServiceProvider extends ServiceProvider {
|
|
23
|
+
register(container) {
|
|
24
|
+
container.instance('Queue', Queue);
|
|
25
|
+
container.instance('dispatch', dispatch);
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
async boot(container) {
|
|
29
|
+
let queueConfig;
|
|
30
|
+
try {
|
|
31
|
+
queueConfig = require(process.cwd() + '/config/queue');
|
|
32
|
+
} catch {
|
|
33
|
+
queueConfig = {
|
|
34
|
+
default: process.env.QUEUE_DRIVER || 'sync',
|
|
35
|
+
drivers: {
|
|
36
|
+
sync: {},
|
|
37
|
+
database: { connection: null, table: 'millas_jobs' },
|
|
38
|
+
},
|
|
39
|
+
};
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
Queue.configure(queueConfig);
|
|
43
|
+
|
|
44
|
+
// Connect Mail to Queue so Mail.queue() works
|
|
45
|
+
if (container.has('Mail')) {
|
|
46
|
+
const Mail = container.make('Mail');
|
|
47
|
+
Mail.setQueue(Queue);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
module.exports = QueueServiceProvider;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* ServiceProvider
|
|
5
|
+
*
|
|
6
|
+
* Base class for all Millas service providers.
|
|
7
|
+
*
|
|
8
|
+
* Providers have two lifecycle hooks:
|
|
9
|
+
*
|
|
10
|
+
* register(container)
|
|
11
|
+
* Called first. Bind things into the container.
|
|
12
|
+
* Do NOT use other bindings here — they may not exist yet.
|
|
13
|
+
*
|
|
14
|
+
* boot(container, app)
|
|
15
|
+
* Called after ALL providers have registered.
|
|
16
|
+
* Safe to resolve other bindings and set up routes,
|
|
17
|
+
* event listeners, middleware, etc.
|
|
18
|
+
*
|
|
19
|
+
* Usage:
|
|
20
|
+
* class AppServiceProvider extends ServiceProvider {
|
|
21
|
+
* register(container) {
|
|
22
|
+
* container.singleton('UserService', UserService);
|
|
23
|
+
* }
|
|
24
|
+
* async boot(container, app) {
|
|
25
|
+
* const logger = container.make('Logger');
|
|
26
|
+
* logger.info('AppServiceProvider booted');
|
|
27
|
+
* }
|
|
28
|
+
* }
|
|
29
|
+
*/
|
|
30
|
+
class ServiceProvider {
|
|
31
|
+
/**
|
|
32
|
+
* Register bindings into the container.
|
|
33
|
+
* @param {import('./Container')} container
|
|
34
|
+
*/
|
|
35
|
+
register(container) {}
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* Bootstrap services after all providers have registered.
|
|
39
|
+
* @param {import('./Container')} container
|
|
40
|
+
* @param {import('express').Application} app
|
|
41
|
+
*/
|
|
42
|
+
async boot(container, app) {}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
module.exports = ServiceProvider;
|
package/src/queue/Job.js
ADDED
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Job
|
|
5
|
+
*
|
|
6
|
+
* Base class for all Millas background jobs.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* class SendEmailJob extends Job {
|
|
10
|
+
* static queue = 'emails'; // which queue to push to
|
|
11
|
+
* static tries = 3; // max attempts before failing
|
|
12
|
+
* static delay = 0; // seconds before first attempt
|
|
13
|
+
* static timeout = 60; // seconds before job times out
|
|
14
|
+
* static backoff = 'exponential'; // 'fixed' | 'exponential'
|
|
15
|
+
*
|
|
16
|
+
* constructor(user, subject) {
|
|
17
|
+
* super();
|
|
18
|
+
* this.user = user;
|
|
19
|
+
* this.subject = subject;
|
|
20
|
+
* }
|
|
21
|
+
*
|
|
22
|
+
* async handle() {
|
|
23
|
+
* await Mail.send({ to: this.user.email, subject: this.subject });
|
|
24
|
+
* }
|
|
25
|
+
*
|
|
26
|
+
* async failed(error) {
|
|
27
|
+
* console.error('SendEmailJob failed:', error.message);
|
|
28
|
+
* }
|
|
29
|
+
* }
|
|
30
|
+
*
|
|
31
|
+
* Dispatch:
|
|
32
|
+
* dispatch(new SendEmailJob(user, 'Welcome!'));
|
|
33
|
+
* dispatch(new SendEmailJob(user)).delay(60); // delay 60 seconds
|
|
34
|
+
* dispatch(new SendEmailJob(user)).onQueue('priority');
|
|
35
|
+
*/
|
|
36
|
+
class Job {
|
|
37
|
+
// ─── Static config (override per job class) ───────────────────────────────
|
|
38
|
+
|
|
39
|
+
/** Queue name this job belongs to */
|
|
40
|
+
static queue = 'default';
|
|
41
|
+
|
|
42
|
+
/** Max attempts before marking as failed */
|
|
43
|
+
static tries = 3;
|
|
44
|
+
|
|
45
|
+
/** Seconds to wait before first execution */
|
|
46
|
+
static delay = 0;
|
|
47
|
+
|
|
48
|
+
/** Seconds before the job is considered timed out */
|
|
49
|
+
static timeout = 60;
|
|
50
|
+
|
|
51
|
+
/** Backoff strategy: 'fixed' | 'exponential' */
|
|
52
|
+
static backoff = 'exponential';
|
|
53
|
+
|
|
54
|
+
// ─── Instance ─────────────────────────────────────────────────────────────
|
|
55
|
+
|
|
56
|
+
constructor() {
|
|
57
|
+
this._queue = null; // runtime override
|
|
58
|
+
this._delay = null; // runtime override
|
|
59
|
+
this._attempts = 0;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Execute the job. Must be implemented by subclasses.
|
|
64
|
+
*/
|
|
65
|
+
async handle() {
|
|
66
|
+
throw new Error(`${this.constructor.name} must implement handle()`);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
/**
|
|
70
|
+
* Called when the job fails after all retries.
|
|
71
|
+
* Override to notify, log, or clean up.
|
|
72
|
+
*/
|
|
73
|
+
async failed(error) {}
|
|
74
|
+
|
|
75
|
+
// ─── Fluent dispatch modifiers ────────────────────────────────────────────
|
|
76
|
+
|
|
77
|
+
/**
|
|
78
|
+
* Override the queue name at dispatch time.
|
|
79
|
+
*/
|
|
80
|
+
onQueue(name) {
|
|
81
|
+
this._queue = name;
|
|
82
|
+
return this;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
/**
|
|
86
|
+
* Delay execution by N seconds.
|
|
87
|
+
*/
|
|
88
|
+
delay(seconds) {
|
|
89
|
+
this._delay = seconds;
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
// ─── Serialisation ────────────────────────────────────────────────────────
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Serialize this job for storage in the queue driver.
|
|
97
|
+
*/
|
|
98
|
+
serialize() {
|
|
99
|
+
return {
|
|
100
|
+
class: this.constructor.name,
|
|
101
|
+
queue: this._queue || this.constructor.queue || 'default',
|
|
102
|
+
tries: this.constructor.tries || 3,
|
|
103
|
+
timeout: this.constructor.timeout || 60,
|
|
104
|
+
backoff: this.constructor.backoff || 'exponential',
|
|
105
|
+
delay: this._delay !== null
|
|
106
|
+
? this._delay
|
|
107
|
+
: (this.constructor.delay || 0),
|
|
108
|
+
payload: this._getPayload(),
|
|
109
|
+
createdAt: new Date().toISOString(),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Get the serializable payload (all own non-private properties).
|
|
115
|
+
*/
|
|
116
|
+
_getPayload() {
|
|
117
|
+
const payload = {};
|
|
118
|
+
for (const key of Object.keys(this)) {
|
|
119
|
+
if (!key.startsWith('_')) payload[key] = this[key];
|
|
120
|
+
}
|
|
121
|
+
return payload;
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Restore a job instance from a serialized record.
|
|
126
|
+
*/
|
|
127
|
+
static deserialize(record, JobClass) {
|
|
128
|
+
const instance = new JobClass();
|
|
129
|
+
Object.assign(instance, record.payload || {});
|
|
130
|
+
instance._attempts = record.attempts || 0;
|
|
131
|
+
return instance;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
module.exports = Job;
|
|
@@ -0,0 +1,147 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const SyncDriver = require('./drivers/SyncDriver');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Queue
|
|
7
|
+
*
|
|
8
|
+
* The primary queue facade.
|
|
9
|
+
*
|
|
10
|
+
* Usage:
|
|
11
|
+
* const { Queue, dispatch } = require('millas/src');
|
|
12
|
+
*
|
|
13
|
+
* // Dispatch a job
|
|
14
|
+
* await dispatch(new SendEmailJob(user));
|
|
15
|
+
*
|
|
16
|
+
* // Dispatch with options
|
|
17
|
+
* await dispatch(new SendEmailJob(user).delay(60).onQueue('emails'));
|
|
18
|
+
*
|
|
19
|
+
* // Direct facade methods
|
|
20
|
+
* await Queue.push(new SendEmailJob(user));
|
|
21
|
+
* await Queue.size('default');
|
|
22
|
+
* await Queue.clear('default');
|
|
23
|
+
*/
|
|
24
|
+
class Queue {
|
|
25
|
+
constructor() {
|
|
26
|
+
this._driver = null;
|
|
27
|
+
this._config = null;
|
|
28
|
+
this._registry = new Map(); // className → JobClass
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
// ─── Configuration ─────────────────────────────────────────────────────────
|
|
32
|
+
|
|
33
|
+
configure(config) {
|
|
34
|
+
this._config = config;
|
|
35
|
+
this._driver = null; // reset so driver is rebuilt with new config
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Register a Job class so the worker can deserialize it.
|
|
40
|
+
*/
|
|
41
|
+
register(JobClass) {
|
|
42
|
+
this._registry.set(JobClass.name, JobClass);
|
|
43
|
+
return this;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
registerMany(classes = []) {
|
|
47
|
+
classes.forEach(c => this.register(c));
|
|
48
|
+
return this;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
// ─── Core API ──────────────────────────────────────────────────────────────
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Push a job onto the queue.
|
|
55
|
+
* Returns { id, status, queue }
|
|
56
|
+
*/
|
|
57
|
+
async push(job) {
|
|
58
|
+
return this._getDriver().push(job);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Get the number of pending jobs on a queue.
|
|
63
|
+
*/
|
|
64
|
+
async size(queue = 'default') {
|
|
65
|
+
return this._getDriver().size(queue);
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
/**
|
|
69
|
+
* Clear all pending jobs from a queue.
|
|
70
|
+
*/
|
|
71
|
+
async clear(queue = 'default') {
|
|
72
|
+
return this._getDriver().clear(queue);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Get queue statistics (DatabaseDriver only).
|
|
77
|
+
*/
|
|
78
|
+
async stats() {
|
|
79
|
+
const driver = this._getDriver();
|
|
80
|
+
if (typeof driver.stats === 'function') return driver.stats();
|
|
81
|
+
return {};
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Get processed jobs (SyncDriver only — for testing).
|
|
86
|
+
*/
|
|
87
|
+
processed() {
|
|
88
|
+
const driver = this._getDriver();
|
|
89
|
+
if (typeof driver.processed === 'function') return driver.processed();
|
|
90
|
+
return [];
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
/**
|
|
94
|
+
* Get the job registry Map (used by QueueWorker).
|
|
95
|
+
*/
|
|
96
|
+
getRegistry() {
|
|
97
|
+
return this._registry;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
getDriver() {
|
|
101
|
+
return this._getDriver();
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// ─── Internal ──────────────────────────────────────────────────────────────
|
|
105
|
+
|
|
106
|
+
_getDriver() {
|
|
107
|
+
if (this._driver) return this._driver;
|
|
108
|
+
|
|
109
|
+
const driverName = this._config?.default
|
|
110
|
+
|| process.env.QUEUE_DRIVER
|
|
111
|
+
|| 'sync';
|
|
112
|
+
|
|
113
|
+
const driverConf = this._config?.drivers?.[driverName] || {};
|
|
114
|
+
|
|
115
|
+
switch (driverName) {
|
|
116
|
+
case 'database': {
|
|
117
|
+
const DatabaseDriver = require('./drivers/DatabaseDriver');
|
|
118
|
+
this._driver = new DatabaseDriver(driverConf);
|
|
119
|
+
break;
|
|
120
|
+
}
|
|
121
|
+
case 'sync':
|
|
122
|
+
default: {
|
|
123
|
+
this._driver = new SyncDriver();
|
|
124
|
+
break;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
return this._driver;
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Singleton
|
|
133
|
+
const queue = new Queue();
|
|
134
|
+
module.exports = queue;
|
|
135
|
+
module.exports.Queue = Queue;
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* dispatch()
|
|
139
|
+
*
|
|
140
|
+
* Global helper — push a job onto the queue.
|
|
141
|
+
*
|
|
142
|
+
* const { dispatch } = require('millas/src');
|
|
143
|
+
* await dispatch(new SendEmailJob(user));
|
|
144
|
+
*/
|
|
145
|
+
module.exports.dispatch = async function dispatch(job) {
|
|
146
|
+
return queue.push(job);
|
|
147
|
+
};
|