@colyseus/bun-websockets 0.15.0-alpha.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,23 @@
1
+ Copyright (c) 2023 Endel Dreyer
2
+ Copyright (c) 2021-2022 Lucid Sight
3
+ Copyright (c) 2015-2021 Endel Dreyer
4
+
5
+ MIT License:
6
+
7
+ Permission is hereby granted, free of charge, to any person obtaining a copy
8
+ of this software and associated documentation files (the "Software"), to deal
9
+ in the Software without restriction, including without limitation the rights
10
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
11
+ copies of the Software, and to permit persons to whom the Software is
12
+ furnished to do so, subject to the following conditions:
13
+
14
+ The above copyright notice and this permission notice shall be included in
15
+ all copies or substantial portions of the Software.
16
+
17
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
18
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
19
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
20
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
21
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
22
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
23
+ THE SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,28 @@
1
+ # @colyseus/ws-transport
2
+
3
+ ```typescript
4
+ import { Server } from "@colyseus/core";
5
+ import { WebSocketTransport } from "@colyseus/ws-transport";
6
+
7
+ const gameServer = new Server({
8
+ transport: new WebSocketTransport(),
9
+ // ...
10
+ })
11
+ ```
12
+
13
+ Re-using existing http server and/or Express:
14
+
15
+ ```typescript
16
+ import http from "http";
17
+ import express from "express";
18
+ import { Server } from "@colyseus/core";
19
+ import { WebSocketTransport } from "@colyseus/ws-transport";
20
+
21
+ const app = express();
22
+ const server = http.createServer(app);
23
+
24
+ const gameServer = new Server({
25
+ transport: new WebSocketTransport({ server }),
26
+ // ...
27
+ })
28
+ ```
@@ -0,0 +1,24 @@
1
+ /// <reference types="bun-types" />
2
+ /// <reference types="node" />
3
+ import Bun, { Server, ServerWebSocket, WebSocketHandler } from "bun";
4
+ import { Transport } from '@colyseus/core';
5
+ import { WebSocketWrapper } from './WebSocketClient';
6
+ export type TransportOptions = Partial<Omit<WebSocketHandler, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
7
+ interface WebSocketData {
8
+ url: URL;
9
+ }
10
+ export declare class BunWebSocket extends Transport {
11
+ private options;
12
+ bunServer: Bun.Server;
13
+ protected clients: ServerWebSocket<WebSocketData>[];
14
+ protected clientWrappers: WeakMap<Bun.ServerWebSocket<WebSocketData>, WebSocketWrapper>;
15
+ constructor(options?: TransportOptions);
16
+ listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void): this;
17
+ shutdown(): void;
18
+ simulateLatency(milliseconds: number): void;
19
+ protected onConnection(rawClient: ServerWebSocket<WebSocketData>): Promise<void>;
20
+ protected handleMatchMakeRequest(req: Request, server: Server, url: URL): Promise<[number, string, {
21
+ [key: string]: string;
22
+ }]>;
23
+ }
24
+ export {};
@@ -0,0 +1,173 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
21
+ mod
22
+ ));
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+ var BunWebSocket_exports = {};
25
+ __export(BunWebSocket_exports, {
26
+ BunWebSocket: () => BunWebSocket
27
+ });
28
+ module.exports = __toCommonJS(BunWebSocket_exports);
29
+ var import_bun = __toESM(require("bun"));
30
+ var import_core = require("@colyseus/core");
31
+ var import_WebSocketClient = require("./WebSocketClient");
32
+ class BunWebSocket extends import_core.Transport {
33
+ constructor(options = {}) {
34
+ super();
35
+ this.options = options;
36
+ if (!this.server) {
37
+ this.server = new import_core.DummyServer();
38
+ }
39
+ }
40
+ bunServer;
41
+ clients = [];
42
+ clientWrappers = /* @__PURE__ */ new WeakMap();
43
+ listen(port, hostname, backlog, listeningListener) {
44
+ const handleMatchMakeRequest = this.handleMatchMakeRequest;
45
+ this.bunServer = import_bun.default.serve({
46
+ port,
47
+ hostname,
48
+ async fetch(req, server) {
49
+ const url = new URL(req.url);
50
+ if (url.pathname.startsWith(`/${import_core.matchMaker.controller.matchmakeRoute}`)) {
51
+ try {
52
+ const [code, response, headers] = await handleMatchMakeRequest(req, server, url);
53
+ return new Response(response, {
54
+ status: code,
55
+ headers: Object.assign(
56
+ headers,
57
+ import_core.matchMaker.controller.DEFAULT_CORS_HEADERS,
58
+ import_core.matchMaker.controller.getCorsHeaders.call(void 0, req)
59
+ )
60
+ });
61
+ } catch (e) {
62
+ return new Response(JSON.stringify({ code: e.code, error: e.message }), {
63
+ status: e.code || import_core.ErrorCode.MATCHMAKE_UNHANDLED,
64
+ headers: Object.assign(
65
+ { "Content-Type": "application/json" },
66
+ import_core.matchMaker.controller.DEFAULT_CORS_HEADERS,
67
+ import_core.matchMaker.controller.getCorsHeaders.call(void 0, req)
68
+ )
69
+ });
70
+ }
71
+ } else {
72
+ server.upgrade(req, { data: { url } });
73
+ return void 0;
74
+ }
75
+ },
76
+ websocket: {
77
+ ...this.options,
78
+ async open(ws) {
79
+ await this.onConnection(ws);
80
+ },
81
+ message(ws, message) {
82
+ this.clientWrappers.get(ws)?.emit("message", message);
83
+ },
84
+ close(ws, code, reason) {
85
+ (0, import_core.spliceOne)(this.clients, this.clients.indexOf(ws));
86
+ const clientWrapper = this.clientWrappers.get(ws);
87
+ if (clientWrapper) {
88
+ this.clientWrappers.delete(ws);
89
+ clientWrapper.emit("close", code);
90
+ }
91
+ }
92
+ }
93
+ });
94
+ listeningListener?.();
95
+ this.server.emit("listening");
96
+ return this;
97
+ }
98
+ shutdown() {
99
+ if (this.bunServer) {
100
+ this.bunServer.stop(true);
101
+ this.server.emit("close");
102
+ }
103
+ }
104
+ simulateLatency(milliseconds) {
105
+ const originalRawSend = import_WebSocketClient.WebSocketClient.prototype.raw;
106
+ import_WebSocketClient.WebSocketClient.prototype.raw = function() {
107
+ setTimeout(() => originalRawSend.apply(this, arguments), milliseconds);
108
+ };
109
+ }
110
+ async onConnection(rawClient) {
111
+ const wrapper = new import_WebSocketClient.WebSocketWrapper(rawClient);
112
+ this.clients.push(rawClient);
113
+ this.clientWrappers.set(rawClient, wrapper);
114
+ const parsedURL = new URL(rawClient.data.url);
115
+ const sessionId = parsedURL.searchParams.get("sessionId");
116
+ const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
117
+ const roomId = processAndRoomId && processAndRoomId[1];
118
+ const room = import_core.matchMaker.getRoomById(roomId);
119
+ const client = new import_WebSocketClient.WebSocketClient(sessionId, wrapper);
120
+ try {
121
+ if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get("reconnectionToken"))) {
122
+ throw new Error("seat reservation expired.");
123
+ }
124
+ await room._onJoin(client, rawClient);
125
+ } catch (e) {
126
+ (0, import_core.debugAndPrintError)(e);
127
+ client.error(e.code, e.message, () => rawClient.close());
128
+ }
129
+ }
130
+ async handleMatchMakeRequest(req, server, url) {
131
+ switch (req.method) {
132
+ case "OPTIONS":
133
+ return [200, void 0, {}];
134
+ case "GET": {
135
+ const matchedParams = url.pathname.match(import_core.matchMaker.controller.allowedRoomNameChars);
136
+ const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : "";
137
+ return [
138
+ 200,
139
+ JSON.stringify(await import_core.matchMaker.controller.getAvailableRooms(roomName || "")),
140
+ {
141
+ "Content-Type": "application/json"
142
+ }
143
+ ];
144
+ }
145
+ case "POST": {
146
+ if (import_core.matchMaker.isGracefullyShuttingDown) {
147
+ throw new import_core.ServerError(503, "server is shutting down");
148
+ }
149
+ const matchedParams = url.pathname.match(import_core.matchMaker.controller.allowedRoomNameChars);
150
+ const matchmakeIndex = matchedParams.indexOf(import_core.matchMaker.controller.matchmakeRoute);
151
+ const clientOptions = import_bun.default.readableStreamToJSON(req.body);
152
+ if (clientOptions === void 0) {
153
+ throw new Error("invalid JSON input");
154
+ }
155
+ const method = matchedParams[matchmakeIndex + 1];
156
+ const roomName = matchedParams[matchmakeIndex + 2] || "";
157
+ return [
158
+ 200,
159
+ JSON.stringify(await import_core.matchMaker.controller.invokeMethod(method, roomName, clientOptions)),
160
+ {
161
+ "Content-Type": "application/json"
162
+ }
163
+ ];
164
+ }
165
+ default:
166
+ return void 0;
167
+ }
168
+ }
169
+ }
170
+ // Annotate the CommonJS export names for ESM import in node:
171
+ 0 && (module.exports = {
172
+ BunWebSocket
173
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/BunWebSocket.ts"],
4
+ "sourcesContent": ["/// <reference types=\"bun-types\" />\nimport Bun, { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\n\nimport http from 'http';\n\nimport { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n // query: string,\n // headers: { [key: string]: string },\n // connection: { remoteAddress: string },\n}\n\nexport class BunWebSocket extends Transport {\n public bunServer: Bun.Server;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n constructor(private options: TransportOptions = {}) {\n super();\n\n // Adding a mock object for Transport.server\n if (!this.server) {\n this.server = new DummyServer();\n }\n }\n\n public listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const handleMatchMakeRequest = this.handleMatchMakeRequest;\n\n this.bunServer = Bun.serve<WebSocketData>({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {\n try {\n const [code, response, headers] = await handleMatchMakeRequest(req, server, url);\n //\n // success response\n //\n return new Response(response, {\n status: code,\n headers: Object.assign(\n headers,\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n\n } catch (e) {\n //\n // error response\n //\n return new Response(JSON.stringify({ code: e.code, error: e.message }), {\n status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,\n headers: Object.assign(\n { 'Content-Type': 'application/json' },\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n }\n\n\n } else {\n // req.headers.get(\"Cookie\");\n server.upgrade(req, { data: { url } });\n\n return undefined;\n }\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await this.onConnection(ws);\n },\n\n message(ws, message) {\n // this.clientWrappers.get(ws)?.emit('message', Buffer.from(message.slice(0)));\n this.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(this.clients, this.clients.indexOf(ws));\n\n const clientWrapper = this.clientWrappers.get(ws);\n if (clientWrapper) {\n this.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n\n }\n });\n\n listeningListener?.();\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.bunServer) {\n this.bunServer.stop(true);\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 const originalRawSend = WebSocketClient.prototype.raw;\n WebSocketClient.prototype.raw = function () {\n setTimeout(() => originalRawSend.apply(this, arguments), 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 const room = matchMaker.getRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n\n //\n // TODO: DRY code below with all transports\n //\n\n try {\n if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get(\"reconnectionToken\") as string)) {\n throw new Error('seat reservation expired.');\n }\n\n await room._onJoin(client, rawClient as unknown as http.IncomingMessage);\n\n } catch (e) {\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 protected async handleMatchMakeRequest (req: Request, server: Server, url: URL): Promise<[number, string, { [key: string]: string }]> {\n switch (req.method) {\n case 'OPTIONS': return [200, undefined, {}];\n\n case 'GET': {\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : \"\";\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || '')), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n case 'POST': {\n // do not accept matchmaking requests if already shutting down\n if (matchMaker.isGracefullyShuttingDown) {\n throw new ServerError(503, \"server is shutting down\");\n }\n\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);\n\n const clientOptions = Bun.readableStreamToJSON(req.body);\n\n if (clientOptions === undefined) {\n throw new Error(\"invalid JSON input\");\n }\n\n const method = matchedParams[matchmakeIndex + 1];\n const roomName = matchedParams[matchmakeIndex + 2] || '';\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n default: return undefined;\n }\n }\n\n}\n"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AACA,iBAA+D;AAI/D,kBAA0G;AAC1G,6BAAkD;AAW3C,MAAM,qBAAqB,sBAAU;AAAA,EAM1C,YAAoB,UAA4B,CAAC,GAAG;AAClD,UAAM;AADY;AAIlB,QAAI,CAAC,KAAK,QAAQ;AAChB,WAAK,SAAS,IAAI,wBAAY;AAAA,IAChC;AAAA,EACF;AAAA,EAZO;AAAA,EAEG,UAA4C,CAAC;AAAA,EAC7C,iBAAiB,oBAAI,QAA0D;AAAA,EAWlF,OAAO,MAAuB,UAAmB,SAAkB,mBAAgC;AACxG,UAAM,yBAAyB,KAAK;AAEpC,SAAK,YAAY,WAAAA,QAAI,MAAqB;AAAA,MACxC;AAAA,MACA;AAAA,MAEA,MAAM,MAAM,KAAK,QAAQ;AACvB,cAAM,MAAM,IAAI,IAAI,IAAI,GAAG;AAE3B,YAAI,IAAI,SAAS,WAAW,IAAI,uBAAW,WAAW,gBAAgB,GAAG;AACvE,cAAI;AACF,kBAAM,CAAC,MAAM,UAAU,OAAO,IAAI,MAAM,uBAAuB,KAAK,QAAQ,GAAG;AAI/E,mBAAO,IAAI,SAAS,UAAU;AAAA,cAC5B,QAAQ;AAAA,cACR,SAAS,OAAO;AAAA,gBACd;AAAA,gBACA,uBAAW,WAAW;AAAA,gBACtB,uBAAW,WAAW,eAAe,KAAK,QAAW,GAAG;AAAA,cAC1D;AAAA,YACF,CAAC;AAAA,UAEH,SAAS,GAAP;AAIA,mBAAO,IAAI,SAAS,KAAK,UAAU,EAAE,MAAM,EAAE,MAAM,OAAO,EAAE,QAAQ,CAAC,GAAG;AAAA,cACtE,QAAQ,EAAE,QAAQ,sBAAU;AAAA,cAC5B,SAAS,OAAO;AAAA,gBACd,EAAE,gBAAgB,mBAAmB;AAAA,gBACrC,uBAAW,WAAW;AAAA,gBACtB,uBAAW,WAAW,eAAe,KAAK,QAAW,GAAG;AAAA,cAC1D;AAAA,YACF,CAAC;AAAA,UACH;AAAA,QAGF,OAAO;AAEL,iBAAO,QAAQ,KAAK,EAAE,MAAM,EAAE,IAAI,EAAE,CAAC;AAErC,iBAAO;AAAA,QACT;AAAA,MACF;AAAA,MAEA,WAAW;AAAA,QACT,GAAG,KAAK;AAAA,QAER,MAAM,KAAK,IAAI;AACb,gBAAM,KAAK,aAAa,EAAE;AAAA,QAC5B;AAAA,QAEA,QAAQ,IAAI,SAAS;AAEnB,eAAK,eAAe,IAAI,EAAE,GAAG,KAAK,WAAW,OAAO;AAAA,QACtD;AAAA,QAEA,MAAM,IAAI,MAAM,QAAQ;AAEtB,qCAAU,KAAK,SAAS,KAAK,QAAQ,QAAQ,EAAE,CAAC;AAEhD,gBAAM,gBAAgB,KAAK,eAAe,IAAI,EAAE;AAChD,cAAI,eAAe;AACjB,iBAAK,eAAe,OAAO,EAAE;AAG7B,0BAAc,KAAK,SAAS,IAAI;AAAA,UAClC;AAAA,QACF;AAAA,MAEF;AAAA,IACF,CAAC;AAED,wBAAoB;AAIpB,SAAK,OAAO,KAAK,WAAW;AAE5B,WAAO;AAAA,EACT;AAAA,EAEO,WAAW;AAChB,QAAI,KAAK,WAAW;AAClB,WAAK,UAAU,KAAK,IAAI;AAGxB,WAAK,OAAO,KAAK,OAAO;AAAA,IAC1B;AAAA,EACF;AAAA,EAEO,gBAAgB,cAAsB;AAC3C,UAAM,kBAAkB,uCAAgB,UAAU;AAClD,2CAAgB,UAAU,MAAM,WAAY;AAC1C,iBAAW,MAAM,gBAAgB,MAAM,MAAM,SAAS,GAAG,YAAY;AAAA,IACvE;AAAA,EACF;AAAA,EAEA,MAAgB,aAAa,WAA2C;AACtE,UAAM,UAAU,IAAI,wCAAiB,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;AAEpD,UAAM,OAAO,uBAAW,YAAY,MAAM;AAC1C,UAAM,SAAS,IAAI,uCAAgB,WAAW,OAAO;AAMrD,QAAI;AACF,UAAI,CAAC,QAAQ,CAAC,KAAK,gBAAgB,WAAW,UAAU,aAAa,IAAI,mBAAmB,CAAW,GAAG;AACxG,cAAM,IAAI,MAAM,2BAA2B;AAAA,MAC7C;AAEA,YAAM,KAAK,QAAQ,QAAQ,SAA4C;AAAA,IAEzE,SAAS,GAAP;AACA,0CAAmB,CAAC;AAGpB,aAAO,MAAM,EAAE,MAAM,EAAE,SAAS,MAAM,UAAU,MAAM,CAAC;AAAA,IACzD;AAAA,EACF;AAAA,EAEA,MAAgB,uBAAwB,KAAc,QAAgB,KAAgE;AACpI,YAAQ,IAAI;AAAA,WACL;AAAW,eAAO,CAAC,KAAK,QAAW,CAAC,CAAC;AAAA,WAErC,OAAO;AACV,cAAM,gBAAgB,IAAI,SAAS,MAAM,uBAAW,WAAW,oBAAoB;AACnF,cAAM,WAAW,cAAc,SAAS,IAAI,cAAc,cAAc,SAAS,KAAK;AAEtF,eAAO;AAAA,UACL;AAAA,UACA,KAAK,UAAU,MAAM,uBAAW,WAAW,kBAAkB,YAAY,EAAE,CAAC;AAAA,UAAG;AAAA,YAC7E,gBAAgB;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAAA,WAEK,QAAQ;AAEX,YAAI,uBAAW,0BAA0B;AACvC,gBAAM,IAAI,wBAAY,KAAK,yBAAyB;AAAA,QACtD;AAEA,cAAM,gBAAgB,IAAI,SAAS,MAAM,uBAAW,WAAW,oBAAoB;AACnF,cAAM,iBAAiB,cAAc,QAAQ,uBAAW,WAAW,cAAc;AAEjF,cAAM,gBAAgB,WAAAA,QAAI,qBAAqB,IAAI,IAAI;AAEvD,YAAI,kBAAkB,QAAW;AAC/B,gBAAM,IAAI,MAAM,oBAAoB;AAAA,QACtC;AAEA,cAAM,SAAS,cAAc,iBAAiB;AAC9C,cAAM,WAAW,cAAc,iBAAiB,MAAM;AAEtD,eAAO;AAAA,UACL;AAAA,UACA,KAAK,UAAU,MAAM,uBAAW,WAAW,aAAa,QAAQ,UAAU,aAAa,CAAC;AAAA,UAAG;AAAA,YACzF,gBAAgB;AAAA,UAClB;AAAA,QACF;AAAA,MACF;AAAA;AAES,eAAO;AAAA;AAAA,EAEpB;AAEF;",
6
+ "names": ["Bun"]
7
+ }
@@ -0,0 +1,144 @@
1
+ import Bun from "bun";
2
+ import { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from "@colyseus/core";
3
+ import { WebSocketClient, WebSocketWrapper } from "./WebSocketClient";
4
+ class BunWebSocket extends Transport {
5
+ constructor(options = {}) {
6
+ super();
7
+ this.options = options;
8
+ if (!this.server) {
9
+ this.server = new DummyServer();
10
+ }
11
+ }
12
+ bunServer;
13
+ clients = [];
14
+ clientWrappers = /* @__PURE__ */ new WeakMap();
15
+ listen(port, hostname, backlog, listeningListener) {
16
+ const handleMatchMakeRequest = this.handleMatchMakeRequest;
17
+ this.bunServer = Bun.serve({
18
+ port,
19
+ hostname,
20
+ async fetch(req, server) {
21
+ const url = new URL(req.url);
22
+ if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {
23
+ try {
24
+ const [code, response, headers] = await handleMatchMakeRequest(req, server, url);
25
+ return new Response(response, {
26
+ status: code,
27
+ headers: Object.assign(
28
+ headers,
29
+ matchMaker.controller.DEFAULT_CORS_HEADERS,
30
+ matchMaker.controller.getCorsHeaders.call(void 0, req)
31
+ )
32
+ });
33
+ } catch (e) {
34
+ return new Response(JSON.stringify({ code: e.code, error: e.message }), {
35
+ status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,
36
+ headers: Object.assign(
37
+ { "Content-Type": "application/json" },
38
+ matchMaker.controller.DEFAULT_CORS_HEADERS,
39
+ matchMaker.controller.getCorsHeaders.call(void 0, req)
40
+ )
41
+ });
42
+ }
43
+ } else {
44
+ server.upgrade(req, { data: { url } });
45
+ return void 0;
46
+ }
47
+ },
48
+ websocket: {
49
+ ...this.options,
50
+ async open(ws) {
51
+ await this.onConnection(ws);
52
+ },
53
+ message(ws, message) {
54
+ this.clientWrappers.get(ws)?.emit("message", message);
55
+ },
56
+ close(ws, code, reason) {
57
+ spliceOne(this.clients, this.clients.indexOf(ws));
58
+ const clientWrapper = this.clientWrappers.get(ws);
59
+ if (clientWrapper) {
60
+ this.clientWrappers.delete(ws);
61
+ clientWrapper.emit("close", code);
62
+ }
63
+ }
64
+ }
65
+ });
66
+ listeningListener?.();
67
+ this.server.emit("listening");
68
+ return this;
69
+ }
70
+ shutdown() {
71
+ if (this.bunServer) {
72
+ this.bunServer.stop(true);
73
+ this.server.emit("close");
74
+ }
75
+ }
76
+ simulateLatency(milliseconds) {
77
+ const originalRawSend = WebSocketClient.prototype.raw;
78
+ WebSocketClient.prototype.raw = function() {
79
+ setTimeout(() => originalRawSend.apply(this, arguments), milliseconds);
80
+ };
81
+ }
82
+ async onConnection(rawClient) {
83
+ const wrapper = new WebSocketWrapper(rawClient);
84
+ this.clients.push(rawClient);
85
+ this.clientWrappers.set(rawClient, wrapper);
86
+ const parsedURL = new URL(rawClient.data.url);
87
+ const sessionId = parsedURL.searchParams.get("sessionId");
88
+ const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
89
+ const roomId = processAndRoomId && processAndRoomId[1];
90
+ const room = matchMaker.getRoomById(roomId);
91
+ const client = new WebSocketClient(sessionId, wrapper);
92
+ try {
93
+ if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get("reconnectionToken"))) {
94
+ throw new Error("seat reservation expired.");
95
+ }
96
+ await room._onJoin(client, rawClient);
97
+ } catch (e) {
98
+ debugAndPrintError(e);
99
+ client.error(e.code, e.message, () => rawClient.close());
100
+ }
101
+ }
102
+ async handleMatchMakeRequest(req, server, url) {
103
+ switch (req.method) {
104
+ case "OPTIONS":
105
+ return [200, void 0, {}];
106
+ case "GET": {
107
+ const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);
108
+ const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : "";
109
+ return [
110
+ 200,
111
+ JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || "")),
112
+ {
113
+ "Content-Type": "application/json"
114
+ }
115
+ ];
116
+ }
117
+ case "POST": {
118
+ if (matchMaker.isGracefullyShuttingDown) {
119
+ throw new ServerError(503, "server is shutting down");
120
+ }
121
+ const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);
122
+ const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);
123
+ const clientOptions = Bun.readableStreamToJSON(req.body);
124
+ if (clientOptions === void 0) {
125
+ throw new Error("invalid JSON input");
126
+ }
127
+ const method = matchedParams[matchmakeIndex + 1];
128
+ const roomName = matchedParams[matchmakeIndex + 2] || "";
129
+ return [
130
+ 200,
131
+ JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)),
132
+ {
133
+ "Content-Type": "application/json"
134
+ }
135
+ ];
136
+ }
137
+ default:
138
+ return void 0;
139
+ }
140
+ }
141
+ }
142
+ export {
143
+ BunWebSocket
144
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/BunWebSocket.ts"],
4
+ "sourcesContent": ["/// <reference types=\"bun-types\" />\nimport Bun, { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\n\nimport http from 'http';\n\nimport { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n // query: string,\n // headers: { [key: string]: string },\n // connection: { remoteAddress: string },\n}\n\nexport class BunWebSocket extends Transport {\n public bunServer: Bun.Server;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n constructor(private options: TransportOptions = {}) {\n super();\n\n // Adding a mock object for Transport.server\n if (!this.server) {\n this.server = new DummyServer();\n }\n }\n\n public listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const handleMatchMakeRequest = this.handleMatchMakeRequest;\n\n this.bunServer = Bun.serve<WebSocketData>({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {\n try {\n const [code, response, headers] = await handleMatchMakeRequest(req, server, url);\n //\n // success response\n //\n return new Response(response, {\n status: code,\n headers: Object.assign(\n headers,\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n\n } catch (e) {\n //\n // error response\n //\n return new Response(JSON.stringify({ code: e.code, error: e.message }), {\n status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,\n headers: Object.assign(\n { 'Content-Type': 'application/json' },\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n }\n\n\n } else {\n // req.headers.get(\"Cookie\");\n server.upgrade(req, { data: { url } });\n\n return undefined;\n }\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await this.onConnection(ws);\n },\n\n message(ws, message) {\n // this.clientWrappers.get(ws)?.emit('message', Buffer.from(message.slice(0)));\n this.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(this.clients, this.clients.indexOf(ws));\n\n const clientWrapper = this.clientWrappers.get(ws);\n if (clientWrapper) {\n this.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n\n }\n });\n\n listeningListener?.();\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.bunServer) {\n this.bunServer.stop(true);\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 const originalRawSend = WebSocketClient.prototype.raw;\n WebSocketClient.prototype.raw = function () {\n setTimeout(() => originalRawSend.apply(this, arguments), 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 const room = matchMaker.getRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n\n //\n // TODO: DRY code below with all transports\n //\n\n try {\n if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get(\"reconnectionToken\") as string)) {\n throw new Error('seat reservation expired.');\n }\n\n await room._onJoin(client, rawClient as unknown as http.IncomingMessage);\n\n } catch (e) {\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 protected async handleMatchMakeRequest (req: Request, server: Server, url: URL): Promise<[number, string, { [key: string]: string }]> {\n switch (req.method) {\n case 'OPTIONS': return [200, undefined, {}];\n\n case 'GET': {\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : \"\";\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || '')), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n case 'POST': {\n // do not accept matchmaking requests if already shutting down\n if (matchMaker.isGracefullyShuttingDown) {\n throw new ServerError(503, \"server is shutting down\");\n }\n\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);\n\n const clientOptions = Bun.readableStreamToJSON(req.body);\n\n if (clientOptions === undefined) {\n throw new Error(\"invalid JSON input\");\n }\n\n const method = matchedParams[matchmakeIndex + 1];\n const roomName = matchedParams[matchmakeIndex + 2] || '';\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n default: return undefined;\n }\n }\n\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,30 @@
1
+ /// <reference types="bun-types" />
2
+ import { ServerWebSocket } from "bun";
3
+ import EventEmitter from 'events';
4
+ import { Client, ClientState, ISendOptions } from '@colyseus/core';
5
+ export declare class WebSocketWrapper extends EventEmitter {
6
+ ws: ServerWebSocket<any>;
7
+ constructor(ws: ServerWebSocket<any>);
8
+ }
9
+ export declare class WebSocketClient implements Client {
10
+ id: string;
11
+ ref: WebSocketWrapper;
12
+ sessionId: string;
13
+ state: ClientState;
14
+ _enqueuedMessages: any[];
15
+ _afterNextPatchQueue: any;
16
+ _reconnectionToken: string;
17
+ constructor(id: string, ref: WebSocketWrapper);
18
+ sendBytes(type: string | number, bytes: number[] | Uint8Array, options?: ISendOptions): void;
19
+ send(messageOrType: any, messageOrOptions?: any | ISendOptions, options?: ISendOptions): void;
20
+ enqueueRaw(data: ArrayLike<number>, options?: ISendOptions): void;
21
+ raw(data: ArrayLike<number>, options?: ISendOptions, cb?: (err?: Error) => void): void;
22
+ error(code: number, message?: string, cb?: (err?: Error) => void): void;
23
+ get readyState(): WebSocketReadyState;
24
+ leave(code?: number, data?: string): void;
25
+ close(code?: number, data?: string): void;
26
+ toJSON(): {
27
+ sessionId: string;
28
+ readyState: WebSocketReadyState;
29
+ };
30
+ }
@@ -0,0 +1,107 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
21
+ mod
22
+ ));
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+ var WebSocketClient_exports = {};
25
+ __export(WebSocketClient_exports, {
26
+ WebSocketClient: () => WebSocketClient,
27
+ WebSocketWrapper: () => WebSocketWrapper
28
+ });
29
+ module.exports = __toCommonJS(WebSocketClient_exports);
30
+ var import_events = __toESM(require("events"));
31
+ var import_core = require("@colyseus/core");
32
+ var import_schema = require("@colyseus/schema");
33
+ class WebSocketWrapper extends import_events.default {
34
+ constructor(ws) {
35
+ super();
36
+ this.ws = ws;
37
+ }
38
+ }
39
+ class WebSocketClient {
40
+ constructor(id, ref) {
41
+ this.id = id;
42
+ this.ref = ref;
43
+ this.sessionId = id;
44
+ }
45
+ sessionId;
46
+ state = import_core.ClientState.JOINING;
47
+ _enqueuedMessages = [];
48
+ _afterNextPatchQueue;
49
+ _reconnectionToken;
50
+ sendBytes(type, bytes, options) {
51
+ (0, import_core.debugMessage)("send bytes(to %s): '%s' -> %j", this.sessionId, type, bytes);
52
+ this.enqueueRaw(
53
+ import_core.getMessageBytes.raw(import_core.Protocol.ROOM_DATA_BYTES, type, void 0, bytes),
54
+ options
55
+ );
56
+ }
57
+ send(messageOrType, messageOrOptions, options) {
58
+ (0, import_core.debugMessage)("send(to %s): '%s' -> %j", this.sessionId, messageOrType, messageOrOptions);
59
+ this.enqueueRaw(
60
+ messageOrType instanceof import_schema.Schema ? import_core.getMessageBytes[import_core.Protocol.ROOM_DATA_SCHEMA](messageOrType) : import_core.getMessageBytes.raw(import_core.Protocol.ROOM_DATA, messageOrType, messageOrOptions),
61
+ options
62
+ );
63
+ }
64
+ enqueueRaw(data, options) {
65
+ if (options?.afterNextPatch) {
66
+ this._afterNextPatchQueue.push([this, arguments]);
67
+ return;
68
+ }
69
+ if (this.state === import_core.ClientState.JOINING) {
70
+ this._enqueuedMessages.push(data);
71
+ return;
72
+ }
73
+ this.raw(data, options);
74
+ }
75
+ raw(data, options, cb) {
76
+ if (this.ref.ws.readyState !== WebSocket.OPEN) {
77
+ return;
78
+ }
79
+ this.ref.ws.sendBinary(data);
80
+ }
81
+ error(code, message = "", cb) {
82
+ this.raw(import_core.getMessageBytes[import_core.Protocol.ERROR](code, message), void 0, cb);
83
+ }
84
+ get readyState() {
85
+ return this.ref.ws.readyState;
86
+ }
87
+ leave(code, data) {
88
+ this.ref.ws.close(code, data);
89
+ }
90
+ close(code, data) {
91
+ import_core.logger.warn("DEPRECATION WARNING: use client.leave() instead of client.close()");
92
+ try {
93
+ throw new Error();
94
+ } catch (e) {
95
+ import_core.logger.info(e.stack);
96
+ }
97
+ this.leave(code, data);
98
+ }
99
+ toJSON() {
100
+ return { sessionId: this.sessionId, readyState: this.readyState };
101
+ }
102
+ }
103
+ // Annotate the CommonJS export names for ESM import in node:
104
+ 0 && (module.exports = {
105
+ WebSocketClient,
106
+ WebSocketWrapper
107
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/WebSocketClient.ts"],
4
+ "sourcesContent": [" /// <reference types=\"bun-types\" />\nimport Bun, { ServerWebSocket } from \"bun\";\nimport EventEmitter from 'events';\n\nimport { Protocol, Client, ClientState, ISendOptions, getMessageBytes, logger, debugMessage } from '@colyseus/core';\nimport { Schema } from '@colyseus/schema';\n\nexport class WebSocketWrapper extends EventEmitter {\n constructor(public ws: ServerWebSocket<any>) {\n super();\n }\n}\n\nexport class WebSocketClient implements Client {\n public sessionId: string;\n public state: ClientState = ClientState.JOINING;\n public _enqueuedMessages: any[] = [];\n public _afterNextPatchQueue;\n public _reconnectionToken: string;\n\n constructor(\n public id: string,\n public ref: WebSocketWrapper,\n ) {\n this.sessionId = id;\n }\n\n public sendBytes(type: string | number, bytes: number[] | 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 (messageOrType instanceof Schema)\n ? getMessageBytes[Protocol.ROOM_DATA_SCHEMA](messageOrType)\n : getMessageBytes.raw(Protocol.ROOM_DATA, messageOrType, messageOrOptions),\n options,\n );\n }\n\n public enqueueRaw(data: ArrayLike<number>, options?: ISendOptions) {\n // use room's afterNextPatch queue\n if (options?.afterNextPatch) {\n this._afterNextPatchQueue.push([this, arguments]);\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 this._enqueuedMessages.push(data);\n return;\n }\n\n this.raw(data, options);\n }\n\n public raw(data: ArrayLike<number>, options?: ISendOptions, cb?: (err?: Error) => void) {\n // skip if client not open\n if (this.ref.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n\n // FIXME: is this good?\n this.ref.ws.sendBinary(data as unknown as ArrayBufferLike);\n }\n\n public error(code: number, message: string = '', cb?: (err?: Error) => void) {\n this.raw(getMessageBytes[Protocol.ERROR](code, message), undefined, cb);\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) {\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;AAEA,oBAAyB;AAEzB,kBAAmG;AACnG,oBAAuB;AAEhB,MAAM,yBAAyB,cAAAA,QAAa;AAAA,EACjD,YAAmB,IAA0B;AAC3C,UAAM;AADW;AAAA,EAEnB;AACF;AAEO,MAAM,gBAAkC;AAAA,EAO7C,YACS,IACA,KACP;AAFO;AACA;AAEP,SAAK,YAAY;AAAA,EACnB;AAAA,EAXO;AAAA,EACA,QAAqB,wBAAY;AAAA,EACjC,oBAA2B,CAAC;AAAA,EAC5B;AAAA,EACA;AAAA,EASA,UAAU,MAAuB,OAA8B,SAAwB;AAC5F,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,MACF,yBAAyB,uBACtB,4BAAgB,qBAAS,kBAAkB,aAAa,IACxD,4BAAgB,IAAI,qBAAS,WAAW,eAAe,gBAAgB;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAAA,EAEO,WAAW,MAAyB,SAAwB;AAEjE,QAAI,SAAS,gBAAgB;AAC3B,WAAK,qBAAqB,KAAK,CAAC,MAAM,SAAS,CAAC;AAChD;AAAA,IACF;AAEA,QAAI,KAAK,UAAU,wBAAY,SAAS;AAItC,WAAK,kBAAkB,KAAK,IAAI;AAChC;AAAA,IACF;AAEA,SAAK,IAAI,MAAM,OAAO;AAAA,EACxB;AAAA,EAEO,IAAI,MAAyB,SAAwB,IAA4B;AAEtF,QAAI,KAAK,IAAI,GAAG,eAAe,UAAU,MAAM;AAC7C;AAAA,IACF;AAGA,SAAK,IAAI,GAAG,WAAW,IAAkC;AAAA,EAC3D;AAAA,EAEO,MAAM,MAAc,UAAkB,IAAI,IAA4B;AAC3E,SAAK,IAAI,4BAAgB,qBAAS,OAAO,MAAM,OAAO,GAAG,QAAW,EAAE;AAAA,EACxE;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,GAAP;AACA,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
+ "names": ["EventEmitter"]
7
+ }
@@ -0,0 +1,77 @@
1
+ import EventEmitter from "events";
2
+ import { Protocol, ClientState, getMessageBytes, logger, debugMessage } from "@colyseus/core";
3
+ import { Schema } from "@colyseus/schema";
4
+ class WebSocketWrapper extends EventEmitter {
5
+ constructor(ws) {
6
+ super();
7
+ this.ws = ws;
8
+ }
9
+ }
10
+ class WebSocketClient {
11
+ constructor(id, ref) {
12
+ this.id = id;
13
+ this.ref = ref;
14
+ this.sessionId = id;
15
+ }
16
+ sessionId;
17
+ state = ClientState.JOINING;
18
+ _enqueuedMessages = [];
19
+ _afterNextPatchQueue;
20
+ _reconnectionToken;
21
+ sendBytes(type, bytes, options) {
22
+ debugMessage("send bytes(to %s): '%s' -> %j", this.sessionId, type, bytes);
23
+ this.enqueueRaw(
24
+ getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, void 0, bytes),
25
+ options
26
+ );
27
+ }
28
+ send(messageOrType, messageOrOptions, options) {
29
+ debugMessage("send(to %s): '%s' -> %j", this.sessionId, messageOrType, messageOrOptions);
30
+ this.enqueueRaw(
31
+ messageOrType instanceof Schema ? getMessageBytes[Protocol.ROOM_DATA_SCHEMA](messageOrType) : getMessageBytes.raw(Protocol.ROOM_DATA, messageOrType, messageOrOptions),
32
+ options
33
+ );
34
+ }
35
+ enqueueRaw(data, options) {
36
+ if (options?.afterNextPatch) {
37
+ this._afterNextPatchQueue.push([this, arguments]);
38
+ return;
39
+ }
40
+ if (this.state === ClientState.JOINING) {
41
+ this._enqueuedMessages.push(data);
42
+ return;
43
+ }
44
+ this.raw(data, options);
45
+ }
46
+ raw(data, options, cb) {
47
+ if (this.ref.ws.readyState !== WebSocket.OPEN) {
48
+ return;
49
+ }
50
+ this.ref.ws.sendBinary(data);
51
+ }
52
+ error(code, message = "", cb) {
53
+ this.raw(getMessageBytes[Protocol.ERROR](code, message), void 0, cb);
54
+ }
55
+ get readyState() {
56
+ return this.ref.ws.readyState;
57
+ }
58
+ leave(code, data) {
59
+ this.ref.ws.close(code, data);
60
+ }
61
+ close(code, data) {
62
+ logger.warn("DEPRECATION WARNING: use client.leave() instead of client.close()");
63
+ try {
64
+ throw new Error();
65
+ } catch (e) {
66
+ logger.info(e.stack);
67
+ }
68
+ this.leave(code, data);
69
+ }
70
+ toJSON() {
71
+ return { sessionId: this.sessionId, readyState: this.readyState };
72
+ }
73
+ }
74
+ export {
75
+ WebSocketClient,
76
+ WebSocketWrapper
77
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/WebSocketClient.ts"],
4
+ "sourcesContent": [" /// <reference types=\"bun-types\" />\nimport Bun, { ServerWebSocket } from \"bun\";\nimport EventEmitter from 'events';\n\nimport { Protocol, Client, ClientState, ISendOptions, getMessageBytes, logger, debugMessage } from '@colyseus/core';\nimport { Schema } from '@colyseus/schema';\n\nexport class WebSocketWrapper extends EventEmitter {\n constructor(public ws: ServerWebSocket<any>) {\n super();\n }\n}\n\nexport class WebSocketClient implements Client {\n public sessionId: string;\n public state: ClientState = ClientState.JOINING;\n public _enqueuedMessages: any[] = [];\n public _afterNextPatchQueue;\n public _reconnectionToken: string;\n\n constructor(\n public id: string,\n public ref: WebSocketWrapper,\n ) {\n this.sessionId = id;\n }\n\n public sendBytes(type: string | number, bytes: number[] | 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 (messageOrType instanceof Schema)\n ? getMessageBytes[Protocol.ROOM_DATA_SCHEMA](messageOrType)\n : getMessageBytes.raw(Protocol.ROOM_DATA, messageOrType, messageOrOptions),\n options,\n );\n }\n\n public enqueueRaw(data: ArrayLike<number>, options?: ISendOptions) {\n // use room's afterNextPatch queue\n if (options?.afterNextPatch) {\n this._afterNextPatchQueue.push([this, arguments]);\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 this._enqueuedMessages.push(data);\n return;\n }\n\n this.raw(data, options);\n }\n\n public raw(data: ArrayLike<number>, options?: ISendOptions, cb?: (err?: Error) => void) {\n // skip if client not open\n if (this.ref.ws.readyState !== WebSocket.OPEN) {\n return;\n }\n\n // FIXME: is this good?\n this.ref.ws.sendBinary(data as unknown as ArrayBufferLike);\n }\n\n public error(code: number, message: string = '', cb?: (err?: Error) => void) {\n this.raw(getMessageBytes[Protocol.ERROR](code, message), undefined, cb);\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) {\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": "AAEA,OAAO,kBAAkB;AAEzB,SAAS,UAAkB,aAA2B,iBAAiB,QAAQ,oBAAoB;AACnG,SAAS,cAAc;AAEhB,MAAM,yBAAyB,aAAa;AAAA,EACjD,YAAmB,IAA0B;AAC3C,UAAM;AADW;AAAA,EAEnB;AACF;AAEO,MAAM,gBAAkC;AAAA,EAO7C,YACS,IACA,KACP;AAFO;AACA;AAEP,SAAK,YAAY;AAAA,EACnB;AAAA,EAXO;AAAA,EACA,QAAqB,YAAY;AAAA,EACjC,oBAA2B,CAAC;AAAA,EAC5B;AAAA,EACA;AAAA,EASA,UAAU,MAAuB,OAA8B,SAAwB;AAC5F,iBAAa,iCAAiC,KAAK,WAAW,MAAM,KAAK;AAEzE,SAAK;AAAA,MACH,gBAAgB,IAAI,SAAS,iBAAiB,MAAM,QAAW,KAAK;AAAA,MACpE;AAAA,IACF;AAAA,EACF;AAAA,EAEO,KAAK,eAAoB,kBAAuC,SAAwB;AAC7F,iBAAa,2BAA2B,KAAK,WAAW,eAAe,gBAAgB;AAEvF,SAAK;AAAA,MACF,yBAAyB,SACtB,gBAAgB,SAAS,kBAAkB,aAAa,IACxD,gBAAgB,IAAI,SAAS,WAAW,eAAe,gBAAgB;AAAA,MAC3E;AAAA,IACF;AAAA,EACF;AAAA,EAEO,WAAW,MAAyB,SAAwB;AAEjE,QAAI,SAAS,gBAAgB;AAC3B,WAAK,qBAAqB,KAAK,CAAC,MAAM,SAAS,CAAC;AAChD;AAAA,IACF;AAEA,QAAI,KAAK,UAAU,YAAY,SAAS;AAItC,WAAK,kBAAkB,KAAK,IAAI;AAChC;AAAA,IACF;AAEA,SAAK,IAAI,MAAM,OAAO;AAAA,EACxB;AAAA,EAEO,IAAI,MAAyB,SAAwB,IAA4B;AAEtF,QAAI,KAAK,IAAI,GAAG,eAAe,UAAU,MAAM;AAC7C;AAAA,IACF;AAGA,SAAK,IAAI,GAAG,WAAW,IAAkC;AAAA,EAC3D;AAAA,EAEO,MAAM,MAAc,UAAkB,IAAI,IAA4B;AAC3E,SAAK,IAAI,gBAAgB,SAAS,OAAO,MAAM,OAAO,GAAG,QAAW,EAAE;AAAA,EACxE;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,WAAO,KAAK,mEAAmE;AAC/E,QAAI;AACF,YAAM,IAAI,MAAM;AAAA,IAClB,SAAS,GAAP;AACA,aAAO,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
+ "names": []
7
+ }
@@ -0,0 +1,24 @@
1
+ /// <reference types="bun-types" />
2
+ /// <reference types="node" />
3
+ import Bun, { Server, ServerWebSocket, WebSocketHandler } from "bun";
4
+ import { Transport } from '@colyseus/core';
5
+ import { WebSocketWrapper } from './WebSocketClient';
6
+ export type TransportOptions = Partial<Omit<WebSocketHandler, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
7
+ interface WebSocketData {
8
+ url: URL;
9
+ }
10
+ export declare class uWebSocketsTransport extends Transport {
11
+ private options;
12
+ bunServer: Bun.Server;
13
+ protected clients: ServerWebSocket<WebSocketData>[];
14
+ protected clientWrappers: WeakMap<Bun.ServerWebSocket<WebSocketData>, WebSocketWrapper>;
15
+ constructor(options?: TransportOptions);
16
+ listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void): this;
17
+ shutdown(): void;
18
+ simulateLatency(milliseconds: number): void;
19
+ protected onConnection(rawClient: ServerWebSocket<WebSocketData>): Promise<void>;
20
+ protected handleMatchMakeRequest(req: Request, server: Server, url: URL): Promise<[number, string, {
21
+ [key: string]: string;
22
+ }]>;
23
+ }
24
+ export {};
@@ -0,0 +1,173 @@
1
+ var __create = Object.create;
2
+ var __defProp = Object.defineProperty;
3
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
4
+ var __getOwnPropNames = Object.getOwnPropertyNames;
5
+ var __getProtoOf = Object.getPrototypeOf;
6
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
7
+ var __export = (target, all) => {
8
+ for (var name in all)
9
+ __defProp(target, name, { get: all[name], enumerable: true });
10
+ };
11
+ var __copyProps = (to, from, except, desc) => {
12
+ if (from && typeof from === "object" || typeof from === "function") {
13
+ for (let key of __getOwnPropNames(from))
14
+ if (!__hasOwnProp.call(to, key) && key !== except)
15
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
16
+ }
17
+ return to;
18
+ };
19
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(
20
+ isNodeMode || !mod || !mod.__esModule ? __defProp(target, "default", { value: mod, enumerable: true }) : target,
21
+ mod
22
+ ));
23
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
24
+ var WebSocketTransport_exports = {};
25
+ __export(WebSocketTransport_exports, {
26
+ uWebSocketsTransport: () => uWebSocketsTransport
27
+ });
28
+ module.exports = __toCommonJS(WebSocketTransport_exports);
29
+ var import_bun = __toESM(require("bun"));
30
+ var import_core = require("@colyseus/core");
31
+ var import_WebSocketClient = require("./WebSocketClient");
32
+ class uWebSocketsTransport extends import_core.Transport {
33
+ constructor(options = {}) {
34
+ super();
35
+ this.options = options;
36
+ if (!this.server) {
37
+ this.server = new import_core.DummyServer();
38
+ }
39
+ }
40
+ bunServer;
41
+ clients = [];
42
+ clientWrappers = /* @__PURE__ */ new WeakMap();
43
+ listen(port, hostname, backlog, listeningListener) {
44
+ const handleMatchMakeRequest = this.handleMatchMakeRequest;
45
+ this.bunServer = import_bun.default.serve({
46
+ port,
47
+ hostname,
48
+ async fetch(req, server) {
49
+ const url = new URL(req.url);
50
+ if (url.pathname.startsWith(`/${import_core.matchMaker.controller.matchmakeRoute}`)) {
51
+ try {
52
+ const [code, response, headers] = await handleMatchMakeRequest(req, server, url);
53
+ return new Response(response, {
54
+ status: code,
55
+ headers: Object.assign(
56
+ headers,
57
+ import_core.matchMaker.controller.DEFAULT_CORS_HEADERS,
58
+ import_core.matchMaker.controller.getCorsHeaders.call(void 0, req)
59
+ )
60
+ });
61
+ } catch (e) {
62
+ return new Response(JSON.stringify({ code: e.code, error: e.message }), {
63
+ status: e.code || import_core.ErrorCode.MATCHMAKE_UNHANDLED,
64
+ headers: Object.assign(
65
+ { "Content-Type": "application/json" },
66
+ import_core.matchMaker.controller.DEFAULT_CORS_HEADERS,
67
+ import_core.matchMaker.controller.getCorsHeaders.call(void 0, req)
68
+ )
69
+ });
70
+ }
71
+ } else {
72
+ server.upgrade(req, { data: { url } });
73
+ return void 0;
74
+ }
75
+ },
76
+ websocket: {
77
+ ...this.options,
78
+ async open(ws) {
79
+ await this.onConnection(ws);
80
+ },
81
+ message(ws, message) {
82
+ this.clientWrappers.get(ws)?.emit("message", message);
83
+ },
84
+ close(ws, code, reason) {
85
+ (0, import_core.spliceOne)(this.clients, this.clients.indexOf(ws));
86
+ const clientWrapper = this.clientWrappers.get(ws);
87
+ if (clientWrapper) {
88
+ this.clientWrappers.delete(ws);
89
+ clientWrapper.emit("close", code);
90
+ }
91
+ }
92
+ }
93
+ });
94
+ listeningListener?.();
95
+ this.server.emit("listening");
96
+ return this;
97
+ }
98
+ shutdown() {
99
+ if (this.bunServer) {
100
+ this.bunServer.stop(true);
101
+ this.server.emit("close");
102
+ }
103
+ }
104
+ simulateLatency(milliseconds) {
105
+ const originalRawSend = import_WebSocketClient.WebSocketClient.prototype.raw;
106
+ import_WebSocketClient.WebSocketClient.prototype.raw = function() {
107
+ setTimeout(() => originalRawSend.apply(this, arguments), milliseconds);
108
+ };
109
+ }
110
+ async onConnection(rawClient) {
111
+ const wrapper = new import_WebSocketClient.WebSocketWrapper(rawClient);
112
+ this.clients.push(rawClient);
113
+ this.clientWrappers.set(rawClient, wrapper);
114
+ const parsedURL = new URL(rawClient.data.url);
115
+ const sessionId = parsedURL.searchParams.get("sessionId");
116
+ const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
117
+ const roomId = processAndRoomId && processAndRoomId[1];
118
+ const room = import_core.matchMaker.getRoomById(roomId);
119
+ const client = new import_WebSocketClient.WebSocketClient(sessionId, wrapper);
120
+ try {
121
+ if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get("reconnectionToken"))) {
122
+ throw new Error("seat reservation expired.");
123
+ }
124
+ await room._onJoin(client, rawClient);
125
+ } catch (e) {
126
+ (0, import_core.debugAndPrintError)(e);
127
+ client.error(e.code, e.message, () => rawClient.close());
128
+ }
129
+ }
130
+ async handleMatchMakeRequest(req, server, url) {
131
+ switch (req.method) {
132
+ case "OPTIONS":
133
+ return [200, void 0, {}];
134
+ case "GET": {
135
+ const matchedParams = url.pathname.match(import_core.matchMaker.controller.allowedRoomNameChars);
136
+ const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : "";
137
+ return [
138
+ 200,
139
+ JSON.stringify(await import_core.matchMaker.controller.getAvailableRooms(roomName || "")),
140
+ {
141
+ "Content-Type": "application/json"
142
+ }
143
+ ];
144
+ }
145
+ case "POST": {
146
+ if (import_core.matchMaker.isGracefullyShuttingDown) {
147
+ throw new import_core.ServerError(503, "server is shutting down");
148
+ }
149
+ const matchedParams = url.pathname.match(import_core.matchMaker.controller.allowedRoomNameChars);
150
+ const matchmakeIndex = matchedParams.indexOf(import_core.matchMaker.controller.matchmakeRoute);
151
+ const clientOptions = import_bun.default.readableStreamToJSON(req.body);
152
+ if (clientOptions === void 0) {
153
+ throw new Error("invalid JSON input");
154
+ }
155
+ const method = matchedParams[matchmakeIndex + 1];
156
+ const roomName = matchedParams[matchmakeIndex + 2] || "";
157
+ return [
158
+ 200,
159
+ JSON.stringify(await import_core.matchMaker.controller.invokeMethod(method, roomName, clientOptions)),
160
+ {
161
+ "Content-Type": "application/json"
162
+ }
163
+ ];
164
+ }
165
+ default:
166
+ return void 0;
167
+ }
168
+ }
169
+ }
170
+ // Annotate the CommonJS export names for ESM import in node:
171
+ 0 && (module.exports = {
172
+ uWebSocketsTransport
173
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/WebSocketTransport.ts"],
4
+ "sourcesContent": ["/// <reference types=\"bun-types\" />\nimport Bun, { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\n\nimport http from 'http';\n\nimport { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n // query: string,\n // headers: { [key: string]: string },\n // connection: { remoteAddress: string },\n}\n\nexport class uWebSocketsTransport extends Transport {\n public bunServer: Bun.Server;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n constructor(private options: TransportOptions = {}) {\n super();\n\n // Adding a mock object for Transport.server\n if (!this.server) {\n this.server = new DummyServer();\n }\n }\n\n public listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const handleMatchMakeRequest = this.handleMatchMakeRequest;\n\n this.bunServer = Bun.serve<WebSocketData>({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {\n try {\n const [code, response, headers] = await handleMatchMakeRequest(req, server, url);\n //\n // success response\n //\n return new Response(response, {\n status: code,\n headers: Object.assign(\n headers,\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n\n } catch (e) {\n //\n // error response\n //\n return new Response(JSON.stringify({ code: e.code, error: e.message }), {\n status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,\n headers: Object.assign(\n { 'Content-Type': 'application/json' },\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n }\n\n\n } else {\n // req.headers.get(\"Cookie\");\n server.upgrade(req, { data: { url } });\n\n return undefined;\n }\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await this.onConnection(ws);\n },\n\n message(ws, message) {\n // this.clientWrappers.get(ws)?.emit('message', Buffer.from(message.slice(0)));\n this.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(this.clients, this.clients.indexOf(ws));\n\n const clientWrapper = this.clientWrappers.get(ws);\n if (clientWrapper) {\n this.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n\n }\n });\n\n listeningListener?.();\n this.server.emit(\"listening\"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\n\n return this;\n }\n\n public shutdown() {\n if (this.bunServer) {\n this.bunServer.stop(true);\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 const originalRawSend = WebSocketClient.prototype.raw;\n WebSocketClient.prototype.raw = function () {\n setTimeout(() => originalRawSend.apply(this, arguments), 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 const room = matchMaker.getRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n\n //\n // TODO: DRY code below with all transports\n //\n\n try {\n if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get(\"reconnectionToken\") as string)) {\n throw new Error('seat reservation expired.');\n }\n\n await room._onJoin(client, rawClient as unknown as http.IncomingMessage);\n\n } catch (e) {\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 protected async handleMatchMakeRequest (req: Request, server: Server, url: URL): Promise<[number, string, { [key: string]: string }]> {\n switch (req.method) {\n case 'OPTIONS': return [200, undefined, {}];\n\n case 'GET': {\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : \"\";\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || '')), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n case 'POST': {\n // do not accept matchmaking requests if already shutting down\n if (matchMaker.isGracefullyShuttingDown) {\n throw new ServerError(503, \"server is shutting down\");\n }\n\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);\n\n const clientOptions = Bun.readableStreamToJSON(req.body);\n\n if (clientOptions === undefined) {\n throw new Error(\"invalid JSON input\");\n }\n\n const method = matchedParams[matchmakeIndex + 1];\n const roomName = matchedParams[matchmakeIndex + 2] || '';\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n default: return undefined;\n }\n }\n\n}\n"],
5
+ "mappings": "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",
6
+ "names": ["Bun"]
7
+ }
@@ -0,0 +1,144 @@
1
+ import Bun from "bun";
2
+ import { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from "@colyseus/core";
3
+ import { WebSocketClient, WebSocketWrapper } from "./WebSocketClient";
4
+ class uWebSocketsTransport extends Transport {
5
+ constructor(options = {}) {
6
+ super();
7
+ this.options = options;
8
+ if (!this.server) {
9
+ this.server = new DummyServer();
10
+ }
11
+ }
12
+ bunServer;
13
+ clients = [];
14
+ clientWrappers = /* @__PURE__ */ new WeakMap();
15
+ listen(port, hostname, backlog, listeningListener) {
16
+ const handleMatchMakeRequest = this.handleMatchMakeRequest;
17
+ this.bunServer = Bun.serve({
18
+ port,
19
+ hostname,
20
+ async fetch(req, server) {
21
+ const url = new URL(req.url);
22
+ if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {
23
+ try {
24
+ const [code, response, headers] = await handleMatchMakeRequest(req, server, url);
25
+ return new Response(response, {
26
+ status: code,
27
+ headers: Object.assign(
28
+ headers,
29
+ matchMaker.controller.DEFAULT_CORS_HEADERS,
30
+ matchMaker.controller.getCorsHeaders.call(void 0, req)
31
+ )
32
+ });
33
+ } catch (e) {
34
+ return new Response(JSON.stringify({ code: e.code, error: e.message }), {
35
+ status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,
36
+ headers: Object.assign(
37
+ { "Content-Type": "application/json" },
38
+ matchMaker.controller.DEFAULT_CORS_HEADERS,
39
+ matchMaker.controller.getCorsHeaders.call(void 0, req)
40
+ )
41
+ });
42
+ }
43
+ } else {
44
+ server.upgrade(req, { data: { url } });
45
+ return void 0;
46
+ }
47
+ },
48
+ websocket: {
49
+ ...this.options,
50
+ async open(ws) {
51
+ await this.onConnection(ws);
52
+ },
53
+ message(ws, message) {
54
+ this.clientWrappers.get(ws)?.emit("message", message);
55
+ },
56
+ close(ws, code, reason) {
57
+ spliceOne(this.clients, this.clients.indexOf(ws));
58
+ const clientWrapper = this.clientWrappers.get(ws);
59
+ if (clientWrapper) {
60
+ this.clientWrappers.delete(ws);
61
+ clientWrapper.emit("close", code);
62
+ }
63
+ }
64
+ }
65
+ });
66
+ listeningListener?.();
67
+ this.server.emit("listening");
68
+ return this;
69
+ }
70
+ shutdown() {
71
+ if (this.bunServer) {
72
+ this.bunServer.stop(true);
73
+ this.server.emit("close");
74
+ }
75
+ }
76
+ simulateLatency(milliseconds) {
77
+ const originalRawSend = WebSocketClient.prototype.raw;
78
+ WebSocketClient.prototype.raw = function() {
79
+ setTimeout(() => originalRawSend.apply(this, arguments), milliseconds);
80
+ };
81
+ }
82
+ async onConnection(rawClient) {
83
+ const wrapper = new WebSocketWrapper(rawClient);
84
+ this.clients.push(rawClient);
85
+ this.clientWrappers.set(rawClient, wrapper);
86
+ const parsedURL = new URL(rawClient.data.url);
87
+ const sessionId = parsedURL.searchParams.get("sessionId");
88
+ const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
89
+ const roomId = processAndRoomId && processAndRoomId[1];
90
+ const room = matchMaker.getRoomById(roomId);
91
+ const client = new WebSocketClient(sessionId, wrapper);
92
+ try {
93
+ if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get("reconnectionToken"))) {
94
+ throw new Error("seat reservation expired.");
95
+ }
96
+ await room._onJoin(client, rawClient);
97
+ } catch (e) {
98
+ debugAndPrintError(e);
99
+ client.error(e.code, e.message, () => rawClient.close());
100
+ }
101
+ }
102
+ async handleMatchMakeRequest(req, server, url) {
103
+ switch (req.method) {
104
+ case "OPTIONS":
105
+ return [200, void 0, {}];
106
+ case "GET": {
107
+ const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);
108
+ const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : "";
109
+ return [
110
+ 200,
111
+ JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || "")),
112
+ {
113
+ "Content-Type": "application/json"
114
+ }
115
+ ];
116
+ }
117
+ case "POST": {
118
+ if (matchMaker.isGracefullyShuttingDown) {
119
+ throw new ServerError(503, "server is shutting down");
120
+ }
121
+ const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);
122
+ const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);
123
+ const clientOptions = Bun.readableStreamToJSON(req.body);
124
+ if (clientOptions === void 0) {
125
+ throw new Error("invalid JSON input");
126
+ }
127
+ const method = matchedParams[matchmakeIndex + 1];
128
+ const roomName = matchedParams[matchmakeIndex + 2] || "";
129
+ return [
130
+ 200,
131
+ JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)),
132
+ {
133
+ "Content-Type": "application/json"
134
+ }
135
+ ];
136
+ }
137
+ default:
138
+ return void 0;
139
+ }
140
+ }
141
+ }
142
+ export {
143
+ uWebSocketsTransport
144
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/WebSocketTransport.ts"],
4
+ "sourcesContent": ["/// <reference types=\"bun-types\" />\nimport Bun, { Server, ServerWebSocket, WebSocketHandler } from \"bun\";\n\nimport http from 'http';\n\nimport { DummyServer, ErrorCode, matchMaker, Transport, debugAndPrintError, spliceOne, ServerError } from '@colyseus/core';\nimport { WebSocketClient, WebSocketWrapper } from './WebSocketClient';\n\nexport type TransportOptions = Partial<Omit<WebSocketHandler, \"message\" | \"open\" | \"drain\" | \"close\" | \"ping\" | \"pong\">>;\n\ninterface WebSocketData {\n url: URL;\n // query: string,\n // headers: { [key: string]: string },\n // connection: { remoteAddress: string },\n}\n\nexport class uWebSocketsTransport extends Transport {\n public bunServer: Bun.Server;\n\n protected clients: ServerWebSocket<WebSocketData>[] = [];\n protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();\n\n constructor(private options: TransportOptions = {}) {\n super();\n\n // Adding a mock object for Transport.server\n if (!this.server) {\n this.server = new DummyServer();\n }\n }\n\n public listen(port: number | string, hostname?: string, backlog?: number, listeningListener?: () => void) {\n const handleMatchMakeRequest = this.handleMatchMakeRequest;\n\n this.bunServer = Bun.serve<WebSocketData>({\n port,\n hostname,\n\n async fetch(req, server) {\n const url = new URL(req.url);\n\n if (url.pathname.startsWith(`/${matchMaker.controller.matchmakeRoute}`)) {\n try {\n const [code, response, headers] = await handleMatchMakeRequest(req, server, url);\n //\n // success response\n //\n return new Response(response, {\n status: code,\n headers: Object.assign(\n headers,\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n\n } catch (e) {\n //\n // error response\n //\n return new Response(JSON.stringify({ code: e.code, error: e.message }), {\n status: e.code || ErrorCode.MATCHMAKE_UNHANDLED,\n headers: Object.assign(\n { 'Content-Type': 'application/json' },\n matchMaker.controller.DEFAULT_CORS_HEADERS,\n matchMaker.controller.getCorsHeaders.call(undefined, req)\n )\n });\n }\n\n\n } else {\n // req.headers.get(\"Cookie\");\n server.upgrade(req, { data: { url } });\n\n return undefined;\n }\n },\n\n websocket: {\n ...this.options,\n\n async open(ws) {\n await this.onConnection(ws);\n },\n\n message(ws, message) {\n // this.clientWrappers.get(ws)?.emit('message', Buffer.from(message.slice(0)));\n this.clientWrappers.get(ws)?.emit('message', message);\n },\n\n close(ws, code, reason) {\n // remove from client list\n spliceOne(this.clients, this.clients.indexOf(ws));\n\n const clientWrapper = this.clientWrappers.get(ws);\n if (clientWrapper) {\n this.clientWrappers.delete(ws);\n\n // emit 'close' on wrapper\n clientWrapper.emit('close', code);\n }\n },\n\n }\n });\n\n listeningListener?.();\n this.server.emit(\"listening\"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458\n\n return this;\n }\n\n public shutdown() {\n if (this.bunServer) {\n this.bunServer.stop(true);\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 const originalRawSend = WebSocketClient.prototype.raw;\n WebSocketClient.prototype.raw = function () {\n setTimeout(() => originalRawSend.apply(this, arguments), 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 const room = matchMaker.getRoomById(roomId);\n const client = new WebSocketClient(sessionId, wrapper);\n\n //\n // TODO: DRY code below with all transports\n //\n\n try {\n if (!room || !room.hasReservedSeat(sessionId, parsedURL.searchParams.get(\"reconnectionToken\") as string)) {\n throw new Error('seat reservation expired.');\n }\n\n await room._onJoin(client, rawClient as unknown as http.IncomingMessage);\n\n } catch (e) {\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 protected async handleMatchMakeRequest (req: Request, server: Server, url: URL): Promise<[number, string, { [key: string]: string }]> {\n switch (req.method) {\n case 'OPTIONS': return [200, undefined, {}];\n\n case 'GET': {\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const roomName = matchedParams.length > 1 ? matchedParams[matchedParams.length - 1] : \"\";\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.getAvailableRooms(roomName || '')), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n case 'POST': {\n // do not accept matchmaking requests if already shutting down\n if (matchMaker.isGracefullyShuttingDown) {\n throw new ServerError(503, \"server is shutting down\");\n }\n\n const matchedParams = url.pathname.match(matchMaker.controller.allowedRoomNameChars);\n const matchmakeIndex = matchedParams.indexOf(matchMaker.controller.matchmakeRoute);\n\n const clientOptions = Bun.readableStreamToJSON(req.body);\n\n if (clientOptions === undefined) {\n throw new Error(\"invalid JSON input\");\n }\n\n const method = matchedParams[matchmakeIndex + 1];\n const roomName = matchedParams[matchmakeIndex + 2] || '';\n\n return [\n 200,\n JSON.stringify(await matchMaker.controller.invokeMethod(method, roomName, clientOptions)), {\n 'Content-Type': 'application/json'\n }\n ];\n }\n\n default: return undefined;\n }\n }\n\n}\n"],
5
+ "mappings": "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",
6
+ "names": []
7
+ }
@@ -0,0 +1,2 @@
1
+ export { WebSocketClient } from "./WebSocketClient";
2
+ export { BunWebSocket, TransportOptions } from "./BunWebSocket";
package/build/index.js ADDED
@@ -0,0 +1,32 @@
1
+ var __defProp = Object.defineProperty;
2
+ var __getOwnPropDesc = Object.getOwnPropertyDescriptor;
3
+ var __getOwnPropNames = Object.getOwnPropertyNames;
4
+ var __hasOwnProp = Object.prototype.hasOwnProperty;
5
+ var __export = (target, all) => {
6
+ for (var name in all)
7
+ __defProp(target, name, { get: all[name], enumerable: true });
8
+ };
9
+ var __copyProps = (to, from, except, desc) => {
10
+ if (from && typeof from === "object" || typeof from === "function") {
11
+ for (let key of __getOwnPropNames(from))
12
+ if (!__hasOwnProp.call(to, key) && key !== except)
13
+ __defProp(to, key, { get: () => from[key], enumerable: !(desc = __getOwnPropDesc(from, key)) || desc.enumerable });
14
+ }
15
+ return to;
16
+ };
17
+ var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
+ var src_exports = {};
19
+ __export(src_exports, {
20
+ BunWebSocket: () => import_BunWebSocket.BunWebSocket,
21
+ TransportOptions: () => import_BunWebSocket.TransportOptions,
22
+ WebSocketClient: () => import_WebSocketClient.WebSocketClient
23
+ });
24
+ module.exports = __toCommonJS(src_exports);
25
+ var import_WebSocketClient = require("./WebSocketClient");
26
+ var import_BunWebSocket = require("./BunWebSocket");
27
+ // Annotate the CommonJS export names for ESM import in node:
28
+ 0 && (module.exports = {
29
+ BunWebSocket,
30
+ TransportOptions,
31
+ WebSocketClient
32
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export { WebSocketClient } from \"./WebSocketClient\";\nexport { BunWebSocket, TransportOptions } from \"./BunWebSocket\";"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAgC;AAChC,0BAA+C;",
6
+ "names": []
7
+ }
@@ -0,0 +1,7 @@
1
+ import { WebSocketClient } from "./WebSocketClient";
2
+ import { BunWebSocket, TransportOptions } from "./BunWebSocket";
3
+ export {
4
+ BunWebSocket,
5
+ TransportOptions,
6
+ WebSocketClient
7
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/index.ts"],
4
+ "sourcesContent": ["export { WebSocketClient } from \"./WebSocketClient\";\nexport { BunWebSocket, TransportOptions } from \"./BunWebSocket\";"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,cAAc,wBAAwB;",
6
+ "names": []
7
+ }
package/package.json ADDED
@@ -0,0 +1,39 @@
1
+ {
2
+ "name": "@colyseus/bun-websockets",
3
+ "version": "0.15.0-alpha.0",
4
+ "input": "./src/index.ts",
5
+ "main": "./build/index.js",
6
+ "module": "./build/index.mjs",
7
+ "typings": "./build/index.d.ts",
8
+ "dependencies": {
9
+ "@colyseus/core": "^0.15.0"
10
+ },
11
+ "devDependencies": {
12
+ "bun-types": "^1.0.1"
13
+ },
14
+ "peerDependencies": {
15
+ "@colyseus/schema": ">=1.0.0"
16
+ },
17
+ "author": "Endel Dreyer",
18
+ "license": "MIT",
19
+ "keywords": [
20
+ "colyseus",
21
+ "transport",
22
+ "websockets",
23
+ "bun"
24
+ ],
25
+ "files": [
26
+ "build",
27
+ "LICENSE",
28
+ "README.md"
29
+ ],
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git://github.com/colyseus/colyseus.git"
33
+ },
34
+ "homepage": "https://colyseus.io/",
35
+ "publishConfig": {
36
+ "access": "public"
37
+ },
38
+ "gitHead": "699947e11bea29a19c1f866c8e07330b7c9cacd3"
39
+ }