@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.
Files changed (79) hide show
  1. package/package.json +6 -6
  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,287 @@
1
+ /**
2
+ * Cron Expression Parser
3
+ *
4
+ * Minimal cron parser without external dependencies.
5
+ *
6
+ * Supported syntax:
7
+ * - Specific values: 0, 15, 30
8
+ * - Wildcard: *
9
+ * - Step values: *\/5, *\/15
10
+ * - Ranges: 1-5
11
+ * - Lists: 1,5,10
12
+ *
13
+ * Format (6 fields with seconds, 5 fields without):
14
+ * ┌───────────── second (0-59)
15
+ * │ ┌───────────── minute (0-59)
16
+ * │ │ ┌───────────── hour (0-23)
17
+ * │ │ │ ┌───────────── day of month (1-31)
18
+ * │ │ │ │ ┌───────────── month (1-12)
19
+ * │ │ │ │ │ ┌───────────── day of week (0-6, 0=Sunday)
20
+ * │ │ │ │ │ │
21
+ * * * * * * *
22
+ */
23
+
24
+ /**
25
+ * Parsed cron schedule
26
+ */
27
+ export interface CronSchedule {
28
+ seconds: number[];
29
+ minutes: number[];
30
+ hours: number[];
31
+ daysOfMonth: number[];
32
+ months: number[];
33
+ daysOfWeek: number[];
34
+ }
35
+
36
+ /**
37
+ * Create an array of numbers from min to max (inclusive)
38
+ */
39
+ function range(min: number, max: number): number[] {
40
+ const result: number[] = [];
41
+ for (let i = min; i <= max; i++) {
42
+ result.push(i);
43
+ }
44
+
45
+ return result;
46
+ }
47
+
48
+ /**
49
+ * Parse a single cron field
50
+ *
51
+ * @param field - The field value (e.g., "*", "5", "*\/10", "1-5", "1,2,3")
52
+ * @param min - Minimum allowed value
53
+ * @param max - Maximum allowed value
54
+ * @returns Array of matching values
55
+ */
56
+ function parseField(field: string, min: number, max: number): number[] {
57
+ // Handle lists first (can contain other patterns)
58
+ if (field.includes(',')) {
59
+ const parts = field.split(',');
60
+ const result = new Set<number>();
61
+ for (const part of parts) {
62
+ for (const value of parseField(part.trim(), min, max)) {
63
+ result.add(value);
64
+ }
65
+ }
66
+
67
+ return Array.from(result).sort((a, b) => a - b);
68
+ }
69
+
70
+ // Wildcard - all values
71
+ if (field === '*') {
72
+ return range(min, max);
73
+ }
74
+
75
+ // Step values: */N or N-M/S
76
+ if (field.includes('/')) {
77
+ const [rangeStr, stepStr] = field.split('/');
78
+ const step = parseInt(stepStr, 10);
79
+
80
+ if (isNaN(step) || step <= 0) {
81
+ throw new Error(`Invalid step value: ${stepStr}`);
82
+ }
83
+
84
+ let rangeValues: number[];
85
+ if (rangeStr === '*') {
86
+ rangeValues = range(min, max);
87
+ } else if (rangeStr.includes('-')) {
88
+ const [start, end] = rangeStr.split('-').map((n) => parseInt(n, 10));
89
+ if (isNaN(start) || isNaN(end)) {
90
+ throw new Error(`Invalid range: ${rangeStr}`);
91
+ }
92
+ rangeValues = range(start, end);
93
+ } else {
94
+ const start = parseInt(rangeStr, 10);
95
+ if (isNaN(start)) {
96
+ throw new Error(`Invalid range start: ${rangeStr}`);
97
+ }
98
+ rangeValues = range(start, max);
99
+ }
100
+
101
+ return rangeValues.filter((_, index) => index % step === 0);
102
+ }
103
+
104
+ // Range: N-M
105
+ if (field.includes('-')) {
106
+ const [startStr, endStr] = field.split('-');
107
+ const start = parseInt(startStr, 10);
108
+ const end = parseInt(endStr, 10);
109
+
110
+ if (isNaN(start) || isNaN(end)) {
111
+ throw new Error(`Invalid range: ${field}`);
112
+ }
113
+
114
+ if (start < min || end > max || start > end) {
115
+ throw new Error(`Range out of bounds: ${field} (allowed: ${min}-${max})`);
116
+ }
117
+
118
+ return range(start, end);
119
+ }
120
+
121
+ // Single value
122
+ const value = parseInt(field, 10);
123
+ if (isNaN(value)) {
124
+ throw new Error(`Invalid value: ${field}`);
125
+ }
126
+
127
+ if (value < min || value > max) {
128
+ throw new Error(`Value out of bounds: ${value} (allowed: ${min}-${max})`);
129
+ }
130
+
131
+ return [value];
132
+ }
133
+
134
+ /**
135
+ * Parse a cron expression into a schedule
136
+ *
137
+ * @param expression - Cron expression (5 or 6 fields)
138
+ * @returns Parsed schedule
139
+ * @throws Error if the expression is invalid
140
+ *
141
+ * @example
142
+ * ```typescript
143
+ * // Every minute
144
+ * parseCronExpression('* * * * *');
145
+ *
146
+ * // Every 5 seconds
147
+ * parseCronExpression('*\/5 * * * * *');
148
+ *
149
+ * // At 9:00 AM every day
150
+ * parseCronExpression('0 9 * * *');
151
+ *
152
+ * // At 9:00 AM on weekdays
153
+ * parseCronExpression('0 9 * * 1-5');
154
+ * ```
155
+ */
156
+ export function parseCronExpression(expression: string): CronSchedule {
157
+ const parts = expression.trim().split(/\s+/);
158
+
159
+ // Support 5 fields (standard cron) and 6 fields (with seconds)
160
+ if (parts.length === 5) {
161
+ parts.unshift('0'); // Default seconds to 0
162
+ }
163
+
164
+ if (parts.length !== 6) {
165
+ throw new Error(
166
+ `Invalid cron expression: "${expression}". Expected 5 or 6 fields, got ${parts.length}`,
167
+ );
168
+ }
169
+
170
+ return {
171
+ seconds: parseField(parts[0], 0, 59),
172
+ minutes: parseField(parts[1], 0, 59),
173
+ hours: parseField(parts[2], 0, 23),
174
+ daysOfMonth: parseField(parts[3], 1, 31),
175
+ months: parseField(parts[4], 1, 12),
176
+ daysOfWeek: parseField(parts[5], 0, 6),
177
+ };
178
+ }
179
+
180
+ /**
181
+ * Check if a date matches a cron schedule
182
+ */
183
+ function matchesSchedule(date: Date, schedule: CronSchedule): boolean {
184
+ return (
185
+ schedule.seconds.includes(date.getSeconds()) &&
186
+ schedule.minutes.includes(date.getMinutes()) &&
187
+ schedule.hours.includes(date.getHours()) &&
188
+ schedule.daysOfMonth.includes(date.getDate()) &&
189
+ schedule.months.includes(date.getMonth() + 1) &&
190
+ schedule.daysOfWeek.includes(date.getDay())
191
+ );
192
+ }
193
+
194
+ /**
195
+ * Get the next run time for a cron schedule
196
+ *
197
+ * @param schedule - Parsed cron schedule
198
+ * @param from - Start time (defaults to now)
199
+ * @param maxIterations - Maximum iterations to prevent infinite loops
200
+ * @returns Next run time, or null if no match found within maxIterations
201
+ */
202
+ export function getNextRun(
203
+ schedule: CronSchedule,
204
+ from: Date = new Date(),
205
+ maxIterations: number = 366 * 24 * 60 * 60, // 1 year in seconds
206
+ ): Date | null {
207
+ // Start from the next second
208
+ const current = new Date(from);
209
+ current.setMilliseconds(0);
210
+ current.setSeconds(current.getSeconds() + 1);
211
+
212
+ for (let i = 0; i < maxIterations; i++) {
213
+ if (matchesSchedule(current, schedule)) {
214
+ return current;
215
+ }
216
+
217
+ // Increment by one second
218
+ current.setSeconds(current.getSeconds() + 1);
219
+ }
220
+
221
+ return null;
222
+ }
223
+
224
+ /**
225
+ * Get multiple future run times for a cron schedule
226
+ *
227
+ * @param schedule - Parsed cron schedule
228
+ * @param count - Number of run times to return
229
+ * @param from - Start time (defaults to now)
230
+ * @returns Array of future run times
231
+ */
232
+ export function getNextRuns(
233
+ schedule: CronSchedule,
234
+ count: number,
235
+ from: Date = new Date(),
236
+ ): Date[] {
237
+ const result: Date[] = [];
238
+ let current = from;
239
+
240
+ for (let i = 0; i < count; i++) {
241
+ const next = getNextRun(schedule, current);
242
+ if (!next) {
243
+ break;
244
+ }
245
+ result.push(next);
246
+ current = next;
247
+ }
248
+
249
+ return result;
250
+ }
251
+
252
+ /**
253
+ * Calculate milliseconds until the next cron run
254
+ *
255
+ * @param expression - Cron expression
256
+ * @param from - Start time (defaults to now)
257
+ * @returns Milliseconds until next run, or null if no match found
258
+ */
259
+ export function getMillisecondsUntilNextRun(
260
+ expression: string,
261
+ from: Date = new Date(),
262
+ ): number | null {
263
+ const schedule = parseCronExpression(expression);
264
+ const nextRun = getNextRun(schedule, from);
265
+
266
+ if (!nextRun) {
267
+ return null;
268
+ }
269
+
270
+ return nextRun.getTime() - from.getTime();
271
+ }
272
+
273
+ /**
274
+ * Validate a cron expression
275
+ *
276
+ * @param expression - Cron expression to validate
277
+ * @returns true if valid, false otherwise
278
+ */
279
+ export function isValidCronExpression(expression: string): boolean {
280
+ try {
281
+ parseCronExpression(expression);
282
+
283
+ return true;
284
+ } catch {
285
+ return false;
286
+ }
287
+ }
@@ -0,0 +1,292 @@
1
+ /**
2
+ * Queue Decorators Tests
3
+ */
4
+
5
+ import {
6
+ describe,
7
+ it,
8
+ expect,
9
+ } from 'bun:test';
10
+
11
+ import type {
12
+ Message,
13
+ MessageGuard,
14
+ MessageExecutionContext,
15
+ } from './types';
16
+
17
+ import {
18
+ Subscribe,
19
+ Cron,
20
+ Interval,
21
+ Timeout,
22
+ UseMessageGuards,
23
+ OnQueueReady,
24
+ OnQueueError,
25
+ OnMessageFailed,
26
+ OnMessageReceived,
27
+ OnMessageProcessed,
28
+ getSubscribeMetadata,
29
+ getCronMetadata,
30
+ getIntervalMetadata,
31
+ getTimeoutMetadata,
32
+ getMessageGuards,
33
+ getLifecycleHandlers,
34
+ hasQueueDecorators,
35
+ QUEUE_METADATA,
36
+ } from './decorators';
37
+
38
+ describe('queue-decorators', () => {
39
+ describe('@Subscribe', () => {
40
+ it('should register subscribe metadata', () => {
41
+ class TestService {
42
+ @Subscribe('orders.created')
43
+ handleOrderCreated(_message: Message) {}
44
+ }
45
+
46
+ const metadata = getSubscribeMetadata(TestService);
47
+ expect(metadata.length).toBe(1);
48
+ expect(metadata[0].pattern).toBe('orders.created');
49
+ expect(metadata[0].propertyKey).toBe('handleOrderCreated');
50
+ });
51
+
52
+ it('should support subscribe options', () => {
53
+ class TestService {
54
+ @Subscribe('orders.*', { ackMode: 'manual', group: 'order-processors' })
55
+ handleOrder(_message: Message) {}
56
+ }
57
+
58
+ const metadata = getSubscribeMetadata(TestService);
59
+ expect(metadata[0].options?.ackMode).toBe('manual');
60
+ expect(metadata[0].options?.group).toBe('order-processors');
61
+ });
62
+
63
+ it('should support multiple subscriptions', () => {
64
+ class TestService {
65
+ @Subscribe('orders.created')
66
+ handleOrderCreated(_message: Message) {}
67
+
68
+ @Subscribe('orders.updated')
69
+ handleOrderUpdated(_message: Message) {}
70
+ }
71
+
72
+ const metadata = getSubscribeMetadata(TestService);
73
+ expect(metadata.length).toBe(2);
74
+ });
75
+ });
76
+
77
+ describe('@Cron', () => {
78
+ it('should register cron metadata', () => {
79
+ class TestService {
80
+ @Cron('0 0 9 * * *', { pattern: 'reports.daily' })
81
+ getDailyReport() {
82
+ return { type: 'daily' };
83
+ }
84
+ }
85
+
86
+ const metadata = getCronMetadata(TestService);
87
+ expect(metadata.length).toBe(1);
88
+ expect(metadata[0].expression).toBe('0 0 9 * * *');
89
+ expect(metadata[0].options.pattern).toBe('reports.daily');
90
+ expect(metadata[0].options.name).toBe('getDailyReport');
91
+ });
92
+
93
+ it('should support custom job name', () => {
94
+ class TestService {
95
+ @Cron('0 0 * * * *', { pattern: 'health.check', name: 'hourly-health' })
96
+ checkHealth() {
97
+ return { status: 'ok' };
98
+ }
99
+ }
100
+
101
+ const metadata = getCronMetadata(TestService);
102
+ expect(metadata[0].options.name).toBe('hourly-health');
103
+ });
104
+ });
105
+
106
+ describe('@Interval', () => {
107
+ it('should register interval metadata', () => {
108
+ class TestService {
109
+ @Interval(60000, { pattern: 'health.check' })
110
+ getHealthData() {
111
+ return { timestamp: Date.now() };
112
+ }
113
+ }
114
+
115
+ const metadata = getIntervalMetadata(TestService);
116
+ expect(metadata.length).toBe(1);
117
+ expect(metadata[0].milliseconds).toBe(60000);
118
+ expect(metadata[0].options.pattern).toBe('health.check');
119
+ });
120
+ });
121
+
122
+ describe('@Timeout', () => {
123
+ it('should register timeout metadata', () => {
124
+ class TestService {
125
+ @Timeout(5000, { pattern: 'init.complete' })
126
+ getInitData() {
127
+ return { started: true };
128
+ }
129
+ }
130
+
131
+ const metadata = getTimeoutMetadata(TestService);
132
+ expect(metadata.length).toBe(1);
133
+ expect(metadata[0].milliseconds).toBe(5000);
134
+ expect(metadata[0].options.pattern).toBe('init.complete');
135
+ });
136
+ });
137
+
138
+ describe('@UseMessageGuards', () => {
139
+ it('should register guard metadata', () => {
140
+ class TestGuard implements MessageGuard {
141
+ canActivate(_context: MessageExecutionContext): boolean {
142
+ return true;
143
+ }
144
+ }
145
+
146
+ class TestService {
147
+ @UseMessageGuards(TestGuard)
148
+ @Subscribe('secure.events')
149
+ handleSecureEvent(_message: Message) {}
150
+ }
151
+
152
+ const guards = getMessageGuards(TestService, 'handleSecureEvent');
153
+ expect(guards.length).toBe(1);
154
+ expect(guards[0]).toBe(TestGuard);
155
+ });
156
+
157
+ it('should support multiple guards', () => {
158
+ class Guard1 implements MessageGuard {
159
+ canActivate(): boolean {
160
+ return true;
161
+ }
162
+ }
163
+ class Guard2 implements MessageGuard {
164
+ canActivate(): boolean {
165
+ return true;
166
+ }
167
+ }
168
+
169
+ class TestService {
170
+ @UseMessageGuards(Guard1, Guard2)
171
+ @Subscribe('secure.events')
172
+ handleSecureEvent(_message: Message) {}
173
+ }
174
+
175
+ const guards = getMessageGuards(TestService, 'handleSecureEvent');
176
+ expect(guards.length).toBe(2);
177
+ });
178
+ });
179
+
180
+ describe('Lifecycle decorators', () => {
181
+ it('should register @OnQueueReady handler', () => {
182
+ class TestService {
183
+ @OnQueueReady()
184
+ handleReady() {}
185
+ }
186
+
187
+ const handlers = getLifecycleHandlers(TestService, 'ON_READY');
188
+ expect(handlers.length).toBe(1);
189
+ expect(handlers[0].propertyKey).toBe('handleReady');
190
+ });
191
+
192
+ it('should register @OnQueueError handler', () => {
193
+ class TestService {
194
+ @OnQueueError()
195
+ handleError(_error: Error) {}
196
+ }
197
+
198
+ const handlers = getLifecycleHandlers(TestService, 'ON_ERROR');
199
+ expect(handlers.length).toBe(1);
200
+ });
201
+
202
+ it('should register @OnMessageFailed handler', () => {
203
+ class TestService {
204
+ @OnMessageFailed()
205
+ handleFailed(_message: Message, _error: Error) {}
206
+ }
207
+
208
+ const handlers = getLifecycleHandlers(TestService, 'ON_MESSAGE_FAILED');
209
+ expect(handlers.length).toBe(1);
210
+ });
211
+
212
+ it('should register @OnMessageReceived handler', () => {
213
+ class TestService {
214
+ @OnMessageReceived()
215
+ handleReceived(_message: Message) {}
216
+ }
217
+
218
+ const handlers = getLifecycleHandlers(TestService, 'ON_MESSAGE_RECEIVED');
219
+ expect(handlers.length).toBe(1);
220
+ });
221
+
222
+ it('should register @OnMessageProcessed handler', () => {
223
+ class TestService {
224
+ @OnMessageProcessed()
225
+ handleProcessed(_message: Message) {}
226
+ }
227
+
228
+ const handlers = getLifecycleHandlers(TestService, 'ON_MESSAGE_PROCESSED');
229
+ expect(handlers.length).toBe(1);
230
+ });
231
+ });
232
+
233
+ describe('hasQueueDecorators', () => {
234
+ it('should return true when class has subscribe decorators', () => {
235
+ class TestService {
236
+ @Subscribe('test')
237
+ handle(_message: Message) {}
238
+ }
239
+
240
+ expect(hasQueueDecorators(TestService)).toBe(true);
241
+ });
242
+
243
+ it('should return true when class has cron decorators', () => {
244
+ class TestService {
245
+ @Cron('* * * * *', { pattern: 'test' })
246
+ handle() {}
247
+ }
248
+
249
+ expect(hasQueueDecorators(TestService)).toBe(true);
250
+ });
251
+
252
+ it('should return true when class has interval decorators', () => {
253
+ class TestService {
254
+ @Interval(1000, { pattern: 'test' })
255
+ handle() {}
256
+ }
257
+
258
+ expect(hasQueueDecorators(TestService)).toBe(true);
259
+ });
260
+
261
+ it('should return true when class has timeout decorators', () => {
262
+ class TestService {
263
+ @Timeout(1000, { pattern: 'test' })
264
+ handle() {}
265
+ }
266
+
267
+ expect(hasQueueDecorators(TestService)).toBe(true);
268
+ });
269
+
270
+ it('should return false when class has no queue decorators', () => {
271
+ class TestService {
272
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
273
+ handle() {}
274
+ }
275
+
276
+ expect(hasQueueDecorators(TestService)).toBe(false);
277
+ });
278
+ });
279
+
280
+ describe('QUEUE_METADATA keys', () => {
281
+ it('should have all required metadata keys', () => {
282
+ expect(QUEUE_METADATA.SUBSCRIBE).toBeDefined();
283
+ expect(QUEUE_METADATA.CRON).toBeDefined();
284
+ expect(QUEUE_METADATA.INTERVAL).toBeDefined();
285
+ expect(QUEUE_METADATA.TIMEOUT).toBeDefined();
286
+ expect(QUEUE_METADATA.GUARDS).toBeDefined();
287
+ expect(QUEUE_METADATA.ON_READY).toBeDefined();
288
+ expect(QUEUE_METADATA.ON_ERROR).toBeDefined();
289
+ expect(QUEUE_METADATA.ON_MESSAGE_FAILED).toBeDefined();
290
+ });
291
+ });
292
+ });