@onebun/core 0.1.2 → 0.1.3

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.
Files changed (79) hide show
  1. package/package.json +1 -1
  2. package/src/{application.test.ts → application/application.test.ts} +6 -5
  3. package/src/{application.ts → application/application.ts} +131 -12
  4. package/src/application/index.ts +9 -0
  5. package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
  6. package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
  7. package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
  8. package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
  9. package/src/{decorators.ts → decorators/decorators.ts} +3 -2
  10. package/src/decorators/index.ts +15 -0
  11. package/src/index.ts +47 -134
  12. package/src/module/index.ts +12 -0
  13. package/src/{module.test.ts → module/module.test.ts} +3 -2
  14. package/src/{module.ts → module/module.ts} +6 -5
  15. package/src/queue/adapters/index.ts +8 -0
  16. package/src/queue/adapters/memory.adapter.test.ts +405 -0
  17. package/src/queue/adapters/memory.adapter.ts +509 -0
  18. package/src/queue/adapters/redis.adapter.ts +673 -0
  19. package/src/queue/cron-expression.test.ts +145 -0
  20. package/src/queue/cron-expression.ts +115 -0
  21. package/src/queue/cron-parser.test.ts +185 -0
  22. package/src/queue/cron-parser.ts +287 -0
  23. package/src/queue/decorators.test.ts +292 -0
  24. package/src/queue/decorators.ts +493 -0
  25. package/src/queue/docs-examples.test.ts +449 -0
  26. package/src/queue/guards.test.ts +309 -0
  27. package/src/queue/guards.ts +307 -0
  28. package/src/queue/index.ts +118 -0
  29. package/src/queue/pattern-matcher.test.ts +191 -0
  30. package/src/queue/pattern-matcher.ts +252 -0
  31. package/src/queue/queue.service.ts +421 -0
  32. package/src/queue/scheduler.test.ts +235 -0
  33. package/src/queue/scheduler.ts +379 -0
  34. package/src/queue/types.ts +502 -0
  35. package/src/redis/index.ts +8 -0
  36. package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
  37. package/src/service-client/index.ts +10 -0
  38. package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
  39. package/src/{service-client.ts → service-client/service-client.ts} +1 -1
  40. package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
  41. package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
  42. package/src/testing/index.ts +7 -0
  43. package/src/types.ts +34 -5
  44. package/src/websocket/index.ts +50 -0
  45. package/src/{ws-decorators.ts → websocket/ws-decorators.ts} +2 -1
  46. package/src/{ws-integration.test.ts → websocket/ws-integration.test.ts} +3 -2
  47. package/src/{ws-service-definition.ts → websocket/ws-service-definition.ts} +2 -1
  48. package/src/{ws-storage-redis.ts → websocket/ws-storage-redis.ts} +1 -1
  49. /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
  50. /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
  51. /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
  52. /package/src/{config.service.ts → module/config.service.ts} +0 -0
  53. /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
  54. /package/src/{controller.ts → module/controller.ts} +0 -0
  55. /package/src/{service.test.ts → module/service.test.ts} +0 -0
  56. /package/src/{service.ts → module/service.ts} +0 -0
  57. /package/src/{redis-client.ts → redis/redis-client.ts} +0 -0
  58. /package/src/{shared-redis.ts → redis/shared-redis.ts} +0 -0
  59. /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
  60. /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
  61. /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
  62. /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
  63. /package/src/{ws-base-gateway.test.ts → websocket/ws-base-gateway.test.ts} +0 -0
  64. /package/src/{ws-base-gateway.ts → websocket/ws-base-gateway.ts} +0 -0
  65. /package/src/{ws-client.test.ts → websocket/ws-client.test.ts} +0 -0
  66. /package/src/{ws-client.ts → websocket/ws-client.ts} +0 -0
  67. /package/src/{ws-client.types.ts → websocket/ws-client.types.ts} +0 -0
  68. /package/src/{ws-decorators.test.ts → websocket/ws-decorators.test.ts} +0 -0
  69. /package/src/{ws-guards.test.ts → websocket/ws-guards.test.ts} +0 -0
  70. /package/src/{ws-guards.ts → websocket/ws-guards.ts} +0 -0
  71. /package/src/{ws-handler.ts → websocket/ws-handler.ts} +0 -0
  72. /package/src/{ws-pattern-matcher.test.ts → websocket/ws-pattern-matcher.test.ts} +0 -0
  73. /package/src/{ws-pattern-matcher.ts → websocket/ws-pattern-matcher.ts} +0 -0
  74. /package/src/{ws-socketio-protocol.test.ts → websocket/ws-socketio-protocol.test.ts} +0 -0
  75. /package/src/{ws-socketio-protocol.ts → websocket/ws-socketio-protocol.ts} +0 -0
  76. /package/src/{ws-storage-memory.test.ts → websocket/ws-storage-memory.test.ts} +0 -0
  77. /package/src/{ws-storage-memory.ts → websocket/ws-storage-memory.ts} +0 -0
  78. /package/src/{ws-storage.ts → websocket/ws-storage.ts} +0 -0
  79. /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
+ });