@colyseus/bun-websockets 0.17.5 → 0.17.6
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/build/BunWebSockets.cjs +94 -22
- package/build/BunWebSockets.cjs.map +2 -2
- package/build/BunWebSockets.d.ts +11 -5
- package/build/BunWebSockets.mjs +95 -23
- package/build/BunWebSockets.mjs.map +2 -2
- package/package.json +3 -3
- package/src/BunWebSockets.ts +133 -71
package/build/BunWebSockets.cjs
CHANGED
|
@@ -33,30 +33,101 @@ var BunWebSockets = class extends import_core.Transport {
|
|
|
33
33
|
this.clientWrappers = /* @__PURE__ */ new WeakMap();
|
|
34
34
|
this._originalRawSend = null;
|
|
35
35
|
this.options = {};
|
|
36
|
-
const self = this;
|
|
37
36
|
this.options = options;
|
|
38
|
-
|
|
39
|
-
|
|
37
|
+
}
|
|
38
|
+
getExpressApp() {
|
|
39
|
+
if (!this._expressApp) {
|
|
40
|
+
console.warn("");
|
|
41
|
+
console.warn("\u274C Error: Express integration not yet implemented for BunWebSockets");
|
|
42
|
+
console.warn(" Please use bindRouter() instead or consider using uWebSocketsTransport");
|
|
43
|
+
console.warn("");
|
|
40
44
|
}
|
|
45
|
+
return this._expressApp;
|
|
46
|
+
}
|
|
47
|
+
bindRouter(router) {
|
|
48
|
+
this._router = router;
|
|
41
49
|
}
|
|
42
50
|
listen(port, hostname, backlog, listeningListener) {
|
|
43
|
-
|
|
44
|
-
this.
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
+
const self = this;
|
|
52
|
+
this._server = Bun.serve({
|
|
53
|
+
port,
|
|
54
|
+
hostname,
|
|
55
|
+
async fetch(req, server) {
|
|
56
|
+
const url = new URL(req.url);
|
|
57
|
+
if (server.upgrade(req, {
|
|
58
|
+
data: {
|
|
59
|
+
url: url.pathname + url.search,
|
|
60
|
+
searchParams: url.searchParams,
|
|
61
|
+
headers: req.headers,
|
|
62
|
+
remoteAddress: server.requestIP(req)?.address || "unknown"
|
|
63
|
+
}
|
|
64
|
+
})) {
|
|
65
|
+
return;
|
|
66
|
+
}
|
|
67
|
+
if (self._router) {
|
|
68
|
+
try {
|
|
69
|
+
const corsHeaders = {
|
|
70
|
+
...import_core.matchMaker.controller.DEFAULT_CORS_HEADERS,
|
|
71
|
+
...import_core.matchMaker.controller.getCorsHeaders(req.headers)
|
|
72
|
+
};
|
|
73
|
+
if (req.method === "OPTIONS") {
|
|
74
|
+
return new Response(null, {
|
|
75
|
+
status: 204,
|
|
76
|
+
headers: corsHeaders
|
|
77
|
+
});
|
|
78
|
+
}
|
|
79
|
+
const response = await self._router.handler(req);
|
|
80
|
+
const headers = new Headers(response.headers);
|
|
81
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
82
|
+
if (!headers.has(key)) {
|
|
83
|
+
headers.set(key, value.toString());
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
return new Response(response.body, {
|
|
87
|
+
status: response.status,
|
|
88
|
+
statusText: response.statusText,
|
|
89
|
+
headers
|
|
90
|
+
});
|
|
91
|
+
} catch (e) {
|
|
92
|
+
(0, import_core.debugAndPrintError)(e);
|
|
93
|
+
return new Response(JSON.stringify({
|
|
94
|
+
code: e.code,
|
|
95
|
+
error: e.message
|
|
96
|
+
}), {
|
|
97
|
+
status: 500,
|
|
98
|
+
headers: { "Content-Type": "application/json" }
|
|
99
|
+
});
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
if (self._expressApp) {
|
|
103
|
+
console.warn("Express integration not yet implemented for BunWebSockets");
|
|
104
|
+
}
|
|
105
|
+
return new Response("Not Found", { status: 404 });
|
|
106
|
+
},
|
|
107
|
+
websocket: {
|
|
108
|
+
...this.options,
|
|
109
|
+
async open(ws) {
|
|
110
|
+
await self.onConnection(ws);
|
|
111
|
+
},
|
|
112
|
+
message(ws, message) {
|
|
113
|
+
self.clientWrappers.get(ws)?.emit("message", message);
|
|
114
|
+
},
|
|
115
|
+
close(ws, code, reason) {
|
|
116
|
+
(0, import_core.spliceOne)(self.clients, self.clients.indexOf(ws));
|
|
117
|
+
const clientWrapper = self.clientWrappers.get(ws);
|
|
118
|
+
if (clientWrapper) {
|
|
119
|
+
self.clientWrappers.delete(ws);
|
|
120
|
+
clientWrapper.emit("close", code);
|
|
121
|
+
}
|
|
122
|
+
}
|
|
51
123
|
}
|
|
52
124
|
});
|
|
53
|
-
|
|
125
|
+
listeningListener?.();
|
|
54
126
|
return this;
|
|
55
127
|
}
|
|
56
128
|
shutdown() {
|
|
57
|
-
if (this.
|
|
58
|
-
this.
|
|
59
|
-
this.server.emit("close");
|
|
129
|
+
if (this._server) {
|
|
130
|
+
this._server.stop();
|
|
60
131
|
}
|
|
61
132
|
}
|
|
62
133
|
simulateLatency(milliseconds) {
|
|
@@ -74,9 +145,10 @@ var BunWebSockets = class extends import_core.Transport {
|
|
|
74
145
|
const wrapper = new import_WebSocketClient.WebSocketWrapper(rawClient);
|
|
75
146
|
this.clients.push(rawClient);
|
|
76
147
|
this.clientWrappers.set(rawClient, wrapper);
|
|
77
|
-
const
|
|
78
|
-
const
|
|
79
|
-
const
|
|
148
|
+
const url = rawClient.data.url;
|
|
149
|
+
const searchParams = rawClient.data.searchParams;
|
|
150
|
+
const sessionId = searchParams.get("sessionId");
|
|
151
|
+
const processAndRoomId = url.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
|
|
80
152
|
const roomId = processAndRoomId && processAndRoomId[1];
|
|
81
153
|
if (!sessionId && !roomId) {
|
|
82
154
|
const timeout = setTimeout(() => rawClient.close(import_core.CloseCode.NORMAL_CLOSURE), 1e3);
|
|
@@ -86,13 +158,13 @@ var BunWebSockets = class extends import_core.Transport {
|
|
|
86
158
|
}
|
|
87
159
|
const room = import_core.matchMaker.getLocalRoomById(roomId);
|
|
88
160
|
const client = new import_WebSocketClient.WebSocketClient(sessionId, wrapper);
|
|
89
|
-
const reconnectionToken =
|
|
90
|
-
const skipHandshake =
|
|
161
|
+
const reconnectionToken = searchParams.get("reconnectionToken");
|
|
162
|
+
const skipHandshake = searchParams.has("skipHandshake");
|
|
91
163
|
try {
|
|
92
164
|
await (0, import_core.connectClientToRoom)(room, client, {
|
|
93
|
-
token:
|
|
165
|
+
token: searchParams.get("_authToken") ?? (0, import_core.getBearerToken)(rawClient.data.headers["authorization"]),
|
|
94
166
|
headers: rawClient.data.headers,
|
|
95
|
-
ip: rawClient.data.headers["x-real-ip"] ?? rawClient.data.headers["x-forwarded-for"] ?? rawClient.remoteAddress
|
|
167
|
+
ip: rawClient.data.headers["x-real-ip"] ?? rawClient.data.headers["x-forwarded-for"] ?? rawClient.data.remoteAddress
|
|
96
168
|
}, {
|
|
97
169
|
reconnectionToken,
|
|
98
170
|
skipHandshake
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/BunWebSockets.ts"],
|
|
4
|
-
"sourcesContent": ["// <reference types=\"bun-types\" />\n\n// \"bun-types\" is currently conflicting with \"ws\" types.\n// @ts-ignore\nimport { ServerWebSocket, WebSocketHandler } from 'bun';\
|
|
5
|
-
"mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAIA,
|
|
4
|
+
"sourcesContent": ["// <reference types=\"bun-types\" />\n\n// \"bun-types\" is currently conflicting with \"ws\" types.\n// @ts-ignore\nimport { Server, ServerWebSocket, WebSocketHandler } from 'bun';\nimport type { Application } from \"express\";\nimport type { Router } from '@colyseus/core';\n\nimport { matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom, spliceOne } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';\n\n// Bun global is available at runtime\ndeclare const Bun: any;\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler<WebSocketData>, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: string;\n searchParams: URLSearchParams;\n headers: Headers;\n remoteAddress: string;\n}\n\nexport class BunWebSockets extends Transport {\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n private _server: Server<WebSocketData> | undefined;\n private _expressApp: Application | undefined;\n private _router: Router | undefined;\n private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;\n private options: TransportOptions = {};\n\n constructor(options: TransportOptions = {}) {\n super();\n this.options = options;\n }\n\n public getExpressApp(): Application {\n if (!this._expressApp) {\n console.warn(\"\");\n console.warn(\"\u274C Error: Express integration not yet implemented for BunWebSockets\");\n console.warn(\" Please use bindRouter() instead or consider using uWebSocketsTransport\");\n console.warn(\"\");\n }\n return this._expressApp;\n }\n\n public bindRouter(router: Router) {\n this._router = router;\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const self = this;\n\n this._server = Bun.serve({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n // Try to upgrade to WebSocket\n if (server.upgrade(req, {\n data: {\n url: url.pathname + url.search,\n searchParams: url.searchParams,\n headers: req.headers as Headers,\n remoteAddress: server.requestIP(req)?.address || 'unknown',\n }\n })) {\n return; // WebSocket upgrade successful\n }\n\n // Handle HTTP requests through router\n if (self._router) {\n try {\n // Write CORS headers\n const corsHeaders = {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(req.headers)\n };\n\n // Handle OPTIONS requests\n if (req.method === \"OPTIONS\") {\n return new Response(null, {\n status: 204,\n headers: corsHeaders\n });\n }\n\n const response = await self._router.handler(req);\n\n // Add CORS headers to response\n const headers = new Headers(response.headers);\n Object.entries(corsHeaders).forEach(([key, value]) => {\n if (!headers.has(key)) {\n headers.set(key, value.toString());\n }\n });\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers\n });\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(JSON.stringify({\n code: e.code,\n error: e.message\n }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' }\n });\n }\n }\n\n // Fallback to express app if available\n if (self._expressApp) {\n // TODO: Implement express integration for Bun\n console.warn(\"Express integration not yet implemented for BunWebSockets\");\n }\n\n return new Response(\"Not Found\", { status: 404 });\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await self.onConnection(ws);\n },\n\n message(ws, message) {\n self.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(self.clients, self.clients.indexOf(ws));\n\n const clientWrapper = self.clientWrappers.get(ws);\n if (clientWrapper) {\n self.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n }\n });\n\n listeningListener?.();\n\n return this;\n }\n\n public shutdown() {\n if (this._server) {\n this._server.stop();\n }\n }\n\n public simulateLatency(milliseconds: number) {\n if (this._originalRawSend == null) {\n this._originalRawSend = WebSocketClient.prototype.raw;\n }\n\n const originalRawSend = this._originalRawSend;\n WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalRawSend : function (...args: any[]) {\n let [buf, ...rest] = args;\n buf = Buffer.from(buf);\n // @ts-ignore\n setTimeout(() => originalRawSend.apply(this, [buf, ...rest]), milliseconds);\n };\n }\n\n protected async onConnection(rawClient: ServerWebSocket<WebSocketData>) {\n const wrapper = new WebSocketWrapper(rawClient);\n // keep reference to client and its wrapper\n this.clients.push(rawClient);\n this.clientWrappers.set(rawClient, wrapper);\n\n const url = rawClient.data.url;\n const searchParams = rawClient.data.searchParams;\n\n const sessionId = searchParams.get(\"sessionId\");\n const processAndRoomId = url.match(/\\/[a-zA-Z0-9_\\-]+\\/([a-zA-Z0-9_\\-]+)$/);\n const roomId = processAndRoomId && processAndRoomId[1];\n\n // If sessionId is not provided, allow ping-pong utility.\n if (!sessionId && !roomId) {\n // Disconnect automatically after 1 second if no message is received.\n const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1000);\n wrapper.on('message', (_) => rawClient.send(new Uint8Array([Protocol.PING])));\n wrapper.on('close', () => clearTimeout(timeout));\n return;\n }\n\n const room = matchMaker.getLocalRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n const reconnectionToken = searchParams.get(\"reconnectionToken\");\n const skipHandshake = searchParams.has(\"skipHandshake\");\n\n try {\n await connectClientToRoom(room, client, {\n token: searchParams.get(\"_authToken\") ?? getBearerToken(rawClient.data.headers['authorization']),\n headers: rawClient.data.headers,\n ip: rawClient.data.headers['x-real-ip'] ?? rawClient.data.headers['x-forwarded-for'] ?? rawClient.data.remoteAddress,\n }, {\n reconnectionToken,\n skipHandshake\n });\n\n } catch (e: any) {\n debugAndPrintError(e);\n\n // send error code to client then terminate\n client.error(e.code, e.message, () => rawClient.close());\n }\n }\n\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/build/BunWebSockets.d.ts
CHANGED
|
@@ -1,20 +1,26 @@
|
|
|
1
1
|
import { ServerWebSocket, WebSocketHandler } from 'bun';
|
|
2
2
|
import type { Application } from "express";
|
|
3
|
+
import type { Router } from '@colyseus/core';
|
|
3
4
|
import { Transport } from '@colyseus/core';
|
|
4
5
|
import { WebSocketWrapper } from './WebSocketClient.ts';
|
|
5
|
-
export type TransportOptions = Partial<Omit<WebSocketHandler
|
|
6
|
+
export type TransportOptions = Partial<Omit<WebSocketHandler<WebSocketData>, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
|
|
6
7
|
interface WebSocketData {
|
|
7
|
-
url:
|
|
8
|
-
|
|
8
|
+
url: string;
|
|
9
|
+
searchParams: URLSearchParams;
|
|
10
|
+
headers: Headers;
|
|
11
|
+
remoteAddress: string;
|
|
9
12
|
}
|
|
10
13
|
export declare class BunWebSockets extends Transport {
|
|
11
|
-
expressApp: Application;
|
|
12
14
|
protected clients: ServerWebSocket<WebSocketData>[];
|
|
13
15
|
protected clientWrappers: WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>;
|
|
14
|
-
private
|
|
16
|
+
private _server;
|
|
17
|
+
private _expressApp;
|
|
18
|
+
private _router;
|
|
15
19
|
private _originalRawSend;
|
|
16
20
|
private options;
|
|
17
21
|
constructor(options?: TransportOptions);
|
|
22
|
+
getExpressApp(): Application;
|
|
23
|
+
bindRouter(router: Router): void;
|
|
18
24
|
listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void): this;
|
|
19
25
|
shutdown(): void;
|
|
20
26
|
simulateLatency(milliseconds: number): void;
|
package/build/BunWebSockets.mjs
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// packages/transport/bun-websockets/src/BunWebSockets.ts
|
|
2
2
|
import "bun";
|
|
3
|
-
import {
|
|
3
|
+
import { matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom, spliceOne } from "@colyseus/core";
|
|
4
4
|
import { WebSocketClient, WebSocketWrapper } from "./WebSocketClient.mjs";
|
|
5
5
|
var BunWebSockets = class extends Transport {
|
|
6
6
|
constructor(options = {}) {
|
|
@@ -9,30 +9,101 @@ var BunWebSockets = class extends Transport {
|
|
|
9
9
|
this.clientWrappers = /* @__PURE__ */ new WeakMap();
|
|
10
10
|
this._originalRawSend = null;
|
|
11
11
|
this.options = {};
|
|
12
|
-
const self = this;
|
|
13
12
|
this.options = options;
|
|
14
|
-
|
|
15
|
-
|
|
13
|
+
}
|
|
14
|
+
getExpressApp() {
|
|
15
|
+
if (!this._expressApp) {
|
|
16
|
+
console.warn("");
|
|
17
|
+
console.warn("\u274C Error: Express integration not yet implemented for BunWebSockets");
|
|
18
|
+
console.warn(" Please use bindRouter() instead or consider using uWebSocketsTransport");
|
|
19
|
+
console.warn("");
|
|
16
20
|
}
|
|
21
|
+
return this._expressApp;
|
|
22
|
+
}
|
|
23
|
+
bindRouter(router) {
|
|
24
|
+
this._router = router;
|
|
17
25
|
}
|
|
18
26
|
listen(port, hostname, backlog, listeningListener) {
|
|
19
|
-
|
|
20
|
-
this.
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
+
const self = this;
|
|
28
|
+
this._server = Bun.serve({
|
|
29
|
+
port,
|
|
30
|
+
hostname,
|
|
31
|
+
async fetch(req, server) {
|
|
32
|
+
const url = new URL(req.url);
|
|
33
|
+
if (server.upgrade(req, {
|
|
34
|
+
data: {
|
|
35
|
+
url: url.pathname + url.search,
|
|
36
|
+
searchParams: url.searchParams,
|
|
37
|
+
headers: req.headers,
|
|
38
|
+
remoteAddress: server.requestIP(req)?.address || "unknown"
|
|
39
|
+
}
|
|
40
|
+
})) {
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
if (self._router) {
|
|
44
|
+
try {
|
|
45
|
+
const corsHeaders = {
|
|
46
|
+
...matchMaker.controller.DEFAULT_CORS_HEADERS,
|
|
47
|
+
...matchMaker.controller.getCorsHeaders(req.headers)
|
|
48
|
+
};
|
|
49
|
+
if (req.method === "OPTIONS") {
|
|
50
|
+
return new Response(null, {
|
|
51
|
+
status: 204,
|
|
52
|
+
headers: corsHeaders
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
const response = await self._router.handler(req);
|
|
56
|
+
const headers = new Headers(response.headers);
|
|
57
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
58
|
+
if (!headers.has(key)) {
|
|
59
|
+
headers.set(key, value.toString());
|
|
60
|
+
}
|
|
61
|
+
});
|
|
62
|
+
return new Response(response.body, {
|
|
63
|
+
status: response.status,
|
|
64
|
+
statusText: response.statusText,
|
|
65
|
+
headers
|
|
66
|
+
});
|
|
67
|
+
} catch (e) {
|
|
68
|
+
debugAndPrintError(e);
|
|
69
|
+
return new Response(JSON.stringify({
|
|
70
|
+
code: e.code,
|
|
71
|
+
error: e.message
|
|
72
|
+
}), {
|
|
73
|
+
status: 500,
|
|
74
|
+
headers: { "Content-Type": "application/json" }
|
|
75
|
+
});
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
if (self._expressApp) {
|
|
79
|
+
console.warn("Express integration not yet implemented for BunWebSockets");
|
|
80
|
+
}
|
|
81
|
+
return new Response("Not Found", { status: 404 });
|
|
82
|
+
},
|
|
83
|
+
websocket: {
|
|
84
|
+
...this.options,
|
|
85
|
+
async open(ws) {
|
|
86
|
+
await self.onConnection(ws);
|
|
87
|
+
},
|
|
88
|
+
message(ws, message) {
|
|
89
|
+
self.clientWrappers.get(ws)?.emit("message", message);
|
|
90
|
+
},
|
|
91
|
+
close(ws, code, reason) {
|
|
92
|
+
spliceOne(self.clients, self.clients.indexOf(ws));
|
|
93
|
+
const clientWrapper = self.clientWrappers.get(ws);
|
|
94
|
+
if (clientWrapper) {
|
|
95
|
+
self.clientWrappers.delete(ws);
|
|
96
|
+
clientWrapper.emit("close", code);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
27
99
|
}
|
|
28
100
|
});
|
|
29
|
-
|
|
101
|
+
listeningListener?.();
|
|
30
102
|
return this;
|
|
31
103
|
}
|
|
32
104
|
shutdown() {
|
|
33
|
-
if (this.
|
|
34
|
-
this.
|
|
35
|
-
this.server.emit("close");
|
|
105
|
+
if (this._server) {
|
|
106
|
+
this._server.stop();
|
|
36
107
|
}
|
|
37
108
|
}
|
|
38
109
|
simulateLatency(milliseconds) {
|
|
@@ -50,9 +121,10 @@ var BunWebSockets = class extends Transport {
|
|
|
50
121
|
const wrapper = new WebSocketWrapper(rawClient);
|
|
51
122
|
this.clients.push(rawClient);
|
|
52
123
|
this.clientWrappers.set(rawClient, wrapper);
|
|
53
|
-
const
|
|
54
|
-
const
|
|
55
|
-
const
|
|
124
|
+
const url = rawClient.data.url;
|
|
125
|
+
const searchParams = rawClient.data.searchParams;
|
|
126
|
+
const sessionId = searchParams.get("sessionId");
|
|
127
|
+
const processAndRoomId = url.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
|
|
56
128
|
const roomId = processAndRoomId && processAndRoomId[1];
|
|
57
129
|
if (!sessionId && !roomId) {
|
|
58
130
|
const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1e3);
|
|
@@ -62,13 +134,13 @@ var BunWebSockets = class extends Transport {
|
|
|
62
134
|
}
|
|
63
135
|
const room = matchMaker.getLocalRoomById(roomId);
|
|
64
136
|
const client = new WebSocketClient(sessionId, wrapper);
|
|
65
|
-
const reconnectionToken =
|
|
66
|
-
const skipHandshake =
|
|
137
|
+
const reconnectionToken = searchParams.get("reconnectionToken");
|
|
138
|
+
const skipHandshake = searchParams.has("skipHandshake");
|
|
67
139
|
try {
|
|
68
140
|
await connectClientToRoom(room, client, {
|
|
69
|
-
token:
|
|
141
|
+
token: searchParams.get("_authToken") ?? getBearerToken(rawClient.data.headers["authorization"]),
|
|
70
142
|
headers: rawClient.data.headers,
|
|
71
|
-
ip: rawClient.data.headers["x-real-ip"] ?? rawClient.data.headers["x-forwarded-for"] ?? rawClient.remoteAddress
|
|
143
|
+
ip: rawClient.data.headers["x-real-ip"] ?? rawClient.data.headers["x-forwarded-for"] ?? rawClient.data.remoteAddress
|
|
72
144
|
}, {
|
|
73
145
|
reconnectionToken,
|
|
74
146
|
skipHandshake
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"version": 3,
|
|
3
3
|
"sources": ["../src/BunWebSockets.ts"],
|
|
4
|
-
"sourcesContent": ["// <reference types=\"bun-types\" />\n\n// \"bun-types\" is currently conflicting with \"ws\" types.\n// @ts-ignore\nimport { ServerWebSocket, WebSocketHandler } from 'bun';\
|
|
5
|
-
"mappings": ";AAIA,
|
|
4
|
+
"sourcesContent": ["// <reference types=\"bun-types\" />\n\n// \"bun-types\" is currently conflicting with \"ws\" types.\n// @ts-ignore\nimport { Server, ServerWebSocket, WebSocketHandler } from 'bun';\nimport type { Application } from \"express\";\nimport type { Router } from '@colyseus/core';\n\nimport { matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom, spliceOne } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';\n\n// Bun global is available at runtime\ndeclare const Bun: any;\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler<WebSocketData>, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: string;\n searchParams: URLSearchParams;\n headers: Headers;\n remoteAddress: string;\n}\n\nexport class BunWebSockets extends Transport {\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n private _server: Server<WebSocketData> | undefined;\n private _expressApp: Application | undefined;\n private _router: Router | undefined;\n private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;\n private options: TransportOptions = {};\n\n constructor(options: TransportOptions = {}) {\n super();\n this.options = options;\n }\n\n public getExpressApp(): Application {\n if (!this._expressApp) {\n console.warn(\"\");\n console.warn(\"\u274C Error: Express integration not yet implemented for BunWebSockets\");\n console.warn(\" Please use bindRouter() instead or consider using uWebSocketsTransport\");\n console.warn(\"\");\n }\n return this._expressApp;\n }\n\n public bindRouter(router: Router) {\n this._router = router;\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const self = this;\n\n this._server = Bun.serve({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n // Try to upgrade to WebSocket\n if (server.upgrade(req, {\n data: {\n url: url.pathname + url.search,\n searchParams: url.searchParams,\n headers: req.headers as Headers,\n remoteAddress: server.requestIP(req)?.address || 'unknown',\n }\n })) {\n return; // WebSocket upgrade successful\n }\n\n // Handle HTTP requests through router\n if (self._router) {\n try {\n // Write CORS headers\n const corsHeaders = {\n ...matchMaker.controller.DEFAULT_CORS_HEADERS,\n ...matchMaker.controller.getCorsHeaders(req.headers)\n };\n\n // Handle OPTIONS requests\n if (req.method === \"OPTIONS\") {\n return new Response(null, {\n status: 204,\n headers: corsHeaders\n });\n }\n\n const response = await self._router.handler(req);\n\n // Add CORS headers to response\n const headers = new Headers(response.headers);\n Object.entries(corsHeaders).forEach(([key, value]) => {\n if (!headers.has(key)) {\n headers.set(key, value.toString());\n }\n });\n\n return new Response(response.body, {\n status: response.status,\n statusText: response.statusText,\n headers\n });\n\n } catch (e: any) {\n debugAndPrintError(e);\n return new Response(JSON.stringify({\n code: e.code,\n error: e.message\n }), {\n status: 500,\n headers: { 'Content-Type': 'application/json' }\n });\n }\n }\n\n // Fallback to express app if available\n if (self._expressApp) {\n // TODO: Implement express integration for Bun\n console.warn(\"Express integration not yet implemented for BunWebSockets\");\n }\n\n return new Response(\"Not Found\", { status: 404 });\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await self.onConnection(ws);\n },\n\n message(ws, message) {\n self.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(self.clients, self.clients.indexOf(ws));\n\n const clientWrapper = self.clientWrappers.get(ws);\n if (clientWrapper) {\n self.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n }\n });\n\n listeningListener?.();\n\n return this;\n }\n\n public shutdown() {\n if (this._server) {\n this._server.stop();\n }\n }\n\n public simulateLatency(milliseconds: number) {\n if (this._originalRawSend == null) {\n this._originalRawSend = WebSocketClient.prototype.raw;\n }\n\n const originalRawSend = this._originalRawSend;\n WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalRawSend : function (...args: any[]) {\n let [buf, ...rest] = args;\n buf = Buffer.from(buf);\n // @ts-ignore\n setTimeout(() => originalRawSend.apply(this, [buf, ...rest]), milliseconds);\n };\n }\n\n protected async onConnection(rawClient: ServerWebSocket<WebSocketData>) {\n const wrapper = new WebSocketWrapper(rawClient);\n // keep reference to client and its wrapper\n this.clients.push(rawClient);\n this.clientWrappers.set(rawClient, wrapper);\n\n const url = rawClient.data.url;\n const searchParams = rawClient.data.searchParams;\n\n const sessionId = searchParams.get(\"sessionId\");\n const processAndRoomId = url.match(/\\/[a-zA-Z0-9_\\-]+\\/([a-zA-Z0-9_\\-]+)$/);\n const roomId = processAndRoomId && processAndRoomId[1];\n\n // If sessionId is not provided, allow ping-pong utility.\n if (!sessionId && !roomId) {\n // Disconnect automatically after 1 second if no message is received.\n const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1000);\n wrapper.on('message', (_) => rawClient.send(new Uint8Array([Protocol.PING])));\n wrapper.on('close', () => clearTimeout(timeout));\n return;\n }\n\n const room = matchMaker.getLocalRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n const reconnectionToken = searchParams.get(\"reconnectionToken\");\n const skipHandshake = searchParams.has(\"skipHandshake\");\n\n try {\n await connectClientToRoom(room, client, {\n token: searchParams.get(\"_authToken\") ?? getBearerToken(rawClient.data.headers['authorization']),\n headers: rawClient.data.headers,\n ip: rawClient.data.headers['x-real-ip'] ?? rawClient.data.headers['x-forwarded-for'] ?? rawClient.data.remoteAddress,\n }, {\n reconnectionToken,\n skipHandshake\n });\n\n } catch (e: any) {\n debugAndPrintError(e);\n\n // send error code to client then terminate\n client.error(e.code, e.message, () => rawClient.close());\n }\n }\n\n}\n"],
|
|
5
|
+
"mappings": "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",
|
|
6
6
|
"names": []
|
|
7
7
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/bun-websockets",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.6",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"input": "./src/index.ts",
|
|
6
6
|
"main": "./build/index.cjs",
|
|
@@ -21,11 +21,11 @@
|
|
|
21
21
|
}
|
|
22
22
|
},
|
|
23
23
|
"dependencies": {
|
|
24
|
-
"@colyseus/core": "^0.17.
|
|
24
|
+
"@colyseus/core": "^0.17.22"
|
|
25
25
|
},
|
|
26
26
|
"devDependencies": {
|
|
27
27
|
"bun-types": "^1.2.0",
|
|
28
|
-
"@colyseus/core": "^0.17.
|
|
28
|
+
"@colyseus/core": "^0.17.22"
|
|
29
29
|
},
|
|
30
30
|
"author": "Endel Dreyer",
|
|
31
31
|
"license": "MIT",
|
package/src/BunWebSockets.ts
CHANGED
|
@@ -2,100 +2,161 @@
|
|
|
2
2
|
|
|
3
3
|
// "bun-types" is currently conflicting with "ws" types.
|
|
4
4
|
// @ts-ignore
|
|
5
|
-
import { ServerWebSocket, WebSocketHandler } from 'bun';
|
|
5
|
+
import { Server, ServerWebSocket, WebSocketHandler } from 'bun';
|
|
6
|
+
import express, { type Application } from "express";
|
|
7
|
+
import type { Router } from '@colyseus/core';
|
|
6
8
|
|
|
7
|
-
|
|
8
|
-
import type { Application, Request, Response } from "express";
|
|
9
|
-
|
|
10
|
-
import { HttpServerMock, matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom } from '@colyseus/core';
|
|
9
|
+
import { matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom, spliceOne } from '@colyseus/core';
|
|
11
10
|
import { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';
|
|
12
11
|
|
|
13
|
-
|
|
12
|
+
// Bun global is available at runtime
|
|
13
|
+
declare const Bun: any;
|
|
14
|
+
|
|
15
|
+
export type TransportOptions = Partial<Omit<WebSocketHandler<WebSocketData>, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
|
|
14
16
|
|
|
15
17
|
interface WebSocketData {
|
|
16
|
-
url:
|
|
17
|
-
|
|
18
|
+
url: string;
|
|
19
|
+
searchParams: URLSearchParams;
|
|
20
|
+
headers: Headers;
|
|
21
|
+
remoteAddress: string;
|
|
18
22
|
}
|
|
19
23
|
|
|
20
24
|
export class BunWebSockets extends Transport {
|
|
21
|
-
public expressApp: Application;
|
|
22
|
-
|
|
23
25
|
protected clients: ServerWebSocket<WebSocketData>[] = [];
|
|
24
26
|
protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();
|
|
25
27
|
|
|
26
|
-
private
|
|
28
|
+
private _server: Server<WebSocketData> | undefined;
|
|
29
|
+
private _expressApp: Application | undefined;
|
|
30
|
+
private _router: Router | undefined;
|
|
27
31
|
private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;
|
|
28
32
|
private options: TransportOptions = {};
|
|
29
33
|
|
|
30
34
|
constructor(options: TransportOptions = {}) {
|
|
31
35
|
super();
|
|
32
|
-
|
|
33
|
-
const self = this;
|
|
34
|
-
|
|
35
36
|
this.options = options;
|
|
37
|
+
}
|
|
36
38
|
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
// async open(ws) {
|
|
42
|
-
// await self.onConnection(ws);
|
|
43
|
-
// },
|
|
44
|
-
|
|
45
|
-
// message(ws, message) {
|
|
46
|
-
// self.clientWrappers.get(ws)?.emit('message', message);
|
|
47
|
-
// },
|
|
48
|
-
|
|
49
|
-
// close(ws, code, reason) {
|
|
50
|
-
// // remove from client list
|
|
51
|
-
// spliceOne(self.clients, self.clients.indexOf(ws));
|
|
52
|
-
|
|
53
|
-
// const clientWrapper = self.clientWrappers.get(ws);
|
|
54
|
-
// if (clientWrapper) {
|
|
55
|
-
// self.clientWrappers.delete(ws);
|
|
56
|
-
|
|
57
|
-
// // emit 'close' on wrapper
|
|
58
|
-
// clientWrapper.emit('close', code);
|
|
59
|
-
// }
|
|
60
|
-
// },
|
|
61
|
-
// }
|
|
62
|
-
// });
|
|
63
|
-
|
|
64
|
-
// Adding a mock object for Transport.server
|
|
65
|
-
if (!this.server) {
|
|
66
|
-
// @ts-ignore
|
|
67
|
-
this.server = new HttpServerMock();
|
|
39
|
+
public getExpressApp(): Application {
|
|
40
|
+
if (!this._expressApp) {
|
|
41
|
+
this._expressApp = express();
|
|
68
42
|
}
|
|
43
|
+
return this._expressApp;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
public bindRouter(router: Router) {
|
|
47
|
+
this._router = router;
|
|
69
48
|
}
|
|
70
49
|
|
|
71
50
|
public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
this.
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
51
|
+
const self = this;
|
|
52
|
+
|
|
53
|
+
this._server = Bun.serve({
|
|
54
|
+
port,
|
|
55
|
+
hostname,
|
|
56
|
+
|
|
57
|
+
async fetch(req, server) {
|
|
58
|
+
const url = new URL(req.url);
|
|
59
|
+
|
|
60
|
+
// Try to upgrade to WebSocket
|
|
61
|
+
if (server.upgrade(req, {
|
|
62
|
+
data: {
|
|
63
|
+
url: url.pathname + url.search,
|
|
64
|
+
searchParams: url.searchParams,
|
|
65
|
+
headers: req.headers as Headers,
|
|
66
|
+
remoteAddress: server.requestIP(req)?.address || 'unknown',
|
|
67
|
+
}
|
|
68
|
+
})) {
|
|
69
|
+
return; // WebSocket upgrade successful
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Handle HTTP requests through router
|
|
73
|
+
if (self._router) {
|
|
74
|
+
try {
|
|
75
|
+
// Write CORS headers
|
|
76
|
+
const corsHeaders = {
|
|
77
|
+
...matchMaker.controller.DEFAULT_CORS_HEADERS,
|
|
78
|
+
...matchMaker.controller.getCorsHeaders(req.headers)
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
// Handle OPTIONS requests
|
|
82
|
+
if (req.method === "OPTIONS") {
|
|
83
|
+
return new Response(null, {
|
|
84
|
+
status: 204,
|
|
85
|
+
headers: corsHeaders
|
|
86
|
+
});
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
const response = await self._router.handler(req);
|
|
90
|
+
|
|
91
|
+
// Add CORS headers to response
|
|
92
|
+
const headers = new Headers(response.headers);
|
|
93
|
+
Object.entries(corsHeaders).forEach(([key, value]) => {
|
|
94
|
+
if (!headers.has(key)) {
|
|
95
|
+
headers.set(key, value.toString());
|
|
96
|
+
}
|
|
97
|
+
});
|
|
98
|
+
|
|
99
|
+
return new Response(response.body, {
|
|
100
|
+
status: response.status,
|
|
101
|
+
statusText: response.statusText,
|
|
102
|
+
headers
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
} catch (e: any) {
|
|
106
|
+
debugAndPrintError(e);
|
|
107
|
+
return new Response(JSON.stringify({
|
|
108
|
+
code: e.code,
|
|
109
|
+
error: e.message
|
|
110
|
+
}), {
|
|
111
|
+
status: 500,
|
|
112
|
+
headers: { 'Content-Type': 'application/json' }
|
|
113
|
+
});
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Fallback to express app if available
|
|
118
|
+
if (self._expressApp) {
|
|
119
|
+
// TODO: Implement express integration for Bun
|
|
120
|
+
console.warn("Express integration not yet implemented for BunWebSockets");
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return new Response("Not Found", { status: 404 });
|
|
124
|
+
},
|
|
125
|
+
|
|
126
|
+
websocket: {
|
|
127
|
+
...this.options,
|
|
128
|
+
|
|
129
|
+
async open(ws) {
|
|
130
|
+
await self.onConnection(ws);
|
|
131
|
+
},
|
|
132
|
+
|
|
133
|
+
message(ws, message) {
|
|
134
|
+
self.clientWrappers.get(ws)?.emit('message', message);
|
|
135
|
+
},
|
|
136
|
+
|
|
137
|
+
close(ws, code, reason) {
|
|
138
|
+
// remove from client list
|
|
139
|
+
spliceOne(self.clients, self.clients.indexOf(ws));
|
|
140
|
+
|
|
141
|
+
const clientWrapper = self.clientWrappers.get(ws);
|
|
142
|
+
if (clientWrapper) {
|
|
143
|
+
self.clientWrappers.delete(ws);
|
|
144
|
+
|
|
145
|
+
// emit 'close' on wrapper
|
|
146
|
+
clientWrapper.emit('close', code);
|
|
147
|
+
}
|
|
148
|
+
},
|
|
83
149
|
}
|
|
84
150
|
});
|
|
85
151
|
|
|
86
|
-
|
|
87
|
-
// @ts-ignore
|
|
88
|
-
this.server.emit("listening");
|
|
152
|
+
listeningListener?.();
|
|
89
153
|
|
|
90
154
|
return this;
|
|
91
155
|
}
|
|
92
156
|
|
|
93
157
|
public shutdown() {
|
|
94
|
-
if (this.
|
|
95
|
-
this.
|
|
96
|
-
|
|
97
|
-
// @ts-ignore
|
|
98
|
-
this.server.emit("close"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458
|
|
158
|
+
if (this._server) {
|
|
159
|
+
this._server.stop();
|
|
99
160
|
}
|
|
100
161
|
}
|
|
101
162
|
|
|
@@ -119,10 +180,11 @@ export class BunWebSockets extends Transport {
|
|
|
119
180
|
this.clients.push(rawClient);
|
|
120
181
|
this.clientWrappers.set(rawClient, wrapper);
|
|
121
182
|
|
|
122
|
-
const
|
|
183
|
+
const url = rawClient.data.url;
|
|
184
|
+
const searchParams = rawClient.data.searchParams;
|
|
123
185
|
|
|
124
|
-
const sessionId =
|
|
125
|
-
const processAndRoomId =
|
|
186
|
+
const sessionId = searchParams.get("sessionId");
|
|
187
|
+
const processAndRoomId = url.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
|
|
126
188
|
const roomId = processAndRoomId && processAndRoomId[1];
|
|
127
189
|
|
|
128
190
|
// If sessionId is not provided, allow ping-pong utility.
|
|
@@ -136,14 +198,14 @@ export class BunWebSockets extends Transport {
|
|
|
136
198
|
|
|
137
199
|
const room = matchMaker.getLocalRoomById(roomId);
|
|
138
200
|
const client = new WebSocketClient(sessionId, wrapper);
|
|
139
|
-
const reconnectionToken =
|
|
140
|
-
const skipHandshake =
|
|
201
|
+
const reconnectionToken = searchParams.get("reconnectionToken");
|
|
202
|
+
const skipHandshake = searchParams.has("skipHandshake");
|
|
141
203
|
|
|
142
204
|
try {
|
|
143
205
|
await connectClientToRoom(room, client, {
|
|
144
|
-
token:
|
|
206
|
+
token: searchParams.get("_authToken") ?? getBearerToken(rawClient.data.headers['authorization']),
|
|
145
207
|
headers: rawClient.data.headers,
|
|
146
|
-
ip: rawClient.data.headers['x-real-ip'] ?? rawClient.data.headers['x-forwarded-for'] ?? rawClient.remoteAddress,
|
|
208
|
+
ip: rawClient.data.headers['x-real-ip'] ?? rawClient.data.headers['x-forwarded-for'] ?? rawClient.data.remoteAddress,
|
|
147
209
|
}, {
|
|
148
210
|
reconnectionToken,
|
|
149
211
|
skipHandshake
|