@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,449 @@
1
+ /**
2
+ * Documentation Examples Tests for Queue Module
3
+ *
4
+ * This file tests code examples from:
5
+ * - docs/api/queue.md
6
+ *
7
+ * Each test case corresponds to a code block in the documentation.
8
+ * Keep these tests in sync with the documentation!
9
+ */
10
+
11
+ import {
12
+ describe,
13
+ it,
14
+ expect,
15
+ beforeEach,
16
+ afterEach,
17
+ } from 'bun:test';
18
+
19
+ import {
20
+ Subscribe,
21
+ Cron,
22
+ Interval,
23
+ Timeout,
24
+ UseMessageGuards,
25
+ OnQueueReady,
26
+ OnQueueError,
27
+ OnMessageReceived,
28
+ OnMessageProcessed,
29
+ OnMessageFailed,
30
+ MessageAuthGuard,
31
+ MessageServiceGuard,
32
+ MessageHeaderGuard,
33
+ MessageTraceGuard,
34
+ MessageAllGuards,
35
+ MessageAnyGuard,
36
+ createMessageGuard,
37
+ type Message,
38
+ CronExpression,
39
+ parseCronExpression,
40
+ getNextRun,
41
+ isValidCronExpression,
42
+ matchQueuePattern,
43
+ isQueuePatternMatch,
44
+ createQueuePatternMatcher,
45
+ InMemoryQueueAdapter,
46
+ getSubscribeMetadata,
47
+ getCronMetadata,
48
+ getIntervalMetadata,
49
+ getTimeoutMetadata,
50
+ hasQueueDecorators,
51
+ } from './index';
52
+
53
+ /**
54
+ * @source docs/api/queue.md#quick-start
55
+ */
56
+ describe('Quick Start Example (docs/api/queue.md)', () => {
57
+ it('should define service with queue decorators', () => {
58
+ // From docs/api/queue.md: Quick Start
59
+ class OrderService {
60
+ @OnQueueReady()
61
+ onReady() {
62
+ // console.log('Queue connected');
63
+ }
64
+
65
+ @Subscribe('orders.created')
66
+ async handleOrderCreated(message: Message<{ orderId: number }>) {
67
+ // console.log('New order:', message.data.orderId);
68
+ expect(message.data.orderId).toBeDefined();
69
+ }
70
+
71
+ @Cron(CronExpression.EVERY_HOUR, { pattern: 'cleanup.expired' })
72
+ getCleanupData() {
73
+ return { timestamp: Date.now() };
74
+ }
75
+ }
76
+
77
+ // Verify decorators are registered
78
+ expect(hasQueueDecorators(OrderService)).toBe(true);
79
+
80
+ const subscriptions = getSubscribeMetadata(OrderService);
81
+ expect(subscriptions.length).toBe(1);
82
+ expect(subscriptions[0].pattern).toBe('orders.created');
83
+
84
+ const cronJobs = getCronMetadata(OrderService);
85
+ expect(cronJobs.length).toBe(1);
86
+ expect(cronJobs[0].expression).toBe(CronExpression.EVERY_HOUR);
87
+ expect(cronJobs[0].options.pattern).toBe('cleanup.expired');
88
+ });
89
+ });
90
+
91
+ /**
92
+ * @source docs/api/queue.md#subscribe-decorator
93
+ */
94
+ describe('Subscribe Decorator Examples (docs/api/queue.md)', () => {
95
+ it('should match wildcard patterns', () => {
96
+ // From docs/api/queue.md: Pattern Syntax table
97
+ // orders.created -> exact match
98
+ expect(matchQueuePattern('orders.created', 'orders.created').matched).toBe(true);
99
+
100
+ // orders.* -> single-level wildcard
101
+ expect(matchQueuePattern('orders.*', 'orders.created').matched).toBe(true);
102
+ expect(matchQueuePattern('orders.*', 'orders.updated').matched).toBe(true);
103
+
104
+ // events.# -> multi-level wildcard
105
+ expect(matchQueuePattern('events.#', 'events.user.created').matched).toBe(true);
106
+ expect(matchQueuePattern('events.#', 'events.order.paid').matched).toBe(true);
107
+
108
+ // orders.{id} -> named parameter
109
+ const result = matchQueuePattern('orders.{id}', 'orders.123');
110
+ expect(result.matched).toBe(true);
111
+ expect(result.params).toEqual({ id: '123' });
112
+ });
113
+
114
+ it('should define subscribe with options', () => {
115
+ // From docs/api/queue.md: Subscribe Options
116
+ class OrderProcessor {
117
+ @Subscribe('orders.*', {
118
+ ackMode: 'manual',
119
+ group: 'order-processors',
120
+ prefetch: 10,
121
+ retry: {
122
+ attempts: 3,
123
+ backoff: 'exponential',
124
+ delay: 1000,
125
+ },
126
+ })
127
+ async handleOrder(message: Message<unknown>) {
128
+ try {
129
+ // await this.processOrder(message.data);
130
+ await message.ack();
131
+ } catch {
132
+ await message.nack(true); // requeue
133
+ }
134
+ }
135
+ }
136
+
137
+ const subscriptions = getSubscribeMetadata(OrderProcessor);
138
+ expect(subscriptions.length).toBe(1);
139
+ expect(subscriptions[0].options?.ackMode).toBe('manual');
140
+ expect(subscriptions[0].options?.group).toBe('order-processors');
141
+ expect(subscriptions[0].options?.prefetch).toBe(10);
142
+ expect(subscriptions[0].options?.retry?.attempts).toBe(3);
143
+ });
144
+ });
145
+
146
+ /**
147
+ * @source docs/api/queue.md#scheduling-decorators
148
+ */
149
+ describe('Scheduling Decorators Examples (docs/api/queue.md)', () => {
150
+ it('should define cron job with expression', () => {
151
+ // From docs/api/queue.md: @Cron section
152
+ class ReportService {
153
+ // Daily at 9 AM
154
+ @Cron('0 0 9 * * *', { pattern: 'reports.daily' })
155
+ getDailyReportData() {
156
+ return { type: 'daily', date: new Date() };
157
+ }
158
+
159
+ // Using CronExpression enum
160
+ @Cron(CronExpression.EVERY_HOUR, { pattern: 'health.check' })
161
+ getHealthData() {
162
+ return { status: 'ok' };
163
+ }
164
+ }
165
+
166
+ const cronJobs = getCronMetadata(ReportService);
167
+ expect(cronJobs.length).toBe(2);
168
+
169
+ expect(cronJobs[0].expression).toBe('0 0 9 * * *');
170
+ expect(cronJobs[0].options.pattern).toBe('reports.daily');
171
+
172
+ expect(cronJobs[1].expression).toBe(CronExpression.EVERY_HOUR);
173
+ expect(cronJobs[1].options.pattern).toBe('health.check');
174
+ });
175
+
176
+ it('should define interval job', () => {
177
+ // From docs/api/queue.md: @Interval section
178
+ class MetricsService {
179
+ // Every 60 seconds
180
+ @Interval(60000, { pattern: 'metrics.collect' })
181
+ getMetrics() {
182
+ return { cpu: process.cpuUsage() };
183
+ }
184
+ }
185
+
186
+ const intervals = getIntervalMetadata(MetricsService);
187
+ expect(intervals.length).toBe(1);
188
+ expect(intervals[0].milliseconds).toBe(60000);
189
+ expect(intervals[0].options.pattern).toBe('metrics.collect');
190
+ });
191
+
192
+ it('should define timeout job', () => {
193
+ // From docs/api/queue.md: @Timeout section
194
+ class InitService {
195
+ private startTime = Date.now();
196
+
197
+ // After 5 seconds
198
+ @Timeout(5000, { pattern: 'init.complete' })
199
+ getInitData() {
200
+ return { startedAt: this.startTime };
201
+ }
202
+ }
203
+
204
+ const timeouts = getTimeoutMetadata(InitService);
205
+ expect(timeouts.length).toBe(1);
206
+ expect(timeouts[0].milliseconds).toBe(5000);
207
+ expect(timeouts[0].options.pattern).toBe('init.complete');
208
+ });
209
+ });
210
+
211
+ /**
212
+ * @source docs/api/queue.md#cronexpression-constants
213
+ */
214
+ describe('CronExpression Constants (docs/api/queue.md)', () => {
215
+ it('should have valid cron expressions', () => {
216
+ // From docs/api/queue.md: CronExpression Constants table
217
+ expect(CronExpression.EVERY_SECOND).toBe('* * * * * *');
218
+ expect(CronExpression.EVERY_5_SECONDS).toBe('*/5 * * * * *');
219
+ expect(CronExpression.EVERY_MINUTE).toBe('0 * * * * *');
220
+ expect(CronExpression.EVERY_5_MINUTES).toBe('0 */5 * * * *');
221
+ expect(CronExpression.EVERY_HOUR).toBe('0 0 * * * *');
222
+ expect(CronExpression.EVERY_DAY_AT_MIDNIGHT).toBe('0 0 0 * * *');
223
+ expect(CronExpression.EVERY_DAY_AT_NOON).toBe('0 0 12 * * *');
224
+ expect(CronExpression.EVERY_WEEKDAY).toBe('0 0 0 * * 1-5');
225
+ expect(CronExpression.EVERY_WEEK).toBe('0 0 0 * * 0');
226
+ expect(CronExpression.EVERY_MONTH).toBe('0 0 0 1 * *');
227
+
228
+ // All should be valid
229
+ expect(isValidCronExpression(CronExpression.EVERY_SECOND)).toBe(true);
230
+ expect(isValidCronExpression(CronExpression.EVERY_HOUR)).toBe(true);
231
+ expect(isValidCronExpression(CronExpression.EVERY_MONTH)).toBe(true);
232
+ });
233
+ });
234
+
235
+ /**
236
+ * @source docs/api/queue.md#message-guards
237
+ */
238
+ describe('Message Guards Examples (docs/api/queue.md)', () => {
239
+ it('should use built-in guards', () => {
240
+ // From docs/api/queue.md: Built-in Guards section
241
+ class SecureService {
242
+ // Require authorization token
243
+ @UseMessageGuards(MessageAuthGuard)
244
+ @Subscribe('secure.events')
245
+ async handleSecure(_message: Message<unknown>) {}
246
+
247
+ // Require specific service
248
+ @UseMessageGuards(new MessageServiceGuard(['payment-service']))
249
+ @Subscribe('internal.events')
250
+ async handleInternal(_message: Message<unknown>) {}
251
+
252
+ // Require header
253
+ @UseMessageGuards(new MessageHeaderGuard('x-api-key'))
254
+ @Subscribe('api.events')
255
+ async handleApi(_message: Message<unknown>) {}
256
+
257
+ // Require trace context
258
+ @UseMessageGuards(MessageTraceGuard)
259
+ @Subscribe('traced.events')
260
+ async handleTraced(_message: Message<unknown>) {}
261
+ }
262
+
263
+ expect(hasQueueDecorators(SecureService)).toBe(true);
264
+ const subscriptions = getSubscribeMetadata(SecureService);
265
+ expect(subscriptions.length).toBe(4);
266
+ });
267
+
268
+ it('should use composite guards', () => {
269
+ // From docs/api/queue.md: Composite Guards section
270
+ class StrictService {
271
+ // All guards must pass
272
+ @UseMessageGuards(
273
+ new MessageAllGuards([MessageAuthGuard, new MessageServiceGuard(['allowed-service'])]),
274
+ )
275
+ @Subscribe('strict.events')
276
+ async handleStrict(_message: Message<unknown>) {}
277
+
278
+ // Any guard can pass
279
+ @UseMessageGuards(
280
+ new MessageAnyGuard([new MessageServiceGuard(['internal-service']), MessageAuthGuard]),
281
+ )
282
+ @Subscribe('flexible.events')
283
+ async handleFlexible(_message: Message<unknown>) {}
284
+ }
285
+
286
+ expect(hasQueueDecorators(StrictService)).toBe(true);
287
+ });
288
+
289
+ it('should create custom guard', () => {
290
+ // From docs/api/queue.md: Custom Guards section
291
+ const customGuard = createMessageGuard((context) => {
292
+ const metadata = context.getMetadata();
293
+
294
+ return metadata.headers?.['x-custom'] === 'expected';
295
+ });
296
+
297
+ // Verify guard was created
298
+ expect(typeof customGuard.canActivate).toBe('function');
299
+ });
300
+ });
301
+
302
+ /**
303
+ * @source docs/api/queue.md#lifecycle-decorators
304
+ */
305
+ describe('Lifecycle Decorators Examples (docs/api/queue.md)', () => {
306
+ it('should define lifecycle handlers', () => {
307
+ // From docs/api/queue.md: Lifecycle Decorators section
308
+ class EventService {
309
+ @OnQueueReady()
310
+ handleReady() {
311
+ // console.log('Queue connected');
312
+ }
313
+
314
+ @OnQueueError()
315
+ handleError(_error: Error) {
316
+ // console.error('Queue error:', error);
317
+ }
318
+
319
+ @OnMessageReceived()
320
+ handleReceived(_message: Message<unknown>) {
321
+ // console.log(`Received: ${message.id}`);
322
+ }
323
+
324
+ @OnMessageProcessed()
325
+ handleProcessed(_message: Message<unknown>) {
326
+ // console.log(`Processed: ${message.id}`);
327
+ }
328
+
329
+ @OnMessageFailed()
330
+ handleFailed(_message: Message<unknown>, _error: Error) {
331
+ // console.error(`Failed: ${message.id}`, error);
332
+ }
333
+ }
334
+
335
+ // Class should be defined without errors
336
+ expect(EventService).toBeDefined();
337
+ });
338
+ });
339
+
340
+ /**
341
+ * @source docs/api/queue.md#inmemoryqueueadapter
342
+ */
343
+ describe('InMemoryQueueAdapter Examples (docs/api/queue.md)', () => {
344
+ let adapter: InMemoryQueueAdapter;
345
+
346
+ beforeEach(async () => {
347
+ adapter = new InMemoryQueueAdapter();
348
+ await adapter.connect();
349
+ });
350
+
351
+ afterEach(async () => {
352
+ await adapter.disconnect();
353
+ });
354
+
355
+ it('should publish and subscribe', async () => {
356
+ // From docs/api/queue.md: InMemoryQueueAdapter section
357
+ const received: Message<unknown>[] = [];
358
+
359
+ await adapter.subscribe('events.*', async (message) => {
360
+ received.push(message);
361
+ });
362
+
363
+ await adapter.publish('events.created', { id: 1 });
364
+
365
+ expect(received.length).toBe(1);
366
+ expect((received[0].data as { id: number }).id).toBe(1);
367
+ });
368
+ });
369
+
370
+ /**
371
+ * @source docs/api/queue.md#cron-parser
372
+ */
373
+ describe('Cron Parser Examples (docs/api/queue.md)', () => {
374
+ it('should parse cron expression', () => {
375
+ // From docs/api/queue.md: Cron Parser section
376
+ const schedule = parseCronExpression('0 30 9 * * 1-5');
377
+
378
+ expect(schedule.seconds).toEqual([0]);
379
+ expect(schedule.minutes).toEqual([30]);
380
+ expect(schedule.hours).toEqual([9]);
381
+ expect(schedule.daysOfMonth.length).toBe(31);
382
+ expect(schedule.months.length).toBe(12);
383
+ expect(schedule.daysOfWeek).toEqual([1, 2, 3, 4, 5]);
384
+ });
385
+
386
+ it('should get next run time', () => {
387
+ // From docs/api/queue.md: Cron Parser section
388
+ const schedule = parseCronExpression('0 0 * * * *'); // Every hour
389
+ const nextRun = getNextRun(schedule);
390
+
391
+ expect(nextRun).not.toBeNull();
392
+ expect(nextRun!.getMinutes()).toBe(0);
393
+ });
394
+
395
+ it('should validate expressions', () => {
396
+ // From docs/api/queue.md: Cron Parser section
397
+ expect(isValidCronExpression('0 0 * * *')).toBe(true);
398
+ expect(isValidCronExpression('invalid')).toBe(false);
399
+ });
400
+ });
401
+
402
+ /**
403
+ * @source docs/api/queue.md#pattern-matcher
404
+ */
405
+ describe('Pattern Matcher Examples (docs/api/queue.md)', () => {
406
+ it('should match with parameter extraction', () => {
407
+ // From docs/api/queue.md: Pattern Matcher section
408
+ const result = matchQueuePattern('orders.{id}.status', 'orders.123.status');
409
+
410
+ expect(result.matched).toBe(true);
411
+ expect(result.params).toEqual({ id: '123' });
412
+ });
413
+
414
+ it('should perform simple match check', () => {
415
+ // From docs/api/queue.md: Pattern Matcher section
416
+ expect(isQueuePatternMatch('events.*', 'events.user')).toBe(true);
417
+ expect(isQueuePatternMatch('events.*', 'orders.user')).toBe(false);
418
+ });
419
+
420
+ it('should create reusable matcher', () => {
421
+ // From docs/api/queue.md: Pattern Matcher section
422
+ const matcher = createQueuePatternMatcher('orders.*.status');
423
+
424
+ expect(matcher('orders.123.status').matched).toBe(true);
425
+ expect(matcher('orders.456.status').matched).toBe(true);
426
+ expect(matcher('orders.123.other').matched).toBe(false);
427
+ });
428
+ });
429
+
430
+ /**
431
+ * @source docs/api/queue.md#feature-support-matrix
432
+ */
433
+ describe('Feature Support Matrix (docs/api/queue.md)', () => {
434
+ it('should report correct feature support for InMemoryQueueAdapter', () => {
435
+ // From docs/api/queue.md: Feature Support Matrix table
436
+ const adapter = new InMemoryQueueAdapter();
437
+
438
+ // Supported
439
+ expect(adapter.supports('pattern-subscriptions')).toBe(true);
440
+ expect(adapter.supports('delayed-messages')).toBe(true);
441
+ expect(adapter.supports('priority')).toBe(true);
442
+ expect(adapter.supports('scheduled-jobs')).toBe(true);
443
+
444
+ // Not supported
445
+ expect(adapter.supports('consumer-groups')).toBe(false);
446
+ expect(adapter.supports('dead-letter-queue')).toBe(false);
447
+ expect(adapter.supports('retry')).toBe(false);
448
+ });
449
+ });