@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
package/src/index.ts CHANGED
@@ -1,7 +1,4 @@
1
- // export * from './metadata';
2
- // export * from './types';
3
- // export * from './config.service';
4
- // Re-export envs from @onebun/envs
1
+ // Re-export from external packages
5
2
  export { Env, type EnvSchema, EnvValidationError } from '@onebun/envs';
6
3
  export type { SyncLogger } from '@onebun/logger';
7
4
  export {
@@ -15,51 +12,63 @@ export {
15
12
  type SuccessResponse,
16
13
  } from '@onebun/requests';
17
14
  export { Span } from '@onebun/trace';
15
+
18
16
  // Re-export Effect and Layer from effect
19
17
  export { Effect, Layer } from 'effect';
20
- export { OneBunApplication } from './application';
21
- export { Controller as BaseController } from './controller';
22
- export * from './decorators';
23
- export { defineMetadata, getMetadata } from './metadata';
24
- export { OneBunModule } from './module';
25
- export { BaseService, getServiceTag, Service } from './service';
26
- export {
27
- FakeTimers,
28
- fakeTimers,
29
- useFakeTimers,
30
- makeMockLoggerLayer,
31
- } from './test-utils';
18
+
19
+ // Types (excluding WebSocket types that are re-exported from ./websocket)
32
20
  export {
33
21
  HttpMethod,
34
- type RouteMetadata,
35
- type ControllerMetadata,
22
+ ParamType,
23
+ type ServiceInterface,
24
+ type ModuleProviders,
25
+ type ModuleInstance,
26
+ type TypedEnvSchema,
27
+ type ApplicationOptions,
36
28
  type ParamMetadata,
37
29
  type ResponseSchemaMetadata,
38
- ParamType,
30
+ type RouteMetadata,
31
+ type ControllerMetadata,
32
+ // WebSocket types are exported from ./websocket
33
+ type WsStorageType,
34
+ type WsStorageOptions,
35
+ type WebSocketApplicationOptions,
39
36
  } from './types';
40
- export * from './validation';
41
37
 
42
- // Multi-service application
43
- export { MultiServiceApplication } from './multi-service-application';
44
- export type {
45
- MultiServiceApplicationOptions,
46
- ServiceConfig,
47
- ServicesMap,
48
- BaseServiceOptions,
49
- EnvOverrideValue,
50
- EnvOverrides,
51
- } from './multi-service.types';
38
+ // Decorators and Metadata (exports Controller decorator, Module decorator, etc.)
39
+ export * from './decorators';
40
+
41
+ // Module System - explicitly re-export to avoid Controller conflict
42
+ export {
43
+ OneBunModule,
44
+ Controller as BaseController,
45
+ BaseService,
46
+ Service,
47
+ getServiceMetadata,
48
+ getServiceTag,
49
+ createServiceLayer,
50
+ ConfigServiceImpl,
51
+ ConfigServiceTag,
52
+ ConfigService,
53
+ } from './module';
54
+
55
+ // Application
56
+ export * from './application';
52
57
 
53
- // Service definition and client
54
- export { createServiceDefinition } from './service-definition';
55
- export type {
56
- ServiceDefinition,
57
- EndpointMetadata,
58
- ControllerDefinition,
59
- } from './service-definition';
58
+ // Service Client
59
+ export * from './service-client';
60
60
 
61
- export { createServiceClient, getServiceUrl } from './service-client';
62
- export type { ServiceClientOptions, ControllerClient } from './service-client.types';
61
+ // Redis
62
+ export * from './redis';
63
+
64
+ // WebSocket
65
+ export * from './websocket';
66
+
67
+ // Queue System
68
+ export * from './queue';
69
+
70
+ // Validation
71
+ export * from './validation';
63
72
 
64
- // ENV resolver
65
- export { resolveEnvOverrides, resolveEnvOverridesSync } from './env-resolver';
73
+ // Testing Utilities
74
+ export * from './testing';
@@ -0,0 +1,12 @@
1
+ /**
2
+ * Module System
3
+ *
4
+ * Core module, controller, and service abstractions.
5
+ */
6
+
7
+ export * from './module';
8
+ export * from './controller';
9
+ // Re-export Controller as BaseController for backward compatibility
10
+ export { Controller as BaseController } from './controller';
11
+ export * from './service';
12
+ export * from './config.service';
@@ -11,9 +11,10 @@ import {
11
11
  } from 'bun:test';
12
12
  import { Context } from 'effect';
13
13
 
14
- import { makeDevLogger } from '../../logger/src/logger';
14
+ import { makeDevLogger } from '@onebun/logger';
15
+
16
+ import { Module } from '../decorators/decorators';
15
17
 
16
- import { Module } from './decorators';
17
18
  import { OneBunModule } from './module';
18
19
  import { Service } from './service';
19
20
 
@@ -5,7 +5,7 @@ import {
5
5
  } from 'effect';
6
6
 
7
7
  import type { Controller } from './controller';
8
- import type { Module } from './types';
8
+ import type { ModuleInstance } from '../types';
9
9
 
10
10
  import {
11
11
  createSyncLogger,
@@ -21,13 +21,15 @@ import {
21
21
  getConstructorParamTypes,
22
22
  getModuleMetadata,
23
23
  registerControllerDependencies,
24
- } from './decorators';
24
+ } from '../decorators/decorators';
25
+ import { isWebSocketGateway } from '../websocket/ws-decorators';
26
+
25
27
  import { getServiceMetadata, getServiceTag } from './service';
26
28
 
27
29
  /**
28
30
  * OneBun Module implementation
29
31
  */
30
- export class OneBunModule implements Module {
32
+ export class OneBunModule implements ModuleInstance {
31
33
  private rootLayer: Layer.Layer<never, never, unknown>;
32
34
  private controllers: Function[] = [];
33
35
  private controllerInstances: Map<Function, Controller> = new Map();
@@ -331,14 +333,19 @@ export class OneBunModule implements Module {
331
333
  const controllerConstructor = controllerClass as new (...args: unknown[]) => Controller;
332
334
  const controller = new controllerConstructor(...dependencies);
333
335
 
334
- // Initialize controller with logger and config
335
- controller.initializeController(this.logger, this.config);
336
+ // Initialize controller with logger and config (skip for WebSocket gateways)
337
+ if (!isWebSocketGateway(controllerClass) && typeof controller.initializeController === 'function') {
338
+ controller.initializeController(this.logger, this.config);
339
+ }
336
340
 
337
341
  this.controllerInstances.set(controllerClass, controller);
338
342
 
339
343
  // Inject all services into controller (for legacy compatibility)
340
- for (const [tag, serviceInstance] of this.serviceInstances.entries()) {
341
- controller.setService(tag, serviceInstance);
344
+ // Skip for WebSocket gateways which don't have setService
345
+ if (!isWebSocketGateway(controllerClass) && typeof controller.setService === 'function') {
346
+ for (const [tag, serviceInstance] of this.serviceInstances.entries()) {
347
+ controller.setService(tag, serviceInstance);
348
+ }
342
349
  }
343
350
 
344
351
  if (paramTypes && paramTypes.length > 0) {
@@ -432,7 +439,7 @@ export class OneBunModule implements Module {
432
439
  moduleClass: Function,
433
440
  loggerLayer?: Layer.Layer<never, never, unknown>,
434
441
  config?: unknown,
435
- ): Module {
442
+ ): ModuleInstance {
436
443
  // Using console.log here because we don't have access to the logger instance yet
437
444
  // The instance will create its own logger in the constructor
438
445
  return new OneBunModule(moduleClass, loggerLayer, config);
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Queue Adapters
3
+ *
4
+ * Built-in queue adapters for OneBun.
5
+ */
6
+
7
+ export { InMemoryQueueAdapter, createInMemoryQueueAdapter } from './memory.adapter';
8
+ export { RedisQueueAdapter, createRedisQueueAdapter, type RedisQueueOptions } from './redis.adapter';
@@ -0,0 +1,405 @@
1
+ /**
2
+ * In-Memory Queue Adapter 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, createInMemoryQueueAdapter } from './memory.adapter';
16
+
17
+ describe('InMemoryQueueAdapter', () => {
18
+ let adapter: InMemoryQueueAdapter;
19
+
20
+ beforeEach(() => {
21
+ adapter = new InMemoryQueueAdapter();
22
+ });
23
+
24
+ afterEach(async () => {
25
+ await adapter.disconnect();
26
+ });
27
+
28
+ describe('lifecycle', () => {
29
+ it('should connect successfully', async () => {
30
+ expect(adapter.isConnected()).toBe(false);
31
+ await adapter.connect();
32
+ expect(adapter.isConnected()).toBe(true);
33
+ });
34
+
35
+ it('should disconnect successfully', async () => {
36
+ await adapter.connect();
37
+ expect(adapter.isConnected()).toBe(true);
38
+ await adapter.disconnect();
39
+ expect(adapter.isConnected()).toBe(false);
40
+ });
41
+
42
+ it('should handle multiple connect calls', async () => {
43
+ await adapter.connect();
44
+ await adapter.connect();
45
+ expect(adapter.isConnected()).toBe(true);
46
+ });
47
+
48
+ it('should have correct name and type', () => {
49
+ expect(adapter.name).toBe('memory');
50
+ expect(adapter.type).toBe('memory');
51
+ });
52
+ });
53
+
54
+ describe('publish/subscribe', () => {
55
+ it('should publish and receive messages', async () => {
56
+ await adapter.connect();
57
+
58
+ const received: Message[] = [];
59
+ await adapter.subscribe('orders.created', async (message) => {
60
+ received.push(message);
61
+ });
62
+
63
+ await adapter.publish('orders.created', { orderId: 123 });
64
+
65
+ expect(received.length).toBe(1);
66
+ expect(received[0].data).toEqual({ orderId: 123 });
67
+ expect(received[0].pattern).toBe('orders.created');
68
+ });
69
+
70
+ it('should match wildcard patterns', async () => {
71
+ await adapter.connect();
72
+
73
+ const received: Message[] = [];
74
+ await adapter.subscribe('orders.*', async (message) => {
75
+ received.push(message);
76
+ });
77
+
78
+ await adapter.publish('orders.created', { type: 'created' });
79
+ await adapter.publish('orders.updated', { type: 'updated' });
80
+ await adapter.publish('users.created', { type: 'user' }); // Should not match
81
+
82
+ expect(received.length).toBe(2);
83
+ expect(received[0].data).toEqual({ type: 'created' });
84
+ expect(received[1].data).toEqual({ type: 'updated' });
85
+ });
86
+
87
+ it('should match multi-level wildcard (#)', async () => {
88
+ await adapter.connect();
89
+
90
+ const received: Message[] = [];
91
+ await adapter.subscribe('events.#', async (message) => {
92
+ received.push(message);
93
+ });
94
+
95
+ await adapter.publish('events.user.created', { type: 'user' });
96
+ await adapter.publish('events.order.payment.completed', { type: 'payment' });
97
+
98
+ expect(received.length).toBe(2);
99
+ });
100
+
101
+ it('should return message ID on publish', async () => {
102
+ await adapter.connect();
103
+
104
+ const messageId = await adapter.publish('test', { data: 'test' });
105
+ expect(messageId).toMatch(/^msg-\d+-\d+$/);
106
+ });
107
+
108
+ it('should use custom message ID when provided', async () => {
109
+ await adapter.connect();
110
+
111
+ const messageId = await adapter.publish('test', { data: 'test' }, { messageId: 'custom-id' });
112
+ expect(messageId).toBe('custom-id');
113
+ });
114
+ });
115
+
116
+ describe('delayed messages', () => {
117
+ it('should delay message delivery', async () => {
118
+ await adapter.connect();
119
+
120
+ const received: Message[] = [];
121
+ await adapter.subscribe('delayed', async (message) => {
122
+ received.push(message);
123
+ });
124
+
125
+ await adapter.publish('delayed', { data: 'delayed' }, { delay: 50 });
126
+
127
+ // Message should not be received immediately
128
+ expect(received.length).toBe(0);
129
+
130
+ // Wait for delay + processing
131
+ await new Promise((resolve) => setTimeout(resolve, 200));
132
+
133
+ expect(received.length).toBe(1);
134
+ expect(received[0].data).toEqual({ data: 'delayed' });
135
+ });
136
+ });
137
+
138
+ describe('subscription management', () => {
139
+ it('should unsubscribe successfully', async () => {
140
+ await adapter.connect();
141
+
142
+ const received: Message[] = [];
143
+ const subscription = await adapter.subscribe('test', async (message) => {
144
+ received.push(message);
145
+ });
146
+
147
+ await adapter.publish('test', { count: 1 });
148
+ expect(received.length).toBe(1);
149
+
150
+ await subscription.unsubscribe();
151
+
152
+ await adapter.publish('test', { count: 2 });
153
+ expect(received.length).toBe(1); // Still 1, no new messages
154
+ });
155
+
156
+ it('should pause and resume subscription', async () => {
157
+ await adapter.connect();
158
+
159
+ const received: Message[] = [];
160
+ const subscription = await adapter.subscribe('test', async (message) => {
161
+ received.push(message);
162
+ });
163
+
164
+ await adapter.publish('test', { count: 1 });
165
+ expect(received.length).toBe(1);
166
+
167
+ subscription.pause();
168
+
169
+ await adapter.publish('test', { count: 2 });
170
+ expect(received.length).toBe(1); // Paused
171
+
172
+ subscription.resume();
173
+
174
+ await adapter.publish('test', { count: 3 });
175
+ expect(received.length).toBe(2);
176
+ });
177
+
178
+ it('should report subscription state', async () => {
179
+ await adapter.connect();
180
+
181
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
182
+ const subscription = await adapter.subscribe('test', async () => {});
183
+
184
+ expect(subscription.isActive).toBe(true);
185
+ expect(subscription.pattern).toBe('test');
186
+
187
+ subscription.pause();
188
+ expect(subscription.isActive).toBe(false);
189
+
190
+ subscription.resume();
191
+ expect(subscription.isActive).toBe(true);
192
+
193
+ await subscription.unsubscribe();
194
+ expect(subscription.isActive).toBe(false);
195
+ });
196
+ });
197
+
198
+ describe('batch publishing', () => {
199
+ it('should publish multiple messages', async () => {
200
+ await adapter.connect();
201
+
202
+ const received: Message[] = [];
203
+ await adapter.subscribe('batch.*', async (message) => {
204
+ received.push(message);
205
+ });
206
+
207
+ const ids = await adapter.publishBatch([
208
+ { pattern: 'batch.1', data: { index: 1 } },
209
+ { pattern: 'batch.2', data: { index: 2 } },
210
+ { pattern: 'batch.3', data: { index: 3 } },
211
+ ]);
212
+
213
+ expect(ids.length).toBe(3);
214
+ expect(received.length).toBe(3);
215
+ });
216
+ });
217
+
218
+ describe('message acknowledgment', () => {
219
+ it('should auto-ack by default', async () => {
220
+ await adapter.connect();
221
+
222
+ let messageRef: Message | null = null;
223
+ await adapter.subscribe('test', async (message) => {
224
+ messageRef = message;
225
+ });
226
+
227
+ await adapter.publish('test', { data: 'test' });
228
+
229
+ expect(messageRef).not.toBeNull();
230
+ // In auto mode, ack is called automatically
231
+ });
232
+
233
+ it('should support manual ack mode', async () => {
234
+ await adapter.connect();
235
+
236
+ let messageRef: Message | null = null;
237
+ await adapter.subscribe(
238
+ 'test',
239
+ async (message) => {
240
+ messageRef = message;
241
+ // In manual mode, we need to ack
242
+ await message.ack();
243
+ },
244
+ { ackMode: 'manual' },
245
+ );
246
+
247
+ await adapter.publish('test', { data: 'test' });
248
+
249
+ expect(messageRef).not.toBeNull();
250
+ });
251
+
252
+ it('should support nack with requeue', async () => {
253
+ await adapter.connect();
254
+
255
+ let callCount = 0;
256
+ await adapter.subscribe(
257
+ 'test',
258
+ async (message) => {
259
+ callCount++;
260
+ if (callCount === 1) {
261
+ await message.nack(true); // Requeue
262
+ } else {
263
+ await message.ack();
264
+ }
265
+ },
266
+ { ackMode: 'manual' },
267
+ );
268
+
269
+ await adapter.publish('test', { data: 'test' });
270
+
271
+ // Wait for requeue processing (setImmediate is used for requeue)
272
+ await new Promise((resolve) => setTimeout(resolve, 50));
273
+
274
+ expect(callCount).toBeGreaterThanOrEqual(2); // Should be called at least twice due to requeue
275
+ });
276
+ });
277
+
278
+ describe('events', () => {
279
+ it('should emit onReady event on connect', async () => {
280
+ let readyEmitted = false;
281
+ adapter.on('onReady', () => {
282
+ readyEmitted = true;
283
+ });
284
+
285
+ await adapter.connect();
286
+ expect(readyEmitted).toBe(true);
287
+ });
288
+
289
+ it('should emit onMessageReceived event', async () => {
290
+ await adapter.connect();
291
+
292
+ let receivedMessage: Message | null = null;
293
+ adapter.on('onMessageReceived', (message) => {
294
+ receivedMessage = message as Message;
295
+ });
296
+
297
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
298
+ await adapter.subscribe('test', async () => {});
299
+ await adapter.publish('test', { data: 'test' });
300
+
301
+ expect(receivedMessage).not.toBeNull();
302
+ expect(receivedMessage!.pattern).toBe('test');
303
+ });
304
+
305
+ it('should emit onMessageProcessed event', async () => {
306
+ await adapter.connect();
307
+
308
+ let processed = false;
309
+ adapter.on('onMessageProcessed', () => {
310
+ processed = true;
311
+ });
312
+
313
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
314
+ await adapter.subscribe('test', async () => {});
315
+ await adapter.publish('test', { data: 'test' });
316
+
317
+ expect(processed).toBe(true);
318
+ });
319
+
320
+ it('should emit onMessageFailed event on error', async () => {
321
+ await adapter.connect();
322
+
323
+ let failedMessage: Message | null = null;
324
+ let failedError: Error | null = null;
325
+ adapter.on('onMessageFailed', (message, error) => {
326
+ failedMessage = message as Message;
327
+ failedError = error as Error;
328
+ });
329
+
330
+ await adapter.subscribe('test', async () => {
331
+ throw new Error('Handler error');
332
+ });
333
+ await adapter.publish('test', { data: 'test' });
334
+
335
+ expect(failedMessage).not.toBeNull();
336
+ expect(failedError).not.toBeNull();
337
+ expect(failedError!.message).toBe('Handler error');
338
+ });
339
+
340
+ it('should unregister event handlers', async () => {
341
+ let callCount = 0;
342
+ const handler = () => {
343
+ callCount++;
344
+ };
345
+
346
+ adapter.on('onReady', handler);
347
+ await adapter.connect();
348
+ expect(callCount).toBe(1);
349
+
350
+ await adapter.disconnect();
351
+ adapter.off('onReady', handler);
352
+
353
+ await adapter.connect();
354
+ expect(callCount).toBe(1); // Should still be 1
355
+ });
356
+ });
357
+
358
+ describe('feature support', () => {
359
+ it('should support pattern-subscriptions', () => {
360
+ expect(adapter.supports('pattern-subscriptions')).toBe(true);
361
+ });
362
+
363
+ it('should support delayed-messages', () => {
364
+ expect(adapter.supports('delayed-messages')).toBe(true);
365
+ });
366
+
367
+ it('should support priority', () => {
368
+ expect(adapter.supports('priority')).toBe(true);
369
+ });
370
+
371
+ it('should support scheduled-jobs', () => {
372
+ expect(adapter.supports('scheduled-jobs')).toBe(true);
373
+ });
374
+
375
+ it('should not support consumer-groups', () => {
376
+ expect(adapter.supports('consumer-groups')).toBe(false);
377
+ });
378
+
379
+ it('should not support dead-letter-queue', () => {
380
+ expect(adapter.supports('dead-letter-queue')).toBe(false);
381
+ });
382
+
383
+ it('should not support retry', () => {
384
+ expect(adapter.supports('retry')).toBe(false);
385
+ });
386
+ });
387
+
388
+ describe('error handling', () => {
389
+ it('should throw when publishing without connecting', async () => {
390
+ await expect(adapter.publish('test', { data: 'test' })).rejects.toThrow();
391
+ });
392
+
393
+ it('should throw when subscribing without connecting', async () => {
394
+ // eslint-disable-next-line @typescript-eslint/no-empty-function
395
+ await expect(adapter.subscribe('test', async () => {})).rejects.toThrow();
396
+ });
397
+ });
398
+
399
+ describe('createInMemoryQueueAdapter', () => {
400
+ it('should create adapter instance', () => {
401
+ const created = createInMemoryQueueAdapter();
402
+ expect(created).toBeInstanceOf(InMemoryQueueAdapter);
403
+ });
404
+ });
405
+ });