From 504c42fe4498238dcd5e8940d3584f82581b745b Mon Sep 17 00:00:00 2001 From: Eryu Xia Date: Wed, 22 Sep 2021 12:02:23 -0700 Subject: [PATCH] Internal code sync Up to Aug 4, 2021 for now.. :) - Also updated Closure dependency to `20210808.0.0` (necessary for tests to pass) --- README.md | 2 +- javascript/net/grpc/web/abstractclientbase.js | 63 +-- javascript/net/grpc/web/clientoptions.js | 25 +- .../net/grpc/web/clientreadablestream.js | 15 + javascript/net/grpc/web/grpc_generator.cc | 216 +++----- javascript/net/grpc/web/grpcwebclientbase.js | 94 ++-- .../net/grpc/web/grpcwebclientbase_test.js | 475 ++++++++---------- .../grpc/web/grpcwebclientreadablestream.js | 59 +-- javascript/net/grpc/web/methodtype.js | 15 +- .../net/grpc/web/{error.js => rpcerror.js} | 42 +- .../echo/commonjs-example/echotest.html | 2 +- net/grpc/gateway/examples/echo/echoapp.js | 2 +- net/grpc/gateway/examples/echo/echotest.html | 2 +- .../examples/echo/ts-example/client.ts | 26 +- .../examples/echo/ts-example/echotest.html | 2 +- packages/grpc-web/README.md | 4 +- packages/grpc-web/exports.js | 2 - packages/grpc-web/index.d.ts | 69 ++- packages/grpc-web/package.json | 2 +- packages/grpc-web/protractor_spec.js | 4 + packages/grpc-web/test/export_test.js | 4 - packages/grpc-web/test/tsc-tests/client02.ts | 2 +- packages/grpc-web/test/tsc-tests/client05.ts | 4 +- 23 files changed, 466 insertions(+), 665 deletions(-) rename javascript/net/grpc/web/{error.js => rpcerror.js} (54%) diff --git a/README.md b/README.md index aa99cccf..2600e3bd 100644 --- a/README.md +++ b/README.md @@ -271,7 +271,7 @@ const request = new EchoRequest(); request.setMessage('Hello World!'); const call = echoService.echo(request, {'custom-header-1': 'value1'}, - (err: grpcWeb.Error, response: EchoResponse) => { + (err: grpcWeb.RpcError, response: EchoResponse) => { console.log(response.getMessage()); }); call.on('status', (status: grpcWeb.Status) => { diff --git a/javascript/net/grpc/web/abstractclientbase.js b/javascript/net/grpc/web/abstractclientbase.js index 5785681d..21da4ac4 100644 --- a/javascript/net/grpc/web/abstractclientbase.js +++ b/javascript/net/grpc/web/abstractclientbase.js @@ -28,9 +28,8 @@ goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); -const GrpcWebError = goog.require('grpc.web.Error'); const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); -const MethodType = goog.require('grpc.web.MethodType'); +const RpcError = goog.require('grpc.web.RpcError'); /** @@ -46,10 +45,9 @@ const AbstractClientBase = class { * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata - * @param {!MethodDescriptor| - * !AbstractClientBase.MethodInfo} + * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method - * @param {function(?GrpcWebError, ?)} + * @param {function(?RpcError, ?)} * callback A callback function which takes (error, RESPONSE or null) * @return {!ClientReadableStream} */ @@ -62,8 +60,7 @@ const AbstractClientBase = class { * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata - * @param {!MethodDescriptor| - * !AbstractClientBase.MethodInfo} + * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method * @return {!IThenable} * A promise that resolves to the response message @@ -76,36 +73,12 @@ const AbstractClientBase = class { * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata - * @param {!MethodDescriptor| - * !AbstractClientBase.MethodInfo} + * @param {!MethodDescriptor} * methodDescriptor Information of this RPC method * @return {!ClientReadableStream} The Client Readable Stream */ serverStreaming(method, requestMessage, metadata, methodDescriptor) {} - /** - * As MethodType is being deprecated, for now we need to convert MethodType to - * MethodDescriptor. - * @static - * @template REQUEST, RESPONSE - * @param {string} method - * @param {REQUEST} requestMessage - * @param {!MethodType} methodType - * @param {!AbstractClientBase.MethodInfo|!MethodDescriptor} - * methodInfo - * @return {!MethodDescriptor} - */ - static ensureMethodDescriptor( - method, requestMessage, methodType, methodInfo) { - if (methodInfo instanceof MethodDescriptor) { - return methodInfo; - } - const requestType = methodInfo.requestType || requestMessage.constructor; - return new MethodDescriptor( - method, methodType, requestType, methodInfo.responseType, - methodInfo.requestSerializeFn, methodInfo.responseDeserializeFn); - } - /** * Get the hostname of the current request. * @static @@ -121,31 +94,5 @@ const AbstractClientBase = class { }; -/** @template REQUEST, RESPONSE */ -AbstractClientBase.MethodInfo = class { - /** - * @param {function(new: RESPONSE, ...)} responseType - * @param {function(REQUEST): ?} requestSerializeFn - * @param {function(?): RESPONSE} responseDeserializeFn - * @param {string=} name - * @param {function(new: REQUEST, ...)=} requestType - */ - constructor( - responseType, requestSerializeFn, responseDeserializeFn, name, - requestType) { - /** @const */ - this.name = name; - /** @const */ - this.requestType = requestType; - /** @const */ - this.responseType = responseType; - /** @const */ - this.requestSerializeFn = requestSerializeFn; - /** @const */ - this.responseDeserializeFn = responseDeserializeFn; - } -}; - - exports = AbstractClientBase; diff --git a/javascript/net/grpc/web/clientoptions.js b/javascript/net/grpc/web/clientoptions.js index 806064a5..da1e3623 100644 --- a/javascript/net/grpc/web/clientoptions.js +++ b/javascript/net/grpc/web/clientoptions.js @@ -1,7 +1,6 @@ goog.module('grpc.web.ClientOptions'); goog.module.declareLegacyNamespace(); -const XmlHttpFactory = goog.requireType('goog.net.XmlHttpFactory'); const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor'); @@ -47,18 +46,20 @@ class ClientOptions { this.format; /** - * The XmlHttpFactory for server-streaming calls. - * Example: use 'goog.net.FetchXmlHttpFactory' to reduce memory consumption - * during high throughput server-streaming calls. - *
-     * ...
-     *
-     * const xmlHttpFactory =
-     *   new FetchXmlHttpFactory({streamBinaryChunks: true});
-     * 
- * @type {!XmlHttpFactory|undefined} + * The Worker global scope. Once this option is specified, gRPC-Web will + * also use 'fetch' API as the underlying transport instead of native + * XmlHttpRequest. + * @type {!WorkerGlobalScope|undefined} */ - this.streamingXmlHttpFactory; + this.workerScope; + + /** + * This is an experimental feature to reduce memory consumption + * during high throughput server-streaming calls by using + * 'streamBinaryChunks' mode FetchXmlHttpFactory. + * @type {boolean|undefined} + */ + this.useFetchDownloadStreams; } } diff --git a/javascript/net/grpc/web/clientreadablestream.js b/javascript/net/grpc/web/clientreadablestream.js index 5ba440ef..60123271 100644 --- a/javascript/net/grpc/web/clientreadablestream.js +++ b/javascript/net/grpc/web/clientreadablestream.js @@ -45,6 +45,21 @@ const ClientReadableStream = function() {}; /** * Register a callback to handle different stream events. * + * Available event types for gRPC-Web: + * 'data': The 'data' event is emitted when a new response message chunk is + * received and successfully handled by gRPC-Web client. + * 'status': the google RPC status of the response stream. + * 'end': The 'end' event is emitted when all the data have been successfully + * consumed from the stream. + * 'error': typically, this may occur when an underlying internal failure + * happens, or a stream implementation attempts to push an invalid chunk of + * data. + * 'metadata': the response metadata. Response headers should be read via + * 'metadata' callbacks. + * + * For server-streaming calls. the 'data' and 'status' callbacks (if exist) + * will always precede 'metadata', 'error', or 'end' callbacks. + * * @param {string} eventType The event type * @param {function(?)} callback The callback to handle the event with * an optional input object diff --git a/javascript/net/grpc/web/grpc_generator.cc b/javascript/net/grpc/web/grpc_generator.cc index 2a366558..955a22a0 100644 --- a/javascript/net/grpc/web/grpc_generator.cc +++ b/javascript/net/grpc/web/grpc_generator.cc @@ -50,9 +50,8 @@ namespace { using std::string; enum Mode { - OP = 0, // first party google3 one platform services - OPJSPB = 1, // first party google3 one platform services with JSPB - GRPCWEB = 2, // client using the application/grpc-web wire format + OP = 0, // first party google3 one platform services + GRPCWEB = 1, // client using the application/grpc-web wire format }; enum ImportStyle { @@ -137,30 +136,28 @@ string GetModeVar(const Mode mode) { switch (mode) { case OP: return "OP"; - case OPJSPB: - return "OPJspb"; case GRPCWEB: return "GrpcWeb"; } return ""; } -string GetDeserializeMethodName(const string& mode_var) { - if (mode_var == GetModeVar(Mode::OPJSPB)) { +string GetDeserializeMethodName(std::map vars) { + if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "deserialize"; } return "deserializeBinary"; } -string GetSerializeMethodName(const string& mode_var) { - if (mode_var == GetModeVar(Mode::OPJSPB)) { +string GetSerializeMethodName(std::map vars) { + if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "serialize"; } return "serializeBinary"; } -std::string GetSerializeMethodReturnType(const string& mode_var) { - if (mode_var == GetModeVar(Mode::OPJSPB)) { +std::string GetSerializeMethodReturnType(std::map vars) { + if (vars["mode"] == GetModeVar(Mode::OP) && vars["binary"] == "false") { return "string"; } return "!Uint8Array"; @@ -568,7 +565,7 @@ void PrintCommonJsMessagesDeps(Printer* printer, const FileDescriptor* file) { "\nvar $alias$ = require('$dep_filename$_pb.js')\n"); } - string package = file->package(); + const string& package = file->package(); vars["package_name"] = package; if (!package.empty()) { @@ -661,15 +658,20 @@ void PrintTypescriptFile(Printer* printer, const FileDescriptor* file, vars["method_name"] = method->name(); vars["input_type"] = JSMessageType(method->input_type()); vars["output_type"] = JSMessageType(method->output_type()); - vars["serialize_func_name"] = GetSerializeMethodName(vars["mode"]); - vars["deserialize_func_name"] = GetDeserializeMethodName(vars["mode"]); + vars["serialize_func_name"] = GetSerializeMethodName(vars); + vars["deserialize_func_name"] = GetDeserializeMethodName(vars); + vars["method_type"] = method->server_streaming() + ? "grpcWeb.MethodType.SERVER_STREAMING" + : "grpcWeb.MethodType.UNARY"; if (!method->client_streaming()) { - // TODO(jennyjiang): use methodDescriptor? printer->Print(vars, "methodInfo$method_name$ = " - "new grpcWeb.AbstractClientBase.MethodInfo(\n"); + "new grpcWeb.MethodDescriptor(\n"); printer->Indent(); printer->Print(vars, + "'/$package_dot$$service_name$/$method_name$',\n" + "$method_type$,\n" + "$input_type$,\n" "$output_type$,\n" "(request: $input_type$) => {\n" " return request.$serialize_func_name$();\n" @@ -709,7 +711,7 @@ void PrintTypescriptFile(Printer* printer, const FileDescriptor* file, printer->Print(vars, "request: $input_type$,\n" "metadata: grpcWeb.Metadata | null,\n" - "callback: (err: grpcWeb.Error,\n" + "callback: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void): " "grpcWeb.ClientReadableStream<$output_type$>;\n\n"); printer->Outdent(); @@ -719,7 +721,7 @@ void PrintTypescriptFile(Printer* printer, const FileDescriptor* file, printer->Print(vars, "request: $input_type$,\n" "metadata: grpcWeb.Metadata | null,\n" - "callback?: (err: grpcWeb.Error,\n" + "callback?: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void) {\n"); printer->Print(vars, "if (callback !== undefined) {\n"); printer->Indent(); @@ -801,7 +803,7 @@ void PrintGrpcWebDtsClientClass(Printer* printer, const FileDescriptor* file, printer->Print(vars, "request: $input_type$,\n" "metadata: grpcWeb.Metadata | undefined,\n" - "callback: (err: grpcWeb.Error,\n" + "callback: (err: grpcWeb.RpcError,\n" " response: $output_type$) => void\n"); printer->Outdent(); printer->Print(vars, @@ -860,7 +862,7 @@ void PrintProtoDtsOneofCase(Printer *printer, const OneofDescriptor *desc) void PrintProtoDtsMessage(Printer *printer, const Descriptor *desc, const FileDescriptor *file) { - string class_name = desc->name(); + const string& class_name = desc->name(); std::map vars; vars["class_name"] = class_name; @@ -1077,16 +1079,14 @@ void PrintMethodDescriptorFile(Printer* printer, "/**\n" " * @param {!proto.$in$} request\n"); printer->Print( - (" * @return {" + GetSerializeMethodReturnType(vars["mode"]) + "}\n") - .c_str()); + (" * @return {" + GetSerializeMethodReturnType(vars) + "}\n").c_str()); printer->Print(" */\n" "function(request) {\n"); printer->Print( - (" return request." + GetSerializeMethodName(vars["mode"]) + "();\n") - .c_str()); + (" return request." + GetSerializeMethodName(vars) + "();\n").c_str()); printer->Print("},\n"); - printer->Print( - vars, ("$out_type$." + GetDeserializeMethodName(vars["mode"])).c_str()); + printer->Print(vars, + ("$out_type$." + GetDeserializeMethodName(vars)).c_str()); printer->Print(vars, ");\n\n\n"); printer->Outdent(); printer->Outdent(); @@ -1095,41 +1095,9 @@ void PrintMethodDescriptorFile(Printer* printer, printer->Print("}); // goog.scope\n\n"); } -void PrintServiceConstructor(Printer* printer, - std::map vars) { - printer->Print( - vars, - "/**\n" - " * @param {string} hostname\n" - " * @param {?Object} credentials\n" - " * @param {?Object} options\n" - " * @constructor\n" - " * @struct\n" - " * @final\n" - " */\n" - "proto.$package_dot$$service_name$Client =\n" - " function(hostname, credentials, options) {\n" - " if (!options) options = {};\n"); - if (vars["mode"] == GetModeVar(Mode::GRPCWEB)) { - printer->Print( - vars, - " options['format'] = '$format$';\n\n"); - } - printer->Print( - vars, - " /**\n" - " * @private @const {!grpc.web.$mode$ClientBase} The client\n" - " */\n" - " this.client_ = new grpc.web.$mode$ClientBase(options);\n\n" - " /**\n" - " * @private @const {string} The hostname\n" - " */\n" - " this.hostname_ = hostname;\n\n" - "};\n\n\n"); -} - -void PrintPromiseServiceConstructor(Printer* printer, - std::map vars) { +void PrintServiceConstructor(Printer* printer, std::map vars, + bool is_promise) { + vars["is_promise"] = is_promise ? "Promise" : ""; printer->Print(vars, "/**\n" " * @param {string} hostname\n" @@ -1139,88 +1107,65 @@ void PrintPromiseServiceConstructor(Printer* printer, " * @struct\n" " * @final\n" " */\n" - "proto.$package_dot$$service_name$PromiseClient =\n" + "proto.$package_dot$$service_name$$is_promise$Client =\n" " function(hostname, credentials, options) {\n" " if (!options) options = {};\n"); if (vars["mode"] == GetModeVar(Mode::GRPCWEB)) { printer->Print(vars, " options['format'] = '$format$';\n\n"); } - printer->Print( - vars, - " /**\n" - " * @private @const {!grpc.web.$mode$ClientBase} The client\n" - " */\n" - " this.client_ = new grpc.web.$mode$ClientBase(options);\n\n" - " /**\n" - " * @private @const {string} The hostname\n" - " */\n" - " this.hostname_ = hostname;\n\n" - "};\n\n\n"); -} - -void PrintMethodInfo(Printer* printer, std::map vars) { - // Print MethodDescriptor. - printer->Print(vars, - "/**\n" - " * @const\n" - " * @type {!grpc.web.MethodDescriptor<\n" - " * !proto.$in$,\n" - " * !proto.$out$>}\n" - " */\n" - "const methodDescriptor_$service_name$_$method_name$ = " - "new grpc.web.MethodDescriptor(\n"); - printer->Indent(); - printer->Print(vars, - "'/$package_dot$$service_name$/$method_name$',\n" - "$method_type$,\n" - "$in_type$,\n"); - printer->Print(vars, - "$out_type$,\n" - "/**\n" - " * @param {!proto.$in$} request\n"); - printer->Print( - (" * @return {" + GetSerializeMethodReturnType(vars["mode"]) + "}\n") - .c_str()); - printer->Print(" */\n" - "function(request) {\n"); + if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print( - (" return request." + GetSerializeMethodName(vars["mode"]) + "();\n") - .c_str()); - printer->Print("},\n"); + vars, + " /**\n" + " * @private @const {!grpc.web.$mode$ClientBase} The client\n" + " */\n" + " this.client_ = new grpc.web.$mode$ClientBase(options, " + "$binary$);\n\n"); + } else { printer->Print( - vars, ("$out_type$." + GetDeserializeMethodName(vars["mode"]) + "\n") - .c_str()); - printer->Outdent(); - printer->Print(vars, ");\n\n\n"); + vars, + " /**\n" + " * @private @const {!grpc.web.$mode$ClientBase} The client\n" + " */\n" + " this.client_ = new grpc.web.$mode$ClientBase(options);\n\n"); + } + printer->Print(vars, + " /**\n" + " * @private @const {string} The hostname\n" + " */\n" + " this.hostname_ = hostname;\n\n" + "};\n\n\n"); +} - // Print AbstractClientBase.MethodInfo, which will be deprecated. +void PrintMethodDescriptor(Printer* printer, std::map vars) { printer->Print(vars, "/**\n" " * @const\n" - " * @type {!grpc.web.AbstractClientBase.MethodInfo<\n" + " * @type {!grpc.web.MethodDescriptor<\n" " * !proto.$in$,\n" " * !proto.$out$>}\n" " */\n" - "const methodInfo_$service_name$_$method_name$ = " - "new grpc.web.AbstractClientBase.MethodInfo(\n"); + "const methodDescriptor_$service_name$_$method_name$ = " + "new grpc.web.MethodDescriptor(\n"); printer->Indent(); - + printer->Print(vars, + "'/$package_dot$$service_name$/$method_name$',\n" + "$method_type$,\n" + "$in_type$,\n"); printer->Print(vars, "$out_type$,\n" "/**\n" " * @param {!proto.$in$} request\n"); printer->Print( - (" * @return {" + GetSerializeMethodReturnType(vars["mode"]) + "}\n") - .c_str()); - printer->Print(" */\n" - "function(request) {\n"); + (" * @return {" + GetSerializeMethodReturnType(vars) + "}\n").c_str()); + printer->Print( + " */\n" + "function(request) {\n"); printer->Print( - (" return request." + GetSerializeMethodName(vars["mode"]) + "();\n") - .c_str()); + (" return request." + GetSerializeMethodName(vars) + "();\n").c_str()); printer->Print("},\n"); printer->Print( - vars, - ("$out_type$." + GetDeserializeMethodName(vars["mode"]) + "\n").c_str()); + vars, ("$out_type$." + GetDeserializeMethodName(vars) + "\n").c_str()); printer->Outdent(); printer->Print(vars, ");\n\n\n"); } @@ -1233,7 +1178,7 @@ void PrintUnaryCall(Printer* printer, std::map vars) { " * request proto\n" " * @param {?Object} metadata User defined\n" " * call metadata\n" - " * @param {function(?grpc.web.Error," + " * @param {function(?grpc.web.RpcError," " ?proto.$out$)}\n" " * callback The callback function(error, response)\n" " * @return {!grpc.web.ClientReadableStream|undefined}\n" @@ -1246,8 +1191,7 @@ void PrintUnaryCall(Printer* printer, std::map vars) { "return this.client_.rpcCall(this.hostname_ +\n"); printer->Indent(); printer->Indent(); - if (vars["mode"] == GetModeVar(Mode::OP) || - vars["mode"] == GetModeVar(Mode::OPJSPB)) { + if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { @@ -1282,8 +1226,7 @@ void PrintPromiseUnaryCall(Printer* printer, std::map vars) { "return this.client_.unaryCall(this.hostname_ +\n"); printer->Indent(); printer->Indent(); - if (vars["mode"] == GetModeVar(Mode::OP) || - vars["mode"] == GetModeVar(Mode::OPJSPB)) { + if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { @@ -1317,8 +1260,7 @@ void PrintServerStreamingCall(Printer* printer, std::map vars) { "return this.client_.serverStreaming(this.hostname_ +\n"); printer->Indent(); printer->Indent(); - if (vars["mode"] == GetModeVar(Mode::OP) || - vars["mode"] == GetModeVar(Mode::OPJSPB)) { + if (vars["mode"] == GetModeVar(Mode::OP)) { printer->Print(vars, "'/$$rpc/$package_dot$$service_name$/$method_name$',\n"); } else { @@ -1408,7 +1350,7 @@ void PrintMultipleFilesMode(const FileDescriptor* file, string file_name, } printer1.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); printer1.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); - printer1.Print(vars, "goog.require('grpc.web.Error');\n"); + printer1.Print(vars, "goog.require('grpc.web.RpcError');\n"); printer2.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); if (has_server_streaming) { printer2.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); @@ -1424,8 +1366,8 @@ void PrintMultipleFilesMode(const FileDescriptor* file, string file_name, ++service_index) { const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); - PrintServiceConstructor(&printer1, vars); - PrintPromiseServiceConstructor(&printer2, vars); + PrintServiceConstructor(&printer1, vars, false); + PrintServiceConstructor(&printer2, vars, true); for (int method_index = 0; method_index < service->method_count(); ++method_index) { @@ -1642,6 +1584,7 @@ class GrpcCodeGenerator : public CodeGenerator { if ("binary" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::OP); + vars["binary"] = "true"; } else if ("grpcweb" == generator_options.mode()) { vars["mode"] = GetModeVar(Mode::GRPCWEB); vars["format"] = "binary"; @@ -1649,7 +1592,8 @@ class GrpcCodeGenerator : public CodeGenerator { vars["mode"] = GetModeVar(Mode::GRPCWEB); vars["format"] = "text"; } else if ("jspb" == generator_options.mode()) { - vars["mode"] = GetModeVar(Mode::OPJSPB); + vars["mode"] = GetModeVar(Mode::OP); + vars["binary"] = "false"; if (generator_options.goog_promise()) { vars["promise"] = GRPC_PROMISE; } @@ -1717,7 +1661,7 @@ class GrpcCodeGenerator : public CodeGenerator { printer.Print(vars, "goog.require('grpc.web.$mode$ClientBase');\n"); printer.Print(vars, "goog.require('grpc.web.AbstractClientBase');\n"); printer.Print(vars, "goog.require('grpc.web.ClientReadableStream');\n"); - printer.Print(vars, "goog.require('grpc.web.Error');\n"); + printer.Print(vars, "goog.require('grpc.web.RpcError');\n"); PrintClosureDependencies(&printer, file); @@ -1736,8 +1680,8 @@ class GrpcCodeGenerator : public CodeGenerator { ++service_index) { const ServiceDescriptor* service = file->service(service_index); vars["service_name"] = service->name(); - PrintServiceConstructor(&printer, vars); - PrintPromiseServiceConstructor(&printer, vars); + PrintServiceConstructor(&printer, vars, false); + PrintServiceConstructor(&printer, vars, true); for (int method_index = 0; method_index < service->method_count(); ++method_index) { @@ -1772,14 +1716,14 @@ class GrpcCodeGenerator : public CodeGenerator { if (!method->client_streaming()) { if (method->server_streaming()) { vars["method_type"] = "grpc.web.MethodType.SERVER_STREAMING"; - PrintMethodInfo(&printer, vars); + PrintMethodDescriptor(&printer, vars); vars["client_type"] = "Client"; PrintServerStreamingCall(&printer, vars); vars["client_type"] = "PromiseClient"; PrintServerStreamingCall(&printer, vars); } else { vars["method_type"] = "grpc.web.MethodType.UNARY"; - PrintMethodInfo(&printer, vars); + PrintMethodDescriptor(&printer, vars); PrintUnaryCall(&printer, vars); PrintPromiseUnaryCall(&printer, vars); } diff --git a/javascript/net/grpc/web/grpcwebclientbase.js b/javascript/net/grpc/web/grpcwebclientbase.js index 50510197..b1458f8a 100644 --- a/javascript/net/grpc/web/grpcwebclientbase.js +++ b/javascript/net/grpc/web/grpcwebclientbase.js @@ -32,12 +32,11 @@ const AbstractClientBase = goog.require('grpc.web.AbstractClientBase'); const ClientOptions = goog.requireType('grpc.web.ClientOptions'); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const ClientUnaryCallImpl = goog.require('grpc.web.ClientUnaryCallImpl'); -const Error = goog.require('grpc.web.Error'); const GrpcWebClientReadableStream = goog.require('grpc.web.GrpcWebClientReadableStream'); const HttpCors = goog.require('goog.net.rpc.HttpCors'); const MethodDescriptor = goog.requireType('grpc.web.MethodDescriptor'); -const MethodType = goog.require('grpc.web.MethodType'); const Request = goog.require('grpc.web.Request'); +const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const XhrIo = goog.require('goog.net.XhrIo'); const googCrypt = goog.require('goog.crypt.base64'); @@ -54,8 +53,9 @@ const {StreamInterceptor, UnaryInterceptor} = goog.require('grpc.web.Interceptor class GrpcWebClientBase { /** * @param {!ClientOptions=} options + * @param {!XhrIo=} xhrIo */ - constructor(options = {}) { + constructor(options = {}, xhrIo = undefined) { /** * @const * @private {string} @@ -90,6 +90,9 @@ class GrpcWebClientBase { */ this.unaryInterceptors_ = options.unaryInterceptors || goog.getObjectByName('unaryInterceptors', options) || []; + + /** @const @private {?XhrIo} */ + this.xhrIo_ = xhrIo || null; } /** @@ -97,13 +100,11 @@ class GrpcWebClientBase { * @export */ rpcCall(method, requestMessage, metadata, methodDescriptor, callback) { - methodDescriptor = AbstractClientBase.ensureMethodDescriptor( - method, requestMessage, MethodType.UNARY, methodDescriptor); - var hostname = AbstractClientBase.getHostname(method, methodDescriptor); - var invoker = GrpcWebClientBase.runInterceptors_( + const hostname = AbstractClientBase.getHostname(method, methodDescriptor); + const invoker = GrpcWebClientBase.runInterceptors_( (request) => this.startStream_(request, hostname), this.streamInterceptors_); - var stream = /** @type {!ClientReadableStream} */ (invoker.call( + const stream = /** @type {!ClientReadableStream} */ (invoker.call( this, methodDescriptor.createRequest(requestMessage, metadata))); GrpcWebClientBase.setCallback_(stream, callback, false); return new ClientUnaryCallImpl(stream); @@ -114,14 +115,12 @@ class GrpcWebClientBase { * @export */ thenableCall(method, requestMessage, metadata, methodDescriptor) { - methodDescriptor = AbstractClientBase.ensureMethodDescriptor( - method, requestMessage, MethodType.UNARY, methodDescriptor); - var hostname = AbstractClientBase.getHostname(method, methodDescriptor); - var initialInvoker = (request) => new Promise((resolve, reject) => { - var stream = this.startStream_(request, hostname); - var unaryMetadata; - var unaryStatus; - var unaryMsg; + const hostname = AbstractClientBase.getHostname(method, methodDescriptor); + const initialInvoker = (request) => new Promise((resolve, reject) => { + const stream = this.startStream_(request, hostname); + let unaryMetadata; + let unaryStatus; + let unaryMsg; GrpcWebClientBase.setCallback_( stream, (error, response, status, metadata) => { if (error) { @@ -138,9 +137,9 @@ class GrpcWebClientBase { } }, true); }); - var invoker = GrpcWebClientBase.runInterceptors_( + const invoker = GrpcWebClientBase.runInterceptors_( initialInvoker, this.unaryInterceptors_); - var unaryResponse = /** @type {!Promise} */ (invoker.call( + const unaryResponse = /** @type {!Promise} */ (invoker.call( this, methodDescriptor.createRequest(requestMessage, metadata))); return unaryResponse.then((response) => response.getResponseMessage()); } @@ -150,9 +149,8 @@ class GrpcWebClientBase { * @param {string} method The method to invoke * @param {REQUEST} requestMessage The request proto * @param {!Object} metadata User defined call metadata - * @param {!MethodDescriptor| - * !AbstractClientBase.MethodInfo} - * methodDescriptor Information of this RPC method + * @param {!MethodDescriptor} methodDescriptor Information + * of this RPC method * @return {!Promise} * @template REQUEST, RESPONSE */ @@ -166,10 +164,8 @@ class GrpcWebClientBase { * @export */ serverStreaming(method, requestMessage, metadata, methodDescriptor) { - methodDescriptor = AbstractClientBase.ensureMethodDescriptor( - method, requestMessage, MethodType.SERVER_STREAMING, methodDescriptor); - var hostname = AbstractClientBase.getHostname(method, methodDescriptor); - var invoker = GrpcWebClientBase.runInterceptors_( + const hostname = AbstractClientBase.getHostname(method, methodDescriptor); + const invoker = GrpcWebClientBase.runInterceptors_( (request) => this.startStream_(request, hostname), this.streamInterceptors_); return /** @type {!ClientReadableStream} */ (invoker.call( @@ -184,30 +180,30 @@ class GrpcWebClientBase { * @return {!ClientReadableStream} */ startStream_(request, hostname) { - var methodDescriptor = request.getMethodDescriptor(); - var path = hostname + methodDescriptor.getName(); + const methodDescriptor = request.getMethodDescriptor(); + let path = hostname + methodDescriptor.getName(); - var xhr = this.newXhr_(); + const xhr = this.xhrIo_ ? this.xhrIo_ : new XhrIo(); xhr.setWithCredentials(this.withCredentials_); - var genericTransportInterface = { + const genericTransportInterface = { xhr: xhr, }; - var stream = new GrpcWebClientReadableStream(genericTransportInterface); + const stream = new GrpcWebClientReadableStream(genericTransportInterface); stream.setResponseDeserializeFn( methodDescriptor.getResponseDeserializeFn()); xhr.headers.addAll(request.getMetadata()); this.processHeaders_(xhr); if (this.suppressCorsPreflight_) { - var headerObject = xhr.headers.toObject(); + const headerObject = xhr.headers.toObject(); xhr.headers.clear(); path = GrpcWebClientBase.setCorsOverride_(path, headerObject); } - var requestSerializeFn = methodDescriptor.getRequestSerializeFn(); - var serialized = requestSerializeFn(request.getRequestMessage()); - var payload = this.encodeRequest_(serialized); + const requestSerializeFn = methodDescriptor.getRequestSerializeFn(); + const serialized = requestSerializeFn(request.getRequestMessage()); + let payload = this.encodeRequest_(serialized); if (this.format_ == 'text') { payload = googCrypt.encodeByteArray(payload); } else if (this.format_ == 'binary') { @@ -222,8 +218,8 @@ class GrpcWebClientBase { * @static * @template RESPONSE * @param {!ClientReadableStream} stream - * @param {function(?Error, ?RESPONSE, ?Status=, ?Object=)| - * function(?Error,?RESPONSE)} callback + * @param {function(?RpcError, ?RESPONSE, ?Status=, ?Object=)| + * function(?RpcError,?RESPONSE)} callback * @param {boolean} useUnaryResponse */ static setCallback_(stream, callback, useUnaryResponse) { @@ -281,16 +277,6 @@ class GrpcWebClientBase { }); } - /** - * Create a new XhrIo object - * - * @private - * @return {!XhrIo} The created XhrIo object - */ - newXhr_() { - return new XhrIo(); - } - /** * Encode the grpc-web request * @@ -299,10 +285,10 @@ class GrpcWebClientBase { * @return {!Uint8Array} The application/grpc-web padded request */ encodeRequest_(serialized) { - var len = serialized.length; - var bytesArray = [0, 0, 0, 0]; - var payload = new Uint8Array(5 + len); - for (var i = 3; i >= 0; i--) { + let len = serialized.length; + const bytesArray = [0, 0, 0, 0]; + const payload = new Uint8Array(5 + len); + for (let i = 3; i >= 0; i--) { bytesArray[i] = (len % 256); len = len >>> 8; } @@ -327,7 +313,7 @@ class GrpcWebClientBase { if (xhr.headers.containsKey('deadline')) { const deadline = xhr.headers.get('deadline'); // in ms const currentTime = (new Date()).getTime(); - let timeout = Math.ceil(deadline - currentTime); + let timeout = Math.round(deadline - currentTime); xhr.headers.remove('deadline'); if (timeout === Infinity) { // grpc-timeout header defaults to infinity if not set. @@ -335,12 +321,6 @@ class GrpcWebClientBase { } if (timeout > 0) { xhr.headers.set('grpc-timeout', timeout + 'm'); - // Also set timeout on the xhr request to terminate the HTTP request - // if the server doesn't respond within the deadline. We use 110% of - // grpc-timeout for this to allow the server to terminate the connection - // with DEADLINE_EXCEEDED rather than terminating it in the Browser, but - // at least 1 second in case the user is on a high-latency network. - xhr.setTimeoutInterval(Math.max(1000, Math.ceil(timeout * 1.1))); } } } diff --git a/javascript/net/grpc/web/grpcwebclientbase_test.js b/javascript/net/grpc/web/grpcwebclientbase_test.js index 19b08455..fb4f93bc 100644 --- a/javascript/net/grpc/web/grpcwebclientbase_test.js +++ b/javascript/net/grpc/web/grpcwebclientbase_test.js @@ -19,332 +19,271 @@ goog.module('grpc.web.GrpcWebClientBaseTest'); goog.setTestOnly('grpc.web.GrpcWebClientBaseTest'); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); -const EventType = goog.require('goog.net.EventType'); const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase'); -const Map = goog.require('goog.structs.Map'); +const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); +const ReadyState = goog.require('goog.net.XmlHttp.ReadyState'); +const Request = goog.requireType('grpc.web.Request'); +const RpcError = goog.require('grpc.web.RpcError'); +const XhrIo = goog.require('goog.testing.net.XhrIo'); const googCrypt = goog.require('goog.crypt.base64'); -const googEvents = goog.require('goog.events'); const testSuite = goog.require('goog.testing.testSuite'); const {StreamInterceptor} = goog.require('grpc.web.Interceptor'); goog.require('goog.testing.jsunit'); -const REQUEST_BYTES = [1, 2, 3]; -const FAKE_METHOD = 'fake-method'; -const PROTO_FIELD_VALUE = 'meow'; const DEFAULT_UNARY_HEADERS = ['Content-Type', 'Accept', 'X-User-Agent', 'X-Grpc-Web']; const DEFAULT_UNARY_HEADER_VALUES = [ - 'application/grpc-web-text', 'application/grpc-web-text', - 'grpc-web-javascript/0.1', '1' + 'application/grpc-web-text', + 'application/grpc-web-text', + 'grpc-web-javascript/0.1', + '1', ]; -let dataCallback; -let expectedHeaders; -let expectedHeaderValues; - +const DEFAULT_RESPONSE_HEADERS = { + 'Content-Type': 'application/grpc-web-text', +}; testSuite({ - setUp: function() { - googEvents.listen = function(a, event_type, listener, d, e) { - if (event_type == EventType.READY_STATE_CHANGE) { - dataCallback = listener; - } - return; - }; - }, - - tearDown: function() { - expectedHeaders = null; - expectedHeaderValues = null; - }, + async testRpcResponse() { + const xhr = new XhrIo(); + const client = new GrpcWebClientBase(/* options= */ {}, xhr); + const methodDescriptor = createMethodDescriptor((bytes) => { + assertElementsEquals([4, 5, 6], [].slice.call(bytes)); + return new MockReply('value'); + }); - testRpcResponse: function() { - var client = new GrpcWebClientBase(); - client.newXhr_ = function() { - return new MockXhr({ - // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] - response: googCrypt.encodeByteArray(new Uint8Array( - [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), - }); - }; + const response = await new Promise((resolve, reject) => { + client.rpcCall( + 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, + (error, response) => { + assertNull(error); + resolve(response); + }); + // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] + xhr.simulatePartialResponse( + googCrypt.encodeByteArray(new Uint8Array( + [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), + DEFAULT_RESPONSE_HEADERS); + xhr.simulateReadyStateChange(ReadyState.COMPLETE); + }); - expectUnaryHeaders(); - client.rpcCall( - FAKE_METHOD, /** requestMessage */ {}, /** metadata */ {}, { - requestSerializeFn: function(request) { - return REQUEST_BYTES; - }, - responseDeserializeFn: function(bytes) { - assertElementsEquals([4, 5, 6], [].slice.call(bytes)); - return {'field1': PROTO_FIELD_VALUE}; - } - }, - function(error, response) { - assertNull(error); - assertEquals(PROTO_FIELD_VALUE, response['field1']); - }); - dataCallback(); + assertEquals('value', response.data); + const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); + assertElementsEquals(DEFAULT_UNARY_HEADERS, Object.keys(headers)); + assertElementsEquals(DEFAULT_UNARY_HEADER_VALUES, Object.values(headers)); }, - testDeadline: function() { - const client = new GrpcWebClientBase(); - client.newXhr_ = function() { - return new MockXhr({ - deadline: true, - response: googCrypt.encodeByteArray(new Uint8Array(0)), - }); - }; + async testDeadline() { + const xhr = new XhrIo(); + const client = new GrpcWebClientBase(/* options= */ {}, xhr); + const methodDescriptor = createMethodDescriptor((bytes) => new MockReply()); - expectUnaryHeaders(); const deadline = new Date(); deadline.setSeconds(deadline.getSeconds() + 1); - client.rpcCall( - FAKE_METHOD, /** requestMessage */ {}, {'deadline': deadline}, { - requestSerializeFn: (request) => REQUEST_BYTES, - responseDeserializeFn: (bytes) => {}, - - }, - (error, response) => assertNull(error)); - dataCallback(); + await new Promise((resolve, reject) => { + client.rpcCall( + 'url', new MockRequest(), {'deadline': deadline}, methodDescriptor, + (error, response) => { + assertNull(error); + resolve(); + }); + // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] + xhr.simulatePartialResponse( + googCrypt.encodeByteArray(new Uint8Array( + [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), + DEFAULT_RESPONSE_HEADERS); + xhr.simulateReadyStateChange(ReadyState.COMPLETE); + }); + const headers = /** @type {!Object} */ (xhr.getLastRequestHeaders()); + const headersWithDeadline = [...DEFAULT_UNARY_HEADERS, 'grpc-timeout']; + assertElementsEquals(headersWithDeadline, Object.keys(headers)); }, - testStreamInterceptor: function() { - var interceptor = new StreamResponseInterceptor(); - var client = new GrpcWebClientBase({'streamInterceptors': [interceptor]}); - client.newXhr_ = function() { - return new MockXhr({ - // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] - response: googCrypt.encodeByteArray(new Uint8Array( - [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), - }); - }; - - expectUnaryHeaders(); - client.rpcCall( - FAKE_METHOD, {}, {}, { - requestSerializeFn: function(request) { - return REQUEST_BYTES; - }, - responseDeserializeFn: function(bytes) { - assertElementsEquals([4, 5, 6], [].slice.call(bytes)); - return {'field1': PROTO_FIELD_VALUE}; - } - }, - function(error, response) { - assertNull(error); - assertEquals('field2', response['field2']); - }); - dataCallback(); + async testRpcError() { + const xhr = new XhrIo(); + const client = new GrpcWebClientBase(/* options= */ {}, xhr); + const methodDescriptor = createMethodDescriptor((bytes) => new MockReply()); + + const error = await new Promise((resolve, reject) => { + client.rpcCall( + 'urlurl', new MockRequest(), /* metadata= */ {}, methodDescriptor, + (error, response) => { + assertNull(response); + resolve(error); + }); + // This decodes to "grpc-status: 3" + xhr.simulatePartialResponse( + googCrypt.encodeByteArray(new Uint8Array([ + 128, 0, 0, 0, 14, 103, 114, 112, 99, 45, + 115, 116, 97, 116, 117, 115, 58, 32, 51, + ])), + DEFAULT_RESPONSE_HEADERS); + }); + assertTrue(error instanceof RpcError); + assertEquals(3, error.code); }, - testRpcError: function() { - var client = new GrpcWebClientBase(); - client.newXhr_ = function() { - return new MockXhr({ - // This decodes to "grpc-status: 3" - response: googCrypt.encodeByteArray(new Uint8Array([ - 128, 0, 0, 0, 14, 103, 114, 112, 99, 45, 115, 116, 97, 116, 117, 115, - 58, 32, 51 - ])), - }); - }; - - expectUnaryHeaders(); - client.rpcCall( - FAKE_METHOD, {}, {}, { - requestSerializeFn: function(request) { - return REQUEST_BYTES; - }, - responseDeserializeFn: function(bytes) { - return {}; - } - }, - function(error, response) { - assertNull(response); - assertEquals(3, error.code); - }); - dataCallback(); - }, + async testRpcResponseHeader() { + const xhr = new XhrIo(); + const client = new GrpcWebClientBase(/* options= */ {}, xhr); + const methodDescriptor = createMethodDescriptor((bytes) => { + assertElementsEquals([4, 5, 6], [].slice.call(bytes)); + return new MockReply('value'); + }); - testRpcResponseHeader: function() { - var client = new GrpcWebClientBase(); - client.newXhr_ = function() { - return new MockXhr({ - // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] - response: googCrypt.encodeByteArray(new Uint8Array( - [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), + const metadata = await new Promise((resolve, reject) => { + const call = client.rpcCall( + 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, + (error, response) => { + assertNull(error); + }); + call.on('metadata', (metadata) => { + resolve(metadata); }); - }; + // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] + xhr.simulatePartialResponse( + googCrypt.encodeByteArray(new Uint8Array( + [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), + { + 'Content-Type': 'application/grpc-web-text', + 'initial-metadata-key': 'initial-metadata-value', + }); + xhr.simulateReadyStateChange(ReadyState.COMPLETE); + }); + assertEquals('initial-metadata-value', metadata['initial-metadata-key']); + }, - expectUnaryHeaders(); - var call = client.rpcCall( - FAKE_METHOD, {}, {}, { - requestSerializeFn: function(request) { - return REQUEST_BYTES; - }, - responseDeserializeFn: function(bytes) { - assertElementsEquals([4, 5, 6], [].slice.call(bytes)); - return {'field1': PROTO_FIELD_VALUE}; - } - }, - function(error, response) { - assertNull(error); - assertEquals(PROTO_FIELD_VALUE, response['field1']); - }); - call.on('metadata', (metadata) => { - assertEquals( - metadata['sample-initial-metadata-1'], 'sample-initial-metadata-val'); + async testStreamInterceptor() { + const xhr = new XhrIo(); + const interceptor = new StreamResponseInterceptor(); + const methodDescriptor = createMethodDescriptor((bytes) => { + assertElementsEquals([4, 5, 6], [].slice.call(bytes)); + return new MockReply('value'); }); - dataCallback(); - } + const client = + new GrpcWebClientBase({'streamInterceptors': [interceptor]}, xhr); + + const response = await new Promise((resolve, reject) => { + client.rpcCall( + 'url', new MockRequest(), /* metadata= */ {}, methodDescriptor, + (error, response) => { + assertNull(error); + resolve(response); + }); + // This parses to [ { DATA: [4,5,6] }, { TRAILER: "a: b" } ] + xhr.simulatePartialResponse( + googCrypt.encodeByteArray(new Uint8Array( + [0, 0, 0, 0, 3, 4, 5, 6, 128, 0, 0, 0, 4, 97, 58, 32, 98])), + DEFAULT_RESPONSE_HEADERS); + xhr.simulateReadyStateChange(ReadyState.COMPLETE); + }); + assertEquals('Intercepted value', response.data); + }, }); - -/** Sets expected headers as the unary response headers */ -function expectUnaryHeaders() { - expectedHeaders = [...DEFAULT_UNARY_HEADERS]; - expectedHeaderValues = [...DEFAULT_UNARY_HEADER_VALUES]; -} - - -/** @unrestricted */ -class MockXhr { +/** Mocks a request proto object. */ +class MockRequest { /** - * @param {?Object} mockValues - * Mock XhrIO object to test the outgoing values + * @param {string=} data */ - constructor(mockValues) { - this.mockValues = mockValues; - this.headers = new Map(); - } - - /** - * @param {string} url - * @param {string=} method - * @param {string=} content - * @param {string=} headers - */ - send(url, method, content, headers) { - assertEquals(FAKE_METHOD, url); - assertEquals('POST', method); - assertElementsEquals( - googCrypt.encodeByteArray(new Uint8Array([0, 0, 0, 0, 3, 1, 2, 3])), - content); - if (!this.mockValues.deadline) { - assertElementsEquals(expectedHeaders, this.headers.getKeys()); - assertElementsEquals(expectedHeaderValues, this.headers.getValues()); - } else { - expectedHeaders.push('grpc-timeout'); - assertElementsEquals(expectedHeaders, this.headers.getKeys()); - } + constructor(data = '') { + /** @type {string} */ + this.data = data; } +} +/** Mocks a response proto object. */ +class MockReply { /** - * @param {number} ms + * @param {string=} data */ - setTimeoutInterval(ms) { - return; + constructor(data = '') { + /** @type {string} */ + this.data = data; } +} - /** - * @param {boolean} withCredentials - */ - setWithCredentials(withCredentials) { - return; - } +/** + * @param {function(string): !MockReply} responseDeSerializeFn + * @return {!MethodDescriptor} + */ +function createMethodDescriptor(responseDeSerializeFn) { + return new MethodDescriptor( + /* name= */ '', /* methodType= */ null, MockRequest, MockReply, + (request) => [1, 2, 3], responseDeSerializeFn); +} - /** - * @return {string} response - */ - getResponseText() { - return this.mockValues.response; - } - /** - * @param {string} key header key - * @return {string} content-type - */ - getStreamingResponseHeader(key) { - return 'application/grpc-web-text'; - } +/** + * @implements {StreamInterceptor} + * @unrestricted + */ +class StreamResponseInterceptor { + constructor() {} /** - * @return {string} response + * @override + * @template REQUEST, RESPONSE + * @param {!Request} request + * @param {function(!Request): + * !ClientReadableStream} invoker + * @return {!ClientReadableStream} */ - getResponseHeaders() { - return {'sample-initial-metadata-1': 'sample-initial-metadata-val'}; + intercept(request, invoker) { + return new InterceptedStream(invoker(request)); } +} +/** + * @implements {ClientReadableStream} + * @template RESPONSE + * @final + */ +class InterceptedStream { /** - * @return {number} xhr state + * @param {!ClientReadableStream} stream */ - getReadyState() { - return 0; + constructor(stream) { + /** @const {!ClientReadableStream} */ + this.stream = stream; } /** - * @return {number} lastErrorCode + * @override + * @param {string} eventType + * @param {function(?)} callback + * @return {!ClientReadableStream} */ - getLastErrorCode() { - return 0; + on(eventType, callback) { + if (eventType == 'data') { + const newCallback = (response) => { + response.data = 'Intercepted ' + response.data; + callback(response); + }; + this.stream.on(eventType, newCallback); + } else { + this.stream.on(eventType, callback); + } + return this; } /** - * @return {string} lastError + * @override + * @return {!ClientReadableStream} */ - getLastError() { - return 'server not responding'; + cancel() { + this.stream.cancel(); + return this; } /** - * @param {string} responseType + * @override + * @param {string} eventType + * @param {function(?)} callback + * @return {!ClientReadableStream} */ - setResponseType(responseType) { - return; - } -} - - - -/** - * @implements {StreamInterceptor} - * @unrestricted - */ -class StreamResponseInterceptor { - constructor() {} - - /** @override */ - intercept(request, invoker) { - /** - * @implements {ClientReadableStream} - * @constructor - * @param {!ClientReadableStream} stream - * @template RESPONSE - */ - const InterceptedStream = function(stream) { - this.stream = stream; - }; - - /** @override */ - InterceptedStream.prototype.on = function(eventType, callback) { - if (eventType == 'data') { - const newCallback = (response) => { - response['field2'] = 'field2'; - callback(response); - }; - this.stream.on(eventType, newCallback); - } else { - this.stream.on(eventType, callback); - } - return this; - }; - - /** @override */ - InterceptedStream.prototype.cancel = function() { - this.stream.cancel(); - return this; - }; - - return new InterceptedStream(invoker(request)); + removeListener(eventType, callback) { + this.stream.removeListener(eventType, callback); + return this; } } diff --git a/javascript/net/grpc/web/grpcwebclientreadablestream.js b/javascript/net/grpc/web/grpcwebclientreadablestream.js index 25872b86..a4a0d5ad 100644 --- a/javascript/net/grpc/web/grpcwebclientreadablestream.js +++ b/javascript/net/grpc/web/grpcwebclientreadablestream.js @@ -34,8 +34,8 @@ goog.module.declareLegacyNamespace(); const ClientReadableStream = goog.require('grpc.web.ClientReadableStream'); const ErrorCode = goog.require('goog.net.ErrorCode'); const EventType = goog.require('goog.net.EventType'); -const GrpcWebError = goog.require('grpc.web.Error'); const GrpcWebStreamParser = goog.require('grpc.web.GrpcWebStreamParser'); +const RpcError = goog.require('grpc.web.RpcError'); const StatusCode = goog.require('grpc.web.StatusCode'); const XhrIo = goog.require('goog.net.XhrIo'); const events = goog.require('goog.events'); @@ -104,7 +104,7 @@ class GrpcWebClientReadableStream { /** * @const * @private - * @type {!Array} The list of error callbacks + * @type {!Array} The list of error callbacks */ this.onErrorCallbacks_ = []; @@ -152,22 +152,16 @@ class GrpcWebClientReadableStream { var byteSource = new Uint8Array( /** @type {!ArrayBuffer} */ (self.xhr_.getResponse())); } else { - self.handleError_({ - code: StatusCode.UNKNOWN, - message: 'Unknown Content-type received.', - metadata: {}, - }); + self.handleError_(new RpcError( + StatusCode.UNKNOWN, 'Unknown Content-type received.')); return; } var messages = null; try { messages = self.parser_.parse(byteSource); } catch (err) { - self.handleError_({ - code: StatusCode.UNKNOWN, - message: 'Error in parsing response body', - metadata: {}, - }); + self.handleError_(new RpcError( + StatusCode.UNKNOWN, 'Error in parsing response body')); } if (messages) { var FrameType = GrpcWebStreamParser.FrameType; @@ -181,11 +175,9 @@ class GrpcWebClientReadableStream { self.sendDataCallbacks_(response); } } catch (err) { - self.handleError_({ - code: StatusCode.UNKNOWN, - message: 'Error in response deserializer function.', - metadata: {}, - }); + self.handleError_(new RpcError( + StatusCode.UNKNOWN, + 'Error in response deserializer function.')); } } } @@ -208,11 +200,8 @@ class GrpcWebClientReadableStream { grpcStatusMessage = trailers[GRPC_STATUS_MESSAGE]; delete trailers[GRPC_STATUS_MESSAGE]; } - self.handleError_({ - code: Number(grpcStatusCode), - message: grpcStatusMessage, - metadata: trailers, - }); + self.handleError_(new RpcError( + Number(grpcStatusCode), grpcStatusMessage, trailers)); } } } @@ -251,11 +240,8 @@ class GrpcWebClientReadableStream { if (grpcStatusCode == StatusCode.ABORTED && self.aborted_) { return; } - self.handleError_({ - code: grpcStatusCode, - message: ErrorCode.getDebugMessage(lastErrorCode), - metadata: {}, - }); + self.handleError_(new RpcError( + grpcStatusCode, ErrorCode.getDebugMessage(lastErrorCode))); return; } @@ -268,11 +254,9 @@ class GrpcWebClientReadableStream { grpcStatusMessage = self.xhr_.getResponseHeader(GRPC_STATUS_MESSAGE); } if (Number(grpcStatusCode) != StatusCode.OK) { - self.handleError_({ - code: Number(grpcStatusCode), - message: grpcStatusMessage, - metadata: responseHeaders - }); + self.handleError_(new RpcError( + Number(grpcStatusCode), grpcStatusMessage || '', + responseHeaders)); errorEmitted = true; } } @@ -375,15 +359,12 @@ class GrpcWebClientReadableStream { * A central place to handle errors * * @private - * @param {!GrpcWebError} error The error object + * @param {!RpcError} error The error object */ handleError_(error) { if (error.code != StatusCode.OK) { - this.sendErrorCallbacks_({ - code: error.code, - message: decodeURIComponent(error.message || ''), - metadata: error.metadata - }); + this.sendErrorCallbacks_(new RpcError( + error.code, decodeURIComponent(error.message || ''), error.metadata)); } this.sendStatusCallbacks_(/** @type {!Status} */ ({ code: error.code, @@ -424,7 +405,7 @@ class GrpcWebClientReadableStream { /** * @private - * @param {!GrpcWebError} error The error to send back + * @param {!RpcError} error The error to send back */ sendErrorCallbacks_(error) { for (var i = 0; i < this.onErrorCallbacks_.length; i++) { diff --git a/javascript/net/grpc/web/methodtype.js b/javascript/net/grpc/web/methodtype.js index 757edcc0..ad18b02a 100644 --- a/javascript/net/grpc/web/methodtype.js +++ b/javascript/net/grpc/web/methodtype.js @@ -1,7 +1,5 @@ /** - * @fileoverview Description of this file. - * - * grpc web MethodType + * @fileoverview gRPC-Web method types. */ goog.module('grpc.web.MethodType'); @@ -9,15 +7,18 @@ goog.module('grpc.web.MethodType'); goog.module.declareLegacyNamespace(); /** - * See grpc.web.AbstractClientBase. - * MethodType.UNARY for rpcCall/unaryCall. - * MethodType.SERVER_STREAMING for serverStreaming. + * Available method types: + * MethodType.UNARY: unary request and unary response. + * MethodType.SERVER_STREAMING: unary request and streaming responses. + * MethodType.BIDI_STREAMING: streaming requests and streaming responses. * * @enum {string} */ const MethodType = { 'UNARY': 'unary', - 'SERVER_STREAMING': 'server_streaming' + 'SERVER_STREAMING': 'server_streaming', + // Bidi streaming is experimental. Do not use. + 'BIDI_STREAMING': 'bidi_streaming', }; exports = MethodType; diff --git a/javascript/net/grpc/web/error.js b/javascript/net/grpc/web/rpcerror.js similarity index 54% rename from javascript/net/grpc/web/error.js rename to javascript/net/grpc/web/rpcerror.js index d4c9c500..a2ce8ebf 100644 --- a/javascript/net/grpc/web/error.js +++ b/javascript/net/grpc/web/rpcerror.js @@ -1,6 +1,6 @@ /** * - * Copyright 2018 Google LLC + * Copyright 2021 Google LLC * * Licensed under the Apache License, Version 2.0 (the "License"); * you may not use this file except in compliance with the License. @@ -20,25 +20,29 @@ * * gRPC-Web Error objects * - * @author stanleycheung@google.com (Stanley Cheung) + * @suppress {lintChecks} gRPC-Web is still using default goog.module exports + * right now, and the output of grpc_generator.cc uses goog.provide. */ -goog.module('grpc.web.Error'); - -goog.module.declareLegacyNamespace(); +goog.module('grpc.web.RpcError'); const Metadata = goog.require('grpc.web.Metadata'); - -/** @record */ -function Error() {} - -/** @export {(number|undefined)} */ -Error.prototype.code; - -/** @export {(string|undefined)} */ -Error.prototype.message; - -/** @export {(?Metadata|undefined)} */ -Error.prototype.metadata; - -exports = Error; +class RpcError extends Error { + /** + * @param {number} code + * @param {string} message + * @param {!Metadata=} metadata + */ + constructor(code, message, metadata = {}) { + super(message); + /** @type {number} */ + this.code = code; + /** @type {!Metadata} */ + this.metadata = metadata; + } +} + +/** @override */ +RpcError.prototype.name = 'RpcError'; + +exports = RpcError; diff --git a/net/grpc/gateway/examples/echo/commonjs-example/echotest.html b/net/grpc/gateway/examples/echo/commonjs-example/echotest.html index 88630df2..3fa1a348 100644 --- a/net/grpc/gateway/examples/echo/commonjs-example/echotest.html +++ b/net/grpc/gateway/examples/echo/commonjs-example/echotest.html @@ -32,7 +32,7 @@ -

Example: "Hello", "4 Hello"

+

Example: "Hello", "4 Hello", "err Hello"

diff --git a/net/grpc/gateway/examples/echo/echoapp.js b/net/grpc/gateway/examples/echo/echoapp.js index 1f6906a3..e4274e55 100644 --- a/net/grpc/gateway/examples/echo/echoapp.js +++ b/net/grpc/gateway/examples/echo/echoapp.js @@ -86,7 +86,7 @@ echoapp.EchoApp.prototype.echo = function(msg) { * @param {string} msg */ echoapp.EchoApp.prototype.echoError = function(msg) { - echoapp.EchoApp.addLeftMessage(msg); + echoapp.EchoApp.addLeftMessage(`Error: ${msg}`); var unaryRequest = new this.ctors.EchoRequest(); unaryRequest.setMessage(msg); this.echoService.echoAbort(unaryRequest, {}, function(err, response) { diff --git a/net/grpc/gateway/examples/echo/echotest.html b/net/grpc/gateway/examples/echo/echotest.html index 2809d746..ec5d08c4 100644 --- a/net/grpc/gateway/examples/echo/echotest.html +++ b/net/grpc/gateway/examples/echo/echotest.html @@ -48,7 +48,7 @@ -

Example: "Hello", "4 Hello"

+

Example: "Hello", "4 Hello", "err Hello"

diff --git a/net/grpc/gateway/examples/echo/ts-example/client.ts b/net/grpc/gateway/examples/echo/ts-example/client.ts index 5c7c8fa7..22f0c559 100644 --- a/net/grpc/gateway/examples/echo/ts-example/client.ts +++ b/net/grpc/gateway/examples/echo/ts-example/client.ts @@ -55,7 +55,7 @@ class EchoApp { request.setMessage(msg); const call = this.echoService.echo( request, {'custom-header-1': 'value1'}, - (err: grpcWeb.Error, response: EchoResponse) => { + (err: grpcWeb.RpcError, response: EchoResponse) => { if (err) { if (err.code !== grpcWeb.StatusCode.OK) { EchoApp.addRightMessage( @@ -75,18 +75,16 @@ class EchoApp { }); } - echoError() { - EchoApp.addLeftMessage('Error'); + echoError(msg: string) { + EchoApp.addLeftMessage(`Error: ${msg}`); const request = new EchoRequest(); - request.setMessage('error'); + request.setMessage(msg); this.echoService.echoAbort( - request, {}, (err: grpcWeb.Error, response: EchoResponse) => { - if (err) { - if (err.code !== grpcWeb.StatusCode.OK) { - EchoApp.addRightMessage( - 'Error code: ' + err.code + ' "' + decodeURI(err.message) + - '"'); - } + request, {}, (err: grpcWeb.RpcError, response: EchoResponse) => { + if (err && err.code !== grpcWeb.StatusCode.OK) { + EchoApp.addRightMessage( + 'Error code: ' + err.code + ' "' + decodeURI(err.message) + + '"'); } }); } @@ -120,7 +118,7 @@ class EchoApp { console.log(status.metadata); } }); - this.stream.on('error', (err: grpcWeb.Error) => { + this.stream.on('error', (err: grpcWeb.RpcError) => { EchoApp.addRightMessage( 'Error code: ' + err.code + ' "' + err.message + '"'); }); @@ -139,11 +137,11 @@ class EchoApp { const count = msg.substr(0, msg.indexOf(' ')); if (/^\d+$/.test(count)) { this.repeatEcho(msg.substr(msg.indexOf(' ') + 1), Number(count)); + } else if (count === 'err') { + this.echoError(msg.substr(msg.indexOf(' ') + 1)); } else { this.echo(msg); } - } else if (msg === 'error') { - this.echoError(); } else if (msg === 'cancel') { this.cancel(); } else { diff --git a/net/grpc/gateway/examples/echo/ts-example/echotest.html b/net/grpc/gateway/examples/echo/ts-example/echotest.html index 88630df2..3fa1a348 100644 --- a/net/grpc/gateway/examples/echo/ts-example/echotest.html +++ b/net/grpc/gateway/examples/echo/ts-example/echotest.html @@ -32,7 +32,7 @@ -

Example: "Hello", "4 Hello"

+

Example: "Hello", "4 Hello", "err Hello"

diff --git a/packages/grpc-web/README.md b/packages/grpc-web/README.md index 692ff51b..e6a97534 100644 --- a/packages/grpc-web/README.md +++ b/packages/grpc-web/README.md @@ -8,7 +8,7 @@ gRPC-Web is now Generally Available, and considered stable enough for production gRPC-Web clients connect to gRPC services via a special gateway proxy: the current version of the library uses [Envoy](https://www.envoyproxy.io/) by -default, in which gRPC-Web support is built-in. +default, in which gRPC-Web support is built-in. In the future, we expect gRPC-Web to be supported in language-specific Web frameworks, such as Python, Java, and Node. See the @@ -113,7 +113,7 @@ const request = new EchoRequest(); request.setMessage('Hello World!'); const call = echoService.echo(request, {'custom-header-1': 'value1'}, - (err: grpcWeb.Error, response: EchoResponse) => { + (err: grpcWeb.RpcError, response: EchoResponse) => { console.log(response.getMessage()); }); call.on('status', (status: grpcWeb.Status) => { diff --git a/packages/grpc-web/exports.js b/packages/grpc-web/exports.js index 9f3b7988..7dd34d61 100644 --- a/packages/grpc-web/exports.js +++ b/packages/grpc-web/exports.js @@ -6,13 +6,11 @@ */ goog.module('grpc.web.Exports'); -const AbstractClientBase = goog.require('grpc.web.AbstractClientBase'); const GrpcWebClientBase = goog.require('grpc.web.GrpcWebClientBase'); const StatusCode = goog.require('grpc.web.StatusCode'); const MethodDescriptor = goog.require('grpc.web.MethodDescriptor'); const MethodType = goog.require('grpc.web.MethodType'); -module['exports']['AbstractClientBase'] = {'MethodInfo': AbstractClientBase.MethodInfo}; module['exports']['GrpcWebClientBase'] = GrpcWebClientBase; module['exports']['StatusCode'] = StatusCode; module['exports']['MethodDescriptor'] = MethodDescriptor; diff --git a/packages/grpc-web/index.d.ts b/packages/grpc-web/index.d.ts index e7612ed9..4b537ce8 100644 --- a/packages/grpc-web/index.d.ts +++ b/packages/grpc-web/index.d.ts @@ -2,41 +2,33 @@ declare module "grpc-web" { export interface Metadata { [s: string]: string; } - export namespace AbstractClientBase { - class MethodInfo { - constructor (responseType: new () => RESP, - requestSerializeFn: (request: REQ) => {}, - responseDeserializeFn: (bytes: Uint8Array) => RESP); - } - } - export class AbstractClientBase { thenableCall ( method: string, request: REQ, metadata: Metadata, - methodDescriptor: MethodDescriptor | AbstractClientBase.MethodInfo + methodDescriptor: MethodDescriptor ): Promise; rpcCall ( method: string, request: REQ, metadata: Metadata, - methodDescriptor: MethodDescriptor | AbstractClientBase.MethodInfo, - callback: (err: Error, response: RESP) => void + methodDescriptor: MethodDescriptor, + callback: (err: RpcError, response: RESP) => void ): ClientReadableStream; serverStreaming ( method: string, request: REQ, metadata: Metadata, - methodDescriptor: MethodDescriptor | AbstractClientBase.MethodInfo + methodDescriptor: MethodDescriptor ): ClientReadableStream; } export class ClientReadableStream { on (eventType: "error", - callback: (err: Error) => void): ClientReadableStream; + callback: (err: RpcError) => void): ClientReadableStream; on (eventType: "status", callback: (status: Status) => void): ClientReadableStream; on (eventType: "metadata", @@ -47,7 +39,7 @@ declare module "grpc-web" { callback: () => void): ClientReadableStream; removeListener (eventType: "error", - callback: (err: Error) => void): void; + callback: (err: RpcError) => void): void; removeListener (eventType: "status", callback: (status: Status) => void): void; removeListener (eventType: "metadata", @@ -56,7 +48,7 @@ declare module "grpc-web" { callback: (response: RESP) => void): void; removeListener (eventType: "end", callback: () => void): void; - + cancel (): void; } @@ -96,14 +88,14 @@ declare module "grpc-web" { getResponseDeserializeFn(): any; getRequestSerializeFn(): any; } - + export class Request { getRequestMessage(): REQ; getMethodDescriptor(): MethodDescriptor; getMetadata(): Metadata; getCallOptions(): CallOptions; } - + export class UnaryResponse { getResponseMessage(): RESP; getMetadata(): Metadata; @@ -123,9 +115,10 @@ declare module "grpc-web" { constructor(options?: GrpcWebClientBaseOptions); } - export interface Error { - code: number; - message: string; + export class RpcError extends Error { + constructor(code: StatusCode, message: string, metadata: Metadata); + code: StatusCode; + metadata: Metadata; } export interface Status { @@ -134,24 +127,24 @@ declare module "grpc-web" { metadata?: Metadata; } - export namespace StatusCode { - const ABORTED: number; - const ALREADY_EXISTS: number; - const CANCELLED: number; - const DATA_LOSS: number; - const DEADLINE_EXCEEDED: number; - const FAILED_PRECONDITION: number; - const INTERNAL: number; - const INVALID_ARGUMENT: number; - const NOT_FOUND: number; - const OK: number; - const OUT_OF_RANGE: number; - const PERMISSION_DENIED: number; - const RESOURCE_EXHAUSTED: number; - const UNAUTHENTICATED: number; - const UNAVAILABLE: number; - const UNIMPLEMENTED: number; - const UNKNOWN: number; + export enum StatusCode { + ABORTED, + ALREADY_EXISTS, + CANCELLED, + DATA_LOSS, + DEADLINE_EXCEEDED, + FAILED_PRECONDITION, + INTERNAL, + INVALID_ARGUMENT, + NOT_FOUND, + OK, + OUT_OF_RANGE, + PERMISSION_DENIED, + RESOURCE_EXHAUSTED, + UNAUTHENTICATED, + UNAVAILABLE, + UNIMPLEMENTED, + UNKNOWN, } export namespace MethodType { diff --git a/packages/grpc-web/package.json b/packages/grpc-web/package.json index 1ed92bc3..6d505203 100644 --- a/packages/grpc-web/package.json +++ b/packages/grpc-web/package.json @@ -29,7 +29,7 @@ "command-exists": "~1.2.8", "google-closure-compiler": "~20200224.0.0", "google-closure-deps": "~20210601.0.0", - "google-closure-library": "~20201102.0.1", + "google-closure-library": "~20210808.0.0", "google-protobuf": "~3.14.0", "gulp": "~4.0.2", "gulp-connect": "~5.7.0", diff --git a/packages/grpc-web/protractor_spec.js b/packages/grpc-web/protractor_spec.js index 2554a7bb..6fef760a 100644 --- a/packages/grpc-web/protractor_spec.js +++ b/packages/grpc-web/protractor_spec.js @@ -98,6 +98,10 @@ describe('Run all Closure unit tests', function() { }); }); + if (!allTests.length) { + throw new Error('Cannot find any JsUnit tests!!'); + } + // Run all tests. for (var i = 0; i < allTests.length; i++) { var testPath = allTests[i]; diff --git a/packages/grpc-web/test/export_test.js b/packages/grpc-web/test/export_test.js index 07069bb8..f26a8a4e 100644 --- a/packages/grpc-web/test/export_test.js +++ b/packages/grpc-web/test/export_test.js @@ -3,10 +3,6 @@ const grpc = {}; grpc.web = require('grpc-web'); describe('grpc-web export test', function() { - it('should have AbstractClientBase.MethodInfo exported', function() { - assert.equal(typeof grpc.web.AbstractClientBase.MethodInfo, 'function'); - }); - it('should have MethodDescriptor exported', function() { assert.equal(typeof grpc.web.MethodDescriptor, 'function'); }); diff --git a/packages/grpc-web/test/tsc-tests/client02.ts b/packages/grpc-web/test/tsc-tests/client02.ts index 5e260abb..a460ad63 100644 --- a/packages/grpc-web/test/tsc-tests/client02.ts +++ b/packages/grpc-web/test/tsc-tests/client02.ts @@ -23,5 +23,5 @@ import {MyServiceClient} from './generated/Test02ServiceClientPb'; const service = new MyServiceClient('http://mydummy.com', null, null); const req = new Integer(); -service.addOne(req, {}, (err: grpcWeb.Error, resp: Integer) => { +service.addOne(req, {}, (err: grpcWeb.RpcError, resp: Integer) => { }); diff --git a/packages/grpc-web/test/tsc-tests/client05.ts b/packages/grpc-web/test/tsc-tests/client05.ts index c45dbc9a..a8b47be2 100644 --- a/packages/grpc-web/test/tsc-tests/client05.ts +++ b/packages/grpc-web/test/tsc-tests/client05.ts @@ -30,7 +30,7 @@ req.setMessage('aaa'); // this test tries to make sure that these types are as accurate as possible let call1 : grpcWeb.ClientReadableStream = - echoService.echo(req, {}, (err: grpcWeb.Error, + echoService.echo(req, {}, (err: grpcWeb.RpcError, response: EchoResponse) => { }); @@ -46,5 +46,5 @@ let call2 : grpcWeb.ClientReadableStream = call2 .on('data', (response: ServerStreamingEchoResponse) => { }) - .on('error', (error: grpcWeb.Error) => { + .on('error', (error: grpcWeb.RpcError) => { });