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