UNPKG

@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
"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