@krisanalfa/bunest-adapter 0.1.0 → 0.3.0
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 +435 -11
- package/dist/bun.adapter.d.ts +24 -4
- package/dist/bun.preflight-http-server.d.ts +62 -0
- package/dist/bun.ws-adapter.d.ts +48 -0
- package/dist/index.d.ts +7 -4
- package/dist/index.js +362 -53
- package/dist/index.js.map +7 -5
- package/dist/internal.types.d.ts +8 -0
- package/package.json +3 -2
package/README.md
CHANGED
|
@@ -2,6 +2,47 @@
|
|
|
2
2
|
|
|
3
3
|
This project provides a native Bun adapter for NestJS, allowing developers to leverage the performance benefits of the Bun runtime while using the powerful features of the NestJS framework.
|
|
4
4
|
|
|
5
|
+
## Table of Contents
|
|
6
|
+
|
|
7
|
+
- [Features](#features)
|
|
8
|
+
- [Native Bun adapter for NestJS](#native-bun-adapter-for-nestjs)
|
|
9
|
+
- [Full NestJS Feature Support](#full-nestjs-feature-support)
|
|
10
|
+
- [Controllers & HTTP Methods](#controllers--http-methods)
|
|
11
|
+
- [Middleware](#middleware)
|
|
12
|
+
- [Guards](#guards)
|
|
13
|
+
- [Interceptors](#interceptors)
|
|
14
|
+
- [Exception Filters](#exception-filters)
|
|
15
|
+
- [Validation](#validation)
|
|
16
|
+
- [File Uploads](#file-uploads)
|
|
17
|
+
- [Streaming Responses](#streaming-responses)
|
|
18
|
+
- [Versioning](#versioning)
|
|
19
|
+
- [CORS](#cors)
|
|
20
|
+
- [Cookies](#cookies)
|
|
21
|
+
- [Popular Express Middleware](#popular-express-middleware)
|
|
22
|
+
- [Bun File API Support](#bun-file-api-support)
|
|
23
|
+
- [BunFileInterceptor](#bunfileinterceptor)
|
|
24
|
+
- [WebSocket Support](#websocket-support)
|
|
25
|
+
- [Basic WebSocket Gateway](#basic-websocket-gateway)
|
|
26
|
+
- [WebSocket with Guards](#websocket-with-guards)
|
|
27
|
+
- [WebSocket with Pipes](#websocket-with-pipes)
|
|
28
|
+
- [WebSocket with Exception Filters](#websocket-with-exception-filters)
|
|
29
|
+
- [Broadcasting Messages](#broadcasting-messages)
|
|
30
|
+
- [Secure WebSocket (WSS)](#secure-websocket-wss)
|
|
31
|
+
- [Limitations](#limitations)
|
|
32
|
+
- [HTTPS](#https)
|
|
33
|
+
- [Code Quality](#code-quality)
|
|
34
|
+
- [Request / Response Objects](#request--response-objects)
|
|
35
|
+
- [BunRequest](#bunrequest)
|
|
36
|
+
- [BunResponse](#bunresponse)
|
|
37
|
+
- [Benchmark Results](#benchmark-results)
|
|
38
|
+
- [HTTP Benchmark](#http-benchmark)
|
|
39
|
+
- [WebSocket Benchmark](#websocket-benchmark)
|
|
40
|
+
- [Running HTTP Benchmark](#running-http-benchmark)
|
|
41
|
+
- [Running WebSocket Benchmark](#running-websocket-benchmark)
|
|
42
|
+
- [Contributing](#contributing)
|
|
43
|
+
- [Future Plans](#future-plans)
|
|
44
|
+
- [License](#license)
|
|
45
|
+
|
|
5
46
|
## Features
|
|
6
47
|
|
|
7
48
|
### Native Bun adapter for NestJS
|
|
@@ -442,6 +483,343 @@ Tested and working with:
|
|
|
442
483
|
- `cors` - CORS handling
|
|
443
484
|
- And most other Express-compatible middleware
|
|
444
485
|
|
|
486
|
+
### WebSocket Support
|
|
487
|
+
|
|
488
|
+
The Bun adapter provides full WebSocket support using Bun's native WebSocket implementation. The `BunWsAdapter` enables real-time, bidirectional communication between clients and servers with excellent performance.
|
|
489
|
+
|
|
490
|
+
#### Basic WebSocket Gateway
|
|
491
|
+
|
|
492
|
+
Create WebSocket gateways using NestJS decorators:
|
|
493
|
+
|
|
494
|
+
```ts
|
|
495
|
+
import { BunWsAdapter, BunAdapter } from "@krisanalfa/bunest-adapter";
|
|
496
|
+
import {
|
|
497
|
+
WebSocketGateway,
|
|
498
|
+
SubscribeMessage,
|
|
499
|
+
MessageBody,
|
|
500
|
+
OnGatewayConnection,
|
|
501
|
+
OnGatewayDisconnect,
|
|
502
|
+
ConnectedSocket,
|
|
503
|
+
} from "@nestjs/websockets";
|
|
504
|
+
import { ServerWebSocket } from "bun";
|
|
505
|
+
|
|
506
|
+
@WebSocketGateway({ cors: true })
|
|
507
|
+
class ChatGateway implements OnGatewayConnection, OnGatewayDisconnect {
|
|
508
|
+
handleConnection(client: ServerWebSocket) {
|
|
509
|
+
client.send(JSON.stringify({ event: "welcome", data: "Welcome!" }));
|
|
510
|
+
}
|
|
511
|
+
|
|
512
|
+
handleDisconnect(client: ServerWebSocket) {
|
|
513
|
+
console.log("Client disconnected");
|
|
514
|
+
}
|
|
515
|
+
|
|
516
|
+
@SubscribeMessage("message")
|
|
517
|
+
handleMessage(@MessageBody() data: string) {
|
|
518
|
+
return {
|
|
519
|
+
event: "message",
|
|
520
|
+
data: `Received: ${data}`,
|
|
521
|
+
};
|
|
522
|
+
}
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
// Enable WebSocket support in your application
|
|
526
|
+
const app = await NestFactory.create(AppModule, new BunAdapter());
|
|
527
|
+
app.useWebSocketAdapter(new BunWsAdapter(app));
|
|
528
|
+
await app.listen(3000);
|
|
529
|
+
```
|
|
530
|
+
|
|
531
|
+
Connect from the client:
|
|
532
|
+
|
|
533
|
+
```ts
|
|
534
|
+
const socket = new WebSocket("ws://localhost:3000");
|
|
535
|
+
|
|
536
|
+
socket.onopen = () => {
|
|
537
|
+
socket.send(JSON.stringify({ event: "message", data: "Hello!" }));
|
|
538
|
+
};
|
|
539
|
+
|
|
540
|
+
socket.onmessage = (event) => {
|
|
541
|
+
const data = JSON.parse(event.data);
|
|
542
|
+
console.log(data); // { event: 'message', data: 'Received: Hello!' }
|
|
543
|
+
};
|
|
544
|
+
```
|
|
545
|
+
|
|
546
|
+
#### WebSocket with Guards
|
|
547
|
+
|
|
548
|
+
Protect WebSocket endpoints with guards:
|
|
549
|
+
|
|
550
|
+
```ts
|
|
551
|
+
import { CanActivate, ExecutionContext, Injectable } from "@nestjs/common";
|
|
552
|
+
import { WsException } from "@nestjs/websockets";
|
|
553
|
+
|
|
554
|
+
@Injectable()
|
|
555
|
+
class WsAuthGuard implements CanActivate {
|
|
556
|
+
canActivate(context: ExecutionContext): boolean {
|
|
557
|
+
const data = context.switchToWs().getData<{ token?: string }>();
|
|
558
|
+
|
|
559
|
+
if (data.token !== "valid-token") {
|
|
560
|
+
throw new WsException("Unauthorized");
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
return true;
|
|
564
|
+
}
|
|
565
|
+
}
|
|
566
|
+
|
|
567
|
+
@WebSocketGateway()
|
|
568
|
+
@UseGuards(WsAuthGuard)
|
|
569
|
+
class ProtectedGateway {
|
|
570
|
+
@SubscribeMessage("protected")
|
|
571
|
+
handleProtected(@MessageBody() data: { message: string }) {
|
|
572
|
+
return {
|
|
573
|
+
event: "protected",
|
|
574
|
+
data: `Protected message: ${data.message}`,
|
|
575
|
+
};
|
|
576
|
+
}
|
|
577
|
+
}
|
|
578
|
+
```
|
|
579
|
+
|
|
580
|
+
#### WebSocket with Pipes
|
|
581
|
+
|
|
582
|
+
Validate WebSocket messages using pipes:
|
|
583
|
+
|
|
584
|
+
```ts
|
|
585
|
+
import { UsePipes, ValidationPipe } from "@nestjs/common";
|
|
586
|
+
import { IsString, IsNotEmpty, MinLength } from "class-validator";
|
|
587
|
+
|
|
588
|
+
class CreateMessageDto {
|
|
589
|
+
@IsString()
|
|
590
|
+
@IsNotEmpty()
|
|
591
|
+
@MinLength(3)
|
|
592
|
+
text!: string;
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
@WebSocketGateway()
|
|
596
|
+
@UsePipes(
|
|
597
|
+
new ValidationPipe({
|
|
598
|
+
exceptionFactory: (errors) => new WsException(errors),
|
|
599
|
+
}),
|
|
600
|
+
)
|
|
601
|
+
class ValidationGateway {
|
|
602
|
+
@SubscribeMessage("createMessage")
|
|
603
|
+
handleCreateMessage(@MessageBody() dto: CreateMessageDto) {
|
|
604
|
+
return {
|
|
605
|
+
event: "messageCreated",
|
|
606
|
+
data: dto.text,
|
|
607
|
+
};
|
|
608
|
+
}
|
|
609
|
+
}
|
|
610
|
+
```
|
|
611
|
+
|
|
612
|
+
#### WebSocket with Exception Filters
|
|
613
|
+
|
|
614
|
+
Handle WebSocket exceptions with custom filters:
|
|
615
|
+
|
|
616
|
+
```ts
|
|
617
|
+
import { Catch, ArgumentsHost, WsExceptionFilter } from "@nestjs/common";
|
|
618
|
+
import { WsException } from "@nestjs/websockets";
|
|
619
|
+
import { ServerWebSocket } from "bun";
|
|
620
|
+
|
|
621
|
+
@Catch(WsException)
|
|
622
|
+
class WsExceptionsFilter implements WsExceptionFilter {
|
|
623
|
+
catch(exception: WsException, host: ArgumentsHost) {
|
|
624
|
+
const client = host.switchToWs().getClient<ServerWebSocket>();
|
|
625
|
+
const error = exception.getError();
|
|
626
|
+
const details = typeof error === "object" ? error : { message: error };
|
|
627
|
+
|
|
628
|
+
client.send(
|
|
629
|
+
JSON.stringify({
|
|
630
|
+
event: "error",
|
|
631
|
+
data: {
|
|
632
|
+
message: "An error occurred",
|
|
633
|
+
details,
|
|
634
|
+
},
|
|
635
|
+
}),
|
|
636
|
+
);
|
|
637
|
+
}
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
@WebSocketGateway()
|
|
641
|
+
@UseFilters(WsExceptionsFilter)
|
|
642
|
+
class ErrorHandlingGateway {
|
|
643
|
+
@SubscribeMessage("risky")
|
|
644
|
+
handleRisky(@MessageBody() data: { shouldFail: boolean }) {
|
|
645
|
+
if (data.shouldFail) {
|
|
646
|
+
throw new WsException("Something went wrong");
|
|
647
|
+
}
|
|
648
|
+
return { event: "success", data: "OK" };
|
|
649
|
+
}
|
|
650
|
+
}
|
|
651
|
+
```
|
|
652
|
+
|
|
653
|
+
#### Broadcasting Messages
|
|
654
|
+
|
|
655
|
+
Broadcast messages to all connected clients using Bun's publish/subscribe system:
|
|
656
|
+
|
|
657
|
+
```ts
|
|
658
|
+
import {
|
|
659
|
+
WebSocketGateway,
|
|
660
|
+
WebSocketServer,
|
|
661
|
+
SubscribeMessage,
|
|
662
|
+
MessageBody,
|
|
663
|
+
ConnectedSocket,
|
|
664
|
+
OnGatewayConnection,
|
|
665
|
+
} from "@nestjs/websockets";
|
|
666
|
+
import { ServerWebSocket } from "bun";
|
|
667
|
+
import { BunPreflightHttpServer } from "@krisanalfa/bunest-adapter";
|
|
668
|
+
|
|
669
|
+
@Injectable() // Mandatory to be able to inject `BunPreflightHttpServer`
|
|
670
|
+
@WebSocketGateway()
|
|
671
|
+
class BroadcastGateway implements OnGatewayConnection {
|
|
672
|
+
@WebSocketServer()
|
|
673
|
+
server!: BunPreflightHttpServer; // Inject BunPreflightHttpServer
|
|
674
|
+
|
|
675
|
+
private readonly roomName = "global-room";
|
|
676
|
+
|
|
677
|
+
handleConnection(client: ServerWebSocket) {
|
|
678
|
+
// Subscribe client to room
|
|
679
|
+
client.subscribe(this.roomName);
|
|
680
|
+
}
|
|
681
|
+
|
|
682
|
+
@SubscribeMessage("broadcast")
|
|
683
|
+
handleBroadcast(
|
|
684
|
+
@MessageBody() message: string,
|
|
685
|
+
@ConnectedSocket() socket: ServerWebSocket,
|
|
686
|
+
) {
|
|
687
|
+
// Get subscriber count
|
|
688
|
+
const count = this.server.getBunServer().subscriberCount(this.roomName);
|
|
689
|
+
|
|
690
|
+
// Publish to all subscribers in the room
|
|
691
|
+
socket.publishText(
|
|
692
|
+
this.roomName,
|
|
693
|
+
JSON.stringify({
|
|
694
|
+
event: "broadcast",
|
|
695
|
+
data: message,
|
|
696
|
+
subscribers: count,
|
|
697
|
+
}),
|
|
698
|
+
);
|
|
699
|
+
}
|
|
700
|
+
}
|
|
701
|
+
```
|
|
702
|
+
|
|
703
|
+
**Key Features:**
|
|
704
|
+
|
|
705
|
+
- **Native Performance** - Uses Bun's native WebSocket implementation for maximum speed
|
|
706
|
+
- **NestJS Integration** - Full support for decorators, guards, pipes, and exception filters
|
|
707
|
+
- **Pub/Sub Support** - Built-in support for broadcasting messages to multiple clients
|
|
708
|
+
- **CORS Configuration** - Easy CORS setup for WebSocket connections
|
|
709
|
+
- **HTTP + WebSocket** - Run both HTTP and WebSocket servers on the same port
|
|
710
|
+
|
|
711
|
+
#### Secure WebSocket (WSS)
|
|
712
|
+
|
|
713
|
+
The Bun adapter supports secure WebSocket connections (WSS) using TLS/SSL certificates. You can configure WSS in two ways:
|
|
714
|
+
|
|
715
|
+
**Using BunAdapter constructor options:**
|
|
716
|
+
|
|
717
|
+
```ts
|
|
718
|
+
import { BunAdapter, BunWsAdapter } from "@krisanalfa/bunest-adapter";
|
|
719
|
+
import { NestFactory } from "@nestjs/core";
|
|
720
|
+
|
|
721
|
+
const app = await NestFactory.create(
|
|
722
|
+
AppModule,
|
|
723
|
+
new BunAdapter({
|
|
724
|
+
tls: {
|
|
725
|
+
cert: Bun.file("/path/to/cert.pem"),
|
|
726
|
+
key: Bun.file("/path/to/key.pem"),
|
|
727
|
+
},
|
|
728
|
+
}),
|
|
729
|
+
);
|
|
730
|
+
|
|
731
|
+
app.useWebSocketAdapter(new BunWsAdapter(app));
|
|
732
|
+
await app.listen(3000);
|
|
733
|
+
|
|
734
|
+
// Clients connect using wss:// protocol
|
|
735
|
+
// const ws = new WebSocket('wss://localhost:3000');
|
|
736
|
+
```
|
|
737
|
+
|
|
738
|
+
**Using NestFactory.create httpsOptions:**
|
|
739
|
+
|
|
740
|
+
```ts
|
|
741
|
+
import { BunAdapter, BunWsAdapter } from "@krisanalfa/bunest-adapter";
|
|
742
|
+
import { NestFactory } from "@nestjs/core";
|
|
743
|
+
|
|
744
|
+
const app = await NestFactory.create(AppModule, new BunAdapter(), {
|
|
745
|
+
httpsOptions: {
|
|
746
|
+
cert: Bun.file("/path/to/cert.pem"),
|
|
747
|
+
key: Bun.file("/path/to/key.pem"),
|
|
748
|
+
},
|
|
749
|
+
});
|
|
750
|
+
|
|
751
|
+
app.useWebSocketAdapter(new BunWsAdapter(app));
|
|
752
|
+
await app.listen(3000);
|
|
753
|
+
```
|
|
754
|
+
|
|
755
|
+
**Unix Socket Support with WSS:**
|
|
756
|
+
|
|
757
|
+
You can also run secure WebSocket servers over Unix sockets:
|
|
758
|
+
|
|
759
|
+
```ts
|
|
760
|
+
import { BunAdapter, BunWsAdapter } from "@krisanalfa/bunest-adapter";
|
|
761
|
+
import { NestFactory } from "@nestjs/core";
|
|
762
|
+
|
|
763
|
+
const app = await NestFactory.create(
|
|
764
|
+
AppModule,
|
|
765
|
+
new BunAdapter({
|
|
766
|
+
tls: {
|
|
767
|
+
cert: Bun.file("/path/to/cert.pem"),
|
|
768
|
+
key: Bun.file("/path/to/key.pem"),
|
|
769
|
+
},
|
|
770
|
+
}),
|
|
771
|
+
);
|
|
772
|
+
|
|
773
|
+
app.useWebSocketAdapter(new BunWsAdapter(app));
|
|
774
|
+
await app.listen("/tmp/secure-nestjs.sock");
|
|
775
|
+
|
|
776
|
+
// Or use abstract namespace socket on Linux
|
|
777
|
+
await app.listen("\0secure-nestjs-socket");
|
|
778
|
+
```
|
|
779
|
+
|
|
780
|
+
**Client Connection Example:**
|
|
781
|
+
|
|
782
|
+
```ts
|
|
783
|
+
// For development with self-signed certificates
|
|
784
|
+
const ws = new WebSocket("wss://localhost:3000", {
|
|
785
|
+
tls: { rejectUnauthorized: false },
|
|
786
|
+
});
|
|
787
|
+
|
|
788
|
+
ws.onopen = () => {
|
|
789
|
+
console.log("Connected to secure WebSocket");
|
|
790
|
+
ws.send(JSON.stringify({ event: "message", data: "Hello WSS!" }));
|
|
791
|
+
};
|
|
792
|
+
|
|
793
|
+
ws.onmessage = (event) => {
|
|
794
|
+
const data = JSON.parse(event.data);
|
|
795
|
+
console.log("Received:", data);
|
|
796
|
+
};
|
|
797
|
+
```
|
|
798
|
+
|
|
799
|
+
**Important Notes:**
|
|
800
|
+
|
|
801
|
+
- WSS automatically uses the same port as your HTTPS server
|
|
802
|
+
- The `wss://` protocol is used instead of `ws://` for secure connections
|
|
803
|
+
- For production, use properly signed certificates from a trusted Certificate Authority
|
|
804
|
+
- For development, you can use self-signed certificates with `rejectUnauthorized: false` on the client
|
|
805
|
+
|
|
806
|
+
#### Limitations
|
|
807
|
+
|
|
808
|
+
**Port Configuration:** The WebSocket server port will always be the same as the Bun HTTP server port. In standard NestJS, you can configure different ports via the `@WebSocketGateway` decorator's `port` option (see [NestJS WebSocket documentation](https://docs.nestjs.com/websockets/gateways#overview)), but with the Bun adapter, WebSocket connections must use the same port as your HTTP server. This is due to Bun's unified server architecture where HTTP and WebSocket upgrades are handled by the same server instance.
|
|
809
|
+
|
|
810
|
+
```ts
|
|
811
|
+
// This port option is ignored with BunWsAdapter
|
|
812
|
+
@WebSocketGateway({ port: 8080 }) // ⚠️ Port option has no effect
|
|
813
|
+
class ChatGateway {
|
|
814
|
+
// Gateway will use the same port as app.listen()
|
|
815
|
+
}
|
|
816
|
+
|
|
817
|
+
// WebSocket will be available on the same port as HTTP
|
|
818
|
+
const app = await NestFactory.create(AppModule, new BunAdapter());
|
|
819
|
+
app.useWebSocketAdapter(new BunWsAdapter(app));
|
|
820
|
+
await app.listen(3000); // Both HTTP and WebSocket use port 3000
|
|
821
|
+
```
|
|
822
|
+
|
|
445
823
|
### Bun File API Support
|
|
446
824
|
|
|
447
825
|
This package provides first-class support for Bun's native [`BunFile`](https://bun.com/docs/runtime/file-io) API, enabling seamless file uploads and downloads using Bun's efficient file handling capabilities.
|
|
@@ -550,7 +928,7 @@ The Bun adapter is developed with high code quality standards, including:
|
|
|
550
928
|
- Strict TypeScript typings
|
|
551
929
|
- Strict linting rules (ESLint)
|
|
552
930
|
- Comprehensive unit and integration tests
|
|
553
|
-
- Coverage reports (>
|
|
931
|
+
- Coverage reports (>97% line coverage, >85% function coverage)
|
|
554
932
|
|
|
555
933
|
## Request / Response Objects
|
|
556
934
|
|
|
@@ -853,38 +1231,85 @@ class HybridController {
|
|
|
853
1231
|
|
|
854
1232
|
## Benchmark Results
|
|
855
1233
|
|
|
856
|
-
Tested on MacOS Sequoia (15.6.1), Apple M1 Max (64GB RAM), Bun 1.3.5, Node.js 20.10.0
|
|
1234
|
+
Tested on MacOS Sequoia (15.6.1), Apple M1 Max (64GB RAM), Bun 1.3.5, Node.js 20.10.0.
|
|
1235
|
+
|
|
1236
|
+
### HTTP Benchmark
|
|
1237
|
+
|
|
1238
|
+
HTTP benchmarks run using [`oha`](https://github.com/hatoo/oha) tool with the following command:
|
|
1239
|
+
|
|
1240
|
+
```
|
|
1241
|
+
oha -c 125 -n 1000000 --no-tui "http://127.0.0.1:3000/"
|
|
1242
|
+
```
|
|
857
1243
|
|
|
858
1244
|
| Configuration | Requests/sec | Compared to Pure Bun |
|
|
859
1245
|
| --------------------------------------------------------------------------------- | -----------: | -------------------: |
|
|
860
1246
|
| Pure Bun | 80,742.72 | 100.00% |
|
|
861
|
-
| Nest + Bun + Native Bun Adapter |
|
|
1247
|
+
| Nest + Bun + Native Bun Adapter | 70,234.76 | 86.98% |
|
|
862
1248
|
| Nest + Bun + Express Adapter | 43,375.97 | 53.72% |
|
|
863
1249
|
| Nest + Bun + [Hono Adapter](https://www.npmjs.com/package/@kiyasov/platform-hono) | 19,194.78 | 23.77% |
|
|
864
1250
|
| Nest + Node + Express | 14,019.88 | 17.36% |
|
|
865
1251
|
|
|
866
1252
|
> **Pure Bun** is the fastest at **80,743 req/s**. **Nest + Bun + Native Bun Adapter** achieves **~86%** of Pure Bun's performance while providing full NestJS features, and is **~5x faster** than Nest + Node + Express. Compared to Bun with Express adapter, the native Bun adapter is **~1.6x faster**.
|
|
867
1253
|
|
|
868
|
-
###
|
|
1254
|
+
### WebSocket Benchmark
|
|
1255
|
+
|
|
1256
|
+
WebSocket benchmarks run using the custom benchmark script in `benchmarks/ws.benchmark.ts`.
|
|
1257
|
+
|
|
1258
|
+
| Configuration | Messages/sec | Compared to Pure Bun |
|
|
1259
|
+
| ---------------------------------- | -----------: | -------------------: |
|
|
1260
|
+
| Pure Bun WebSocket | 817,594.60 | 100.00% |
|
|
1261
|
+
| Nest + Bun + BunWsAdapter | 764,962.70 | 93.56% |
|
|
1262
|
+
| Nest + Bun + WebSocketAdapter (ws) | 299,161.50 | 36.59% |
|
|
1263
|
+
|
|
1264
|
+
> **Pure Bun WebSocket** achieves **817,595 msg/s**. **Nest + Bun + BunWsAdapter** achieves **~94%** of Pure Bun's performance, and is **~2.6x faster** than using the standard WebSocketAdapter with `ws` library.
|
|
1265
|
+
|
|
1266
|
+
### Running HTTP Benchmark
|
|
869
1267
|
|
|
870
1268
|
This project includes benchmark configurations in the `benchmarks` directory.
|
|
871
|
-
To run the specific server benchmark, you can use predefined scripts in `package.json
|
|
1269
|
+
To run the specific HTTP server benchmark, you can use predefined scripts in `package.json`:
|
|
872
1270
|
|
|
873
1271
|
```bash
|
|
874
1272
|
# Running native bun server benchmark
|
|
875
|
-
bun run native
|
|
1273
|
+
bun run http:native
|
|
876
1274
|
|
|
877
1275
|
# Running NestJS with Bun adapter benchmark
|
|
878
|
-
bun run bun
|
|
1276
|
+
bun run http:bun
|
|
879
1277
|
|
|
880
1278
|
# Running NestJS with Hono adapter benchmark
|
|
881
|
-
bun run hono
|
|
1279
|
+
bun run http:hono
|
|
882
1280
|
|
|
883
1281
|
# Running NestJS with Express adapter benchmark
|
|
884
|
-
bun run express
|
|
1282
|
+
bun run http:express
|
|
885
1283
|
|
|
886
1284
|
# Running NestJS with Node and Express benchmark
|
|
887
|
-
bun run node
|
|
1285
|
+
bun run http:node
|
|
1286
|
+
```
|
|
1287
|
+
|
|
1288
|
+
Then run the benchmark using [`oha`](https://github.com/hatoo/oha):
|
|
1289
|
+
|
|
1290
|
+
```bash
|
|
1291
|
+
oha -c 125 -n 1000000 --no-tui "http://127.0.0.1:3000/"
|
|
1292
|
+
```
|
|
1293
|
+
|
|
1294
|
+
### Running WebSocket Benchmark
|
|
1295
|
+
|
|
1296
|
+
To run WebSocket benchmarks, first start the WebSocket server:
|
|
1297
|
+
|
|
1298
|
+
```bash
|
|
1299
|
+
# Running native bun websocket benchmark
|
|
1300
|
+
bun run ws:native
|
|
1301
|
+
|
|
1302
|
+
# Running NestJS with BunWsAdapter websocket benchmark
|
|
1303
|
+
bun run ws:bun
|
|
1304
|
+
|
|
1305
|
+
# Running NestJS with WebSocketAdapter (ws) websocket benchmark
|
|
1306
|
+
bun run ws:ws
|
|
1307
|
+
```
|
|
1308
|
+
|
|
1309
|
+
Then run the benchmark script:
|
|
1310
|
+
|
|
1311
|
+
```bash
|
|
1312
|
+
bun benchmarks/ws.benchmark.ts
|
|
888
1313
|
```
|
|
889
1314
|
|
|
890
1315
|
All benchmarks use port `3000` by default. You can adjust the port in the respective benchmark files if needed.
|
|
@@ -895,7 +1320,6 @@ Contributions are welcome! Please open issues or submit pull requests for bug fi
|
|
|
895
1320
|
|
|
896
1321
|
## Future Plans
|
|
897
1322
|
|
|
898
|
-
- Support for WebSocket integration with Bun
|
|
899
1323
|
- Enhanced trusted proxy configuration for host header handling
|
|
900
1324
|
- Additional performance optimizations and benchmarks
|
|
901
1325
|
- Release automation via CI/CD pipelines
|
package/dist/bun.adapter.d.ts
CHANGED
|
@@ -1,21 +1,30 @@
|
|
|
1
1
|
import { CorsOptions, CorsOptionsDelegate } from '@nestjs/common/interfaces/external/cors-options.interface.js';
|
|
2
2
|
import { ErrorHandler, RequestHandler } from '@nestjs/common/interfaces/index.js';
|
|
3
|
+
import { Server } from 'bun';
|
|
3
4
|
import { NestApplicationOptions, RequestMethod, VersioningOptions } from '@nestjs/common';
|
|
4
|
-
import { Serve, Server } from 'bun';
|
|
5
5
|
import { AbstractHttpAdapter } from '@nestjs/core';
|
|
6
6
|
import { VersionValue } from '@nestjs/common/interfaces/version-options.interface.js';
|
|
7
|
+
import { ServerOptions } from './internal.types.js';
|
|
8
|
+
import { BunPreflightHttpServer } from './bun.preflight-http-server.js';
|
|
7
9
|
import { BunRequest } from './bun.request.js';
|
|
8
10
|
import { BunResponse } from './bun.response.js';
|
|
9
11
|
export declare class BunAdapter extends AbstractHttpAdapter<Server<unknown>, BunRequest, BunResponse> {
|
|
10
|
-
|
|
12
|
+
protected bunServeOptions: ServerOptions;
|
|
11
13
|
private readonly logger;
|
|
12
14
|
private readonly middlewareEngine;
|
|
13
15
|
private useVersioning;
|
|
14
16
|
private readonly routes;
|
|
15
17
|
private readonly routeHandlers;
|
|
16
18
|
private notFoundHandler;
|
|
17
|
-
|
|
19
|
+
private readonly wsHandlers;
|
|
20
|
+
private readonly wsMiddlewareEngine;
|
|
21
|
+
private wsOptions;
|
|
22
|
+
private useWs;
|
|
23
|
+
private useWsCors;
|
|
24
|
+
private wsCorsHeaders?;
|
|
25
|
+
constructor(bunServeOptions?: ServerOptions);
|
|
18
26
|
use(middleware: RequestHandler<BunRequest, BunResponse>): void;
|
|
27
|
+
private createHttpMethodHandler;
|
|
19
28
|
get(handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
20
29
|
get(path: unknown, handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
21
30
|
post(handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
@@ -30,6 +39,7 @@ export declare class BunAdapter extends AbstractHttpAdapter<Server<unknown>, Bun
|
|
|
30
39
|
head(path: unknown, handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
31
40
|
options(handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
32
41
|
options(path: unknown, handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
42
|
+
private createUnsupportedMethod;
|
|
33
43
|
all(handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
34
44
|
all(path: unknown, handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
35
45
|
propfind(handler: RequestHandler<BunRequest, BunResponse>): void;
|
|
@@ -52,7 +62,7 @@ export declare class BunAdapter extends AbstractHttpAdapter<Server<unknown>, Bun
|
|
|
52
62
|
setViewEngine(engine: string): void;
|
|
53
63
|
render(response: unknown, view: string, options: unknown): void;
|
|
54
64
|
close(): Promise<void>;
|
|
55
|
-
initHttpServer(options: NestApplicationOptions):
|
|
65
|
+
initHttpServer(options: NestApplicationOptions): BunPreflightHttpServer;
|
|
56
66
|
getRequestHostname(request: BunRequest): string;
|
|
57
67
|
getRequestMethod(request: BunRequest): string;
|
|
58
68
|
getRequestUrl(request: BunRequest): string;
|
|
@@ -84,7 +94,17 @@ export declare class BunAdapter extends AbstractHttpAdapter<Server<unknown>, Bun
|
|
|
84
94
|
* @param callback Optional callback to invoke once the server is listening.
|
|
85
95
|
*/
|
|
86
96
|
listen(port: string | number, hostname: string, callback?: () => void): void;
|
|
97
|
+
private static isNumericPort;
|
|
98
|
+
private static omitKeys;
|
|
99
|
+
private isWebSocketUpgradeRequest;
|
|
100
|
+
private handleWebSocketCors;
|
|
101
|
+
private upgradeWebSocket;
|
|
102
|
+
private setupWebSocketIfNeeded;
|
|
103
|
+
private createServer;
|
|
87
104
|
private delegateRouteHandler;
|
|
105
|
+
private ensureRouteExists;
|
|
106
|
+
private prepareRequestHandler;
|
|
107
|
+
private createRouteFetchHandler;
|
|
88
108
|
private createVersioningHandlers;
|
|
89
109
|
private executeHandlerChain;
|
|
90
110
|
private createChainedHandlerForVersioningResolution;
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Server, ServerWebSocket } from 'bun';
|
|
2
|
+
import { BaseWsInstance } from '@nestjs/websockets';
|
|
3
|
+
import { ServerOptions, WsOptions } from './internal.types.js';
|
|
4
|
+
interface BunHttpAdapter {
|
|
5
|
+
setWsOptions(options: WsOptions): void;
|
|
6
|
+
getBunHttpServerInstance(): Server<unknown>;
|
|
7
|
+
getWsHandlers(): {
|
|
8
|
+
onOpen: ((ws: ServerWebSocket<unknown>) => void) | undefined;
|
|
9
|
+
onMessage: ((ws: ServerWebSocket<unknown>, message: string | ArrayBuffer | Buffer | Buffer[], server: Server<unknown>) => void) | undefined;
|
|
10
|
+
onClose: ((ws: ServerWebSocket<unknown>, code: number, reason: string) => void) | undefined;
|
|
11
|
+
};
|
|
12
|
+
getBunServerOptions(): Pick<ServerOptions, 'port' | 'hostname'>;
|
|
13
|
+
}
|
|
14
|
+
/**
|
|
15
|
+
* Bun HTTP server placeholder used before the actual server instance is created.
|
|
16
|
+
* This class provides compatibility methods expected by NestJS framework.
|
|
17
|
+
*
|
|
18
|
+
* There's limitation in Bun where we can't create the server instance without
|
|
19
|
+
* listening on a port right away. This placeholder allows us to defer
|
|
20
|
+
* the server creation until NestJS calls the listen method.
|
|
21
|
+
*/
|
|
22
|
+
export declare class BunPreflightHttpServer implements BaseWsInstance {
|
|
23
|
+
private readonly adapter;
|
|
24
|
+
constructor(adapter: BunHttpAdapter);
|
|
25
|
+
on(event: string, callback: Function): void;
|
|
26
|
+
/**
|
|
27
|
+
* NestJS compatibility methods
|
|
28
|
+
* Nest use this to listen for "error" events during HTTP server initialization
|
|
29
|
+
*/
|
|
30
|
+
once(): void;
|
|
31
|
+
/**
|
|
32
|
+
* NestJS compatibility methods
|
|
33
|
+
* Nest use this to remove "error" event listeners during HTTP server cleanup
|
|
34
|
+
*/
|
|
35
|
+
removeListener(): void;
|
|
36
|
+
/**
|
|
37
|
+
* NestJS compatibility methods
|
|
38
|
+
*/
|
|
39
|
+
stop(force?: boolean): Promise<void>;
|
|
40
|
+
address(): {
|
|
41
|
+
address: "0.0.0.0" | "127.0.0.1" | "localhost" | (string & {});
|
|
42
|
+
port: string | number;
|
|
43
|
+
} | {
|
|
44
|
+
address: string | undefined;
|
|
45
|
+
port: number | undefined;
|
|
46
|
+
};
|
|
47
|
+
setWsOptions(options: WsOptions): void;
|
|
48
|
+
registerWsOpenHandler(handler: (ws: ServerWebSocket<unknown>) => void): void;
|
|
49
|
+
registerWsMessageHandler(handler: (ws: ServerWebSocket<unknown>, message: string | ArrayBuffer | Buffer | Buffer[], server: Server<unknown>) => void): void;
|
|
50
|
+
registerWsCloseHandler(handler: (ws: ServerWebSocket<unknown>, code: number, reason: string) => void): void;
|
|
51
|
+
getWsHandlers(): {
|
|
52
|
+
onOpen: ((ws: ServerWebSocket<unknown>) => void) | undefined;
|
|
53
|
+
onMessage: ((ws: ServerWebSocket<unknown>, message: string | ArrayBuffer | Buffer | Buffer[], server: Server<unknown>) => void) | undefined;
|
|
54
|
+
onClose: ((ws: ServerWebSocket<unknown>, code: number, reason: string) => void) | undefined;
|
|
55
|
+
};
|
|
56
|
+
getBunServer(): Server<unknown>;
|
|
57
|
+
/**
|
|
58
|
+
* Proxy method for WebSocket server close
|
|
59
|
+
*/
|
|
60
|
+
close(): Promise<void>;
|
|
61
|
+
}
|
|
62
|
+
export {};
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
import { AbstractWsAdapter, BaseWsInstance, MessageMappingProperties } from '@nestjs/websockets';
|
|
2
|
+
import { Observable } from 'rxjs';
|
|
3
|
+
import { INestApplicationContext } from '@nestjs/common';
|
|
4
|
+
import { ServerWebSocket } from 'bun';
|
|
5
|
+
import { BunPreflightHttpServer } from './bun.preflight-http-server.js';
|
|
6
|
+
import { WsOptions } from './internal.types.js';
|
|
7
|
+
export type WsData = string | Buffer | ArrayBuffer | Buffer[];
|
|
8
|
+
export type WsMessageParser<TData = unknown> = (data: WsData) => WsParsedData<TData>;
|
|
9
|
+
export interface WsParsedData<TData = unknown> {
|
|
10
|
+
event: string;
|
|
11
|
+
data: TData;
|
|
12
|
+
}
|
|
13
|
+
export interface BunWsAdapterOptions extends WsOptions {
|
|
14
|
+
messageParser?: WsMessageParser;
|
|
15
|
+
}
|
|
16
|
+
/** Internal data stored on each WebSocket connection - must match bun.adapter.ts */
|
|
17
|
+
interface BunWsClientData {
|
|
18
|
+
/** Called when a message is received - matches bun.adapter.ts onMessageInternal */
|
|
19
|
+
onMessageInternal?: (message: WsData) => void;
|
|
20
|
+
/** Called when the connection closes - matches bun.adapter.ts onCloseInternal */
|
|
21
|
+
onCloseInternal?: () => void;
|
|
22
|
+
/** Called by NestJS for disconnect handling */
|
|
23
|
+
onDisconnect?: (ws: ServerWebSocket<unknown>) => void;
|
|
24
|
+
}
|
|
25
|
+
type BunWsClient = ServerWebSocket<BunWsClientData> & BaseWsInstance;
|
|
26
|
+
/**
|
|
27
|
+
* High-performance WebSocket adapter for Bun runtime with NestJS.
|
|
28
|
+
*/
|
|
29
|
+
export declare class BunWsAdapter extends AbstractWsAdapter<BunPreflightHttpServer, BunWsClient> {
|
|
30
|
+
private readonly logger;
|
|
31
|
+
private readonly nestApp;
|
|
32
|
+
private messageParser;
|
|
33
|
+
private onOpenHandler?;
|
|
34
|
+
private globalHandlersInitialized;
|
|
35
|
+
constructor(appOrHttpServer?: INestApplicationContext | object);
|
|
36
|
+
create(_port: number, options?: BunWsAdapterOptions): BunPreflightHttpServer;
|
|
37
|
+
private extractWsOptions;
|
|
38
|
+
close(server: BunPreflightHttpServer): Promise<void>;
|
|
39
|
+
bindClientConnect(server: BunPreflightHttpServer, callback: (client: BunWsClient) => void): void;
|
|
40
|
+
bindClientDisconnect(client: BunWsClient, callback: (client: BunWsClient) => void): void;
|
|
41
|
+
bindMessageHandlers(client: BunWsClient, handlers: MessageMappingProperties[], transform: (data: unknown) => Observable<unknown>): void;
|
|
42
|
+
private initializeGlobalHandlers;
|
|
43
|
+
private buildHandlerMap;
|
|
44
|
+
private processMessage;
|
|
45
|
+
private sendResponse;
|
|
46
|
+
private cleanupClient;
|
|
47
|
+
}
|
|
48
|
+
export {};
|
package/dist/index.d.ts
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
export
|
|
2
|
-
export
|
|
3
|
-
export
|
|
4
|
-
export
|
|
1
|
+
export * from './bun.adapter.js';
|
|
2
|
+
export * from './bun.request.js';
|
|
3
|
+
export * from './bun.response.js';
|
|
4
|
+
export * from './bun.file.interceptor.js';
|
|
5
|
+
export * from './internal.types.js';
|
|
6
|
+
export * from './bun.preflight-http-server.js';
|
|
7
|
+
export * from './bun.ws-adapter.js';
|