@monque/tsed 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 +15 -0
- package/README.md +222 -0
- package/dist/CHANGELOG.md +19 -0
- package/dist/LICENSE +15 -0
- package/dist/README.md +222 -0
- package/dist/index.cjs +712 -0
- package/dist/index.cjs.map +1 -0
- package/dist/index.d.cts +582 -0
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.mts +582 -0
- package/dist/index.d.mts.map +1 -0
- package/dist/index.mjs +691 -0
- package/dist/index.mjs.map +1 -0
- package/package.json +98 -0
- package/src/config/config.ts +60 -0
- package/src/config/index.ts +2 -0
- package/src/config/types.ts +114 -0
- package/src/constants/constants.ts +12 -0
- package/src/constants/index.ts +2 -0
- package/src/constants/types.ts +21 -0
- package/src/decorators/cron.ts +58 -0
- package/src/decorators/index.ts +10 -0
- package/src/decorators/types.ts +131 -0
- package/src/decorators/worker-controller.ts +55 -0
- package/src/decorators/worker.ts +66 -0
- package/src/index.ts +20 -0
- package/src/monque-module.ts +199 -0
- package/src/services/index.ts +1 -0
- package/src/services/monque-service.ts +267 -0
- package/src/utils/build-job-name.ts +17 -0
- package/src/utils/collect-worker-metadata.ts +95 -0
- package/src/utils/get-worker-token.ts +27 -0
- package/src/utils/guards.ts +62 -0
- package/src/utils/index.ts +13 -0
- package/src/utils/resolve-database.ts +119 -0
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @Worker method decorator
|
|
3
|
+
*
|
|
4
|
+
* Registers a method as a job handler. The method will be called when a job
|
|
5
|
+
* with the matching name is picked up for processing.
|
|
6
|
+
*
|
|
7
|
+
* @param name - Job name (combined with controller namespace if present).
|
|
8
|
+
* @param options - Worker configuration options.
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @WorkerController("notifications")
|
|
13
|
+
* export class NotificationWorkers {
|
|
14
|
+
* @Worker("push", { concurrency: 10 })
|
|
15
|
+
* async sendPush(job: Job<PushPayload>) {
|
|
16
|
+
* await pushService.send(job.data);
|
|
17
|
+
* }
|
|
18
|
+
* }
|
|
19
|
+
* ```
|
|
20
|
+
*/
|
|
21
|
+
import { Store } from '@tsed/core';
|
|
22
|
+
|
|
23
|
+
import { MONQUE } from '@/constants';
|
|
24
|
+
|
|
25
|
+
import type { WorkerDecoratorOptions, WorkerMetadata, WorkerStore } from './types.js';
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Method decorator that registers a method as a job handler.
|
|
29
|
+
*
|
|
30
|
+
* @param name - The job name (will be prefixed with controller namespace if present)
|
|
31
|
+
* @param options - Optional worker configuration (concurrency, replace, etc.)
|
|
32
|
+
*/
|
|
33
|
+
export function Worker(name: string, options?: WorkerDecoratorOptions): MethodDecorator {
|
|
34
|
+
return <T>(
|
|
35
|
+
target: object,
|
|
36
|
+
propertyKey: string | symbol,
|
|
37
|
+
_descriptor: TypedPropertyDescriptor<T>,
|
|
38
|
+
): void => {
|
|
39
|
+
const methodName = String(propertyKey);
|
|
40
|
+
|
|
41
|
+
const workerMetadata: WorkerMetadata = {
|
|
42
|
+
name,
|
|
43
|
+
method: methodName,
|
|
44
|
+
opts: options || {},
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
// Get the class constructor (target is the prototype for instance methods)
|
|
48
|
+
const targetConstructor = target.constructor;
|
|
49
|
+
const store = Store.from(targetConstructor);
|
|
50
|
+
|
|
51
|
+
// Get or initialize the MONQUE store
|
|
52
|
+
const existing = store.get<Partial<WorkerStore>>(MONQUE) || {
|
|
53
|
+
type: 'controller',
|
|
54
|
+
workers: [],
|
|
55
|
+
cronJobs: [],
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
// Add this worker to the list
|
|
59
|
+
const workers = [...(existing.workers || []), workerMetadata];
|
|
60
|
+
|
|
61
|
+
store.set(MONQUE, {
|
|
62
|
+
...existing,
|
|
63
|
+
workers,
|
|
64
|
+
});
|
|
65
|
+
};
|
|
66
|
+
}
|
package/src/index.ts
ADDED
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
export { type MonqueTsedConfig, validateDatabaseConfig } from './config';
|
|
2
|
+
export { MONQUE, type ProviderType, ProviderTypes } from './constants';
|
|
3
|
+
export type {
|
|
4
|
+
CronDecoratorOptions,
|
|
5
|
+
CronMetadata,
|
|
6
|
+
WorkerDecoratorOptions,
|
|
7
|
+
WorkerMetadata,
|
|
8
|
+
WorkerStore,
|
|
9
|
+
} from './decorators';
|
|
10
|
+
export { Cron, Worker, WorkerController } from './decorators';
|
|
11
|
+
export { MonqueModule } from './monque-module.js';
|
|
12
|
+
export { MonqueService } from './services';
|
|
13
|
+
export {
|
|
14
|
+
buildJobName,
|
|
15
|
+
type CollectedWorkerMetadata,
|
|
16
|
+
collectWorkerMetadata,
|
|
17
|
+
getWorkerToken,
|
|
18
|
+
type InjectorFn,
|
|
19
|
+
resolveDatabase,
|
|
20
|
+
} from './utils';
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonqueModule - Main Integration Module
|
|
3
|
+
*
|
|
4
|
+
* Orchestrates the integration between Monque and Ts.ED.
|
|
5
|
+
* Handles lifecycle hooks, configuration resolution, and worker registration.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
type Job,
|
|
10
|
+
Monque,
|
|
11
|
+
type MonqueOptions,
|
|
12
|
+
type ScheduleOptions,
|
|
13
|
+
type WorkerOptions,
|
|
14
|
+
} from '@monque/core';
|
|
15
|
+
import {
|
|
16
|
+
Configuration,
|
|
17
|
+
DIContext,
|
|
18
|
+
Inject,
|
|
19
|
+
InjectorService,
|
|
20
|
+
LOGGER,
|
|
21
|
+
Module,
|
|
22
|
+
type OnDestroy,
|
|
23
|
+
type OnInit,
|
|
24
|
+
ProviderScope,
|
|
25
|
+
runInContext,
|
|
26
|
+
type TokenProvider,
|
|
27
|
+
} from '@tsed/di';
|
|
28
|
+
|
|
29
|
+
import { type MonqueTsedConfig, validateDatabaseConfig } from '@/config';
|
|
30
|
+
import { ProviderTypes } from '@/constants';
|
|
31
|
+
import { MonqueService } from '@/services';
|
|
32
|
+
import { collectWorkerMetadata, resolveDatabase } from '@/utils';
|
|
33
|
+
|
|
34
|
+
@Module({
|
|
35
|
+
imports: [MonqueService],
|
|
36
|
+
})
|
|
37
|
+
export class MonqueModule implements OnInit, OnDestroy {
|
|
38
|
+
protected injector: InjectorService;
|
|
39
|
+
protected monqueService: MonqueService;
|
|
40
|
+
protected logger: LOGGER;
|
|
41
|
+
protected monqueConfig: MonqueTsedConfig;
|
|
42
|
+
|
|
43
|
+
protected monque: Monque | null = null;
|
|
44
|
+
|
|
45
|
+
constructor(
|
|
46
|
+
@Inject(InjectorService) injector: InjectorService,
|
|
47
|
+
@Inject(MonqueService) monqueService: MonqueService,
|
|
48
|
+
@Inject(LOGGER) logger: LOGGER,
|
|
49
|
+
@Inject(Configuration) configuration: Configuration,
|
|
50
|
+
) {
|
|
51
|
+
this.injector = injector;
|
|
52
|
+
this.monqueService = monqueService;
|
|
53
|
+
this.logger = logger;
|
|
54
|
+
this.monqueConfig = configuration.get<MonqueTsedConfig>('monque') || {};
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async $onInit(): Promise<void> {
|
|
58
|
+
const config = this.monqueConfig;
|
|
59
|
+
|
|
60
|
+
if (config?.enabled === false) {
|
|
61
|
+
this.logger.info('Monque integration is disabled');
|
|
62
|
+
|
|
63
|
+
return;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
validateDatabaseConfig(config);
|
|
67
|
+
|
|
68
|
+
try {
|
|
69
|
+
const db = await resolveDatabase(config, (token) =>
|
|
70
|
+
this.injector.get(token as TokenProvider),
|
|
71
|
+
);
|
|
72
|
+
|
|
73
|
+
// We construct the options object carefully to match MonqueOptions
|
|
74
|
+
const { db: _db, ...restConfig } = config;
|
|
75
|
+
const options: MonqueOptions = restConfig;
|
|
76
|
+
|
|
77
|
+
this.monque = new Monque(db, options);
|
|
78
|
+
this.monqueService._setMonque(this.monque);
|
|
79
|
+
|
|
80
|
+
this.logger.info('Monque: Connecting to MongoDB...');
|
|
81
|
+
await this.monque.initialize();
|
|
82
|
+
|
|
83
|
+
await this.registerWorkers();
|
|
84
|
+
|
|
85
|
+
await this.monque.start();
|
|
86
|
+
|
|
87
|
+
this.logger.info('Monque: Started successfully');
|
|
88
|
+
} catch (error) {
|
|
89
|
+
this.logger.error({
|
|
90
|
+
event: 'MONQUE_INIT_ERROR',
|
|
91
|
+
message: 'Failed to initialize Monque',
|
|
92
|
+
error,
|
|
93
|
+
});
|
|
94
|
+
|
|
95
|
+
throw error;
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
async $onDestroy(): Promise<void> {
|
|
100
|
+
if (this.monque) {
|
|
101
|
+
this.logger.info('Monque: Stopping...');
|
|
102
|
+
|
|
103
|
+
await this.monque.stop();
|
|
104
|
+
|
|
105
|
+
this.logger.info('Monque: Stopped');
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
/**
|
|
110
|
+
* Discover and register all workers from @WorkerController providers
|
|
111
|
+
*/
|
|
112
|
+
protected async registerWorkers(): Promise<void> {
|
|
113
|
+
if (!this.monque) {
|
|
114
|
+
throw new Error('Monque instance not initialized');
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const monque = this.monque;
|
|
118
|
+
const workerControllers = this.injector.getProviders(ProviderTypes.WORKER_CONTROLLER);
|
|
119
|
+
const registeredJobs = new Set<string>();
|
|
120
|
+
|
|
121
|
+
this.logger.info(`Monque: Found ${workerControllers.length} worker controllers`);
|
|
122
|
+
|
|
123
|
+
for (const provider of workerControllers) {
|
|
124
|
+
const useClass = provider.useClass;
|
|
125
|
+
const workers = collectWorkerMetadata(useClass);
|
|
126
|
+
// Try to resolve singleton instance immediately
|
|
127
|
+
const instance = this.injector.get(provider.token);
|
|
128
|
+
|
|
129
|
+
if (!instance && provider.scope !== ProviderScope.REQUEST) {
|
|
130
|
+
this.logger.warn(
|
|
131
|
+
`Monque: Could not resolve instance for controller ${provider.name}. Skipping.`,
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
continue;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
for (const worker of workers) {
|
|
138
|
+
const { fullName, method, opts, isCron, cronPattern } = worker;
|
|
139
|
+
|
|
140
|
+
if (registeredJobs.has(fullName)) {
|
|
141
|
+
throw new Error(
|
|
142
|
+
`Monque: Duplicate job registration detected. Job "${fullName}" is already registered.`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
registeredJobs.add(fullName);
|
|
147
|
+
|
|
148
|
+
const handler = async (job: Job) => {
|
|
149
|
+
const $ctx = new DIContext({
|
|
150
|
+
injector: this.injector,
|
|
151
|
+
id: job._id?.toString() || 'unknown',
|
|
152
|
+
});
|
|
153
|
+
$ctx.set('MONQUE_JOB', job);
|
|
154
|
+
$ctx.container.set(DIContext, $ctx);
|
|
155
|
+
|
|
156
|
+
await runInContext($ctx, async () => {
|
|
157
|
+
try {
|
|
158
|
+
let targetInstance = instance;
|
|
159
|
+
if (provider.scope === ProviderScope.REQUEST || !targetInstance) {
|
|
160
|
+
targetInstance = await this.injector.invoke(provider.token, {
|
|
161
|
+
locals: $ctx.container,
|
|
162
|
+
});
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const typedInstance = targetInstance as Record<string, (job: Job) => unknown>;
|
|
166
|
+
|
|
167
|
+
if (typedInstance && typeof typedInstance[method] === 'function') {
|
|
168
|
+
await typedInstance[method](job);
|
|
169
|
+
}
|
|
170
|
+
} catch (error) {
|
|
171
|
+
this.logger.error({
|
|
172
|
+
event: 'MONQUE_JOB_ERROR',
|
|
173
|
+
jobName: fullName,
|
|
174
|
+
jobId: job._id,
|
|
175
|
+
message: `Error processing job ${fullName}`,
|
|
176
|
+
error,
|
|
177
|
+
});
|
|
178
|
+
throw error;
|
|
179
|
+
} finally {
|
|
180
|
+
await $ctx.destroy();
|
|
181
|
+
}
|
|
182
|
+
});
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
if (isCron && cronPattern) {
|
|
186
|
+
this.logger.debug(`Monque: Registering cron job "${fullName}" (${cronPattern})`);
|
|
187
|
+
|
|
188
|
+
monque.register(fullName, handler, opts as WorkerOptions);
|
|
189
|
+
await monque.schedule(cronPattern, fullName, {}, opts as ScheduleOptions);
|
|
190
|
+
} else {
|
|
191
|
+
this.logger.debug(`Monque: Registering worker "${fullName}"`);
|
|
192
|
+
monque.register(fullName, handler, opts as WorkerOptions);
|
|
193
|
+
}
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
this.logger.info(`Monque: Registered ${registeredJobs.size} jobs`);
|
|
198
|
+
}
|
|
199
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export { MonqueService } from './monque-service.js';
|
|
@@ -0,0 +1,267 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* MonqueService - Injectable wrapper for Monque
|
|
3
|
+
*
|
|
4
|
+
* Provides a DI-friendly interface to the Monque job queue.
|
|
5
|
+
* All methods delegate to the underlying Monque instance.
|
|
6
|
+
*
|
|
7
|
+
* @example
|
|
8
|
+
* ```typescript
|
|
9
|
+
* @Service()
|
|
10
|
+
* export class OrderService {
|
|
11
|
+
* @Inject()
|
|
12
|
+
* private monque: MonqueService;
|
|
13
|
+
*
|
|
14
|
+
* async createOrder(data: CreateOrderDto) {
|
|
15
|
+
* const order = await this.save(data);
|
|
16
|
+
* await this.monque.enqueue("order.process", { orderId: order.id });
|
|
17
|
+
* return order;
|
|
18
|
+
* }
|
|
19
|
+
* }
|
|
20
|
+
* ```
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
import type {
|
|
24
|
+
BulkOperationResult,
|
|
25
|
+
CursorOptions,
|
|
26
|
+
CursorPage,
|
|
27
|
+
EnqueueOptions,
|
|
28
|
+
GetJobsFilter,
|
|
29
|
+
JobSelector,
|
|
30
|
+
Monque,
|
|
31
|
+
PersistedJob,
|
|
32
|
+
QueueStats,
|
|
33
|
+
ScheduleOptions,
|
|
34
|
+
} from '@monque/core';
|
|
35
|
+
import { MonqueError } from '@monque/core';
|
|
36
|
+
import { Injectable } from '@tsed/di';
|
|
37
|
+
import { ObjectId } from 'mongodb';
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Injectable service that wraps the Monque instance.
|
|
41
|
+
*
|
|
42
|
+
* Exposes the full Monque public API through dependency injection.
|
|
43
|
+
*/
|
|
44
|
+
@Injectable()
|
|
45
|
+
export class MonqueService {
|
|
46
|
+
/**
|
|
47
|
+
* Internal Monque instance (set by MonqueModule)
|
|
48
|
+
* @internal
|
|
49
|
+
*/
|
|
50
|
+
private _monque: Monque | null = null;
|
|
51
|
+
|
|
52
|
+
/**
|
|
53
|
+
* Set the internal Monque instance.
|
|
54
|
+
* Called by MonqueModule during initialization.
|
|
55
|
+
* @internal
|
|
56
|
+
*/
|
|
57
|
+
_setMonque(monque: Monque): void {
|
|
58
|
+
this._monque = monque;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Access the underlying Monque instance.
|
|
63
|
+
* @throws Error if MonqueModule is not initialized
|
|
64
|
+
*/
|
|
65
|
+
get monque(): Monque {
|
|
66
|
+
if (!this._monque) {
|
|
67
|
+
throw new MonqueError(
|
|
68
|
+
'MonqueService is not initialized. Ensure MonqueModule is imported and enabled.',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return this._monque;
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
76
|
+
// Job Scheduling
|
|
77
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Enqueue a job for processing.
|
|
81
|
+
*
|
|
82
|
+
* @param name - Job type identifier (use full namespaced name, e.g., "email.send")
|
|
83
|
+
* @param data - Job payload
|
|
84
|
+
* @param options - Scheduling and deduplication options
|
|
85
|
+
* @returns The created or existing job document
|
|
86
|
+
*/
|
|
87
|
+
async enqueue<T>(name: string, data: T, options?: EnqueueOptions): Promise<PersistedJob<T>> {
|
|
88
|
+
return this.monque.enqueue(name, data, options);
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
/**
|
|
92
|
+
* Enqueue a job for immediate processing.
|
|
93
|
+
*
|
|
94
|
+
* @param name - Job type identifier
|
|
95
|
+
* @param data - Job payload
|
|
96
|
+
* @returns The created job document
|
|
97
|
+
*/
|
|
98
|
+
async now<T>(name: string, data: T): Promise<PersistedJob<T>> {
|
|
99
|
+
return this.monque.now(name, data);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Schedule a recurring job with a cron expression.
|
|
104
|
+
*
|
|
105
|
+
* @param cron - Cron expression (5-field standard or predefined like @daily)
|
|
106
|
+
* @param name - Job type identifier
|
|
107
|
+
* @param data - Job payload
|
|
108
|
+
* @param options - Scheduling options
|
|
109
|
+
* @returns The created job document
|
|
110
|
+
*/
|
|
111
|
+
async schedule<T>(
|
|
112
|
+
cron: string,
|
|
113
|
+
name: string,
|
|
114
|
+
data: T,
|
|
115
|
+
options?: ScheduleOptions,
|
|
116
|
+
): Promise<PersistedJob<T>> {
|
|
117
|
+
return this.monque.schedule(cron, name, data, options);
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
121
|
+
// Job Management (Single Job Operations)
|
|
122
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
123
|
+
|
|
124
|
+
/**
|
|
125
|
+
* Cancel a pending or scheduled job.
|
|
126
|
+
*
|
|
127
|
+
* @param jobId - The ID of the job to cancel
|
|
128
|
+
* @returns The cancelled job, or null if not found
|
|
129
|
+
*/
|
|
130
|
+
async cancelJob(jobId: string): Promise<PersistedJob<unknown> | null> {
|
|
131
|
+
return this.monque.cancelJob(jobId);
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
/**
|
|
135
|
+
* Retry a failed or cancelled job.
|
|
136
|
+
*
|
|
137
|
+
* @param jobId - The ID of the job to retry
|
|
138
|
+
* @returns The updated job, or null if not found
|
|
139
|
+
*/
|
|
140
|
+
async retryJob(jobId: string): Promise<PersistedJob<unknown> | null> {
|
|
141
|
+
return this.monque.retryJob(jobId);
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
/**
|
|
145
|
+
* Reschedule a pending job to run at a different time.
|
|
146
|
+
*
|
|
147
|
+
* @param jobId - The ID of the job to reschedule
|
|
148
|
+
* @param runAt - The new Date when the job should run
|
|
149
|
+
* @returns The updated job, or null if not found
|
|
150
|
+
*/
|
|
151
|
+
async rescheduleJob(jobId: string, runAt: Date): Promise<PersistedJob<unknown> | null> {
|
|
152
|
+
return this.monque.rescheduleJob(jobId, runAt);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Permanently delete a job.
|
|
157
|
+
*
|
|
158
|
+
* @param jobId - The ID of the job to delete
|
|
159
|
+
* @returns true if deleted, false if job not found
|
|
160
|
+
*/
|
|
161
|
+
async deleteJob(jobId: string): Promise<boolean> {
|
|
162
|
+
return this.monque.deleteJob(jobId);
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
166
|
+
// Job Management (Bulk Operations)
|
|
167
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Cancel multiple jobs matching the given filter.
|
|
171
|
+
*
|
|
172
|
+
* @param filter - Selector for which jobs to cancel
|
|
173
|
+
* @returns Result with count of cancelled jobs
|
|
174
|
+
*/
|
|
175
|
+
async cancelJobs(filter: JobSelector): Promise<BulkOperationResult> {
|
|
176
|
+
return this.monque.cancelJobs(filter);
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
/**
|
|
180
|
+
* Retry multiple jobs matching the given filter.
|
|
181
|
+
*
|
|
182
|
+
* @param filter - Selector for which jobs to retry
|
|
183
|
+
* @returns Result with count of retried jobs
|
|
184
|
+
*/
|
|
185
|
+
async retryJobs(filter: JobSelector): Promise<BulkOperationResult> {
|
|
186
|
+
return this.monque.retryJobs(filter);
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Delete multiple jobs matching the given filter.
|
|
191
|
+
*
|
|
192
|
+
* @param filter - Selector for which jobs to delete
|
|
193
|
+
* @returns Result with count of deleted jobs
|
|
194
|
+
*/
|
|
195
|
+
async deleteJobs(filter: JobSelector): Promise<BulkOperationResult> {
|
|
196
|
+
return this.monque.deleteJobs(filter);
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
200
|
+
// Job Queries
|
|
201
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
202
|
+
|
|
203
|
+
/**
|
|
204
|
+
* Get a job by its ID.
|
|
205
|
+
*
|
|
206
|
+
* @param jobId - The job's ObjectId (as string or ObjectId)
|
|
207
|
+
* @returns The job document, or null if not found
|
|
208
|
+
* @throws MonqueError if jobId is an invalid hex string
|
|
209
|
+
*/
|
|
210
|
+
async getJob<T>(jobId: string | ObjectId): Promise<PersistedJob<T> | null> {
|
|
211
|
+
let id: ObjectId;
|
|
212
|
+
|
|
213
|
+
if (typeof jobId === 'string') {
|
|
214
|
+
if (!ObjectId.isValid(jobId)) {
|
|
215
|
+
throw new MonqueError(`Invalid job ID format: ${jobId}`);
|
|
216
|
+
}
|
|
217
|
+
id = ObjectId.createFromHexString(jobId);
|
|
218
|
+
} else {
|
|
219
|
+
id = jobId;
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return this.monque.getJob(id);
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
/**
|
|
226
|
+
* Query jobs from the queue with optional filters.
|
|
227
|
+
*
|
|
228
|
+
* @param filter - Optional filter criteria (name, status, limit, skip)
|
|
229
|
+
* @returns Array of matching jobs
|
|
230
|
+
*/
|
|
231
|
+
async getJobs<T>(filter?: GetJobsFilter): Promise<PersistedJob<T>[]> {
|
|
232
|
+
return this.monque.getJobs(filter);
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
/**
|
|
236
|
+
* Get a paginated list of jobs using opaque cursors.
|
|
237
|
+
*
|
|
238
|
+
* @param options - Pagination options (cursor, limit, direction, filter)
|
|
239
|
+
* @returns Page of jobs with next/prev cursors
|
|
240
|
+
*/
|
|
241
|
+
async getJobsWithCursor<T>(options?: CursorOptions): Promise<CursorPage<T>> {
|
|
242
|
+
return this.monque.getJobsWithCursor(options);
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
/**
|
|
246
|
+
* Get aggregate statistics for the job queue.
|
|
247
|
+
*
|
|
248
|
+
* @param filter - Optional filter to scope statistics by job name
|
|
249
|
+
* @returns Queue statistics
|
|
250
|
+
*/
|
|
251
|
+
async getQueueStats(filter?: Pick<JobSelector, 'name'>): Promise<QueueStats> {
|
|
252
|
+
return this.monque.getQueueStats(filter);
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
256
|
+
// Health Check
|
|
257
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Check if the scheduler is healthy and running.
|
|
261
|
+
*
|
|
262
|
+
* @returns true if running and connected
|
|
263
|
+
*/
|
|
264
|
+
isHealthy(): boolean {
|
|
265
|
+
return this.monque.isHealthy();
|
|
266
|
+
}
|
|
267
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Build the full job name by combining namespace and name.
|
|
3
|
+
*
|
|
4
|
+
* @param namespace - Optional namespace from @WorkerController
|
|
5
|
+
* @param name - Job name from @Worker or @Cron
|
|
6
|
+
* @returns Full job name (e.g., "email.send" or just "send")
|
|
7
|
+
*
|
|
8
|
+
* @example
|
|
9
|
+
* ```typescript
|
|
10
|
+
* buildJobName("email", "send"); // "email.send"
|
|
11
|
+
* buildJobName(undefined, "send"); // "send"
|
|
12
|
+
* buildJobName("", "send"); // "send"
|
|
13
|
+
* ```
|
|
14
|
+
*/
|
|
15
|
+
export function buildJobName(namespace: string | undefined, name: string): string {
|
|
16
|
+
return namespace ? `${namespace}.${name}` : name;
|
|
17
|
+
}
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Collect worker metadata utility
|
|
3
|
+
*
|
|
4
|
+
* Collects all worker metadata from a class decorated with @WorkerController.
|
|
5
|
+
* Used by MonqueModule to discover and register all workers.
|
|
6
|
+
*/
|
|
7
|
+
import { Store } from '@tsed/core';
|
|
8
|
+
|
|
9
|
+
import { MONQUE } from '@/constants';
|
|
10
|
+
import type { CronDecoratorOptions, WorkerDecoratorOptions, WorkerStore } from '@/decorators';
|
|
11
|
+
|
|
12
|
+
import { buildJobName } from './build-job-name.js';
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Collected worker registration info ready for Monque.register()
|
|
16
|
+
*/
|
|
17
|
+
export interface CollectedWorkerMetadata {
|
|
18
|
+
/**
|
|
19
|
+
* Full job name (with namespace prefix if applicable)
|
|
20
|
+
*/
|
|
21
|
+
fullName: string;
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Method name on the controller class
|
|
25
|
+
*/
|
|
26
|
+
method: string;
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Worker options to pass to Monque.register()
|
|
30
|
+
*/
|
|
31
|
+
opts: WorkerDecoratorOptions | CronDecoratorOptions;
|
|
32
|
+
|
|
33
|
+
/**
|
|
34
|
+
* Whether this is a cron job
|
|
35
|
+
*/
|
|
36
|
+
isCron: boolean;
|
|
37
|
+
|
|
38
|
+
/**
|
|
39
|
+
* Cron pattern (only for cron jobs)
|
|
40
|
+
*/
|
|
41
|
+
cronPattern?: string;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Collect all worker metadata from a class.
|
|
46
|
+
*
|
|
47
|
+
* @param target - The class constructor (decorated with @WorkerController)
|
|
48
|
+
* @returns Array of collected worker metadata ready for registration
|
|
49
|
+
*
|
|
50
|
+
* @example
|
|
51
|
+
* ```typescript
|
|
52
|
+
* const metadata = collectWorkerMetadata(EmailWorkers);
|
|
53
|
+
* // Returns:
|
|
54
|
+
* // [
|
|
55
|
+
* // { fullName: "email.send", method: "sendEmail", opts: {}, isCron: false },
|
|
56
|
+
* // { fullName: "email.daily-digest", method: "sendDailyDigest", opts: {}, isCron: true, cronPattern: "0 9 * * *" }
|
|
57
|
+
* // ]
|
|
58
|
+
* ```
|
|
59
|
+
*/
|
|
60
|
+
export function collectWorkerMetadata(
|
|
61
|
+
target: new (...args: unknown[]) => unknown,
|
|
62
|
+
): CollectedWorkerMetadata[] {
|
|
63
|
+
const store = Store.from(target);
|
|
64
|
+
const workerStore = store.get<WorkerStore>(MONQUE);
|
|
65
|
+
|
|
66
|
+
if (!workerStore) {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const results: CollectedWorkerMetadata[] = [];
|
|
71
|
+
const namespace = workerStore.namespace;
|
|
72
|
+
|
|
73
|
+
// Collect regular workers
|
|
74
|
+
for (const worker of workerStore.workers) {
|
|
75
|
+
results.push({
|
|
76
|
+
fullName: buildJobName(namespace, worker.name),
|
|
77
|
+
method: worker.method,
|
|
78
|
+
opts: worker.opts,
|
|
79
|
+
isCron: false,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
// Collect cron jobs
|
|
84
|
+
for (const cron of workerStore.cronJobs) {
|
|
85
|
+
results.push({
|
|
86
|
+
fullName: buildJobName(namespace, cron.name),
|
|
87
|
+
method: cron.method,
|
|
88
|
+
opts: cron.opts,
|
|
89
|
+
isCron: true,
|
|
90
|
+
cronPattern: cron.pattern,
|
|
91
|
+
});
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
return results;
|
|
95
|
+
}
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Generate a unique token for a worker controller.
|
|
3
|
+
*
|
|
4
|
+
* Used internally by Ts.ED DI to identify worker controller providers.
|
|
5
|
+
* The token is based on the class name for debugging purposes.
|
|
6
|
+
*
|
|
7
|
+
* @param target - The class constructor
|
|
8
|
+
* @returns A Symbol token unique to this worker controller
|
|
9
|
+
*
|
|
10
|
+
* @example
|
|
11
|
+
* ```typescript
|
|
12
|
+
* @WorkerController("email")
|
|
13
|
+
* class EmailWorkers {}
|
|
14
|
+
*
|
|
15
|
+
* const token = getWorkerToken(EmailWorkers);
|
|
16
|
+
* // Symbol("monque:worker:EmailWorkers")
|
|
17
|
+
* ```
|
|
18
|
+
*/
|
|
19
|
+
export function getWorkerToken(target: new (...args: unknown[]) => unknown): symbol {
|
|
20
|
+
const name = target.name?.trim();
|
|
21
|
+
|
|
22
|
+
if (!name) {
|
|
23
|
+
throw new Error('Worker class must have a non-empty name');
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return Symbol.for(`monque:worker:${name}`);
|
|
27
|
+
}
|