UNPKG

rpc_ts

Version:

Remote Procedure Calls in TypeScript made simple

309 lines 14 kB
"use strict"; /** * Client-side implementation of the gRPC-Web protocol. * * @see The [gRPC-Web reference implementation of a JavaScript client](https://github.com/grpc/grpc-web), * based on a Protocol Buffer codec, and also this * [TypeScript-first implementation](https://github.com/improbable-eng/grpc-web) of a gRPC-Web client, * again with Protocol Buffers. * * @module ModuleRpcProtocolGrpcWebClient * @preferred * * @license * Copyright (c) Aiden.ai * * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. */ Object.defineProperty(exports, "__esModule", { value: true }); /** */ const http_status_1 = require("../private/http_status"); const grpc_1 = require("../private/grpc"); const grpc_web_1 = require("@improbable-eng/grpc-web"); const chunk_parser_1 = require("./private/chunk_parser"); const json_codec_1 = require("../common/json_codec"); const events = require("events"); const headers_1 = require("../private/headers"); const utils_1 = require("../../../utils"); const client_1 = require("../../../client"); const common_1 = require("../../../common"); /** * Returns an RPC client for the gRPC-Web protocol. * * @see [[getRpcClient]] offers a more generic interface with a more intuitive "method map" * service interface and is enough for most use cases. * * @param serviceDefinition The definition of the service for which to implement a client. * @param clientContextConnector The context connector to use to inject metadata (such * as authentication/authorization metadata) to the RPC. If one wishes to not inject * any metadata, [[ModuleRpcContextClient.EmptyClientContextConnector]] can used. * @param options Various options to change the behavior of the client, include the * address of the server to connect to. */ function getGrpcWebClient(serviceDefinition, clientContextConnector, options) { const codec = options.codec || new json_codec_1.GrpcWebJsonCodec(); const getTransport = options.getTransport || (options => grpc_web_1.grpc.CrossBrowserHttpTransport({})(options)); return new client_1.ModuleRpcClient.Service(serviceDefinition, (method, request) => new GrpcWebStream(`${options.remoteAddress}/${method}`, method, getTransport, clientContextConnector, codec, request)); } exports.getGrpcWebClient = getGrpcWebClient; /** * All the states of a grpc-web stream are gathered here for better clarity. */ var GrpcWebStreamState; (function (GrpcWebStreamState) { /** Initial state */ GrpcWebStreamState["initial"] = "initial"; /** Waiting for the request, the request context and the transport to start. */ GrpcWebStreamState["waitingForRequest"] = "waitingForRequest"; /** The transport is started, waiting for the headers/metadata. */ GrpcWebStreamState["started"] = "started"; /** The headers/metadata had been received and successfully decoded, we are ready to receive messages. */ GrpcWebStreamState["ready"] = "ready"; /** The stream has been canceled. */ GrpcWebStreamState["canceled"] = "canceled"; /** The stream errored. */ GrpcWebStreamState["error"] = "error"; /** The trailers have been received. */ GrpcWebStreamState["trailersReceived"] = "trailedsReceived"; /** The stream successfully completed. */ GrpcWebStreamState["complete"] = "complete"; /* The transported ended before the state was equal to `ready` */ GrpcWebStreamState["endedBeforeReady"] = "endedBeforeReady"; })(GrpcWebStreamState || (GrpcWebStreamState = {})); class GrpcWebStream extends events.EventEmitter { constructor(url, method, getTransport, clientContextConnector, codec, request) { super(); this.url = url; this.method = method; this.getTransport = getTransport; this.clientContextConnector = clientContextConnector; this.codec = codec; this.request = request; /** * Used to buffer frames as received from the server so as to read entire chunks * if they are saddled on multiple frames. */ this.chunkParser = new chunk_parser_1.ChunkParser(); /** * The chunks that are pending processing because the headers have not yet been decoded * (the decoding is asynchronous). When the state becomes `GrpcWebStreamState.ready`, * these chunk bytes are processed all at once. */ this.pendingChunkBytes = []; /** The state of the stream. We use this to enforce state transitions. */ this.state = GrpcWebStreamState.initial; } /** @override */ start() { this.ensureState(GrpcWebStreamState.initial); this.state = GrpcWebStreamState.waitingForRequest; const transport = this.getTransport({ methodDefinition: {}, debug: false, url: this.url, onHeaders: (headers, status) => this.onHeaders(headers, status) .then(responseContext => { this.responseContext = responseContext; switch (this.state) { case GrpcWebStreamState.started: case GrpcWebStreamState.endedBeforeReady: this.ready(); break; /* istanbul ignore next */ case GrpcWebStreamState.initial: /* istanbul ignore next */ case GrpcWebStreamState.ready: /* istanbul ignore next */ case GrpcWebStreamState.waitingForRequest: /* istanbul ignore next */ case GrpcWebStreamState.complete: /* istanbul ignore next */ case GrpcWebStreamState.trailersReceived: /* istanbul ignore next */ this.unexpectedState(); break; case GrpcWebStreamState.canceled: case GrpcWebStreamState.error: break; /* istanbul ignore next */ default: utils_1.ModuleRpcUtils.assertUnreachable(this.state, true); } }) .catch(err => this.emit('error', err)), onChunk: (chunkBytes, _flush) => { switch (this.state) { case GrpcWebStreamState.started: this.pendingChunkBytes.push(chunkBytes); break; case GrpcWebStreamState.ready: this.onChunk(chunkBytes); break; case GrpcWebStreamState.canceled: case GrpcWebStreamState.error: return; /* istanbul ignore next */ case GrpcWebStreamState.initial: /* istanbul ignore next */ case GrpcWebStreamState.waitingForRequest: /* istanbul ignore next */ case GrpcWebStreamState.complete: /* istanbul ignore next */ case GrpcWebStreamState.endedBeforeReady: /* istanbul ignore next */ case GrpcWebStreamState.trailersReceived: return this.unexpectedState(); /* istanbul ignore next */ default: utils_1.ModuleRpcUtils.assertUnreachable(this.state, true); } }, onEnd: err => { if (this.state === GrpcWebStreamState.error) { return; } if (err) { this.state = GrpcWebStreamState.error; /* tslint:disable:no-unnecessary-type-assertion */ /* istanbul ignore else */ if (err.code === 'ECONNREFUSED') { /* tslint:enable:no-unnecessary-type-assertion */ this.emit('error', new client_1.ModuleRpcClient.ClientRpcError(common_1.ModuleRpcCommon.RpcErrorType.unavailable, err.stack || /* istanbul ignore next */ err.message)); } else { this.emit('error', err); } } else { if (this.state === GrpcWebStreamState.started) { this.state = GrpcWebStreamState.endedBeforeReady; return; } this.end(); } }, }); /* istanbul ignore next */ if (transport instanceof Error) { this.state = GrpcWebStreamState.error; this.emit('error', new client_1.ModuleRpcClient.ClientTransportError(transport)); return this; } this.transport = transport; this.clientContextConnector .provideRequestContext() .then(requestContext => { transport.start(new grpc_web_1.grpc.Metadata(Object.assign({ 'content-type': this.codec.getContentType(), accept: this.codec.getContentType(), 'x-user-agent': 'grpc-web-javascript/0.1' }, requestContext))); this.state = GrpcWebStreamState.started; this.transport.sendMessage(this.codec.encodeRequest(this.method, this.request instanceof Function ? this.request() : this.request)); this.transport.finishSend(); }) .catch(err => { this.state = GrpcWebStreamState.error; this.emit('error', new client_1.ModuleRpcClient.RequestContextError(err)); }); return this; } /** @override */ cancel() { if (this.transport && this.state === GrpcWebStreamState.started) { this.transport.cancel(); } this.state = GrpcWebStreamState.canceled; this.emit('canceled'); return this; } /** When the headers are received */ async onHeaders(headers, status) { const encodedResponseContext = {}; headers.forEach((name, values) => { encodedResponseContext[name] = values.map(headers_1.decodeHeaderValue).join(','); }); const responseContext = await this.clientContextConnector.decodeResponseContext(encodedResponseContext); const error = grpc_1.getGrpcWebErrorFromMetadata(headers); if (error) { this.transport.cancel(); throw new client_1.ModuleRpcClient.ClientRpcError(error.errorType, error.message, responseContext); } if (status !== 200) { const { type, message } = guessErrorTypeAndMessageFromHttpStatus(status); throw new client_1.ModuleRpcClient.ClientRpcError(type, message, responseContext); } return responseContext; } /** When a chunk (message or trailer) is received */ onChunk(chunkBytes) { for (const chunk of this.chunkParser.parse(chunkBytes)) { switch (chunk.chunkType) { case chunk_parser_1.ChunkType.MESSAGE: if (chunk.data) { const response = this.codec.decodeMessage(this.method, chunk.data); this.emit('message', { response, responseContext: this.responseContext, }); } break; case chunk_parser_1.ChunkType.TRAILERS: { if (chunk.trailers) { const error = grpc_1.getGrpcWebErrorFromMetadata(chunk.trailers); if (error) { this.state = GrpcWebStreamState.error; this.emit('error', new client_1.ModuleRpcClient.ClientRpcError(error.errorType, error.message, this.responseContext)); break; } } this.state = GrpcWebStreamState.trailersReceived; break; } /* istanbul ignore next */ default: utils_1.ModuleRpcUtils.assertUnreachable(chunk.chunkType, true); } } } /** Call this when the stream is ready. */ ready() { const endedBeforeReady = this.state === GrpcWebStreamState.endedBeforeReady; this.state = GrpcWebStreamState.ready; this.emit('ready'); for (const chunkBytes of this.pendingChunkBytes) { this.onChunk(chunkBytes); } if (endedBeforeReady) { this.end(); } } /** Call this when the stream ends. */ end() { if (this.state === GrpcWebStreamState.trailersReceived) { this.state = GrpcWebStreamState.complete; this.emit('complete'); } else if (this.state !== GrpcWebStreamState.error) { // The connection was closed before the trailers could be received this.emit('error', new client_1.ModuleRpcClient.ClientRpcError(common_1.ModuleRpcCommon.RpcErrorType.unavailable)); } } /* istanbul ignore next */ ensureState(state) { if (this.state !== state) { throw new Error(`invalid retrier state: actual: ${this.state}; expected: ${state}`); } } /* istanbul ignore next */ unexpectedState() { throw new Error(`unexpected state ${this.state}`); } } function guessErrorTypeAndMessageFromHttpStatus(httpStatus) { return { type: http_status_1.httpStatusesToErrorTypes[httpStatus] || /* istanbul ignore next */ common_1.ModuleRpcCommon.RpcErrorType.unknown, message: http_status_1.httpStatusesToClientErrorMessages[httpStatus] || `non-200 HTTP status code (${httpStatus})`, }; } //# sourceMappingURL=client.js.map