@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
@@ -1,6 +1,7 @@
1
1
  import { Effect, type Layer } from 'effect';
2
2
 
3
- import type { Controller } from './controller';
3
+ import type { Controller } from '../module/controller';
4
+ import type { WsClientData } from '../websocket/ws.types';
4
5
 
5
6
  import { TypedEnv } from '@onebun/envs';
6
7
  import {
@@ -19,18 +20,24 @@ import {
19
20
  } from '@onebun/requests';
20
21
  import { makeTraceService, TraceService } from '@onebun/trace';
21
22
 
22
- import { ConfigServiceImpl } from './config.service';
23
- import { getControllerMetadata } from './decorators';
24
- import { OneBunModule } from './module';
23
+ import { getControllerMetadata } from '../decorators/decorators';
24
+ import { ConfigServiceImpl } from '../module/config.service';
25
+ import { OneBunModule } from '../module/module';
26
+ import { QueueService, type QueueAdapter } from '../queue';
27
+ import { InMemoryQueueAdapter } from '../queue/adapters/memory.adapter';
28
+ import { RedisQueueAdapter } from '../queue/adapters/redis.adapter';
29
+ import { hasQueueDecorators } from '../queue/decorators';
30
+ import { SharedRedisProvider } from '../redis/shared-redis';
25
31
  import {
26
32
  type ApplicationOptions,
27
33
  type HttpMethod,
28
- type Module,
34
+ type ModuleInstance,
29
35
  type ParamMetadata,
30
36
  ParamType,
31
37
  type RouteMetadata,
32
- } from './types';
33
- import { validateOrThrow } from './validation';
38
+ } from '../types';
39
+ import { validateOrThrow } from '../validation';
40
+ import { WsHandler, isWebSocketGateway } from '../websocket/ws-handler';
34
41
 
35
42
  // Conditionally import metrics
36
43
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
@@ -60,7 +67,7 @@ function clearGlobalTraceContext(): void {
60
67
  * OneBun Application
61
68
  */
62
69
  export class OneBunApplication {
63
- private rootModule: Module;
70
+ private rootModule: ModuleInstance;
64
71
  private server: ReturnType<typeof Bun.serve> | null = null;
65
72
  private options: ApplicationOptions = {
66
73
  port: 3000,
@@ -75,6 +82,9 @@ export class OneBunApplication {
75
82
  private metricsService: any = null;
76
83
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
77
84
  private traceService: any = null;
85
+ private wsHandler: WsHandler | null = null;
86
+ private queueService: QueueService | null = null;
87
+ private queueAdapter: QueueAdapter | null = null;
78
88
 
79
89
  /**
80
90
  * Create application instance
@@ -260,6 +270,23 @@ export class OneBunApplication {
260
270
  const controllers = this.rootModule.getControllers();
261
271
  this.logger.debug(`Loaded ${controllers.length} controllers`);
262
272
 
273
+ // Initialize WebSocket handler and detect gateways
274
+ this.wsHandler = new WsHandler(this.logger, this.options.websocket);
275
+
276
+ // Register WebSocket gateways (they are in controllers array but decorated with @WebSocketGateway)
277
+ for (const controllerClass of controllers) {
278
+ if (isWebSocketGateway(controllerClass)) {
279
+ const instance = this.rootModule.getControllerInstance?.(controllerClass);
280
+ if (instance) {
281
+ this.wsHandler.registerGateway(controllerClass, instance as import('../websocket/ws-base-gateway').BaseWebSocketGateway);
282
+ this.logger.info(`Registered WebSocket gateway: ${controllerClass.name}`);
283
+ }
284
+ }
285
+ }
286
+
287
+ // Initialize Queue system if configured or handlers exist
288
+ await this.initializeQueue(controllers);
289
+
263
290
  // Create a map of routes with metadata
264
291
  const routes = new Map<
265
292
  string,
@@ -359,15 +386,48 @@ export class OneBunApplication {
359
386
 
360
387
  // Create server with proper context binding
361
388
  const app = this;
362
- this.server = Bun.serve({
389
+ const hasWebSocketGateways = this.wsHandler?.hasGateways() ?? false;
390
+
391
+ // Prepare WebSocket handlers if gateways exist
392
+ // When no gateways, use no-op handlers (required by Bun.serve)
393
+ const wsHandlers = hasWebSocketGateways ? this.wsHandler!.createWebSocketHandlers() : {
394
+
395
+ open() { /* no-op */ },
396
+
397
+ message() { /* no-op */ },
398
+
399
+ close() { /* no-op */ },
400
+
401
+ drain() { /* no-op */ },
402
+ };
403
+
404
+ this.server = Bun.serve<WsClientData>({
363
405
  port: this.options.port,
364
406
  hostname: this.options.host,
365
- async fetch(req) {
407
+ // WebSocket handlers
408
+ websocket: wsHandlers,
409
+ async fetch(req, server) {
366
410
  const url = new URL(req.url);
367
411
  const path = url.pathname;
368
412
  const method = req.method;
369
413
  const startTime = Date.now();
370
414
 
415
+ // Handle WebSocket upgrade if gateways exist
416
+ if (hasWebSocketGateways && app.wsHandler) {
417
+ const upgradeHeader = req.headers.get('upgrade')?.toLowerCase();
418
+
419
+ // Check for WebSocket upgrade or Socket.IO polling
420
+ if (upgradeHeader === 'websocket' || path.startsWith('/socket.io')) {
421
+ const response = await app.wsHandler.handleUpgrade(req, server);
422
+ if (response === undefined) {
423
+ return undefined; // Successfully upgraded
424
+ }
425
+
426
+ // Return response if upgrade failed
427
+ return response;
428
+ }
429
+ }
430
+
371
431
  // Setup tracing context if available and enabled
372
432
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
373
433
  let traceSpan: any = null;
@@ -675,6 +735,12 @@ export class OneBunApplication {
675
735
  },
676
736
  });
677
737
 
738
+ // Initialize WebSocket gateways with server
739
+ if (hasWebSocketGateways && this.wsHandler && this.server) {
740
+ this.wsHandler.initializeGateways(this.server);
741
+ this.logger.info(`WebSocket server enabled at ws://${this.options.host}:${this.options.port}`);
742
+ }
743
+
678
744
  this.logger.info(`Server started on http://${this.options.host}:${this.options.port}`);
679
745
  if (this.metricsService) {
680
746
  this.logger.info(
@@ -685,6 +751,11 @@ export class OneBunApplication {
685
751
  'Metrics enabled but @onebun/metrics module not available. Install with: bun add @onebun/metrics',
686
752
  );
687
753
  }
754
+
755
+ // Enable graceful shutdown by default (can be disabled with gracefulShutdown: false)
756
+ if (this.options.gracefulShutdown !== false) {
757
+ this.enableGracefulShutdown();
758
+ }
688
759
  } catch (error) {
689
760
  this.logger.error(
690
761
  'Failed to start application:',
@@ -910,14 +981,169 @@ export class OneBunApplication {
910
981
  }
911
982
 
912
983
  /**
913
- * Stop the application
984
+ * Stop the application with graceful shutdown
985
+ * @param options - Shutdown options
914
986
  */
915
- stop(): void {
987
+ async stop(options?: { closeSharedRedis?: boolean }): Promise<void> {
988
+ const closeRedis = options?.closeSharedRedis ?? true;
989
+
990
+ this.logger.info('Stopping OneBun application...');
991
+
992
+ // Cleanup WebSocket resources
993
+ if (this.wsHandler) {
994
+ this.logger.debug('Cleaning up WebSocket handler');
995
+ await this.wsHandler.cleanup();
996
+ this.wsHandler = null;
997
+ }
998
+
999
+ // Stop queue service
1000
+ if (this.queueService) {
1001
+ this.logger.debug('Stopping queue service');
1002
+ await this.queueService.stop();
1003
+ this.queueService = null;
1004
+ }
1005
+
1006
+ // Disconnect queue adapter
1007
+ if (this.queueAdapter) {
1008
+ this.logger.debug('Disconnecting queue adapter');
1009
+ await this.queueAdapter.disconnect();
1010
+ this.queueAdapter = null;
1011
+ }
1012
+
1013
+ // Stop HTTP server
916
1014
  if (this.server) {
917
1015
  this.server.stop();
918
1016
  this.server = null;
919
- this.logger.info('OneBun application stopped');
1017
+ this.logger.debug('HTTP server stopped');
1018
+ }
1019
+
1020
+ // Close shared Redis connection if configured and requested
1021
+ if (closeRedis && SharedRedisProvider.isConnected()) {
1022
+ this.logger.debug('Disconnecting shared Redis');
1023
+ await SharedRedisProvider.disconnect();
920
1024
  }
1025
+
1026
+ this.logger.info('OneBun application stopped');
1027
+ }
1028
+
1029
+ /**
1030
+ * Initialize the queue system based on configuration and detected handlers
1031
+ */
1032
+ private async initializeQueue(controllers: Function[]): Promise<void> {
1033
+ const queueOptions = this.options.queue;
1034
+
1035
+ // Check if any controller has queue-related decorators
1036
+ const hasQueueHandlers = controllers.some(controller => {
1037
+ const instance = this.rootModule.getControllerInstance?.(controller);
1038
+ if (!instance) {
1039
+ return false;
1040
+ }
1041
+
1042
+ return hasQueueDecorators(instance.constructor);
1043
+ });
1044
+
1045
+ // Determine if queue should be enabled
1046
+ const shouldEnableQueue = queueOptions?.enabled ?? hasQueueHandlers;
1047
+ if (!shouldEnableQueue) {
1048
+ this.logger.debug('Queue system not enabled (no handlers detected or explicitly disabled)');
1049
+
1050
+ return;
1051
+ }
1052
+
1053
+ // Create the appropriate adapter
1054
+ const adapterType = queueOptions?.adapter ?? 'memory';
1055
+
1056
+ if (adapterType === 'memory') {
1057
+ this.queueAdapter = new InMemoryQueueAdapter();
1058
+ this.logger.info('Queue system initialized with in-memory adapter');
1059
+ } else if (adapterType === 'redis') {
1060
+ const redisOptions = queueOptions?.redis ?? {};
1061
+ if (redisOptions.useSharedProvider !== false) {
1062
+ // Use shared Redis provider
1063
+ this.queueAdapter = new RedisQueueAdapter({
1064
+ useSharedClient: true,
1065
+ keyPrefix: redisOptions.prefix ?? 'onebun:queue:',
1066
+ });
1067
+ this.logger.info('Queue system initialized with Redis adapter (shared provider)');
1068
+ } else if (redisOptions.url) {
1069
+ // Create dedicated Redis connection
1070
+ this.queueAdapter = new RedisQueueAdapter({
1071
+ useSharedClient: false,
1072
+ url: redisOptions.url,
1073
+ keyPrefix: redisOptions.prefix ?? 'onebun:queue:',
1074
+ });
1075
+ this.logger.info('Queue system initialized with Redis adapter (dedicated connection)');
1076
+ } else {
1077
+ throw new Error('Redis queue adapter requires either useSharedProvider: true or a url');
1078
+ }
1079
+ } else {
1080
+ throw new Error(`Unknown queue adapter type: ${adapterType}`);
1081
+ }
1082
+
1083
+ // Connect the adapter
1084
+ await this.queueAdapter.connect();
1085
+
1086
+ // Create queue service with config
1087
+ this.queueService = new QueueService({
1088
+ adapter: adapterType,
1089
+ options: queueOptions?.redis,
1090
+ });
1091
+
1092
+ // Initialize with the adapter
1093
+ await this.queueService.initialize(this.queueAdapter);
1094
+
1095
+ // Register handlers from controllers using registerService
1096
+ for (const controllerClass of controllers) {
1097
+ const instance = this.rootModule.getControllerInstance?.(controllerClass);
1098
+ if (!instance) {
1099
+ continue;
1100
+ }
1101
+
1102
+ // Only register if the controller has queue decorators
1103
+ if (hasQueueDecorators(controllerClass)) {
1104
+ await this.queueService.registerService(
1105
+ instance,
1106
+ controllerClass as new (...args: unknown[]) => unknown,
1107
+ );
1108
+ this.logger.debug(`Registered queue handlers for controller: ${controllerClass.name}`);
1109
+ }
1110
+ }
1111
+
1112
+ // Start the queue service
1113
+ await this.queueService.start();
1114
+ this.logger.info('Queue service started');
1115
+ }
1116
+
1117
+ /**
1118
+ * Get the queue service instance
1119
+ * @returns The queue service or null if not enabled
1120
+ */
1121
+ getQueueService(): QueueService | null {
1122
+ return this.queueService;
1123
+ }
1124
+
1125
+ /**
1126
+ * Register signal handlers for graceful shutdown
1127
+ * Call this after start() to enable automatic shutdown on SIGTERM/SIGINT
1128
+ *
1129
+ * @example
1130
+ * ```typescript
1131
+ * const app = new OneBunApplication(AppModule, options);
1132
+ * await app.start();
1133
+ * app.enableGracefulShutdown();
1134
+ * ```
1135
+ */
1136
+ enableGracefulShutdown(): void {
1137
+ const shutdown = async (signal: string) => {
1138
+ this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
1139
+ await this.stop();
1140
+ process.exit(0);
1141
+ };
1142
+
1143
+ process.on('SIGTERM', () => shutdown('SIGTERM'));
1144
+ process.on('SIGINT', () => shutdown('SIGINT'));
1145
+
1146
+ this.logger.debug('Graceful shutdown handlers registered');
921
1147
  }
922
1148
 
923
1149
  /**
@@ -0,0 +1,9 @@
1
+ /**
2
+ * Application Module
3
+ *
4
+ * Application bootstrapping and multi-service support.
5
+ */
6
+
7
+ export * from './multi-service.types';
8
+ export * from './application';
9
+ export * from './multi-service-application';
@@ -7,7 +7,8 @@ import {
7
7
 
8
8
  import { TypedEnv } from '@onebun/envs';
9
9
 
10
- import { Module } from './decorators';
10
+ import { Module } from '../decorators/decorators';
11
+
11
12
  import { MultiServiceApplication } from './multi-service-application';
12
13
 
13
14
  // Test modules
@@ -15,8 +15,9 @@ import {
15
15
  type SyncLogger,
16
16
  } from '@onebun/logger';
17
17
 
18
+ import { resolveEnvOverrides } from '../service-client/env-resolver';
19
+
18
20
  import { OneBunApplication } from './application';
19
- import { resolveEnvOverrides } from './env-resolver';
20
21
 
21
22
  /**
22
23
  * ENV schema for service filtering
@@ -1,4 +1,4 @@
1
- import type { ApplicationOptions } from './types';
1
+ import type { ApplicationOptions } from '../types';
2
2
 
3
3
  import type { EnvLoadOptions, EnvSchema } from '@onebun/envs';
4
4
 
@@ -12,6 +12,8 @@ import {
12
12
  beforeEach,
13
13
  } from 'bun:test';
14
14
 
15
+ import { HttpMethod, ParamType } from '../types';
16
+
15
17
  import {
16
18
  injectable,
17
19
  Controller,
@@ -39,7 +41,6 @@ import {
39
41
  getModuleMetadata,
40
42
  ApiResponse,
41
43
  } from './decorators';
42
- import { HttpMethod, ParamType } from './types';
43
44
 
44
45
  describe('decorators', () => {
45
46
  beforeEach(() => {
@@ -1,13 +1,14 @@
1
1
  import './metadata'; // Import polyfill first
2
2
  import type { Type } from 'arktype';
3
3
 
4
- import { getConstructorParamTypes as getDesignParamTypes, Reflect } from './metadata';
5
4
  import {
6
5
  type ControllerMetadata,
7
6
  HttpMethod,
8
7
  type ParamMetadata,
9
8
  ParamType,
10
- } from './types';
9
+ } from '../types';
10
+
11
+ import { getConstructorParamTypes as getDesignParamTypes, Reflect } from './metadata';
11
12
 
12
13
  /**
13
14
  * Metadata storage for controllers
@@ -0,0 +1,15 @@
1
+ /**
2
+ * Decorators Module
3
+ *
4
+ * HTTP decorators and metadata utilities.
5
+ */
6
+
7
+ // Export metadata utilities (excluding getConstructorParamTypes which is re-exported from decorators)
8
+ export {
9
+ defineMetadata,
10
+ getMetadata,
11
+ Reflect,
12
+ } from './metadata';
13
+
14
+ // Export all decorators (including getConstructorParamTypes)
15
+ export * from './decorators';