diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 1fccc378..e3e809e6 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -15,4 +15,11 @@ The following is a set of guidelines for contributing to `ts-protoc-gen`. Please review and follow our [Code of Conduct](https://github.com/improbable-eng/ts-protoc-gen/blob/master/README.md). ## Releasing -Your changes will be released with the next version release. \ No newline at end of file +Your changes will be released with the next version release. + +## Debugging +You can attach the Chrome Inspector when running the tests by setting the `MOCHA_DEBUG` environment variable before running the tests, ie: + +``` +MOCHA_DEBUG=true npm test +``` \ No newline at end of file diff --git a/README.md b/README.md index b5ae07f0..ad64ccfa 100644 --- a/README.md +++ b/README.md @@ -28,6 +28,7 @@ For our latest build straight from master: ```bash npm install ts-protoc-gen@next ``` + ### bazel Include the following in your `WORKSPACE`: ```python diff --git a/examples/generated/proto/examplecom/simple_service_pb.d.ts b/examples/generated/proto/examplecom/simple_service_pb.d.ts index f8c9f620..3f35e95b 100644 --- a/examples/generated/proto/examplecom/simple_service_pb.d.ts +++ b/examples/generated/proto/examplecom/simple_service_pb.d.ts @@ -3,6 +3,7 @@ import * as jspb from "google-protobuf"; import * as proto_othercom_external_child_message_pb from "../../proto/othercom/external_child_message_pb"; +import * as google_protobuf_empty_pb from "google-protobuf/google/protobuf/empty_pb"; import * as google_protobuf_timestamp_pb from "google-protobuf/google/protobuf/timestamp_pb"; export class UnaryRequest extends jspb.Message { diff --git a/examples/generated/proto/examplecom/simple_service_pb.js b/examples/generated/proto/examplecom/simple_service_pb.js index 1b4b5107..2674f47d 100644 --- a/examples/generated/proto/examplecom/simple_service_pb.js +++ b/examples/generated/proto/examplecom/simple_service_pb.js @@ -12,6 +12,7 @@ var goog = jspb; var global = Function('return this')(); var proto_othercom_external_child_message_pb = require('../../proto/othercom/external_child_message_pb.js'); +var google_protobuf_empty_pb = require('google-protobuf/google/protobuf/empty_pb.js'); var google_protobuf_timestamp_pb = require('google-protobuf/google/protobuf/timestamp_pb.js'); goog.exportSymbol('proto.examplecom.StreamRequest', null, global); goog.exportSymbol('proto.examplecom.UnaryRequest', null, global); diff --git a/examples/generated/proto/examplecom/simple_service_pb_service.d.ts b/examples/generated/proto/examplecom/simple_service_pb_service.d.ts index b2a436ba..cf7f30c8 100644 --- a/examples/generated/proto/examplecom/simple_service_pb_service.d.ts +++ b/examples/generated/proto/examplecom/simple_service_pb_service.d.ts @@ -3,6 +3,7 @@ import * as proto_examplecom_simple_service_pb from "../../proto/examplecom/simple_service_pb"; import * as proto_othercom_external_child_message_pb from "../../proto/othercom/external_child_message_pb"; +import * as google_protobuf_empty_pb from "google-protobuf/google/protobuf/empty_pb"; import {grpc} from "grpc-web-client"; type SimpleServiceDoUnary = { @@ -14,7 +15,7 @@ type SimpleServiceDoUnary = { readonly responseType: typeof proto_othercom_external_child_message_pb.ExternalChildMessage; }; -type SimpleServiceDoStream = { +type SimpleServiceDoServerStream = { readonly methodName: string; readonly service: typeof SimpleService; readonly requestStream: false; @@ -23,6 +24,24 @@ type SimpleServiceDoStream = { readonly responseType: typeof proto_othercom_external_child_message_pb.ExternalChildMessage; }; +type SimpleServiceDoClientStream = { + readonly methodName: string; + readonly service: typeof SimpleService; + readonly requestStream: true; + readonly responseStream: false; + readonly requestType: typeof proto_examplecom_simple_service_pb.StreamRequest; + readonly responseType: typeof google_protobuf_empty_pb.Empty; +}; + +type SimpleServiceDoBidiStream = { + readonly methodName: string; + readonly service: typeof SimpleService; + readonly requestStream: true; + readonly responseStream: true; + readonly requestType: typeof proto_examplecom_simple_service_pb.StreamRequest; + readonly responseType: typeof proto_othercom_external_child_message_pb.ExternalChildMessage; +}; + type SimpleServiceDelete = { readonly methodName: string; readonly service: typeof SimpleService; @@ -35,7 +54,9 @@ type SimpleServiceDelete = { export class SimpleService { static readonly serviceName: string; static readonly DoUnary: SimpleServiceDoUnary; - static readonly DoStream: SimpleServiceDoStream; + static readonly DoServerStream: SimpleServiceDoServerStream; + static readonly DoClientStream: SimpleServiceDoClientStream; + static readonly DoBidiStream: SimpleServiceDoBidiStream; static readonly Delete: SimpleServiceDelete; } @@ -49,6 +70,21 @@ interface ResponseStream { on(type: 'end', handler: () => void): ResponseStream; on(type: 'status', handler: (status: Status) => void): ResponseStream; } +interface RequestStream { + write(message: T): RequestStream; + end(): void; + cancel(): void; + on(type: 'end', handler: () => void): RequestStream; + on(type: 'status', handler: (status: Status) => void): RequestStream; +} +interface BidirectionalStream { + write(message: T): BidirectionalStream; + end(): void; + cancel(): void; + on(type: 'data', handler: (message: T) => void): BidirectionalStream; + on(type: 'end', handler: () => void): BidirectionalStream; + on(type: 'status', handler: (status: Status) => void): BidirectionalStream; +} export class SimpleServiceClient { readonly serviceHost: string; @@ -63,7 +99,9 @@ export class SimpleServiceClient { requestMessage: proto_examplecom_simple_service_pb.UnaryRequest, callback: (error: ServiceError, responseMessage: proto_othercom_external_child_message_pb.ExternalChildMessage|null) => void ): void; - doStream(requestMessage: proto_examplecom_simple_service_pb.StreamRequest, metadata?: grpc.Metadata): ResponseStream; + doServerStream(requestMessage: proto_examplecom_simple_service_pb.StreamRequest, metadata?: grpc.Metadata): ResponseStream; + doClientStream(metadata?: grpc.Metadata): RequestStream; + doBidiStream(metadata?: grpc.Metadata): BidirectionalStream; delete( requestMessage: proto_examplecom_simple_service_pb.UnaryRequest, metadata: grpc.Metadata, diff --git a/examples/generated/proto/examplecom/simple_service_pb_service.js b/examples/generated/proto/examplecom/simple_service_pb_service.js index 7e5d456c..14a5a407 100644 --- a/examples/generated/proto/examplecom/simple_service_pb_service.js +++ b/examples/generated/proto/examplecom/simple_service_pb_service.js @@ -3,6 +3,7 @@ var proto_examplecom_simple_service_pb = require("../../proto/examplecom/simple_service_pb"); var proto_othercom_external_child_message_pb = require("../../proto/othercom/external_child_message_pb"); +var google_protobuf_empty_pb = require("google-protobuf/google/protobuf/empty_pb"); var grpc = require("grpc-web-client").grpc; var SimpleService = (function () { @@ -20,8 +21,8 @@ SimpleService.DoUnary = { responseType: proto_othercom_external_child_message_pb.ExternalChildMessage }; -SimpleService.DoStream = { - methodName: "DoStream", +SimpleService.DoServerStream = { + methodName: "DoServerStream", service: SimpleService, requestStream: false, responseStream: true, @@ -29,6 +30,24 @@ SimpleService.DoStream = { responseType: proto_othercom_external_child_message_pb.ExternalChildMessage }; +SimpleService.DoClientStream = { + methodName: "DoClientStream", + service: SimpleService, + requestStream: true, + responseStream: false, + requestType: proto_examplecom_simple_service_pb.StreamRequest, + responseType: google_protobuf_empty_pb.Empty +}; + +SimpleService.DoBidiStream = { + methodName: "DoBidiStream", + service: SimpleService, + requestStream: true, + responseStream: true, + requestType: proto_examplecom_simple_service_pb.StreamRequest, + responseType: proto_othercom_external_child_message_pb.ExternalChildMessage +}; + SimpleService.Delete = { methodName: "Delete", service: SimpleService, @@ -67,13 +86,13 @@ SimpleServiceClient.prototype.doUnary = function doUnary(requestMessage, metadat }); }; -SimpleServiceClient.prototype.doStream = function doStream(requestMessage, metadata) { +SimpleServiceClient.prototype.doServerStream = function doServerStream(requestMessage, metadata) { var listeners = { data: [], end: [], status: [] }; - var client = grpc.invoke(SimpleService.DoStream, { + var client = grpc.invoke(SimpleService.DoServerStream, { request: requestMessage, host: this.serviceHost, metadata: metadata, @@ -106,6 +125,92 @@ SimpleServiceClient.prototype.doStream = function doStream(requestMessage, metad }; }; +SimpleServiceClient.prototype.doClientStream = function doClientStream(metadata) { + var listeners = { + end: [], + status: [] + }; + var client = grpc.client(SimpleService.DoClientStream, { + host: this.serviceHost, + metadata: metadata, + transport: this.options.transport + }); + client.onEnd(function (status, statusMessage, trailers) { + listeners.end.forEach(function (handler) { + handler(); + }); + listeners.status.forEach(function (handler) { + handler({ code: status, details: statusMessage, metadata: trailers }); + }); + listeners = null; + }); + return { + on: function (type, handler) { + listeners[type].push(handler); + return this; + }, + write: function (requestMessage) { + if (!client.started) { + client.start(metadata); + } + client.send(requestMessage); + return this; + }, + end: function () { + client.finishSend(); + }, + cancel: function () { + listeners = null; + client.close(); + } + }; +}; + +SimpleServiceClient.prototype.doBidiStream = function doBidiStream(metadata) { + var listeners = { + data: [], + end: [], + status: [] + }; + var client = grpc.client(SimpleService.DoBidiStream, { + host: this.serviceHost, + metadata: metadata, + transport: this.options.transport + }); + client.onEnd(function (status, statusMessage, trailers) { + listeners.end.forEach(function (handler) { + handler(); + }); + listeners.status.forEach(function (handler) { + handler({ code: status, details: statusMessage, metadata: trailers }); + }); + listeners = null; + }); + client.onMessage(function (message) { + listeners.data.forEach(function (handler) { + handler(message); + }) + }); + client.start(metadata); + return { + on: function (type, handler) { + listeners[type].push(handler); + return this; + }, + write: function (requestMessage) { + client.send(requestMessage); + return this; + }, + end: function () { + client.finishSend(); + }, + cancel: function () { + listeners = null; + client.close(); + } + }; +}; + SimpleServiceClient.prototype.delete = function pb_delete(requestMessage, metadata, callback) { if (arguments.length === 2) { callback = arguments[1]; diff --git a/examples/generated/proto/orphan_pb_service.d.ts b/examples/generated/proto/orphan_pb_service.d.ts index 727476d8..de676141 100644 --- a/examples/generated/proto/orphan_pb_service.d.ts +++ b/examples/generated/proto/orphan_pb_service.d.ts @@ -38,6 +38,21 @@ interface ResponseStream { on(type: 'end', handler: () => void): ResponseStream; on(type: 'status', handler: (status: Status) => void): ResponseStream; } +interface RequestStream { + write(message: T): RequestStream; + end(): void; + cancel(): void; + on(type: 'end', handler: () => void): RequestStream; + on(type: 'status', handler: (status: Status) => void): RequestStream; +} +interface BidirectionalStream { + write(message: T): BidirectionalStream; + end(): void; + cancel(): void; + on(type: 'data', handler: (message: T) => void): BidirectionalStream; + on(type: 'end', handler: () => void): BidirectionalStream; + on(type: 'status', handler: (status: Status) => void): BidirectionalStream; +} export class OrphanServiceClient { readonly serviceHost: string; diff --git a/package-lock.json b/package-lock.json index 3ab159c1..c7a3432a 100644 --- a/package-lock.json +++ b/package-lock.json @@ -302,9 +302,9 @@ "dev": true }, "grpc-web-client": { - "version": "0.5.0", - "resolved": "https://registry.npmjs.org/grpc-web-client/-/grpc-web-client-0.5.0.tgz", - "integrity": "sha512-AcLecuqaDp5STYXGViTQmNTCoZVfM6gi3+hvfTGXGP5YTIimASesNi39jnP8dox3x8QBelMWDdOck5/4UJaZdg==", + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/grpc-web-client/-/grpc-web-client-0.6.2.tgz", + "integrity": "sha512-7LSp9Gr0cWLJp53ijrNW+KHNyw8rZKDMMmFKmRkxu1QJLU9vuE+5hG4NTc8ao5YwuP1rsBQi74eZ1R+sOiQVtg==", "dev": true, "requires": { "browser-headers": "0.4.0" diff --git a/package.json b/package.json index 15dc1ed1..4c4dee74 100644 --- a/package.json +++ b/package.json @@ -39,7 +39,7 @@ "@types/node": "^7.0.52", "babel": "^6.5.2", "chai": "^3.5.0", - "grpc-web-client": "^0.5.0", + "grpc-web-client": "^0.6.0", "lodash": "^4.17.5", "lodash.isequal": "^4.5.0", "mocha": "^5.2.0", diff --git a/proto/examplecom/simple_service.proto b/proto/examplecom/simple_service.proto index dcd5feb2..8cf2571c 100644 --- a/proto/examplecom/simple_service.proto +++ b/proto/examplecom/simple_service.proto @@ -4,7 +4,8 @@ package examplecom; import "proto/othercom/external_child_message.proto"; -// this import should not be output in the generated typescript service +// these imports should not be output in the generated typescript service +import "google/protobuf/empty.proto"; import "google/protobuf/timestamp.proto"; message UnaryRequest { @@ -20,7 +21,9 @@ message StreamRequest { service SimpleService { rpc DoUnary(UnaryRequest) returns (othercom.ExternalChildMessage) {} - rpc DoStream(StreamRequest) returns (stream othercom.ExternalChildMessage) {} + rpc DoServerStream(StreamRequest) returns (stream othercom.ExternalChildMessage) {} + rpc DoClientStream(stream StreamRequest) returns (google.protobuf.Empty) {} + rpc DoBidiStream(stream StreamRequest) returns (stream othercom.ExternalChildMessage) {} // checks that rpc methods that use reserved JS words don't generate invalid code rpc Delete(UnaryRequest) returns (UnaryResponse) {} diff --git a/src/service/grpcweb.ts b/src/service/grpcweb.ts index fbdd87b4..b08a070f 100644 --- a/src/service/grpcweb.ts +++ b/src/service/grpcweb.ts @@ -212,6 +212,21 @@ function generateTypescriptDefinition(fileDescriptor: FileDescriptorProto, expor printer.printIndentedLn(`on(type: 'end', handler: () => void): ResponseStream;`); printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): ResponseStream;`); printer.printLn(`}`); + printer.printLn(`interface RequestStream {`); + printer.printIndentedLn(`write(message: T): RequestStream;`); + printer.printIndentedLn(`end(): void;`); + printer.printIndentedLn(`cancel(): void;`); + printer.printIndentedLn(`on(type: 'end', handler: () => void): RequestStream;`); + printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): RequestStream;`); + printer.printLn(`}`); + printer.printLn(`interface BidirectionalStream {`); + printer.printIndentedLn(`write(message: T): BidirectionalStream;`); + printer.printIndentedLn(`end(): void;`); + printer.printIndentedLn(`cancel(): void;`); + printer.printIndentedLn(`on(type: 'data', handler: (message: T) => void): BidirectionalStream;`); + printer.printIndentedLn(`on(type: 'end', handler: () => void): BidirectionalStream;`); + printer.printIndentedLn(`on(type: 'status', handler: (status: Status) => void): BidirectionalStream;`); + printer.printLn(`}`); printer.printEmptyLn(); // Add a client stub that talks with the grpc-web-client library @@ -371,18 +386,96 @@ function printServerStreamStubMethod(printer: CodePrinter, method: RPCMethodDesc .dedent().printLn(`};`); } -function printBidirectionalStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { +function printClientStreamStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { printer - .printLn(`${method.serviceName}.prototype.${method.nameAsCamelCase} = function ${method.functionName}() {`) - .indent().printLn(`throw new Error("Client streaming is not currently supported");`) - .dedent().printLn(`}`); + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(metadata) {`) + .indent().printLn(`var listeners = {`) + .indent().printLn(`end: [],`) + .printLn(`status: []`) + .dedent().printLn(`};`) + .printLn(`var client = grpc.client(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport`) + .dedent().printLn(`});`) + .printLn(`client.onEnd(function (status, statusMessage, trailers) {`) + .indent().printLn(`listeners.end.forEach(function (handler) {`) + .indent().printLn(`handler();`) + .dedent().printLn(`});`) + .printLn(`listeners.status.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners = null;`) + .dedent().printLn(`});`) + .printLn(`return {`) + .indent().printLn(`on: function (type, handler) {`) + .indent().printLn(`listeners[type].push(handler);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`write: function (requestMessage) {`) + .indent().printLn(`if (!client.started) {`) + .indent().printLn(`client.start(metadata);`) + .dedent().printLn(`}`) + .printLn(`client.send(requestMessage);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`end: function () {`) + .indent().printLn(`client.finishSend();`) + .dedent().printLn(`},`) + .printLn(`cancel: function () {`) + .indent().printLn(`listeners = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); } -function printClientStreamStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { +function printBidirectionalStubMethod(printer: CodePrinter, method: RPCMethodDescriptor) { printer - .printLn(`${method.serviceName}.prototype.${method.nameAsCamelCase} = function ${method.functionName}() {`) - .indent().printLn(`throw new Error("Bi-directional streaming is not currently supported");`) - .dedent().printLn(`}`); + .printLn(`${method.serviceName}Client.prototype.${method.nameAsCamelCase} = function ${method.functionName}(metadata) {`) + .indent().printLn(`var listeners = {`) + .indent().printLn(`data: [],`) + .printLn(`end: [],`) + .printLn(`status: []`) + .dedent().printLn(`};`) + .printLn(`var client = grpc.client(${method.serviceName}.${method.nameAsPascalCase}, {`) + .indent().printLn(`host: this.serviceHost,`) + .printLn(`metadata: metadata,`) + .printLn(`transport: this.options.transport`) + .dedent().printLn(`});`) + .printLn(`client.onEnd(function (status, statusMessage, trailers) {`) + .indent().printLn(`listeners.end.forEach(function (handler) {`) + .indent().printLn(`handler();`) + .dedent().printLn(`});`) + .printLn(`listeners.status.forEach(function (handler) {`) + .indent().printLn(`handler({ code: status, details: statusMessage, metadata: trailers });`) + .dedent().printLn(`});`) + .printLn(`listeners = null;`) + .dedent().printLn(`});`) + .printLn(`client.onMessage(function (message) {`) + .indent().printLn(`listeners.data.forEach(function (handler) {`) + .indent().printLn(`handler(message);`) + .dedent().printLn(`})`) + .dedent().printLn(`});`) + .printLn(`client.start(metadata);`) + .printLn(`return {`) + .indent().printLn(`on: function (type, handler) {`) + .indent().printLn(`listeners[type].push(handler);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`write: function (requestMessage) {`) + .indent().printLn(`client.send(requestMessage);`) + .printLn(`return this;`) + .dedent().printLn(`},`) + .printLn(`end: function () {`) + .indent().printLn(`client.finishSend();`) + .dedent().printLn(`},`) + .printLn(`cancel: function () {`) + .indent().printLn(`listeners = null;`) + .printLn(`client.close();`) + .dedent().printLn(`}`) + .dedent().printLn(`};`) + .dedent().printLn(`};`); } function printServiceStubTypes(methodPrinter: Printer, service: RPCDescriptor) { @@ -425,10 +518,10 @@ function printServerStreamStubMethodTypes(printer: CodePrinter, method: RPCMetho printer.printLn(`${method.nameAsCamelCase}(requestMessage: ${method.requestType}, metadata?: grpc.Metadata): ResponseStream<${method.responseType}>;`); } -function printBidirectionalStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { - printer.printLn(`${method.nameAsCamelCase}(): void;`); +function printClientStreamStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer.printLn(`${method.nameAsCamelCase}(metadata?: grpc.Metadata): RequestStream<${method.responseType}>;`); } -function printClientStreamStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { - printer.printLn(`${method.nameAsCamelCase}(): void;`); +function printBidirectionalStubMethodTypes(printer: CodePrinter, method: RPCMethodDescriptor) { + printer.printLn(`${method.nameAsCamelCase}(metadata?: grpc.Metadata): BidirectionalStream<${method.responseType}>;`); } diff --git a/test/helpers/fakeGrpcTransport.ts b/test/helpers/fakeGrpcTransport.ts index 0092030d..851b9fcf 100644 --- a/test/helpers/fakeGrpcTransport.ts +++ b/test/helpers/fakeGrpcTransport.ts @@ -11,6 +11,14 @@ function frameResponse(request: Message): Uint8Array { return new Uint8Array(frame); } +export function frameRequest(request: Message): ArrayBufferView { + const bytes = request.serializeBinary(); + const frame = new ArrayBuffer(bytes.byteLength + 5); + new DataView(frame, 1, 4).setUint32(0, bytes.length, false /* big endian */); + new Uint8Array(frame, 5).set(bytes); + return new Uint8Array(frame); +} + function frameTrailers(trailers: grpc.Metadata): Uint8Array { let asString = ""; trailers.forEach((key: string, values: string[]) => { diff --git a/test/integration/service/grpcweb.ts b/test/integration/service/grpcweb.ts index e1f56865..235474c9 100644 --- a/test/integration/service/grpcweb.ts +++ b/test/integration/service/grpcweb.ts @@ -4,34 +4,55 @@ import { assert } from "chai"; import { grpc } from "grpc-web-client"; import { createContext, runInContext } from "vm"; -import { StubTransportBuilder } from "../../helpers/fakeGrpcTransport"; +import { frameRequest, StubTransportBuilder } from "../../helpers/fakeGrpcTransport"; import { ExternalChildMessage } from "../../../examples/generated/proto/othercom/external_child_message_pb"; import { SimpleService, SimpleServiceClient } from "../../../examples/generated/proto/examplecom/simple_service_pb_service"; import { StreamRequest, UnaryRequest } from "../../../examples/generated/proto/examplecom/simple_service_pb"; +import { Empty } from "google-protobuf/google/protobuf/empty_pb"; describe("service/grpc-web", () => { - it("should generate a service definition", () => { - assert.strictEqual(SimpleService.serviceName, "examplecom.SimpleService"); - - assert.strictEqual(SimpleService.DoUnary.methodName, "DoUnary"); - assert.strictEqual(SimpleService.DoUnary.service, SimpleService); - assert.strictEqual(SimpleService.DoUnary.requestStream, false); - assert.strictEqual(SimpleService.DoUnary.responseStream, false); - assert.strictEqual(SimpleService.DoUnary.requestType, UnaryRequest); - assert.strictEqual(SimpleService.DoUnary.responseType, ExternalChildMessage); - - assert.strictEqual(SimpleService.DoStream.methodName, "DoStream"); - assert.strictEqual(SimpleService.DoStream.service, SimpleService); - assert.strictEqual(SimpleService.DoStream.requestStream, false); - assert.strictEqual(SimpleService.DoStream.responseStream, true); - assert.strictEqual(SimpleService.DoStream.requestType, StreamRequest); - assert.strictEqual(SimpleService.DoStream.responseType, ExternalChildMessage); - }); + describe("generated service definitions", () => { - it("should generate service definition files for protos that have no service definitions", () => { - assert.isTrue(existsSync(resolve(__dirname, "../../../examples/generated/proto/examplecom/empty_message_no_service_pb_service.d.ts"))); - assert.isTrue(existsSync(resolve(__dirname, "../../../examples/generated/proto/examplecom/empty_message_no_service_pb_service.js"))); + it("should be exported", () => { + assert.strictEqual(SimpleService.serviceName, "examplecom.SimpleService"); + }); + + it("should contain the expected DoUnary method", () => { + assert.strictEqual(SimpleService.DoUnary.methodName, "DoUnary"); + assert.strictEqual(SimpleService.DoUnary.service, SimpleService); + assert.strictEqual(SimpleService.DoUnary.requestStream, false); + assert.strictEqual(SimpleService.DoUnary.responseStream, false); + assert.strictEqual(SimpleService.DoUnary.requestType, UnaryRequest); + assert.strictEqual(SimpleService.DoUnary.responseType, ExternalChildMessage); + }); + + it("should contain the expected DoServerStream method", () => { + assert.strictEqual(SimpleService.DoServerStream.methodName, "DoServerStream"); + assert.strictEqual(SimpleService.DoServerStream.service, SimpleService); + assert.strictEqual(SimpleService.DoServerStream.requestStream, false); + assert.strictEqual(SimpleService.DoServerStream.responseStream, true); + assert.strictEqual(SimpleService.DoServerStream.requestType, StreamRequest); + assert.strictEqual(SimpleService.DoServerStream.responseType, ExternalChildMessage); + }); + + it("should contain the expected DoClientStream method", () => { + assert.strictEqual(SimpleService.DoClientStream.methodName, "DoClientStream"); + assert.strictEqual(SimpleService.DoClientStream.service, SimpleService); + assert.strictEqual(SimpleService.DoClientStream.requestStream, true); + assert.strictEqual(SimpleService.DoClientStream.responseStream, false); + assert.strictEqual(SimpleService.DoClientStream.requestType, StreamRequest); + assert.strictEqual(SimpleService.DoClientStream.responseType, Empty); + }); + + it("should contain the expected DoClientStream method", () => { + assert.strictEqual(SimpleService.DoBidiStream.methodName, "DoBidiStream"); + assert.strictEqual(SimpleService.DoBidiStream.service, SimpleService); + assert.strictEqual(SimpleService.DoBidiStream.requestStream, true); + assert.strictEqual(SimpleService.DoBidiStream.responseStream, true); + assert.strictEqual(SimpleService.DoBidiStream.requestType, StreamRequest); + assert.strictEqual(SimpleService.DoBidiStream.responseType, ExternalChildMessage); + }); }); it("should not output imports for namespaces that are not used in the service definition", () => { @@ -42,6 +63,11 @@ describe("service/grpc-web", () => { assert.include(generatedProto, "google-protobuf/google/protobuf/timestamp_pb"); }); + it("should generate service definition files for protos that have no service definitions", () => { + assert.isTrue(existsSync(resolve(__dirname, "../../../examples/generated/proto/examplecom/empty_message_no_service_pb_service.d.ts"))); + assert.isTrue(existsSync(resolve(__dirname, "../../../examples/generated/proto/examplecom/empty_message_no_service_pb_service.js"))); + }); + it("should generate valid javascript sources", () => { const generatedService = readFileSync(resolve(__dirname, "../../../examples/generated/proto/examplecom/simple_service_pb_service.js"), "utf8"); @@ -72,14 +98,6 @@ describe("service/grpc-web", () => { assert.strictEqual(typeof sandbox.exports.SimpleService, "function"); assert.strictEqual(sandbox.exports.SimpleService.serviceName, "examplecom.SimpleService"); - assert.strictEqual(typeof sandbox.exports.SimpleService.DoStream, "object"); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.methodName, "DoStream"); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.service, sandbox.exports.SimpleService); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.requestStream, false); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.responseStream, true); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.requestType, StreamRequest); - assert.strictEqual(sandbox.exports.SimpleService.DoStream.responseType, ExternalChildMessage); - assert.strictEqual(typeof sandbox.exports.SimpleService.DoUnary, "object"); assert.strictEqual(sandbox.exports.SimpleService.DoUnary.methodName, "DoUnary"); assert.strictEqual(sandbox.exports.SimpleService.DoUnary.service, sandbox.exports.SimpleService); @@ -87,6 +105,15 @@ describe("service/grpc-web", () => { assert.strictEqual(sandbox.exports.SimpleService.DoUnary.responseStream, false); assert.strictEqual(sandbox.exports.SimpleService.DoUnary.requestType, UnaryRequest); assert.strictEqual(sandbox.exports.SimpleService.DoUnary.responseType, ExternalChildMessage); + + assert.strictEqual(typeof sandbox.exports.SimpleService.DoServerStream, "object"); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.methodName, "DoServerStream"); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.service, sandbox.exports.SimpleService); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.requestStream, false); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.responseStream, true); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.requestType, StreamRequest); + assert.strictEqual(sandbox.exports.SimpleService.DoServerStream.responseType, ExternalChildMessage); + }); describe("grpc-web service stubs", () => { @@ -111,7 +138,7 @@ describe("service/grpc-web", () => { assert.equal(client.serviceHost, "http://localhost:1", "Service host should be stored from constructor"); assert.typeOf(client.doUnary, "function", "Service should have doUnary method"); - assert.typeOf(client.doStream, "function", "Service should have doStream method"); + assert.typeOf(client.doServerStream, "function", "Service should have doServerStream method"); }); describe("unary", () => { @@ -160,6 +187,23 @@ describe("service/grpc-web", () => { }); }); + it("should send the supplied payload to the server", (done) => { + let sentMessageBytes: ArrayBufferView = new Uint8Array(0); + + const payload = new UnaryRequest(); + payload.setSomeInt64(42); + + makeClient(new StubTransportBuilder().withMessageListener(v => sentMessageBytes = v)) + .doUnary( + payload, + (err) => { + assert.ok(err === null, "should not yield an error"); + assert.deepEqual(sentMessageBytes, frameRequest(payload), "expected request message supplied to transport"); + done(); + } + ); + }); + it("should allow the caller to supply Metadata", (done) => { let sentHeaders: grpc.Metadata; @@ -175,14 +219,14 @@ describe("service/grpc-web", () => { }); }); - describe("streaming", () => { + describe("server streaming", () => { it("should route the request to the expected endpoint", (done) => { let targetUrl = ""; makeClient(new StubTransportBuilder().withRequestListener(options => targetUrl = options.url)) - .doStream(new StreamRequest()) + .doServerStream(new StreamRequest()) .on("end", () => { - assert.equal(targetUrl, "http://localhost:1/examplecom.SimpleService/DoStream"); + assert.equal(targetUrl, "http://localhost:1/examplecom.SimpleService/DoServerStream"); done(); }); }); @@ -192,7 +236,7 @@ describe("service/grpc-web", () => { let onEndInvoked = false; makeClient(new StubTransportBuilder().withMessages([payload])) - .doStream(new StreamRequest()) + .doServerStream(new StreamRequest()) .on("end", () => { onEndInvoked = true; }) .on("status", () => { assert.ok(onEndInvoked, "onEnd callback should be invoked before onStatus"); @@ -200,9 +244,9 @@ describe("service/grpc-web", () => { }); }); - it("should handle an error returned ahead of any data by the unary endpoint", (done) => { + it("should handle an error returned ahead of any data by the endpoint", (done) => { makeClient(new StubTransportBuilder().withPreMessagesError(grpc.Code.Internal, "some error")) - .doStream(new StreamRequest()) + .doServerStream(new StreamRequest()) .on("status", (status) => { assert.equal(status.code, grpc.Code.Internal, "expected grpc status code returned"); assert.equal(status.details, "some error", "expected grpc error details returned"); @@ -210,7 +254,7 @@ describe("service/grpc-web", () => { }); }); - it("should handle an error returned mid-stream by the unary endpoint", (done) => { + it("should handle an error returned mid-stream by the endpoint", (done) => { const [payload] = makePayloads("some value"); let actualData: ExternalChildMessage[] = []; @@ -218,7 +262,7 @@ describe("service/grpc-web", () => { .withMessages([payload]) .withPreTrailersError(grpc.Code.Internal, "some error") ) - .doStream(new StreamRequest()) + .doServerStream(new StreamRequest()) .on("data", payload => actualData.push(payload)) .on("status", status => { assert.equal(status.code, grpc.Code.Internal, "expected grpc status code returned"); @@ -234,7 +278,7 @@ describe("service/grpc-web", () => { let actualData: ExternalChildMessage[] = []; makeClient(new StubTransportBuilder().withMessages([payload1, payload2])) - .doStream(new StreamRequest()) + .doServerStream(new StreamRequest()) .on("data", payload => actualData.push(payload)) .on("status", status => { assert.equal(status.code, grpc.Code.OK, "status code is ok"); @@ -249,7 +293,7 @@ describe("service/grpc-web", () => { let sentHeaders: grpc.Metadata; makeClient(new StubTransportBuilder().withHeadersListener(headers => sentHeaders = headers)) - .doStream(new StreamRequest(), new grpc.Metadata({ "foo": "bar" })) + .doServerStream(new StreamRequest(), new grpc.Metadata({ "foo": "bar" })) .on("end", () => { assert.deepEqual(sentHeaders.get("foo"), ["bar"]); done(); @@ -257,8 +301,11 @@ describe("service/grpc-web", () => { }); it("should allow the caller to cancel the request", (done) => { + let cancelInvoked = false; + const transport = new StubTransportBuilder() .withMessages(makePayloads("foo", "bar")) + .withCancelListener(() => cancelInvoked = true) .withManualTrigger() .build(); @@ -267,7 +314,7 @@ describe("service/grpc-web", () => { let onEndFired = false; let onStatusFired = false; - const handle = client.doStream(new StreamRequest()) + const handle = client.doServerStream(new StreamRequest()) .on("data", () => messageCount++) .on("end", () => onEndFired = true) .on("status", () => onStatusFired = true); @@ -278,6 +325,7 @@ describe("service/grpc-web", () => { transport.sendTrailers(); setTimeout(() => { + assert.equal(cancelInvoked, true, "the Transport should have been cancelled by the client"); assert.equal(messageCount, 0, "invocation cancelled before any messages were sent"); assert.equal(onEndFired, false, "'end' should not have fired when the invocation is cancelled"); assert.equal(onStatusFired, false, "'status' should not have fired when the invocation is cancelled"); @@ -286,6 +334,262 @@ describe("service/grpc-web", () => { }); }); + describe("client streaming", () => { + const [ payload ] = makePayloads("some value"); + + it("should route the request to the expected endpoint", (done) => { + let targetUrl = ""; + + makeClient(new StubTransportBuilder().withRequestListener(options => targetUrl = options.url)) + .doClientStream() + .on("end", () => { + assert.equal(targetUrl, "http://localhost:1/examplecom.SimpleService/DoClientStream"); + done(); + }) + .write(payload) + .end(); + }); + + it("should close the connection when end() is invoked", (done) => { + let finishSendInvoked = false; + makeClient(new StubTransportBuilder().withFinishSendListener(() => finishSendInvoked = true)) + .doClientStream() + .on("end", () => { + assert.ok(finishSendInvoked); + done(); + }) + .write(payload) + .end(); + }); + + it("should invoke onEnd before onStatus", (done) => { + let onEndInvoked = false; + + makeClient(new StubTransportBuilder()) + .doClientStream() + .on("end", () => { onEndInvoked = true; }) + .on("status", () => { + assert.ok(onEndInvoked, "onEnd callback should be invoked before onStatus"); + done(); + }) + .write(payload) + .end(); + }); + + it("should handle an error returned ahead of any data by the server", (done) => { + makeClient(new StubTransportBuilder().withPreMessagesError(grpc.Code.Internal, "some error")) + .doClientStream() + .on("status", (status) => { + assert.equal(status.code, grpc.Code.Internal, "expected grpc status code returned"); + assert.equal(status.details, "some error", "expected grpc error details returned"); + done(); + }) + .write(payload) + .end(); + }); + + it("should allow the caller to supply multiple messages", (done) => { + const [ reqMsgOne, reqMsgTwo ] = makePayloads("one", "two"); + const sentMessageBytes: ArrayBufferView[] = []; + + makeClient(new StubTransportBuilder().withMessageListener(v => { sentMessageBytes.push(v); })) + .doClientStream() + .on("end", () => { + assert.equal(sentMessageBytes.length, 2, "Two messages are sent"); + assert.deepEqual(sentMessageBytes[0], frameRequest(reqMsgOne)); + assert.deepEqual(sentMessageBytes[1], frameRequest(reqMsgTwo)); + done(); + }) + .write(reqMsgOne) + .write(reqMsgTwo) + .end(); + }); + + it("should allow the caller to supply Metadata", (done) => { + let sentHeaders: grpc.Metadata; + + makeClient(new StubTransportBuilder().withHeadersListener(headers => sentHeaders = headers)) + .doClientStream(new grpc.Metadata({ "foo": "bar" })) + .on("end", () => { + assert.deepEqual(sentHeaders.get("foo"), ["bar"]); + done(); + }) + .write(payload) + .end(); + }); + + it("should allow the caller to cancel the request", (done) => { + let cancelInvoked = true; + + const transport = new StubTransportBuilder() + .withCancelListener(() => cancelInvoked = true) + .withManualTrigger() + .build(); + + const client = new SimpleServiceClient("http://localhost:1", { transport }); + let onEndFired = false; + let onStatusFired = false; + + const handle = client.doClientStream() + .on("end", () => onEndFired = true) + .on("status", () => onStatusFired = true) + .write(payload); + + transport.sendHeaders(); + handle.cancel(); + transport.sendTrailers(); + + setTimeout(() => { + assert.equal(cancelInvoked, true, "the Transport should have been cancelled by the client"); + assert.equal(onEndFired, false, "'end' should not have fired when the invocation is cancelled"); + assert.equal(onStatusFired, false, "'status' should not have fired when the invocation is cancelled"); + done(); + }, 20); + }); + }); + + describe("bidirectional streaming", () => { + const [ payload ] = makePayloads("some value"); + + it("should route the request to the expected endpoint", (done) => { + let targetUrl = ""; + + makeClient(new StubTransportBuilder().withRequestListener(options => targetUrl = options.url)) + .doBidiStream() + .on("end", () => { + assert.equal(targetUrl, "http://localhost:1/examplecom.SimpleService/DoBidiStream"); + done(); + }) + .end(); + }); + + it("should invoke onEnd before onStatus if the client ends the stream", (done) => { + let onEndInvoked = false; + + makeClient(new StubTransportBuilder()) + .doBidiStream() + .on("end", () => { onEndInvoked = true; }) + .on("status", () => { + assert.ok(onEndInvoked, "onEnd callback should be invoked before onStatus"); + done(); + }) + .write(payload) + .end(); + }); + + it("should close the connection when end() is invoked", (done) => { + let finishSendInvoked = false; + makeClient(new StubTransportBuilder().withFinishSendListener(() => finishSendInvoked = true)) + .doBidiStream() + .on("end", () => { + assert.ok(finishSendInvoked); + done(); + }) + .write(payload) + .end(); + }); + + it("should invoke onEnd before onStatus if the server ends the stream", (done) => { + let onEndInvoked = false; + + makeClient(new StubTransportBuilder().withMessages([ payload ])) + .doBidiStream() + .on("end", () => { onEndInvoked = true; }) + .on("status", () => { + assert.ok(onEndInvoked, "onEnd callback should be invoked before onStatus"); + done(); + }); + }); + + it("should handle an error returned ahead of any data by the server", (done) => { + makeClient(new StubTransportBuilder().withPreMessagesError(grpc.Code.Internal, "some error")) + .doClientStream() + .on("status", (status) => { + assert.equal(status.code, grpc.Code.Internal, "expected grpc status code returned"); + assert.equal(status.details, "some error", "expected grpc error details returned"); + done(); + }) + .write(payload); + }); + + it("should handle an error returned mid-stream by the server", (done) => { + let actualData: ExternalChildMessage[] = []; + + makeClient(new StubTransportBuilder() + .withMessages([payload]) + .withPreTrailersError(grpc.Code.Internal, "some error") + ) + .doBidiStream() + .on("data", payload => actualData.push(payload)) + .on("status", status => { + assert.equal(status.code, grpc.Code.Internal, "expected grpc status code returned"); + assert.equal(status.details, "some error", "expected grpc error details returned"); + assert.equal(actualData.length, 1, "messages sent by the server, ahead of any error are exposed"); + assert.equal(actualData[0].getMyString(), "some value", "payload is well formed"); + done(); + }); + }); + + it("should allow the caller to supply multiple messages", (done) => { + const [ reqMsgOne, reqMsgTwo ] = makePayloads("one", "two"); + const sentMessageBytes: ArrayBufferView[] = []; + + makeClient(new StubTransportBuilder().withMessageListener(v => { sentMessageBytes.push(v); })) + .doBidiStream() + .on("end", () => { + assert.equal(sentMessageBytes.length, 2, "Two messages are sent"); + assert.deepEqual(sentMessageBytes[0], frameRequest(reqMsgOne)); + assert.deepEqual(sentMessageBytes[1], frameRequest(reqMsgTwo)); + done(); + }) + .write(reqMsgOne) + .write(reqMsgTwo) + .end(); + }); + + it("should allow the caller to supply Metadata", (done) => { + let sentHeaders: grpc.Metadata; + + makeClient(new StubTransportBuilder().withHeadersListener(headers => sentHeaders = headers)) + .doBidiStream(new grpc.Metadata({ "foo": "bar" })) + .on("end", () => { + assert.deepEqual(sentHeaders.get("foo"), ["bar"]); + done(); + }) + .write(payload) + .end(); + }); + + it("should allow the caller to cancel the request", (done) => { + let cancelInvoked = false; + + const transport = new StubTransportBuilder() + .withManualTrigger() + .withCancelListener(() => cancelInvoked = true) + .build(); + + const client = new SimpleServiceClient("http://localhost:1", { transport }); + let onEndFired = false; + let onStatusFired = false; + + const handle = client.doBidiStream() + .on("end", () => onEndFired = true) + .on("status", () => onStatusFired = true) + .write(payload); + + transport.sendHeaders(); + handle.cancel(); + transport.sendTrailers(); + + setTimeout(() => { + assert.equal(cancelInvoked, true, "the Transport should have been cancelled by the client"); + assert.equal(onEndFired, false, "'end' should not have fired when the invocation is cancelled"); + assert.equal(onStatusFired, false, "'status' should not have fired when the invocation is cancelled"); + done(); + }, 20); + }); + }); + describe("methods named using reserved words", () => { it("should route the request to the expected endpoint", () => { const client = new SimpleServiceClient("http://localhost:1"); diff --git a/test/mocha-run-suite.sh b/test/mocha-run-suite.sh index e8e35a67..63be4051 100755 --- a/test/mocha-run-suite.sh +++ b/test/mocha-run-suite.sh @@ -12,11 +12,16 @@ if [[ -z "${TEST_SUITE}" ]]; then exit 1 fi +if [[ "x${MOCHA_DEBUG}" != "x" ]]; then + MOCHA_DEBUG="--inspect-brk" +fi + mocha \ --reporter mocha-spec-json-output-reporter \ - --reporter-options fileName=./test/mocha-report.json \ + --reporter-options "fileName=./test/mocha-report.json" \ --require ts-node/register/type-check \ --require source-map-support/register \ + ${MOCHA_DEBUG} \ "${TEST_SUITE}" node ./test/mocha-check-report ./test/mocha-report.json