@jsonjoy.com/reactive-rpc
Version:
Reactive-RPC is a library for building reactive APIs over WebSocket, HTTP, and other RPCs.
248 lines • 9.94 kB
JavaScript
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.Http1Server = exports.Http1EndpointMatch = void 0;
const tslib_1 = require("tslib");
const http = tslib_1.__importStar(require("http"));
const https = tslib_1.__importStar(require("https"));
const Writer_1 = require("@jsonjoy.com/util/lib/buffers/Writer");
const Codecs_1 = require("@jsonjoy.com/json-pack/lib/codecs/Codecs");
const WsServerConnection_1 = require("../ws/server/WsServerConnection");
const WsFrameEncoder_1 = require("../ws/codec/WsFrameEncoder");
const jit_router_1 = require("@jsonjoy.com/jit-router");
const printTree_1 = require("sonic-forest/lib/print/printTree");
const errors_1 = require("../errors");
const util_1 = require("./util");
const util_2 = require("../util");
const context_1 = require("./context");
const RpcCodecs_1 = require("../../common/codec/RpcCodecs");
const RpcMessageCodecs_1 = require("../../common/codec/RpcMessageCodecs");
const NullObject_1 = require("@jsonjoy.com/util/lib/NullObject");
class Http1EndpointMatch {
constructor(handler, msgCodec) {
this.handler = handler;
this.msgCodec = msgCodec;
}
}
exports.Http1EndpointMatch = Http1EndpointMatch;
class Http1Server {
constructor(opts) {
this.opts = opts;
this.onnotfound = (res) => {
res.writeHead(404, 'Not Found');
res.end();
};
this.oninternalerror = (error, res) => {
if (error instanceof errors_1.PayloadTooLarge) {
res.statusCode = 413;
res.statusMessage = 'Payload Too Large';
res.end();
return;
}
res.statusCode = 500;
res.statusMessage = 'Internal Server Error';
res.end();
};
this.httpRouter = new jit_router_1.Router();
this.httpMatcher = () => undefined;
this.onRequest = async (req, res) => {
try {
res.sendDate = false;
const url = req.url ?? '';
const queryStartIndex = url.indexOf('?');
let path = url;
let query = '';
if (queryStartIndex >= 0) {
path = url.slice(0, queryStartIndex);
query = url.slice(queryStartIndex + 1);
}
const route = (req.method || '') + path;
const match = this.httpMatcher(route);
if (!match) {
this.onnotfound(res, req);
return;
}
const codecs = this.codecs;
const ip = this.findIp(req);
const token = this.findToken(req);
const matchData = match.data;
const ctx = new context_1.Http1ConnectionContext(req, res, path, query, ip, token, match.params, new NullObject_1.NullObject(), codecs.value.json, codecs.value.json, matchData.msgCodec);
const headers = req.headers;
const contentType = headers['content-type'];
if (typeof contentType === 'string')
(0, util_1.setCodecs)(ctx, contentType, codecs);
const handler = matchData.handler;
await handler(ctx);
}
catch (error) {
this.oninternalerror(error, res, req);
}
};
this.wsRouter = new jit_router_1.Router();
this.wsMatcher = () => undefined;
this.onUpgrade = (req, socket) => {
if (req.headers.upgrade === 'websocket') {
this.onWsUpgrade(req, socket);
}
else {
}
};
this.onWsUpgrade = (req, socket) => {
const url = req.url ?? '';
const queryStartIndex = url.indexOf('?');
let path = url;
let query = '';
if (queryStartIndex >= 0) {
path = url.slice(0, queryStartIndex);
query = url.slice(queryStartIndex + 1);
}
const match = this.wsMatcher(path);
if (!match) {
socket.end();
return;
}
const def = match.data;
const headers = req.headers;
const connection = new WsServerConnection_1.WsServerConnection(this.wsEncoder, socket);
connection.maxIncomingMessage = def.maxIncomingMessage ?? 2 * 1024 * 1024;
connection.maxBackpressure = def.maxOutgoingBackpressure ?? 2 * 1024 * 1024;
if (def.onWsUpgrade)
def.onWsUpgrade(req, connection);
else {
const secWebSocketKey = headers['sec-websocket-key'] ?? '';
const secWebSocketProtocol = headers['sec-websocket-protocol'] ?? '';
const secWebSocketExtensions = headers['sec-websocket-extensions'] ?? '';
connection.upgrade(secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions);
}
const codecs = this.codecs;
const ip = this.findIp(req);
const token = this.findToken(req);
const ctx = new context_1.WsConnectionContext(connection, path, query, ip, token, match.params, new NullObject_1.NullObject(), codecs.value.json, codecs.value.json, codecs.messages.compact);
const contentType = headers['content-type'];
if (typeof contentType === 'string')
(0, util_1.setCodecs)(ctx, contentType, codecs);
else {
const secWebSocketProtocol = headers['sec-websocket-protocol'] ?? '';
if (typeof secWebSocketProtocol === 'string')
(0, util_1.setCodecs)(ctx, secWebSocketProtocol, codecs);
}
def.handler(ctx, req);
};
this.server = opts.server;
const writer = opts.writer ?? new Writer_1.Writer();
this.codecs = opts.codecs ?? new RpcCodecs_1.RpcCodecs(opts.codecs ?? new Codecs_1.Codecs(writer), new RpcMessageCodecs_1.RpcMessageCodecs());
this.wsEncoder = new WsFrameEncoder_1.WsFrameEncoder(writer);
}
async start() {
const server = this.server;
this.httpMatcher = this.httpRouter.compile();
this.wsMatcher = this.wsRouter.compile();
server.on('request', this.onRequest);
server.on('upgrade', this.onUpgrade);
server.on('clientError', (err, socket) => {
socket.end('HTTP/1.1 400 Bad Request\r\n\r\n');
});
}
async stop() {
const server = this.server;
server.removeAllListeners();
}
route(def) {
let path = def.path;
if (path[0] !== '/')
path = '/' + path;
const method = def.method ? def.method.toUpperCase() : 'GET';
const route = method + path;
Number(route);
const match = new Http1EndpointMatch(def.handler, def.msgCodec ?? this.codecs.messages.jsonRpc2);
this.httpRouter.add(route, match);
}
ws(def) {
this.wsRouter.add(def.path, def);
}
findIp(req) {
const headers = req.headers;
const ip = headers['x-forwarded-for'] || headers['x-real-ip'] || req.socket.remoteAddress || '';
return ip instanceof Array ? ip[0] : ip;
}
findToken(req) {
let token = '';
const headers = req.headers;
let header;
header = headers.authorization;
if (typeof header === 'string')
token = (0, util_2.findTokenInText)(header);
if (token)
return token;
const url = req.url;
if (typeof url === 'string')
token = (0, util_2.findTokenInText)(url);
if (token)
return token;
header = headers.cookie;
if (typeof header === 'string')
token = (0, util_2.findTokenInText)(header);
if (token)
return token;
header = headers['sec-websocket-protocol'];
if (typeof header === 'string')
token = (0, util_2.findTokenInText)(header);
return token;
}
enableHttpPing(path = '/ping') {
this.route({
path,
handler: (ctx) => {
ctx.res.end('"pong"');
},
});
}
enableKamalPing(path = '/up', response = 'yes') {
this.route({
path,
handler: (ctx) => {
ctx.res.end(response);
},
});
}
toString(tab = '') {
return (`${this.constructor.name}` +
(0, printTree_1.printTree)(tab, [
(tab) => `HTTP ${this.httpRouter.toString(tab)}`,
(tab) => `WebSocket ${this.wsRouter.toString(tab)}`,
]));
}
}
exports.Http1Server = Http1Server;
_a = Http1Server;
Http1Server.create = async (opts = {}) => {
if (opts.tls) {
const { secureContext, secureContextRefreshInterval } = opts;
const server = https.createServer({
...(secureContext ? await secureContext() : {}),
...opts.conf,
});
if (secureContext && secureContextRefreshInterval) {
const timer = setInterval(() => {
try {
secureContext()
.then((context) => {
server.setSecureContext(context);
})
.catch((error) => {
console.error('Failed to update secure context:', error);
});
}
catch (error) {
console.error('Failed to update secure context:', error);
}
}, secureContextRefreshInterval);
server.once('close', () => {
clearInterval(timer);
});
}
return server;
}
return http.createServer(opts.conf || {});
};
//# sourceMappingURL=Http1Server.js.map
;