@colyseus/bun-websockets 0.17.0 → 0.17.2
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/package.json +8 -7
- package/src/BunWebSockets.ts +160 -0
- package/src/WebSocketClient.ts +119 -0
- package/src/index.ts +2 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@colyseus/bun-websockets",
|
|
3
|
-
"version": "0.17.
|
|
3
|
+
"version": "0.17.2",
|
|
4
4
|
"type": "module",
|
|
5
5
|
"input": "./src/index.ts",
|
|
6
6
|
"main": "./build/index.js",
|
|
@@ -8,22 +8,24 @@
|
|
|
8
8
|
"typings": "./build/index.d.ts",
|
|
9
9
|
"exports": {
|
|
10
10
|
".": {
|
|
11
|
+
"@source": "./src/index.ts",
|
|
11
12
|
"types": "./build/index.d.ts",
|
|
12
|
-
"import": "./
|
|
13
|
+
"import": "./build/index.mjs",
|
|
13
14
|
"require": "./build/index.js"
|
|
14
15
|
},
|
|
15
16
|
"./*": {
|
|
17
|
+
"@source": "./src/*.ts",
|
|
16
18
|
"types": "./build/*.d.ts",
|
|
17
|
-
"import": "./
|
|
19
|
+
"import": "./build/*.mjs",
|
|
18
20
|
"require": "./build/*.js"
|
|
19
21
|
}
|
|
20
22
|
},
|
|
21
23
|
"dependencies": {
|
|
22
|
-
"@colyseus/core": "^0.17.
|
|
24
|
+
"@colyseus/core": "^0.17.2"
|
|
23
25
|
},
|
|
24
26
|
"devDependencies": {
|
|
25
27
|
"bun-types": "^1.2.0",
|
|
26
|
-
"@colyseus/core": "^0.17.
|
|
28
|
+
"@colyseus/core": "^0.17.2"
|
|
27
29
|
},
|
|
28
30
|
"author": "Endel Dreyer",
|
|
29
31
|
"license": "MIT",
|
|
@@ -35,8 +37,7 @@
|
|
|
35
37
|
],
|
|
36
38
|
"files": [
|
|
37
39
|
"build",
|
|
38
|
-
"
|
|
39
|
-
"README.md"
|
|
40
|
+
"src"
|
|
40
41
|
],
|
|
41
42
|
"repository": {
|
|
42
43
|
"type": "git",
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
// "bun-types" is currently conflicting with "ws" types.
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import { ServerWebSocket, WebSocketHandler } from 'bun';
|
|
6
|
+
|
|
7
|
+
// import bunExpress from 'bun-serve-express';
|
|
8
|
+
import type { Application, Request, Response } from "express";
|
|
9
|
+
|
|
10
|
+
import { HttpServerMock, matchMaker, Protocol, Transport, debugAndPrintError, getBearerToken, CloseCode, connectClientToRoom } from '@colyseus/core';
|
|
11
|
+
import { WebSocketClient, WebSocketWrapper } from './WebSocketClient.ts';
|
|
12
|
+
|
|
13
|
+
export type TransportOptions = Partial<Omit<WebSocketHandler, "message" | "open" | "drain" | "close" | "ping" | "pong">>;
|
|
14
|
+
|
|
15
|
+
interface WebSocketData {
|
|
16
|
+
url: URL;
|
|
17
|
+
headers: any;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
export class BunWebSockets extends Transport {
|
|
21
|
+
public expressApp: Application;
|
|
22
|
+
|
|
23
|
+
protected clients: ServerWebSocket<WebSocketData>[] = [];
|
|
24
|
+
protected clientWrappers = new WeakMap<ServerWebSocket<WebSocketData>, WebSocketWrapper>();
|
|
25
|
+
|
|
26
|
+
private _listening: any;
|
|
27
|
+
private _originalRawSend: typeof WebSocketClient.prototype.raw | null = null;
|
|
28
|
+
private options: TransportOptions = {};
|
|
29
|
+
|
|
30
|
+
constructor(options: TransportOptions = {}) {
|
|
31
|
+
super();
|
|
32
|
+
|
|
33
|
+
const self = this;
|
|
34
|
+
|
|
35
|
+
this.options = options;
|
|
36
|
+
|
|
37
|
+
// this.expressApp = bunExpress({
|
|
38
|
+
// websocket: {
|
|
39
|
+
// ...this.options,
|
|
40
|
+
|
|
41
|
+
// async open(ws) {
|
|
42
|
+
// await self.onConnection(ws);
|
|
43
|
+
// },
|
|
44
|
+
|
|
45
|
+
// message(ws, message) {
|
|
46
|
+
// self.clientWrappers.get(ws)?.emit('message', message);
|
|
47
|
+
// },
|
|
48
|
+
|
|
49
|
+
// close(ws, code, reason) {
|
|
50
|
+
// // remove from client list
|
|
51
|
+
// spliceOne(self.clients, self.clients.indexOf(ws));
|
|
52
|
+
|
|
53
|
+
// const clientWrapper = self.clientWrappers.get(ws);
|
|
54
|
+
// if (clientWrapper) {
|
|
55
|
+
// self.clientWrappers.delete(ws);
|
|
56
|
+
|
|
57
|
+
// // emit 'close' on wrapper
|
|
58
|
+
// clientWrapper.emit('close', code);
|
|
59
|
+
// }
|
|
60
|
+
// },
|
|
61
|
+
// }
|
|
62
|
+
// });
|
|
63
|
+
|
|
64
|
+
// Adding a mock object for Transport.server
|
|
65
|
+
if (!this.server) {
|
|
66
|
+
// @ts-ignore
|
|
67
|
+
this.server = new HttpServerMock();
|
|
68
|
+
}
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
public listen(port: number, hostname?: string, backlog?: number, listeningListener?: () => void) {
|
|
72
|
+
this._listening = this.expressApp.listen(port, listeningListener);
|
|
73
|
+
|
|
74
|
+
this.expressApp.use(`/${matchMaker.controller.matchmakeRoute}`, async (req, res) => {
|
|
75
|
+
try {
|
|
76
|
+
// TODO: use shared handler here
|
|
77
|
+
// await this.handleMatchMakeRequest(req, res);
|
|
78
|
+
} catch (e: any) {
|
|
79
|
+
res.status(500).json({
|
|
80
|
+
code: e.code,
|
|
81
|
+
error: e.message
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
// Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458
|
|
87
|
+
// @ts-ignore
|
|
88
|
+
this.server.emit("listening");
|
|
89
|
+
|
|
90
|
+
return this;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
public shutdown() {
|
|
94
|
+
if (this._listening) {
|
|
95
|
+
this._listening.close();
|
|
96
|
+
|
|
97
|
+
// @ts-ignore
|
|
98
|
+
this.server.emit("close"); // Mocking Transport.server behaviour, https://github.com/colyseus/colyseus/issues/458
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public simulateLatency(milliseconds: number) {
|
|
103
|
+
if (this._originalRawSend == null) {
|
|
104
|
+
this._originalRawSend = WebSocketClient.prototype.raw;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
const originalRawSend = this._originalRawSend;
|
|
108
|
+
WebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalRawSend : function (...args: any[]) {
|
|
109
|
+
let [buf, ...rest] = args;
|
|
110
|
+
buf = Buffer.from(buf);
|
|
111
|
+
// @ts-ignore
|
|
112
|
+
setTimeout(() => originalRawSend.apply(this, [buf, ...rest]), milliseconds);
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
protected async onConnection(rawClient: ServerWebSocket<WebSocketData>) {
|
|
117
|
+
const wrapper = new WebSocketWrapper(rawClient);
|
|
118
|
+
// keep reference to client and its wrapper
|
|
119
|
+
this.clients.push(rawClient);
|
|
120
|
+
this.clientWrappers.set(rawClient, wrapper);
|
|
121
|
+
|
|
122
|
+
const parsedURL = new URL(rawClient.data.url);
|
|
123
|
+
|
|
124
|
+
const sessionId = parsedURL.searchParams.get("sessionId");
|
|
125
|
+
const processAndRoomId = parsedURL.pathname.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
|
|
126
|
+
const roomId = processAndRoomId && processAndRoomId[1];
|
|
127
|
+
|
|
128
|
+
// If sessionId is not provided, allow ping-pong utility.
|
|
129
|
+
if (!sessionId && !roomId) {
|
|
130
|
+
// Disconnect automatically after 1 second if no message is received.
|
|
131
|
+
const timeout = setTimeout(() => rawClient.close(CloseCode.NORMAL_CLOSURE), 1000);
|
|
132
|
+
wrapper.on('message', (_) => rawClient.send(new Uint8Array([Protocol.PING])));
|
|
133
|
+
wrapper.on('close', () => clearTimeout(timeout));
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const room = matchMaker.getLocalRoomById(roomId);
|
|
138
|
+
const client = new WebSocketClient(sessionId, wrapper);
|
|
139
|
+
const reconnectionToken = parsedURL.searchParams.get("reconnectionToken");
|
|
140
|
+
const skipHandshake = (parsedURL.searchParams.has("skipHandshake"));
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
await connectClientToRoom(room, client, {
|
|
144
|
+
token: parsedURL.searchParams.get("_authToken") ?? getBearerToken(rawClient.data.headers['authorization']),
|
|
145
|
+
headers: rawClient.data.headers,
|
|
146
|
+
ip: rawClient.data.headers['x-real-ip'] ?? rawClient.data.headers['x-forwarded-for'] ?? rawClient.remoteAddress,
|
|
147
|
+
}, {
|
|
148
|
+
reconnectionToken,
|
|
149
|
+
skipHandshake
|
|
150
|
+
});
|
|
151
|
+
|
|
152
|
+
} catch (e: any) {
|
|
153
|
+
debugAndPrintError(e);
|
|
154
|
+
|
|
155
|
+
// send error code to client then terminate
|
|
156
|
+
client.error(e.code, e.message, () => rawClient.close());
|
|
157
|
+
}
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
// <reference types="bun-types" />
|
|
2
|
+
|
|
3
|
+
// "bun-types" is currently conflicting with "ws" types.
|
|
4
|
+
// @ts-ignore
|
|
5
|
+
import type { ServerWebSocket } from 'bun';
|
|
6
|
+
import EventEmitter from 'events';
|
|
7
|
+
|
|
8
|
+
import { Protocol, type Client, type ClientPrivate, ClientState, type ISendOptions, getMessageBytes, logger, debugMessage } from '@colyseus/core';
|
|
9
|
+
|
|
10
|
+
export class WebSocketWrapper extends EventEmitter {
|
|
11
|
+
public ws: ServerWebSocket<any>;
|
|
12
|
+
|
|
13
|
+
constructor(ws: ServerWebSocket<any>) {
|
|
14
|
+
super();
|
|
15
|
+
this.ws = ws;
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export class WebSocketClient implements Client, ClientPrivate {
|
|
20
|
+
'~messages': any;
|
|
21
|
+
|
|
22
|
+
public id: string;
|
|
23
|
+
public ref: WebSocketWrapper;
|
|
24
|
+
|
|
25
|
+
public sessionId: string;
|
|
26
|
+
public state: ClientState = ClientState.JOINING;
|
|
27
|
+
public reconnectionToken: string;
|
|
28
|
+
|
|
29
|
+
public _enqueuedMessages: any[] = [];
|
|
30
|
+
public _afterNextPatchQueue;
|
|
31
|
+
public _reconnectionToken: string;
|
|
32
|
+
public _joinedAt: number;
|
|
33
|
+
|
|
34
|
+
constructor(id: string, ref: WebSocketWrapper,) {
|
|
35
|
+
this.id = this.sessionId = id;
|
|
36
|
+
this.ref = ref;
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
public sendBytes(type: string | number, bytes: Buffer | Uint8Array, options?: ISendOptions) {
|
|
40
|
+
debugMessage("send bytes(to %s): '%s' -> %j", this.sessionId, type, bytes);
|
|
41
|
+
|
|
42
|
+
this.enqueueRaw(
|
|
43
|
+
getMessageBytes.raw(Protocol.ROOM_DATA_BYTES, type, undefined, bytes),
|
|
44
|
+
options,
|
|
45
|
+
);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
public send(messageOrType: any, messageOrOptions?: any | ISendOptions, options?: ISendOptions) {
|
|
49
|
+
debugMessage("send(to %s): '%s' -> %j", this.sessionId, messageOrType, messageOrOptions);
|
|
50
|
+
|
|
51
|
+
this.enqueueRaw(
|
|
52
|
+
getMessageBytes.raw(Protocol.ROOM_DATA, messageOrType, messageOrOptions),
|
|
53
|
+
options,
|
|
54
|
+
);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
public enqueueRaw(data: Uint8Array | Buffer, options?: ISendOptions) {
|
|
58
|
+
// use room's afterNextPatch queue
|
|
59
|
+
if (options?.afterNextPatch) {
|
|
60
|
+
this._afterNextPatchQueue.push([this, [data]]);
|
|
61
|
+
return;
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
if (this.state === ClientState.JOINING) {
|
|
65
|
+
// sending messages during `onJoin`.
|
|
66
|
+
// - the client-side cannot register "onMessage" callbacks at this point.
|
|
67
|
+
// - enqueue the messages to be send after JOIN_ROOM message has been sent
|
|
68
|
+
// - create a new buffer for enqueued messages, as the underlying buffer might be modified
|
|
69
|
+
this._enqueuedMessages.push(data);
|
|
70
|
+
return;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
this.raw(data, options);
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
public raw(data: Uint8Array | Buffer, options?: ISendOptions, cb?: (err?: Error) => void) {
|
|
77
|
+
// skip if client not open
|
|
78
|
+
|
|
79
|
+
// WebSocket is globally available on Bun runtime
|
|
80
|
+
// @ts-ignore
|
|
81
|
+
if (this.ref.ws.readyState !== WebSocket.OPEN) {
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// FIXME: can we avoid creating a new buffer here?
|
|
86
|
+
this.ref.ws.sendBinary(data);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
public error(code: number, message: string = '', cb?: (err?: Error) => void) {
|
|
90
|
+
this.raw(getMessageBytes[Protocol.ERROR](code, message));
|
|
91
|
+
|
|
92
|
+
if (cb) {
|
|
93
|
+
// (same API as "ws" transport)
|
|
94
|
+
setTimeout(cb, 1);
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
get readyState() {
|
|
99
|
+
return this.ref.ws.readyState;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
public leave(code?: number, data?: string) {
|
|
103
|
+
this.ref.ws.close(code, data);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
public close(code?: number, data?: string) {
|
|
107
|
+
logger.warn('DEPRECATION WARNING: use client.leave() instead of client.close()');
|
|
108
|
+
try {
|
|
109
|
+
throw new Error();
|
|
110
|
+
} catch (e: any) {
|
|
111
|
+
logger.info(e.stack);
|
|
112
|
+
}
|
|
113
|
+
this.leave(code, data);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
public toJSON() {
|
|
117
|
+
return { sessionId: this.sessionId, readyState: this.readyState };
|
|
118
|
+
}
|
|
119
|
+
}
|
package/src/index.ts
ADDED