UNPKG

allserver

Version:

Multi-protocol simple RPC server and [optional] client. Boilerplate-less. Opinionated. Minimalistic. DX-first.

110 lines (89 loc) 4.34 kB
const { isPlainObject, isFunction } = require("../util"); module.exports = require("./Transport").compose({ name: "GrpcTransport", props: { _fs: require("fs"), _grpc: require("@grpc/grpc-js"), _protoLoader: require("@grpc/proto-loader"), port: process.env.PORT, protoFile: null, credentials: null, protoFileContents: null, options: null, }, init({ port, protoFile, options, credentials }) { if (port) this.port = port; if (protoFile) this.protoFile = protoFile; if (options) this.options = options; this.credentials = credentials || this.credentials || this._grpc.ServerCredentials.createInsecure(); }, methods: { _validateResponseTypes(obj, inspectedSet = new Set()) { inspectedSet.add(obj); // circle linking protection for (const [k, v] of Object.entries(obj)) { if (!isPlainObject(v) || inspectedSet.has(v)) continue; // Not sure how long this code would survive in modern fast pace days. if (k === "responseType") { let f = v.type.field[0]; const successOk = f && f.name === "success" && f.type === "TYPE_BOOL"; if (!successOk) throw new Error(`Method ${obj.originalName} must return "bool success = 1"`); f = v.type.field[1]; const codeOk = f && f.name === "code" && f.type === "TYPE_STRING"; if (!codeOk) throw new Error(`Method ${obj.originalName} must return "string code = 2"`); f = v.type.field[2]; const messageOk = f && f.name === "message" && f.type === "TYPE_STRING"; if (!messageOk) throw new Error(`Method ${obj.originalName} must return "string message = 3"`); } else { this._validateResponseTypes(v, inspectedSet); } } }, _validatePackageDefinition(packageDefinition) { if (!packageDefinition.Allserver || !packageDefinition.Allserver.introspect) { throw new Error(`Server .proto file is missing Allserver mandatory introspection declarations`); } this._validateResponseTypes(packageDefinition); }, async startServer(defaultCtx) { this.server = new this._grpc.Server(this.options); async function wrappedCallback(call, callback) { const ctx = { ...defaultCtx, arg: call.request, grpc: { call, callback } }; await ctx.allserver.handleCall(ctx); } const packageDefinition = this._protoLoader.loadSync(this.protoFile); this._validatePackageDefinition(packageDefinition); const protoDescriptor = this._grpc.loadPackageDefinition(packageDefinition); for (const typeOfProto of Object.values(protoDescriptor)) { if (!(isFunction(typeOfProto) && isPlainObject(typeOfProto.service))) continue; const proxies = { introspect: wrappedCallback }; for (const [name, impl] of Object.entries(defaultCtx.allserver.procedures)) { if (isFunction(impl)) proxies[name] = wrappedCallback; } this.server.addService(typeOfProto.service, proxies); } await new Promise((resolve, reject) => { this.server.bindAsync(`0.0.0.0:${this.port}`, this.credentials, (err, result) => err ? reject(err) : resolve(result) ); }); }, stopServer() { return new Promise((r) => this.server.tryShutdown(r)); }, getProcedureName(ctx) { return ctx.grpc.call.call.handler.path.split("/").pop(); }, isIntrospection(ctx) { return this.getProcedureName(ctx) === "introspect"; }, async prepareIntrospectionReply(ctx) { if (!this.protoFileContents) { this.protoFileContents = this._fs.readFileSync(this.protoFile, "utf8"); } ctx.result.proto = this.protoFileContents; }, reply(ctx) { ctx.grpc.callback(null, ctx.result); }, }, });