UNPKG

@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
"use strict"; 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