@colyseus/uwebsockets-transport
Version:
<div align="center"> <a href="https://github.com/colyseus/colyseus"> <img src="media/logo.svg?raw=true" width="60%" height="300" /> </a> <br> <br> <a href="https://npmjs.com/package/colyseus"> <img src="https://img.shields.io/npm/dm/coly
243 lines (242 loc) • 8.02 kB
JavaScript
// packages/transport/uwebsockets-transport/src/uWebSocketsTransport.ts
import querystring from "querystring";
import uWebSockets from "uWebSockets.js";
import expressify from "uwebsockets-express";
import { HttpServerMock, ErrorCode, matchMaker, getBearerToken, Transport, debugAndPrintError, spliceOne } from "@colyseus/core";
import { uWebSocketClient, uWebSocketWrapper } from "./uWebSocketClient.mjs";
var uWebSocketsTransport = class extends Transport {
constructor(options = {}, appOptions = {}) {
super();
this.clients = [];
this.clientWrappers = /* @__PURE__ */ new WeakMap();
this._originalRawSend = null;
this.app = appOptions.cert_file_name && appOptions.key_file_name ? uWebSockets.SSLApp(appOptions) : uWebSockets.App(appOptions);
this.expressApp = expressify(this.app);
if (options.maxBackpressure === void 0) {
options.maxBackpressure = 1024 * 1024;
}
if (options.compression === void 0) {
options.compression = uWebSockets.DISABLED;
}
if (options.maxPayloadLength === void 0) {
options.maxPayloadLength = 4 * 1024;
}
if (options.sendPingsAutomatically === void 0) {
options.sendPingsAutomatically = true;
}
if (!this.server) {
this.server = new HttpServerMock();
}
this.app.ws("/*", {
...options,
upgrade: (res, req, context) => {
const headers = {};
req.forEach((key, value) => headers[key] = value);
res.upgrade(
{
url: req.getUrl(),
query: req.getQuery(),
context: {
token: getBearerToken(req.getHeader("authorization")),
headers,
ip: Buffer.from(res.getRemoteAddressAsText()).toString()
}
},
req.getHeader("sec-websocket-key"),
req.getHeader("sec-websocket-protocol"),
req.getHeader("sec-websocket-extensions"),
context
);
},
open: async (ws) => {
await this.onConnection(ws);
},
// pong: (ws: RawWebSocketClient) => {
// ws.pingCount = 0;
// },
close: (ws, code, message) => {
spliceOne(this.clients, this.clients.indexOf(ws));
const clientWrapper = this.clientWrappers.get(ws);
if (clientWrapper) {
this.clientWrappers.delete(ws);
clientWrapper.emit("close", code);
}
},
message: (ws, message, isBinary) => {
this.clientWrappers.get(ws)?.emit("message", Buffer.from(message));
}
});
this.registerMatchMakeRequest();
}
listen(port, hostname, backlog, listeningListener) {
const callback = (listeningSocket) => {
this._listeningSocket = listeningSocket;
listeningListener?.();
this.server.emit("listening");
};
if (typeof port === "string") {
this.app.listen_unix(callback, port);
} else {
this.app.listen(port, callback);
}
return this;
}
shutdown() {
if (this._listeningSocket) {
uWebSockets.us_listen_socket_close(this._listeningSocket);
this.server.emit("close");
}
}
simulateLatency(milliseconds) {
if (this._originalRawSend == null) {
this._originalRawSend = uWebSocketClient.prototype.raw;
}
const originalRawSend = this._originalRawSend;
uWebSocketClient.prototype.raw = milliseconds <= Number.EPSILON ? originalRawSend : function(...args) {
let [buf, ...rest] = args;
buf = Buffer.from(buf);
setTimeout(() => originalRawSend.apply(this, [buf, ...rest]), milliseconds);
};
}
async onConnection(rawClient) {
const wrapper = new uWebSocketWrapper(rawClient);
this.clients.push(rawClient);
this.clientWrappers.set(rawClient, wrapper);
const query = rawClient.query;
const url = rawClient.url;
const searchParams = querystring.parse(query);
const sessionId = searchParams.sessionId;
const processAndRoomId = url.match(/\/[a-zA-Z0-9_\-]+\/([a-zA-Z0-9_\-]+)$/);
const roomId = processAndRoomId && processAndRoomId[1];
const room = matchMaker.getLocalRoomById(roomId);
const client = new uWebSocketClient(sessionId, wrapper);
try {
if (!room || !room.hasReservedSeat(sessionId, searchParams.reconnectionToken)) {
throw new Error("seat reservation expired.");
}
await room._onJoin(client, rawClient.context);
} catch (e) {
debugAndPrintError(e);
client.error(e.code, e.message, () => client.leave());
}
}
registerMatchMakeRequest() {
const matchmakeRoute = "matchmake";
const allowedRoomNameChars = /([a-zA-Z_\-0-9]+)/gi;
const writeHeaders = (req, res) => {
if (res.aborted) {
return;
}
const headers = Object.assign(
{},
matchMaker.controller.DEFAULT_CORS_HEADERS,
matchMaker.controller.getCorsHeaders.call(void 0, req)
);
for (const header in headers) {
res.writeHeader(header, headers[header].toString());
}
return true;
};
const writeError = (res, error) => {
if (res.aborted) {
return;
}
res.cork(() => {
res.writeStatus("406 Not Acceptable");
res.end(JSON.stringify(error));
});
};
const onAborted = (res) => {
res.aborted = true;
};
this.app.options("/matchmake/*", (res, req) => {
res.onAborted(() => onAborted(res));
if (writeHeaders(req, res)) {
res.writeStatus("204 No Content");
res.end();
}
});
this.app.post("/matchmake/*", (res, req) => {
res.onAborted(() => onAborted(res));
if (matchMaker.state === matchMaker.MatchMakerState.SHUTTING_DOWN) {
return res.close();
}
writeHeaders(req, res);
res.writeHeader("Content-Type", "application/json");
const url = req.getUrl();
const matchedParams = url.match(allowedRoomNameChars);
const matchmakeIndex = matchedParams.indexOf(matchmakeRoute);
const headers = {};
req.forEach((key, value) => headers[key] = value);
const token = getBearerToken(headers["authorization"]);
this.readJson(res, async (clientOptions) => {
try {
if (clientOptions === void 0) {
throw new Error("invalid JSON input");
}
const method = matchedParams[matchmakeIndex + 1];
const roomName = matchedParams[matchmakeIndex + 2] || "";
const response = await matchMaker.controller.invokeMethod(
method,
roomName,
clientOptions,
{
token,
headers,
ip: headers["x-real-ip"] ?? Buffer.from(res.getRemoteAddressAsText()).toString()
}
);
if (!res.aborted) {
res.cork(() => {
res.writeStatus("200 OK");
res.end(JSON.stringify(response));
});
}
} catch (e) {
debugAndPrintError(e);
writeError(res, {
code: e.code || ErrorCode.MATCHMAKE_UNHANDLED,
error: e.message
});
}
});
});
}
/* Helper function for reading a posted JSON body */
/* Extracted from https://github.com/uNetworking/uWebSockets.js/blob/master/examples/JsonPost.js */
readJson(res, cb) {
let buffer;
res.onData((ab, isLast) => {
let chunk = Buffer.from(ab);
if (isLast) {
let json;
if (buffer) {
try {
json = JSON.parse(Buffer.concat([buffer, chunk]));
} catch (e) {
cb(void 0);
return;
}
cb(json);
} else {
try {
json = JSON.parse(chunk);
} catch (e) {
cb(void 0);
return;
}
cb(json);
}
} else {
if (buffer) {
buffer = Buffer.concat([buffer, chunk]);
} else {
buffer = Buffer.concat([chunk]);
}
}
});
}
};
export {
uWebSocketsTransport
};