@onebun/core 0.1.2 → 0.1.4
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/package.json +6 -6
- package/src/{application.test.ts → application/application.test.ts} +6 -5
- package/src/{application.ts → application/application.ts} +131 -12
- package/src/application/index.ts +9 -0
- package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
- package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
- package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
- package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
- package/src/{decorators.ts → decorators/decorators.ts} +3 -2
- package/src/decorators/index.ts +15 -0
- package/src/index.ts +47 -134
- package/src/module/index.ts +12 -0
- package/src/{module.test.ts → module/module.test.ts} +3 -2
- package/src/{module.ts → module/module.ts} +6 -5
- package/src/queue/adapters/index.ts +8 -0
- package/src/queue/adapters/memory.adapter.test.ts +405 -0
- package/src/queue/adapters/memory.adapter.ts +509 -0
- package/src/queue/adapters/redis.adapter.ts +673 -0
- package/src/queue/cron-expression.test.ts +145 -0
- package/src/queue/cron-expression.ts +115 -0
- package/src/queue/cron-parser.test.ts +185 -0
- package/src/queue/cron-parser.ts +287 -0
- package/src/queue/decorators.test.ts +292 -0
- package/src/queue/decorators.ts +493 -0
- package/src/queue/docs-examples.test.ts +449 -0
- package/src/queue/guards.test.ts +309 -0
- package/src/queue/guards.ts +307 -0
- package/src/queue/index.ts +118 -0
- package/src/queue/pattern-matcher.test.ts +191 -0
- package/src/queue/pattern-matcher.ts +252 -0
- package/src/queue/queue.service.ts +421 -0
- package/src/queue/scheduler.test.ts +235 -0
- package/src/queue/scheduler.ts +379 -0
- package/src/queue/types.ts +502 -0
- package/src/redis/index.ts +8 -0
- package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
- package/src/service-client/index.ts +10 -0
- package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
- package/src/{service-client.ts → service-client/service-client.ts} +1 -1
- package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
- package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
- package/src/testing/index.ts +7 -0
- package/src/types.ts +34 -5
- package/src/websocket/index.ts +50 -0
- package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
- package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
- package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
- package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
- /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
- /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
- /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
- /package/src/{config.service.ts → module/config.service.ts} +0 -0
- /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
- /package/src/{controller.ts → module/controller.ts} +0 -0
- /package/src/{service.test.ts → module/service.test.ts} +0 -0
- /package/src/{service.ts → module/service.ts} +0 -0
- /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
- /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
- /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
- /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
- /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
- /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
- /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
- /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
- /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
- /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
- /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
- /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
- /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
- /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
- /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
- /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
- /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
- /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
- /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
- /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
- /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
- /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
- /package/src/{ws.types.ts → websocket/ws.types.ts} +0 -0
|
@@ -0,0 +1,421 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Service
|
|
3
|
+
*
|
|
4
|
+
* Main service for queue operations. Provides DI integration and
|
|
5
|
+
* high-level API for queue operations.
|
|
6
|
+
*/
|
|
7
|
+
|
|
8
|
+
import {
|
|
9
|
+
Context,
|
|
10
|
+
Layer,
|
|
11
|
+
Effect,
|
|
12
|
+
} from 'effect';
|
|
13
|
+
|
|
14
|
+
import type {
|
|
15
|
+
QueueAdapter,
|
|
16
|
+
QueueConfig,
|
|
17
|
+
Message,
|
|
18
|
+
PublishOptions,
|
|
19
|
+
SubscribeOptions,
|
|
20
|
+
Subscription,
|
|
21
|
+
ScheduledJobOptions,
|
|
22
|
+
ScheduledJobInfo,
|
|
23
|
+
QueueFeature,
|
|
24
|
+
QueueEvents,
|
|
25
|
+
MessageHandler,
|
|
26
|
+
BuiltInAdapterType,
|
|
27
|
+
} from './types';
|
|
28
|
+
|
|
29
|
+
import { getMetadata } from '../decorators/metadata';
|
|
30
|
+
|
|
31
|
+
import {
|
|
32
|
+
getSubscribeMetadata,
|
|
33
|
+
getCronMetadata,
|
|
34
|
+
getIntervalMetadata,
|
|
35
|
+
getTimeoutMetadata,
|
|
36
|
+
getMessageGuards,
|
|
37
|
+
QUEUE_METADATA,
|
|
38
|
+
} from './decorators';
|
|
39
|
+
import { executeMessageGuards, MessageExecutionContextImpl } from './guards';
|
|
40
|
+
import { QueueScheduler } from './scheduler';
|
|
41
|
+
|
|
42
|
+
// ============================================================================
|
|
43
|
+
// Queue Service Class
|
|
44
|
+
// ============================================================================
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* Queue Service
|
|
48
|
+
*
|
|
49
|
+
* Provides a unified API for queue operations, handles adapter lifecycle,
|
|
50
|
+
* and integrates with the scheduler for cron/interval/timeout jobs.
|
|
51
|
+
*/
|
|
52
|
+
export class QueueService {
|
|
53
|
+
private adapter: QueueAdapter | null = null;
|
|
54
|
+
private scheduler: QueueScheduler | null = null;
|
|
55
|
+
private subscriptions: Subscription[] = [];
|
|
56
|
+
private started = false;
|
|
57
|
+
private config: QueueConfig;
|
|
58
|
+
|
|
59
|
+
constructor(config: QueueConfig) {
|
|
60
|
+
this.config = config;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* Initialize the queue service with an adapter
|
|
65
|
+
*/
|
|
66
|
+
async initialize(adapter: QueueAdapter): Promise<void> {
|
|
67
|
+
this.adapter = adapter;
|
|
68
|
+
this.scheduler = new QueueScheduler(adapter);
|
|
69
|
+
await adapter.connect();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Start the queue service (connect and start scheduler)
|
|
74
|
+
*/
|
|
75
|
+
async start(): Promise<void> {
|
|
76
|
+
if (this.started) {
|
|
77
|
+
return;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
if (!this.adapter) {
|
|
81
|
+
throw new Error('Queue adapter not initialized. Call initialize() first.');
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
if (!this.adapter.isConnected()) {
|
|
85
|
+
await this.adapter.connect();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (this.scheduler) {
|
|
89
|
+
this.scheduler.start();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
this.started = true;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
/**
|
|
96
|
+
* Stop the queue service (disconnect and stop scheduler)
|
|
97
|
+
*/
|
|
98
|
+
async stop(): Promise<void> {
|
|
99
|
+
if (!this.started) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
// Stop scheduler
|
|
104
|
+
if (this.scheduler) {
|
|
105
|
+
this.scheduler.stop();
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
// Unsubscribe all subscriptions
|
|
109
|
+
for (const subscription of this.subscriptions) {
|
|
110
|
+
await subscription.unsubscribe();
|
|
111
|
+
}
|
|
112
|
+
this.subscriptions = [];
|
|
113
|
+
|
|
114
|
+
// Disconnect adapter
|
|
115
|
+
if (this.adapter) {
|
|
116
|
+
await this.adapter.disconnect();
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
this.started = false;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
/**
|
|
123
|
+
* Get the underlying adapter
|
|
124
|
+
*/
|
|
125
|
+
getAdapter(): QueueAdapter {
|
|
126
|
+
if (!this.adapter) {
|
|
127
|
+
throw new Error('Queue adapter not initialized');
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
return this.adapter;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
/**
|
|
134
|
+
* Get the scheduler
|
|
135
|
+
*/
|
|
136
|
+
getScheduler(): QueueScheduler {
|
|
137
|
+
if (!this.scheduler) {
|
|
138
|
+
throw new Error('Queue scheduler not initialized');
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
return this.scheduler;
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
// ============================================================================
|
|
145
|
+
// Publishing
|
|
146
|
+
// ============================================================================
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Publish a message to a pattern
|
|
150
|
+
*/
|
|
151
|
+
async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
|
|
152
|
+
return await this.getAdapter().publish(pattern, data, options);
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* Publish multiple messages
|
|
157
|
+
*/
|
|
158
|
+
async publishBatch<T>(
|
|
159
|
+
messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
|
|
160
|
+
): Promise<string[]> {
|
|
161
|
+
return await this.getAdapter().publishBatch(messages);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// ============================================================================
|
|
165
|
+
// Subscribing
|
|
166
|
+
// ============================================================================
|
|
167
|
+
|
|
168
|
+
/**
|
|
169
|
+
* Subscribe to a pattern
|
|
170
|
+
*/
|
|
171
|
+
async subscribe<T>(
|
|
172
|
+
pattern: string,
|
|
173
|
+
handler: MessageHandler<T>,
|
|
174
|
+
options?: SubscribeOptions,
|
|
175
|
+
): Promise<Subscription> {
|
|
176
|
+
const subscription = await this.getAdapter().subscribe(pattern, handler, options);
|
|
177
|
+
this.subscriptions.push(subscription);
|
|
178
|
+
|
|
179
|
+
return subscription;
|
|
180
|
+
}
|
|
181
|
+
|
|
182
|
+
// ============================================================================
|
|
183
|
+
// Scheduled Jobs
|
|
184
|
+
// ============================================================================
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Add a scheduled job
|
|
188
|
+
*/
|
|
189
|
+
async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
|
|
190
|
+
return await this.getAdapter().addScheduledJob(name, options);
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/**
|
|
194
|
+
* Remove a scheduled job
|
|
195
|
+
*/
|
|
196
|
+
async removeScheduledJob(name: string): Promise<boolean> {
|
|
197
|
+
return await this.getAdapter().removeScheduledJob(name);
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
/**
|
|
201
|
+
* Get all scheduled jobs
|
|
202
|
+
*/
|
|
203
|
+
async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
|
|
204
|
+
return await this.getAdapter().getScheduledJobs();
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
// ============================================================================
|
|
208
|
+
// Features
|
|
209
|
+
// ============================================================================
|
|
210
|
+
|
|
211
|
+
/**
|
|
212
|
+
* Check if a feature is supported
|
|
213
|
+
*/
|
|
214
|
+
supports(feature: QueueFeature): boolean {
|
|
215
|
+
return this.getAdapter().supports(feature);
|
|
216
|
+
}
|
|
217
|
+
|
|
218
|
+
// ============================================================================
|
|
219
|
+
// Events
|
|
220
|
+
// ============================================================================
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Register an event handler
|
|
224
|
+
*/
|
|
225
|
+
on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
226
|
+
this.getAdapter().on(event, handler);
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
/**
|
|
230
|
+
* Unregister an event handler
|
|
231
|
+
*/
|
|
232
|
+
off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
|
|
233
|
+
this.getAdapter().off(event, handler);
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// ============================================================================
|
|
237
|
+
// Service Registration
|
|
238
|
+
// ============================================================================
|
|
239
|
+
|
|
240
|
+
/**
|
|
241
|
+
* Register a service class with queue decorators
|
|
242
|
+
*/
|
|
243
|
+
/* eslint-disable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */
|
|
244
|
+
async registerService(
|
|
245
|
+
serviceInstance: any,
|
|
246
|
+
serviceClass: new (...args: any[]) => any,
|
|
247
|
+
): Promise<void> {
|
|
248
|
+
/* eslint-enable @typescript-eslint/no-explicit-any, @typescript-eslint/explicit-module-boundary-types */
|
|
249
|
+
// Register subscribe handlers
|
|
250
|
+
const subscriptions = getSubscribeMetadata(serviceClass);
|
|
251
|
+
for (const sub of subscriptions) {
|
|
252
|
+
const method = serviceInstance[sub.propertyKey].bind(serviceInstance) as (
|
|
253
|
+
...args: unknown[]
|
|
254
|
+
) => unknown;
|
|
255
|
+
const guards = getMessageGuards(serviceClass, sub.propertyKey);
|
|
256
|
+
|
|
257
|
+
// Wrap handler with guards
|
|
258
|
+
const wrappedHandler = async (message: Message) => {
|
|
259
|
+
if (guards.length > 0) {
|
|
260
|
+
const context = new MessageExecutionContextImpl(
|
|
261
|
+
message,
|
|
262
|
+
sub.pattern,
|
|
263
|
+
method,
|
|
264
|
+
serviceClass,
|
|
265
|
+
);
|
|
266
|
+
|
|
267
|
+
const passed = await executeMessageGuards(guards, context);
|
|
268
|
+
if (!passed) {
|
|
269
|
+
return; // Guard rejected the message
|
|
270
|
+
}
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
await method(message);
|
|
274
|
+
};
|
|
275
|
+
|
|
276
|
+
await this.subscribe(sub.pattern, wrappedHandler, sub.options);
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
// Register cron jobs
|
|
280
|
+
const cronJobs = getCronMetadata(serviceClass);
|
|
281
|
+
for (const cron of cronJobs) {
|
|
282
|
+
const method = serviceInstance[cron.propertyKey].bind(serviceInstance);
|
|
283
|
+
this.getScheduler().addCronJob(
|
|
284
|
+
cron.options.name ?? String(cron.propertyKey),
|
|
285
|
+
cron.expression,
|
|
286
|
+
cron.options.pattern,
|
|
287
|
+
method,
|
|
288
|
+
{
|
|
289
|
+
metadata: cron.options.metadata,
|
|
290
|
+
overlapStrategy: cron.options.overlapStrategy,
|
|
291
|
+
},
|
|
292
|
+
);
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
// Register interval jobs
|
|
296
|
+
const intervalJobs = getIntervalMetadata(serviceClass);
|
|
297
|
+
for (const interval of intervalJobs) {
|
|
298
|
+
const method = serviceInstance[interval.propertyKey].bind(serviceInstance);
|
|
299
|
+
this.getScheduler().addIntervalJob(
|
|
300
|
+
interval.options.name ?? String(interval.propertyKey),
|
|
301
|
+
interval.milliseconds,
|
|
302
|
+
interval.options.pattern,
|
|
303
|
+
method,
|
|
304
|
+
{ metadata: interval.options.metadata },
|
|
305
|
+
);
|
|
306
|
+
}
|
|
307
|
+
|
|
308
|
+
// Register timeout jobs
|
|
309
|
+
const timeoutJobs = getTimeoutMetadata(serviceClass);
|
|
310
|
+
for (const timeout of timeoutJobs) {
|
|
311
|
+
const method = serviceInstance[timeout.propertyKey].bind(serviceInstance);
|
|
312
|
+
this.getScheduler().addTimeoutJob(
|
|
313
|
+
timeout.options.name ?? String(timeout.propertyKey),
|
|
314
|
+
timeout.milliseconds,
|
|
315
|
+
timeout.options.pattern,
|
|
316
|
+
method,
|
|
317
|
+
{ metadata: timeout.options.metadata },
|
|
318
|
+
);
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
// Register lifecycle handlers
|
|
322
|
+
const onReadyHandlers = getMetadata(QUEUE_METADATA.ON_READY, serviceClass) || [];
|
|
323
|
+
for (const handler of onReadyHandlers) {
|
|
324
|
+
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
325
|
+
this.on('onReady', method);
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
const onErrorHandlers = getMetadata(QUEUE_METADATA.ON_ERROR, serviceClass) || [];
|
|
329
|
+
for (const handler of onErrorHandlers) {
|
|
330
|
+
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
331
|
+
this.on('onError', method);
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const onMessageFailedHandlers = getMetadata(QUEUE_METADATA.ON_MESSAGE_FAILED, serviceClass) || [];
|
|
335
|
+
for (const handler of onMessageFailedHandlers) {
|
|
336
|
+
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
337
|
+
this.on('onMessageFailed', method);
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
const onMessageReceivedHandlers = getMetadata(QUEUE_METADATA.ON_MESSAGE_RECEIVED, serviceClass) || [];
|
|
341
|
+
for (const handler of onMessageReceivedHandlers) {
|
|
342
|
+
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
343
|
+
this.on('onMessageReceived', method);
|
|
344
|
+
}
|
|
345
|
+
|
|
346
|
+
const onMessageProcessedHandlers = getMetadata(QUEUE_METADATA.ON_MESSAGE_PROCESSED, serviceClass) || [];
|
|
347
|
+
for (const handler of onMessageProcessedHandlers) {
|
|
348
|
+
const method = serviceInstance[handler.propertyKey].bind(serviceInstance);
|
|
349
|
+
this.on('onMessageProcessed', method);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
354
|
+
// ============================================================================
|
|
355
|
+
// Effect.js Integration
|
|
356
|
+
// ============================================================================
|
|
357
|
+
|
|
358
|
+
/**
|
|
359
|
+
* Effect.js Tag for queue service
|
|
360
|
+
*/
|
|
361
|
+
export class QueueServiceTag extends Context.Tag('QueueService')<QueueServiceTag, QueueService>() {}
|
|
362
|
+
|
|
363
|
+
/**
|
|
364
|
+
* Create Effect.js Layer for queue service
|
|
365
|
+
*
|
|
366
|
+
* @example
|
|
367
|
+
* ```typescript
|
|
368
|
+
* const queueLayer = makeQueueLayer({
|
|
369
|
+
* adapter: 'memory',
|
|
370
|
+
* });
|
|
371
|
+
*
|
|
372
|
+
* const program = pipe(
|
|
373
|
+
* QueueServiceTag,
|
|
374
|
+
* Effect.flatMap(queue => queue.publish('test', { data: 'hello' })),
|
|
375
|
+
* Effect.provide(queueLayer),
|
|
376
|
+
* );
|
|
377
|
+
* ```
|
|
378
|
+
*/
|
|
379
|
+
export function makeQueueLayer(config: QueueConfig): Layer.Layer<QueueServiceTag> {
|
|
380
|
+
return Layer.scoped(
|
|
381
|
+
QueueServiceTag,
|
|
382
|
+
Effect.gen(function* () {
|
|
383
|
+
const service = new QueueService(config);
|
|
384
|
+
|
|
385
|
+
// Add finalizer for cleanup
|
|
386
|
+
yield* Effect.addFinalizer(() =>
|
|
387
|
+
Effect.promise(async () => {
|
|
388
|
+
await service.stop();
|
|
389
|
+
}),
|
|
390
|
+
);
|
|
391
|
+
|
|
392
|
+
return service;
|
|
393
|
+
}),
|
|
394
|
+
);
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
// ============================================================================
|
|
398
|
+
// Factory Functions
|
|
399
|
+
// ============================================================================
|
|
400
|
+
|
|
401
|
+
/**
|
|
402
|
+
* Create a queue service with the specified configuration
|
|
403
|
+
*/
|
|
404
|
+
export function createQueueService(config: QueueConfig): QueueService {
|
|
405
|
+
return new QueueService(config);
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
/**
|
|
409
|
+
* Resolve adapter type to adapter class
|
|
410
|
+
*/
|
|
411
|
+
export function resolveAdapterType(
|
|
412
|
+
adapterType: BuiltInAdapterType | (new (options?: unknown) => QueueAdapter),
|
|
413
|
+
): new (options?: unknown) => QueueAdapter {
|
|
414
|
+
if (typeof adapterType === 'function') {
|
|
415
|
+
return adapterType;
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
// Built-in adapters will be resolved by the application
|
|
419
|
+
// This is a placeholder that will be replaced with actual adapter classes
|
|
420
|
+
throw new Error(`Unknown adapter type: ${adapterType}. Use adapter class directly.`);
|
|
421
|
+
}
|
|
@@ -0,0 +1,235 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Queue Scheduler Tests
|
|
3
|
+
*/
|
|
4
|
+
|
|
5
|
+
import {
|
|
6
|
+
describe,
|
|
7
|
+
it,
|
|
8
|
+
expect,
|
|
9
|
+
beforeEach,
|
|
10
|
+
afterEach,
|
|
11
|
+
} from 'bun:test';
|
|
12
|
+
|
|
13
|
+
import type { Message } from './types';
|
|
14
|
+
|
|
15
|
+
import { InMemoryQueueAdapter } from './adapters/memory.adapter';
|
|
16
|
+
import { QueueScheduler, createQueueScheduler } from './scheduler';
|
|
17
|
+
|
|
18
|
+
describe('QueueScheduler', () => {
|
|
19
|
+
let adapter: InMemoryQueueAdapter;
|
|
20
|
+
let scheduler: QueueScheduler;
|
|
21
|
+
|
|
22
|
+
beforeEach(async () => {
|
|
23
|
+
adapter = new InMemoryQueueAdapter();
|
|
24
|
+
await adapter.connect();
|
|
25
|
+
scheduler = new QueueScheduler(adapter);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
afterEach(async () => {
|
|
29
|
+
scheduler.stop();
|
|
30
|
+
await adapter.disconnect();
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
describe('interval jobs', () => {
|
|
34
|
+
it('should execute interval job immediately on start', async () => {
|
|
35
|
+
const received: Message[] = [];
|
|
36
|
+
await adapter.subscribe('test.interval', async (message) => {
|
|
37
|
+
received.push(message);
|
|
38
|
+
});
|
|
39
|
+
|
|
40
|
+
scheduler.addIntervalJob('test-interval', 100000, 'test.interval', () => ({ count: 1 }));
|
|
41
|
+
scheduler.start();
|
|
42
|
+
|
|
43
|
+
// Wait a bit for async processing
|
|
44
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
45
|
+
|
|
46
|
+
// Should execute immediately
|
|
47
|
+
expect(received.length).toBe(1);
|
|
48
|
+
});
|
|
49
|
+
|
|
50
|
+
it('should use data from getDataFn', async () => {
|
|
51
|
+
const received: Message[] = [];
|
|
52
|
+
await adapter.subscribe('test.data', async (message) => {
|
|
53
|
+
received.push(message);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
let counter = 0;
|
|
57
|
+
scheduler.addIntervalJob('test-data', 100000, 'test.data', () => ({
|
|
58
|
+
counter: ++counter,
|
|
59
|
+
}));
|
|
60
|
+
scheduler.start();
|
|
61
|
+
|
|
62
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
63
|
+
|
|
64
|
+
expect(received.length).toBe(1);
|
|
65
|
+
expect((received[0].data as { counter: number }).counter).toBe(1);
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
it('should include metadata in messages', async () => {
|
|
69
|
+
const received: Message[] = [];
|
|
70
|
+
await adapter.subscribe('test.meta', async (message) => {
|
|
71
|
+
received.push(message);
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
scheduler.addIntervalJob('test-meta', 100000, 'test.meta', () => ({}), {
|
|
75
|
+
metadata: { serviceId: 'test-service' },
|
|
76
|
+
});
|
|
77
|
+
scheduler.start();
|
|
78
|
+
|
|
79
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
80
|
+
|
|
81
|
+
expect(received.length).toBe(1);
|
|
82
|
+
expect(received[0].metadata.serviceId).toBe('test-service');
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
describe('timeout jobs', () => {
|
|
87
|
+
it('should execute timeout job after delay', async () => {
|
|
88
|
+
const received: Message[] = [];
|
|
89
|
+
await adapter.subscribe('test.timeout', async (message) => {
|
|
90
|
+
received.push(message);
|
|
91
|
+
});
|
|
92
|
+
|
|
93
|
+
scheduler.addTimeoutJob('test-timeout', 50, 'test.timeout', () => ({ fired: true }));
|
|
94
|
+
scheduler.start();
|
|
95
|
+
|
|
96
|
+
// Should not have fired yet
|
|
97
|
+
expect(received.length).toBe(0);
|
|
98
|
+
|
|
99
|
+
// Wait for timeout
|
|
100
|
+
await new Promise((resolve) => setTimeout(resolve, 100));
|
|
101
|
+
|
|
102
|
+
expect(received.length).toBe(1);
|
|
103
|
+
expect((received[0].data as { fired: boolean }).fired).toBe(true);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('should remove job after execution', async () => {
|
|
107
|
+
scheduler.addTimeoutJob('one-time', 30, 'test.one-time');
|
|
108
|
+
scheduler.start();
|
|
109
|
+
|
|
110
|
+
expect(scheduler.hasJob('one-time')).toBe(true);
|
|
111
|
+
|
|
112
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
113
|
+
|
|
114
|
+
expect(scheduler.hasJob('one-time')).toBe(false);
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
|
|
118
|
+
describe('cron jobs', () => {
|
|
119
|
+
it('should add cron job with next run time', () => {
|
|
120
|
+
scheduler.addCronJob('test-cron', '* * * * * *', 'test.cron', () => ({ cron: true }));
|
|
121
|
+
|
|
122
|
+
const job = scheduler.getJob('test-cron');
|
|
123
|
+
expect(job).toBeDefined();
|
|
124
|
+
expect(job!.schedule.cron).toBe('* * * * * *');
|
|
125
|
+
expect(job!.nextRun).toBeDefined();
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
it('should track job running state', () => {
|
|
129
|
+
scheduler.addCronJob('cron-job', '0 0 * * * *', 'test.cron', () => ({}), {
|
|
130
|
+
overlapStrategy: 'skip',
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
const job = scheduler.getJob('cron-job');
|
|
134
|
+
expect(job).toBeDefined();
|
|
135
|
+
// Initially not running
|
|
136
|
+
expect(job!.isRunning).toBeFalsy();
|
|
137
|
+
});
|
|
138
|
+
});
|
|
139
|
+
|
|
140
|
+
describe('job management', () => {
|
|
141
|
+
it('should add and remove jobs', () => {
|
|
142
|
+
scheduler.addIntervalJob('job1', 1000, 'test');
|
|
143
|
+
scheduler.addIntervalJob('job2', 2000, 'test');
|
|
144
|
+
|
|
145
|
+
expect(scheduler.hasJob('job1')).toBe(true);
|
|
146
|
+
expect(scheduler.hasJob('job2')).toBe(true);
|
|
147
|
+
|
|
148
|
+
const removed = scheduler.removeJob('job1');
|
|
149
|
+
expect(removed).toBe(true);
|
|
150
|
+
expect(scheduler.hasJob('job1')).toBe(false);
|
|
151
|
+
expect(scheduler.hasJob('job2')).toBe(true);
|
|
152
|
+
});
|
|
153
|
+
|
|
154
|
+
it('should return false when removing non-existent job', () => {
|
|
155
|
+
const removed = scheduler.removeJob('non-existent');
|
|
156
|
+
expect(removed).toBe(false);
|
|
157
|
+
});
|
|
158
|
+
|
|
159
|
+
it('should get all jobs info', () => {
|
|
160
|
+
scheduler.addIntervalJob('interval-job', 1000, 'test.interval');
|
|
161
|
+
scheduler.addCronJob('cron-job', '0 0 * * * *', 'test.cron');
|
|
162
|
+
|
|
163
|
+
const jobs = scheduler.getJobs();
|
|
164
|
+
expect(jobs.length).toBe(2);
|
|
165
|
+
|
|
166
|
+
const intervalJob = jobs.find((j) => j.name === 'interval-job');
|
|
167
|
+
expect(intervalJob).toBeDefined();
|
|
168
|
+
expect(intervalJob!.pattern).toBe('test.interval');
|
|
169
|
+
expect(intervalJob!.schedule.every).toBe(1000);
|
|
170
|
+
|
|
171
|
+
const cronJob = jobs.find((j) => j.name === 'cron-job');
|
|
172
|
+
expect(cronJob).toBeDefined();
|
|
173
|
+
expect(cronJob!.pattern).toBe('test.cron');
|
|
174
|
+
expect(cronJob!.schedule.cron).toBe('0 0 * * * *');
|
|
175
|
+
});
|
|
176
|
+
|
|
177
|
+
it('should get specific job info', () => {
|
|
178
|
+
scheduler.addIntervalJob('my-job', 5000, 'test.pattern');
|
|
179
|
+
|
|
180
|
+
const job = scheduler.getJob('my-job');
|
|
181
|
+
expect(job).toBeDefined();
|
|
182
|
+
expect(job!.name).toBe('my-job');
|
|
183
|
+
expect(job!.pattern).toBe('test.pattern');
|
|
184
|
+
expect(job!.schedule.every).toBe(5000);
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
it('should return undefined for non-existent job', () => {
|
|
188
|
+
const job = scheduler.getJob('non-existent');
|
|
189
|
+
expect(job).toBeUndefined();
|
|
190
|
+
});
|
|
191
|
+
});
|
|
192
|
+
|
|
193
|
+
describe('start/stop', () => {
|
|
194
|
+
it('should not execute jobs before start', async () => {
|
|
195
|
+
const received: Message[] = [];
|
|
196
|
+
await adapter.subscribe('test', async (message) => {
|
|
197
|
+
received.push(message);
|
|
198
|
+
});
|
|
199
|
+
|
|
200
|
+
scheduler.addIntervalJob('test', 100000, 'test');
|
|
201
|
+
// Not calling start()
|
|
202
|
+
|
|
203
|
+
await new Promise((resolve) => setTimeout(resolve, 50));
|
|
204
|
+
expect(received.length).toBe(0);
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
it('should stop scheduler cleanly', async () => {
|
|
208
|
+
scheduler.addIntervalJob('test', 100000, 'test');
|
|
209
|
+
scheduler.start();
|
|
210
|
+
|
|
211
|
+
// Should not throw
|
|
212
|
+
scheduler.stop();
|
|
213
|
+
expect(true).toBe(true);
|
|
214
|
+
});
|
|
215
|
+
|
|
216
|
+
it('should handle multiple start calls', () => {
|
|
217
|
+
scheduler.start();
|
|
218
|
+
scheduler.start(); // Should not throw
|
|
219
|
+
expect(true).toBe(true);
|
|
220
|
+
});
|
|
221
|
+
|
|
222
|
+
it('should handle multiple stop calls', () => {
|
|
223
|
+
scheduler.stop();
|
|
224
|
+
scheduler.stop(); // Should not throw
|
|
225
|
+
expect(true).toBe(true);
|
|
226
|
+
});
|
|
227
|
+
});
|
|
228
|
+
|
|
229
|
+
describe('createQueueScheduler', () => {
|
|
230
|
+
it('should create scheduler instance', () => {
|
|
231
|
+
const created = createQueueScheduler(adapter);
|
|
232
|
+
expect(created).toBeInstanceOf(QueueScheduler);
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
});
|