@colyseus/tools 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.
Files changed (3) hide show
  1. package/package.json +12 -13
  2. package/src/index.ts +318 -0
  3. package/src/loadenv.ts +67 -0
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@colyseus/tools",
3
- "version": "0.17.0",
3
+ "version": "0.17.2",
4
4
  "type": "module",
5
5
  "description": "Simplify the development and production settings for your Colyseus project.",
6
6
  "input": "./src/index.ts",
@@ -39,11 +39,10 @@
39
39
  "url": "https://github.com/colyseus/colyseus/issues"
40
40
  },
41
41
  "files": [
42
- "pm2",
43
- "html",
44
42
  "build",
45
- "LICENSE",
46
- "README.md"
43
+ "src",
44
+ "pm2",
45
+ "html"
47
46
  ],
48
47
  "homepage": "https://colyseus.io",
49
48
  "dependencies": {
@@ -56,16 +55,16 @@
56
55
  "@types/cors": "^2.8.10",
57
56
  "@types/dotenv": "^8.2.0",
58
57
  "uwebsockets-express": "^1.1.10",
59
- "@colyseus/ws-transport": "^0.17.0",
60
- "@colyseus/core": "^0.17.0",
61
- "@colyseus/bun-websockets": "^0.17.0",
62
- "@colyseus/redis-presence": "^0.17.0",
63
- "@colyseus/uwebsockets-transport": "^0.17.0",
64
- "@colyseus/redis-driver": "^0.17.0"
58
+ "@colyseus/core": "^0.17.1",
59
+ "@colyseus/ws-transport": "^0.17.2",
60
+ "@colyseus/bun-websockets": "^0.17.1",
61
+ "@colyseus/redis-driver": "^0.17.1",
62
+ "@colyseus/redis-presence": "^0.17.1",
63
+ "@colyseus/uwebsockets-transport": "^0.17.2"
65
64
  },
66
65
  "peerDependencies": {
67
- "@colyseus/core": "0.16.x",
68
- "@colyseus/ws-transport": "0.16.x"
66
+ "@colyseus/core": "0.17.x",
67
+ "@colyseus/ws-transport": "0.17.x"
69
68
  },
70
69
  "publishConfig": {
71
70
  "access": "public"
package/src/index.ts ADDED
@@ -0,0 +1,318 @@
1
+ import './loadenv.ts';
2
+ import os from 'os';
3
+ import fs from "fs";
4
+ import net from "net";
5
+ import http from 'http';
6
+ import cors from 'cors';
7
+ import express from 'express';
8
+ import {
9
+ type ServerOptions,
10
+ type SDKTypes,
11
+ type Router,
12
+ logger,
13
+ Server,
14
+ Transport,
15
+ matchMaker,
16
+ RegisteredHandler,
17
+ defineServer,
18
+ LocalDriver,
19
+ LocalPresence
20
+ } from '@colyseus/core';
21
+ import { WebSocketTransport } from '@colyseus/ws-transport';
22
+
23
+ const BunWebSockets = import('@colyseus/bun-websockets'); BunWebSockets.catch(() => {});
24
+ const RedisDriver = import('@colyseus/redis-driver'); RedisDriver.catch(() => {});
25
+ const RedisPresence = import('@colyseus/redis-presence'); RedisPresence.catch(() => {});
26
+
27
+ export interface ConfigOptions<
28
+ RoomTypes extends Record<string, RegisteredHandler> = any,
29
+ Routes extends Router = any
30
+ > extends SDKTypes<RoomTypes, Routes> {
31
+ options?: ServerOptions,
32
+ displayLogs?: boolean,
33
+ rooms?: RoomTypes,
34
+ routes?: Routes,
35
+ initializeTransport?: (options: any) => Transport,
36
+ initializeExpress?: (app: express.Express) => void,
37
+ initializeGameServer?: (app: Server) => void,
38
+ beforeListen?: () => void,
39
+ /**
40
+ * @deprecated getId() has no effect anymore.
41
+ */
42
+ getId?: () => string,
43
+ }
44
+
45
+ const ALLOWED_KEYS: { [key in keyof Partial<ConfigOptions>]: string } = {
46
+ 'displayLogs': "boolean",
47
+ 'options': "object",
48
+ 'rooms': "object",
49
+ 'routes': "object",
50
+ 'initializeTransport': "function",
51
+ 'initializeExpress': "function",
52
+ 'initializeGameServer': "function",
53
+ 'beforeListen': "function",
54
+ // deprecated options (will be removed in the next major version)
55
+ 'getId': "function",
56
+ };
57
+
58
+ export default function <
59
+ RoomTypes extends Record<string, RegisteredHandler> = any,
60
+ Routes extends Router = any
61
+ >(options: Omit<ConfigOptions<RoomTypes, Routes>, '~rooms' | '~routes'>) {
62
+ for (const option in options) {
63
+ if (!ALLOWED_KEYS[option]) {
64
+ throw new Error(`❌ Invalid option '${option}'. Allowed options are: ${Object.keys(ALLOWED_KEYS).join(", ")}`);
65
+ }
66
+ if(options[option] !== undefined && typeof(options[option]) !== ALLOWED_KEYS[option]) {
67
+ throw new Error(`❌ Invalid type for ${option}: please provide a ${ALLOWED_KEYS[option]} value.`);
68
+ }
69
+ }
70
+ return options as ConfigOptions<RoomTypes, Routes>;
71
+ }
72
+
73
+ /**
74
+ * Expose server instance and listen on the port specified
75
+ * @param options Application options
76
+ * @param port Port number to bind Colyseus + Express
77
+ */
78
+ export async function listen<
79
+ RoomTypes extends Record<string, RegisteredHandler> = any,
80
+ Routes extends Router = any
81
+ >(
82
+ options: ConfigOptions<RoomTypes, Routes>,
83
+ port?: number,
84
+ ): Promise<Server<RoomTypes, Routes>>;
85
+
86
+ /**
87
+ * Expose server instance and listen on the port specified
88
+ * @param server Server instance
89
+ * @param port Port number to bind Colyseus + Express
90
+ */
91
+ export async function listen<
92
+ RoomTypes extends Record<string, RegisteredHandler> = any,
93
+ Routes extends Router = any
94
+ >(
95
+ server: Server<RoomTypes, Routes>,
96
+ port?: number,
97
+ ): Promise<Server<RoomTypes, Routes>>;
98
+
99
+ export async function listen<
100
+ RoomTypes extends Record<string, RegisteredHandler> = any,
101
+ Routes extends Router = any
102
+ >(
103
+ options: ConfigOptions<RoomTypes, Routes> | Server<RoomTypes, Routes>,
104
+ port: number = Number(process.env.PORT || 2567),
105
+ ) {
106
+ // Force 2567 port on Colyseus Cloud
107
+ if (process.env.COLYSEUS_CLOUD !== undefined) {
108
+ port = 2567;
109
+ }
110
+
111
+ //
112
+ // Handling multiple processes
113
+ // Use NODE_APP_INSTANCE to play nicely with pm2
114
+ //
115
+ const processNumber = Number(process.env.NODE_APP_INSTANCE || "0");
116
+ port += processNumber;
117
+
118
+ let server: Server<RoomTypes, Routes>;
119
+ let displayLogs = true;
120
+
121
+ if (options instanceof Server) {
122
+ server = options;
123
+
124
+ // automatically configure for production under Colyseus Cloud
125
+ if (process.env.COLYSEUS_CLOUD !== undefined) {
126
+ // check if local driver/presence are being used (defaults)
127
+ const isLocalDriver = matchMaker.driver instanceof LocalDriver;
128
+ const isLocalPresence = matchMaker.presence instanceof LocalPresence;
129
+
130
+ const cloudConfig = await getColyseusCloudConfig(
131
+ port,
132
+ isLocalDriver ? undefined : matchMaker.driver,
133
+ isLocalPresence ? undefined : matchMaker.presence,
134
+ );
135
+
136
+ // re-setup matchMaker with Redis driver/presence
137
+ if (cloudConfig && (isLocalDriver || isLocalPresence)) {
138
+ await matchMaker.setup(cloudConfig.presence, cloudConfig.driver, cloudConfig.publicAddress);
139
+ }
140
+ }
141
+
142
+ } else {
143
+ server = await buildServerFromOptions<RoomTypes, Routes>(options, port);
144
+ displayLogs = options.displayLogs;
145
+
146
+ await options.initializeGameServer?.(server);
147
+ await matchMaker.onReady;
148
+ await options.beforeListen?.();
149
+ }
150
+
151
+ if (process.env.COLYSEUS_CLOUD !== undefined) {
152
+ // listening on socket
153
+ // const socketPath: any = `/run/colyseus/${port}.sock`;
154
+ const socketPath: any = `/tmp/${port}.sock`;
155
+
156
+ // check if .sock file is active
157
+ // (fixes "ADDRINUSE" issue when restarting the server)
158
+ await checkInactiveSocketFile(socketPath);
159
+
160
+ await server.listen(socketPath);
161
+
162
+ } else {
163
+ // listening on port
164
+ await server.listen(port);
165
+ }
166
+
167
+ // notify process manager (production)
168
+ if (typeof(process.send) === "function") {
169
+ process.send('ready');
170
+ }
171
+
172
+ if (displayLogs) {
173
+ logger.info(`⚔️ Listening on http://localhost:${port}`);
174
+ }
175
+
176
+ return server;
177
+ }
178
+
179
+ async function buildServerFromOptions<
180
+ RoomTypes extends Record<string, RegisteredHandler> = any,
181
+ Routes extends Router = any
182
+ >(options: ConfigOptions<RoomTypes, Routes>, port: number) {
183
+ const serverOptions = options.options || {};
184
+ options.displayLogs = options.displayLogs ?? true;
185
+
186
+ // automatically configure for production under Colyseus Cloud
187
+ if (process.env.COLYSEUS_CLOUD !== undefined) {
188
+ const cloudConfig = await getColyseusCloudConfig(port, serverOptions.driver, serverOptions.presence);
189
+ if (cloudConfig) {
190
+ serverOptions.driver = cloudConfig.driver;
191
+ serverOptions.presence = cloudConfig.presence;
192
+ serverOptions.publicAddress = cloudConfig.publicAddress;
193
+ }
194
+ }
195
+
196
+ return defineServer<RoomTypes, Routes>(options.rooms || {} as RoomTypes, options.routes, {
197
+ ...serverOptions,
198
+ transport: await getTransport(options),
199
+ });
200
+ }
201
+
202
+ export async function getTransport(options: ConfigOptions) {
203
+ let transport: Transport;
204
+
205
+ if (!options.initializeTransport) {
206
+ // @ts-ignore
207
+ if (typeof Bun !== "undefined") {
208
+ // @colyseus/bun-websockets
209
+ BunWebSockets.catch(() => {
210
+ logger.warn("");
211
+ logger.warn("❌ could not initialize BunWebSockets.");
212
+ logger.warn("👉 npm install --save @colyseus/bun-websockets");
213
+ logger.warn("");
214
+ })
215
+ const module = await BunWebSockets;
216
+ options.initializeTransport = (options: any) => new module.BunWebSockets(options);
217
+
218
+ } else {
219
+ // use WebSocketTransport by default
220
+ options.initializeTransport = (options: any) => new WebSocketTransport(options);
221
+ }
222
+ }
223
+
224
+ let app: express.Express | undefined = express();
225
+ let server = http.createServer(app);
226
+
227
+ transport = await options.initializeTransport({ server, app });
228
+
229
+ //
230
+ // TODO: refactor me!
231
+ // BunWebSockets: There's no need to instantiate "app" and "server" above
232
+ //
233
+ if (transport['expressApp']) {
234
+ app = transport['expressApp'];
235
+ }
236
+
237
+ if (app) {
238
+ // Enable CORS
239
+ app.use(cors({ origin: true, credentials: true, }));
240
+
241
+ if (options.initializeExpress) {
242
+ await options.initializeExpress(app);
243
+ }
244
+
245
+ // health check for load balancers
246
+ app.get("/__healthcheck", (req, res) => {
247
+ res.status(200).end();
248
+ });
249
+
250
+ if (options.displayLogs) {
251
+ logger.info("✅ Express initialized");
252
+ }
253
+ }
254
+
255
+ return transport;
256
+ }
257
+
258
+ /**
259
+ * Configure Redis driver/presence for Colyseus Cloud when needed.
260
+ * Returns configured driver, presence, and publicAddress.
261
+ */
262
+ async function getColyseusCloudConfig(port: number, currentDriver?: any, currentPresence?: any) {
263
+ const useRedisConfig = (os.cpus().length > 1) || (process.env.REDIS_URI !== undefined);
264
+
265
+ if (!useRedisConfig) {
266
+ return null;
267
+ }
268
+
269
+ let driver = currentDriver;
270
+ let presence = currentPresence;
271
+ const publicAddress = process.env.SUBDOMAIN + "." + process.env.SERVER_NAME + "/" + port;
272
+
273
+ if (!driver) {
274
+ try {
275
+ const module = await RedisDriver;
276
+ driver = new module.RedisDriver(process.env.REDIS_URI);
277
+ } catch (e) {
278
+ console.error(e);
279
+ logger.warn("");
280
+ logger.warn("❌ could not initialize RedisDriver.");
281
+ logger.warn("👉 npm install --save @colyseus/redis-driver");
282
+ logger.warn("");
283
+ }
284
+ }
285
+
286
+ if (!presence) {
287
+ try {
288
+ const module = await RedisPresence;
289
+ presence = new module.RedisPresence(process.env.REDIS_URI);
290
+ } catch (e) {
291
+ console.error(e);
292
+ logger.warn("");
293
+ logger.warn("❌ could not initialize RedisPresence.");
294
+ logger.warn("👉 npm install --save @colyseus/redis-presence");
295
+ logger.warn("");
296
+ }
297
+ }
298
+
299
+ return { driver, presence, publicAddress };
300
+ }
301
+
302
+ /**
303
+ * Check if a socket file is active and remove it if it's not.
304
+ */
305
+ function checkInactiveSocketFile(sockFilePath: string) {
306
+ return new Promise((resolve, reject) => {
307
+ const client = net.createConnection({ path: sockFilePath })
308
+ .on('connect', () => {
309
+ // socket file is active, close the connection
310
+ client.end();
311
+ throw new Error(`EADDRINUSE: Already listening on '${sockFilePath}'`);
312
+ })
313
+ .on('error', () => {
314
+ // socket file is inactive, remove it
315
+ fs.unlink(sockFilePath, () => resolve(true));
316
+ });
317
+ });
318
+ }
package/src/loadenv.ts ADDED
@@ -0,0 +1,67 @@
1
+ import fs from 'fs';
2
+ import path from 'path';
3
+ import dotenv from 'dotenv';
4
+
5
+ function getEnvFromArgv() {
6
+ const envIndex = process.argv.indexOf("--env");
7
+ return (envIndex !== -1) ? process.argv[envIndex + 1] : undefined;
8
+ }
9
+
10
+ function getNodeEnv() {
11
+ return process.env.NODE_ENV || getEnvFromArgv() || "development";
12
+ }
13
+
14
+ function getRegion() {
15
+ // EU, NA, AS, AF, AU, SA, UNKNOWN
16
+ return (process.env.REGION || "unknown").toLowerCase();
17
+ }
18
+
19
+ function loadEnvFile(envFileOptions: string[], log: 'none' | 'success' | 'both' = 'none') {
20
+ const envPaths = [];
21
+ envFileOptions.forEach((envFilename) => {
22
+ if (envFilename.startsWith("/")) {
23
+ envPaths.push(envFilename);
24
+ } else {
25
+ envPaths.push(path.resolve(path.dirname(typeof(require) !== "undefined" && require?.main?.filename || process.cwd()), "..", envFilename));
26
+ envPaths.push(path.resolve(process.cwd(), envFilename));
27
+ }
28
+ });
29
+
30
+ // return the first .env path found
31
+ const envPath = envPaths.find((envPath) => fs.existsSync(envPath));
32
+
33
+ if (envPath) {
34
+ dotenv.config({ path: envPath });
35
+
36
+ if (log !== "none") {
37
+ console.info(`✅ ${path.basename(envPath)} loaded.`);
38
+ }
39
+
40
+ } else if (log === "both") {
41
+ console.info(`ℹ️ optional .env file not found: ${envFileOptions.join(", ")}`);
42
+ }
43
+ }
44
+
45
+ // reload /etc/environment, if exists
46
+ if (fs.existsSync("/etc/environment")) {
47
+ dotenv.config({ path: "/etc/environment", override: true })
48
+ }
49
+
50
+ // load .env.cloud defined on admin panel
51
+ if (process.env.COLYSEUS_CLOUD !== undefined) {
52
+ const cloudEnvFileNames = [".env.cloud"];
53
+
54
+ // prepend .env.cloud file from APP_ROOT_PATH
55
+ if (process.env.APP_ROOT_PATH) {
56
+ cloudEnvFileNames.unshift(`${process.env.APP_ROOT_PATH}${(process.env.APP_ROOT_PATH.endsWith("/") ? "" : "/")}.env.cloud`);
57
+ }
58
+
59
+ loadEnvFile(cloudEnvFileNames);
60
+ }
61
+
62
+ // (overrides previous env configs)
63
+ loadEnvFile([`.env.${getNodeEnv()}`, `.env`], 'both');
64
+
65
+ if (process.env.REGION !== undefined) {
66
+ loadEnvFile([`.env.${getRegion()}.${getNodeEnv()}`], 'success');
67
+ }