@rvncom/socket-bun-engine 1.0.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/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ The MIT License (MIT)
2
+
3
+ Copyright (c) 2025-present Guillermo Rauch and Socket.IO contributors
4
+ Copyright (c) 2026 RVNcom
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,148 @@
1
+ # @rvncom/socket-bun-engine
2
+
3
+ Engine.IO server implementation for the Bun runtime. Provides native WebSocket and HTTP long-polling transports for [Socket.IO](https://socket.io/).
4
+
5
+ Fork of `@socket.io/bun-engine` with bug fixes, improved API, and active maintenance.
6
+
7
+ ## Installation
8
+
9
+ ```bash
10
+ bun add @rvn/bun-engine
11
+ ```
12
+
13
+ ## Usage
14
+
15
+ ```ts
16
+ import { Server as Engine } from "@rvn/bun-engine";
17
+ import { Server } from "socket.io";
18
+
19
+ const engine = new Engine({
20
+ path: "/socket.io/",
21
+ });
22
+
23
+ const io = new Server();
24
+ io.bind(engine);
25
+
26
+ io.on("connection", (socket) => {
27
+ // ...
28
+ });
29
+
30
+ export default {
31
+ port: 3000,
32
+ ...engine.handler(),
33
+ };
34
+ ```
35
+
36
+ You can also use `engine.handleRequest()` directly for custom routing:
37
+
38
+ ```ts
39
+ Bun.serve({
40
+ port: 3000,
41
+
42
+ fetch(req, server) {
43
+ const url = new URL(req.url);
44
+
45
+ if (url.pathname === "/health") {
46
+ return new Response(JSON.stringify({ status: "ok", connections: engine.clientsCount }), {
47
+ headers: { "Content-Type": "application/json" },
48
+ });
49
+ }
50
+
51
+ return engine.handleRequest(req, server);
52
+ },
53
+
54
+ websocket: engine.handler().websocket,
55
+ });
56
+ ```
57
+
58
+ ## Options
59
+
60
+ ### `path`
61
+
62
+ Default: `/engine.io/`
63
+
64
+ The path to handle on the server side. Must match the client configuration.
65
+
66
+ ### `pingTimeout`
67
+
68
+ Default: `20000`
69
+
70
+ Milliseconds without a pong packet before considering the connection closed.
71
+
72
+ ### `pingInterval`
73
+
74
+ Default: `25000`
75
+
76
+ Milliseconds between ping packets sent by the server.
77
+
78
+ ### `upgradeTimeout`
79
+
80
+ Default: `10000`
81
+
82
+ Milliseconds before an uncompleted transport upgrade is cancelled.
83
+
84
+ ### `maxHttpBufferSize`
85
+
86
+ Default: `1e6` (1 MB)
87
+
88
+ Maximum message size in bytes before closing the session.
89
+
90
+ ### `maxClients`
91
+
92
+ Default: `0` (unlimited)
93
+
94
+ Maximum number of concurrent clients. New connections are rejected with HTTP 503 when the limit is reached.
95
+
96
+ ### `allowRequest`
97
+
98
+ A function that receives the handshake/upgrade request and can reject it:
99
+
100
+ ```ts
101
+ const engine = new Engine({
102
+ allowRequest: (req, server) => {
103
+ return Promise.reject("not allowed");
104
+ },
105
+ });
106
+ ```
107
+
108
+ ### `cors`
109
+
110
+ Cross-Origin Resource Sharing options:
111
+
112
+ ```ts
113
+ const engine = new Engine({
114
+ cors: {
115
+ origin: ["https://example.com"],
116
+ allowedHeaders: ["my-header"],
117
+ credentials: true,
118
+ },
119
+ });
120
+ ```
121
+
122
+ ### `editHandshakeHeaders`
123
+
124
+ Edit response headers for the handshake request:
125
+
126
+ ```ts
127
+ const engine = new Engine({
128
+ editHandshakeHeaders: (responseHeaders, req, server) => {
129
+ responseHeaders.set("set-cookie", "sid=1234");
130
+ },
131
+ });
132
+ ```
133
+
134
+ ### `editResponseHeaders`
135
+
136
+ Edit response headers for all requests:
137
+
138
+ ```ts
139
+ const engine = new Engine({
140
+ editResponseHeaders: (responseHeaders, req, server) => {
141
+ responseHeaders.set("my-header", "abcd");
142
+ },
143
+ });
144
+ ```
145
+
146
+ ## License
147
+
148
+ [MIT](/LICENSE)
package/dist/cors.d.ts ADDED
@@ -0,0 +1,11 @@
1
+ type OriginOption = boolean | string | RegExp | (string | RegExp)[];
2
+ export interface CorsOptions {
3
+ origin?: OriginOption;
4
+ methods?: string | string[];
5
+ allowedHeaders?: string | string[];
6
+ exposedHeaders?: string | string[];
7
+ credentials?: boolean;
8
+ maxAge?: number;
9
+ }
10
+ export declare function addCorsHeaders(headers: Headers, opts: CorsOptions, req: Request): void;
11
+ export {};
package/dist/cors.js ADDED
@@ -0,0 +1,83 @@
1
+ export function addCorsHeaders(headers, opts, req) {
2
+ addOrigin(opts, headers, req);
3
+ addCredentials(opts, headers);
4
+ addExposedHeaders(opts, headers);
5
+ if (req.method === "OPTIONS") {
6
+ addMethods(opts, headers);
7
+ addAllowedHeaders(opts, headers, req);
8
+ addMaxAge(opts, headers);
9
+ }
10
+ }
11
+ function join(arg) {
12
+ return Array.isArray(arg) ? arg.join(",") : arg;
13
+ }
14
+ function isOriginAllowed(allowedOrigin, origin) {
15
+ if (Array.isArray(allowedOrigin)) {
16
+ for (let i = 0; i < allowedOrigin.length; i++) {
17
+ if (isOriginAllowed(allowedOrigin[i], origin)) {
18
+ return true;
19
+ }
20
+ }
21
+ return false;
22
+ }
23
+ else if (typeof allowedOrigin === "string") {
24
+ return allowedOrigin === origin;
25
+ }
26
+ else if (allowedOrigin instanceof RegExp) {
27
+ return allowedOrigin.test(origin);
28
+ }
29
+ else {
30
+ return !!allowedOrigin;
31
+ }
32
+ }
33
+ function addOrigin(opts, headers, req) {
34
+ const origin = req.headers.get("origin");
35
+ const allowedOrigin = opts.origin;
36
+ if (!allowedOrigin || allowedOrigin === "*") {
37
+ headers.set("Access-Control-Allow-Origin", "*");
38
+ }
39
+ else if (typeof allowedOrigin === "string") {
40
+ headers.set("Access-Control-Allow-Origin", allowedOrigin);
41
+ headers.append("Vary", "Origin");
42
+ }
43
+ else if (origin) {
44
+ const isAllowed = isOriginAllowed(allowedOrigin, origin);
45
+ headers.set("Access-Control-Allow-Origin", isAllowed ? origin : "false");
46
+ headers.append("Vary", "Origin");
47
+ }
48
+ else {
49
+ headers.set("Access-Control-Allow-Origin", "false");
50
+ headers.append("Vary", "Origin");
51
+ }
52
+ }
53
+ function addMethods(opts, headers) {
54
+ if (opts.methods) {
55
+ headers.set("Access-Control-Allow-Methods", join(opts.methods));
56
+ }
57
+ }
58
+ function addAllowedHeaders(opts, headers, req) {
59
+ if (opts.allowedHeaders) {
60
+ headers.set("Access-Control-Allow-Headers", join(opts.allowedHeaders));
61
+ return;
62
+ }
63
+ const requestedHeaders = req.headers.get("access-control-request-headers");
64
+ if (requestedHeaders) {
65
+ headers.append("Vary", "Access-Control-Request-Headers");
66
+ headers.set("Access-Control-Allow-Headers", requestedHeaders);
67
+ }
68
+ }
69
+ function addExposedHeaders(opts, headers) {
70
+ if (opts.exposedHeaders) {
71
+ headers.set("Access-Control-Expose-Headers", join(opts.exposedHeaders));
72
+ }
73
+ }
74
+ function addCredentials(opts, headers) {
75
+ if (opts.credentials) {
76
+ headers.set("Access-Control-Allow-Credentials", "true");
77
+ }
78
+ }
79
+ function addMaxAge(opts, headers) {
80
+ if (opts.maxAge) {
81
+ headers.set("Access-Control-Max-Age", opts.maxAge.toString());
82
+ }
83
+ }
@@ -0,0 +1,105 @@
1
+ /**
2
+ * An events map is an interface that maps event names to their value, which represents the type of the `on` listener.
3
+ */
4
+ export interface EventsMap {
5
+ [event: string]: any;
6
+ }
7
+ /**
8
+ * The default events map, used if no EventsMap is given. Using this EventsMap is equivalent to accepting all event
9
+ * names, and any data.
10
+ */
11
+ export interface DefaultEventsMap {
12
+ [event: string]: (...args: any[]) => void;
13
+ }
14
+ /**
15
+ * Returns a union type containing all the keys of an event map.
16
+ */
17
+ export type EventNames<Map extends EventsMap> = keyof Map & (string | symbol);
18
+ /** The tuple type representing the parameters of an event listener */
19
+ export type EventParams<Map extends EventsMap, Ev extends EventNames<Map>> = Parameters<Map[Ev]>;
20
+ /**
21
+ * The event names that are either in ReservedEvents or in UserEvents
22
+ */
23
+ export type ReservedOrUserEventNames<ReservedEventsMap extends EventsMap, UserEvents extends EventsMap> = EventNames<ReservedEventsMap> | EventNames<UserEvents>;
24
+ /**
25
+ * Type of a listener of a user event or a reserved event. If `Ev` is in `ReservedEvents`, the reserved event listener
26
+ * is returned.
27
+ */
28
+ export type ReservedOrUserListener<ReservedEvents extends EventsMap, UserEvents extends EventsMap, Ev extends ReservedOrUserEventNames<ReservedEvents, UserEvents>> = FallbackToUntypedListener<Ev extends EventNames<ReservedEvents> ? ReservedEvents[Ev] : Ev extends EventNames<UserEvents> ? UserEvents[Ev] : never>;
29
+ /**
30
+ * Returns an untyped listener type if `T` is `never`; otherwise, returns `T`.
31
+ *
32
+ * Needed because of https://github.com/microsoft/TypeScript/issues/41778
33
+ */
34
+ type FallbackToUntypedListener<T> = [T] extends [never] ? (...args: any[]) => void | Promise<void> : T;
35
+ /**
36
+ * Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type parameters for mappings of event names
37
+ * to event data types, and strictly types method calls to the `EventEmitter` according to these event maps.
38
+ *
39
+ * @typeParam ListenEvents - `EventsMap` of user-defined events that can be listened to with `on` or `once`
40
+ * @typeParam EmitEvents - `EventsMap` of user-defined events that can be emitted with `emit`
41
+ * @typeParam ReservedEvents - `EventsMap` of reserved events, that can be emitted with `emitReserved`, and can be
42
+ * listened to with `listen`.
43
+ */
44
+ declare abstract class BaseEventEmitter<ListenEvents extends EventsMap, EmitEvents extends EventsMap, ReservedEvents extends EventsMap = never> {
45
+ private _listeners;
46
+ /**
47
+ * Adds the `listener` function as an event listener for `ev`.
48
+ *
49
+ * @param event - Name of the event
50
+ * @param listener - Callback function
51
+ */
52
+ on<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(event: Ev, listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>): this;
53
+ /**
54
+ * Adds a one-time `listener` function as an event listener for `ev`.
55
+ *
56
+ * @param event - Name of the event
57
+ * @param listener - Callback function
58
+ */
59
+ once<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(event: Ev, listener: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>): this;
60
+ /**
61
+ * Removes the `listener` function as an event listener for `ev`.
62
+ *
63
+ * @param event - Name of the event
64
+ * @param listener - Callback function
65
+ */
66
+ off<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(event?: Ev, listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>): this;
67
+ /**
68
+ * Removes the `listener` function as an event listener for `ev`.
69
+ *
70
+ * @param event - Name of the event
71
+ * @param listener - Callback function
72
+ */
73
+ removeListener<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(event?: Ev, listener?: ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>): this;
74
+ /**
75
+ * Emits an event.
76
+ *
77
+ * @param event - Name of the event
78
+ * @param args - Values to send to listeners of this event
79
+ */
80
+ emit<Ev extends EventNames<EmitEvents>>(event: Ev, ...args: EventParams<EmitEvents, Ev>): boolean;
81
+ /**
82
+ * Returns the listeners listening to an event.
83
+ *
84
+ * @param event - Event name
85
+ * @returns Array of listeners subscribed to `event`
86
+ */
87
+ listeners<Ev extends ReservedOrUserEventNames<ReservedEvents, ListenEvents>>(event: Ev): ReservedOrUserListener<ReservedEvents, ListenEvents, Ev>[];
88
+ }
89
+ /**
90
+ * This class extends the BaseEventEmitter abstract class, so a class extending `EventEmitter` can override the `emit`
91
+ * method and still call `emitReserved()` (since it uses `super.emit()`)
92
+ */
93
+ export declare class EventEmitter<ListenEvents extends EventsMap, EmitEvents extends EventsMap, ReservedEvents extends EventsMap = never> extends BaseEventEmitter<ListenEvents, EmitEvents, ReservedEvents> {
94
+ /**
95
+ * Emits a reserved event.
96
+ *
97
+ * This method is `protected`, so that only a class extending `EventEmitter` can emit its own reserved events.
98
+ *
99
+ * @param event - Reserved event name
100
+ * @param args - Arguments to emit along with the event
101
+ * @protected
102
+ */
103
+ protected emitReserved<Ev extends EventNames<ReservedEvents>>(event: Ev, ...args: EventParams<ReservedEvents, Ev>): boolean;
104
+ }
105
+ export {};
@@ -0,0 +1,129 @@
1
+ /**
2
+ * Strictly typed version of an `EventEmitter`. A `TypedEventEmitter` takes type parameters for mappings of event names
3
+ * to event data types, and strictly types method calls to the `EventEmitter` according to these event maps.
4
+ *
5
+ * @typeParam ListenEvents - `EventsMap` of user-defined events that can be listened to with `on` or `once`
6
+ * @typeParam EmitEvents - `EventsMap` of user-defined events that can be emitted with `emit`
7
+ * @typeParam ReservedEvents - `EventsMap` of reserved events, that can be emitted with `emitReserved`, and can be
8
+ * listened to with `listen`.
9
+ */
10
+ class BaseEventEmitter {
11
+ _listeners = new Map();
12
+ /**
13
+ * Adds the `listener` function as an event listener for `ev`.
14
+ *
15
+ * @param event - Name of the event
16
+ * @param listener - Callback function
17
+ */
18
+ on(event, listener) {
19
+ const listeners = this._listeners.get(event);
20
+ if (listeners) {
21
+ listeners.push(listener);
22
+ }
23
+ else {
24
+ this._listeners.set(event, [listener]);
25
+ }
26
+ return this;
27
+ }
28
+ /**
29
+ * Adds a one-time `listener` function as an event listener for `ev`.
30
+ *
31
+ * @param event - Name of the event
32
+ * @param listener - Callback function
33
+ */
34
+ once(event, listener) {
35
+ const onceListener = ((...args) => {
36
+ this.off(event, onceListener);
37
+ listener.apply(this, args);
38
+ });
39
+ onceListener.fn = listener;
40
+ return this.on(event, onceListener);
41
+ }
42
+ /**
43
+ * Removes the `listener` function as an event listener for `ev`.
44
+ *
45
+ * @param event - Name of the event
46
+ * @param listener - Callback function
47
+ */
48
+ off(event, listener) {
49
+ if (!event) {
50
+ this._listeners.clear();
51
+ return this;
52
+ }
53
+ if (!listener) {
54
+ this._listeners.delete(event);
55
+ return this;
56
+ }
57
+ const listeners = this._listeners.get(event);
58
+ if (!listeners) {
59
+ return this;
60
+ }
61
+ for (let i = 0; i < listeners.length; i++) {
62
+ if (listeners[i] === listener || listeners[i].fn === listener) {
63
+ listeners.splice(i, 1);
64
+ break;
65
+ }
66
+ }
67
+ if (listeners.length === 0) {
68
+ this._listeners.delete(event);
69
+ }
70
+ return this;
71
+ }
72
+ /**
73
+ * Removes the `listener` function as an event listener for `ev`.
74
+ *
75
+ * @param event - Name of the event
76
+ * @param listener - Callback function
77
+ */
78
+ removeListener(event, listener) {
79
+ return this.off(event, listener);
80
+ }
81
+ /**
82
+ * Emits an event.
83
+ *
84
+ * @param event - Name of the event
85
+ * @param args - Values to send to listeners of this event
86
+ */
87
+ emit(event, ...args) {
88
+ const listeners = this._listeners.get(event);
89
+ if (!listeners) {
90
+ return false;
91
+ }
92
+ if (listeners.length === 1) {
93
+ listeners[0].apply(this, args);
94
+ }
95
+ else {
96
+ for (const listener of listeners.slice()) {
97
+ listener.apply(this, args);
98
+ }
99
+ }
100
+ return true;
101
+ }
102
+ /**
103
+ * Returns the listeners listening to an event.
104
+ *
105
+ * @param event - Event name
106
+ * @returns Array of listeners subscribed to `event`
107
+ */
108
+ listeners(event) {
109
+ return this._listeners.get(event) || [];
110
+ }
111
+ }
112
+ /**
113
+ * This class extends the BaseEventEmitter abstract class, so a class extending `EventEmitter` can override the `emit`
114
+ * method and still call `emitReserved()` (since it uses `super.emit()`)
115
+ */
116
+ export class EventEmitter extends BaseEventEmitter {
117
+ /**
118
+ * Emits a reserved event.
119
+ *
120
+ * This method is `protected`, so that only a class extending `EventEmitter` can emit its own reserved events.
121
+ *
122
+ * @param event - Reserved event name
123
+ * @param args - Arguments to emit along with the event
124
+ * @protected
125
+ */
126
+ emitReserved(event, ...args) {
127
+ return super.emit(event, ...args);
128
+ }
129
+ }
@@ -0,0 +1,3 @@
1
+ export { Server, type ServerOptions } from "./server";
2
+ export { type RawData } from "./parser";
3
+ export { type BunWebSocket, type WebSocketData } from "./transports/websocket";
package/dist/index.js ADDED
@@ -0,0 +1,3 @@
1
+ export { Server } from "./server";
2
+ export {} from "./parser";
3
+ export {} from "./transports/websocket";
@@ -0,0 +1,14 @@
1
+ export type PacketType = "open" | "close" | "ping" | "pong" | "message" | "upgrade" | "noop" | "error";
2
+ export type RawData = string | Buffer;
3
+ export interface Packet {
4
+ type: PacketType;
5
+ data?: RawData;
6
+ }
7
+ type BinaryType = "arraybuffer" | "blob";
8
+ export declare const Parser: {
9
+ encodePacket({ type, data }: Packet, supportsBinary: boolean): RawData;
10
+ decodePacket(encodedPacket: RawData, _binaryType?: BinaryType): Packet;
11
+ encodePayload(packets: Packet[]): string;
12
+ decodePayload(encodedPayload: string, binaryType?: BinaryType): Packet[];
13
+ };
14
+ export {};
package/dist/parser.js ADDED
@@ -0,0 +1,73 @@
1
+ const SEPARATOR = String.fromCharCode(30); // see https://en.wikipedia.org/wiki/Delimiter#ASCII_delimited_text
2
+ const PACKET_TYPES = new Map();
3
+ const PACKET_TYPES_REVERSE = new Map();
4
+ [
5
+ "open",
6
+ "close",
7
+ "ping",
8
+ "pong",
9
+ "message",
10
+ "upgrade",
11
+ "noop",
12
+ ].forEach((type, index) => {
13
+ PACKET_TYPES.set(type, "" + index);
14
+ PACKET_TYPES_REVERSE.set("" + index, type);
15
+ });
16
+ const ERROR_PACKET = { type: "error", data: "parser error" };
17
+ export const Parser = {
18
+ encodePacket({ type, data }, supportsBinary) {
19
+ if (Buffer.isBuffer(data)) {
20
+ return supportsBinary ? data : "b" + data.toString("base64");
21
+ }
22
+ else {
23
+ return PACKET_TYPES.get(type) + (data || "");
24
+ }
25
+ },
26
+ decodePacket(encodedPacket, _binaryType) {
27
+ if (typeof encodedPacket !== "string") {
28
+ return {
29
+ type: "message",
30
+ data: encodedPacket,
31
+ };
32
+ }
33
+ const typeChar = encodedPacket.charAt(0);
34
+ if (typeChar === "b") {
35
+ const buffer = Buffer.from(encodedPacket.substring(1), "base64");
36
+ return {
37
+ type: "message",
38
+ data: buffer,
39
+ };
40
+ }
41
+ if (!PACKET_TYPES_REVERSE.has(typeChar)) {
42
+ return ERROR_PACKET;
43
+ }
44
+ const type = PACKET_TYPES_REVERSE.get(typeChar);
45
+ return encodedPacket.length > 1
46
+ ? {
47
+ type,
48
+ data: encodedPacket.substring(1),
49
+ }
50
+ : {
51
+ type,
52
+ };
53
+ },
54
+ encodePayload(packets) {
55
+ const encodedPackets = [];
56
+ for (const packet of packets) {
57
+ encodedPackets.push(this.encodePacket(packet, false));
58
+ }
59
+ return encodedPackets.join(SEPARATOR);
60
+ },
61
+ decodePayload(encodedPayload, binaryType) {
62
+ const encodedPackets = encodedPayload.split(SEPARATOR);
63
+ const packets = [];
64
+ for (let i = 0; i < encodedPackets.length; i++) {
65
+ const decodedPacket = this.decodePacket(encodedPackets[i], binaryType);
66
+ packets.push(decodedPacket);
67
+ if (decodedPacket.type === "error") {
68
+ break;
69
+ }
70
+ }
71
+ return packets;
72
+ },
73
+ };