@colyseus/bun-websockets 0.17.4 → 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.
@@ -16,6 +16,8 @@ var __copyProps = (to, from, except, desc) => {
16
16
  return to;
17
17
  };
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/transport/bun-websockets/src/BunWebSockets.ts
19
21
  var BunWebSockets_exports = {};
20
22
  __export(BunWebSockets_exports, {
21
23
  BunWebSockets: () => BunWebSockets
@@ -23,38 +25,109 @@ __export(BunWebSockets_exports, {
23
25
  module.exports = __toCommonJS(BunWebSockets_exports);
24
26
  var import_bun = require("bun");
25
27
  var import_core = require("@colyseus/core");
26
- var import_WebSocketClient = require("./WebSocketClient.ts");
27
- class BunWebSockets extends import_core.Transport {
28
+ var import_WebSocketClient = require("./WebSocketClient.cjs");
29
+ var BunWebSockets = class extends import_core.Transport {
28
30
  constructor(options = {}) {
29
31
  super();
30
32
  this.clients = [];
31
33
  this.clientWrappers = /* @__PURE__ */ new WeakMap();
32
34
  this._originalRawSend = null;
33
35
  this.options = {};
34
- const self = this;
35
36
  this.options = options;
36
- if (!this.server) {
37
- this.server = new import_core.HttpServerMock();
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("");
38
44
  }
45
+ return this._expressApp;
46
+ }
47
+ bindRouter(router) {
48
+ this._router = router;
39
49
  }
40
50
  listen(port, hostname, backlog, listeningListener) {
41
- this._listening = this.expressApp.listen(port, listeningListener);
42
- this.expressApp.use(`/${import_core.matchMaker.controller.matchmakeRoute}`, async (req, res) => {
43
- try {
44
- } catch (e) {
45
- res.status(500).json({
46
- code: e.code,
47
- error: e.message
48
- });
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
+ }
49
123
  }
50
124
  });
51
- this.server.emit("listening");
125
+ listeningListener?.();
52
126
  return this;
53
127
  }
54
128
  shutdown() {
55
- if (this._listening) {
56
- this._listening.close();
57
- this.server.emit("close");
129
+ if (this._server) {
130
+ this._server.stop();
58
131
  }
59
132
  }
60
133
  simulateLatency(milliseconds) {
@@ -72,9 +145,10 @@ class BunWebSockets extends import_core.Transport {
72
145
  const wrapper = new import_WebSocketClient.WebSocketWrapper(rawClient);
73
146
  this.clients.push(rawClient);
74
147
  this.clientWrappers.set(rawClient, wrapper);
75
- const parsedURL = new URL(rawClient.data.url);
76
- const sessionId = parsedURL.searchParams.get("sessionId");
77
- const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
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_\-]+)$/);
78
152
  const roomId = processAndRoomId && processAndRoomId[1];
79
153
  if (!sessionId && !roomId) {
80
154
  const timeout = setTimeout(() => rawClient.close(import_core.CloseCode.NORMAL_CLOSURE), 1e3);
@@ -84,13 +158,13 @@ class BunWebSockets extends import_core.Transport {
84
158
  }
85
159
  const room = import_core.matchMaker.getLocalRoomById(roomId);
86
160
  const client = new import_WebSocketClient.WebSocketClient(sessionId, wrapper);
87
- const reconnectionToken = parsedURL.searchParams.get("reconnectionToken");
88
- const skipHandshake = parsedURL.searchParams.has("skipHandshake");
161
+ const reconnectionToken = searchParams.get("reconnectionToken");
162
+ const skipHandshake = searchParams.has("skipHandshake");
89
163
  try {
90
164
  await (0, import_core.connectClientToRoom)(room, client, {
91
- token: parsedURL.searchParams.get("_authToken") ?? (0, import_core.getBearerToken)(rawClient.data.headers["authorization"]),
165
+ token: searchParams.get("_authToken") ?? (0, import_core.getBearerToken)(rawClient.data.headers["authorization"]),
92
166
  headers: rawClient.data.headers,
93
- 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
94
168
  }, {
95
169
  reconnectionToken,
96
170
  skipHandshake
@@ -100,7 +174,7 @@ class BunWebSockets extends import_core.Transport {
100
174
  client.error(e.code, e.message, () => rawClient.close());
101
175
  }
102
176
  }
103
- }
177
+ };
104
178
  // Annotate the CommonJS export names for ESM import in node:
105
179
  0 && (module.exports = {
106
180
  BunWebSockets
@@ -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';\n\n// import bunExpress from 'bun-serve-express';\nimport type { Application, Request, Response } from \"express\";\n\nimport { HttpServerMock, matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n headers: any;\n}\n\nexport class BunWebSockets extends Transport {\n public expressApp: Application;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n private _listening: any;\n private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;\n private options: TransportOptions = {};\n\n constructor(options: TransportOptions = {}) {\n super();\n\n const self = this;\n\n this.options = options;\n\n // this.expressApp = bunExpress({\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 // Adding a mock object for Transport.server\n if (!this.server) {\n // @ts-ignore\n this.server = new HttpServerMock();\n }\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n this._listening = this.expressApp.listen(port, listeningListener);\n\n this.expressApp.use(`/${matchMaker.controller.matchmakeRoute}`, async (req, res) => {\n try {\n // TODO: use shared handler here\n // await this.handleMatchMakeRequest(req, res);\n } catch (e: any) {\n res.status(500).json({\n code: e.code,\n error: e.message\n });\n }\n });\n\n // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\n // @ts-ignore\n this.server.emit(\"listening\");\n\n return this;\n }\n\n public shutdown() {\n if (this._listening) {\n this._listening.close();\n\n // @ts-ignore\n this.server.emit(\"close\"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\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 parsedURL = new URL(rawClient.data.url);\n\n const sessionId = parsedURL.searchParams.get(\"sessionId\");\n const processAndRoomId = parsedURL.pathname.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 = parsedURL.searchParams.get(\"reconnectionToken\");\n const skipHandshake = (parsedURL.searchParams.has(\"skipHandshake\"));\n\n try {\n await connectClientToRoom(room, client, {\n token: parsedURL.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.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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",
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
  }
@@ -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, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
6
+ export type TransportOptions = Partial<Omit<WebSocketHandler<WebSocketData>, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
6
7
  interface WebSocketData {
7
- url: URL;
8
- headers: any;
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 _listening;
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;
@@ -1,6 +1,6 @@
1
1
  // packages/transport/bun-websockets/src/BunWebSockets.ts
2
2
  import "bun";
3
- import { HttpServerMock, matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom } from "@colyseus/core";
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
- if (!this.server) {
15
- this.server = new HttpServerMock();
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
- this._listening = this.expressApp.listen(port, listeningListener);
20
- this.expressApp.use(`/${matchMaker.controller.matchmakeRoute}`, async (req, res) => {
21
- try {
22
- } catch (e) {
23
- res.status(500).json({
24
- code: e.code,
25
- error: e.message
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
- this.server.emit("listening");
101
+ listeningListener?.();
30
102
  return this;
31
103
  }
32
104
  shutdown() {
33
- if (this._listening) {
34
- this._listening.close();
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 parsedURL = new URL(rawClient.data.url);
54
- const sessionId = parsedURL.searchParams.get("sessionId");
55
- const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
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 = parsedURL.searchParams.get("reconnectionToken");
66
- const skipHandshake = parsedURL.searchParams.has("skipHandshake");
137
+ const reconnectionToken = searchParams.get("reconnectionToken");
138
+ const skipHandshake = searchParams.has("skipHandshake");
67
139
  try {
68
140
  await connectClientToRoom(room, client, {
69
- token: parsedURL.searchParams.get("_authToken") ?? getBearerToken(rawClient.data.headers["authorization"]),
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';\n\n// import bunExpress from 'bun-serve-express';\nimport type { Application, Request, Response } from \"express\";\n\nimport { HttpServerMock, matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n headers: any;\n}\n\nexport class BunWebSockets extends Transport {\n public expressApp: Application;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n private _listening: any;\n private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;\n private options: TransportOptions = {};\n\n constructor(options: TransportOptions = {}) {\n super();\n\n const self = this;\n\n this.options = options;\n\n // this.expressApp = bunExpress({\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 // Adding a mock object for Transport.server\n if (!this.server) {\n // @ts-ignore\n this.server = new HttpServerMock();\n }\n }\n\n public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {\n this._listening = this.expressApp.listen(port, listeningListener);\n\n this.expressApp.use(`/${matchMaker.controller.matchmakeRoute}`, async (req, res) => {\n try {\n // TODO: use shared handler here\n // await this.handleMatchMakeRequest(req, res);\n } catch (e: any) {\n res.status(500).json({\n code: e.code,\n error: e.message\n });\n }\n });\n\n // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\n // @ts-ignore\n this.server.emit(\"listening\");\n\n return this;\n }\n\n public shutdown() {\n if (this._listening) {\n this._listening.close();\n\n // @ts-ignore\n this.server.emit(\"close\"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\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 parsedURL = new URL(rawClient.data.url);\n\n const sessionId = parsedURL.searchParams.get(\"sessionId\");\n const processAndRoomId = parsedURL.pathname.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 = parsedURL.searchParams.get(\"reconnectionToken\");\n const skipHandshake = (parsedURL.searchParams.has(\"skipHandshake\"));\n\n try {\n await connectClientToRoom(room, client, {\n token: parsedURL.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.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": ";AAIA,OAAkD;AAKlD,SAAS,gBAAgB,YAAY,UAAU,WAAW,oBAAoB,gBAAgB,WAAW,2BAA2B;AACpI,SAAS,iBAAiB,wBAAwB;AAS3C,IAAM,gBAAN,cAA4B,UAAU;AAAA,EAU3C,YAAY,UAA4B,CAAC,GAAG;AAC1C,UAAM;AARR,SAAU,UAA4C,CAAC;AACvD,SAAU,iBAAiB,oBAAI,QAA0D;AAGzF,SAAQ,mBAAgE;AACxE,SAAQ,UAA4B,CAAC;AAKnC,UAAM,OAAO;AAEb,SAAK,UAAU;AA8Bf,QAAI,CAAC,KAAK,QAAQ;AAEhB,WAAK,SAAS,IAAI,eAAe;AAAA,IACnC;AAAA,EACF;AAAA,EAEO,OAAO,MAAc,UAAmB,SAAkB,mBAAgC;AAC/F,SAAK,aAAa,KAAK,WAAW,OAAO,MAAM,iBAAiB;AAEhE,SAAK,WAAW,IAAI,IAAI,WAAW,WAAW,cAAc,IAAI,OAAO,KAAK,QAAQ;AAClF,UAAI;AAAA,MAGJ,SAAS,GAAQ;AACf,YAAI,OAAO,GAAG,EAAE,KAAK;AAAA,UACnB,MAAM,EAAE;AAAA,UACR,OAAO,EAAE;AAAA,QACX,CAAC;AAAA,MACH;AAAA,IACF,CAAC;AAID,SAAK,OAAO,KAAK,WAAW;AAE5B,WAAO;AAAA,EACT;AAAA,EAEO,WAAW;AAChB,QAAI,KAAK,YAAY;AACnB,WAAK,WAAW,MAAM;AAGtB,WAAK,OAAO,KAAK,OAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEO,gBAAgB,cAAsB;AAC3C,QAAI,KAAK,oBAAoB,MAAM;AACjC,WAAK,mBAAmB,gBAAgB,UAAU;AAAA,IACpD;AAEA,UAAM,kBAAkB,KAAK;AAC7B,oBAAgB,UAAU,MAAM,gBAAgB,OAAO,UAAU,kBAAkB,YAAa,MAAa;AAC3G,UAAI,CAAC,KAAK,GAAG,IAAI,IAAI;AACrB,YAAM,OAAO,KAAK,GAAG;AAErB,iBAAW,MAAM,gBAAgB,MAAM,MAAM,CAAC,KAAK,GAAG,IAAI,CAAC,GAAG,YAAY;AAAA,IAC5E;AAAA,EACF;AAAA,EAEA,MAAgB,aAAa,WAA2C;AACtE,UAAM,UAAU,IAAI,iBAAiB,SAAS;AAE9C,SAAK,QAAQ,KAAK,SAAS;AAC3B,SAAK,eAAe,IAAI,WAAW,OAAO;AAE1C,UAAM,YAAY,IAAI,IAAI,UAAU,KAAK,GAAG;AAE5C,UAAM,YAAY,UAAU,aAAa,IAAI,WAAW;AACxD,UAAM,mBAAmB,UAAU,SAAS,MAAM,uCAAuC;AACzF,UAAM,SAAS,oBAAoB,iBAAiB,CAAC;AAGrD,QAAI,CAAC,aAAa,CAAC,QAAQ;AAEzB,YAAM,UAAU,WAAW,MAAM,UAAU,MAAM,UAAU,cAAc,GAAG,GAAI;AAChF,cAAQ,GAAG,WAAW,CAAC,MAAM,UAAU,KAAK,IAAI,WAAW,CAAC,SAAS,IAAI,CAAC,CAAC,CAAC;AAC5E,cAAQ,GAAG,SAAS,MAAM,aAAa,OAAO,CAAC;AAC/C;AAAA,IACF;AAEA,UAAM,OAAO,WAAW,iBAAiB,MAAM;AAC/C,UAAM,SAAS,IAAI,gBAAgB,WAAW,OAAO;AACrD,UAAM,oBAAoB,UAAU,aAAa,IAAI,mBAAmB;AACxE,UAAM,gBAAiB,UAAU,aAAa,IAAI,eAAe;AAEjE,QAAI;AACF,YAAM,oBAAoB,MAAM,QAAQ;AAAA,QACtC,OAAO,UAAU,aAAa,IAAI,YAAY,KAAK,eAAe,UAAU,KAAK,QAAQ,eAAe,CAAC;AAAA,QACzG,SAAS,UAAU,KAAK;AAAA,QACxB,IAAI,UAAU,KAAK,QAAQ,WAAW,KAAK,UAAU,KAAK,QAAQ,iBAAiB,KAAK,UAAU;AAAA,MACpG,GAAG;AAAA,QACD;AAAA,QACA;AAAA,MACF,CAAC;AAAA,IAEH,SAAS,GAAQ;AACf,yBAAmB,CAAC;AAGpB,aAAO,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,UAAU,MAAM,CAAC;AAAA,IACzD;AAAA,EACF;AAEF;",
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
  }
@@ -26,6 +26,8 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
26
26
  mod
27
27
  ));
28
28
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
29
+
30
+ // packages/transport/bun-websockets/src/WebSocketClient.ts
29
31
  var WebSocketClient_exports = {};
30
32
  __export(WebSocketClient_exports, {
31
33
  WebSocketClient: () => WebSocketClient,
@@ -34,13 +36,13 @@ __export(WebSocketClient_exports, {
34
36
  module.exports = __toCommonJS(WebSocketClient_exports);
35
37
  var import_events = __toESM(require("events"), 1);
36
38
  var import_core = require("@colyseus/core");
37
- class WebSocketWrapper extends import_events.default {
39
+ var WebSocketWrapper = class extends import_events.default {
38
40
  constructor(ws) {
39
41
  super();
40
42
  this.ws = ws;
41
43
  }
42
- }
43
- class WebSocketClient {
44
+ };
45
+ var WebSocketClient = class {
44
46
  constructor(id, ref) {
45
47
  this.state = import_core.ClientState.JOINING;
46
48
  this._enqueuedMessages = [];
@@ -102,7 +104,7 @@ class WebSocketClient {
102
104
  toJSON() {
103
105
  return { sessionId: this.sessionId, readyState: this.readyState };
104
106
  }
105
- }
107
+ };
106
108
  // Annotate the CommonJS export names for ESM import in node:
107
109
  0 && (module.exports = {
108
110
  WebSocketClient,
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/WebSocketClient.ts"],
4
4
  "sourcesContent": ["// <reference types=\"bun-types\" />\n\n// \"bun-types\" is currently conflicting with \"ws\" types.\n// @ts-ignore\nimport type { ServerWebSocket } from 'bun';\nimport EventEmitter from 'events';\n\nimport { Protocol, type Client, type ClientPrivate, ClientState, type ISendOptions, getMessageBytes, logger, debugMessage } from '@colyseus/core';\n\nexport class WebSocketWrapper extends EventEmitter {\n public ws: ServerWebSocket<any>;\n\n constructor(ws: ServerWebSocket<any>) {\n super();\n this.ws = ws;\n }\n}\n\nexport class WebSocketClient implements Client, ClientPrivate {\n '~messages': any;\n\n public id: string;\n public ref: WebSocketWrapper;\n\n public sessionId: string;\n public state: ClientState = ClientState.JOINING;\n public reconnectionToken: string;\n\n public _enqueuedMessages: any[] = [];\n public _afterNextPatchQueue;\n public _reconnectionToken: string;\n public _joinedAt: number;\n\n constructor(id: string, ref: WebSocketWrapper,) {\n this.id = this.sessionId = id;\n this.ref = ref;\n }\n\n public sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions) {\n debugMessage(\"send bytes(to %s): '%s' -> %j\", this.sessionId, type, bytes);\n\n this.enqueueRaw(\n getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, undefined, bytes),\n options,\n );\n }\n\n public send(messageOrType: any, messageOrOptions?: any | ISendOptions, options?: ISendOptions) {\n debugMessage(\"send(to %s): '%s' -> %j\", this.sessionId, messageOrType, messageOrOptions);\n\n this.enqueueRaw(\n getMessageBytes.raw(Protocol.ROOM_DATA, messageOrType, messageOrOptions),\n options,\n );\n }\n\n public enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions) {\n // use room's afterNextPatch queue\n if (options?.afterNextPatch) {\n this._afterNextPatchQueue.push([this, [data]]);\n return;\n }\n\n if (this.state === ClientState.JOINING) {\n // sending messages during `onJoin`.\n // - the client-side cannot register \"onMessage\" callbacks at this point.\n // - enqueue the messages to be send after JOIN_ROOM message has been sent\n // - create a new buffer for enqueued messages, as the underlying buffer might be modified\n this._enqueuedMessages.push(data);\n return;\n }\n\n this.raw(data, options);\n }\n\n public raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void) {\n // skip if client not open\n\n // WebSocket is globally available on Bun runtime\n // @ts-ignore\n if (this.ref.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n\n // FIXME: can we avoid creating a new buffer here?\n this.ref.ws.sendBinary(data);\n }\n\n public error(code: number, message: string = '', cb?: (err?: Error) => void) {\n this.raw(getMessageBytes[Protocol.ERROR](code, message));\n\n if (cb) {\n // (same API as \"ws\" transport)\n setTimeout(cb, 1);\n }\n }\n\n get readyState() {\n return this.ref.ws.readyState;\n }\n\n public leave(code?: number, data?: string) {\n this.ref.ws.close(code, data);\n }\n\n public close(code?: number, data?: string) {\n logger.warn('DEPRECATION WARNING: use client.leave() instead of client.close()');\n try {\n throw new Error();\n } catch (e: any) {\n logger.info(e.stack);\n }\n this.leave(code, data);\n }\n\n public toJSON() {\n return { sessionId: this.sessionId, readyState: this.readyState };\n }\n}\n"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,oBAAyB;AAEzB,kBAAiI;AAE1H,MAAM,yBAAyB,cAAAA,QAAa;AAAA,EAGjD,YAAY,IAA0B;AACpC,UAAM;AACN,SAAK,KAAK;AAAA,EACZ;AACF;AAEO,MAAM,gBAAiD;AAAA,EAe5D,YAAY,IAAY,KAAwB;AARhD,SAAO,QAAqB,wBAAY;AAGxC,SAAO,oBAA2B,CAAC;AAMjC,SAAK,KAAK,KAAK,YAAY;AAC3B,SAAK,MAAM;AAAA,EACb;AAAA,EAEO,UAAU,MAAuB,OAA4B,SAAwB;AAC1F,kCAAa,iCAAiC,KAAK,WAAW,MAAM,KAAK;AAEzE,SAAK;AAAA,MACH,4BAAgB,IAAI,qBAAS,iBAAiB,MAAM,QAAW,KAAK;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAAA,EAEO,KAAK,eAAoB,kBAAuC,SAAwB;AAC7F,kCAAa,2BAA2B,KAAK,WAAW,eAAe,gBAAgB;AAEvF,SAAK;AAAA,MACH,4BAAgB,IAAI,qBAAS,WAAW,eAAe,gBAAgB;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA,EAEO,WAAW,MAA2B,SAAwB;AAEnE,QAAI,SAAS,gBAAgB;AAC3B,WAAK,qBAAqB,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7C;AAAA,IACF;AAEA,QAAI,KAAK,UAAU,wBAAY,SAAS;AAKtC,WAAK,kBAAkB,KAAK,IAAI;AAChC;AAAA,IACF;AAEA,SAAK,IAAI,MAAM,OAAO;AAAA,EACxB;AAAA,EAEO,IAAI,MAA2B,SAAwB,IAA4B;AAKxF,QAAI,KAAK,IAAI,GAAG,eAAe,UAAU,MAAM;AAC7C;AAAA,IACF;AAGA,SAAK,IAAI,GAAG,WAAW,IAAI;AAAA,EAC7B;AAAA,EAEO,MAAM,MAAc,UAAkB,IAAI,IAA4B;AAC3E,SAAK,IAAI,4BAAgB,qBAAS,KAAK,EAAE,MAAM,OAAO,CAAC;AAEvD,QAAI,IAAI;AAEN,iBAAW,IAAI,CAAC;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,IAAI,aAAa;AACf,WAAO,KAAK,IAAI,GAAG;AAAA,EACrB;AAAA,EAEO,MAAM,MAAe,MAAe;AACzC,SAAK,IAAI,GAAG,MAAM,MAAM,IAAI;AAAA,EAC9B;AAAA,EAEO,MAAM,MAAe,MAAe;AACzC,uBAAO,KAAK,mEAAmE;AAC/E,QAAI;AACF,YAAM,IAAI,MAAM;AAAA,IAClB,SAAS,GAAQ;AACf,yBAAO,KAAK,EAAE,KAAK;AAAA,IACrB;AACA,SAAK,MAAM,MAAM,IAAI;AAAA,EACvB;AAAA,EAEO,SAAS;AACd,WAAO,EAAE,WAAW,KAAK,WAAW,YAAY,KAAK,WAAW;AAAA,EAClE;AACF;",
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAKA,oBAAyB;AAEzB,kBAAiI;AAE1H,IAAM,mBAAN,cAA+B,cAAAA,QAAa;AAAA,EAGjD,YAAY,IAA0B;AACpC,UAAM;AACN,SAAK,KAAK;AAAA,EACZ;AACF;AAEO,IAAM,kBAAN,MAAuD;AAAA,EAe5D,YAAY,IAAY,KAAwB;AARhD,SAAO,QAAqB,wBAAY;AAGxC,SAAO,oBAA2B,CAAC;AAMjC,SAAK,KAAK,KAAK,YAAY;AAC3B,SAAK,MAAM;AAAA,EACb;AAAA,EAEO,UAAU,MAAuB,OAA4B,SAAwB;AAC1F,kCAAa,iCAAiC,KAAK,WAAW,MAAM,KAAK;AAEzE,SAAK;AAAA,MACH,4BAAgB,IAAI,qBAAS,iBAAiB,MAAM,QAAW,KAAK;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAAA,EAEO,KAAK,eAAoB,kBAAuC,SAAwB;AAC7F,kCAAa,2BAA2B,KAAK,WAAW,eAAe,gBAAgB;AAEvF,SAAK;AAAA,MACH,4BAAgB,IAAI,qBAAS,WAAW,eAAe,gBAAgB;AAAA,MACvE;AAAA,IACF;AAAA,EACF;AAAA,EAEO,WAAW,MAA2B,SAAwB;AAEnE,QAAI,SAAS,gBAAgB;AAC3B,WAAK,qBAAqB,KAAK,CAAC,MAAM,CAAC,IAAI,CAAC,CAAC;AAC7C;AAAA,IACF;AAEA,QAAI,KAAK,UAAU,wBAAY,SAAS;AAKtC,WAAK,kBAAkB,KAAK,IAAI;AAChC;AAAA,IACF;AAEA,SAAK,IAAI,MAAM,OAAO;AAAA,EACxB;AAAA,EAEO,IAAI,MAA2B,SAAwB,IAA4B;AAKxF,QAAI,KAAK,IAAI,GAAG,eAAe,UAAU,MAAM;AAC7C;AAAA,IACF;AAGA,SAAK,IAAI,GAAG,WAAW,IAAI;AAAA,EAC7B;AAAA,EAEO,MAAM,MAAc,UAAkB,IAAI,IAA4B;AAC3E,SAAK,IAAI,4BAAgB,qBAAS,KAAK,EAAE,MAAM,OAAO,CAAC;AAEvD,QAAI,IAAI;AAEN,iBAAW,IAAI,CAAC;AAAA,IAClB;AAAA,EACF;AAAA,EAEA,IAAI,aAAa;AACf,WAAO,KAAK,IAAI,GAAG;AAAA,EACrB;AAAA,EAEO,MAAM,MAAe,MAAe;AACzC,SAAK,IAAI,GAAG,MAAM,MAAM,IAAI;AAAA,EAC9B;AAAA,EAEO,MAAM,MAAe,MAAe;AACzC,uBAAO,KAAK,mEAAmE;AAC/E,QAAI;AACF,YAAM,IAAI,MAAM;AAAA,IAClB,SAAS,GAAQ;AACf,yBAAO,KAAK,EAAE,KAAK;AAAA,IACrB;AACA,SAAK,MAAM,MAAM,IAAI;AAAA,EACvB;AAAA,EAEO,SAAS;AACd,WAAO,EAAE,WAAW,KAAK,WAAW,YAAY,KAAK,WAAW;AAAA,EAClE;AACF;",
6
6
  "names": ["EventEmitter"]
7
7
  }
package/build/index.cjs CHANGED
@@ -16,14 +16,16 @@ var __copyProps = (to, from, except, desc) => {
16
16
  return to;
17
17
  };
18
18
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
19
+
20
+ // packages/transport/bun-websockets/src/index.ts
19
21
  var index_exports = {};
20
22
  __export(index_exports, {
21
23
  BunWebSockets: () => import_BunWebSockets.BunWebSockets,
22
24
  WebSocketClient: () => import_WebSocketClient.WebSocketClient
23
25
  });
24
26
  module.exports = __toCommonJS(index_exports);
25
- var import_WebSocketClient = require("./WebSocketClient.ts");
26
- var import_BunWebSockets = require("./BunWebSockets.ts");
27
+ var import_WebSocketClient = require("./WebSocketClient.cjs");
28
+ var import_BunWebSockets = require("./BunWebSockets.cjs");
27
29
  // Annotate the CommonJS export names for ESM import in node:
28
30
  0 && (module.exports = {
29
31
  BunWebSockets,
@@ -2,6 +2,6 @@
2
2
  "version": 3,
3
3
  "sources": ["../src/index.ts"],
4
4
  "sourcesContent": ["export { WebSocketClient } from './WebSocketClient.ts';\nexport { BunWebSockets, type TransportOptions } from './BunWebSockets.ts';"],
5
- "mappings": ";;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAgC;AAChC,2BAAqD;",
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAgC;AAChC,2BAAqD;",
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.4",
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.4"
24
+ "@colyseus/core": "^0.17.22"
25
25
  },
26
26
  "devDependencies": {
27
27
  "bun-types": "^1.2.0",
28
- "@colyseus/core": "^0.17.4"
28
+ "@colyseus/core": "^0.17.22"
29
29
  },
30
30
  "author": "Endel Dreyer",
31
31
  "license": "MIT",
@@ -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
- // import bunExpress from 'bun-serve-express';
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
- export type TransportOptions = Partial<Omit<WebSocketHandler, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
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: URL;
17
- headers: any;
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 _listening: any;
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
- // this.expressApp = bunExpress({
38
- // websocket: {
39
- // ...this.options,
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
- this._listening = this.expressApp.listen(port, listeningListener);
73
-
74
- this.expressApp.use(`/${matchMaker.controller.matchmakeRoute}`, async (req, res) => {
75
- try {
76
- // TODO: use shared handler here
77
- // await this.handleMatchMakeRequest(req, res);
78
- } catch (e: any) {
79
- res.status(500).json({
80
- code: e.code,
81
- error: e.message
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
- // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458
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._listening) {
95
- this._listening.close();
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 parsedURL = new URL(rawClient.data.url);
183
+ const url = rawClient.data.url;
184
+ const searchParams = rawClient.data.searchParams;
123
185
 
124
- const sessionId = parsedURL.searchParams.get("sessionId");
125
- const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
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 = parsedURL.searchParams.get("reconnectionToken");
140
- const skipHandshake = (parsedURL.searchParams.has("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: parsedURL.searchParams.get("_authToken") ?? getBearerToken(rawClient.data.headers['authorization']),
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