@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/bun-websockets",
3
- "version": "0.17.0",
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": "./src/index.ts",
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": "./src/*.ts",
19
+ "import": "./build/*.mjs",
18
20
  "require": "./build/*.js"
19
21
  }
20
22
  },
21
23
  "dependencies": {
22
- "@colyseus/core": "^0.17.0"
24
+ "@colyseus/core": "^0.17.2"
23
25
  },
24
26
  "devDependencies": {
25
27
  "bun-types": "^1.2.0",
26
- "@colyseus/core": "^0.17.0"
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
- "LICENSE",
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
@@ -0,0 +1,2 @@
1
+ export { WebSocketClient } from './WebSocketClient.ts';
2
+ export { BunWebSockets, type TransportOptions } from './BunWebSockets.ts';