rpc_ts
Version:
Remote Procedure Calls in TypeScript made simple
309 lines • 14 kB
JavaScript
"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