@onebun/core 0.1.1 → 0.1.2
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.
- package/README.md +233 -0
- package/package.json +1 -1
- package/src/application.test.ts +119 -0
- package/src/application.ts +112 -5
- package/src/docs-examples.test.ts +753 -0
- package/src/index.ts +96 -0
- package/src/module.ts +10 -4
- package/src/redis-client.ts +502 -0
- package/src/shared-redis.ts +231 -0
- package/src/types.ts +50 -0
- package/src/ws-base-gateway.test.ts +479 -0
- package/src/ws-base-gateway.ts +514 -0
- package/src/ws-client.test.ts +511 -0
- package/src/ws-client.ts +628 -0
- package/src/ws-client.types.ts +129 -0
- package/src/ws-decorators.test.ts +331 -0
- package/src/ws-decorators.ts +417 -0
- package/src/ws-guards.test.ts +334 -0
- package/src/ws-guards.ts +298 -0
- package/src/ws-handler.ts +658 -0
- package/src/ws-integration.test.ts +517 -0
- package/src/ws-pattern-matcher.test.ts +152 -0
- package/src/ws-pattern-matcher.ts +240 -0
- package/src/ws-service-definition.ts +223 -0
- package/src/ws-socketio-protocol.test.ts +344 -0
- package/src/ws-socketio-protocol.ts +567 -0
- package/src/ws-storage-memory.test.ts +246 -0
- package/src/ws-storage-memory.ts +222 -0
- package/src/ws-storage-redis.ts +302 -0
- package/src/ws-storage.ts +210 -0
- package/src/ws.types.ts +342 -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
package/src/application.test.ts
CHANGED
|
@@ -1102,4 +1102,123 @@ describe('OneBunApplication', () => {
|
|
|
1102
1102
|
expect(mockTraceService.extractFromHeaders).not.toHaveBeenCalled();
|
|
1103
1103
|
});
|
|
1104
1104
|
});
|
|
1105
|
+
|
|
1106
|
+
describe('Graceful shutdown', () => {
|
|
1107
|
+
let originalServe: typeof Bun.serve;
|
|
1108
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1109
|
+
let mockServer: any;
|
|
1110
|
+
|
|
1111
|
+
beforeEach(() => {
|
|
1112
|
+
register.clear();
|
|
1113
|
+
|
|
1114
|
+
mockServer = {
|
|
1115
|
+
stop: mock(),
|
|
1116
|
+
hostname: 'localhost',
|
|
1117
|
+
port: 3000,
|
|
1118
|
+
};
|
|
1119
|
+
|
|
1120
|
+
originalServe = Bun.serve;
|
|
1121
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1122
|
+
(Bun as any).serve = mock(() => mockServer);
|
|
1123
|
+
});
|
|
1124
|
+
|
|
1125
|
+
afterEach(() => {
|
|
1126
|
+
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
1127
|
+
(Bun as any).serve = originalServe;
|
|
1128
|
+
});
|
|
1129
|
+
|
|
1130
|
+
test('should stop server and cleanup resources on stop()', async () => {
|
|
1131
|
+
@Module({})
|
|
1132
|
+
class TestModule {}
|
|
1133
|
+
|
|
1134
|
+
const app = createTestApp(TestModule);
|
|
1135
|
+
await app.start();
|
|
1136
|
+
|
|
1137
|
+
await app.stop();
|
|
1138
|
+
|
|
1139
|
+
expect(mockServer.stop).toHaveBeenCalled();
|
|
1140
|
+
});
|
|
1141
|
+
|
|
1142
|
+
test('should accept closeSharedRedis option in stop()', async () => {
|
|
1143
|
+
@Module({})
|
|
1144
|
+
class TestModule {}
|
|
1145
|
+
|
|
1146
|
+
const app = createTestApp(TestModule);
|
|
1147
|
+
await app.start();
|
|
1148
|
+
|
|
1149
|
+
// Stop without closing Redis
|
|
1150
|
+
await app.stop({ closeSharedRedis: false });
|
|
1151
|
+
|
|
1152
|
+
expect(mockServer.stop).toHaveBeenCalled();
|
|
1153
|
+
});
|
|
1154
|
+
|
|
1155
|
+
test('should enable graceful shutdown by default', async () => {
|
|
1156
|
+
@Module({})
|
|
1157
|
+
class TestModule {}
|
|
1158
|
+
|
|
1159
|
+
// Track process.on calls
|
|
1160
|
+
const processOnMock = mock();
|
|
1161
|
+
const originalProcessOn = process.on.bind(process);
|
|
1162
|
+
process.on = processOnMock as typeof process.on;
|
|
1163
|
+
|
|
1164
|
+
// No gracefulShutdown option - should be enabled by default
|
|
1165
|
+
const app = createTestApp(TestModule);
|
|
1166
|
+
await app.start();
|
|
1167
|
+
|
|
1168
|
+
// Verify handlers were registered
|
|
1169
|
+
expect(processOnMock).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
|
1170
|
+
expect(processOnMock).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
1171
|
+
|
|
1172
|
+
// Cleanup
|
|
1173
|
+
process.on = originalProcessOn;
|
|
1174
|
+
await app.stop();
|
|
1175
|
+
});
|
|
1176
|
+
|
|
1177
|
+
test('should disable graceful shutdown when option is false', async () => {
|
|
1178
|
+
@Module({})
|
|
1179
|
+
class TestModule {}
|
|
1180
|
+
|
|
1181
|
+
// Track process.on calls
|
|
1182
|
+
const processOnMock = mock();
|
|
1183
|
+
const originalProcessOn = process.on.bind(process);
|
|
1184
|
+
process.on = processOnMock as typeof process.on;
|
|
1185
|
+
|
|
1186
|
+
// Explicitly disable graceful shutdown
|
|
1187
|
+
const app = createTestApp(TestModule, { gracefulShutdown: false });
|
|
1188
|
+
await app.start();
|
|
1189
|
+
|
|
1190
|
+
// Verify handlers were NOT registered
|
|
1191
|
+
expect(processOnMock).not.toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
|
1192
|
+
expect(processOnMock).not.toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
1193
|
+
|
|
1194
|
+
// Cleanup
|
|
1195
|
+
process.on = originalProcessOn;
|
|
1196
|
+
await app.stop();
|
|
1197
|
+
});
|
|
1198
|
+
|
|
1199
|
+
test('should provide enableGracefulShutdown method for manual setup', async () => {
|
|
1200
|
+
@Module({})
|
|
1201
|
+
class TestModule {}
|
|
1202
|
+
|
|
1203
|
+
// Disable automatic graceful shutdown
|
|
1204
|
+
const app = createTestApp(TestModule, { gracefulShutdown: false });
|
|
1205
|
+
await app.start();
|
|
1206
|
+
|
|
1207
|
+
// Track process.on calls
|
|
1208
|
+
const processOnMock = mock();
|
|
1209
|
+
const originalProcessOn = process.on.bind(process);
|
|
1210
|
+
process.on = processOnMock as typeof process.on;
|
|
1211
|
+
|
|
1212
|
+
// Manually enable graceful shutdown
|
|
1213
|
+
app.enableGracefulShutdown();
|
|
1214
|
+
|
|
1215
|
+
// Verify handlers were registered
|
|
1216
|
+
expect(processOnMock).toHaveBeenCalledWith('SIGTERM', expect.any(Function));
|
|
1217
|
+
expect(processOnMock).toHaveBeenCalledWith('SIGINT', expect.any(Function));
|
|
1218
|
+
|
|
1219
|
+
// Cleanup
|
|
1220
|
+
process.on = originalProcessOn;
|
|
1221
|
+
await app.stop();
|
|
1222
|
+
});
|
|
1223
|
+
});
|
|
1105
1224
|
});
|
package/src/application.ts
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
import { Effect, type Layer } from 'effect';
|
|
2
2
|
|
|
3
3
|
import type { Controller } from './controller';
|
|
4
|
+
import type { WsClientData } from './ws.types';
|
|
4
5
|
|
|
5
6
|
import { TypedEnv } from '@onebun/envs';
|
|
6
7
|
import {
|
|
@@ -22,6 +23,7 @@ import { makeTraceService, TraceService } from '@onebun/trace';
|
|
|
22
23
|
import { ConfigServiceImpl } from './config.service';
|
|
23
24
|
import { getControllerMetadata } from './decorators';
|
|
24
25
|
import { OneBunModule } from './module';
|
|
26
|
+
import { SharedRedisProvider } from './shared-redis';
|
|
25
27
|
import {
|
|
26
28
|
type ApplicationOptions,
|
|
27
29
|
type HttpMethod,
|
|
@@ -31,6 +33,7 @@ import {
|
|
|
31
33
|
type RouteMetadata,
|
|
32
34
|
} from './types';
|
|
33
35
|
import { validateOrThrow } from './validation';
|
|
36
|
+
import { WsHandler, isWebSocketGateway } from './ws-handler';
|
|
34
37
|
|
|
35
38
|
// Conditionally import metrics
|
|
36
39
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
@@ -75,6 +78,7 @@ export class OneBunApplication {
|
|
|
75
78
|
private metricsService: any = null;
|
|
76
79
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
77
80
|
private traceService: any = null;
|
|
81
|
+
private wsHandler: WsHandler | null = null;
|
|
78
82
|
|
|
79
83
|
/**
|
|
80
84
|
* Create application instance
|
|
@@ -260,6 +264,20 @@ export class OneBunApplication {
|
|
|
260
264
|
const controllers = this.rootModule.getControllers();
|
|
261
265
|
this.logger.debug(`Loaded ${controllers.length} controllers`);
|
|
262
266
|
|
|
267
|
+
// Initialize WebSocket handler and detect gateways
|
|
268
|
+
this.wsHandler = new WsHandler(this.logger, this.options.websocket);
|
|
269
|
+
|
|
270
|
+
// Register WebSocket gateways (they are in controllers array but decorated with @WebSocketGateway)
|
|
271
|
+
for (const controllerClass of controllers) {
|
|
272
|
+
if (isWebSocketGateway(controllerClass)) {
|
|
273
|
+
const instance = this.rootModule.getControllerInstance?.(controllerClass);
|
|
274
|
+
if (instance) {
|
|
275
|
+
this.wsHandler.registerGateway(controllerClass, instance as import('./ws-base-gateway').BaseWebSocketGateway);
|
|
276
|
+
this.logger.info(`Registered WebSocket gateway: ${controllerClass.name}`);
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
|
|
263
281
|
// Create a map of routes with metadata
|
|
264
282
|
const routes = new Map<
|
|
265
283
|
string,
|
|
@@ -359,15 +377,48 @@ export class OneBunApplication {
|
|
|
359
377
|
|
|
360
378
|
// Create server with proper context binding
|
|
361
379
|
const app = this;
|
|
362
|
-
|
|
380
|
+
const hasWebSocketGateways = this.wsHandler?.hasGateways() ?? false;
|
|
381
|
+
|
|
382
|
+
// Prepare WebSocket handlers if gateways exist
|
|
383
|
+
// When no gateways, use no-op handlers (required by Bun.serve)
|
|
384
|
+
const wsHandlers = hasWebSocketGateways ? this.wsHandler!.createWebSocketHandlers() : {
|
|
385
|
+
|
|
386
|
+
open() { /* no-op */ },
|
|
387
|
+
|
|
388
|
+
message() { /* no-op */ },
|
|
389
|
+
|
|
390
|
+
close() { /* no-op */ },
|
|
391
|
+
|
|
392
|
+
drain() { /* no-op */ },
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
this.server = Bun.serve<WsClientData>({
|
|
363
396
|
port: this.options.port,
|
|
364
397
|
hostname: this.options.host,
|
|
365
|
-
|
|
398
|
+
// WebSocket handlers
|
|
399
|
+
websocket: wsHandlers,
|
|
400
|
+
async fetch(req, server) {
|
|
366
401
|
const url = new URL(req.url);
|
|
367
402
|
const path = url.pathname;
|
|
368
403
|
const method = req.method;
|
|
369
404
|
const startTime = Date.now();
|
|
370
405
|
|
|
406
|
+
// Handle WebSocket upgrade if gateways exist
|
|
407
|
+
if (hasWebSocketGateways && app.wsHandler) {
|
|
408
|
+
const upgradeHeader = req.headers.get('upgrade')?.toLowerCase();
|
|
409
|
+
|
|
410
|
+
// Check for WebSocket upgrade or Socket.IO polling
|
|
411
|
+
if (upgradeHeader === 'websocket' || path.startsWith('/socket.io')) {
|
|
412
|
+
const response = await app.wsHandler.handleUpgrade(req, server);
|
|
413
|
+
if (response === undefined) {
|
|
414
|
+
return undefined; // Successfully upgraded
|
|
415
|
+
}
|
|
416
|
+
|
|
417
|
+
// Return response if upgrade failed
|
|
418
|
+
return response;
|
|
419
|
+
}
|
|
420
|
+
}
|
|
421
|
+
|
|
371
422
|
// Setup tracing context if available and enabled
|
|
372
423
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
|
373
424
|
let traceSpan: any = null;
|
|
@@ -675,6 +726,12 @@ export class OneBunApplication {
|
|
|
675
726
|
},
|
|
676
727
|
});
|
|
677
728
|
|
|
729
|
+
// Initialize WebSocket gateways with server
|
|
730
|
+
if (hasWebSocketGateways && this.wsHandler && this.server) {
|
|
731
|
+
this.wsHandler.initializeGateways(this.server);
|
|
732
|
+
this.logger.info(`WebSocket server enabled at ws://${this.options.host}:${this.options.port}`);
|
|
733
|
+
}
|
|
734
|
+
|
|
678
735
|
this.logger.info(`Server started on http://${this.options.host}:${this.options.port}`);
|
|
679
736
|
if (this.metricsService) {
|
|
680
737
|
this.logger.info(
|
|
@@ -685,6 +742,11 @@ export class OneBunApplication {
|
|
|
685
742
|
'Metrics enabled but @onebun/metrics module not available. Install with: bun add @onebun/metrics',
|
|
686
743
|
);
|
|
687
744
|
}
|
|
745
|
+
|
|
746
|
+
// Enable graceful shutdown by default (can be disabled with gracefulShutdown: false)
|
|
747
|
+
if (this.options.gracefulShutdown !== false) {
|
|
748
|
+
this.enableGracefulShutdown();
|
|
749
|
+
}
|
|
688
750
|
} catch (error) {
|
|
689
751
|
this.logger.error(
|
|
690
752
|
'Failed to start application:',
|
|
@@ -910,14 +972,59 @@ export class OneBunApplication {
|
|
|
910
972
|
}
|
|
911
973
|
|
|
912
974
|
/**
|
|
913
|
-
* Stop the application
|
|
975
|
+
* Stop the application with graceful shutdown
|
|
976
|
+
* @param options - Shutdown options
|
|
914
977
|
*/
|
|
915
|
-
stop(): void {
|
|
978
|
+
async stop(options?: { closeSharedRedis?: boolean }): Promise<void> {
|
|
979
|
+
const closeRedis = options?.closeSharedRedis ?? true;
|
|
980
|
+
|
|
981
|
+
this.logger.info('Stopping OneBun application...');
|
|
982
|
+
|
|
983
|
+
// Cleanup WebSocket resources
|
|
984
|
+
if (this.wsHandler) {
|
|
985
|
+
this.logger.debug('Cleaning up WebSocket handler');
|
|
986
|
+
await this.wsHandler.cleanup();
|
|
987
|
+
this.wsHandler = null;
|
|
988
|
+
}
|
|
989
|
+
|
|
990
|
+
// Stop HTTP server
|
|
916
991
|
if (this.server) {
|
|
917
992
|
this.server.stop();
|
|
918
993
|
this.server = null;
|
|
919
|
-
this.logger.
|
|
994
|
+
this.logger.debug('HTTP server stopped');
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
// Close shared Redis connection if configured and requested
|
|
998
|
+
if (closeRedis && SharedRedisProvider.isConnected()) {
|
|
999
|
+
this.logger.debug('Disconnecting shared Redis');
|
|
1000
|
+
await SharedRedisProvider.disconnect();
|
|
920
1001
|
}
|
|
1002
|
+
|
|
1003
|
+
this.logger.info('OneBun application stopped');
|
|
1004
|
+
}
|
|
1005
|
+
|
|
1006
|
+
/**
|
|
1007
|
+
* Register signal handlers for graceful shutdown
|
|
1008
|
+
* Call this after start() to enable automatic shutdown on SIGTERM/SIGINT
|
|
1009
|
+
*
|
|
1010
|
+
* @example
|
|
1011
|
+
* ```typescript
|
|
1012
|
+
* const app = new OneBunApplication(AppModule, options);
|
|
1013
|
+
* await app.start();
|
|
1014
|
+
* app.enableGracefulShutdown();
|
|
1015
|
+
* ```
|
|
1016
|
+
*/
|
|
1017
|
+
enableGracefulShutdown(): void {
|
|
1018
|
+
const shutdown = async (signal: string) => {
|
|
1019
|
+
this.logger.info(`Received ${signal}, initiating graceful shutdown...`);
|
|
1020
|
+
await this.stop();
|
|
1021
|
+
process.exit(0);
|
|
1022
|
+
};
|
|
1023
|
+
|
|
1024
|
+
process.on('SIGTERM', () => shutdown('SIGTERM'));
|
|
1025
|
+
process.on('SIGINT', () => shutdown('SIGINT'));
|
|
1026
|
+
|
|
1027
|
+
this.logger.debug('Graceful shutdown handlers registered');
|
|
921
1028
|
}
|
|
922
1029
|
|
|
923
1030
|
/**
|