@onebun/core 0.1.1 → 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 (81) hide show
  1. package/README.md +233 -0
  2. package/package.json +1 -1
  3. package/src/{application.test.ts → application/application.test.ts} +125 -5
  4. package/src/{application.ts → application/application.ts} +239 -13
  5. package/src/application/index.ts +9 -0
  6. package/src/{multi-service-application.test.ts → application/multi-service-application.test.ts} +2 -1
  7. package/src/{multi-service-application.ts → application/multi-service-application.ts} +2 -1
  8. package/src/{multi-service.types.ts → application/multi-service.types.ts} +1 -1
  9. package/src/{decorators.test.ts → decorators/decorators.test.ts} +2 -1
  10. package/src/{decorators.ts → decorators/decorators.ts} +3 -2
  11. package/src/decorators/index.ts +15 -0
  12. package/src/docs-examples.test.ts +753 -0
  13. package/src/index.ts +50 -41
  14. package/src/module/index.ts +12 -0
  15. package/src/{module.test.ts → module/module.test.ts} +3 -2
  16. package/src/{module.ts → module/module.ts} +15 -8
  17. package/src/queue/adapters/index.ts +8 -0
  18. package/src/queue/adapters/memory.adapter.test.ts +405 -0
  19. package/src/queue/adapters/memory.adapter.ts +509 -0
  20. package/src/queue/adapters/redis.adapter.ts +673 -0
  21. package/src/queue/cron-expression.test.ts +145 -0
  22. package/src/queue/cron-expression.ts +115 -0
  23. package/src/queue/cron-parser.test.ts +185 -0
  24. package/src/queue/cron-parser.ts +287 -0
  25. package/src/queue/decorators.test.ts +292 -0
  26. package/src/queue/decorators.ts +493 -0
  27. package/src/queue/docs-examples.test.ts +449 -0
  28. package/src/queue/guards.test.ts +309 -0
  29. package/src/queue/guards.ts +307 -0
  30. package/src/queue/index.ts +118 -0
  31. package/src/queue/pattern-matcher.test.ts +191 -0
  32. package/src/queue/pattern-matcher.ts +252 -0
  33. package/src/queue/queue.service.ts +421 -0
  34. package/src/queue/scheduler.test.ts +235 -0
  35. package/src/queue/scheduler.ts +379 -0
  36. package/src/queue/types.ts +502 -0
  37. package/src/redis/index.ts +8 -0
  38. package/src/redis/redis-client.ts +502 -0
  39. package/src/redis/shared-redis.ts +231 -0
  40. package/src/{env-resolver.ts → service-client/env-resolver.ts} +1 -1
  41. package/src/service-client/index.ts +10 -0
  42. package/src/{service-client.test.ts → service-client/service-client.test.ts} +3 -2
  43. package/src/{service-client.ts → service-client/service-client.ts} +1 -1
  44. package/src/{service-definition.test.ts → service-client/service-definition.test.ts} +3 -2
  45. package/src/{service-definition.ts → service-client/service-definition.ts} +2 -2
  46. package/src/testing/index.ts +7 -0
  47. package/src/types.ts +84 -5
  48. package/src/websocket/index.ts +50 -0
  49. package/src/websocket/ws-base-gateway.test.ts +479 -0
  50. package/src/websocket/ws-base-gateway.ts +514 -0
  51. package/src/websocket/ws-client.test.ts +511 -0
  52. package/src/websocket/ws-client.ts +628 -0
  53. package/src/websocket/ws-client.types.ts +129 -0
  54. package/src/websocket/ws-decorators.test.ts +331 -0
  55. package/src/websocket/ws-decorators.ts +418 -0
  56. package/src/websocket/ws-guards.test.ts +334 -0
  57. package/src/websocket/ws-guards.ts +298 -0
  58. package/src/websocket/ws-handler.ts +658 -0
  59. package/src/websocket/ws-integration.test.ts +518 -0
  60. package/src/websocket/ws-pattern-matcher.test.ts +152 -0
  61. package/src/websocket/ws-pattern-matcher.ts +240 -0
  62. package/src/websocket/ws-service-definition.ts +224 -0
  63. package/src/websocket/ws-socketio-protocol.test.ts +344 -0
  64. package/src/websocket/ws-socketio-protocol.ts +567 -0
  65. package/src/websocket/ws-storage-memory.test.ts +246 -0
  66. package/src/websocket/ws-storage-memory.ts +222 -0
  67. package/src/websocket/ws-storage-redis.ts +302 -0
  68. package/src/websocket/ws-storage.ts +210 -0
  69. package/src/websocket/ws.types.ts +342 -0
  70. /package/src/{metadata.test.ts → decorators/metadata.test.ts} +0 -0
  71. /package/src/{metadata.ts → decorators/metadata.ts} +0 -0
  72. /package/src/{config.service.test.ts → module/config.service.test.ts} +0 -0
  73. /package/src/{config.service.ts → module/config.service.ts} +0 -0
  74. /package/src/{controller.test.ts → module/controller.test.ts} +0 -0
  75. /package/src/{controller.ts → module/controller.ts} +0 -0
  76. /package/src/{service.test.ts → module/service.test.ts} +0 -0
  77. /package/src/{service.ts → module/service.ts} +0 -0
  78. /package/src/{env-resolver.test.ts → service-client/env-resolver.test.ts} +0 -0
  79. /package/src/{service-client.types.ts → service-client/service-client.types.ts} +0 -0
  80. /package/src/{test-utils.test.ts → testing/test-utils.test.ts} +0 -0
  81. /package/src/{test-utils.ts → testing/test-utils.ts} +0 -0
@@ -0,0 +1,509 @@
1
+ /**
2
+ * In-Memory Queue Adapter
3
+ *
4
+ * A simple in-process message bus. Useful for development, testing,
5
+ * and single-instance deployments where external dependencies are not needed.
6
+ */
7
+
8
+ import type {
9
+ QueueAdapter,
10
+ QueueAdapterType,
11
+ QueueFeature,
12
+ QueueEvents,
13
+ Message,
14
+ MessageMetadata,
15
+ PublishOptions,
16
+ SubscribeOptions,
17
+ Subscription,
18
+ ScheduledJobOptions,
19
+ ScheduledJobInfo,
20
+ MessageHandler,
21
+ } from '../types';
22
+
23
+ import { createQueuePatternMatcher, type QueuePatternMatch } from '../pattern-matcher';
24
+ import { QueueScheduler } from '../scheduler';
25
+
26
+ // ============================================================================
27
+ // Types
28
+ // ============================================================================
29
+
30
+ interface SubscriptionEntry {
31
+ pattern: string;
32
+ handler: MessageHandler;
33
+ options?: SubscribeOptions;
34
+ matcher: (topic: string) => QueuePatternMatch;
35
+ paused: boolean;
36
+ }
37
+
38
+ interface DelayedMessage {
39
+ pattern: string;
40
+ data: unknown;
41
+ options?: PublishOptions;
42
+ executeAt: number;
43
+ }
44
+
45
+ // ============================================================================
46
+ // In-Memory Message Implementation
47
+ // ============================================================================
48
+
49
+ /**
50
+ * In-memory message implementation
51
+ */
52
+ class InMemoryMessage<T> implements Message<T> {
53
+ id: string;
54
+ pattern: string;
55
+ data: T;
56
+ timestamp: number;
57
+ redelivered: boolean;
58
+ metadata: MessageMetadata;
59
+ attempt?: number;
60
+ maxAttempts?: number;
61
+
62
+ private acked = false;
63
+ private nacked = false;
64
+ private onAck?: () => void;
65
+ private onNack?: (requeue: boolean) => void;
66
+
67
+ constructor(
68
+ id: string,
69
+ pattern: string,
70
+ data: T,
71
+ metadata: MessageMetadata,
72
+ options?: {
73
+ redelivered?: boolean;
74
+ attempt?: number;
75
+ maxAttempts?: number;
76
+ onAck?: () => void;
77
+ onNack?: (requeue: boolean) => void;
78
+ },
79
+ ) {
80
+ this.id = id;
81
+ this.pattern = pattern;
82
+ this.data = data;
83
+ this.timestamp = Date.now();
84
+ this.metadata = metadata;
85
+ this.redelivered = options?.redelivered ?? false;
86
+ this.attempt = options?.attempt;
87
+ this.maxAttempts = options?.maxAttempts;
88
+ this.onAck = options?.onAck;
89
+ this.onNack = options?.onNack;
90
+ }
91
+
92
+ async ack(): Promise<void> {
93
+ if (this.acked || this.nacked) {
94
+ return;
95
+ }
96
+ this.acked = true;
97
+ this.onAck?.();
98
+ }
99
+
100
+ async nack(requeue = false): Promise<void> {
101
+ if (this.acked || this.nacked) {
102
+ return;
103
+ }
104
+ this.nacked = true;
105
+ this.onNack?.(requeue);
106
+ }
107
+ }
108
+
109
+ // ============================================================================
110
+ // In-Memory Subscription Implementation
111
+ // ============================================================================
112
+
113
+ class InMemorySubscription implements Subscription {
114
+ private active = true;
115
+
116
+ constructor(
117
+ private readonly entry: SubscriptionEntry,
118
+ private readonly onUnsubscribe: () => void,
119
+ ) {}
120
+
121
+ async unsubscribe(): Promise<void> {
122
+ this.active = false;
123
+ this.onUnsubscribe();
124
+ }
125
+
126
+ pause(): void {
127
+ this.entry.paused = true;
128
+ }
129
+
130
+ resume(): void {
131
+ this.entry.paused = false;
132
+ }
133
+
134
+ get pattern(): string {
135
+ return this.entry.pattern;
136
+ }
137
+
138
+ get isActive(): boolean {
139
+ return this.active && !this.entry.paused;
140
+ }
141
+ }
142
+
143
+ // ============================================================================
144
+ // In-Memory Queue Adapter
145
+ // ============================================================================
146
+
147
+ /**
148
+ * In-Memory Queue Adapter
149
+ *
150
+ * Implements a simple in-process message bus.
151
+ *
152
+ * Supported features:
153
+ * - Pattern subscriptions with wildcards
154
+ * - Delayed messages
155
+ * - Priority (via sorting)
156
+ * - Scheduled jobs
157
+ *
158
+ * Not supported (requires external storage):
159
+ * - Consumer groups (all handlers receive all messages)
160
+ * - Dead letter queues
161
+ * - Retry with persistence
162
+ *
163
+ * @example
164
+ * ```typescript
165
+ * const adapter = new InMemoryQueueAdapter();
166
+ * await adapter.connect();
167
+ *
168
+ * await adapter.subscribe('orders.*', async (message) => {
169
+ * console.log('Received:', message.data);
170
+ * });
171
+ *
172
+ * await adapter.publish('orders.created', { orderId: 123 });
173
+ * ```
174
+ */
175
+ export class InMemoryQueueAdapter implements QueueAdapter {
176
+ readonly name = 'memory';
177
+ readonly type: QueueAdapterType = 'memory';
178
+
179
+ private subscriptions: SubscriptionEntry[] = [];
180
+ private delayedMessages: DelayedMessage[] = [];
181
+ private messageIdCounter = 0;
182
+ private connected = false;
183
+ private scheduler: QueueScheduler | null = null;
184
+ private delayedInterval?: ReturnType<typeof setInterval>;
185
+
186
+ // Event handlers
187
+ private eventHandlers: Map<keyof QueueEvents, Set<(...args: unknown[]) => void>> = new Map();
188
+
189
+ // ============================================================================
190
+ // Lifecycle
191
+ // ============================================================================
192
+
193
+ async connect(): Promise<void> {
194
+ if (this.connected) {
195
+ return;
196
+ }
197
+
198
+ this.connected = true;
199
+ this.scheduler = new QueueScheduler(this);
200
+
201
+ // Start delayed message processor
202
+ this.delayedInterval = setInterval(() => this.processDelayedMessages(), 100);
203
+
204
+ // Emit ready event
205
+ this.emit('onReady');
206
+ }
207
+
208
+ async disconnect(): Promise<void> {
209
+ if (!this.connected) {
210
+ return;
211
+ }
212
+
213
+ // Stop scheduler
214
+ if (this.scheduler) {
215
+ this.scheduler.stop();
216
+ this.scheduler = null;
217
+ }
218
+
219
+ // Clear delayed interval
220
+ if (this.delayedInterval) {
221
+ clearInterval(this.delayedInterval);
222
+ this.delayedInterval = undefined;
223
+ }
224
+
225
+ // Clear all subscriptions
226
+ this.subscriptions = [];
227
+ this.delayedMessages = [];
228
+
229
+ this.connected = false;
230
+ }
231
+
232
+ isConnected(): boolean {
233
+ return this.connected;
234
+ }
235
+
236
+ // ============================================================================
237
+ // Publishing
238
+ // ============================================================================
239
+
240
+ async publish<T>(pattern: string, data: T, options?: PublishOptions): Promise<string> {
241
+ this.ensureConnected();
242
+
243
+ const messageId = options?.messageId ?? this.generateMessageId();
244
+
245
+ // Handle delayed messages
246
+ if (options?.delay && options.delay > 0) {
247
+ this.delayedMessages.push({
248
+ pattern,
249
+ data,
250
+ options: { ...options, messageId },
251
+ executeAt: Date.now() + options.delay,
252
+ });
253
+ // Sort by priority (higher first) and then by time
254
+ this.delayedMessages.sort((a, b) => {
255
+ const priorityA = a.options?.priority ?? 0;
256
+ const priorityB = b.options?.priority ?? 0;
257
+ if (priorityA !== priorityB) {
258
+ return priorityB - priorityA; // Higher priority first
259
+ }
260
+
261
+ return a.executeAt - b.executeAt;
262
+ });
263
+
264
+ return messageId;
265
+ }
266
+
267
+ // Dispatch immediately
268
+ await this.dispatch(pattern, data, messageId, options?.metadata);
269
+
270
+ return messageId;
271
+ }
272
+
273
+ async publishBatch<T>(
274
+ messages: Array<{ pattern: string; data: T; options?: PublishOptions }>,
275
+ ): Promise<string[]> {
276
+ const ids: string[] = [];
277
+
278
+ // Sort by priority if any
279
+ const sorted = [...messages].sort((a, b) => {
280
+ const priorityA = a.options?.priority ?? 0;
281
+ const priorityB = b.options?.priority ?? 0;
282
+
283
+ return priorityB - priorityA;
284
+ });
285
+
286
+ for (const msg of sorted) {
287
+ const id = await this.publish(msg.pattern, msg.data, msg.options);
288
+ ids.push(id);
289
+ }
290
+
291
+ return ids;
292
+ }
293
+
294
+ // ============================================================================
295
+ // Subscribing
296
+ // ============================================================================
297
+
298
+ async subscribe<T>(
299
+ pattern: string,
300
+ handler: MessageHandler<T>,
301
+ options?: SubscribeOptions,
302
+ ): Promise<Subscription> {
303
+ this.ensureConnected();
304
+
305
+ const entry: SubscriptionEntry = {
306
+ pattern,
307
+ handler: handler as MessageHandler,
308
+ options,
309
+ matcher: createQueuePatternMatcher(pattern),
310
+ paused: false,
311
+ };
312
+
313
+ this.subscriptions.push(entry);
314
+
315
+ const subscription = new InMemorySubscription(entry, () => {
316
+ const index = this.subscriptions.indexOf(entry);
317
+ if (index !== -1) {
318
+ this.subscriptions.splice(index, 1);
319
+ }
320
+ });
321
+
322
+ return subscription;
323
+ }
324
+
325
+ // ============================================================================
326
+ // Scheduled Jobs
327
+ // ============================================================================
328
+
329
+ async addScheduledJob(name: string, options: ScheduledJobOptions): Promise<void> {
330
+ this.ensureConnected();
331
+
332
+ if (!this.scheduler) {
333
+ throw new Error('Scheduler not initialized');
334
+ }
335
+
336
+ if (options.schedule.cron) {
337
+ this.scheduler.addCronJob(name, options.schedule.cron, options.pattern, () => options.data, {
338
+ metadata: options.metadata,
339
+ overlapStrategy: options.overlapStrategy,
340
+ });
341
+ } else if (options.schedule.every) {
342
+ this.scheduler.addIntervalJob(name, options.schedule.every, options.pattern, () => options.data, {
343
+ metadata: options.metadata,
344
+ });
345
+ }
346
+ }
347
+
348
+ async removeScheduledJob(name: string): Promise<boolean> {
349
+ this.ensureConnected();
350
+
351
+ if (!this.scheduler) {
352
+ return false;
353
+ }
354
+
355
+ return this.scheduler.removeJob(name);
356
+ }
357
+
358
+ async getScheduledJobs(): Promise<ScheduledJobInfo[]> {
359
+ this.ensureConnected();
360
+
361
+ if (!this.scheduler) {
362
+ return [];
363
+ }
364
+
365
+ return this.scheduler.getJobs();
366
+ }
367
+
368
+ // ============================================================================
369
+ // Features
370
+ // ============================================================================
371
+
372
+ supports(feature: QueueFeature): boolean {
373
+ switch (feature) {
374
+ case 'delayed-messages':
375
+ case 'priority':
376
+ case 'scheduled-jobs':
377
+ case 'pattern-subscriptions':
378
+ return true;
379
+ case 'consumer-groups':
380
+ case 'dead-letter-queue':
381
+ case 'retry':
382
+ return false;
383
+ default:
384
+ return false;
385
+ }
386
+ }
387
+
388
+ // ============================================================================
389
+ // Events
390
+ // ============================================================================
391
+
392
+ on<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
393
+ if (!this.eventHandlers.has(event)) {
394
+ this.eventHandlers.set(event, new Set());
395
+ }
396
+ this.eventHandlers.get(event)!.add(handler as (...args: unknown[]) => void);
397
+ }
398
+
399
+ off<E extends keyof QueueEvents>(event: E, handler: NonNullable<QueueEvents[E]>): void {
400
+ const handlers = this.eventHandlers.get(event);
401
+ if (handlers) {
402
+ handlers.delete(handler as (...args: unknown[]) => void);
403
+ }
404
+ }
405
+
406
+ // ============================================================================
407
+ // Private Methods
408
+ // ============================================================================
409
+
410
+ private ensureConnected(): void {
411
+ if (!this.connected) {
412
+ throw new Error('InMemoryQueueAdapter not connected. Call connect() first.');
413
+ }
414
+ }
415
+
416
+ private generateMessageId(): string {
417
+ return `msg-${++this.messageIdCounter}-${Date.now()}`;
418
+ }
419
+
420
+ private async dispatch<T>(
421
+ pattern: string,
422
+ data: T,
423
+ messageId: string,
424
+ metadata?: Partial<MessageMetadata>,
425
+ ): Promise<void> {
426
+ const fullMetadata: MessageMetadata = {
427
+ headers: {},
428
+ ...metadata,
429
+ };
430
+
431
+ // Find all matching subscriptions
432
+ for (const entry of this.subscriptions) {
433
+ if (entry.paused) {
434
+ continue;
435
+ }
436
+
437
+ const match = entry.matcher(pattern);
438
+ if (!match.matched) {
439
+ continue;
440
+ }
441
+
442
+ const message = new InMemoryMessage<T>(messageId, pattern, data, fullMetadata, {
443
+ onNack: (requeue) => {
444
+ if (requeue) {
445
+ // Re-dispatch the message
446
+ setImmediate(() => {
447
+ this.dispatch(pattern, data, messageId, metadata);
448
+ });
449
+ }
450
+ },
451
+ });
452
+
453
+ // Emit received event
454
+ this.emit('onMessageReceived', message);
455
+
456
+ try {
457
+ await entry.handler(message);
458
+
459
+ // Auto-ack if not manual mode
460
+ if (entry.options?.ackMode !== 'manual') {
461
+ await message.ack();
462
+ }
463
+
464
+ // Emit processed event
465
+ this.emit('onMessageProcessed', message);
466
+ } catch (error) {
467
+ // Emit failed event
468
+ this.emit('onMessageFailed', message, error as Error);
469
+
470
+ // Auto-nack if not manual mode
471
+ if (entry.options?.ackMode !== 'manual') {
472
+ await message.nack(false);
473
+ }
474
+ }
475
+ }
476
+ }
477
+
478
+ private processDelayedMessages(): void {
479
+ const now = Date.now();
480
+
481
+ while (this.delayedMessages.length > 0 && this.delayedMessages[0].executeAt <= now) {
482
+ const delayed = this.delayedMessages.shift()!;
483
+ const messageId = delayed.options?.messageId ?? this.generateMessageId();
484
+
485
+ // Dispatch without delay
486
+ this.dispatch(delayed.pattern, delayed.data, messageId, delayed.options?.metadata);
487
+ }
488
+ }
489
+
490
+ private emit<E extends keyof QueueEvents>(event: E, ...args: unknown[]): void {
491
+ const handlers = this.eventHandlers.get(event);
492
+ if (handlers) {
493
+ for (const handler of handlers) {
494
+ try {
495
+ handler(...args);
496
+ } catch {
497
+ // Silently ignore event handler errors
498
+ }
499
+ }
500
+ }
501
+ }
502
+ }
503
+
504
+ /**
505
+ * Create an in-memory queue adapter
506
+ */
507
+ export function createInMemoryQueueAdapter(): InMemoryQueueAdapter {
508
+ return new InMemoryQueueAdapter();
509
+ }