@colyseus/tools 0.17.1 → 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 +10 -11
- package/src/index.ts +318 -0
- 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.
|
|
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
|
-
"
|
|
46
|
-
"
|
|
43
|
+
"src",
|
|
44
|
+
"pm2",
|
|
45
|
+
"html"
|
|
47
46
|
],
|
|
48
47
|
"homepage": "https://colyseus.io",
|
|
49
48
|
"dependencies": {
|
|
@@ -56,12 +55,12 @@
|
|
|
56
55
|
"@types/cors": "^2.8.10",
|
|
57
56
|
"@types/dotenv": "^8.2.0",
|
|
58
57
|
"uwebsockets-express": "^1.1.10",
|
|
59
|
-
"@colyseus/
|
|
60
|
-
"@colyseus/
|
|
61
|
-
"@colyseus/
|
|
62
|
-
"@colyseus/
|
|
63
|
-
"@colyseus/redis-presence": "^0.17.
|
|
64
|
-
"@colyseus/
|
|
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
66
|
"@colyseus/core": "0.17.x",
|
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
|
+
}
|