@colyseus/bun-websockets 0.15.0-alpha.0 → 0.15.0-alpha.1

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.
@@ -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 BunWebSockets 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 BunWebSockets_exports = {};
25
+ __export(BunWebSockets_exports, {
26
+ BunWebSockets: () => BunWebSockets
27
+ });
28
+ module.exports = __toCommonJS(BunWebSockets_exports);
29
+ var import_bun = __toESM(require("bun"));
30
+ var import_core = require("@colyseus/core");
31
+ var import_WebSocketClient = require("./WebSocketClient");
32
+ class BunWebSockets 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 self = this;
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 self.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 self.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)(self.clients, self.clients.indexOf(ws));
86
+ const clientWrapper = self.clientWrappers.get(ws);
87
+ if (clientWrapper) {
88
+ self.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
+ BunWebSockets
173
+ });
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/BunWebSockets.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 BunWebSockets 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 self = this;\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 self.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 self.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(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\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": ["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 BunWebSockets 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 self = this;
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 self.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 self.onConnection(ws);
52
+ },
53
+ message(ws, message) {
54
+ this.clientWrappers.get(ws)?.emit("message", message);
55
+ },
56
+ close(ws, code, reason) {
57
+ spliceOne(self.clients, self.clients.indexOf(ws));
58
+ const clientWrapper = self.clientWrappers.get(ws);
59
+ if (clientWrapper) {
60
+ self.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
+ BunWebSockets
144
+ };
@@ -0,0 +1,7 @@
1
+ {
2
+ "version": 3,
3
+ "sources": ["../src/BunWebSockets.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 BunWebSockets 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 self = this;\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 self.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 self.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(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\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
+ }
package/build/index.d.ts CHANGED
@@ -1,2 +1,2 @@
1
1
  export { WebSocketClient } from "./WebSocketClient";
2
- export { BunWebSocket, TransportOptions } from "./BunWebSocket";
2
+ export { BunWebSockets, TransportOptions } from "./BunWebSockets";
package/build/index.js CHANGED
@@ -17,16 +17,16 @@ var __copyProps = (to, from, except, desc) => {
17
17
  var __toCommonJS = (mod) => __copyProps(__defProp({}, "__esModule", { value: true }), mod);
18
18
  var src_exports = {};
19
19
  __export(src_exports, {
20
- BunWebSocket: () => import_BunWebSocket.BunWebSocket,
21
- TransportOptions: () => import_BunWebSocket.TransportOptions,
20
+ BunWebSockets: () => import_BunWebSockets.BunWebSockets,
21
+ TransportOptions: () => import_BunWebSockets.TransportOptions,
22
22
  WebSocketClient: () => import_WebSocketClient.WebSocketClient
23
23
  });
24
24
  module.exports = __toCommonJS(src_exports);
25
25
  var import_WebSocketClient = require("./WebSocketClient");
26
- var import_BunWebSocket = require("./BunWebSocket");
26
+ var import_BunWebSockets = require("./BunWebSockets");
27
27
  // Annotate the CommonJS export names for ESM import in node:
28
28
  0 && (module.exports = {
29
- BunWebSocket,
29
+ BunWebSockets,
30
30
  TransportOptions,
31
31
  WebSocketClient
32
32
  });
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
4
+ "sourcesContent": ["export { WebSocketClient } from \"./WebSocketClient\";\nexport { BunWebSockets, TransportOptions } from \"./BunWebSockets\";"],
5
+ "mappings": ";;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA;AAAA,6BAAgC;AAChC,2BAAgD;",
6
6
  "names": []
7
7
  }
package/build/index.mjs CHANGED
@@ -1,7 +1,7 @@
1
1
  import { WebSocketClient } from "./WebSocketClient";
2
- import { BunWebSocket, TransportOptions } from "./BunWebSocket";
2
+ import { BunWebSockets, TransportOptions } from "./BunWebSockets";
3
3
  export {
4
- BunWebSocket,
4
+ BunWebSockets,
5
5
  TransportOptions,
6
6
  WebSocketClient
7
7
  };
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "version": 3,
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;",
4
+ "sourcesContent": ["export { WebSocketClient } from \"./WebSocketClient\";\nexport { BunWebSockets, TransportOptions } from \"./BunWebSockets\";"],
5
+ "mappings": "AAAA,SAAS,uBAAuB;AAChC,SAAS,eAAe,wBAAwB;",
6
6
  "names": []
7
7
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/bun-websockets",
3
- "version": "0.15.0-alpha.0",
3
+ "version": "0.15.0-alpha.1",
4
4
  "input": "./src/index.ts",
5
5
  "main": "./build/index.js",
6
6
  "module": "./build/index.mjs",
@@ -35,5 +35,5 @@
35
35
  "publishConfig": {
36
36
  "access": "public"
37
37
  },
38
- "gitHead": "699947e11bea29a19c1f866c8e07330b7c9cacd3"
38
+ "gitHead": "a602d77919d9e76d32a6ad3a663161bad7919b6f"
39
39
  }