@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/README.md CHANGED
@@ -291,6 +291,170 @@ return this.error('User not found', 404);
291
291
 
292
292
  Errors thrown in controller methods are automatically caught and converted to standardized error responses.
293
293
 
294
+ ## WebSocket Gateway
295
+
296
+ OneBun provides WebSocket support with a Gateway pattern similar to NestJS, with full Socket.IO protocol compatibility.
297
+
298
+ ### Basic Gateway
299
+
300
+ ```typescript
301
+ import {
302
+ WebSocketGateway,
303
+ BaseWebSocketGateway,
304
+ OnConnect,
305
+ OnDisconnect,
306
+ OnMessage,
307
+ Client,
308
+ MessageData,
309
+ } from '@onebun/core';
310
+ import type { WsClientData } from '@onebun/core';
311
+
312
+ @WebSocketGateway({ path: '/ws' })
313
+ export class ChatGateway extends BaseWebSocketGateway {
314
+ @OnConnect()
315
+ handleConnect(@Client() client: WsClientData) {
316
+ console.log(`Client ${client.id} connected`);
317
+ return { event: 'welcome', data: { clientId: client.id } };
318
+ }
319
+
320
+ @OnDisconnect()
321
+ handleDisconnect(@Client() client: WsClientData) {
322
+ console.log(`Client ${client.id} disconnected`);
323
+ }
324
+
325
+ @OnMessage('chat:message')
326
+ handleMessage(
327
+ @Client() client: WsClientData,
328
+ @MessageData() data: { text: string }
329
+ ) {
330
+ // Broadcast to all clients
331
+ this.broadcast('chat:message', {
332
+ userId: client.id,
333
+ text: data.text,
334
+ });
335
+ }
336
+ }
337
+ ```
338
+
339
+ ### Pattern Matching
340
+
341
+ Event patterns support wildcards and named parameters:
342
+
343
+ ```typescript
344
+ // Wildcard: matches chat:general, chat:private, etc.
345
+ @OnMessage('chat:*')
346
+ handleAnyChat(@MessageData() data: unknown) {}
347
+
348
+ // Named parameter: extracts roomId
349
+ @OnMessage('chat:{roomId}:message')
350
+ handleRoomMessage(@PatternParams() params: { roomId: string }) {}
351
+ ```
352
+
353
+ ### Room Management
354
+
355
+ ```typescript
356
+ @WebSocketGateway({ path: '/ws' })
357
+ export class RoomGateway extends BaseWebSocketGateway {
358
+ @OnJoinRoom('room:{roomId}')
359
+ async handleJoin(
360
+ @Client() client: WsClientData,
361
+ @RoomName() room: string,
362
+ @PatternParams() params: { roomId: string }
363
+ ) {
364
+ await this.joinRoom(client.id, room);
365
+ this.emitToRoom(room, 'user:joined', { userId: client.id });
366
+ }
367
+
368
+ @OnLeaveRoom('room:*')
369
+ async handleLeave(@Client() client: WsClientData, @RoomName() room: string) {
370
+ await this.leaveRoom(client.id, room);
371
+ this.emitToRoom(room, 'user:left', { userId: client.id });
372
+ }
373
+ }
374
+ ```
375
+
376
+ ### Guards
377
+
378
+ Protect WebSocket handlers with guards:
379
+
380
+ ```typescript
381
+ import { UseWsGuards, WsAuthGuard, WsPermissionGuard } from '@onebun/core';
382
+
383
+ @UseWsGuards(WsAuthGuard)
384
+ @OnMessage('protected:*')
385
+ handleProtected(@Client() client: WsClientData) {
386
+ // Only authenticated clients can access
387
+ }
388
+
389
+ @UseWsGuards(new WsPermissionGuard('admin'))
390
+ @OnMessage('admin:*')
391
+ handleAdmin(@Client() client: WsClientData) {
392
+ // Only clients with 'admin' permission
393
+ }
394
+ ```
395
+
396
+ ### Typed Client
397
+
398
+ Generate a type-safe WebSocket client:
399
+
400
+ ```typescript
401
+ import { createWsServiceDefinition, createWsClient } from '@onebun/core';
402
+ import { AppModule } from './app.module';
403
+
404
+ const definition = createWsServiceDefinition(AppModule);
405
+ const client = createWsClient(definition, {
406
+ url: 'ws://localhost:3000/ws',
407
+ auth: { token: 'your-token' },
408
+ });
409
+
410
+ await client.connect();
411
+
412
+ // Type-safe event emission
413
+ await client.ChatGateway.emit('chat:message', { text: 'Hello!' });
414
+
415
+ // Subscribe to events
416
+ client.ChatGateway.on('chat:message', (data) => {
417
+ console.log('Received:', data);
418
+ });
419
+
420
+ client.disconnect();
421
+ ```
422
+
423
+ ### Storage Options
424
+
425
+ Configure client/room storage:
426
+
427
+ ```typescript
428
+ const app = new OneBunApplication(AppModule, {
429
+ websocket: {
430
+ storage: {
431
+ type: 'memory', // or 'redis' for multi-instance
432
+ redis: {
433
+ url: 'redis://localhost:6379',
434
+ prefix: 'ws:',
435
+ },
436
+ },
437
+ },
438
+ });
439
+ ```
440
+
441
+ ### Socket.IO Compatibility
442
+
443
+ OneBun WebSocket Gateway is fully compatible with `socket.io-client`:
444
+
445
+ ```typescript
446
+ import { io } from 'socket.io-client';
447
+
448
+ const socket = io('ws://localhost:3000/ws', {
449
+ auth: { token: 'your-token' },
450
+ });
451
+
452
+ socket.on('chat:message', (data) => console.log(data));
453
+ socket.emit('chat:message', { text: 'Hello!' });
454
+ ```
455
+
456
+ For complete API documentation, see [docs/api/websocket.md](../../docs/api/websocket.md).
457
+
294
458
  ## Application
295
459
 
296
460
  Create and start an OneBun application:
@@ -313,6 +477,75 @@ app.start()
313
477
 
314
478
  The application automatically creates a logger based on NODE_ENV (development or production) and handles all Effect.js calls internally.
315
479
 
480
+ ## Graceful Shutdown
481
+
482
+ OneBun supports graceful shutdown to cleanly close connections and release resources when the application stops. **Graceful shutdown is enabled by default.**
483
+
484
+ ### Default Behavior
485
+
486
+ By default, the application automatically handles SIGTERM and SIGINT signals:
487
+
488
+ ```typescript
489
+ const app = new OneBunApplication(AppModule, {
490
+ port: 3000,
491
+ // gracefulShutdown: true is the default
492
+ });
493
+
494
+ await app.start();
495
+ // Application will automatically handle shutdown signals
496
+ ```
497
+
498
+ ### Disabling Graceful Shutdown
499
+
500
+ If you need to manage shutdown manually, you can disable automatic handling:
501
+
502
+ ```typescript
503
+ const app = new OneBunApplication(AppModule, {
504
+ gracefulShutdown: false, // Disable automatic SIGTERM/SIGINT handling
505
+ });
506
+
507
+ await app.start();
508
+
509
+ // Now you must handle shutdown manually:
510
+ // Option 1: Enable signal handlers later
511
+ app.enableGracefulShutdown();
512
+
513
+ // Option 2: Stop programmatically
514
+ await app.stop();
515
+
516
+ // Option 3: Stop but keep shared Redis connection open (for other consumers)
517
+ await app.stop({ closeSharedRedis: false });
518
+ ```
519
+
520
+ ### What Gets Cleaned Up
521
+
522
+ When the application stops, the following resources are cleaned up:
523
+
524
+ 1. **HTTP Server** - Bun server is stopped
525
+ 2. **WebSocket Handler** - All WebSocket connections are closed
526
+ 3. **Shared Redis** - If using SharedRedisProvider, the connection is closed (unless `closeSharedRedis: false`)
527
+
528
+ ### Shared Redis Connection
529
+
530
+ When using `SharedRedisProvider` for cache, WebSocket storage, or other features, the connection is automatically closed on shutdown:
531
+
532
+ ```typescript
533
+ import { SharedRedisProvider, OneBunApplication } from '@onebun/core';
534
+
535
+ // Configure shared Redis at startup
536
+ SharedRedisProvider.configure({
537
+ url: 'redis://localhost:6379',
538
+ keyPrefix: 'myapp:',
539
+ });
540
+
541
+ const app = new OneBunApplication(AppModule, {
542
+ gracefulShutdown: true,
543
+ });
544
+
545
+ await app.start();
546
+ // Shared Redis will be closed when app receives SIGTERM/SIGINT
547
+ ```
548
+
316
549
  ## License
317
550
 
318
551
  [LGPL-3.0](../../LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onebun/core",
3
- "version": "0.1.1",
3
+ "version": "0.1.3",
4
4
  "description": "Core package for OneBun framework - decorators, DI, modules, controllers",
5
5
  "license": "LGPL-3.0",
6
6
  "author": "RemRyahirev",
@@ -9,10 +9,8 @@ import {
9
9
  // eslint-disable-next-line import/no-extraneous-dependencies
10
10
  import { register } from 'prom-client';
11
11
 
12
- import type { ApplicationOptions } from './types';
12
+ import type { ApplicationOptions } from '../types';
13
13
 
14
- import { OneBunApplication } from './application';
15
- import { Controller as BaseController } from './controller';
16
14
  import {
17
15
  Module,
18
16
  Controller,
@@ -21,8 +19,11 @@ import {
21
19
  Param,
22
20
  Query,
23
21
  Body,
24
- } from './decorators';
25
- import { makeMockLoggerLayer } from './test-utils';
22
+ } from '../decorators/decorators';
23
+ import { Controller as BaseController } from '../module/controller';
24
+ import { makeMockLoggerLayer } from '../testing/test-utils';
25
+
26
+ import { OneBunApplication } from './application';
26
27
 
27
28
  // Helper function to create app with mock logger to suppress logs in tests
28
29
  function createTestApp(
@@ -1102,4 +1103,123 @@ describe('OneBunApplication', () => {
1102
1103
  expect(mockTraceService.extractFromHeaders).not.toHaveBeenCalled();
1103
1104
  });
1104
1105
  });
1106
+
1107
+ describe('Graceful shutdown', () => {
1108
+ let originalServe: typeof Bun.serve;
1109
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1110
+ let mockServer: any;
1111
+
1112
+ beforeEach(() => {
1113
+ register.clear();
1114
+
1115
+ mockServer = {
1116
+ stop: mock(),
1117
+ hostname: 'localhost',
1118
+ port: 3000,
1119
+ };
1120
+
1121
+ originalServe = Bun.serve;
1122
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1123
+ (Bun as any).serve = mock(() => mockServer);
1124
+ });
1125
+
1126
+ afterEach(() => {
1127
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
1128
+ (Bun as any).serve = originalServe;
1129
+ });
1130
+
1131
+ test('should stop server and cleanup resources on stop()', async () => {
1132
+ @Module({})
1133
+ class TestModule {}
1134
+
1135
+ const app = createTestApp(TestModule);
1136
+ await app.start();
1137
+
1138
+ await app.stop();
1139
+
1140
+ expect(mockServer.stop).toHaveBeenCalled();
1141
+ });
1142
+
1143
+ test('should accept closeSharedRedis option in stop()', async () => {
1144
+ @Module({})
1145
+ class TestModule {}
1146
+
1147
+ const app = createTestApp(TestModule);
1148
+ await app.start();
1149
+
1150
+ // Stop without closing Redis
1151
+ await app.stop({ closeSharedRedis: false });
1152
+
1153
+ expect(mockServer.stop).toHaveBeenCalled();
1154
+ });
1155
+
1156
+ test('should enable graceful shutdown by default', async () => {
1157
+ @Module({})
1158
+ class TestModule {}
1159
+
1160
+ // Track process.on calls
1161
+ const processOnMock = mock();
1162
+ const originalProcessOn = process.on.bind(process);
1163
+ process.on = processOnMock as typeof process.on;
1164
+
1165
+ // No gracefulShutdown option - should be enabled by default
1166
+ const app = createTestApp(TestModule);
1167
+ await app.start();
1168
+
1169
+ // Verify handlers were registered
1170
+ expect(processOnMock).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
1171
+ expect(processOnMock).toHaveBeenCalledWith('SIGINT', expect.any(Function));
1172
+
1173
+ // Cleanup
1174
+ process.on = originalProcessOn;
1175
+ await app.stop();
1176
+ });
1177
+
1178
+ test('should disable graceful shutdown when option is false', async () => {
1179
+ @Module({})
1180
+ class TestModule {}
1181
+
1182
+ // Track process.on calls
1183
+ const processOnMock = mock();
1184
+ const originalProcessOn = process.on.bind(process);
1185
+ process.on = processOnMock as typeof process.on;
1186
+
1187
+ // Explicitly disable graceful shutdown
1188
+ const app = createTestApp(TestModule, { gracefulShutdown: false });
1189
+ await app.start();
1190
+
1191
+ // Verify handlers were NOT registered
1192
+ expect(processOnMock).not.toHaveBeenCalledWith('SIGTERM', expect.any(Function));
1193
+ expect(processOnMock).not.toHaveBeenCalledWith('SIGINT', expect.any(Function));
1194
+
1195
+ // Cleanup
1196
+ process.on = originalProcessOn;
1197
+ await app.stop();
1198
+ });
1199
+
1200
+ test('should provide enableGracefulShutdown method for manual setup', async () => {
1201
+ @Module({})
1202
+ class TestModule {}
1203
+
1204
+ // Disable automatic graceful shutdown
1205
+ const app = createTestApp(TestModule, { gracefulShutdown: false });
1206
+ await app.start();
1207
+
1208
+ // Track process.on calls
1209
+ const processOnMock = mock();
1210
+ const originalProcessOn = process.on.bind(process);
1211
+ process.on = processOnMock as typeof process.on;
1212
+
1213
+ // Manually enable graceful shutdown
1214
+ app.enableGracefulShutdown();
1215
+
1216
+ // Verify handlers were registered
1217
+ expect(processOnMock).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
1218
+ expect(processOnMock).toHaveBeenCalledWith('SIGINT', expect.any(Function));
1219
+
1220
+ // Cleanup
1221
+ process.on = originalProcessOn;
1222
+ await app.stop();
1223
+ });
1224
+ });
1105
1225
  });