@jsonjoy.com/reactive-rpc
Version:
Reactive-RPC is a library for building reactive APIs over WebSocket, HTTP, and other RPCs.
221 lines • 8.28 kB
JavaScript
var _a;
Object.defineProperty(exports, "__esModule", { value: true });
exports.RpcServer = void 0;
const printTree_1 = require("sonic-forest/lib/print/printTree");
const Http1Server_1 = require("./Http1Server");
const caller_1 = require("../../common/rpc/caller");
const common_1 = require("../../common");
const ObjectValueCaller_1 = require("../../common/rpc/caller/ObjectValueCaller");
const gzip_1 = require("@jsonjoy.com/util/lib/compression/gzip");
const DEFAULT_MAX_PAYLOAD = 4 * 1024 * 1024;
class RpcServer {
constructor(opts) {
this.opts = opts;
this.processHttpRpcRequest = async (ctx) => {
const res = ctx.res;
const body = await ctx.body(DEFAULT_MAX_PAYLOAD);
if (!res.socket)
return;
try {
const messageCodec = ctx.msgCodec;
const incomingMessages = messageCodec.decodeBatch(ctx.reqCodec, body);
try {
const outgoingMessages = await this.batchProcessor.onBatch(incomingMessages, ctx);
if (!res.socket)
return;
const resCodec = ctx.resCodec;
messageCodec.encodeBatch(resCodec, outgoingMessages);
const buf = resCodec.encoder.writer.flush();
if (!res.socket)
return;
res.end(buf);
}
catch (error) {
const logger = this.opts.logger ?? console;
logger.error('HTTP_RPC_PROCESSING', error, { messages: incomingMessages });
throw caller_1.RpcError.from(error);
}
}
catch (error) {
if (typeof error === 'object' && error)
if (error.message === 'Invalid JSON')
throw caller_1.RpcError.badRequest();
throw caller_1.RpcError.from(error);
}
};
const http1 = (this.http1 = opts.http1);
const onInternalError = http1.oninternalerror;
http1.oninternalerror = (error, res, req) => {
if (error instanceof caller_1.RpcError) {
res.statusCode = 400;
const data = JSON.stringify(error.toJson());
res.end(data);
return;
}
onInternalError(error, res, req);
};
this.batchProcessor = new common_1.RpcMessageBatchProcessor({ caller: opts.caller });
}
enableHttpPing() {
const http1 = this.http1;
http1.enableHttpPing();
http1.enableKamalPing();
}
enableCors() {
this.http1.route({
method: 'OPTIONS',
path: '/{::\n}',
handler: (ctx) => {
const res = ctx.res;
res.writeHead(200, 'OK', {
'Access-Control-Allow-Origin': '*',
'Access-Control-Allow-Credentials': 'true',
});
res.end();
},
});
}
enableHttpRpc(path = '/rx') {
const http1 = this.http1;
http1.route({
method: 'POST',
path,
handler: this.processHttpRpcRequest,
msgCodec: http1.codecs.messages.compact,
});
}
enableJsonRcp2HttpRpc(path = '/rpc') {
const http1 = this.http1;
http1.route({
method: 'POST',
path,
handler: this.processHttpRpcRequest,
msgCodec: http1.codecs.messages.jsonRpc2,
});
}
enableWsRpc(path = '/rx') {
const opts = this.opts;
const logger = opts.logger ?? console;
const caller = opts.caller;
this.http1.ws({
path,
maxIncomingMessage: 2 * 1024 * 1024,
maxOutgoingBackpressure: 2 * 1024 * 1024,
handler: (ctx) => {
const connection = ctx.connection;
const reqCodec = ctx.reqCodec;
const resCodec = ctx.resCodec;
const msgCodec = ctx.msgCodec;
const encoder = resCodec.encoder;
const rpc = new common_1.RpcMessageStreamProcessor({
caller,
send: (messages) => {
try {
const writer = encoder.writer;
writer.reset();
msgCodec.encodeBatch(resCodec, messages);
const encoded = writer.flush();
connection.sendBinMsg(encoded);
}
catch (error) {
logger.error('WS_SEND', error, { messages });
connection.close();
}
},
bufferSize: 1,
bufferTime: 0,
});
connection.onmessage = (uint8) => {
let messages;
try {
messages = msgCodec.decodeBatch(reqCodec, uint8);
}
catch (error) {
logger.error('RX_RPC_DECODING', error, { codec: reqCodec.id, buf: Buffer.from(uint8).toString('base64') });
connection.close();
return;
}
try {
rpc.onMessages(messages, ctx);
}
catch (error) {
logger.error('RX_RPC_PROCESSING', error, messages);
connection.close();
return;
}
};
connection.onclose = () => {
rpc.stop();
};
},
});
}
enableSchema(path = '/schema', method = 'GET') {
const caller = this.opts.caller;
let responseBody = Buffer.from('{}');
if (caller instanceof ObjectValueCaller_1.ObjectValueCaller) {
const api = caller.router;
const schema = {
value: api.type.getSchema(),
types: api.type.system?.exportTypes(),
};
responseBody = Buffer.from(JSON.stringify(schema));
}
let responseBodyCompressed = new Uint8Array(0);
(0, gzip_1.gzip)(responseBody).then((compressed) => (responseBodyCompressed = compressed));
this.http1.route({
method,
path,
handler: (ctx) => {
const res = ctx.res;
res.writeHead(200, 'OK', {
'Content-Type': 'application/json',
'Content-Encoding': 'gzip',
'Cache-Control': 'public, max-age=3600, immutable',
'Content-Length': responseBodyCompressed.length,
});
res.end(responseBodyCompressed);
},
});
}
enableDefaults() {
this.enableCors();
this.enableHttpPing();
this.enableHttpRpc();
this.enableJsonRcp2HttpRpc();
this.enableWsRpc();
this.enableSchema();
}
toString(tab = '') {
return (`${this.constructor.name}` +
(0, printTree_1.printTree)(tab, [
(tab) => this.http1.toString(tab),
() => '',
(tab) => this.opts.caller.toString(tab),
]));
}
}
exports.RpcServer = RpcServer;
_a = RpcServer;
RpcServer.startWithDefaults = async (opts) => {
const port = opts.port || 8080;
const logger = opts.logger ?? console;
const server = await Http1Server_1.Http1Server.create(opts.create);
const http1 = new Http1Server_1.Http1Server({ ...opts.server, server });
const rpc = new _a({
caller: opts.caller,
http1,
logger,
});
rpc.enableDefaults();
await http1.start();
server.listen(port, () => {
let host = server.address() || 'localhost';
if (typeof host === 'object')
host = host.address;
logger.log({ msg: 'SERVER_STARTED', host, port });
});
return rpc;
};
//# sourceMappingURL=RpcServer.js.map
;