@jsonjoy.com/reactive-rpc
Version:
Reactive-RPC is a library for building reactive APIs over WebSocket, HTTP, and other RPCs.
262 lines • 11.1 kB
JavaScript
"use strict";
Object.defineProperty(exports, "__esModule", { value: true });
exports.RpcApp = void 0;
const Codecs_1 = require("@jsonjoy.com/json-pack/lib/codecs/Codecs");
const Writer_1 = require("@jsonjoy.com/util/lib/buffers/Writer");
const copy_1 = require("@jsonjoy.com/util/lib/buffers/copy");
const jit_router_1 = require("@jsonjoy.com/jit-router");
const util_1 = require("./util");
const RpcMessageBatchProcessor_1 = require("../../common/rpc/RpcMessageBatchProcessor");
const RpcError_1 = require("../../common/rpc/caller/error/RpcError");
const RpcErrorType_1 = require("../../common/rpc/caller/error/RpcErrorType");
const context_1 = require("../context");
const RpcMessageCodecs_1 = require("../../common/codec/RpcMessageCodecs");
const Value_1 = require("../../common/messages/Value");
const RpcCodecs_1 = require("../../common/codec/RpcCodecs");
const printTree_1 = require("sonic-forest/lib/print/printTree");
const common_1 = require("../../common");
const HDR_BAD_REQUEST = Buffer.from('400 Bad Request', 'utf8');
const HDR_NOT_FOUND = Buffer.from('404 Not Found', 'utf8');
const ERR_NOT_FOUND = RpcError_1.RpcError.fromCode('NOT_FOUND', 'Not Found');
const noop = () => { };
class RpcApp {
constructor(options) {
this.options = options;
this.router = new jit_router_1.Router();
this.app = options.uws;
this.maxRequestBodySize = options.maxRequestBodySize ?? 1024 * 1024;
this.codecs = new RpcCodecs_1.RpcCodecs(options.codecs ?? new Codecs_1.Codecs(new Writer_1.Writer()), new RpcMessageCodecs_1.RpcMessageCodecs());
this.batchProcessor = new RpcMessageBatchProcessor_1.RpcMessageBatchProcessor({ caller: options.caller });
}
enableCors() {
(0, util_1.enableCors)(this.options.uws);
}
routeRaw(method, path, handler) {
method = method.toLowerCase();
this.router.add(method + path, handler);
}
route(method, path, handler) {
this.routeRaw(method, path, async (ctx) => {
const result = await handler(ctx);
const res = ctx.res;
if (res.aborted)
return;
const codec = ctx.resCodec;
const encoder = codec.encoder;
const writer = encoder.writer;
writer.reset();
if (res instanceof Value_1.RpcValue) {
if (res.type)
res.type.encoder(codec.format)(res.data, encoder);
else
encoder.writeAny(res.data);
}
else {
encoder.writeAny(result);
}
if (res.aborted)
return;
ctx.sendResponse(writer.flush());
});
}
enableHttpPing(path = '/ping') {
this.route('GET', path, async () => {
return 'pong';
});
return this;
}
enableHttpRpc(path = '/rx') {
this.routeRaw('POST', path, async (ctx) => {
try {
const res = ctx.res;
const bodyUint8 = await ctx.requestBody(this.maxRequestBodySize);
if (res.aborted)
return;
const messageCodec = ctx.msgCodec;
const incomingMessages = messageCodec.decodeBatch(ctx.reqCodec, bodyUint8);
try {
const outgoingMessages = await this.batchProcessor.onBatch(incomingMessages, ctx);
if (res.aborted)
return;
const resCodec = ctx.resCodec;
messageCodec.encodeBatch(resCodec, outgoingMessages);
const buf = resCodec.encoder.writer.flush();
if (res.aborted)
return;
res.end(buf);
}
catch (error) {
const logger = this.options.logger ?? console;
logger.error('HTTP_RPC_PROCESSING', error, { messages: incomingMessages });
throw RpcError_1.RpcError.internal(error);
}
}
catch (error) {
if (typeof error === 'object' && error)
if (error.message === 'Invalid JSON')
throw RpcError_1.RpcError.badRequest();
throw RpcError_1.RpcError.from(error);
}
});
return this;
}
enableWsRpc(path = '/rx') {
const maxBackpressure = 4 * 1024 * 1024;
const augmentContext = this.options.augmentContext ?? noop;
const options = this.options;
const logger = options.logger ?? console;
const caller = options.caller;
this.app.ws(path, {
idleTimeout: 0,
maxPayloadLength: 4 * 1024 * 1024,
upgrade: (res, req, context) => {
const secWebSocketKey = req.getHeader('sec-websocket-key');
const secWebSocketProtocol = req.getHeader('sec-websocket-protocol');
const secWebSocketExtensions = req.getHeader('sec-websocket-extensions');
const ctx = context_1.ConnectionContext.fromWs(req, res, secWebSocketProtocol, null, this);
augmentContext(ctx);
res.upgrade({ ctx }, secWebSocketKey, secWebSocketProtocol, secWebSocketExtensions, context);
},
open: (ws_) => {
try {
const ws = ws_;
const ctx = ws.ctx;
const resCodec = ctx.resCodec;
const msgCodec = ctx.msgCodec;
const encoder = resCodec.encoder;
ws.rpc = new common_1.RpcMessageStreamProcessor({
caller,
send: (messages) => {
try {
if (ws.getBufferedAmount() > maxBackpressure)
return;
const writer = encoder.writer;
writer.reset();
msgCodec.encodeBatch(resCodec, messages);
const encoded = writer.flush();
ws.send(encoded, true, false);
}
catch (error) {
logger.error('WS_SEND', error, { messages });
}
},
bufferSize: 1,
bufferTime: 0,
});
}
catch (error) {
logger.error('RX_WS_OPEN', error);
}
},
message: (ws_, buf) => {
try {
const ws = ws_;
const ctx = ws.ctx;
const reqCodec = ctx.reqCodec;
const msgCodec = ctx.msgCodec;
const uint8 = (0, copy_1.copy)(new Uint8Array(buf));
const rpc = ws.rpc;
try {
const messages = msgCodec.decodeBatch(reqCodec, uint8);
try {
rpc.onMessages(messages, ctx);
}
catch (error) {
logger.error('RX_RPC_PROCESSING', error, messages);
return;
}
}
catch (error) {
logger.error('RX_RPC_DECODING', error, { codec: reqCodec.id, buf: Buffer.from(uint8).toString() });
}
}
catch (error) {
logger.error('RX_WS_MESSAGE', error);
}
},
close: (ws_) => {
const ws = ws_;
ws.rpc.stop();
},
});
return this;
}
startRouting() {
const matcher = this.router.compile();
const codecs = this.codecs;
let responseCodec = codecs.value.json;
const options = this.options;
const augmentContext = options.augmentContext ?? noop;
const logger = options.logger ?? console;
this.app.any('/*', async (res, req) => {
try {
res.onAborted(() => {
res.aborted = true;
});
const method = req.getMethod();
const url = req.getUrl();
try {
const match = matcher(method + url);
if (!match) {
res.cork(() => {
res.writeStatus(HDR_NOT_FOUND);
res.end(RpcErrorType_1.RpcErrorType.encode(responseCodec, ERR_NOT_FOUND));
});
return;
}
const handler = match.data;
const params = match.params;
const ctx = context_1.ConnectionContext.fromReqRes(req, res, params, this);
responseCodec = ctx.resCodec;
augmentContext(ctx);
await handler(ctx);
}
catch (_error) {
let err = _error;
if (err instanceof Value_1.RpcValue)
err = err.data;
if (!(err instanceof RpcError_1.RpcError))
err = RpcError_1.RpcError.from(err);
const error = err;
if (error.errno === RpcError_1.RpcErrorCodes.INTERNAL_ERROR) {
logger.error('UWS_ROUTER_INTERNAL_ERROR', error, { originalError: error.originalError ?? null });
}
res.cork(() => {
res.writeStatus(HDR_BAD_REQUEST);
res.end(RpcErrorType_1.RpcErrorType.encode(responseCodec, error));
});
}
}
catch { }
});
}
startWithDefaults() {
this.enableCors();
this.enableHttpPing();
this.enableHttpRpc();
this.enableWsRpc();
this.startRouting();
const options = this.options;
const port = options.port ?? +(process.env.PORT || 9999);
const host = options.host ?? process.env.HOST ?? '0.0.0.0';
const logger = options.logger ?? console;
this.options.uws.listen(host, port, (token) => {
if (token) {
logger.log({ msg: 'SERVER_STARTED', url: `http://localhost:${port}` });
}
else {
logger.error('SERVER_START', new Error(`Failed to listen on ${port} port.`));
}
});
}
toString(tab = '') {
return (`${this.constructor.name}` +
(0, printTree_1.printTree)(tab, [
(tab) => this.router.toString(tab),
() => '',
(tab) => this.options.caller.toString(tab),
]));
}
}
exports.RpcApp = RpcApp;
//# sourceMappingURL=RpcApp.js.map