@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.
- package/package.json +1 -1
- 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,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
|
+
});
|